X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=modules%2Fdecklink%2Fconsumer%2Fdecklink_consumer.cpp;h=e453e33382d1444ef8c39aa59d64062460e5b9b0;hb=149552fc1baeb375695690d85f999dcb5007a78c;hp=36abd464ce056691c74041d39e25b95d478b227d;hpb=8612f40e8d3f3f37903b4462ae34e279f3b1f991;p=casparcg diff --git a/modules/decklink/consumer/decklink_consumer.cpp b/modules/decklink/consumer/decklink_consumer.cpp index 36abd464c..e453e3338 100644 --- a/modules/decklink/consumer/decklink_consumer.cpp +++ b/modules/decklink/consumer/decklink_consumer.cpp @@ -28,6 +28,7 @@ #include "../decklink_api.h" #include +#include #include #include #include @@ -39,6 +40,8 @@ #include #include #include +#include +#include #include #include #include @@ -60,44 +63,139 @@ struct configuration { internal_keyer, external_keyer, - default_keyer + external_separate_device_keyer, + default_keyer = external_keyer }; enum class latency_t { low_latency, normal_latency, - default_latency + default_latency = normal_latency }; - int device_index = 1; - bool embedded_audio = true; - keyer_t keyer = keyer_t::default_keyer; - latency_t latency = latency_t::default_latency; - bool key_only = false; - int base_buffer_depth = 3; + int device_index = 1; + int key_device_idx = 0; + bool embedded_audio = true; + keyer_t keyer = keyer_t::default_keyer; + latency_t latency = latency_t::default_latency; + bool key_only = false; + int base_buffer_depth = 3; + core::audio_channel_layout out_channel_layout = core::audio_channel_layout::invalid(); int buffer_depth() const { return base_buffer_depth + (latency == latency_t::low_latency ? 0 : 1) + (embedded_audio ? 1 : 0); } + + int key_device_index() const + { + return key_device_idx == 0 ? device_index + 1 : key_device_idx; + } + + core::audio_channel_layout get_adjusted_layout(const core::audio_channel_layout& in_layout) const + { + auto adjusted = out_channel_layout == core::audio_channel_layout::invalid() ? in_layout : out_channel_layout; + + if (adjusted.num_channels == 1) // Duplicate mono-signal into both left and right. + { + adjusted.num_channels = 2; + adjusted.channel_order.push_back(adjusted.channel_order.at(0)); // Usually FC -> FC FC + } + else if (adjusted.num_channels == 2) + { + adjusted.num_channels = 2; + } + else if (adjusted.num_channels <= 8) + { + adjusted.num_channels = 8; + } + else // Over 8 always pad to 16 or drop >16 + { + adjusted.num_channels = 16; + } + + return adjusted; + } }; +void set_latency( + const com_iface_ptr& config, + configuration::latency_t latency, + const std::wstring& print) +{ + if (latency == configuration::latency_t::low_latency) + { + config->SetFlag(bmdDeckLinkConfigLowLatencyVideoOutput, true); + CASPAR_LOG(info) << print << L" Enabled low-latency mode."; + } + else if (latency == configuration::latency_t::normal_latency) + { + config->SetFlag(bmdDeckLinkConfigLowLatencyVideoOutput, false); + CASPAR_LOG(info) << print << L" Disabled low-latency mode."; + } +} + +void set_keyer( + const com_iface_ptr& attributes, + const com_iface_ptr& decklink_keyer, + configuration::keyer_t keyer, + const std::wstring& print) +{ + if (keyer == configuration::keyer_t::internal_keyer) + { + BOOL value = true; + if (SUCCEEDED(attributes->GetFlag(BMDDeckLinkSupportsInternalKeying, &value)) && !value) + CASPAR_LOG(error) << print << L" Failed to enable internal keyer."; + else if (FAILED(decklink_keyer->Enable(FALSE))) + CASPAR_LOG(error) << print << L" Failed to enable internal keyer."; + else if (FAILED(decklink_keyer->SetLevel(255))) + CASPAR_LOG(error) << print << L" Failed to set key-level to max."; + else + CASPAR_LOG(info) << print << L" Enabled internal keyer."; + } + else if (keyer == configuration::keyer_t::external_keyer) + { + BOOL value = true; + if (SUCCEEDED(attributes->GetFlag(BMDDeckLinkSupportsExternalKeying, &value)) && !value) + CASPAR_LOG(error) << print << L" Failed to enable external keyer."; + else if (FAILED(decklink_keyer->Enable(TRUE))) + CASPAR_LOG(error) << print << L" Failed to enable external keyer."; + else if (FAILED(decklink_keyer->SetLevel(255))) + CASPAR_LOG(error) << print << L" Failed to set key-level to max."; + else + CASPAR_LOG(info) << print << L" Enabled external keyer."; + } +} + class decklink_frame : public IDeckLinkVideoFrame { - tbb::atomic ref_count_; - core::const_frame frame_; - const core::video_format_desc format_desc_; + tbb::atomic ref_count_; + core::const_frame frame_; + const core::video_format_desc format_desc_; - const bool key_only_; - cache_aligned_vector data_; + const bool key_only_; + bool needs_to_copy_; + cache_aligned_vector> data_; public: - decklink_frame(core::const_frame frame, const core::video_format_desc& format_desc, bool key_only) + decklink_frame(core::const_frame frame, const core::video_format_desc& format_desc, bool key_only, bool will_attempt_dma) : frame_(frame) , format_desc_(format_desc) , key_only_(key_only) { ref_count_ = 0; + + bool dma_transfer_from_gl_buffer_impossible; + +#if !defined(_MSC_VER) + // On Linux Decklink cannot DMA transfer from memory returned by glMapBuffer (at least on nvidia) + dma_transfer_from_gl_buffer_impossible = true; +#else + // On Windows it is possible. + dma_transfer_from_gl_buffer_impossible = false; +#endif + + needs_to_copy_ = will_attempt_dma && dma_transfer_from_gl_buffer_impossible; } // IUnknown @@ -133,7 +231,7 @@ public: { if(static_cast(frame_.image_data().size()) != format_desc_.size) { - data_.resize(format_desc_.size, 0); + data_.resize(format_desc_.size); *buffer = data_.data(); } else if(key_only_) @@ -146,7 +244,16 @@ public: *buffer = data_.data(); } else + { *buffer = const_cast(frame_.image_data().begin()); + + if (needs_to_copy_) + { + data_.resize(frame_.image_data().size()); + fast_memcpy(data_.data(), *buffer, frame_.image_data().size()); + *buffer = data_.data(); + } + } } catch(...) { @@ -166,6 +273,81 @@ public: { return frame_.audio_data(); } + + int64_t get_age_millis() const + { + return frame_.get_age_millis(); + } +}; + +struct key_video_context : public IDeckLinkVideoOutputCallback, boost::noncopyable +{ + const configuration config_; + com_ptr decklink_ = get_device(config_.key_device_index()); + com_iface_ptr output_ = iface_cast(decklink_); + com_iface_ptr keyer_ = iface_cast(decklink_); + com_iface_ptr attributes_ = iface_cast(decklink_); + com_iface_ptr configuration_ = iface_cast(decklink_); + tbb::atomic current_presentation_delay_; + tbb::atomic scheduled_frames_completed_; + + key_video_context(const configuration& config, const std::wstring& print) + : config_(config) + { + current_presentation_delay_ = 0; + scheduled_frames_completed_ = 0; + + set_latency(configuration_, config.latency, print); + set_keyer(attributes_, keyer_, config.keyer, print); + + if (FAILED(output_->SetScheduledFrameCompletionCallback(this))) + CASPAR_THROW_EXCEPTION(caspar_exception() + << msg_info(print + L" Failed to set key playback completion callback.") + << boost::errinfo_api_function("SetScheduledFrameCompletionCallback")); + } + + template + void enable_video(BMDDisplayMode display_mode, const Print& print) + { + if (FAILED(output_->EnableVideoOutput(display_mode, bmdVideoOutputFlagDefault))) + CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(print() + L" Could not enable key video output.")); + + if (FAILED(output_->SetScheduledFrameCompletionCallback(this))) + CASPAR_THROW_EXCEPTION(caspar_exception() + << msg_info(print() + L" Failed to set key playback completion callback.") + << boost::errinfo_api_function("SetScheduledFrameCompletionCallback")); + } + + virtual ~key_video_context() + { + if (output_) + { + output_->StopScheduledPlayback(0, nullptr, 0); + output_->DisableVideoOutput(); + } + } + + virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, LPVOID*) {return E_NOINTERFACE;} + virtual ULONG STDMETHODCALLTYPE AddRef() {return 1;} + virtual ULONG STDMETHODCALLTYPE Release() {return 1;} + + virtual HRESULT STDMETHODCALLTYPE ScheduledPlaybackHasStopped() + { + return S_OK; + } + + virtual HRESULT STDMETHODCALLTYPE ScheduledFrameCompleted( + IDeckLinkVideoFrame* completed_frame, + BMDOutputFrameCompletionResult result) + { + auto dframe = reinterpret_cast(completed_frame); + current_presentation_delay_ = dframe->get_age_millis(); + ++scheduled_frames_completed_; + + // Let the fill callback keep the pace, so no scheduling here. + + return S_OK; + } }; struct decklink_consumer : public IDeckLinkVideoOutputCallback, public IDeckLinkAudioOutputCallback, boost::noncopyable @@ -179,36 +361,51 @@ struct decklink_consumer : public IDeckLinkVideoOutputCallback, public IDeckLink com_iface_ptr keyer_ = iface_cast(decklink_); com_iface_ptr attributes_ = iface_cast(decklink_); - tbb::spin_mutex exception_mutex_; - std::exception_ptr exception_; + tbb::spin_mutex exception_mutex_; + std::exception_ptr exception_; - tbb::atomic is_running_; + tbb::atomic is_running_; - const std::wstring model_name_ = get_model_name(decklink_); - const core::video_format_desc format_desc_; - const int buffer_size_ = config_.buffer_depth(); // Minimum buffer-size 3. - - long long video_scheduled_ = 0; - long long audio_scheduled_ = 0; - - int preroll_count_ = 0; + const std::wstring model_name_ = get_model_name(decklink_); + bool will_attempt_dma_; + const core::video_format_desc format_desc_; + const core::audio_channel_layout in_channel_layout_; + const core::audio_channel_layout out_channel_layout_ = config_.get_adjusted_layout(in_channel_layout_); + core::audio_channel_remapper channel_remapper_ { in_channel_layout_, out_channel_layout_ }; + const int buffer_size_ = config_.buffer_depth(); // Minimum buffer-size 3. + + long long video_scheduled_ = 0; + long long audio_scheduled_ = 0; + + int preroll_count_ = 0; - boost::circular_buffer> audio_container_ { buffer_size_ + 1 }; + boost::circular_buffer> audio_container_ { buffer_size_ + 1 }; - tbb::concurrent_bounded_queue video_frame_buffer_; - tbb::concurrent_bounded_queue audio_frame_buffer_; + tbb::concurrent_bounded_queue video_frame_buffer_; + tbb::concurrent_bounded_queue audio_frame_buffer_; - spl::shared_ptr graph_; + spl::shared_ptr graph_; caspar::timer tick_timer_; - retry_task send_completion_; + retry_task send_completion_; + reference_signal_detector reference_signal_detector_ { output_ }; + tbb::atomic current_presentation_delay_; + tbb::atomic scheduled_frames_completed_; + std::unique_ptr key_context_; public: - decklink_consumer(const configuration& config, const core::video_format_desc& format_desc, int channel_index) + decklink_consumer( + const configuration& config, + const core::video_format_desc& format_desc, + const core::audio_channel_layout& in_channel_layout, + int channel_index) : channel_index_(channel_index) , config_(config) , format_desc_(format_desc) + , in_channel_layout_(in_channel_layout) { is_running_ = true; + current_presentation_delay_ = 0; + scheduled_frames_completed_ = 0; video_frame_buffer_.set_capacity(1); @@ -217,23 +414,32 @@ public: // samples from 2 frames in order to keep up audio_frame_buffer_.set_capacity((format_desc.fps > 50.0) ? 2 : 1); + if (config.keyer == configuration::keyer_t::external_separate_device_keyer) + key_context_.reset(new key_video_context(config, print())); + graph_->set_color("tick-time", diagnostics::color(0.0f, 0.6f, 0.9f)); graph_->set_color("late-frame", diagnostics::color(0.6f, 0.3f, 0.3f)); graph_->set_color("dropped-frame", diagnostics::color(0.3f, 0.6f, 0.3f)); graph_->set_color("flushed-frame", diagnostics::color(0.4f, 0.3f, 0.8f)); graph_->set_color("buffered-audio", diagnostics::color(0.9f, 0.9f, 0.5f)); graph_->set_color("buffered-video", diagnostics::color(0.2f, 0.9f, 0.9f)); + + if (key_context_) + { + graph_->set_color("key-offset", diagnostics::color(1.0f, 0.0f, 0.0f)); + } + graph_->set_text(print()); diagnostics::register_graph(graph_); - enable_video(get_display_mode(output_, format_desc_.format, bmdFormat8BitBGRA, bmdVideoOutputFlagDefault)); + enable_video(get_display_mode(output_, format_desc_.format, bmdFormat8BitBGRA, bmdVideoOutputFlagDefault, will_attempt_dma_)); if(config.embedded_audio) enable_audio(); - set_latency(config.latency); - set_keyer(config.keyer); - + set_latency(configuration_, config.latency, print()); + set_keyer(attributes_, keyer_, config.keyer, print()); + if(config.embedded_audio) output_->BeginAudioPreroll(); @@ -258,56 +464,14 @@ public: output_->DisableVideoOutput(); } } - - void set_latency(configuration::latency_t latency) - { - if(latency == configuration::latency_t::low_latency) - { - configuration_->SetFlag(bmdDeckLinkConfigLowLatencyVideoOutput, true); - CASPAR_LOG(info) << print() << L" Enabled low-latency mode."; - } - else if(latency == configuration::latency_t::normal_latency) - { - configuration_->SetFlag(bmdDeckLinkConfigLowLatencyVideoOutput, false); - CASPAR_LOG(info) << print() << L" Disabled low-latency mode."; - } - } - - void set_keyer(configuration::keyer_t keyer) - { - if(keyer == configuration::keyer_t::internal_keyer) - { - BOOL value = true; - if(SUCCEEDED(attributes_->GetFlag(BMDDeckLinkSupportsInternalKeying, &value)) && !value) - CASPAR_LOG(error) << print() << L" Failed to enable internal keyer."; - else if(FAILED(keyer_->Enable(FALSE))) - CASPAR_LOG(error) << print() << L" Failed to enable internal keyer."; - else if(FAILED(keyer_->SetLevel(255))) - CASPAR_LOG(error) << print() << L" Failed to set key-level to max."; - else - CASPAR_LOG(info) << print() << L" Enabled internal keyer."; - } - else if(keyer == configuration::keyer_t::external_keyer) - { - BOOL value = true; - if(SUCCEEDED(attributes_->GetFlag(BMDDeckLinkSupportsExternalKeying, &value)) && !value) - CASPAR_LOG(error) << print() << L" Failed to enable external keyer."; - else if(FAILED(keyer_->Enable(TRUE))) - CASPAR_LOG(error) << print() << L" Failed to enable external keyer."; - else if(FAILED(keyer_->SetLevel(255))) - CASPAR_LOG(error) << print() << L" Failed to set key-level to max."; - else - CASPAR_LOG(info) << print() << L" Enabled external keyer."; - } - } void enable_audio() { - if(FAILED(output_->EnableAudioOutput(bmdAudioSampleRate48kHz, bmdAudioSampleType32bitInteger, 2, bmdAudioOutputStreamTimestamped))) - CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(u8(print()) + " Could not enable audio output.")); + if(FAILED(output_->EnableAudioOutput(bmdAudioSampleRate48kHz, bmdAudioSampleType32bitInteger, out_channel_layout_.num_channels, bmdAudioOutputStreamTimestamped))) + CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(print() + L" Could not enable audio output.")); if(FAILED(output_->SetAudioCallback(this))) - CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(u8(print()) + " Could not set audio callback.")); + CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(print() + L" Could not set audio callback.")); CASPAR_LOG(info) << print() << L" Enabled embedded-audio."; } @@ -315,18 +479,24 @@ public: void enable_video(BMDDisplayMode display_mode) { if(FAILED(output_->EnableVideoOutput(display_mode, bmdVideoOutputFlagDefault))) - CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(u8(print()) + " Could not enable video output.")); + CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(print() + L" Could not enable fill video output.")); if(FAILED(output_->SetScheduledFrameCompletionCallback(this))) CASPAR_THROW_EXCEPTION(caspar_exception() - << msg_info(u8(print()) + " Failed to set playback completion callback.") + << msg_info(print() + L" Failed to set fill playback completion callback.") << boost::errinfo_api_function("SetScheduledFrameCompletionCallback")); + + if (key_context_) + key_context_->enable_video(display_mode, [this]() { return print(); }); } void start_playback() { if(FAILED(output_->StartScheduledPlayback(0, format_desc_.time_scale, 1.0))) - CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(u8(print()) + " Failed to schedule playback.")); + CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(print() + L" Failed to schedule fill playback.")); + + if (key_context_ && FAILED(key_context_->output_->StartScheduledPlayback(0, format_desc_.time_scale, 1.0))) + CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(print() + L" Failed to schedule key playback.")); } virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, LPVOID*) {return E_NOINTERFACE;} @@ -347,28 +517,40 @@ public: try { + auto dframe = reinterpret_cast(completed_frame); + current_presentation_delay_ = dframe->get_age_millis(); + ++scheduled_frames_completed_; + + if (key_context_) + graph_->set_value( + "key-offset", + static_cast( + scheduled_frames_completed_ + - key_context_->scheduled_frames_completed_) + * 0.1 + 0.5); + if(result == bmdOutputFrameDisplayedLate) { - graph_->set_tag("late-frame"); + graph_->set_tag(diagnostics::tag_severity::WARNING, "late-frame"); video_scheduled_ += format_desc_.duration; - audio_scheduled_ += reinterpret_cast(completed_frame)->audio_data().size()/format_desc_.audio_channels; + audio_scheduled_ += dframe->audio_data().size() / out_channel_layout_.num_channels; //++video_scheduled_; //audio_scheduled_ += format_desc_.audio_cadence[0]; //++audio_scheduled_; } else if(result == bmdOutputFrameDropped) - graph_->set_tag("dropped-frame"); + graph_->set_tag(diagnostics::tag_severity::WARNING, "dropped-frame"); else if(result == bmdOutputFrameFlushed) - graph_->set_tag("flushed-frame"); + graph_->set_tag(diagnostics::tag_severity::WARNING, "flushed-frame"); - auto frame = core::const_frame::empty(); + UINT32 buffered; + output_->GetBufferedVideoFrameCount(&buffered); + graph_->set_value("buffered-video", static_cast(buffered) / (config_.buffer_depth())); + + auto frame = core::const_frame::empty(); video_frame_buffer_.pop(frame); send_completion_.try_completion(); schedule_next_video(frame); - - UINT32 buffered; - output_->GetBufferedVideoFrameCount(&buffered); - graph_->set_value("buffered-video", static_cast(buffered)/format_desc_.fps); } catch(...) { @@ -398,7 +580,7 @@ public: } else { - schedule_next_audio(core::audio_buffer(format_desc_.audio_cadence[preroll % format_desc_.audio_cadence.size()] * format_desc_.audio_channels, 0)); + schedule_next_audio(core::mutable_audio_buffer(format_desc_.audio_cadence[preroll % format_desc_.audio_cadence.size()] * out_channel_layout_.num_channels, 0)); } } else @@ -407,14 +589,14 @@ public: while(audio_frame_buffer_.try_pop(frame)) { + UINT32 buffered; + output_->GetBufferedAudioSampleFrameCount(&buffered); + graph_->set_value("buffered-audio", static_cast(buffered) / (format_desc_.audio_cadence[0] * config_.buffer_depth())); + send_completion_.try_completion(); - schedule_next_audio(frame.audio_data()); + schedule_next_audio(channel_remapper_.mix_and_rearrange(frame.audio_data())); } } - - UINT32 buffered; - output_->GetBufferedAudioSampleFrameCount(&buffered); - graph_->set_value("buffered-audio", static_cast(buffered) / (format_desc_.audio_cadence[0] * format_desc_.audio_channels * 2)); } catch(...) { @@ -429,7 +611,7 @@ public: template void schedule_next_audio(const T& audio_data) { - auto sample_frame_count = static_cast(audio_data.size()/format_desc_.audio_channels); + auto sample_frame_count = static_cast(audio_data.size()/out_channel_layout_.num_channels); audio_container_.push_back(std::vector(audio_data.begin(), audio_data.end())); @@ -441,14 +623,23 @@ public: void schedule_next_video(core::const_frame frame) { - auto frame2 = wrap_raw(new decklink_frame(frame, format_desc_, config_.key_only)); - if(FAILED(output_->ScheduleVideoFrame(get_raw(frame2), video_scheduled_, format_desc_.duration, format_desc_.time_scale))) - CASPAR_LOG(error) << print() << L" Failed to schedule video."; + if (key_context_) + { + auto key_frame = wrap_raw(new decklink_frame(frame, format_desc_, true, will_attempt_dma_)); + if (FAILED(key_context_->output_->ScheduleVideoFrame(get_raw(key_frame), video_scheduled_, format_desc_.duration, format_desc_.time_scale))) + CASPAR_LOG(error) << print() << L" Failed to schedule key video."; + } + + auto fill_frame = wrap_raw(new decklink_frame(frame, format_desc_, config_.key_only, will_attempt_dma_)); + if (FAILED(output_->ScheduleVideoFrame(get_raw(fill_frame), video_scheduled_, format_desc_.duration, format_desc_.time_scale))) + CASPAR_LOG(error) << print() << L" Failed to schedule fill video."; video_scheduled_ += format_desc_.duration; graph_->set_value("tick-time", tick_timer_.elapsed()*format_desc_.fps*0.5); tick_timer_.restart(); + + reference_signal_detector_.detect_change([this]() { return print(); }); } std::future send(core::const_frame frame) @@ -462,7 +653,7 @@ public: std::rethrow_exception(exception); if(!is_running_) - CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(u8(print()) + " Is not running.")); + CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(print() + L" Is not running.")); bool audio_ready = !config_.embedded_audio; bool video_ready = false; @@ -491,8 +682,16 @@ public: std::wstring print() const { - return model_name_ + L" [" + boost::lexical_cast(channel_index_) + L"-" + - boost::lexical_cast(config_.device_index) + L"|" + format_desc_.name + L"]"; + if (config_.keyer == configuration::keyer_t::external_separate_device_keyer) + return model_name_ + L" [" + boost::lexical_cast(channel_index_)+L"-" + + boost::lexical_cast(config_.device_index) + + L"&&" + + boost::lexical_cast(config_.key_device_index()) + + L"|" + + format_desc_.name + L"]"; + else + return model_name_ + L" [" + boost::lexical_cast(channel_index_)+L"-" + + boost::lexical_cast(config_.device_index) + L"|" + format_desc_.name + L"]"; } }; @@ -501,6 +700,7 @@ struct decklink_consumer_proxy : public core::frame_consumer core::monitor::subject monitor_subject_; const configuration config_; std::unique_ptr consumer_; + core::video_format_desc format_desc_; executor executor_; public: @@ -527,12 +727,13 @@ public: // frame_consumer - void initialize(const core::video_format_desc& format_desc, int channel_index) override + void initialize(const core::video_format_desc& format_desc, const core::audio_channel_layout& channel_layout, int channel_index) override { + format_desc_ = format_desc; executor_.invoke([=] { consumer_.reset(); - consumer_.reset(new decklink_consumer(config_, format_desc, channel_index)); + consumer_.reset(new decklink_consumer(config_, format_desc, channel_layout, channel_index)); }); } @@ -557,15 +758,22 @@ public: info.add(L"type", L"decklink"); info.add(L"key-only", config_.key_only); info.add(L"device", config_.device_index); + + if (config_.keyer == configuration::keyer_t::external_separate_device_keyer) + { + info.add(L"key-device", config_.key_device_index()); + } + info.add(L"low-latency", config_.latency == configuration::latency_t::low_latency); info.add(L"embedded-audio", config_.embedded_audio); + info.add(L"presentation-frame-age", presentation_frame_age_millis()); //info.add(L"internal-key", config_.internal_key); return info; } int buffer_depth() const override { - return config_.buffer_depth(); + return config_.buffer_depth() + 2; } int index() const override @@ -573,6 +781,11 @@ public: return 300 + config_.device_index; } + int64_t presentation_frame_age_millis() const override + { + return consumer_ ? static_cast(consumer_->current_presentation_delay_) : 0; + } + core::monitor::subject& monitor_output() { return monitor_subject_; @@ -584,27 +797,34 @@ void describe_consumer(core::help_sink& sink, const core::help_repository& repo) sink.short_description(L"Sends video on an SDI output using Blackmagic Decklink video cards."); sink.syntax(L"DECKLINK " L"{[device_index:int]|1} " - L"{[keyer:INTERNAL_KEY,EXTERNAL_KEY]} " + L"{[keyer:INTERNAL_KEY,EXTERNAL_KEY,EXTERNAL_SEPARATE_DEVICE_KEY]} " L"{[low_latency:LOW_LATENCY]} " L"{[embedded_audio:EMBEDDED_AUDIO]} " - L"{[key_only:KEY_ONLY]}"); + L"{[key_only:KEY_ONLY]} " + L"{CHANNEL_LAYOUT [channel_layout:string]}"); sink.para()->text(L"Sends video on an SDI output using Blackmagic Decklink video cards."); sink.definitions() ->item(L"device_index", L"The Blackmagic video card to use (See Blackmagic control panel for card order). Default is 1.") - ->item(L"keyer", L"If given tries to enable either internal or external keying. Not all Blackmagic cards supports this.") + ->item(L"keyer", + L"If given tries to enable either internal or external keying. Not all Blackmagic cards supports this. " + L"There is also a third experimental option (EXTERNAL_SEPARATE_DEVICE_KEY) which allocates device_index + 1 for synhronized key output.") ->item(L"low_latency", L"Tries to enable low latency if given.") ->item(L"embedded_audio", L"Embeds the audio into the SDI signal if given.") ->item(L"key_only", L" will extract only the alpha channel from the " L"channel. This is useful when you have two SDI video cards, and neither has native support " - L"for separate fill/key output"); + L"for separate fill/key output") + ->item(L"channel_layout", L"If specified, overrides the audio channel layout used by the channel."); sink.para()->text(L"Examples:"); sink.example(L">> ADD 1 DECKLINK", L"for using the default device_index of 1."); sink.example(L">> ADD 1 DECKLINK 2", L"uses device_index 2."); sink.example(L">> ADD 1 DECKLINK 1 EXTERNAL_KEY EMBEDDED_AUDIO"); sink.example( - L">> ADD 1 DECKLINK 1 EMBEDDED_AUDIO\n" - L">> ADD 1 DECKLINK 2 KEY_ONLY", L"uses device with index 1 as fill output with audio and device with index 2 as key output."); + L">> ADD 1 DECKLINK 1 EMBEDDED_AUDIO\n" + L">> ADD 1 DECKLINK 2 KEY_ONLY", L"uses device with index 1 as fill output with audio and device with index 2 as key output."); + sink.example( + L">> ADD 1 DECKLINK 1 EXTERNAL_SEPARATE_DEVICE_KEY EMBEDDED_AUDIO", + L"Uses device 2 for key output. May give better sync between key and fill than the previous method."); } spl::shared_ptr create_consumer( @@ -622,6 +842,8 @@ spl::shared_ptr create_consumer( config.keyer = configuration::keyer_t::internal_keyer; else if (contains_param(L"EXTERNAL_KEY", params)) config.keyer = configuration::keyer_t::external_keyer; + else if (contains_param(L"EXTERNAL_SEPARATE_DEVICE_KEY", params)) + config.keyer = configuration::keyer_t::external_separate_device_keyer; else config.keyer = configuration::keyer_t::default_keyer; @@ -631,6 +853,18 @@ spl::shared_ptr create_consumer( config.embedded_audio = contains_param(L"EMBEDDED_AUDIO", params); config.key_only = contains_param(L"KEY_ONLY", params); + auto channel_layout = get_param(L"CHANNEL_LAYOUT", params); + + if (!channel_layout.empty()) + { + auto found_layout = core::audio_channel_layout_repository::get_default()->get_layout(channel_layout); + + if (!found_layout) + CASPAR_THROW_EXCEPTION(user_error() << msg_info(L"Channel layout " + channel_layout + L" not found.")); + + config.out_channel_layout = *found_layout; + } + return spl::make_shared(config); } @@ -644,15 +878,32 @@ spl::shared_ptr create_preconfigured_consumer( config.keyer = configuration::keyer_t::external_keyer; else if(keyer == L"internal") config.keyer = configuration::keyer_t::internal_keyer; + else if (keyer == L"external_separate_device") + config.keyer = configuration::keyer_t::external_separate_device_keyer; - auto latency = ptree.get(L"latency", L"normal"); + auto latency = ptree.get(L"latency", L"default"); if(latency == L"low") config.latency = configuration::latency_t::low_latency; else if(latency == L"normal") config.latency = configuration::latency_t::normal_latency; + auto channel_layout = ptree.get_optional(L"channel-layout"); + + if (channel_layout) + { + CASPAR_SCOPED_CONTEXT_MSG("/channel-layout") + + auto found_layout = core::audio_channel_layout_repository::get_default()->get_layout(*channel_layout); + + if (!found_layout) + CASPAR_THROW_EXCEPTION(user_error() << msg_info(L"Channel layout " + *channel_layout + L" not found.")); + + config.out_channel_layout = *found_layout; + } + config.key_only = ptree.get(L"key-only", config.key_only); config.device_index = ptree.get(L"device", config.device_index); + config.key_device_idx = ptree.get(L"key-device", config.key_device_idx); config.embedded_audio = ptree.get(L"embedded-audio", config.embedded_audio); config.base_buffer_depth = ptree.get(L"buffer-depth", config.base_buffer_depth);