From: Steinar H. Gunderson Date: Sat, 2 Oct 2021 14:54:58 +0000 (+0200) Subject: Support unsynchronized HDMI/SDI output. X-Git-Tag: 2.1.0~5 X-Git-Url: https://git.sesse.net/?p=nageru;a=commitdiff_plain;h=403942c08c2403321fcfd5e6c5f2e6aa7c099874 Support unsynchronized HDMI/SDI output. This is for if you want just a monitor output without synchronizing your entire stream chain to the output card (ie., you want to keep some other camera as the master). Sound support is untested, and is probably going to crackle a fair bit. There's no GUI support for changing this currently. --- diff --git a/nageru/decklink_output.cpp b/nageru/decklink_output.cpp index 56a184e..91fbd40 100644 --- a/nageru/decklink_output.cpp +++ b/nageru/decklink_output.cpp @@ -131,10 +131,11 @@ bool DeckLinkOutput::set_device(IDeckLink *decklink, IDeckLinkInput *input_arg) return true; } -void DeckLinkOutput::start_output(uint32_t mode, int64_t base_pts) +void DeckLinkOutput::start_output(uint32_t mode, int64_t base_pts, bool is_master_card_arg) { assert(output); assert(!playback_initiated); + this->is_master_card = is_master_card_arg; if (video_modes.empty()) { fprintf(stderr, "ERROR: No matching output modes for %dx%d found\n", width, height); @@ -224,9 +225,13 @@ void DeckLinkOutput::start_output(uint32_t mode, int64_t base_pts) fprintf(stderr, "Couldn't enable audio output\n"); abort(); } - if (output->BeginAudioPreroll() != S_OK) { - fprintf(stderr, "Couldn't begin audio preroll\n"); - abort(); + if (is_master_card) { + if (output->BeginAudioPreroll() != S_OK) { + fprintf(stderr, "Couldn't begin audio preroll\n"); + abort(); + } + } else { + playback_started = true; } present_thread = thread([this]{ @@ -253,7 +258,9 @@ void DeckLinkOutput::end_output() present_thread.join(); playback_initiated = false; - output->StopScheduledPlayback(0, nullptr, 0); + if (is_master_card) { + output->StopScheduledPlayback(0, nullptr, 0); + } output->DisableVideoOutput(); output->DisableAudioOutput(); @@ -348,13 +355,20 @@ void DeckLinkOutput::send_audio(int64_t pts, const std::vector &samples) } uint32_t frames_written; - HRESULT result = output->ScheduleAudioSamples(int_samples.get(), samples.size() / 2, - pts, TIMEBASE, &frames_written); + HRESULT result; + if (is_master_card) { + result = output->ScheduleAudioSamples(int_samples.get(), samples.size() / 2, + pts, TIMEBASE, &frames_written); + } else { + result = output->WriteAudioSamplesSync(int_samples.get(), samples.size() / 2, + &frames_written); + } if (result != S_OK) { - fprintf(stderr, "ScheduleAudioSamples(pts=%" PRId64 ") failed (result=0x%08x)\n", pts, result); + fprintf(stderr, "write audio to DeckLink (pts=%" PRId64 ") failed (result=0x%08x)\n", pts, result); } else { - if (frames_written != samples.size() / 2) { - fprintf(stderr, "ScheduleAudioSamples() returned short write (%u/%zu)\n", frames_written, samples.size() / 2); + // Non-master card is not really synchronized on audio at all, so we don't warn on it. + if (frames_written != samples.size() / 2 && is_master_card) { + fprintf(stderr, "write audio to DeckLink returned short write (%u/%zu)\n", frames_written, samples.size() / 2); } } metric_decklink_output_scheduled_samples += samples.size() / 2; @@ -593,16 +607,24 @@ void DeckLinkOutput::present_thread_func() // Release any input frames we needed to render this frame. frame->input_frames.clear(); - BMDTimeValue pts = frame->pts; - BMDTimeValue duration = frame->duration; - HRESULT res = output->ScheduleVideoFrame(frame.get(), pts, duration, TIMEBASE); - lock_guard lock(frame_queue_mutex); - if (res == S_OK) { - scheduled_frames.insert(frame.release()); // Owned by the driver now. - ++metric_decklink_output_inflight_frames; - } else { - fprintf(stderr, "Could not schedule video frame! (error=0x%08x)\n", res); + if (is_master_card) { + BMDTimeValue pts = frame->pts; + BMDTimeValue duration = frame->duration; + HRESULT res = output->ScheduleVideoFrame(frame.get(), pts, duration, TIMEBASE); + lock_guard lock(frame_queue_mutex); + if (res == S_OK) { + scheduled_frames.insert(frame.release()); // Owned by the driver now. + ++metric_decklink_output_inflight_frames; + } else { + fprintf(stderr, "Could not schedule video frame! (error=0x%08x)\n", res); + frame_freelist.push(move(frame)); + } + } else { + HRESULT res = output->DisplayVideoFrameSync(frame.get()); + if (res != S_OK) { + fprintf(stderr, "Could not schedule video frame! (error=0x%08x)\n", res); + } frame_freelist.push(move(frame)); } } diff --git a/nageru/decklink_output.h b/nageru/decklink_output.h index f008dc3..4aa76be 100644 --- a/nageru/decklink_output.h +++ b/nageru/decklink_output.h @@ -46,7 +46,7 @@ public: // where you get a freeze if querying an IDeckLinkInput interface // on an already-started card. bool set_device(IDeckLink *decklink, IDeckLinkInput *input_arg); - void start_output(uint32_t mode, int64_t base_pts); // Mode comes from get_available_video_modes(). + void start_output(uint32_t mode, int64_t base_pts, bool is_master_card); // Mode comes from get_available_video_modes(). void end_output(); void send_frame(GLuint y_tex, GLuint cbcr_tex, movit::YCbCrLumaCoefficients ycbcr_coefficients, const std::vector &input_frames, int64_t pts, int64_t duration); @@ -143,7 +143,7 @@ private: std::unordered_set scheduled_frames; // Owned by the driver, so no unique_ptr. Under . std::condition_variable frame_queues_changed; - bool playback_initiated = false, playback_started = false; + bool playback_initiated = false, playback_started = false, is_master_card = false; int64_t base_pts, frame_duration; BMDDisplayModeFlags current_mode_flags = 0; bool last_frame_had_mode_mismatch = false; diff --git a/nageru/flags.cpp b/nageru/flags.cpp index ed35062..42fdb88 100644 --- a/nageru/flags.cpp +++ b/nageru/flags.cpp @@ -69,6 +69,7 @@ enum LongOption { OPTION_OUTPUT_YCBCR_COEFFICIENTS, OPTION_OUTPUT_BUFFER_FRAMES, OPTION_OUTPUT_SLOP_FRAMES, + OPTION_OUTPUT_CARD_UNSYNCHRONIZED, OPTION_TIMECODE_STREAM, OPTION_TIMECODE_STDOUT, OPTION_QUICK_CUT_KEYS, @@ -230,6 +231,8 @@ void usage(Program program) fprintf(stderr, " --output-slop-frames=NUM if more less than this number of frames behind for\n"); fprintf(stderr, " --output-card, try to submit anyway instead of\n"); fprintf(stderr, " dropping the frame (default 0.5)\n"); + fprintf(stderr, " --output-card-unsynchronized if --output-card is in use, do _not_ use it as\n"); + fprintf(stderr, " master clock (may give jittery output and audio breakups)\n"); fprintf(stderr, " --timecode-stream show timestamp and timecode in stream\n"); fprintf(stderr, " --timecode-stdout show timestamp and timecode on standard output\n"); fprintf(stderr, " --quick-cut-keys enable direct cutting by Q, W, E, ... keys\n"); @@ -315,6 +318,7 @@ void parse_flags(Program program, int argc, char * const argv[]) { "output-ycbcr-coefficients", required_argument, 0, OPTION_OUTPUT_YCBCR_COEFFICIENTS }, { "output-buffer-frames", required_argument, 0, OPTION_OUTPUT_BUFFER_FRAMES }, { "output-slop-frames", required_argument, 0, OPTION_OUTPUT_SLOP_FRAMES }, + { "output-card-unsynchronized", no_argument, 0, OPTION_OUTPUT_CARD_UNSYNCHRONIZED }, { "timecode-stream", no_argument, 0, OPTION_TIMECODE_STREAM }, { "timecode-stdout", no_argument, 0, OPTION_TIMECODE_STDOUT }, { "quick-cut-keys", no_argument, 0, OPTION_QUICK_CUT_KEYS }, @@ -549,6 +553,9 @@ void parse_flags(Program program, int argc, char * const argv[]) case OPTION_OUTPUT_SLOP_FRAMES: global_flags.output_slop_frames = atof(optarg); break; + case OPTION_OUTPUT_CARD_UNSYNCHRONIZED: + global_flags.output_card_is_master = false; + break; case OPTION_TIMECODE_STREAM: global_flags.display_timecode_in_stream = true; break; diff --git a/nageru/flags.h b/nageru/flags.h index 9b9976c..734e290 100644 --- a/nageru/flags.h +++ b/nageru/flags.h @@ -64,6 +64,7 @@ struct Flags { int output_card = -1; double output_buffer_frames = 6.0; double output_slop_frames = 0.5; + bool output_card_is_master = true; int max_input_queue_frames = 6; int http_port = DEFAULT_HTTPD_PORT; int srt_port = DEFAULT_SRT_PORT; // -1 for none. diff --git a/nageru/glwidget.cpp b/nageru/glwidget.cpp index f5f350a..6ff304c 100644 --- a/nageru/glwidget.cpp +++ b/nageru/glwidget.cpp @@ -378,7 +378,7 @@ void GLWidget::show_preview_context_menu(unsigned signal_num, const QPoint &pos) // And a master clock selector. QAction *master_clock_action = new QAction("Use as master clock", &menu); master_clock_action->setCheckable(true); - if (global_mixer->get_output_card_index() != -1) { + if (global_mixer->get_output_card_index() != -1 && global_mixer->get_output_card_is_master()) { master_clock_action->setChecked(false); master_clock_action->setEnabled(false); } else if (global_mixer->get_master_clock() == signal_num) { diff --git a/nageru/mixer.cpp b/nageru/mixer.cpp index 3a137d6..5de75c6 100644 --- a/nageru/mixer.cpp +++ b/nageru/mixer.cpp @@ -543,6 +543,7 @@ Mixer::Mixer(const QSurfaceFormat &format) if (global_flags.enable_alsa_output) { alsa.reset(new ALSAOutput(OUTPUT_FREQUENCY, /*num_channels=*/2)); } + output_card_is_master = global_flags.output_card_is_master; if (global_flags.output_card != -1) { desired_output_card_index = global_flags.output_card; set_output_card_internal(global_flags.output_card); @@ -899,7 +900,7 @@ void Mixer::set_output_card_internal(int card_index) card->jitter_history.clear(); card->capture->start_bm_capture(); desired_output_video_mode = output_video_mode = card->output->pick_video_mode(desired_output_video_mode); - card->output->start_output(desired_output_video_mode, pts_int); + card->output->start_output(desired_output_video_mode, pts_int, /*is_master_card=*/output_card_is_master); } output_card_index = card_index; output_jitter_history.clear(); @@ -1286,7 +1287,7 @@ void Mixer::thread_func() DeckLinkOutput *output = cards[output_card_index].output.get(); output->end_output(); desired_output_video_mode = output_video_mode = output->pick_video_mode(desired_output_video_mode); - output->start_output(desired_output_video_mode, pts_int); + output->start_output(desired_output_video_mode, pts_int, /*is_master_card=*/output_card_is_master); } { @@ -1299,7 +1300,7 @@ void Mixer::thread_func() bool master_card_is_output; unsigned master_card_index; - if (output_card_index != -1) { + if (output_card_index != -1 && output_card_is_master) { master_card_is_output = true; master_card_index = output_card_index; } else { @@ -1406,7 +1407,7 @@ void Mixer::thread_func() bool Mixer::input_card_is_master_clock(unsigned card_index, unsigned master_card_index) const { - if (output_card_index != -1) { + if (output_card_index != -1 && output_card_is_master) { // The output card (ie., cards[output_card_index].output) is the master clock, // so no input card (ie., cards[card_index].capture) is. return false; diff --git a/nageru/mixer.h b/nageru/mixer.h index 6e9da90..99385fa 100644 --- a/nageru/mixer.h +++ b/nageru/mixer.h @@ -326,6 +326,10 @@ public: desired_output_card_index = card_index; } + bool get_output_card_is_master() const { + return output_card_is_master; + } + std::map get_available_output_video_modes() const; uint32_t get_output_video_mode() const { @@ -396,9 +400,10 @@ private: std::unique_ptr resource_pool; std::unique_ptr theme; std::atomic audio_source_channel{0}; - std::atomic master_clock_channel{0}; // Gets overridden by if set. + std::atomic master_clock_channel{0}; // Gets overridden by if output_card_is_master == true. int output_card_index = -1; // -1 for none. uint32_t output_video_mode = -1; + bool output_card_is_master = false; // Only relevant if output_card_index != -1. // The mechanics of changing the output card and modes are so intricately connected // with the work the mixer thread is doing. Thus, we don't change it directly,