diff --git a/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp b/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp index e61005c18..8dab0838b 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp @@ -4,8 +4,10 @@ #include "xstudio/utility/logging.hpp" #include "xstudio/utility/chrono.hpp" #include "xstudio/enums.hpp" +#include #include #include +#include #ifdef __linux__ #include @@ -48,8 +50,105 @@ class TimeLogger { std::string label_; std::chrono::high_resolution_clock::time_point start_time_; }; + +std::string with_runtime_details( + const std::string &message, const std::string &runtime_info) { + if (runtime_info.empty()) { + return message; + } + return fmt::format("{} ({})", message, runtime_info); +} + +std::string format_hresult(const HRESULT result) { + return fmt::format("0x{:08X}", static_cast(result)); +} + +struct DeckLinkVersion { + int major = 0; + int minor = 0; + int patch = 0; +}; + +bool parse_decklink_version(const std::string &version, DeckLinkVersion &parsed) { + if (version.empty()) { + return false; + } + + std::stringstream ss(version); + char dot1 = 0; + char dot2 = 0; + if (!(ss >> parsed.major >> dot1 >> parsed.minor >> dot2 >> parsed.patch)) { + return false; + } + + return dot1 == '.' && dot2 == '.'; +} + +bool is_decklink_version_older_than( + const DeckLinkVersion &lhs, const DeckLinkVersion &rhs) { + if (lhs.major != rhs.major) { + return lhs.major < rhs.major; + } + if (lhs.minor != rhs.minor) { + return lhs.minor < rhs.minor; + } + return lhs.patch < rhs.patch; +} } // namespace +void DecklinkOutput::detect_runtime_info() { + std::vector details; + api_version_.clear(); + +#ifdef __linux__ + Dl_info dl_info; + if (dladdr(reinterpret_cast(CreateDeckLinkIteratorInstance), &dl_info) && + dl_info.dli_fname) { + details.emplace_back(fmt::format("library={}", dl_info.dli_fname)); + } +#endif + + if (auto *api_info = CreateDeckLinkAPIInformationInstance()) { + const char *api_version = nullptr; + if (api_info->GetString(BMDDeckLinkAPIVersion, &api_version) == S_OK && api_version) { + api_version_ = api_version; + details.emplace_back(fmt::format("api_version={}", api_version)); + } + api_info->Release(); + } + + if (details.empty()) { + runtime_info_.clear(); + return; + } + + std::ostringstream out; + for (size_t i = 0; i < details.size(); ++i) { + if (i) { + out << ", "; + } + out << details[i]; + } + runtime_info_ = out.str(); +} + +void DecklinkOutput::log_runtime_info() const { + if (runtime_info_.empty() && output_interface_info_.empty()) { + return; + } + + if (output_interface_info_.empty()) { + spdlog::info("DeckLink runtime detected: {}", runtime_info_); + } else if (runtime_info_.empty()) { + spdlog::info("DeckLink runtime detected: output_interface={}", output_interface_info_); + } else { + spdlog::info( + "DeckLink runtime detected: {}, output_interface={}", + runtime_info_, + output_interface_info_); + } +} + void DecklinkOutput::check_decklink_installation() { // here we try to open the decklink driver libs. If they are not installed @@ -117,22 +216,47 @@ DecklinkOutput::DecklinkOutput(BMDecklinkPlugin *decklink_xstudio_plugin) } DecklinkOutput::~DecklinkOutput() { + running_ = false; + { + std::lock_guard lk(audio_samples_cv_mutex_); + fetch_more_samples_from_xstudio_ = true; + } + audio_samples_cv_.notify_all(); - if (decklink_output_interface_ != NULL) { + release_resources(); + spdlog::info("Closing Decklink Output"); +} +void DecklinkOutput::release_resources() { + + if (decklink_output_interface_ != NULL) { spdlog::info("Stopping Decklink output loop."); - decklink_output_interface_->StopScheduledPlayback(0, NULL, 0); - decklink_output_interface_->DisableVideoOutput(); - decklink_output_interface_->DisableAudioOutput(); + decklink_output_interface_->SetScheduledFrameCompletionCallback(nullptr); + decklink_output_interface_->SetAudioCallback(nullptr); + if (scheduled_playback_started_) { + decklink_output_interface_->StopScheduledPlayback(0, NULL, 0); + scheduled_playback_started_ = false; + } + if (video_output_enabled_) { + decklink_output_interface_->DisableVideoOutput(); + video_output_enabled_ = false; + } + if (audio_output_enabled_) { + decklink_output_interface_->DisableAudioOutput(); + audio_output_enabled_ = false; + } decklink_output_interface_->Release(); } if (decklink_interface_ != NULL) { decklink_interface_->Release(); } - if (output_callback_ != NULL) { - output_callback_->Release(); + if (video_output_callback_ != NULL) { + video_output_callback_->Release(); + } + if (audio_output_callback_ != NULL) { + audio_output_callback_->Release(); } if (frame_converter_ != NULL) { @@ -141,8 +265,17 @@ DecklinkOutput::~DecklinkOutput() { if (intermediate_frame_ != nullptr) { intermediate_frame_->Release(); + intermediate_frame_ = nullptr; } - spdlog::info("Closing Decklink Output"); + + video_output_callback_ = nullptr; + audio_output_callback_ = nullptr; + frame_converter_ = nullptr; + decklink_output_interface_ = nullptr; + decklink_interface_ = nullptr; + scheduled_playback_started_ = false; + video_output_enabled_ = false; + audio_output_enabled_ = false; } void DecklinkOutput::set_preroll() { @@ -245,8 +378,12 @@ bool DecklinkOutput::init_decklink() { bool bSuccess = false; IDeckLinkIterator *decklink_iterator = NULL; + last_error_.clear(); + api_version_.clear(); + output_interface_info_.clear(); try { + detect_runtime_info(); #ifdef _WIN32 HRESULT result; @@ -271,27 +408,81 @@ bool DecklinkOutput::init_decklink() { "Blackmagic DeckLink drivers to use the features of this plugin."); } - if (decklink_iterator->Next(&decklink_interface_) != S_OK) { - throw std::runtime_error( - "This plugin requires a DeckLink device. You will not be able to use the " - "features of this plugin until a DeckLink device is installed."); + const auto next_result = decklink_iterator->Next(&decklink_interface_); + if (next_result != S_OK) { + spdlog::warn( + "DeckLink iterator returned {} while probing for a device.", + format_hresult(next_result)); + throw std::runtime_error(with_runtime_details( + "DeckLink drivers found but no device is installed", runtime_info_)); } - if (decklink_interface_->QueryInterface( - IID_IDeckLinkOutput, (void **)&decklink_output_interface_) != S_OK) { - throw std::runtime_error("QueryInterface failed."); + DeckLinkVersion parsed_version; + const DeckLinkVersion minimum_supported{15, 0, 0}; + const bool parsed = parse_decklink_version(api_version_, parsed_version); + if (!parsed || is_decklink_version_older_than(parsed_version, minimum_supported)) { + const auto upgrade_message = with_runtime_details( + "Unsupported Blackmagic DeckLink runtime detected. xStudio requires " + "Blackmagic Desktop Video drivers 15.x or newer to use Blackmagic cards.", + runtime_info_); + spdlog::error("{}", upgrade_message); + throw std::runtime_error(upgrade_message); } - output_callback_ = new AVOutputCallback(this); - if (output_callback_ == NULL) + const auto modern_output_result = decklink_interface_->QueryInterface( + IID_IDeckLinkOutput, (void **)&decklink_output_interface_); + if (modern_output_result != S_OK) { + spdlog::warn( + "DeckLink modern output interface query failed with {}.", + format_hresult(modern_output_result)); + throw std::runtime_error(with_runtime_details( + "Unsupported Blackmagic DeckLink runtime detected. xStudio requires " + "Blackmagic Desktop Video drivers 15.x or newer to use Blackmagic cards.", + runtime_info_)); + } else { + output_interface_info_ = "IID_IDeckLinkOutput"; + spdlog::info( + "DeckLink modern output interface query succeeded with {}.", + format_hresult(modern_output_result)); + } + + video_output_callback_ = new AVOutputCallback(this); + if (video_output_callback_ == NULL) throw std::runtime_error("Failed to create Video Output Callback."); - if (decklink_output_interface_->SetScheduledFrameCompletionCallback(output_callback_) != - S_OK) + audio_output_callback_ = new AudioOutputCallback(this); + if (audio_output_callback_ == NULL) + throw std::runtime_error("Failed to create Audio Output Callback."); + + spdlog::info( + "Registering DeckLink video callback via {}.", + output_interface_info_.empty() ? "unknown interface" : output_interface_info_); + const auto video_callback_result = decklink_output_interface_->SetScheduledFrameCompletionCallback( + static_cast(video_output_callback_)); + if (video_callback_result != S_OK) { + spdlog::error( + "DeckLink SetScheduledFrameCompletionCallback failed with {}.", + format_hresult(video_callback_result)); throw std::runtime_error("SetScheduledFrameCompletionCallback failed."); - - if (decklink_output_interface_->SetAudioCallback(output_callback_) != S_OK) + } + spdlog::info( + "DeckLink SetScheduledFrameCompletionCallback succeeded with {}.", + format_hresult(video_callback_result)); + + spdlog::info( + "Registering DeckLink audio callback via {}.", + output_interface_info_.empty() ? "unknown interface" : output_interface_info_); + const auto audio_callback_result = decklink_output_interface_->SetAudioCallback( + static_cast(audio_output_callback_)); + if (audio_callback_result != S_OK) { + spdlog::error( + "DeckLink SetAudioCallback failed with {}.", + format_hresult(audio_callback_result)); throw std::runtime_error("SetAudioCallback failed."); + } + spdlog::info( + "DeckLink SetAudioCallback succeeded with {}.", + format_hresult(audio_callback_result)); #ifdef _WIN32 // Create an IDeckLinkVideoConversion interface object to provide pixel format @@ -310,38 +501,39 @@ bool DecklinkOutput::init_decklink() { #else frame_converter_ = CreateVideoConversionInstance(); + if (!frame_converter_) { + throw std::runtime_error(with_runtime_details( + "DeckLink runtime ABI mismatch: failed to create the video conversion " + "interface", + runtime_info_)); + } #endif - bSuccess = true; + bSuccess = true; + is_available_ = true; + log_runtime_info(); + spdlog::info("DeckLink runtime is supported."); query_display_modes(); } catch (std::exception &e) { - std::cerr << "DecklinkOutput::init_decklink() failed: " << e.what() << "\n"; + is_available_ = false; + last_error_ = e.what(); report_error(e.what()); - - if (decklink_output_interface_ != NULL) { - decklink_output_interface_->Release(); - decklink_output_interface_ = NULL; - } - if (decklink_interface_ != NULL) { - decklink_interface_->Release(); - decklink_interface_ = NULL; - } - if (output_callback_ != NULL) { - output_callback_->Release(); - output_callback_ = NULL; - } - - if (decklink_iterator != NULL) { - decklink_iterator->Release(); - decklink_iterator = NULL; + if (decklink_output_interface_) { + decklink_output_interface_->SetScheduledFrameCompletionCallback(nullptr); + decklink_output_interface_->SetAudioCallback(nullptr); } + release_resources(); } + if (decklink_iterator != NULL) { + decklink_iterator->Release(); + decklink_iterator = NULL; + } return bSuccess; } @@ -351,6 +543,10 @@ void DecklinkOutput::query_display_modes() { IDeckLinkDisplayModeIterator *display_mode_iterator = NULL; IDeckLinkDisplayMode *display_mode = NULL; + if (!decklink_output_interface_) { + return; + } + try { // Get first avaliable video mode for Output @@ -437,7 +633,8 @@ bool DecklinkOutput::start_sdi_output() { try { if (!decklink_output_interface_) { - throw std::runtime_error("No DeckLink device is available."); + throw std::runtime_error( + last_error_.empty() ? "No DeckLink device is available." : last_error_); } bool mode_matched = false; @@ -467,11 +664,18 @@ bool DecklinkOutput::start_sdi_output() { uiFPS = ((frame_timescale_ + (frame_duration_ - 1)) / frame_duration_); - if (decklink_output_interface_->EnableVideoOutput( - display_mode->GetDisplayMode(), bmdVideoOutputFlagDefault) != - S_OK) { + const auto enable_video_result = decklink_output_interface_->EnableVideoOutput( + display_mode->GetDisplayMode(), bmdVideoOutputFlagDefault); + if (enable_video_result != S_OK) { + spdlog::error( + "DeckLink EnableVideoOutput failed with {}.", + format_hresult(enable_video_result)); throw std::runtime_error("EnableVideoOutput call failed."); } + spdlog::info( + "DeckLink EnableVideoOutput succeeded with {}.", + format_hresult(enable_video_result)); + video_output_enabled_ = true; } } } @@ -483,29 +687,56 @@ bool DecklinkOutput::start_sdi_output() { uiTotalFrames = 0; // Set the audio output mode - if (decklink_output_interface_->EnableAudioOutput( - bmdAudioSampleRate48kHz, - bmdAudioSampleType16bitInteger, - 2, // num channels - bmdAudioOutputStreamTimestamped) != S_OK) { + const auto enable_audio_result = decklink_output_interface_->EnableAudioOutput( + bmdAudioSampleRate48kHz, + bmdAudioSampleType16bitInteger, + 2, // num channels + bmdAudioOutputStreamTimestamped); + if (enable_audio_result != S_OK) { + spdlog::error( + "DeckLink EnableAudioOutput failed with {}.", + format_hresult(enable_audio_result)); throw std::runtime_error("Failed to enable audio output."); } + spdlog::info( + "DeckLink EnableAudioOutput succeeded with {}.", + format_hresult(enable_audio_result)); + audio_output_enabled_ = true; set_preroll(); samples_delivered_ = 0; - if (decklink_output_interface_->BeginAudioPreroll() != S_OK) { + const auto preroll_result = decklink_output_interface_->BeginAudioPreroll(); + if (preroll_result != S_OK) { + spdlog::error( + "DeckLink BeginAudioPreroll failed with {}.", + format_hresult(preroll_result)); throw std::runtime_error("Failed to pre-roll audio output."); } + spdlog::info( + "DeckLink BeginAudioPreroll succeeded with {}.", + format_hresult(preroll_result)); decklink_output_interface_->StartScheduledPlayback(0, 100, 1.0); + scheduled_playback_started_ = true; bSuccess = true; } catch (std::exception &e) { - + if (scheduled_playback_started_) { + decklink_output_interface_->StopScheduledPlayback(0, NULL, 0); + scheduled_playback_started_ = false; + } + if (audio_output_enabled_) { + decklink_output_interface_->DisableAudioOutput(); + audio_output_enabled_ = false; + } + if (video_output_enabled_) { + decklink_output_interface_->DisableVideoOutput(); + video_output_enabled_ = false; + } report_error(e.what()); } @@ -537,12 +768,27 @@ bool DecklinkOutput::stop_sdi_output(const std::string &error_message) { spdlog::info("Stopping Decklink output loop. {}", error_message); if (decklink_output_interface_) { - decklink_output_interface_->StopScheduledPlayback(0, NULL, 0); - decklink_output_interface_->DisableVideoOutput(); - decklink_output_interface_->DisableAudioOutput(); + if (scheduled_playback_started_) { + decklink_output_interface_->StopScheduledPlayback(0, NULL, 0); + scheduled_playback_started_ = false; + } + if (video_output_enabled_) { + decklink_output_interface_->DisableVideoOutput(); + video_output_enabled_ = false; + } + if (audio_output_enabled_) { + decklink_output_interface_->DisableAudioOutput(); + audio_output_enabled_ = false; + } } - mutex_.lock(); + { + std::lock_guard lk(audio_samples_cv_mutex_); + fetch_more_samples_from_xstudio_ = true; + } + audio_samples_cv_.notify_all(); + + std::unique_lock output_lock(mutex_); free(pFrameBuf); pFrameBuf = NULL; @@ -649,15 +895,17 @@ void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_vid auto tp1 = utility::clock::now(); tp = tp1; - mutex_.lock(); + std::unique_lock output_lock(mutex_); // SDK v15.3: GetBytes() moved from IDeckLinkVideoFrame to IDeckLinkVideoBuffer IDeckLinkVideoBuffer *video_buffer = nullptr; decklink_video_frame->QueryInterface(IID_IDeckLinkVideoBuffer, (void **)&video_buffer); - frames_mutex_.lock(); - media_reader::ImageBufPtr the_frame = current_frame_; - frames_mutex_.unlock(); + media_reader::ImageBufPtr the_frame; + { + std::lock_guard frame_lock(frames_mutex_); + the_frame = current_frame_; + } if (the_frame) { @@ -671,6 +919,8 @@ void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_vid // On macOS, DMA buffers may need StartAccess to map into CPU memory HRESULT sa_hr = video_buffer->StartAccess(bmdBufferAccessReadAndWrite); + IDeckLinkVideoBuffer *intermediate_video_buffer = nullptr; + bool intermediate_access_started = false; try { void *pFrame = nullptr; @@ -732,12 +982,15 @@ void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_vid pFrame, src_buf, num_pix); } else { + if (!frame_converter_) { + throw std::runtime_error( + "DeckLink video conversion interface is unavailable."); + } // here we do our own conversion from 16 bit RGBA to 12 bit RGB // TimeLogger l("RGBA16_to_12bitRGBLE"); make_intermediate_frame(); - IDeckLinkVideoBuffer *intermediate_video_buffer = nullptr; auto r = intermediate_frame_->QueryInterface( IID_IDeckLinkVideoBuffer, (void **)&intermediate_video_buffer); if (r != S_OK || !intermediate_video_buffer) { @@ -749,6 +1002,7 @@ void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_vid throw std::runtime_error( "Could not access the video frame byte buffer"); } + intermediate_access_started = true; void *pFrame2 = nullptr; @@ -760,8 +1014,6 @@ void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_vid pixel_swizzler_.copy_frame_buffer_12bit( pFrame2, src_buf, num_pix); - intermediate_video_buffer->EndAccess(bmdBufferAccessWrite); - // Now we use Decklink's conversion to convert from our intermediate 12 // bit RGB frame to the desired output format (e.g. 10 bit YUV) r = frame_converter_->ConvertFrame( @@ -782,20 +1034,28 @@ void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_vid error_count = 0; } } + if (intermediate_video_buffer) { + if (intermediate_access_started) { + intermediate_video_buffer->EndAccess(bmdBufferAccessWrite); + } + intermediate_video_buffer->Release(); + } video_buffer->EndAccess(bmdBufferAccessReadAndWrite); } } } + IDeckLinkMutableVideoFrame *mutable_video_buffer = nullptr; try { - IDeckLinkMutableVideoFrame *mutable_video_buffer = nullptr; auto r = decklink_video_frame->QueryInterface( IID_IDeckLinkMutableVideoFrame, (void **)&mutable_video_buffer); if (r != S_OK || !mutable_video_buffer) { throw std::runtime_error("Failed to get mutable video buffer"); } update_frame_metadata(mutable_video_buffer); + mutable_video_buffer->Release(); + mutable_video_buffer = nullptr; } catch (std::exception &e) { // reduce log spamming if we're getting errors on every frame @@ -807,6 +1067,9 @@ void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_vid error_count = 0; } } + if (mutable_video_buffer) { + mutable_video_buffer->Release(); + } if (video_buffer) video_buffer->Release(); @@ -816,7 +1079,6 @@ void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_vid (uiTotalFrames * frame_duration_), frame_duration_, frame_timescale_) != S_OK) { - mutex_.unlock(); running_ = false; decklink_xstudio_plugin_->stop(); report_error("Failed to schedule video frame."); @@ -829,12 +1091,16 @@ void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_vid decklink_xstudio_plugin_->start(frameWidth(), frameHeight()); } uiTotalFrames++; - mutex_.unlock(); } void DecklinkOutput::update_frame_metadata(IDeckLinkMutableVideoFrame *mutableFrame) { + HDRMetadata hdr_metadata; + { + std::lock_guard lock(hdr_metadata_mutex_); + hdr_metadata = hdr_metadata_; + } - if (hdr_metadata_.EOTF == 0) { + if (hdr_metadata.EOTF == 0) { // SDR Mode - we don't need to set any metadata, but we do need to make sure to clear // the HDR flag if it was set by a previous HDR frame mutableFrame->SetFlags(mutableFrame->GetFlags() & ~bmdFrameContainsHDRMetadata); @@ -852,38 +1118,37 @@ void DecklinkOutput::update_frame_metadata(IDeckLinkMutableVideoFrame *mutableFr "Failed to get mutable metadata extensions for HDR metadata update."); } - frameMeta->AddRef(); - frameMeta->SetInt(bmdDeckLinkFrameMetadataColorspace, hdr_metadata_.colourspace_); + frameMeta->SetInt(bmdDeckLinkFrameMetadataColorspace, hdr_metadata.colourspace_); frameMeta->SetInt( - bmdDeckLinkFrameMetadataHDRElectroOpticalTransferFunc, hdr_metadata_.EOTF); + bmdDeckLinkFrameMetadataHDRElectroOpticalTransferFunc, hdr_metadata.EOTF); frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedX, hdr_metadata_.referencePrimaries[0]); + bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedX, hdr_metadata.referencePrimaries[0]); frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedY, hdr_metadata_.referencePrimaries[1]); + bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedY, hdr_metadata.referencePrimaries[1]); frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenX, hdr_metadata_.referencePrimaries[2]); + bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenX, hdr_metadata.referencePrimaries[2]); frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenY, hdr_metadata_.referencePrimaries[3]); + bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenY, hdr_metadata.referencePrimaries[3]); frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueX, hdr_metadata_.referencePrimaries[4]); + bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueX, hdr_metadata.referencePrimaries[4]); frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueY, hdr_metadata_.referencePrimaries[5]); + bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueY, hdr_metadata.referencePrimaries[5]); frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRWhitePointX, hdr_metadata_.referencePrimaries[6]); + bmdDeckLinkFrameMetadataHDRWhitePointX, hdr_metadata.referencePrimaries[6]); frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRWhitePointY, hdr_metadata_.referencePrimaries[7]); + bmdDeckLinkFrameMetadataHDRWhitePointY, hdr_metadata.referencePrimaries[7]); frameMeta->SetFloat( bmdDeckLinkFrameMetadataHDRMaxDisplayMasteringLuminance, - hdr_metadata_.luminanceSettings[0]); + hdr_metadata.luminanceSettings[0]); frameMeta->SetFloat( bmdDeckLinkFrameMetadataHDRMinDisplayMasteringLuminance, - hdr_metadata_.luminanceSettings[1]); + hdr_metadata.luminanceSettings[1]); frameMeta->SetFloat( bmdDeckLinkFrameMetadataHDRMaximumContentLightLevel, - hdr_metadata_.luminanceSettings[2]); + hdr_metadata.luminanceSettings[2]); frameMeta->SetFloat( bmdDeckLinkFrameMetadataHDRMaximumFrameAverageLightLevel, - hdr_metadata_.luminanceSettings[3]); + hdr_metadata.luminanceSettings[3]); frameMeta->Release(); } @@ -930,7 +1195,10 @@ long DecklinkOutput::num_samples_in_buffer() { void DecklinkOutput::copy_audio_samples_to_decklink_buffer(const bool /*preroll*/) { if (!decklink_output_interface_) { - fetch_more_samples_from_xstudio_ = true; + { + std::lock_guard m(audio_samples_cv_mutex_); + fetch_more_samples_from_xstudio_ = true; + } audio_samples_cv_.notify_one(); return; } @@ -990,9 +1258,25 @@ void DecklinkOutput::copy_audio_samples_to_decklink_buffer(const bool /*preroll* //////////////////////////////////////////// AVOutputCallback::AVOutputCallback(DecklinkOutput *pOwner) { owner_ = pOwner; } -HRESULT AVOutputCallback::QueryInterface(REFIID /*iid*/, LPVOID *ppv) { +HRESULT AVOutputCallback::QueryInterface(REFIID iid, LPVOID *ppv) { + if (!ppv) { + return E_INVALIDARG; + } + *ppv = NULL; - return E_NOINTERFACE; + + const auto iid_unknown = IID_IUnknown; + + if (std::memcmp(&iid, &iid_unknown, sizeof(REFIID)) == 0) { + *ppv = static_cast(static_cast(this)); + } else if (std::memcmp(&iid, &IID_IDeckLinkVideoOutputCallback, sizeof(REFIID)) == 0) { + *ppv = static_cast(this); + } else { + return E_NOINTERFACE; + } + + AddRef(); + return S_OK; } ULONG AVOutputCallback::AddRef() { @@ -1015,16 +1299,71 @@ ULONG AVOutputCallback::Release() { HRESULT AVOutputCallback::ScheduledFrameCompleted( IDeckLinkVideoFrame *completedFrame, BMDOutputFrameCompletionResult /*result*/) { - owner_->fill_decklink_video_frame(completedFrame); - return S_OK; + try { + owner_->fill_decklink_video_frame(completedFrame); + return S_OK; + } catch (const std::exception &e) { + spdlog::error("DeckLink video callback failed: {}", e.what()); + } catch (...) { + spdlog::error("DeckLink video callback failed with an unknown exception."); + } + return E_FAIL; } HRESULT AVOutputCallback::ScheduledPlaybackHasStopped() { return S_OK; } -HRESULT AVOutputCallback::RenderAudioSamples(BOOL preroll) { +AudioOutputCallback::AudioOutputCallback(DecklinkOutput *pOwner) { owner_ = pOwner; } + +HRESULT AudioOutputCallback::QueryInterface(REFIID iid, LPVOID *ppv) { + if (!ppv) { + return E_INVALIDARG; + } + + *ppv = NULL; + + const auto iid_unknown = IID_IUnknown; + + if (std::memcmp(&iid, &iid_unknown, sizeof(REFIID)) == 0) { + *ppv = static_cast(static_cast(this)); + } else if (std::memcmp(&iid, &IID_IDeckLinkAudioOutputCallback, sizeof(REFIID)) == 0) { + *ppv = static_cast(this); + } else { + return E_NOINTERFACE; + } + + AddRef(); + return S_OK; +} + +ULONG AudioOutputCallback::AddRef() { + int oldValue; + + oldValue = ref_count_.fetchAndAddAcquire(1); + return (ULONG)(oldValue + 1); +} + +ULONG AudioOutputCallback::Release() { + int oldValue; + + oldValue = ref_count_.fetchAndAddAcquire(-1); + if (oldValue == 1) { + delete this; + } + + return (ULONG)(oldValue - 1); +} + +HRESULT AudioOutputCallback::RenderAudioSamples(BOOL preroll) { // decklink driver is calling this at regular intervals. There may be // plenty of samples in the buffer for it to render, we check that in // our own function - owner_->copy_audio_samples_to_decklink_buffer(preroll); - return S_OK; + try { + owner_->copy_audio_samples_to_decklink_buffer(preroll); + return S_OK; + } catch (const std::exception &e) { + spdlog::error("DeckLink audio callback failed: {}", e.what()); + } catch (...) { + spdlog::error("DeckLink audio callback failed with an unknown exception."); + } + return E_FAIL; } diff --git a/src/plugin/video_output/bmd_decklink/src/decklink_output.hpp b/src/plugin/video_output/bmd_decklink/src/decklink_output.hpp index 4ce9a2715..e239bfb52 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.hpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.hpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include "extern/decklink_compat.h" @@ -143,27 +144,34 @@ namespace bm_decklink_plugin_1_0 { } [[nodiscard]] bool is_available() const { return is_available_; } + [[nodiscard]] const std::string &last_error() const { return last_error_; } + [[nodiscard]] const std::string &runtime_info() const { return runtime_info_; } private: - AVOutputCallback *output_callback_; + void release_resources(); + void detect_runtime_info(); + void log_runtime_info() const; + + AVOutputCallback *video_output_callback_ = {nullptr}; + class AudioOutputCallback *audio_output_callback_ = {nullptr}; std::mutex mutex_; - GLenum glStatus; - GLuint idFrameBuf, idColorBuf, idDepthBuf; - char *pFrameBuf; + GLenum glStatus = {0}; + GLuint idFrameBuf = {0}, idColorBuf = {0}, idDepthBuf = {0}; + char *pFrameBuf = {nullptr}; // DeckLink - uint32_t frame_width_; - uint32_t frame_height_; + uint32_t frame_width_ = {0}; + uint32_t frame_height_ = {0}; - IDeckLink *decklink_interface_; - IDeckLinkOutput *decklink_output_interface_; - IDeckLinkVideoConversion *frame_converter_; + IDeckLink *decklink_interface_ = {nullptr}; + IDeckLinkOutput *decklink_output_interface_ = {nullptr}; + IDeckLinkVideoConversion *frame_converter_ = {nullptr}; - BMDTimeValue frame_duration_; - BMDTimeScale frame_timescale_; - uint32_t uiFPS; - uint32_t uiTotalFrames; + BMDTimeValue frame_duration_ = {0}; + BMDTimeScale frame_timescale_ = {0}; + uint32_t uiFPS = {0}; + uint32_t uiTotalFrames = {0}; media_reader::ImageBufPtr current_frame_; std::mutex frames_mutex_; @@ -193,15 +201,21 @@ namespace bm_decklink_plugin_1_0 { unsigned long samples_delivered_ = {0}; uint32_t samples_water_level_ = {4096}; long audio_sync_delay_milliseconds_ = {0}; + bool video_output_enabled_ = {false}; + bool audio_output_enabled_ = {false}; + bool scheduled_playback_started_ = {false}; PixelSwizzler pixel_swizzler_; HDRMetadata hdr_metadata_; std::mutex hdr_metadata_mutex_; - bool is_available_ = {false}; + bool is_available_ = {false}; + std::string last_error_ = {}; + std::string api_version_ = {}; + std::string runtime_info_ = {}; + std::string output_interface_info_ = {}; }; - class AVOutputCallback : public IDeckLinkVideoOutputCallback, - public IDeckLinkAudioOutputCallback { + class AVOutputCallback : public IDeckLinkVideoOutputCallback { private: struct RefCt { @@ -226,9 +240,6 @@ namespace bm_decklink_plugin_1_0 { ULONG AddRef() override; ULONG Release() override; - // IDeckLinkAudioOutputCallback - HRESULT RenderAudioSamples(BOOL preroll) override; - // IDeckLinkVideoOutputCallback HRESULT ScheduledFrameCompleted( IDeckLinkVideoFrame *completedFrame, @@ -236,5 +247,34 @@ namespace bm_decklink_plugin_1_0 { HRESULT ScheduledPlaybackHasStopped() override; }; + class AudioOutputCallback : public IDeckLinkAudioOutputCallback { + private: + struct RefCt { + + int fetchAndAddAcquire(const int delta) { + + std::lock_guard l(m); + int old = count; + count += delta; + return old; + } + std::atomic count = 1; + std::mutex m; + } ref_count_; + + DecklinkOutput *owner_; + + public: + AudioOutputCallback(DecklinkOutput *pOwner); + + // IUnknown + HRESULT QueryInterface(REFIID /*iid*/, LPVOID * /*ppv*/) override; + ULONG AddRef() override; + ULONG Release() override; + + // IDeckLinkAudioOutputCallback + HRESULT RenderAudioSamples(BOOL preroll) override; + }; + } // namespace bm_decklink_plugin_1_0 } // namespace xstudio diff --git a/src/plugin/video_output/bmd_decklink/src/decklink_plugin.cpp b/src/plugin/video_output/bmd_decklink/src/decklink_plugin.cpp index 7d147f4fc..bc624d996 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_plugin.cpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_plugin.cpp @@ -1,597 +1,614 @@ -// SPDX-License-Identifier: Apache-2.0 -#ifdef __APPLE__ -#include -#else -#undef WIN32_LEAN_AND_MEAN -#include -#include -#endif - -#include - -#include - -#include "decklink_audio_device.hpp" -#include "decklink_plugin.hpp" -#include "decklink_output.hpp" - -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" - -#include -#include - -#ifdef _WIN32 -#include -#endif - -using namespace xstudio; -using namespace xstudio::ui; -using namespace xstudio::bm_decklink_plugin_1_0; - -namespace { -static std::map bmd_pixel_formats( - {{"8 bit YUV", bmdFormat8BitYUV}, - {"10 bit YUV", bmdFormat10BitYUV}, - {"8 bit ARGB", bmdFormat8BitARGB}, - {"8 bit BGRA", bmdFormat8BitBGRA}, - {"10 bit RGB", bmdFormat10BitRGB}, - {"12 bit RGB", bmdFormat12BitRGB}, - {"12 bit RGB-LE", bmdFormat12BitRGBLE}, - {"10 bit RGB Video Range", bmdFormat10BitRGBX}, - {"10 bit RGB-LE Video Range", bmdFormat10BitRGBXLE}}); - -static const std::vector hdr_metadata_value_names( - {"Red x", "Red y", "Green x", "Green y", "Blue x", "Blue y", "White Pt. x", "White Pt. y"}); - -const std::vector - light_level_controls_names({"D.M. Lum. Min", "D.M. Lum. Max", "CLL Max", "Frm Avg Light"}); - -const std::map eotf_modes({ - {"SDR", 0}, - {"HDR", 1}, - {"PQ", 2}, - {"HLG", 3}, - {"Auto PQ", -1}, -}); - -const std::map colourspaces( - {{"BT. 601", bmdColorspaceRec601}, - {"BT. 709", bmdColorspaceRec709}, - {"BT. 2020", bmdColorspaceRec2020}}); - -static const std::string shelf_button_qml(R"( -import QtQuick 2.12 -import BlackmagicSDI 1.0 -DecklinkShelfButton { -} -)"); -} // namespace - -BMDecklinkPlugin::BMDecklinkPlugin( - caf::actor_config &cfg, const utility::JsonStore &init_settings) - : ui::viewport::VideoOutputPlugin(cfg, init_settings, "BMDecklinkPlugin") { - - // Here we check that the Decklink drivers are installed. If not, we log a - // message and disable the plugin by throwing an exception in the constructor - // which will prevent the plugin from being created. This prevents the SDI - // UI component from appearing in xSTUDIO and confusing the user when they - // don't have a DeckLink device or drivers installed. - try { - - DecklinkOutput::check_decklink_installation(); - - } catch (const std::exception &e) { - - send_exit(this, caf::exit_reason::user_shutdown); - spdlog::info("Blackmagic Decklink SDI output disabled: {}", e.what()); - return; - } - - // add attributes used for configuring the SDI output - pixel_formats_ = add_string_choice_attribute( - "Pixel Format", "Pix Fmt", "10 bit YUV", utility::map_key_to_vec(bmd_pixel_formats)); - pixel_formats_->expose_in_ui_attrs_group("Decklink Settings"); - pixel_formats_->set_preference_path("/plugin/decklink/pixel_format"); - - sdi_output_is_running_ = add_boolean_attribute("Enabled", "Enabled", false); - sdi_output_is_running_->expose_in_ui_attrs_group("Decklink Settings"); - - status_message_ = add_string_attribute("Status", "Status", "Not Started"); - status_message_->expose_in_ui_attrs_group("Decklink Settings"); - - is_in_error_ = add_boolean_attribute("In Error", "In Error", false); - is_in_error_->expose_in_ui_attrs_group("Decklink Settings"); - - track_main_viewport_ = add_boolean_attribute("Track Viewport", "Track Viewport", true); - track_main_viewport_->expose_in_ui_attrs_group("Decklink Settings"); - - resolutions_ = add_string_choice_attribute("Output Resolution", "Out. Res."); - - resolutions_->expose_in_ui_attrs_group("Decklink Settings"); - resolutions_->set_preference_path("/plugin/decklink/output_resolution"); - - frame_rates_ = add_string_choice_attribute("Refresh Rate", "Hz"); - frame_rates_->expose_in_ui_attrs_group("Decklink Settings"); - frame_rates_->set_preference_path("/plugin/decklink/output_frame_rate"); - - start_stop_ = add_boolean_attribute("Start Stop", "Start Stop", false); - start_stop_->expose_in_ui_attrs_group("Decklink Settings"); - - auto_start_ = add_boolean_attribute("Auto Start", "Auto Start", false); - auto_start_->set_preference_path("/plugin/decklink/auto_start_sdi"); - - samples_water_level_ = - add_integer_attribute("Audio Samples Water Level", "Audio Samples Water Level", 4096); - samples_water_level_->set_preference_path("/plugin/decklink/audio_samps_water_level"); - - // This attr is currently not exposed to the user. It can be used to delay (or advance) the - // image that is sent to the SDI output vs. the main xSTUDIO viewport in order to exactly - // sync up the image that shows in xSTUDIO UI vs. the SDI display. This helps if you have a - // screening room with xSTUDIO UI on a monitor and the SDI output shown by a projector at - // the front of the room ... the person operating xSTUDIO may see the image on their screen - // is a few frames ahead of the SDI. However, for this to work we have to buffer a lot more - // frames in the playhead and this is expensive which is why I'm leaving it here but not - // exposing it. - video_pipeline_delay_milliseconds_ = - add_integer_attribute("Video Sync Delay", "Video Sync Delay", 0); - video_pipeline_delay_milliseconds_->set_preference_path( - "/plugin/decklink/video_sync_delay"); - video_pipeline_delay_milliseconds_->expose_in_ui_attrs_group("Decklink Settings"); - - audio_sync_delay_milliseconds_ = - add_integer_attribute("Audio Sync Delay", "Audio Sync Delay", 0); - audio_sync_delay_milliseconds_->set_preference_path( - "/plugin/decklink/new_audio_sync_delay"); - audio_sync_delay_milliseconds_->expose_in_ui_attrs_group("Decklink Settings"); - - disable_pc_audio_when_running_ = - add_boolean_attribute("Auto Disable PC Audio", "Auto Disable PC Audio", false); - disable_pc_audio_when_running_->set_preference_path( - "/plugin/decklink/disable_pc_audio_when_sdi_is_running"); - disable_pc_audio_when_running_->expose_in_ui_attrs_group("Decklink Settings"); - - make_hdr_attributes(); - - VideoOutputPlugin::finalise(); -} - -// This method is called when a new image buffer is ready to be displayed -void BMDecklinkPlugin::incoming_video_frame_callback(media_reader::ImageBufPtr incoming) { - dcl_output_->incoming_frame(incoming); -} - -void BMDecklinkPlugin::exit_cleanup() { - // the dcl_output_ has caf::actor handles pointing to this (BMDecklinkPlugin) - // instance. The BMDecklinkPlugin will therefore never get deleted due to - // circular dependency so we use the on_exit - delete dcl_output_; -} - -void BMDecklinkPlugin::receive_status_callback(const utility::JsonStore &status_data) { - - if (status_data.contains("status_message") && status_data["status_message"].is_string()) { - status_message_->set_value(status_data["status_message"].get()); - } - if (status_data.contains("sdi_output_is_active") && - status_data["sdi_output_is_active"].is_boolean()) { - sdi_output_is_running_->set_value(status_data["sdi_output_is_active"].get()); - } - if (status_data.contains("error_state") && status_data["error_state"].is_boolean()) { - is_in_error_->set_value(status_data["error_state"].get()); - } -} - -void BMDecklinkPlugin::attribute_changed(const utility::Uuid &attribute_uuid, const int role) { - - if (dcl_output_) { - - if (resolutions_ && attribute_uuid == resolutions_->uuid() && - role == module::Attribute::Value) { - - const auto rates = dcl_output_->get_available_refresh_rates(resolutions_->value()); - frame_rates_->set_role_data(module::Attribute::StringChoices, rates); - - // pick a sensible refresh rate, if the current rate isn't available for new - // resolution - auto i = std::find( - rates.begin(), rates.end(), frame_rates_->value()); // prefer current rate - if (i == rates.end()) { - i = std::find(rates.begin(), rates.end(), "24.0"); // otherwise prefer 24.0 - if (i == rates.end()) { - i = std::find(rates.begin(), rates.end(), "60.0"); // use 60.0 otherwise - if (i == rates.end()) { - i = rates.begin(); - } - } - } - if (i != rates.end()) { - frame_rates_->set_value(*i); - } - - } else if (attribute_uuid == start_stop_->uuid()) { - - dcl_output_->StartStop(); - } - - if (attribute_uuid == pixel_formats_->uuid() || - attribute_uuid == resolutions_->uuid() || attribute_uuid == frame_rates_->uuid()) { - - try { - - if (bmd_pixel_formats.find(pixel_formats_->value()) == - bmd_pixel_formats.end()) { - throw std::runtime_error( - fmt::format("Invalid pixel format: {}", pixel_formats_->value())); - } - - const BMDPixelFormat pix_fmt = bmd_pixel_formats[pixel_formats_->value()]; - - - dcl_output_->set_display_mode( - resolutions_->value(), frame_rates_->value(), pix_fmt); - - } catch (std::exception &e) { - - status_message_->set_value(e.what()); - is_in_error_->set_value(true); - } - - } else if (attribute_uuid == track_main_viewport_->uuid()) { - - sync_geometry_to_main_viewport(track_main_viewport_->value()); - - } else if (attribute_uuid == samples_water_level_->uuid()) { - dcl_output_->set_audio_samples_water_level(samples_water_level_->value()); - } else if (attribute_uuid == audio_sync_delay_milliseconds_->uuid()) { - dcl_output_->set_audio_sync_delay_milliseconds( - audio_sync_delay_milliseconds_->value()); - } else if (attribute_uuid == video_pipeline_delay_milliseconds_->uuid()) { - video_delay_milliseconds(video_pipeline_delay_milliseconds_->value()); - } else if (attribute_uuid == disable_pc_audio_when_running_->uuid()) { - set_pc_audio_muting(); - } else if (attribute_uuid == sdi_output_is_running_->uuid()) { - set_pc_audio_muting(); - } else if (attribute_uuid == hdr_presets_->uuid()) { - - if (hdr_presets_data_.contains(hdr_presets_->value())) { - - const auto &vals = hdr_presets_data_[hdr_presets_->value()]; - if (vals.is_array() && vals.size() == 8) { - int idx = 0; - for (auto &setting_attr : hdr_metadata_settings_) { - setting_attr->set_value(vals[idx++].get()); - } - } - } - - } else if ( - role == module::Attribute::Value && - hdr_metadata_settings_uuids_.find(attribute_uuid) != - hdr_metadata_settings_uuids_.end()) { - set_hdr_mode_and_metadata(); - if (!prefs_save_scheduled_) { - prefs_save_scheduled_ = true; - delayed_anon_send( - caf::actor_cast(this), - std::chrono::seconds(5), - "save_hdr_colour_prefs"); - } - } - } - StandardPlugin::attribute_changed(attribute_uuid, role); -} - -audio::AudioOutputDevice * -BMDecklinkPlugin::make_audio_output_device(const utility::JsonStore &prefs) { - if (!dcl_output_ || !dcl_output_->is_available()) { - return nullptr; - } - return static_cast( - new DecklinkAudioOutputDevice(prefs, dcl_output_)); -} - -void BMDecklinkPlugin::initialise() { - - try { - - dcl_output_ = new DecklinkOutput(this); - set_hdr_mode_and_metadata(); - - if (!dcl_output_->is_available()) { - status_message_->set_value("No DeckLink device detected."); +// SPDX-License-Identifier: Apache-2.0 +#ifdef __APPLE__ +#include +#else +#undef WIN32_LEAN_AND_MEAN +#include +#include +#endif + +#include + +#include + +#include "decklink_audio_device.hpp" +#include "decklink_plugin.hpp" +#include "decklink_output.hpp" + +#include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/logging.hpp" + +#include +#include + +#ifdef _WIN32 +#include +#endif + +using namespace xstudio; +using namespace xstudio::ui; +using namespace xstudio::bm_decklink_plugin_1_0; + +namespace { +static std::map bmd_pixel_formats( + {{"8 bit YUV", bmdFormat8BitYUV}, + {"10 bit YUV", bmdFormat10BitYUV}, + {"8 bit ARGB", bmdFormat8BitARGB}, + {"8 bit BGRA", bmdFormat8BitBGRA}, + {"10 bit RGB", bmdFormat10BitRGB}, + {"12 bit RGB", bmdFormat12BitRGB}, + {"12 bit RGB-LE", bmdFormat12BitRGBLE}, + {"10 bit RGB Video Range", bmdFormat10BitRGBX}, + {"10 bit RGB-LE Video Range", bmdFormat10BitRGBXLE}}); + +static const std::vector hdr_metadata_value_names( + {"Red x", "Red y", "Green x", "Green y", "Blue x", "Blue y", "White Pt. x", "White Pt. y"}); + +const std::vector + light_level_controls_names({"D.M. Lum. Min", "D.M. Lum. Max", "CLL Max", "Frm Avg Light"}); + +const std::map eotf_modes({ + {"SDR", 0}, + {"HDR", 1}, + {"PQ", 2}, + {"HLG", 3}, + {"Auto PQ", -1}, +}); + +const std::map colourspaces( + {{"BT. 601", bmdColorspaceRec601}, + {"BT. 709", bmdColorspaceRec709}, + {"BT. 2020", bmdColorspaceRec2020}}); + +static const std::string shelf_button_qml(R"( +import QtQuick 2.12 +import BlackmagicSDI 1.0 +DecklinkShelfButton { +} +)"); +} // namespace + +BMDecklinkPlugin::BMDecklinkPlugin( + caf::actor_config &cfg, const utility::JsonStore &init_settings) + : ui::viewport::VideoOutputPlugin(cfg, init_settings, "BMDecklinkPlugin") { + + // Here we check that the Decklink drivers are installed. If not, we log a + // message and disable the plugin by throwing an exception in the constructor + // which will prevent the plugin from being created. This prevents the SDI + // UI component from appearing in xSTUDIO and confusing the user when they + // don't have a DeckLink device or drivers installed. + try { + + DecklinkOutput::check_decklink_installation(); + + } catch (const std::exception &e) { + + send_exit(this, caf::exit_reason::user_shutdown); + spdlog::info("Blackmagic Decklink SDI output disabled: {}", e.what()); + return; + } + + // add attributes used for configuring the SDI output + pixel_formats_ = add_string_choice_attribute( + "Pixel Format", "Pix Fmt", "10 bit YUV", utility::map_key_to_vec(bmd_pixel_formats)); + pixel_formats_->expose_in_ui_attrs_group("Decklink Settings"); + pixel_formats_->set_preference_path("/plugin/decklink/pixel_format"); + + sdi_output_is_running_ = add_boolean_attribute("Enabled", "Enabled", false); + sdi_output_is_running_->expose_in_ui_attrs_group("Decklink Settings"); + + status_message_ = add_string_attribute("Status", "Status", "Not Started"); + status_message_->expose_in_ui_attrs_group("Decklink Settings"); + + is_in_error_ = add_boolean_attribute("In Error", "In Error", false); + is_in_error_->expose_in_ui_attrs_group("Decklink Settings"); + + track_main_viewport_ = add_boolean_attribute("Track Viewport", "Track Viewport", true); + track_main_viewport_->expose_in_ui_attrs_group("Decklink Settings"); + + resolutions_ = add_string_choice_attribute("Output Resolution", "Out. Res."); + + resolutions_->expose_in_ui_attrs_group("Decklink Settings"); + resolutions_->set_preference_path("/plugin/decklink/output_resolution"); + + frame_rates_ = add_string_choice_attribute("Refresh Rate", "Hz"); + frame_rates_->expose_in_ui_attrs_group("Decklink Settings"); + frame_rates_->set_preference_path("/plugin/decklink/output_frame_rate"); + + start_stop_ = add_boolean_attribute("Start Stop", "Start Stop", false); + start_stop_->expose_in_ui_attrs_group("Decklink Settings"); + + auto_start_ = add_boolean_attribute("Auto Start", "Auto Start", false); + auto_start_->set_preference_path("/plugin/decklink/auto_start_sdi"); + + samples_water_level_ = + add_integer_attribute("Audio Samples Water Level", "Audio Samples Water Level", 4096); + samples_water_level_->set_preference_path("/plugin/decklink/audio_samps_water_level"); + + // This attr is currently not exposed to the user. It can be used to delay (or advance) the + // image that is sent to the SDI output vs. the main xSTUDIO viewport in order to exactly + // sync up the image that shows in xSTUDIO UI vs. the SDI display. This helps if you have a + // screening room with xSTUDIO UI on a monitor and the SDI output shown by a projector at + // the front of the room ... the person operating xSTUDIO may see the image on their screen + // is a few frames ahead of the SDI. However, for this to work we have to buffer a lot more + // frames in the playhead and this is expensive which is why I'm leaving it here but not + // exposing it. + video_pipeline_delay_milliseconds_ = + add_integer_attribute("Video Sync Delay", "Video Sync Delay", 0); + video_pipeline_delay_milliseconds_->set_preference_path( + "/plugin/decklink/video_sync_delay"); + video_pipeline_delay_milliseconds_->expose_in_ui_attrs_group("Decklink Settings"); + + audio_sync_delay_milliseconds_ = + add_integer_attribute("Audio Sync Delay", "Audio Sync Delay", 0); + audio_sync_delay_milliseconds_->set_preference_path( + "/plugin/decklink/new_audio_sync_delay"); + audio_sync_delay_milliseconds_->expose_in_ui_attrs_group("Decklink Settings"); + + disable_pc_audio_when_running_ = + add_boolean_attribute("Auto Disable PC Audio", "Auto Disable PC Audio", false); + disable_pc_audio_when_running_->set_preference_path( + "/plugin/decklink/disable_pc_audio_when_sdi_is_running"); + disable_pc_audio_when_running_->expose_in_ui_attrs_group("Decklink Settings"); + + make_hdr_attributes(); + + VideoOutputPlugin::finalise(); +} + +// This method is called when a new image buffer is ready to be displayed +void BMDecklinkPlugin::incoming_video_frame_callback(media_reader::ImageBufPtr incoming) { + if (dcl_output_ && dcl_output_->is_available()) { + dcl_output_->incoming_frame(incoming); + } +} + +void BMDecklinkPlugin::exit_cleanup() { + // the dcl_output_ has caf::actor handles pointing to this (BMDecklinkPlugin) + // instance. The BMDecklinkPlugin will therefore never get deleted due to + // circular dependency so we use the on_exit + delete dcl_output_; + dcl_output_ = nullptr; +} + +void BMDecklinkPlugin::receive_status_callback(const utility::JsonStore &status_data) { + + if (status_data.contains("status_message") && status_data["status_message"].is_string()) { + status_message_->set_value(status_data["status_message"].get()); + } + if (status_data.contains("sdi_output_is_active") && + status_data["sdi_output_is_active"].is_boolean()) { + sdi_output_is_running_->set_value(status_data["sdi_output_is_active"].get()); + } + if (status_data.contains("error_state") && status_data["error_state"].is_boolean()) { + is_in_error_->set_value(status_data["error_state"].get()); + } +} + +void BMDecklinkPlugin::attribute_changed(const utility::Uuid &attribute_uuid, const int role) { + + if (dcl_output_) { + + if (resolutions_ && attribute_uuid == resolutions_->uuid() && + role == module::Attribute::Value) { + + const auto rates = dcl_output_->get_available_refresh_rates(resolutions_->value()); + frame_rates_->set_role_data(module::Attribute::StringChoices, rates); + + // pick a sensible refresh rate, if the current rate isn't available for new + // resolution + auto i = std::find( + rates.begin(), rates.end(), frame_rates_->value()); // prefer current rate + if (i == rates.end()) { + i = std::find(rates.begin(), rates.end(), "24.0"); // otherwise prefer 24.0 + if (i == rates.end()) { + i = std::find(rates.begin(), rates.end(), "60.0"); // use 60.0 otherwise + if (i == rates.end()) { + i = rates.begin(); + } + } + } + if (i != rates.end()) { + frame_rates_->set_value(*i); + } + + } else if (attribute_uuid == start_stop_->uuid()) { + + dcl_output_->StartStop(); + } + + if (attribute_uuid == pixel_formats_->uuid() || + attribute_uuid == resolutions_->uuid() || attribute_uuid == frame_rates_->uuid()) { + + try { + + if (bmd_pixel_formats.find(pixel_formats_->value()) == + bmd_pixel_formats.end()) { + throw std::runtime_error( + fmt::format("Invalid pixel format: {}", pixel_formats_->value())); + } + + const BMDPixelFormat pix_fmt = bmd_pixel_formats[pixel_formats_->value()]; + + + dcl_output_->set_display_mode( + resolutions_->value(), frame_rates_->value(), pix_fmt); + + } catch (std::exception &e) { + + status_message_->set_value(e.what()); + is_in_error_->set_value(true); + } + + } else if (attribute_uuid == track_main_viewport_->uuid()) { + + sync_geometry_to_main_viewport(track_main_viewport_->value()); + + } else if (attribute_uuid == samples_water_level_->uuid()) { + dcl_output_->set_audio_samples_water_level(samples_water_level_->value()); + } else if (attribute_uuid == audio_sync_delay_milliseconds_->uuid()) { + dcl_output_->set_audio_sync_delay_milliseconds( + audio_sync_delay_milliseconds_->value()); + } else if (attribute_uuid == video_pipeline_delay_milliseconds_->uuid()) { + video_delay_milliseconds(video_pipeline_delay_milliseconds_->value()); + } else if (attribute_uuid == disable_pc_audio_when_running_->uuid()) { + set_pc_audio_muting(); + } else if (attribute_uuid == sdi_output_is_running_->uuid()) { + set_pc_audio_muting(); + } else if (attribute_uuid == hdr_presets_->uuid()) { + + if (hdr_presets_data_.contains(hdr_presets_->value())) { + + const auto &vals = hdr_presets_data_[hdr_presets_->value()]; + if (vals.is_array() && vals.size() == 8) { + int idx = 0; + for (auto &setting_attr : hdr_metadata_settings_) { + setting_attr->set_value(vals[idx++].get()); + } + } + } + + } else if ( + role == module::Attribute::Value && + hdr_metadata_settings_uuids_.find(attribute_uuid) != + hdr_metadata_settings_uuids_.end()) { + set_hdr_mode_and_metadata(); + if (!prefs_save_scheduled_) { + prefs_save_scheduled_ = true; + delayed_anon_send( + caf::actor_cast(this), + std::chrono::seconds(5), + "save_hdr_colour_prefs"); + } + } + } + StandardPlugin::attribute_changed(attribute_uuid, role); +} + +audio::AudioOutputDevice * +BMDecklinkPlugin::make_audio_output_device(const utility::JsonStore &prefs) { + if (!dcl_output_ || !dcl_output_->is_available()) { + return nullptr; + } + return static_cast( + new DecklinkAudioOutputDevice(prefs, dcl_output_)); +} + +void BMDecklinkPlugin::initialise() { + + try { + + dcl_output_ = new DecklinkOutput(this); + + if (!dcl_output_->is_available()) { + const auto decklink_error = + dcl_output_->last_error().empty() ? "No DeckLink device detected." + : dcl_output_->last_error(); + delete dcl_output_; + dcl_output_ = nullptr; + status_message_->set_value( + decklink_error); is_in_error_->set_value(true); - spdlog::warn("Decklink drivers found, but no DeckLink device is available."); + spdlog::warn("Decklink output unavailable."); return; } - - resolutions_->set_role_data( - module::Attribute::StringChoices, dcl_output_->output_resolution_names()); - - dcl_output_->set_audio_samples_water_level(samples_water_level_->value()); - dcl_output_->set_audio_sync_delay_milliseconds(audio_sync_delay_milliseconds_->value()); - - spdlog::info("Decklink Card Initialised"); - - // We register the UI here - register_viewport_dockable_widget( - "SDI Output Controls 2", - shelf_button_qml, // QML for a custom shelf button to activate the tool - "Show/Hide SDI Output Controls", // tooltip for the button, - 10.0f, // button position in the buttons bar - true, - "", // no dockable left/right widget - // qml code to create the top/bottom dockable widget - R"( - import QtQuick - import BlackmagicSDI 1.0 - DecklinkSettingsHorizontalWidget { - } - )"); - - // now we are set-up we can kick ourselves to fill in the refresh rate list etc. - attribute_changed(resolutions_->uuid(), module::Attribute::Value); - - if (auto_start_->value()) { - // start output immediately if auto_start_ is enabled (via prefs) - dcl_output_->StartStop(); - } - - sync_geometry_to_main_viewport(track_main_viewport_->value()); - - video_delay_milliseconds(video_pipeline_delay_milliseconds_->value()); - - // tell our viewport what sort of display we are. This info is used by - // the colour management system to try and pick an appropriate display - // transform - display_info("SDI Video Output", "Decklink", "Blackmagic Design", ""); - - // Here we add the Display attribute from the colour pipeline and the - // Fit mode attr from the offscreen viewport to an attribute group - // called "decklink viewport attrs" which is exposed in the UI. - // Our UI bar references this attribute group to make the dropdowns to - // control those two attributes. - anon_mail( - module::change_attribute_request_atom_v, - "Display", - int(module::Attribute::UIDataModels), - utility::JsonStore( - nlohmann::json::parse(R"(["decklink viewport attrs", "decklink ocio"])"))) - .send(colour_pipeline()); - - anon_mail( - module::change_attribute_request_atom_v, - "Fit (F)", - int(module::Attribute::UIDataModels), - utility::JsonStore(nlohmann::json::parse(R"(["decklink viewport attrs"])"))) - .send(offscreen_viewport()); - - } catch (std::exception &e) { - spdlog::critical("{} {}", __PRETTY_FUNCTION__, e.what()); - } -} - -void BMDecklinkPlugin::set_pc_audio_muting() { - - // we can get access to the - auto pc_audio_output_actor = - system().registry().template get(pc_audio_output_registry); - - if (pc_audio_output_actor) { - const bool mute = - disable_pc_audio_when_running_->value() && sdi_output_is_running_->value(); - anon_send( - pc_audio_output_actor, audio::set_override_volume_atom_v, mute ? 0.0f : -1.0f); - } -} - -void BMDecklinkPlugin::make_hdr_attributes() { - - // Add the HDR mode attribute - hdr_mode_ = add_string_choice_attribute( - "HDR Mode", "HDR Mode", "SDR", utility::map_key_to_vec(eotf_modes)); - hdr_mode_->expose_in_ui_attrs_group("Decklink HDR Settings"); - hdr_mode_->set_preference_path("/plugin/decklink/hdr_mode"); - hdr_metadata_settings_uuids_.insert(hdr_mode_->uuid()); - - // Add the Colourspace mode attribute - colourspace_ = add_string_choice_attribute( - "Colour Space", "Colour Space", "BT. 709", utility::map_key_to_vec(colourspaces)); - colourspace_->expose_in_ui_attrs_group("Decklink HDR Settings"); - colourspace_->set_preference_path("/plugin/decklink/colourspace"); - hdr_metadata_settings_uuids_.insert(colourspace_->uuid()); - - // Fetch the ocio display name match string for auto HDR mode switching - auto prefs = global_store::GlobalStoreHelper(system()); - try { - ocio_display_hdr_match_string_ = utility::to_lower( - prefs.value("/plugin/decklink/ocio_display_hdr_match_string")); - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - std::array colour_settings({0.64, 0.33, 0.30, 0.6, 0.15, 0.06, 0.3127, 0.329}); - try { - auto colour_vals = - prefs.value("/plugin/decklink/hdr_colour_metadata_values"); - if (colour_vals.is_array() && colour_vals.size() == 8) { - int idx = 0; - for (auto &o : colour_vals) { - if (!o.is_number()) { - throw std::runtime_error( - "hdr_colour_metadata_values should be 8 float values."); - } - colour_settings[idx++] = o.get(); - } - } else { - throw std::runtime_error( - "hdr_colour_metadata_values should be an array of 8 float values."); - } - } catch (std::exception &e) { - spdlog::warn( - "{}: Failed to load perference /plugin/decklink/hdr_colour_metadata_values: {}", - __PRETTY_FUNCTION__, - e.what()); - } - - int idx = 0; - for (const auto &o : hdr_metadata_value_names) { - hdr_metadata_settings_.push_back(add_float_attribute(o, o, colour_settings[idx++])); - hdr_metadata_settings_.back()->expose_in_ui_attrs_group("Decklink HDR Values"); - hdr_metadata_settings_uuids_.insert(hdr_metadata_settings_.back()->uuid()); - } - - - std::array lum_settings({0.0005, 1000.0, 1000.0, 400.0}); - try { - auto lum__vals = - prefs.value("/plugin/decklink/hdr_luminance_metadata_values"); - if (lum__vals.is_array() && lum__vals.size() == 4) { - int idx = 0; - for (auto &o : lum__vals) { - if (!o.is_number()) { - throw std::runtime_error( - "hdr_luminance_metadata_values should be 4 float values."); - } - lum_settings[idx++] = o.get(); - } - } else { - throw std::runtime_error( - "hdr_luminance_metadata_values should be an array of 4 float values."); - } - } catch (std::exception &e) { - spdlog::warn( - "{}: Failed to load perference /plugin/decklink/hdr_luminance_metadata_values: {}", - __PRETTY_FUNCTION__, - e.what()); - } - - idx = 0; - for (const auto &o : light_level_controls_names) { - hdr_metadata_lightlevel_.push_back(add_float_attribute(o, o, lum_settings[idx++])); - hdr_metadata_lightlevel_.back()->expose_in_ui_attrs_group("Decklink HDR Values"); - hdr_metadata_settings_uuids_.insert(hdr_metadata_lightlevel_.back()->uuid()); - } - - try { - - hdr_presets_ = add_string_choice_attribute("HDR Presets", "HDR Presets", "", {}); - hdr_presets_->expose_in_ui_attrs_group("Decklink HDR Settings"); - - hdr_presets_data_ = prefs.value("/plugin/decklink/hdr_presets"); - std::vector presets_names; - - if (hdr_presets_data_.is_object()) { - for (auto it = hdr_presets_data_.begin(); it != hdr_presets_data_.end(); it++) { - presets_names.push_back(it.key()); - } - hdr_presets_->set_role_data(module::Attribute::StringChoices, presets_names); - } else { - throw std::runtime_error("hdr_presets should a json dictionary."); - } - - } catch (std::exception &e) { - spdlog::warn( - "{}: Failed to load perference /plugin/decklink/hdr_presets: {}", - __PRETTY_FUNCTION__, - e.what()); - } -} - -void BMDecklinkPlugin::set_hdr_mode_and_metadata() { - - if (!dcl_output_) - return; - - HDRMetadata metadata; - auto p = eotf_modes.find(hdr_mode_->value()); - metadata.EOTF = p != eotf_modes.end() ? p->second : 0; - - if (metadata.EOTF == -1) { - // auto turn on HDR mode if the ocio display includes the string - // 'ocio_display_hdr_match_string_' which was set from our preferences. So for example, - // if ocio_display_hdr_match_string_ is 'hdr' then if the ocio display is 'Screening - // Room (HDR)' then HDR output is turned on - if (utility::to_lower(get_ocio_display_name()).find(ocio_display_hdr_match_string_) != - std::string::npos) { - metadata.EOTF = 2; // PQ (Perceptual Quantization or something) - } else { - metadata.EOTF = 0; - } - } - - auto q = colourspaces.find(colourspace_->value()); - metadata.colourspace_ = q != colourspaces.end() ? q->second : bmdColorspaceRec709; - - for (size_t i = 0; i < hdr_metadata_settings_.size(); ++i) { - metadata.referencePrimaries[i] = hdr_metadata_settings_[i]->value(); - } - - for (size_t i = 0; i < hdr_metadata_lightlevel_.size(); ++i) { - metadata.luminanceSettings[i] = hdr_metadata_lightlevel_[i]->value(); - } - - dcl_output_->set_hdr_metadata(metadata); -} - -void BMDecklinkPlugin::save_hdr_colour_prefs() { - - utility::JsonStore cvals(nlohmann::json::parse("[]")); - utility::JsonStore lvals(nlohmann::json::parse("[]")); - for (size_t i = 0; i < hdr_metadata_settings_.size(); ++i) { - cvals.push_back(hdr_metadata_settings_[i]->value()); - } - - for (size_t i = 0; i < hdr_metadata_lightlevel_.size(); ++i) { - lvals.push_back(hdr_metadata_lightlevel_[i]->value()); - } - - // Fetch the ocio display name match string for auto HDR mode switching - auto prefs = global_store::GlobalStoreHelper(system()); - try { - prefs.set_value(cvals, "/plugin/decklink/hdr_colour_metadata_values"); - prefs.set_value(lvals, "/plugin/decklink/hdr_luminance_metadata_values"); - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - prefs_save_scheduled_ = false; -} - -std::string BMDecklinkPlugin::get_ocio_display_name() { - - if (!colour_pipeline()) - return std::string(); - - try { - - scoped_actor sys{system()}; - - auto ocio_display = utility::request_receive( - *sys, colour_pipeline(), module::attribute_value_atom_v, std::string("Display")); - - if (ocio_display.is_string()) { - return ocio_display.get(); - } - - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - return std::string(); -} - -BMDecklinkPlugin::~BMDecklinkPlugin() {} - -XSTUDIO_PLUGIN_DECLARE_BEGIN() - -XSTUDIO_REGISTER_PLUGIN( - BMDecklinkPlugin, - BMDecklinkPlugin::PLUGIN_UUID, - BlackMagic Decklink Viewport, - plugin_manager::PluginFlags::PF_VIDEO_OUTPUT, - false, - Ted Waine, - BlackMagic Decklink SDI Output Plugin, - 1.0.0) - -XSTUDIO_PLUGIN_DECLARE_END() + + set_hdr_mode_and_metadata(); + + if (!dcl_output_->is_available()) { + status_message_->set_value("No DeckLink device detected."); + is_in_error_->set_value(true); + spdlog::warn("Decklink drivers found, but no DeckLink device is available."); + return; + } + + resolutions_->set_role_data( + module::Attribute::StringChoices, dcl_output_->output_resolution_names()); + + dcl_output_->set_audio_samples_water_level(samples_water_level_->value()); + dcl_output_->set_audio_sync_delay_milliseconds(audio_sync_delay_milliseconds_->value()); + + spdlog::info("Decklink Card Initialised"); + + // We register the UI here + register_viewport_dockable_widget( + "SDI Output Controls 2", + shelf_button_qml, // QML for a custom shelf button to activate the tool + "Show/Hide SDI Output Controls", // tooltip for the button, + 10.0f, // button position in the buttons bar + true, + "", // no dockable left/right widget + // qml code to create the top/bottom dockable widget + R"( + import QtQuick + import BlackmagicSDI 1.0 + DecklinkSettingsHorizontalWidget { + } + )"); + + // now we are set-up we can kick ourselves to fill in the refresh rate list etc. + attribute_changed(resolutions_->uuid(), module::Attribute::Value); + + if (auto_start_->value()) { + // start output immediately if auto_start_ is enabled (via prefs) + dcl_output_->StartStop(); + } + + sync_geometry_to_main_viewport(track_main_viewport_->value()); + + video_delay_milliseconds(video_pipeline_delay_milliseconds_->value()); + + // tell our viewport what sort of display we are. This info is used by + // the colour management system to try and pick an appropriate display + // transform + display_info("SDI Video Output", "Decklink", "Blackmagic Design", ""); + + // Here we add the Display attribute from the colour pipeline and the + // Fit mode attr from the offscreen viewport to an attribute group + // called "decklink viewport attrs" which is exposed in the UI. + // Our UI bar references this attribute group to make the dropdowns to + // control those two attributes. + anon_mail( + module::change_attribute_request_atom_v, + "Display", + int(module::Attribute::UIDataModels), + utility::JsonStore( + nlohmann::json::parse(R"(["decklink viewport attrs", "decklink ocio"])"))) + .send(colour_pipeline()); + + anon_mail( + module::change_attribute_request_atom_v, + "Fit (F)", + int(module::Attribute::UIDataModels), + utility::JsonStore(nlohmann::json::parse(R"(["decklink viewport attrs"])"))) + .send(offscreen_viewport()); + + } catch (std::exception &e) { + spdlog::critical("{} {}", __PRETTY_FUNCTION__, e.what()); + } +} + +void BMDecklinkPlugin::set_pc_audio_muting() { + + // we can get access to the + auto pc_audio_output_actor = + system().registry().template get(pc_audio_output_registry); + + if (pc_audio_output_actor) { + const bool mute = + disable_pc_audio_when_running_->value() && sdi_output_is_running_->value(); + anon_send( + pc_audio_output_actor, audio::set_override_volume_atom_v, mute ? 0.0f : -1.0f); + } +} + +void BMDecklinkPlugin::make_hdr_attributes() { + + // Add the HDR mode attribute + hdr_mode_ = add_string_choice_attribute( + "HDR Mode", "HDR Mode", "SDR", utility::map_key_to_vec(eotf_modes)); + hdr_mode_->expose_in_ui_attrs_group("Decklink HDR Settings"); + hdr_mode_->set_preference_path("/plugin/decklink/hdr_mode"); + hdr_metadata_settings_uuids_.insert(hdr_mode_->uuid()); + + // Add the Colourspace mode attribute + colourspace_ = add_string_choice_attribute( + "Colour Space", "Colour Space", "BT. 709", utility::map_key_to_vec(colourspaces)); + colourspace_->expose_in_ui_attrs_group("Decklink HDR Settings"); + colourspace_->set_preference_path("/plugin/decklink/colourspace"); + hdr_metadata_settings_uuids_.insert(colourspace_->uuid()); + + // Fetch the ocio display name match string for auto HDR mode switching + auto prefs = global_store::GlobalStoreHelper(system()); + try { + ocio_display_hdr_match_string_ = utility::to_lower( + prefs.value("/plugin/decklink/ocio_display_hdr_match_string")); + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + + std::array colour_settings({0.64, 0.33, 0.30, 0.6, 0.15, 0.06, 0.3127, 0.329}); + try { + auto colour_vals = + prefs.value("/plugin/decklink/hdr_colour_metadata_values"); + if (colour_vals.is_array() && colour_vals.size() == 8) { + int idx = 0; + for (auto &o : colour_vals) { + if (!o.is_number()) { + throw std::runtime_error( + "hdr_colour_metadata_values should be 8 float values."); + } + colour_settings[idx++] = o.get(); + } + } else { + throw std::runtime_error( + "hdr_colour_metadata_values should be an array of 8 float values."); + } + } catch (std::exception &e) { + spdlog::warn( + "{}: Failed to load perference /plugin/decklink/hdr_colour_metadata_values: {}", + __PRETTY_FUNCTION__, + e.what()); + } + + int idx = 0; + for (const auto &o : hdr_metadata_value_names) { + hdr_metadata_settings_.push_back(add_float_attribute(o, o, colour_settings[idx++])); + hdr_metadata_settings_.back()->expose_in_ui_attrs_group("Decklink HDR Values"); + hdr_metadata_settings_uuids_.insert(hdr_metadata_settings_.back()->uuid()); + } + + + std::array lum_settings({0.0005, 1000.0, 1000.0, 400.0}); + try { + auto lum__vals = + prefs.value("/plugin/decklink/hdr_luminance_metadata_values"); + if (lum__vals.is_array() && lum__vals.size() == 4) { + int idx = 0; + for (auto &o : lum__vals) { + if (!o.is_number()) { + throw std::runtime_error( + "hdr_luminance_metadata_values should be 4 float values."); + } + lum_settings[idx++] = o.get(); + } + } else { + throw std::runtime_error( + "hdr_luminance_metadata_values should be an array of 4 float values."); + } + } catch (std::exception &e) { + spdlog::warn( + "{}: Failed to load perference /plugin/decklink/hdr_luminance_metadata_values: {}", + __PRETTY_FUNCTION__, + e.what()); + } + + idx = 0; + for (const auto &o : light_level_controls_names) { + hdr_metadata_lightlevel_.push_back(add_float_attribute(o, o, lum_settings[idx++])); + hdr_metadata_lightlevel_.back()->expose_in_ui_attrs_group("Decklink HDR Values"); + hdr_metadata_settings_uuids_.insert(hdr_metadata_lightlevel_.back()->uuid()); + } + + try { + + hdr_presets_ = add_string_choice_attribute("HDR Presets", "HDR Presets", "", {}); + hdr_presets_->expose_in_ui_attrs_group("Decklink HDR Settings"); + + hdr_presets_data_ = prefs.value("/plugin/decklink/hdr_presets"); + std::vector presets_names; + + if (hdr_presets_data_.is_object()) { + for (auto it = hdr_presets_data_.begin(); it != hdr_presets_data_.end(); it++) { + presets_names.push_back(it.key()); + } + hdr_presets_->set_role_data(module::Attribute::StringChoices, presets_names); + } else { + throw std::runtime_error("hdr_presets should a json dictionary."); + } + + } catch (std::exception &e) { + spdlog::warn( + "{}: Failed to load perference /plugin/decklink/hdr_presets: {}", + __PRETTY_FUNCTION__, + e.what()); + } +} + +void BMDecklinkPlugin::set_hdr_mode_and_metadata() { + + if (!dcl_output_) + return; + + HDRMetadata metadata; + auto p = eotf_modes.find(hdr_mode_->value()); + metadata.EOTF = p != eotf_modes.end() ? p->second : 0; + + if (metadata.EOTF == -1) { + // auto turn on HDR mode if the ocio display includes the string + // 'ocio_display_hdr_match_string_' which was set from our preferences. So for example, + // if ocio_display_hdr_match_string_ is 'hdr' then if the ocio display is 'Screening + // Room (HDR)' then HDR output is turned on + if (utility::to_lower(get_ocio_display_name()).find(ocio_display_hdr_match_string_) != + std::string::npos) { + metadata.EOTF = 2; // PQ (Perceptual Quantization or something) + } else { + metadata.EOTF = 0; + } + } + + auto q = colourspaces.find(colourspace_->value()); + metadata.colourspace_ = q != colourspaces.end() ? q->second : bmdColorspaceRec709; + + for (size_t i = 0; i < hdr_metadata_settings_.size(); ++i) { + metadata.referencePrimaries[i] = hdr_metadata_settings_[i]->value(); + } + + for (size_t i = 0; i < hdr_metadata_lightlevel_.size(); ++i) { + metadata.luminanceSettings[i] = hdr_metadata_lightlevel_[i]->value(); + } + + dcl_output_->set_hdr_metadata(metadata); +} + +void BMDecklinkPlugin::save_hdr_colour_prefs() { + + utility::JsonStore cvals(nlohmann::json::parse("[]")); + utility::JsonStore lvals(nlohmann::json::parse("[]")); + for (size_t i = 0; i < hdr_metadata_settings_.size(); ++i) { + cvals.push_back(hdr_metadata_settings_[i]->value()); + } + + for (size_t i = 0; i < hdr_metadata_lightlevel_.size(); ++i) { + lvals.push_back(hdr_metadata_lightlevel_[i]->value()); + } + + // Fetch the ocio display name match string for auto HDR mode switching + auto prefs = global_store::GlobalStoreHelper(system()); + try { + prefs.set_value(cvals, "/plugin/decklink/hdr_colour_metadata_values"); + prefs.set_value(lvals, "/plugin/decklink/hdr_luminance_metadata_values"); + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + prefs_save_scheduled_ = false; +} + +std::string BMDecklinkPlugin::get_ocio_display_name() { + + if (!colour_pipeline()) + return std::string(); + + try { + + scoped_actor sys{system()}; + + auto ocio_display = utility::request_receive( + *sys, colour_pipeline(), module::attribute_value_atom_v, std::string("Display")); + + if (ocio_display.is_string()) { + return ocio_display.get(); + } + + } catch (const std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + + return std::string(); +} + +BMDecklinkPlugin::~BMDecklinkPlugin() {} + +XSTUDIO_PLUGIN_DECLARE_BEGIN() + +XSTUDIO_REGISTER_PLUGIN( + BMDecklinkPlugin, + BMDecklinkPlugin::PLUGIN_UUID, + BlackMagic Decklink Viewport, + plugin_manager::PluginFlags::PF_VIDEO_OUTPUT, + false, + Ted Waine, + BlackMagic Decklink SDI Output Plugin, + 1.0.0) + +XSTUDIO_PLUGIN_DECLARE_END() diff --git a/src/plugin/video_output/bmd_decklink/src/extern/linux/DeckLinkAPIDispatch.cpp b/src/plugin/video_output/bmd_decklink/src/extern/linux/DeckLinkAPIDispatch.cpp index 485117887..7a6fd8beb 100644 --- a/src/plugin/video_output/bmd_decklink/src/extern/linux/DeckLinkAPIDispatch.cpp +++ b/src/plugin/video_output/bmd_decklink/src/extern/linux/DeckLinkAPIDispatch.cpp @@ -1,186 +1,246 @@ -/* -LICENSE-START- -** Copyright (c) 2009 Blackmagic Design -** -** Permission is hereby granted, free of charge, to any person or organization -** obtaining a copy of the software and accompanying documentation (the -** "Software") to use, reproduce, display, distribute, sub-license, execute, -** and transmit the Software, and to prepare derivative works of the Software, -** and to permit third-parties to whom the Software is furnished to do so, in -** accordance with: -** -** (1) if the Software is obtained from Blackmagic Design, the End User License -** Agreement for the Software Development Kit ("EULA") available at -** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or -** -** (2) if the Software is obtained from any third party, such licensing terms -** as notified by that third party, -** -** and all subject to the following: -** -** (3) the copyright notices in the Software and this entire statement, -** including the above license grant, this restriction and the following -** disclaimer, must be included in all copies of the Software, in whole or in -** part, and all derivative works of the Software, unless such copies or -** derivative works are solely in the form of machine-executable object code -** generated by a source language processor. -** -** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT -** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE -** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, -** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -** DEALINGS IN THE SOFTWARE. -** -** A copy of the Software is available free of charge at -** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA. -** -** -LICENSE-END- -**/ - -#include -#include -#include - -#include "DeckLinkAPI.h" - -#define kDeckLinkAPI_Name "libDeckLinkAPI.so" -#define KDeckLinkPreviewAPI_Name "libDeckLinkPreviewAPI.so" - -typedef IDeckLinkIterator *(*CreateIteratorFunc)(void); -typedef IDeckLinkAPIInformation *(*CreateAPIInformationFunc)(void); -typedef IDeckLinkGLScreenPreviewHelper *(*CreateOpenGLScreenPreviewHelperFunc)(void); -typedef IDeckLinkGLScreenPreviewHelper *(*CreateOpenGL3ScreenPreviewHelperFunc)(void); -typedef IDeckLinkVideoConversion *(*CreateVideoConversionInstanceFunc)(void); -typedef IDeckLinkDiscovery *(*CreateDeckLinkDiscoveryInstanceFunc)(void); -typedef IDeckLinkVideoFrameAncillaryPackets *(*CreateVideoFrameAncillaryPacketsInstanceFunc)( - void); - -static pthread_once_t gDeckLinkOnceControl = PTHREAD_ONCE_INIT; -static pthread_once_t gPreviewOnceControl = PTHREAD_ONCE_INIT; - -static bool gLoadedDeckLinkAPI = false; - -static CreateIteratorFunc gCreateIteratorFunc = NULL; -static CreateAPIInformationFunc gCreateAPIInformationFunc = NULL; -static CreateOpenGLScreenPreviewHelperFunc gCreateOpenGLPreviewFunc = NULL; -static CreateOpenGL3ScreenPreviewHelperFunc gCreateOpenGL3PreviewFunc = NULL; -static CreateVideoConversionInstanceFunc gCreateVideoConversionFunc = NULL; -static CreateDeckLinkDiscoveryInstanceFunc gCreateDeckLinkDiscoveryFunc = NULL; -static CreateVideoFrameAncillaryPacketsInstanceFunc gCreateVideoFrameAncillaryPacketsFunc = - NULL; - -static void InitDeckLinkAPI(void) { - void *libraryHandle; - - libraryHandle = dlopen(kDeckLinkAPI_Name, RTLD_NOW | RTLD_GLOBAL); - if (!libraryHandle) { - fprintf(stderr, "%s\n", dlerror()); - return; +/* -LICENSE-START- +** Copyright (c) 2009 Blackmagic Design +** +** Permission is hereby granted, free of charge, to any person or organization +** obtaining a copy of the software and accompanying documentation (the +** "Software") to use, reproduce, display, distribute, sub-license, execute, +** and transmit the Software, and to prepare derivative works of the Software, +** and to permit third-parties to whom the Software is furnished to do so, in +** accordance with: +** +** (1) if the Software is obtained from Blackmagic Design, the End User License +** Agreement for the Software Development Kit ("EULA") available at +** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or +** +** (2) if the Software is obtained from any third party, such licensing terms +** as notified by that third party, +** +** and all subject to the following: +** +** (3) the copyright notices in the Software and this entire statement, +** including the above license grant, this restriction and the following +** disclaimer, must be included in all copies of the Software, in whole or in +** part, and all derivative works of the Software, unless such copies or +** derivative works are solely in the form of machine-executable object code +** generated by a source language processor. +** +** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +** DEALINGS IN THE SOFTWARE. +** +** A copy of the Software is available free of charge at +** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA. +** +** -LICENSE-END- +**/ + +#include +#include +#include +#include + +#include "DeckLinkAPI.h" + +#define kDeckLinkAPI_Name "libDeckLinkAPI.so" +#define KDeckLinkPreviewAPI_Name "libDeckLinkPreviewAPI.so" + +typedef IDeckLinkIterator *(*CreateIteratorFunc)(void); +typedef IDeckLinkAPIInformation *(*CreateAPIInformationFunc)(void); +typedef IDeckLinkGLScreenPreviewHelper *(*CreateOpenGLScreenPreviewHelperFunc)(void); +typedef IDeckLinkGLScreenPreviewHelper *(*CreateOpenGL3ScreenPreviewHelperFunc)(void); +typedef IDeckLinkVideoConversion *(*CreateVideoConversionInstanceFunc)(void); +typedef IDeckLinkDiscovery *(*CreateDeckLinkDiscoveryInstanceFunc)(void); +typedef IDeckLinkVideoFrameAncillaryPackets *(*CreateVideoFrameAncillaryPacketsInstanceFunc)( + void); + +static pthread_once_t gDeckLinkOnceControl = PTHREAD_ONCE_INIT; +static pthread_once_t gPreviewOnceControl = PTHREAD_ONCE_INIT; + +static bool gLoadedDeckLinkAPI = false; + +static CreateIteratorFunc gCreateIteratorFunc = NULL; +static CreateAPIInformationFunc gCreateAPIInformationFunc = NULL; +static CreateOpenGLScreenPreviewHelperFunc gCreateOpenGLPreviewFunc = NULL; +static CreateOpenGL3ScreenPreviewHelperFunc gCreateOpenGL3PreviewFunc = NULL; +static CreateVideoConversionInstanceFunc gCreateVideoConversionFunc = NULL; +static CreateDeckLinkDiscoveryInstanceFunc gCreateDeckLinkDiscoveryFunc = NULL; +static CreateVideoFrameAncillaryPacketsInstanceFunc gCreateVideoFrameAncillaryPacketsFunc = + NULL; +static const char *gVideoConversionSymbolName = NULL; +static const char *gAncillaryPacketsSymbolName = NULL; + +static void *GetSymbolAddress( + void *libraryHandle, + const char *primarySymbol, + const char *fallbackSymbol, + const char *symbolDescription) { + dlerror(); + void *symbol = dlsym(libraryHandle, primarySymbol); + if (!dlerror() && symbol) { + if (fallbackSymbol && strcmp(primarySymbol, "CreateVideoConversionInstance_0002") == 0) { + gVideoConversionSymbolName = primarySymbol; + } else if ( + fallbackSymbol && + strcmp(primarySymbol, "CreateVideoFrameAncillaryPacketsInstance_0002") == 0) { + gAncillaryPacketsSymbolName = primarySymbol; + } + return symbol; + } + + if (!fallbackSymbol) { + fprintf(stderr, "DeckLink API missing symbol %s\n", primarySymbol); + return NULL; + } + + dlerror(); + symbol = dlsym(libraryHandle, fallbackSymbol); + if (!dlerror() && symbol) { + if (strcmp(primarySymbol, "CreateVideoConversionInstance_0002") == 0) { + gVideoConversionSymbolName = fallbackSymbol; + } else if ( + strcmp(primarySymbol, "CreateVideoFrameAncillaryPacketsInstance_0002") == 0) { + gAncillaryPacketsSymbolName = fallbackSymbol; + } + return symbol; } - - gLoadedDeckLinkAPI = true; - - gCreateIteratorFunc = - (CreateIteratorFunc)dlsym(libraryHandle, "CreateDeckLinkIteratorInstance_0004"); - if (!gCreateIteratorFunc) - fprintf(stderr, "%s\n", dlerror()); - gCreateAPIInformationFunc = (CreateAPIInformationFunc)dlsym( - libraryHandle, "CreateDeckLinkAPIInformationInstance_0001"); - if (!gCreateAPIInformationFunc) - fprintf(stderr, "%s\n", dlerror()); - gCreateVideoConversionFunc = (CreateVideoConversionInstanceFunc)dlsym( - libraryHandle, "CreateVideoConversionInstance_0002"); - if (!gCreateVideoConversionFunc) - fprintf(stderr, "%s\n", dlerror()); - gCreateDeckLinkDiscoveryFunc = (CreateDeckLinkDiscoveryInstanceFunc)dlsym( - libraryHandle, "CreateDeckLinkDiscoveryInstance_0003"); - if (!gCreateDeckLinkDiscoveryFunc) - fprintf(stderr, "%s\n", dlerror()); - gCreateVideoFrameAncillaryPacketsFunc = (CreateVideoFrameAncillaryPacketsInstanceFunc)dlsym( - libraryHandle, "CreateVideoFrameAncillaryPacketsInstance_0002"); - if (!gCreateVideoFrameAncillaryPacketsFunc) - fprintf(stderr, "%s\n", dlerror()); -} - -static void InitDeckLinkPreviewAPI(void) { - void *libraryHandle; - - libraryHandle = dlopen(KDeckLinkPreviewAPI_Name, RTLD_NOW | RTLD_GLOBAL); - if (!libraryHandle) { - fprintf(stderr, "%s\n", dlerror()); - return; - } - gCreateOpenGLPreviewFunc = (CreateOpenGLScreenPreviewHelperFunc)dlsym( - libraryHandle, "CreateOpenGLScreenPreviewHelper_0002"); - if (!gCreateOpenGLPreviewFunc) - fprintf(stderr, "%s\n", dlerror()); - gCreateOpenGL3PreviewFunc = (CreateOpenGL3ScreenPreviewHelperFunc)dlsym( - libraryHandle, "CreateOpenGL3ScreenPreviewHelper_0002"); - if (!gCreateOpenGL3PreviewFunc) - fprintf(stderr, "%s\n", dlerror()); -} - -bool IsDeckLinkAPIPresent(void) { - // If the DeckLink API dynamic library was successfully loaded, return this knowledge to the - // caller - return gLoadedDeckLinkAPI; -} - -IDeckLinkIterator *CreateDeckLinkIteratorInstance(void) { - pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); - - if (gCreateIteratorFunc == NULL) - return NULL; - return gCreateIteratorFunc(); -} - -IDeckLinkAPIInformation *CreateDeckLinkAPIInformationInstance(void) { - pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); - - if (gCreateAPIInformationFunc == NULL) - return NULL; - return gCreateAPIInformationFunc(); -} - -IDeckLinkGLScreenPreviewHelper *CreateOpenGLScreenPreviewHelper(void) { - pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); - pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI); - - if (gCreateOpenGLPreviewFunc == NULL) - return NULL; - return gCreateOpenGLPreviewFunc(); -} - -IDeckLinkGLScreenPreviewHelper *CreateOpenGL3ScreenPreviewHelper(void) { - pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); - pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI); - - if (gCreateOpenGL3PreviewFunc == NULL) - return NULL; - return gCreateOpenGL3PreviewFunc(); -} - -IDeckLinkVideoConversion *CreateVideoConversionInstance(void) { - pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); - - if (gCreateVideoConversionFunc == NULL) - return NULL; - return gCreateVideoConversionFunc(); -} - -IDeckLinkDiscovery *CreateDeckLinkDiscoveryInstance(void) { - pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); - - if (gCreateDeckLinkDiscoveryFunc == NULL) - return NULL; - return gCreateDeckLinkDiscoveryFunc(); -} - -IDeckLinkVideoFrameAncillaryPackets *CreateVideoFrameAncillaryPacketsInstance(void) { - pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); - - if (gCreateVideoFrameAncillaryPacketsFunc == NULL) - return NULL; - return gCreateVideoFrameAncillaryPacketsFunc(); -} + + fprintf( + stderr, + "DeckLink API missing %s symbols %s and %s\n", + symbolDescription, + primarySymbol, + fallbackSymbol); + return NULL; +} + +static void InitDeckLinkAPI(void) { + void *libraryHandle; + + libraryHandle = dlopen(kDeckLinkAPI_Name, RTLD_NOW | RTLD_GLOBAL); + if (!libraryHandle) { + fprintf(stderr, "%s\n", dlerror()); + return; + } + + gLoadedDeckLinkAPI = true; + + gCreateIteratorFunc = + (CreateIteratorFunc)dlsym(libraryHandle, "CreateDeckLinkIteratorInstance_0004"); + if (!gCreateIteratorFunc) + fprintf(stderr, "%s\n", dlerror()); + gCreateAPIInformationFunc = (CreateAPIInformationFunc)dlsym( + libraryHandle, "CreateDeckLinkAPIInformationInstance_0001"); + if (!gCreateAPIInformationFunc) + fprintf(stderr, "%s\n", dlerror()); + gCreateVideoConversionFunc = (CreateVideoConversionInstanceFunc)GetSymbolAddress( + libraryHandle, + "CreateVideoConversionInstance_0002", + "CreateVideoConversionInstance_0001", + "video conversion"); + gCreateDeckLinkDiscoveryFunc = (CreateDeckLinkDiscoveryInstanceFunc)dlsym( + libraryHandle, "CreateDeckLinkDiscoveryInstance_0003"); + if (!gCreateDeckLinkDiscoveryFunc) + fprintf(stderr, "%s\n", dlerror()); + gCreateVideoFrameAncillaryPacketsFunc = + (CreateVideoFrameAncillaryPacketsInstanceFunc)GetSymbolAddress( + libraryHandle, + "CreateVideoFrameAncillaryPacketsInstance_0002", + "CreateVideoFrameAncillaryPacketsInstance_0001", + "ancillary packets"); +} + +static void InitDeckLinkPreviewAPI(void) { + void *libraryHandle; + + libraryHandle = dlopen(KDeckLinkPreviewAPI_Name, RTLD_NOW | RTLD_GLOBAL); + if (!libraryHandle) { + fprintf(stderr, "%s\n", dlerror()); + return; + } + gCreateOpenGLPreviewFunc = (CreateOpenGLScreenPreviewHelperFunc)dlsym( + libraryHandle, "CreateOpenGLScreenPreviewHelper_0002"); + if (!gCreateOpenGLPreviewFunc) + fprintf(stderr, "%s\n", dlerror()); + gCreateOpenGL3PreviewFunc = (CreateOpenGL3ScreenPreviewHelperFunc)dlsym( + libraryHandle, "CreateOpenGL3ScreenPreviewHelper_0002"); + if (!gCreateOpenGL3PreviewFunc) + fprintf(stderr, "%s\n", dlerror()); +} + +bool IsDeckLinkAPIPresent(void) { + // If the DeckLink API dynamic library was successfully loaded, return this knowledge to the + // caller + return gLoadedDeckLinkAPI; +} + +extern "C" const char *GetDeckLinkVideoConversionSymbolName(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + return gVideoConversionSymbolName; +} + +extern "C" const char *GetDeckLinkAncillaryPacketsSymbolName(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + return gAncillaryPacketsSymbolName; +} + +IDeckLinkIterator *CreateDeckLinkIteratorInstance(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + + if (gCreateIteratorFunc == NULL) + return NULL; + return gCreateIteratorFunc(); +} + +IDeckLinkAPIInformation *CreateDeckLinkAPIInformationInstance(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + + if (gCreateAPIInformationFunc == NULL) + return NULL; + return gCreateAPIInformationFunc(); +} + +IDeckLinkGLScreenPreviewHelper *CreateOpenGLScreenPreviewHelper(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI); + + if (gCreateOpenGLPreviewFunc == NULL) + return NULL; + return gCreateOpenGLPreviewFunc(); +} + +IDeckLinkGLScreenPreviewHelper *CreateOpenGL3ScreenPreviewHelper(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI); + + if (gCreateOpenGL3PreviewFunc == NULL) + return NULL; + return gCreateOpenGL3PreviewFunc(); +} + +IDeckLinkVideoConversion *CreateVideoConversionInstance(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + + if (gCreateVideoConversionFunc == NULL) + return NULL; + return gCreateVideoConversionFunc(); +} + +IDeckLinkDiscovery *CreateDeckLinkDiscoveryInstance(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + + if (gCreateDeckLinkDiscoveryFunc == NULL) + return NULL; + return gCreateDeckLinkDiscoveryFunc(); +} + +IDeckLinkVideoFrameAncillaryPackets *CreateVideoFrameAncillaryPacketsInstance(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + + if (gCreateVideoFrameAncillaryPacketsFunc == NULL) + return NULL; + return gCreateVideoFrameAncillaryPacketsFunc(); +}