]> git.sesse.net Git - nageru/blobdiff - mixer.cpp
Upgrade to newer bmusb, with namespacing.
[nageru] / mixer.cpp
index efd4952b7e565f379128673e11f2a7089d46c644..6bc878074f2fa77e9ef9ef6da6157d54dc202eaa 100644 (file)
--- a/mixer.cpp
+++ b/mixer.cpp
 #include <utility>
 #include <vector>
 #include <arpa/inet.h>
+#include <sys/time.h>
+#include <sys/resource.h>
 
 #include "bmusb/bmusb.h"
 #include "context.h"
 #include "decklink_capture.h"
 #include "defs.h"
+#include "fake_capture.h"
 #include "flags.h"
-#include "quicksync_encoder.h"
+#include "video_encoder.h"
 #include "pbo_frame_allocator.h"
 #include "ref_counted_gl_sync.h"
 #include "timebase.h"
@@ -43,8 +46,10 @@ class QOpenGLContext;
 using namespace movit;
 using namespace std;
 using namespace std::placeholders;
+using namespace bmusb;
 
 Mixer *global_mixer = nullptr;
+bool uses_mlock = false;
 
 namespace {
 
@@ -92,23 +97,6 @@ void insert_new_frame(RefCountedFrame frame, unsigned field_num, bool interlaced
        }
 }
 
-string generate_local_dump_filename(int frame)
-{
-       time_t now = time(NULL);
-       tm now_tm;
-       localtime_r(&now, &now_tm);
-
-       char timestamp[256];
-       strftime(timestamp, sizeof(timestamp), "%F-%T%z", &now_tm);
-
-       // Use the frame number to disambiguate between two cuts starting
-       // on the same second.
-       char filename[256];
-       snprintf(filename, sizeof(filename), "%s%s-f%02d%s",
-               LOCAL_DUMP_PREFIX, timestamp, frame % 100, LOCAL_DUMP_SUFFIX);
-       return filename;
-}
-
 }  // namespace
 
 void QueueLengthPolicy::update_policy(int queue_length)
@@ -127,9 +115,9 @@ void QueueLengthPolicy::update_policy(int queue_length)
                if (queue_length >= int(safe_queue_length)) {
                        been_at_safe_point_since_last_starvation = true;
                }
-               if (++frames_with_at_least_one >= 50 && safe_queue_length > 0) {
+               if (++frames_with_at_least_one >= 1000 && safe_queue_length > 0) {
                        --safe_queue_length;
-                       fprintf(stderr, "Card %u: Spare frames for more than 50 frames, reducing safe limit to %u frames\n",
+                       fprintf(stderr, "Card %u: Spare frames for more than 1000 frames, reducing safe limit to %u frames\n",
                                card_index, safe_queue_length);
                        frames_with_at_least_one = 0;
                }
@@ -159,6 +147,7 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards)
        theme.reset(new Theme(global_flags.theme_filename.c_str(), resource_pool.get(), num_cards));
        for (unsigned i = 0; i < NUM_OUTPUTS; ++i) {
                output_channel[i].parent = this;
+               output_channel[i].channel = i;
        }
 
        ImageFormat inout_format;
@@ -174,38 +163,56 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards)
        display_chain->set_dither_bits(0);  // Don't bother.
        display_chain->finalize();
 
-       quicksync_encoder.reset(new QuickSyncEncoder(h264_encoder_surface, global_flags.va_display, WIDTH, HEIGHT, &httpd));
-       quicksync_encoder->open_output_file(generate_local_dump_filename(/*frame=*/0).c_str());
+       video_encoder.reset(new VideoEncoder(resource_pool.get(), h264_encoder_surface, global_flags.va_display, WIDTH, HEIGHT, &httpd));
 
-       // Start listening for clients only once H264Encoder has written its header, if any.
+       // Start listening for clients only once VideoEncoder has written its header, if any.
        httpd.start(9095);
 
-       // First try initializing the PCI devices, then USB, until we have the desired number of cards.
+       // First try initializing the fake devices, then PCI devices, then USB,
+       // until we have the desired number of cards.
        unsigned num_pci_devices = 0, num_usb_devices = 0;
        unsigned card_index = 0;
 
-       IDeckLinkIterator *decklink_iterator = CreateDeckLinkIteratorInstance();
-       if (decklink_iterator != nullptr) {
-               for ( ; card_index < num_cards; ++card_index) {
-                       IDeckLink *decklink;
-                       if (decklink_iterator->Next(&decklink) != S_OK) {
-                               break;
-                       }
+       assert(global_flags.num_fake_cards >= 0);  // Enforced in flags.cpp.
+       unsigned num_fake_cards = global_flags.num_fake_cards;
+
+       assert(num_fake_cards <= num_cards);  // Enforced in flags.cpp.
+       for ( ; card_index < num_fake_cards; ++card_index) {
+               configure_card(card_index, new FakeCapture(card_index), /*is_fake_capture=*/true);
+       }
+
+       if (global_flags.num_fake_cards > 0) {
+               fprintf(stderr, "Initialized %d fake cards.\n", global_flags.num_fake_cards);
+       }
 
-                       configure_card(card_index, format, new DeckLinkCapture(decklink, card_index));
-                       ++num_pci_devices;
+       if (card_index < num_cards) {
+               IDeckLinkIterator *decklink_iterator = CreateDeckLinkIteratorInstance();
+               if (decklink_iterator != nullptr) {
+                       for ( ; card_index < num_cards; ++card_index) {
+                               IDeckLink *decklink;
+                               if (decklink_iterator->Next(&decklink) != S_OK) {
+                                       break;
+                               }
+
+                               configure_card(card_index, new DeckLinkCapture(decklink, card_index - num_fake_cards), /*is_fake_capture=*/false);
+                               ++num_pci_devices;
+                       }
+                       decklink_iterator->Release();
+                       fprintf(stderr, "Found %d DeckLink PCI card(s).\n", num_pci_devices);
+               } else {
+                       fprintf(stderr, "DeckLink drivers not found. Probing for USB cards only.\n");
                }
-               decklink_iterator->Release();
-               fprintf(stderr, "Found %d DeckLink PCI card(s).\n", num_pci_devices);
-       } else {
-               fprintf(stderr, "DeckLink drivers not found. Probing for USB cards only.\n");
        }
        for ( ; card_index < num_cards; ++card_index) {
-               configure_card(card_index, format, new BMUSBCapture(card_index - num_pci_devices));
+               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();
        }
 
@@ -262,26 +269,29 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards)
 
        locut.init(FILTER_HPF, 2);
 
-       // If --flat-audio is given, turn off everything that messes with the sound,
-       // except the final makeup gain.
-       if (global_flags.flat_audio) {
-               set_locut_enabled(false);
-               set_limiter_enabled(false);
-               set_compressor_enabled(false);
-       }
+       set_locut_enabled(global_flags.locut_enabled);
+       set_gain_staging_db(global_flags.initial_gain_staging_db);
+       set_gain_staging_auto(global_flags.gain_staging_auto);
+       set_compressor_enabled(global_flags.compressor_enabled);
+       set_limiter_enabled(global_flags.limiter_enabled);
+       set_final_makeup_gain_auto(global_flags.final_makeup_gain_auto);
 
        // hlen=16 is pretty low quality, but we use quite a bit of CPU otherwise,
        // and there's a limit to how important the peak meter is.
        peak_resampler.setup(OUTPUT_FREQUENCY, OUTPUT_FREQUENCY * 4, /*num_channels=*/2, /*hlen=*/16, /*frel=*/1.0);
 
-       alsa.reset(new ALSAOutput(OUTPUT_FREQUENCY, /*num_channels=*/2));
+       if (global_flags.enable_alsa_output) {
+               alsa.reset(new ALSAOutput(OUTPUT_FREQUENCY, /*num_channels=*/2));
+       }
 }
 
 Mixer::~Mixer()
 {
        resource_pool->release_glsl_program(cbcr_program_num);
        glDeleteBuffers(1, &cbcr_vbo);
-       BMUSBCapture::stop_bm_thread();
+       if (has_bmusb_thread) {
+               BMUSBCapture::stop_bm_thread();
+       }
 
        for (unsigned card_index = 0; card_index < num_cards; ++card_index) {
                {
@@ -292,32 +302,36 @@ Mixer::~Mixer()
                cards[card_index].capture->stop_dequeue_thread();
        }
 
-       quicksync_encoder.reset(nullptr);
+       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->capture->set_dequeue_thread_callbacks(
-               [card]{
-                       eglBindAPI(EGL_OPENGL_API);
-                       card->context = create_context(card->surface);
-                       if (!make_current(card->context, card->surface)) {
-                               printf("failed to create bmusb context\n");
-                               exit(1);
-                       }
-               },
-               [this]{
-                       resource_pool->clean_context();
-               });
-       card->resampling_queue.reset(new ResamplingQueue(OUTPUT_FREQUENCY, OUTPUT_FREQUENCY, 2));
+       if (card->surface == nullptr) {
+               card->surface = create_surface_with_same_format(mixer_surface);
+       }
+       {
+               unique_lock<mutex> 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();
 }
 
@@ -386,7 +400,8 @@ void Mixer::bm_frame(unsigned card_index, uint16_t timecode,
                }
        }
 
-       int64_t frame_length = int64_t(TIMEBASE * video_format.frame_rate_den) / video_format.frame_rate_nom;
+       int64_t frame_length = int64_t(TIMEBASE) * video_format.frame_rate_den / video_format.frame_rate_nom;
+       assert(frame_length > 0);
 
        size_t num_samples = (audio_frame.len > audio_offset) ? (audio_frame.len - audio_offset) / audio_format.num_channels / (audio_format.bits_per_sample / 8) : 0;
        if (num_samples > OUTPUT_FREQUENCY / 10) {
@@ -437,7 +452,7 @@ void Mixer::bm_frame(unsigned card_index, uint16_t timecode,
                if (dropped_frames > MAX_FPS * 2) {
                        fprintf(stderr, "Card %d lost more than two seconds (or time code jumping around; from 0x%04x to 0x%04x), resetting resampler\n",
                                card_index, card->last_timecode, timecode);
-                       card->resampling_queue.reset(new ResamplingQueue(OUTPUT_FREQUENCY, OUTPUT_FREQUENCY, 2));
+                       card->resampling_queue.reset(new ResamplingQueue(card_index, OUTPUT_FREQUENCY, OUTPUT_FREQUENCY, 2));
                        dropped_frames = 0;
                } else if (dropped_frames > 0) {
                        // Insert silence as needed.
@@ -509,6 +524,7 @@ void Mixer::bm_frame(unsigned card_index, uint16_t timecode,
        }
        userdata->last_interlaced = video_format.interlaced;
        userdata->last_has_signal = video_format.has_signal;
+       userdata->last_is_connected = video_format.is_connected;
        userdata->last_frame_rate_nom = video_format.frame_rate_nom;
        userdata->last_frame_rate_den = video_format.frame_rate_den;
        RefCountedFrame frame(video_frame);
@@ -519,59 +535,64 @@ void Mixer::bm_frame(unsigned card_index, uint16_t timecode,
        size_t y_offset = video_frame.size / 2 + video_offset / 2;
 
        for (unsigned field = 0; field < num_fields; ++field) {
-               unsigned field_start_line = (field == 1) ? video_format.second_field_start : video_format.extra_lines_top + field * (video_format.height + 22);
-
-               if (userdata->tex_y[field] == 0 ||
-                   userdata->tex_cbcr[field] == 0 ||
-                   video_format.width != userdata->last_width[field] ||
-                   video_format.height != userdata->last_height[field]) {
-                       // We changed resolution since last use of this texture, so we need to create
-                       // a new object. Note that this each card has its own PBOFrameAllocator,
-                       // we don't need to worry about these flip-flopping between resolutions.
+               // Put the actual texture upload in a lambda that is executed in the main thread.
+               // It is entirely possible to do this in the same thread (and it might even be
+               // faster, depending on the GPU and driver), but it appears to be trickling
+               // driver bugs very easily.
+               //
+               // Note that this means we must hold on to the actual frame data in <userdata>
+               // until the upload command is run, but we hold on to <frame> much longer than that
+               // (in fact, all the way until we no longer use the texture in rendering).
+               auto upload_func = [field, video_format, y_offset, cbcr_offset, cbcr_width, userdata]() {
+                       unsigned field_start_line = (field == 1) ? video_format.second_field_start : video_format.extra_lines_top + field * (video_format.height + 22);
+
+                       if (userdata->tex_y[field] == 0 ||
+                           userdata->tex_cbcr[field] == 0 ||
+                           video_format.width != userdata->last_width[field] ||
+                           video_format.height != userdata->last_height[field]) {
+                               // We changed resolution since last use of this texture, so we need to create
+                               // a new object. Note that this each card has its own PBOFrameAllocator,
+                               // we don't need to worry about these flip-flopping between resolutions.
+                               glBindTexture(GL_TEXTURE_2D, userdata->tex_cbcr[field]);
+                               check_error();
+                               glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, cbcr_width, video_format.height, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr);
+                               check_error();
+                               glBindTexture(GL_TEXTURE_2D, userdata->tex_y[field]);
+                               check_error();
+                               glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, video_format.width, video_format.height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
+                               check_error();
+                               userdata->last_width[field] = video_format.width;
+                               userdata->last_height[field] = video_format.height;
+                       }
+
+                       GLuint pbo = userdata->pbo;
+                       check_error();
+                       glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
+                       check_error();
+
+                       size_t field_y_start = y_offset + video_format.width * field_start_line;
+                       size_t field_cbcr_start = cbcr_offset + cbcr_width * field_start_line * sizeof(uint16_t);
+
+                       if (global_flags.flush_pbos) {
+                               glFlushMappedBufferRange(GL_PIXEL_UNPACK_BUFFER, field_y_start, video_format.width * video_format.height);
+                               check_error();
+                               glFlushMappedBufferRange(GL_PIXEL_UNPACK_BUFFER, field_cbcr_start, cbcr_width * video_format.height * sizeof(uint16_t));
+                               check_error();
+                       }
+
                        glBindTexture(GL_TEXTURE_2D, userdata->tex_cbcr[field]);
                        check_error();
-                       glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, cbcr_width, video_format.height, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr);
+                       glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, cbcr_width, video_format.height, GL_RG, GL_UNSIGNED_BYTE, BUFFER_OFFSET(field_cbcr_start));
                        check_error();
                        glBindTexture(GL_TEXTURE_2D, userdata->tex_y[field]);
                        check_error();
-                       glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, video_format.width, video_format.height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
+                       glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, video_format.width, video_format.height, GL_RED, GL_UNSIGNED_BYTE, BUFFER_OFFSET(field_y_start));
                        check_error();
-                       userdata->last_width[field] = video_format.width;
-                       userdata->last_height[field] = video_format.height;
-               }
-
-               GLuint pbo = userdata->pbo;
-               check_error();
-               glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
-               check_error();
-
-               size_t field_y_start = y_offset + video_format.width * field_start_line;
-               size_t field_cbcr_start = cbcr_offset + cbcr_width * field_start_line * sizeof(uint16_t);
-
-               if (global_flags.flush_pbos) {
-                       glFlushMappedBufferRange(GL_PIXEL_UNPACK_BUFFER, field_y_start, video_format.width * video_format.height);
+                       glBindTexture(GL_TEXTURE_2D, 0);
                        check_error();
-                       glFlushMappedBufferRange(GL_PIXEL_UNPACK_BUFFER, field_cbcr_start, cbcr_width * video_format.height * sizeof(uint16_t));
+                       glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
                        check_error();
-               }
-
-               glBindTexture(GL_TEXTURE_2D, userdata->tex_cbcr[field]);
-               check_error();
-               glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, cbcr_width, video_format.height, GL_RG, GL_UNSIGNED_BYTE, BUFFER_OFFSET(field_cbcr_start));
-               check_error();
-               glBindTexture(GL_TEXTURE_2D, userdata->tex_y[field]);
-               check_error();
-               glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, video_format.width, video_format.height, GL_RED, GL_UNSIGNED_BYTE, BUFFER_OFFSET(field_y_start));
-               check_error();
-               glBindTexture(GL_TEXTURE_2D, 0);
-               check_error();
-               glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
-               check_error();
-               RefCountedGLsync fence(GL_SYNC_GPU_COMMANDS_COMPLETE, /*flags=*/0);
-               check_error();
-               assert(fence.get() != nullptr);
-               glFlush();  // Make sure the main thread doesn't have to wait until we push out enough frames to make a new command buffer.
-               check_error();
+               };
 
                if (field == 1) {
                        // Don't upload the second field as fast as we can; wait until
@@ -599,7 +620,7 @@ void Mixer::bm_frame(unsigned card_index, uint16_t timecode,
                        new_frame.length = frame_length;
                        new_frame.field = field;
                        new_frame.interlaced = video_format.interlaced;
-                       new_frame.ready_fence = fence;
+                       new_frame.upload_func = upload_func;
                        new_frame.dropped_frames = dropped_frames;
                        card->new_frames.push(move(new_frame));
                        card->new_frames_changed.notify_all();
@@ -607,6 +628,17 @@ void Mixer::bm_frame(unsigned card_index, uint16_t timecode,
        }
 }
 
+void Mixer::bm_hotplug_add(libusb_device *dev)
+{
+       lock_guard<mutex> 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);
@@ -627,7 +659,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);
 
@@ -636,6 +667,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;
@@ -666,13 +699,10 @@ void Mixer::thread_func()
                        insert_new_frame(new_frame->frame, new_frame->field, new_frame->interlaced, card_index, &input_state);
                        check_error();
 
-                       // The new texture might still be uploaded,
-                       // tell the GPU to wait until it's there.
-                       if (new_frame->ready_fence) {
-                               glWaitSync(new_frame->ready_fence.get(), /*flags=*/0, GL_TIMEOUT_IGNORED);
-                               check_error();
-                               new_frame->ready_fence.reset();
-                               check_error();
+                       // The new texture might need uploading before use.
+                       if (new_frame->upload_func) {
+                               new_frame->upload_func();
+                               new_frame->upload_func = nullptr;
                        }
                }
 
@@ -685,19 +715,41 @@ void Mixer::thread_func()
                double elapsed = now.tv_sec - start.tv_sec +
                        1e-9 * (now.tv_nsec - start.tv_nsec);
                if (frame % 100 == 0) {
-                       printf("%d frames (%d dropped) in %.3f seconds = %.1f fps (%.1f ms/frame)\n",
+                       printf("%d frames (%d dropped) in %.3f seconds = %.1f fps (%.1f ms/frame)",
                                frame, stats_dropped_frames, elapsed, frame / elapsed,
                                1e3 * elapsed / frame);
                //      chain->print_phase_timing();
+
+                       // Check our memory usage, to see if we are close to our mlockall()
+                       // limit (if at all set).
+                       rusage used;
+                       if (getrusage(RUSAGE_SELF, &used) == -1) {
+                               perror("getrusage(RUSAGE_SELF)");
+                               assert(false);
+                       }
+
+                       if (uses_mlock) {
+                               rlimit limit;
+                               if (getrlimit(RLIMIT_MEMLOCK, &limit) == -1) {
+                                       perror("getrlimit(RLIMIT_MEMLOCK)");
+                                       assert(false);
+                               }
+
+                               printf(", using %ld / %ld MB lockable memory (%.1f%%)",
+                                       long(used.ru_maxrss / 1024),
+                                       long(limit.rlim_cur / 1048576),
+                                       float(100.0 * (used.ru_maxrss * 1024.0) / limit.rlim_cur));
+                       } else {
+                               printf(", using %ld MB memory (not locked)",
+                                       long(used.ru_maxrss / 1024));
+                       }
+
+                       printf("\n");
                }
 
+
                if (should_cut.exchange(false)) {  // Test and clear.
-                       string filename = generate_local_dump_filename(frame);
-                       printf("Starting new recording: %s\n", filename.c_str());
-                       quicksync_encoder->close_output_file();
-                       quicksync_encoder->shutdown();
-                       quicksync_encoder.reset(new QuickSyncEncoder(h264_encoder_surface, global_flags.va_display, WIDTH, HEIGHT, &httpd));
-                       quicksync_encoder->open_output_file(filename.c_str());
+                       video_encoder->do_cut(frame);
                }
 
 #if 0
@@ -718,9 +770,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<mutex> 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];
@@ -755,6 +817,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<libusb_device *> hotplugged_cards_copy;
+       {
+               lock_guard<mutex> 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.
@@ -784,7 +892,7 @@ void Mixer::render_one_frame(int64_t duration)
        //theme_main_chain.chain->enable_phase_timing(true);
 
        GLuint y_tex, cbcr_tex;
-       bool got_frame = quicksync_encoder->begin_frame(&y_tex, &cbcr_tex);
+       bool got_frame = video_encoder->begin_frame(&y_tex, &cbcr_tex);
        assert(got_frame);
 
        // Render main chain.
@@ -806,7 +914,7 @@ void Mixer::render_one_frame(int64_t duration)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 
        const int64_t av_delay = TIMEBASE / 10;  // Corresponds to the fixed delay in resampling_queue.h. TODO: Make less hard-coded.
-       RefCountedGLsync fence = quicksync_encoder->end_frame(pts_int + av_delay, duration, theme_main_chain.input_frames);
+       RefCountedGLsync fence = video_encoder->end_frame(pts_int + av_delay, duration, theme_main_chain.input_frames);
 
        // The live frame just shows the RGBA texture we just rendered.
        // It owns rgba_tex now.
@@ -858,7 +966,10 @@ void Mixer::audio_thread_func()
 
                {
                        unique_lock<mutex> lock(audio_mutex);
-                       audio_task_queue_changed.wait(lock, [this]{ return !audio_task_queue.empty(); });
+                       audio_task_queue_changed.wait(lock, [this]{ return should_quit || !audio_task_queue.empty(); });
+                       if (should_quit) {
+                               return;
+                       }
                        task = audio_task_queue.front();
                        audio_task_queue.pop();
                }
@@ -880,9 +991,7 @@ void Mixer::process_audio_one_frame(int64_t frame_pts_int, int num_samples)
                samples_card.resize(num_samples * 2);
                {
                        unique_lock<mutex> lock(cards[card_index].audio_mutex);
-                       if (!cards[card_index].resampling_queue->get_output_samples(double(frame_pts_int) / TIMEBASE, &samples_card[0], num_samples)) {
-                               printf("Card %d reported previous underrun.\n", card_index);
-                       }
+                       cards[card_index].resampling_queue->get_output_samples(double(frame_pts_int) / TIMEBASE, &samples_card[0], num_samples);
                }
                if (card_index == selected_audio_card) {
                        samples_out = move(samples_card);
@@ -1027,7 +1136,7 @@ void Mixer::process_audio_one_frame(int64_t frame_pts_int, int num_samples)
        }
 
        // And finally add them to the output.
-       quicksync_encoder->add_audio(frame_pts_int, move(samples_out));
+       video_encoder->add_audio(frame_pts_int, move(samples_out));
 }
 
 void Mixer::subsample_chroma(GLuint src_tex, GLuint dst_tex)
@@ -1108,6 +1217,7 @@ void Mixer::start()
 void Mixer::quit()
 {
        should_quit = true;
+       audio_task_queue_changed.notify_one();
        mixer_thread.join();
        audio_thread.join();
 }
@@ -1171,9 +1281,47 @@ void Mixer::OutputChannel::output_frame(DisplayFrame frame)
                has_ready_frame = true;
        }
 
-       if (has_new_frame_ready_callback) {
+       if (new_frame_ready_callback) {
                new_frame_ready_callback();
        }
+
+       // Reduce the number of callbacks by filtering duplicates. The reason
+       // why we bother doing this is that Qt seemingly can get into a state
+       // where its builds up an essentially unbounded queue of signals,
+       // consuming more and more memory, and there's no good way of collapsing
+       // user-defined signals or limiting the length of the queue.
+       if (transition_names_updated_callback) {
+               vector<string> transition_names = global_mixer->get_transition_names();
+               bool changed = false;
+               if (transition_names.size() != last_transition_names.size()) {
+                       changed = true;
+               } else {
+                       for (unsigned i = 0; i < transition_names.size(); ++i) {
+                               if (transition_names[i] != last_transition_names[i]) {
+                                       changed = true;
+                                       break;
+                               }
+                       }
+               }
+               if (changed) {
+                       transition_names_updated_callback(transition_names);
+                       last_transition_names = transition_names;
+               }
+       }
+       if (name_updated_callback) {
+               string name = global_mixer->get_channel_name(channel);
+               if (name != last_name) {
+                       name_updated_callback(name);
+                       last_name = name;
+               }
+       }
+       if (color_updated_callback) {
+               string color = global_mixer->get_channel_color(channel);
+               if (color != last_color) {
+                       color_updated_callback(color);
+                       last_color = color;
+               }
+       }
 }
 
 bool Mixer::OutputChannel::get_display_frame(DisplayFrame *frame)
@@ -1204,7 +1352,21 @@ bool Mixer::OutputChannel::get_display_frame(DisplayFrame *frame)
 void Mixer::OutputChannel::set_frame_ready_callback(Mixer::new_frame_ready_callback_t callback)
 {
        new_frame_ready_callback = callback;
-       has_new_frame_ready_callback = true;
+}
+
+void Mixer::OutputChannel::set_transition_names_updated_callback(Mixer::transition_names_updated_callback_t callback)
+{
+       transition_names_updated_callback = callback;
+}
+
+void Mixer::OutputChannel::set_name_updated_callback(Mixer::name_updated_callback_t callback)
+{
+       name_updated_callback = callback;
+}
+
+void Mixer::OutputChannel::set_color_updated_callback(Mixer::color_updated_callback_t callback)
+{
+       color_updated_callback = callback;
 }
 
 mutex RefCountedGLsync::fence_lock;