diff --git a/UI/frontend-plugins/decklink-output-ui/decklink-ui-main.cpp b/UI/frontend-plugins/decklink-output-ui/decklink-ui-main.cpp index 35de8b1b3..b81be5a96 100644 --- a/UI/frontend-plugins/decklink-output-ui/decklink-ui-main.cpp +++ b/UI/frontend-plugins/decklink-output-ui/decklink-ui-main.cpp @@ -116,9 +116,6 @@ void output_start() context.stage_index = 0; - const video_output_info *mainVOI = - video_output_get_info(obs_get_video()); - video_output_info vi = {0}; vi.format = VIDEO_FORMAT_BGRA; vi.width = width; @@ -126,7 +123,7 @@ void output_start() vi.fps_den = context.ovi.fps_den; vi.fps_num = context.ovi.fps_num; vi.cache_size = 16; - vi.colorspace = mainVOI->colorspace; + vi.colorspace = VIDEO_CS_DEFAULT; vi.range = VIDEO_RANGE_FULL; vi.name = "decklink_output"; @@ -253,9 +250,6 @@ void preview_output_start() context.stage_index = 0; - const video_output_info *mainVOI = - video_output_get_info(obs_get_video()); - video_output_info vi = {0}; vi.format = VIDEO_FORMAT_BGRA; vi.width = width; @@ -263,7 +257,7 @@ void preview_output_start() vi.fps_den = context.ovi.fps_den; vi.fps_num = context.ovi.fps_num; vi.cache_size = 16; - vi.colorspace = mainVOI->colorspace; + vi.colorspace = VIDEO_CS_DEFAULT; vi.range = VIDEO_RANGE_FULL; vi.name = "decklink_preview_output"; @@ -386,13 +380,24 @@ static void decklink_ui_render(void *param) return; const bool previous = gs_framebuffer_srgb_enabled(); - gs_enable_framebuffer_srgb(true); + const bool source_hdr = (context.ovi.colorspace == VIDEO_CS_2100_PQ) || + (context.ovi.colorspace == VIDEO_CS_2100_HLG); + const bool target_hdr = source_hdr && + (conversion->colorspace == VIDEO_CS_2100_PQ); + gs_enable_framebuffer_srgb(!target_hdr); gs_enable_blending(false); gs_effect_t *const effect = obs_get_base_effect(OBS_EFFECT_DEFAULT); gs_effect_set_texture_srgb(gs_effect_get_param_by_name(effect, "image"), tex); - while (gs_effect_loop(effect, "DrawAlphaDivide")) { + const char *const tech_name = + target_hdr ? "DrawAlphaDivideR10L" + : (source_hdr ? "DrawAlphaDivideTonemap" + : "DrawAlphaDivide"); + while (gs_effect_loop(effect, tech_name)) { + gs_effect_set_float(gs_effect_get_param_by_name(effect, + "multiplier"), + obs_get_video_sdr_white_level() / 10000.f); gs_draw_sprite(tex, 0, 0, 0); } diff --git a/plugins/decklink/DecklinkOutput.hpp b/plugins/decklink/DecklinkOutput.hpp index efc50be14..f50f234f9 100644 --- a/plugins/decklink/DecklinkOutput.hpp +++ b/plugins/decklink/DecklinkOutput.hpp @@ -21,6 +21,7 @@ public: size_t audio_planes; size_t audio_size; int keyerMode; + bool force_sdr; DeckLinkOutput(obs_output_t *output, DeckLinkDeviceDiscovery *discovery); diff --git a/plugins/decklink/OBSVideoFrame.cpp b/plugins/decklink/OBSVideoFrame.cpp index 981d203ee..ce4affdd4 100644 --- a/plugins/decklink/OBSVideoFrame.cpp +++ b/plugins/decklink/OBSVideoFrame.cpp @@ -89,3 +89,152 @@ HRESULT OBSVideoFrame::GetBytes(void **buffer) *buffer = this->data; return S_OK; } + +#define CompareREFIID(iid1, iid2) (memcmp(&iid1, &iid2, sizeof(REFIID)) == 0) + +HDRVideoFrame::HDRVideoFrame(IDeckLinkMutableVideoFrame *frame) + : m_videoFrame(frame), + m_refCount(1) +{ +} + +HRESULT HDRVideoFrame::QueryInterface(REFIID iid, LPVOID *ppv) +{ + if (ppv == nullptr) + return E_INVALIDARG; + + CFUUIDBytes unknown = CFUUIDGetUUIDBytes(IUnknownUUID); + if (CompareREFIID(iid, unknown)) + *ppv = static_cast(this); + else if (CompareREFIID(iid, IID_IDeckLinkVideoFrame)) + *ppv = static_cast(this); + else if (CompareREFIID(iid, IID_IDeckLinkVideoFrameMetadataExtensions)) + *ppv = static_cast( + this); + else { + *ppv = nullptr; + return E_NOINTERFACE; + } + + AddRef(); + return S_OK; +} + +ULONG HDRVideoFrame::AddRef(void) +{ + return ++m_refCount; +} + +ULONG HDRVideoFrame::Release(void) +{ + ULONG newRefValue = --m_refCount; + + if (newRefValue == 0) + delete this; + + return newRefValue; +} + +HRESULT HDRVideoFrame::GetInt(BMDDeckLinkFrameMetadataID metadataID, + int64_t *value) +{ + HRESULT result = S_OK; + + switch (metadataID) { + case bmdDeckLinkFrameMetadataHDRElectroOpticalTransferFunc: + *value = 2; + break; + + case bmdDeckLinkFrameMetadataColorspace: + // Colorspace is fixed for this sample + *value = bmdColorspaceRec2020; + break; + + default: + result = E_INVALIDARG; + } + + return result; +} + +HRESULT HDRVideoFrame::GetFloat(BMDDeckLinkFrameMetadataID metadataID, + double *value) +{ + HRESULT result = S_OK; + + switch (metadataID) { + case bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedX: + *value = 0.708; + break; + + case bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedY: + *value = 0.292; + break; + + case bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenX: + *value = 0.17; + break; + + case bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenY: + *value = 0.797; + break; + + case bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueX: + *value = 0.131; + break; + + case bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueY: + *value = 0.046; + break; + + case bmdDeckLinkFrameMetadataHDRWhitePointX: + *value = 0.3127; + break; + + case bmdDeckLinkFrameMetadataHDRWhitePointY: + *value = 0.329; + break; + + case bmdDeckLinkFrameMetadataHDRMaxDisplayMasteringLuminance: + *value = obs_get_video_hdr_nominal_peak_level(); + break; + + case bmdDeckLinkFrameMetadataHDRMinDisplayMasteringLuminance: + *value = 0.00001; + break; + + case bmdDeckLinkFrameMetadataHDRMaximumContentLightLevel: + *value = obs_get_video_hdr_nominal_peak_level(); + break; + + case bmdDeckLinkFrameMetadataHDRMaximumFrameAverageLightLevel: + *value = obs_get_video_hdr_nominal_peak_level(); + break; + + default: + result = E_INVALIDARG; + } + + return result; +} + +HRESULT HDRVideoFrame::GetFlag(BMDDeckLinkFrameMetadataID, decklink_bool_t *) +{ + // Not expecting GetFlag + return E_INVALIDARG; +} + +HRESULT HDRVideoFrame::GetString(BMDDeckLinkFrameMetadataID, + decklink_string_t *) +{ + // Not expecting GetString + return E_INVALIDARG; +} + +HRESULT HDRVideoFrame::GetBytes(BMDDeckLinkFrameMetadataID, void *, + uint32_t *bufferSize) +{ + *bufferSize = 0; + // Not expecting GetBytes + return E_INVALIDARG; +} diff --git a/plugins/decklink/OBSVideoFrame.h b/plugins/decklink/OBSVideoFrame.h index 828f6462d..fe92cacb5 100644 --- a/plugins/decklink/OBSVideoFrame.h +++ b/plugins/decklink/OBSVideoFrame.h @@ -2,6 +2,7 @@ #include "platform.hpp" #include "obs.hpp" +#include class OBSVideoFrame : public IDeckLinkMutableVideoFrame { private: @@ -75,3 +76,70 @@ public: virtual ULONG STDMETHODCALLTYPE AddRef() override { return 1; } virtual ULONG STDMETHODCALLTYPE Release() override { return 1; } }; + +class HDRVideoFrame : public IDeckLinkVideoFrame, + public IDeckLinkVideoFrameMetadataExtensions { +public: + HDRVideoFrame(IDeckLinkMutableVideoFrame *frame); + virtual ~HDRVideoFrame() {} + + // IUnknown interface + virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, + LPVOID *ppv); + virtual ULONG STDMETHODCALLTYPE AddRef(void); + virtual ULONG STDMETHODCALLTYPE Release(void); + + // IDeckLinkVideoFrame interface + virtual long STDMETHODCALLTYPE GetWidth(void) + { + return m_videoFrame->GetWidth(); + } + virtual long STDMETHODCALLTYPE GetHeight(void) + { + return m_videoFrame->GetHeight(); + } + virtual long STDMETHODCALLTYPE GetRowBytes(void) + { + return m_videoFrame->GetRowBytes(); + } + virtual BMDPixelFormat STDMETHODCALLTYPE GetPixelFormat(void) + { + return m_videoFrame->GetPixelFormat(); + } + virtual BMDFrameFlags STDMETHODCALLTYPE GetFlags(void) + { + return m_videoFrame->GetFlags() | bmdFrameContainsHDRMetadata; + } + virtual HRESULT STDMETHODCALLTYPE GetBytes(void **buffer) + { + return m_videoFrame->GetBytes(buffer); + } + virtual HRESULT STDMETHODCALLTYPE + GetTimecode(BMDTimecodeFormat format, IDeckLinkTimecode **timecode) + { + return m_videoFrame->GetTimecode(format, timecode); + } + virtual HRESULT STDMETHODCALLTYPE + GetAncillaryData(IDeckLinkVideoFrameAncillary **ancillary) + { + return m_videoFrame->GetAncillaryData(ancillary); + } + + // IDeckLinkVideoFrameMetadataExtensions interface + virtual HRESULT STDMETHODCALLTYPE + GetInt(BMDDeckLinkFrameMetadataID metadataID, int64_t *value); + virtual HRESULT STDMETHODCALLTYPE + GetFloat(BMDDeckLinkFrameMetadataID metadataID, double *value); + virtual HRESULT STDMETHODCALLTYPE + GetFlag(BMDDeckLinkFrameMetadataID metadataID, decklink_bool_t *value); + virtual HRESULT STDMETHODCALLTYPE + GetString(BMDDeckLinkFrameMetadataID metadataID, + decklink_string_t *value); + virtual HRESULT STDMETHODCALLTYPE + GetBytes(BMDDeckLinkFrameMetadataID metadataID, void *buffer, + uint32_t *bufferSize); + +private: + ComPtr m_videoFrame; + std::atomic m_refCount; +}; diff --git a/plugins/decklink/const.h b/plugins/decklink/const.h index db97d0594..9a2d09486 100644 --- a/plugins/decklink/const.h +++ b/plugins/decklink/const.h @@ -11,6 +11,7 @@ #define BUFFERING "buffering" #define DEACTIVATE_WNS "deactivate_when_not_showing" #define AUTO_START "auto_start" +#define FORCE_SDR "force_sdr" #define KEYER "keyer" #define SWAP "swap" #define ALLOW_10_BIT "allow_10_bit" @@ -37,6 +38,7 @@ #define TEXT_BUFFERING obs_module_text("Buffering") #define TEXT_DWNS obs_module_text("DeactivateWhenNotShowing") #define TEXT_AUTO_START obs_module_text("AutoStart") +#define TEXT_FORCE_SDR obs_module_text("ForceSDR") #define TEXT_ENABLE_KEYER obs_module_text("Keyer") #define TEXT_SWAP obs_module_text("SwapFC-LFE") #define TEXT_SWAP_TOOLTIP obs_module_text("SwapFC-LFE.Tooltip") diff --git a/plugins/decklink/data/locale/en-US.ini b/plugins/decklink/data/locale/en-US.ini index f6be4c982..d12fc8cb2 100644 --- a/plugins/decklink/data/locale/en-US.ini +++ b/plugins/decklink/data/locale/en-US.ini @@ -19,8 +19,9 @@ ChannelFormat.5_1ch="5.1ch" ChannelFormat.7_1ch="7.1ch" DeactivateWhenNotShowing="Deactivate when not showing" AutoStart="Auto start on launch" +ForceSDR="Force SDR" SwapFC-LFE="Swap FC and LFE" SwapFC-LFE.Tooltip="Swap Front Center Channel and LFE Channel" VideoConnection="Video Connection" AudioConnection="Audio Connection" -Allow10Bit="Allow 10 Bit (Required for SDI captions, may cause performance overhead)" \ No newline at end of file +Allow10Bit="Allow 10 Bit (Required for SDI captions, may cause performance overhead)" diff --git a/plugins/decklink/decklink-device-instance.cpp b/plugins/decklink/decklink-device-instance.cpp index 9d23ecf39..8b886ccac 100644 --- a/plugins/decklink/decklink-device-instance.cpp +++ b/plugins/decklink/decklink-device-instance.cpp @@ -626,13 +626,24 @@ bool DeckLinkDeviceInstance::StartOutput(DeckLinkDeviceMode *mode_) } activeBlob = nullptr; + struct obs_video_info ovi; + const enum video_colorspace colorspace = + obs_get_video_info(&ovi) ? ovi.colorspace : VIDEO_CS_DEFAULT; + const bool source_hdr = (colorspace == VIDEO_CS_2100_PQ) || + (colorspace == VIDEO_CS_2100_HLG); + const bool enable_hdr = + source_hdr && + (obs_output_get_video_conversion(decklinkOutput->GetOutput()) + ->colorspace == VIDEO_CS_2100_PQ); + BMDPixelFormat pixelFormat = enable_hdr ? bmdFormat10BitRGBXLE + : bmdFormat8BitBGRA; const int64_t minimumPrerollFrames = std::max(device->GetMinimumPrerollFrames(), INT64_C(3)); for (int64_t i = 0; i < minimumPrerollFrames; ++i) { ComPtr decklinkOutputFrame; HRESULT result = output_->CreateVideoFrame( decklinkOutput->GetWidth(), decklinkOutput->GetHeight(), - rowSize, bmdFormat8BitBGRA, bmdFrameFlagDefault, + rowSize, pixelFormat, bmdFrameFlagDefault, &decklinkOutputFrame); if (result != S_OK) { blog(LOG_ERROR, "failed to create video frame 0x%X", @@ -640,7 +651,15 @@ bool DeckLinkDeviceInstance::StartOutput(DeckLinkDeviceMode *mode_) return false; } - result = output_->ScheduleVideoFrame(decklinkOutputFrame, + IDeckLinkVideoFrame *theFrame = decklinkOutputFrame.Get(); + ComPtr decklinkOutputHDRFrame; + if (enable_hdr) { + *decklinkOutputHDRFrame.Assign() = + new HDRVideoFrame(decklinkOutputFrame); + theFrame = decklinkOutputHDRFrame.Get(); + } + + result = output_->ScheduleVideoFrame(theFrame, i * frameDuration, frameDuration, frameTimescale); diff --git a/plugins/decklink/decklink-device.cpp b/plugins/decklink/decklink-device.cpp index 1d67aae3c..489e47862 100644 --- a/plugins/decklink/decklink-device.cpp +++ b/plugins/decklink/decklink-device.cpp @@ -110,6 +110,9 @@ bool DeckLinkDevice::Init() attributes->GetFlag(BMDDeckLinkSupportsInternalKeying, &supportsInternalKeyer); + attributes->GetFlag(BMDDeckLinkSupportsHDRMetadata, + &supportsHDRMetadata); + // Sub Device Counts attributes->GetInt(BMDDeckLinkSubDeviceIndex, &subDeviceIndex); attributes->GetInt(BMDDeckLinkNumberOfSubDevices, &numSubDevices); @@ -250,6 +253,11 @@ bool DeckLinkDevice::GetSupportsInternalKeyer(void) const return supportsInternalKeyer; } +bool DeckLinkDevice::GetSupportsHDRMetadata(void) const +{ + return supportsHDRMetadata; +} + int64_t DeckLinkDevice::GetSubDeviceCount() { return numSubDevices; diff --git a/plugins/decklink/decklink-device.hpp b/plugins/decklink/decklink-device.hpp index 290118d80..107b34d8b 100644 --- a/plugins/decklink/decklink-device.hpp +++ b/plugins/decklink/decklink-device.hpp @@ -19,6 +19,7 @@ class DeckLinkDevice { int32_t maxChannel = 0; decklink_bool_t supportsExternalKeyer = false; decklink_bool_t supportsInternalKeyer = false; + decklink_bool_t supportsHDRMetadata = false; int64_t subDeviceIndex = 0; int64_t numSubDevices = 0; int64_t minimumPrerollFrames = 3; @@ -48,6 +49,7 @@ public: int64_t GetAudioInputConnections(); bool GetSupportsExternalKeyer(void) const; bool GetSupportsInternalKeyer(void) const; + bool GetSupportsHDRMetadata(void) const; int64_t GetSubDeviceCount(); int64_t GetSubDeviceIndex(); int64_t GetMinimumPrerollFrames(); diff --git a/plugins/decklink/decklink-output.cpp b/plugins/decklink/decklink-output.cpp index 2aade8376..66bdb9940 100644 --- a/plugins/decklink/decklink-output.cpp +++ b/plugins/decklink/decklink-output.cpp @@ -24,6 +24,7 @@ static void *decklink_output_create(obs_data_t *settings, obs_output_t *output) decklinkOutput->deviceHash = obs_data_get_string(settings, DEVICE_HASH); decklinkOutput->modeID = obs_data_get_int(settings, MODE_ID); decklinkOutput->keyerMode = (int)obs_data_get_int(settings, KEYER); + decklinkOutput->force_sdr = obs_data_get_bool(settings, FORCE_SDR); ComPtr device; device.Set(deviceEnum->FindByHash(decklinkOutput->deviceHash)); @@ -36,9 +37,10 @@ static void *decklink_output_create(obs_data_t *settings, obs_output_t *output) to.width = mode->GetWidth(); to.height = mode->GetHeight(); to.range = VIDEO_RANGE_FULL; - struct obs_video_info ovi; - to.colorspace = obs_get_video_info(&ovi) ? ovi.colorspace - : VIDEO_CS_DEFAULT; + to.colorspace = (device->GetSupportsHDRMetadata() && + !decklinkOutput->force_sdr) + ? VIDEO_CS_2100_PQ + : VIDEO_CS_709; obs_output_set_video_conversion(output, &to); } @@ -53,6 +55,7 @@ static void decklink_output_update(void *data, obs_data_t *settings) decklink->deviceHash = obs_data_get_string(settings, DEVICE_HASH); decklink->modeID = obs_data_get_int(settings, MODE_ID); decklink->keyerMode = (int)obs_data_get_int(settings, KEYER); + decklink->force_sdr = obs_data_get_bool(settings, FORCE_SDR); } static bool decklink_output_start(void *data) @@ -272,6 +275,7 @@ static obs_properties_t *decklink_output_properties(void *unused) OBS_COMBO_FORMAT_INT); obs_properties_add_bool(props, AUTO_START, TEXT_AUTO_START); + obs_properties_add_bool(props, FORCE_SDR, TEXT_FORCE_SDR); obs_properties_add_list(props, KEYER, TEXT_ENABLE_KEYER, OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);