From 79e3f6d7dfe2528d38e84564fcaf181e8f1bf5a9 Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Fri, 22 Jul 2016 17:34:04 +0200 Subject: [PATCH] Allow hotplugging of USB cards, instead of just exiting when something goes away. Note that there are still limitations; e.g., you cannot start with 0 USB cards and add them later, and you cannot have more cards than your initial --num-cards parameter. This is primarily useful for if a cable falls out during broadcast. --- Makefile | 2 +- bmusb | 2 +- context.cpp | 5 +++ context.h | 1 + decklink_capture.h | 3 ++ fake_capture.h | 1 + mixer.cpp | 105 +++++++++++++++++++++++++++++++++++++++++---- mixer.h | 15 +++++-- 8 files changed, 120 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index 98558d2..1422406 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -CXX=g++ +CXX=g++ -fsanitize=address PKG_MODULES = Qt5Core Qt5Gui Qt5Widgets Qt5OpenGLExtensions Qt5OpenGL libusb-1.0 movit lua52 libmicrohttpd epoxy x264 CXXFLAGS := -O2 -march=native -g -std=gnu++11 -Wall -Wno-deprecated-declarations -Werror -fPIC $(shell pkg-config --cflags $(PKG_MODULES)) -pthread -DMOVIT_SHADER_DIR=\"$(shell pkg-config --variable=shaderdir movit)\" -Idecklink/ LDFLAGS=$(shell pkg-config --libs $(PKG_MODULES)) -lEGL -lGL -pthread -lva -lva-drm -lva-x11 -lX11 -lavformat -lavcodec -lavutil -lswscale -lavresample -lzita-resampler -lasound -ldl diff --git a/bmusb b/bmusb index 76a878c..ec5eaa5 160000 --- a/bmusb +++ b/bmusb @@ -1 +1 @@ -Subproject commit 76a878cbab84d8c3c2859b28a2363085d6a2fcb1 +Subproject commit ec5eaa5e319e91f89db15dcbccdf64a056f710ae diff --git a/context.cpp b/context.cpp index ea576cd..4736b82 100644 --- a/context.cpp +++ b/context.cpp @@ -22,6 +22,11 @@ QSurface *create_surface(const QSurfaceFormat &format) return surface; } +QSurface *create_surface_with_same_format(const QSurface *surface) +{ + return create_surface(surface->format()); +} + QOpenGLContext *create_context(const QSurface *surface) { QOpenGLContext *context = new QOpenGLContext; diff --git a/context.h b/context.h index aeda12b..9b72eaf 100644 --- a/context.h +++ b/context.h @@ -9,6 +9,7 @@ class QGLWidget; extern QGLWidget *global_share_widget; QSurface *create_surface(const QSurfaceFormat &format); +QSurface *create_surface_with_same_format(const QSurface *surface); QOpenGLContext *create_context(const QSurface *surface); bool make_current(QOpenGLContext *context, QSurface *surface); void delete_context(QOpenGLContext *context); diff --git a/decklink_capture.h b/decklink_capture.h index 59c3b02..bfb6d0e 100644 --- a/decklink_capture.h +++ b/decklink_capture.h @@ -81,6 +81,9 @@ public: void start_bm_capture() override; void stop_dequeue_thread() override; + // TODO: Can the API communicate this to us somehow, for e.g. Thunderbolt cards? + bool get_disconnected() const override { return false; } + std::map get_available_video_modes() const override { return video_modes; } void set_video_mode(uint32_t video_mode_id) override; uint32_t get_current_video_mode() const override { return current_video_mode; } diff --git a/fake_capture.h b/fake_capture.h index 583b421..c517257 100644 --- a/fake_capture.h +++ b/fake_capture.h @@ -61,6 +61,7 @@ public: void configure_card() override; void start_bm_capture() override; void stop_dequeue_thread() override; + bool get_disconnected() const override { return false; } std::map get_available_video_modes() const override; void set_video_mode(uint32_t video_mode_id) override; diff --git a/mixer.cpp b/mixer.cpp index ce3ceb4..4e3449b 100644 --- a/mixer.cpp +++ b/mixer.cpp @@ -177,7 +177,7 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards) assert(num_fake_cards <= num_cards); // Enforced in flags.cpp. for ( ; card_index < num_fake_cards; ++card_index) { - configure_card(card_index, format, new FakeCapture(card_index)); + configure_card(card_index, new FakeCapture(card_index), /*is_fake_capture=*/true); } if (global_flags.num_fake_cards > 0) { @@ -193,7 +193,7 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards) break; } - configure_card(card_index, format, new DeckLinkCapture(decklink, card_index - num_fake_cards)); + configure_card(card_index, new DeckLinkCapture(decklink, card_index - num_fake_cards), /*is_fake_capture=*/false); ++num_pci_devices; } decklink_iterator->Release(); @@ -203,12 +203,15 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards) } } for ( ; card_index < num_cards; ++card_index) { - configure_card(card_index, format, new BMUSBCapture(card_index - num_pci_devices - num_fake_cards)); + BMUSBCapture *capture = new BMUSBCapture(card_index - num_pci_devices - num_fake_cards); + capture->set_card_disconnected_callback(bind(&Mixer::bm_hotplug_remove, this, card_index)); + configure_card(card_index, capture, /*is_fake_capture=*/false); ++num_usb_devices; } if (num_usb_devices > 0) { has_bmusb_thread = true; + BMUSBCapture::set_card_connected_callback(bind(&Mixer::bm_hotplug_add, this, _1)); BMUSBCapture::start_bm_thread(); } @@ -303,17 +306,33 @@ Mixer::~Mixer() video_encoder.reset(nullptr); } -void Mixer::configure_card(unsigned card_index, const QSurfaceFormat &format, CaptureInterface *capture) +void Mixer::configure_card(unsigned card_index, CaptureInterface *capture, bool is_fake_capture) { printf("Configuring card %d...\n", card_index); CaptureCard *card = &cards[card_index]; + if (card->capture != nullptr) { + card->capture->stop_dequeue_thread(); + delete card->capture; + } card->capture = capture; + card->is_fake_capture = is_fake_capture; card->capture->set_frame_callback(bind(&Mixer::bm_frame, this, card_index, _1, _2, _3, _4, _5, _6, _7)); - card->frame_allocator.reset(new PBOFrameAllocator(8 << 20, WIDTH, HEIGHT)); // 8 MB. + if (card->frame_allocator == nullptr) { + card->frame_allocator.reset(new PBOFrameAllocator(8 << 20, WIDTH, HEIGHT)); // 8 MB. + } card->capture->set_video_frame_allocator(card->frame_allocator.get()); - card->surface = create_surface(format); - card->resampling_queue.reset(new ResamplingQueue(card_index, OUTPUT_FREQUENCY, OUTPUT_FREQUENCY, 2)); + if (card->surface == nullptr) { + card->surface = create_surface_with_same_format(mixer_surface); + } + { + unique_lock lock(cards[card_index].audio_mutex); + card->resampling_queue.reset(new ResamplingQueue(card_index, OUTPUT_FREQUENCY, OUTPUT_FREQUENCY, 2)); + } + while (!card->new_frames.empty()) card->new_frames.pop(); + card->fractional_samples = 0; + card->last_timecode = -1; + card->next_local_pts = 0; card->capture->configure_card(); } @@ -610,6 +629,17 @@ void Mixer::bm_frame(unsigned card_index, uint16_t timecode, } } +void Mixer::bm_hotplug_add(libusb_device *dev) +{ + lock_guard lock(hotplug_mutex); + hotplugged_cards.push_back(dev); +} + +void Mixer::bm_hotplug_remove(unsigned card_index) +{ + cards[card_index].new_frames_changed.notify_all(); +} + void Mixer::thread_func() { eglBindAPI(EGL_OPENGL_API); @@ -630,7 +660,6 @@ void Mixer::thread_func() bool has_new_frame[MAX_CARDS] = { false }; int num_samples[MAX_CARDS] = { 0 }; - // TODO: Add a timeout. unsigned master_card_index = theme->map_signal(master_clock_channel); assert(master_card_index < num_cards); @@ -639,6 +668,8 @@ void Mixer::thread_func() stats_dropped_frames += new_frames[master_card_index].dropped_frames; send_audio_level_callback(); + handle_hotplugged_cards(); + for (unsigned card_index = 0; card_index < num_cards; ++card_index) { if (card_index == master_card_index || !has_new_frame[card_index]) { continue; @@ -751,9 +782,19 @@ void Mixer::thread_func() void Mixer::get_one_frame_from_each_card(unsigned master_card_index, CaptureCard::NewFrame new_frames[MAX_CARDS], bool has_new_frame[MAX_CARDS], int num_samples[MAX_CARDS]) { +start: // The first card is the master timer, so wait for it to have a new frame. + // TODO: Add a timeout. unique_lock lock(bmusb_mutex); - cards[master_card_index].new_frames_changed.wait(lock, [this, master_card_index]{ return !cards[master_card_index].new_frames.empty(); }); + cards[master_card_index].new_frames_changed.wait(lock, [this, master_card_index]{ return !cards[master_card_index].new_frames.empty() || cards[master_card_index].capture->get_disconnected(); }); + + if (cards[master_card_index].new_frames.empty()) { + // We were woken up, but not due to a new frame. Deal with it + // and then restart. + assert(cards[master_card_index].capture->get_disconnected()); + handle_hotplugged_cards(); + goto start; + } for (unsigned card_index = 0; card_index < num_cards; ++card_index) { CaptureCard *card = &cards[card_index]; @@ -788,6 +829,52 @@ void Mixer::get_one_frame_from_each_card(unsigned master_card_index, CaptureCard } } +void Mixer::handle_hotplugged_cards() +{ + // Check for cards that have been disconnected since last frame. + for (unsigned card_index = 0; card_index < num_cards; ++card_index) { + CaptureCard *card = &cards[card_index]; + if (card->capture->get_disconnected()) { + fprintf(stderr, "Card %u went away, replacing with a fake card.\n", card_index); + configure_card(card_index, new FakeCapture(card_index), /*is_fake_capture=*/true); + card->queue_length_policy.reset(card_index); + card->capture->start_bm_capture(); + } + } + + // Check for cards that have been connected since last frame. + vector hotplugged_cards_copy; + { + lock_guard lock(hotplug_mutex); + swap(hotplugged_cards, hotplugged_cards_copy); + } + for (libusb_device *new_dev : hotplugged_cards_copy) { + // Look for a fake capture card where we can stick this in. + int free_card_index = -1; + for (unsigned card_index = 0; card_index < num_cards; ++card_index) { + if (cards[card_index].is_fake_capture) { + free_card_index = int(card_index); + break; + } + } + + if (free_card_index == -1) { + fprintf(stderr, "New card plugged in, but no free slots -- ignoring.\n"); + libusb_unref_device(new_dev); + } else { + // BMUSBCapture takes ownership. + fprintf(stderr, "New card plugged in, choosing slot %d.\n", free_card_index); + CaptureCard *card = &cards[free_card_index]; + BMUSBCapture *capture = new BMUSBCapture(free_card_index, new_dev); + configure_card(free_card_index, capture, /*is_fake_capture=*/false); + card->queue_length_policy.reset(free_card_index); + capture->set_card_disconnected_callback(bind(&Mixer::bm_hotplug_remove, this, free_card_index)); + capture->start_bm_capture(); + } + } +} + + void Mixer::schedule_audio_resampling_tasks(unsigned dropped_frames, int num_samples_per_frame, int length_per_frame) { // Resample the audio as needed, including from previously dropped frames. diff --git a/mixer.h b/mixer.h index 0214063..1d6d030 100644 --- a/mixer.h +++ b/mixer.h @@ -388,12 +388,15 @@ public: } private: - void configure_card(unsigned card_index, const QSurfaceFormat &format, CaptureInterface *capture); + void configure_card(unsigned card_index, CaptureInterface *capture, bool is_fake_capture); void bm_frame(unsigned card_index, uint16_t timecode, FrameAllocator::Frame video_frame, size_t video_offset, VideoFormat video_format, FrameAllocator::Frame audio_frame, size_t audio_offset, AudioFormat audio_format); + void bm_hotplug_add(libusb_device *dev); + void bm_hotplug_remove(unsigned card_index); void place_rectangle(movit::Effect *resample_effect, movit::Effect *padding_effect, float x0, float y0, float x1, float y1); void thread_func(); + void handle_hotplugged_cards(); void schedule_audio_resampling_tasks(unsigned dropped_frames, int num_samples_per_frame, int length_per_frame); void render_one_frame(int64_t duration); void send_audio_level_callback(); @@ -425,11 +428,12 @@ private: std::mutex bmusb_mutex; bool has_bmusb_thread = false; struct CaptureCard { - CaptureInterface *capture; + CaptureInterface *capture = nullptr; + bool is_fake_capture; std::unique_ptr frame_allocator; // Stuff for the OpenGL context (for texture uploading). - QSurface *surface; + QSurface *surface = nullptr; struct NewFrame { RefCountedFrame frame; @@ -459,6 +463,11 @@ private: InputState input_state; + // Cards we have been noticed about being hotplugged, but haven't tried adding yet. + // Protected by its own mutex. + std::mutex hotplug_mutex; + std::vector hotplugged_cards; + class OutputChannel { public: ~OutputChannel(); -- 2.39.2