diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index c12f015c8b85ae..23b49623f970d6 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -87,4 +87,5 @@ add_obs_plugin(text-freetype2) add_obs_plugin(vlc-video WITH_MESSAGE) add_obs_plugin(win-capture PLATFORMS WINDOWS) add_obs_plugin(win-dshow PLATFORMS WINDOWS) +add_obs_plugin(win-mediafoundation PLATFORMS WINDOWS) add_obs_plugin(win-wasapi PLATFORMS WINDOWS) diff --git a/plugins/win-mediafoundation/CMakeLists.txt b/plugins/win-mediafoundation/CMakeLists.txt new file mode 100644 index 00000000000000..46800730bf11fc --- /dev/null +++ b/plugins/win-mediafoundation/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.28...3.30) + +add_obs_plugin(encoder PLATFORMS WINDOWS ARCHITECTURES ARM64) diff --git a/plugins/win-mediafoundation/encoder/CMakeLists.txt b/plugins/win-mediafoundation/encoder/CMakeLists.txt new file mode 100644 index 00000000000000..1809b2f0a1085e --- /dev/null +++ b/plugins/win-mediafoundation/encoder/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.28...3.30) + +add_library(win-mediafoundation-encoder MODULE) +add_library(OBS::win-mediafoundation-encoder ALIAS win-mediafoundation-encoder) + +target_sources( + win-mediafoundation-encoder + PRIVATE + mf-av1-encoder.cpp + mf-av1-encoder.hpp + mf-av1.cpp + mf-common.cpp + mf-common.hpp + mf-encoder-descriptor.cpp + mf-encoder-descriptor.hpp + mf-h264-encoder.cpp + mf-h264-encoder.hpp + mf-h264.cpp + mf-hevc-encoder.cpp + mf-hevc-encoder.hpp + mf-hevc.cpp + mf-plugin.cpp +) + +target_link_libraries( + win-mediafoundation-encoder + PRIVATE d3d9 dxva2 uuid mfplat mfuuid mf wmcodecdspuuid libobs +) + +set_target_properties_obs(win-mediafoundation-encoder PROPERTIES FOLDER "plugins" PREFIX "") diff --git a/plugins/win-mediafoundation/encoder/data/locale/en-US.ini b/plugins/win-mediafoundation/encoder/data/locale/en-US.ini new file mode 100644 index 00000000000000..130a8a968033f3 --- /dev/null +++ b/plugins/win-mediafoundation/encoder/data/locale/en-US.ini @@ -0,0 +1,73 @@ +MF.H264.EncoderName="Media Foundation H264 Encoder" +MF.H264.Encoder="Encoder Name" +MF.H264.LowLatency="Low Latency (Disable frame re-ordering)" +MF.H264.BFrames="Consecutive B-Frame count" +MF.H264.CustomBufsize="Use Custom Buffer Size" +MF.H264.BufferSize="Buffer Size" +MF.H264.CustomMaxBitrate="Use Custom Max Bitrate" +MF.H264.Bitrate="Bitrate" +MF.H264.MaxBitrate="Max Bitrate" +MF.H264.KeyframeIntervalSec="Keyframe Interval (seconds, 0=auto)" +MF.H264.RateControl="Rate Control" +MF.H264.CBR="CBR (Constant Bitrate)" +MF.H264.VBR="VBR (Variable Bitrate)" +MF.H264.CQP="CQP (Constant Quality)" +MF.H264.MinQP="Minimum QP" +MF.H264.MaxQP="Maximum QP" +MF.H264.QPI="QP I-Frame" +MF.H264.QPP="QP P-Frame" +MF.H264.QPB="QP B-Frame" +MF.H264.Profile="Profile" +MF.H264.Advanced="Advanced" + +MF.H264.EncoderSWMicrosoft="Microsoft Software H.264 Encoder" +MF.H264.EncoderHWAMD="AMD Video Coding Engine H.264 Encoder (Media Foundation)" +MF.H264.EncoderHWIntel="Intel Quick Sync H.264 Encoder (Media Foundation)" +MF.H264.EncoderHWNVIDIA="NVIDIA NVENC H.264 Encoder (Media Foundation)" +MF.H264.EncoderHWQCOM="Qualcomm® Adreno™ H.264 (AVC) Encoder" +MF.HEVC.EncoderHWQCOM="Qualcomm® Adreno™ H.265 (HEVC) Encoder" +MF.AV1.EncoderHWQCOM="Qualcomm® Adreno™ AV1 Encoder" + +MF.HEVC.EncoderName="Media Foundation HEVC Encoder" +MF.HEVC.Encoder="Encoder Name" +MF.HEVC.LowLatency="Low Latency (Disable frame re-ordering)" +MF.HEVC.BFrames="Consecutive B-Frame count" +MF.HEVC.CustomBufsize="Use Custom Buffer Size" +MF.HEVC.BufferSize="Buffer Size" +MF.HEVC.CustomMaxBitrate="Use Custom Max Bitrate" +MF.HEVC.Bitrate="Bitrate" +MF.HEVC.MaxBitrate="Max Bitrate" +MF.HEVC.KeyframeIntervalSec="Keyframe Interval (seconds, 0=auto)" +MF.HEVC.RateControl="Rate Control" +MF.HEVC.CBR="CBR (Constant Bitrate)" +MF.HEVC.VBR="VBR (Variable Bitrate)" +MF.HEVC.CQP="CQP (Constant Quality)" +MF.HEVC.MinQP="Minimum QP" +MF.HEVC.MaxQP="Maximum QP" +MF.HEVC.QPI="QP I-Frame" +MF.HEVC.QPP="QP P-Frame" +MF.HEVC.QPB="QP B-Frame" +MF.HEVC.Profile="Profile" +MF.HEVC.Advanced="Advanced" + +MF.AV1.EncoderName="Media Foundation AV1 Encoder" +MF.AV1.Encoder="Encoder Name" +MF.AV1.LowLatency="Low Latency (Disable frame re-ordering)" +MF.AV1.BFrames="Consecutive B-Frame count" +MF.AV1.CustomBufsize="Use Custom Buffer Size" +MF.AV1.BufferSize="Buffer Size" +MF.AV1.CustomMaxBitrate="Use Custom Max Bitrate" +MF.AV1.Bitrate="Bitrate" +MF.AV1.MaxBitrate="Max Bitrate" +MF.AV1.KeyframeIntervalSec="Keyframe Interval (seconds, 0=auto)" +MF.AV1.RateControl="Rate Control" +MF.AV1.CBR="CBR (Constant Bitrate)" +MF.AV1.VBR="VBR (Variable Bitrate)" +MF.AV1.CQP="CQP (Constant Quality)" +MF.AV1.MinQP="Minimum QP" +MF.AV1.MaxQP="Maximum QP" +MF.AV1.QPI="QP I-Frame" +MF.AV1.QPP="QP P-Frame" +MF.AV1.QPB="QP B-Frame" +MF.AV1.Profile="Profile" +MF.AV1.Advanced="Advanced" diff --git a/plugins/win-mediafoundation/encoder/mf-av1-encoder.cpp b/plugins/win-mediafoundation/encoder/mf-av1-encoder.cpp new file mode 100644 index 00000000000000..fba524e841e9dd --- /dev/null +++ b/plugins/win-mediafoundation/encoder/mf-av1-encoder.cpp @@ -0,0 +1,780 @@ +#include +#include + +#include "mf-common.hpp" +#include "mf-av1-encoder.hpp" + +#include +#include + +#include +#include + +// DirectX +#include + +// Media Foundation +#include +#include +#include +#include +#include + +using namespace MF; + +namespace { +eAVEncAV1VProfile MapProfile(AV1Profile profile) +{ + switch (profile) { + case AV1ProfileMain: + return eAVEncAV1VProfile_Main_420_8; + default: + return eAVEncAV1VProfile_Main_420_8; + } +} + +eAVEncCommonRateControlMode MapRateControl(AV1RateControl rc) +{ + switch (rc) { + case AV1RateControlCBR: + return eAVEncCommonRateControlMode_CBR; + case AV1RateControlVBR: + return eAVEncCommonRateControlMode_UnconstrainedVBR; + default: + return eAVEncCommonRateControlMode_CBR; + } +} + +UINT32 MapQpToQuality(AV1QP &qp) +{ + return 100 - (UINT32)floor(100.0 / 51.0 * qp.defaultQp + 0.5f); +} + +bool ProcessNV12(std::function func, UINT32 height) +{ + INT32 plane = 0; + + func(height, plane++); + func(height / 2, plane); + + return true; +} +} // namespace + +AV1Encoder::AV1Encoder(const obs_encoder_t *encoder, std::shared_ptr descriptor, UINT32 width, + UINT32 height, UINT32 framerateNum, UINT32 framerateDen, AV1Profile profile, UINT32 bitrate) + : encoder(encoder), + descriptor(descriptor), + width(width), + height(height), + framerateNum(framerateNum), + framerateDen(framerateDen), + initialBitrate(bitrate), + profile(profile) +{ +} + +AV1Encoder::~AV1Encoder() +{ + HRESULT hr; + + if (!descriptor->Async() || !eventGenerator || !pendingRequests) + return; + + // Make sure all events have finished before releasing, and drain + // all output requests until it makes an input request. + // If you do not do this, you risk it releasing while there's still + // encoder activity, which can cause a crash with certain interfaces. + while (inputRequests == 0) { + hr = ProcessOutput(); + if (hr != MF_E_TRANSFORM_NEED_MORE_INPUT && FAILED(hr)) { + MF_LOG_COM(LOG_ERROR, + "AV1Encoder::~AV1Encoder: " + "ProcessOutput()", + hr); + break; + } + + if (inputRequests == 0) + Sleep(1); + } +} + +HRESULT AV1Encoder::CreateMediaTypes(ComPtr &i, ComPtr &o) +{ + HRESULT hr; + CHECK_HR_ERROR(MFCreateMediaType(&i)); + CHECK_HR_ERROR(MFCreateMediaType(&o)); + + CHECK_HR_ERROR(i->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video)); + CHECK_HR_ERROR(i->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_NV12)); + CHECK_HR_ERROR(MFSetAttributeSize(i, MF_MT_FRAME_SIZE, width, height)); + CHECK_HR_ERROR(MFSetAttributeRatio(i, MF_MT_FRAME_RATE, framerateNum, framerateDen)); + CHECK_HR_ERROR(i->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlaceMode::MFVideoInterlace_Progressive)); + CHECK_HR_ERROR(MFSetAttributeRatio(i, MF_MT_PIXEL_ASPECT_RATIO, 1, 1)); + + CHECK_HR_ERROR(o->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video)); + CHECK_HR_ERROR(o->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_AV1)); + CHECK_HR_ERROR(MFSetAttributeSize(o, MF_MT_FRAME_SIZE, width, height)); + CHECK_HR_ERROR(MFSetAttributeRatio(o, MF_MT_FRAME_RATE, framerateNum, framerateDen)); + CHECK_HR_ERROR(o->SetUINT32(MF_MT_AVG_BITRATE, initialBitrate * 1000)); + CHECK_HR_ERROR(o->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlaceMode::MFVideoInterlace_Progressive)); + CHECK_HR_ERROR(MFSetAttributeRatio(o, MF_MT_PIXEL_ASPECT_RATIO, 1, 1)); + CHECK_HR_ERROR(o->SetUINT32(MF_MT_VIDEO_LEVEL, (UINT32)-1)); + + return S_OK; + +fail: + return hr; +} + +HRESULT AV1Encoder::DrainEvents() +{ + HRESULT hr; + while ((hr = DrainEvent(false)) == S_OK) + ; + if (hr == MF_E_NO_EVENTS_AVAILABLE) + hr = S_OK; + return hr; +} + +HRESULT AV1Encoder::DrainEvent(bool block) +{ + HRESULT hr, eventStatus; + ComPtr event; + MediaEventType type; + + hr = eventGenerator->GetEvent(block ? 0 : MF_EVENT_FLAG_NO_WAIT, &event); + + if (hr != MF_E_NO_EVENTS_AVAILABLE && FAILED(hr)) + goto fail; + if (hr == MF_E_NO_EVENTS_AVAILABLE) + return hr; + + CHECK_HR_ERROR(event->GetType(&type)); + CHECK_HR_ERROR(event->GetStatus(&eventStatus)); + + if (SUCCEEDED(eventStatus)) { + if (type == METransformNeedInput) { + inputRequests++; + } else if (type == METransformHaveOutput) { + outputRequests++; + } + } + + return S_OK; + +fail: + return hr; +} + +HRESULT AV1Encoder::InitializeEventGenerator() +{ + HRESULT hr; + + CHECK_HR_ERROR(transform->QueryInterface(&eventGenerator)); + + return S_OK; + +fail: + return hr; +} + +HRESULT AV1Encoder::InitializeExtraData() +{ + HRESULT hr; + ComPtr inputType; + UINT32 headerSize; + + extraData.clear(); + + CHECK_HR_ERROR(transform->GetOutputCurrentType(0, &inputType)); + + CHECK_HR_ERROR(inputType->GetBlobSize(MF_MT_MPEG_SEQUENCE_HEADER, &headerSize)); + + extraData.resize(headerSize); + + CHECK_HR_ERROR(inputType->GetBlob(MF_MT_MPEG_SEQUENCE_HEADER, extraData.data(), headerSize, NULL)); + + return S_OK; + +fail: + return hr; +} + +namespace { +static HRESULT SetCodecProperty(ComPtr &codecApi, GUID guid, bool value) +{ + VARIANT v; + v.vt = VT_BOOL; + v.boolVal = value ? VARIANT_TRUE : VARIANT_FALSE; + return codecApi->SetValue(&guid, &v); +} + +static HRESULT SetCodecProperty(ComPtr &codecApi, GUID guid, UINT32 value) +{ + VARIANT v; + v.vt = VT_UI4; + v.ulVal = value; + return codecApi->SetValue(&guid, &v); +} + +static HRESULT SetCodecProperty(ComPtr &codecApi, GUID guid, UINT64 value) +{ + VARIANT v; + v.vt = VT_UI8; + v.ullVal = value; + return codecApi->SetValue(&guid, &v); +} +} // namespace + +bool AV1Encoder::SetBitrate(UINT32 bitrate) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, + SetCodecProperty(codecApi, CODECAPI_AVEncCommonMeanBitRate, UINT32(bitrate * 1000))); + } + + return true; + +fail: + return false; +} + +bool AV1Encoder::SetQP(AV1QP &qp) +{ + HRESULT hr; + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, + SetCodecProperty(codecApi, CODECAPI_AVEncCommonQuality, UINT32(MapQpToQuality(qp)))); + CHECK_HR_WARNING(SetCodecProperty(codecApi, CODECAPI_AVEncVideoEncodeQP, UINT64(qp.Pack(true)))); + CHECK_HR_WARNING( + SetCodecProperty(codecApi, CODECAPI_AVEncVideoEncodeFrameTypeQP, UINT64(qp.Pack(false)))); + } + + return true; + +fail: + return false; +} + +bool AV1Encoder::SetMinQP(UINT32 minQp) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, SetCodecProperty(codecApi, CODECAPI_AVEncVideoMinQP, UINT32(minQp))); + } + + return true; + +fail: + return false; +} + +bool AV1Encoder::SetMaxQP(UINT32 maxQp) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, SetCodecProperty(codecApi, CODECAPI_AVEncVideoMaxQP, UINT32(maxQp))); + } + + return true; + +fail: + return false; +} + +bool AV1Encoder::SetRateControl(AV1RateControl rateControl) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, SetCodecProperty(codecApi, CODECAPI_AVEncCommonRateControlMode, + UINT32(MapRateControl(rateControl)))); + } + + return true; + +fail: + return false; +} + +bool AV1Encoder::SetKeyframeInterval(UINT32 seconds) +{ + HRESULT hr; + + if (codecApi) { + float gopSize = float(framerateNum) / framerateDen * seconds; + CHECK_HR_LEVEL(LOG_WARNING, SetCodecProperty(codecApi, CODECAPI_AVEncMPVGOPSize, UINT32(gopSize))); + } + + return true; + +fail: + return false; +} + +bool AV1Encoder::SetMaxBitrate(UINT32 maxBitrate) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, + SetCodecProperty(codecApi, CODECAPI_AVEncCommonMaxBitRate, UINT32(maxBitrate * 1000))); + } + + return true; + +fail: + return false; +} + +bool AV1Encoder::SetLowLatency(bool lowLatency) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, SetCodecProperty(codecApi, CODECAPI_AVLowLatencyMode, lowLatency)); + } + + return true; + +fail: + return false; +} + +bool AV1Encoder::SetBufferSize(UINT32 bufferSize) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, + SetCodecProperty(codecApi, CODECAPI_AVEncCommonBufferSize, UINT32(bufferSize * 1000))); + } + + return true; + +fail: + return false; +} + +bool AV1Encoder::SetBFrameCount(UINT32 bFrames) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, + SetCodecProperty(codecApi, CODECAPI_AVEncMPVDefaultBPictureCount, UINT32(bFrames))); + } + + return true; + +fail: + return false; +} + +bool AV1Encoder::Initialize(std::function func) +{ + ProfileScope("AV1Encoder::Initialize"); + + HRESULT hr; + + ComPtr inputType, outputType; + ComPtr transformAttributes; + MFT_OUTPUT_STREAM_INFO streamInfo = {0}; + + CHECK_HR_ERROR(CoCreateInstance(descriptor->Guid(), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&transform))); + + CHECK_HR_ERROR(CreateMediaTypes(inputType, outputType)); + + if (descriptor->Async()) { + CHECK_HR_ERROR(transform->GetAttributes(&transformAttributes)); + CHECK_HR_ERROR(transformAttributes->SetUINT32(MF_TRANSFORM_ASYNC_UNLOCK, TRUE)); + } + + CHECK_HR_ERROR(transform->QueryInterface(&codecApi)); + + if (func && !func()) { + MF_LOG(LOG_ERROR, "Failed setting custom properties"); + goto fail; + } + + MF_LOG(LOG_INFO, "Activating encoder: %s", typeNames[(int)descriptor->Type()]); + + MF_LOG(LOG_INFO, " Setting output type to transform:"); + LogMediaType(outputType.Get()); + CHECK_HR_ERROR(transform->SetOutputType(0, outputType.Get(), 0)); + + MF_LOG(LOG_INFO, " Setting input type to transform:"); + LogMediaType(inputType.Get()); + CHECK_HR_ERROR(transform->SetInputType(0, inputType.Get(), 0)); + + CHECK_HR_ERROR(transform->ProcessMessage(MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, NULL)); + + CHECK_HR_ERROR(transform->ProcessMessage(MFT_MESSAGE_NOTIFY_START_OF_STREAM, NULL)); + + if (descriptor->Async()) + CHECK_HR_ERROR(InitializeEventGenerator()); + + CHECK_HR_ERROR(transform->GetOutputStreamInfo(0, &streamInfo)); + createOutputSample = + !(streamInfo.dwFlags & (MFT_OUTPUT_STREAM_PROVIDES_SAMPLES | MFT_OUTPUT_STREAM_CAN_PROVIDE_SAMPLES)); + + return true; + +fail: + return false; +} + +bool AV1Encoder::ExtraData(UINT8 **data, UINT32 *dataLength) +{ + if (extraData.empty()) + return false; + + *data = extraData.data(); + *dataLength = (UINT32)extraData.size(); + + return true; +} + +HRESULT AV1Encoder::CreateEmptySample(ComPtr &sample, ComPtr &buffer, DWORD length) +{ + HRESULT hr; + + CHECK_HR_ERROR(MFCreateSample(&sample)); + CHECK_HR_ERROR(MFCreateMemoryBuffer(length, &buffer)); + CHECK_HR_ERROR(sample->AddBuffer(buffer.Get())); + return S_OK; + +fail: + return hr; +} + +HRESULT AV1Encoder::EnsureCapacity(ComPtr &sample, DWORD length) +{ + HRESULT hr; + ComPtr buffer; + DWORD currentLength; + + if (!sample) { + CHECK_HR_ERROR(CreateEmptySample(sample, buffer, length)); + } else { + CHECK_HR_ERROR(sample->GetBufferByIndex(0, &buffer)); + } + + CHECK_HR_ERROR(buffer->GetMaxLength(¤tLength)); + if (currentLength < length) { + CHECK_HR_ERROR(sample->RemoveAllBuffers()); + CHECK_HR_ERROR(MFCreateMemoryBuffer(length, &buffer)); + CHECK_HR_ERROR(sample->AddBuffer(buffer)); + } else { + buffer->SetCurrentLength(0); + } + + return S_OK; + +fail: + return hr; +} + +HRESULT AV1Encoder::ProcessInput(ComPtr &sample) +{ + ProfileScope("AV1Encoder::ProcessInput(sample)"); + + HRESULT hr = S_OK; + if (descriptor->Async()) { + if (inputRequests == 1 && inputSamples.empty()) { + inputRequests--; + return transform->ProcessInput(0, sample, 0); + } + + inputSamples.push(sample); + + while (inputRequests > 0) { + if (inputSamples.empty()) + return hr; + ComPtr queuedSample = inputSamples.front(); + inputSamples.pop(); + inputRequests--; + CHECK_HR_ERROR(transform->ProcessInput(0, queuedSample, 0)); + } + } else { + return transform->ProcessInput(0, sample, 0); + } + +fail: + return hr; +} + +bool AV1Encoder::ProcessInput(UINT8 **data, UINT32 *linesize, UINT64 pts, Status *status) +{ + ProfileScope("AV1Encoder::ProcessInput"); + + HRESULT hr; + ComPtr sample; + ComPtr buffer; + BYTE *bufferData; + UINT64 sampleDur; + UINT32 imageSize; + + CHECK_HR_ERROR(MFCalculateImageSize(MFVideoFormat_NV12, width, height, &imageSize)); + + CHECK_HR_ERROR(CreateEmptySample(sample, buffer, imageSize)); + + { + ProfileScope("AV1EncoderCopyInputSample"); + + CHECK_HR_ERROR(buffer->Lock(&bufferData, NULL, NULL)); + + ProcessNV12( + [&, this](DWORD height, int plane) { + MFCopyImage(bufferData, width, data[plane], linesize[plane], width, height); + bufferData += width * height; + }, + height); + } + + CHECK_HR_ERROR(buffer->Unlock()); + CHECK_HR_ERROR(buffer->SetCurrentLength(imageSize)); + + MFFrameRateToAverageTimePerFrame(framerateNum, framerateDen, &sampleDur); + + CHECK_HR_ERROR(sample->SetSampleTime(pts * sampleDur)); + CHECK_HR_ERROR(sample->SetSampleDuration(sampleDur)); + + if (descriptor->Async()) { + CHECK_HR_ERROR(DrainEvents()); + + while (outputRequests > 0 && (hr = ProcessOutput()) == S_OK) + ; + + if (hr != MF_E_TRANSFORM_NEED_MORE_INPUT && FAILED(hr)) { + MF_LOG_COM(LOG_ERROR, "ProcessOutput()", hr); + goto fail; + } + + while (inputRequests == 0) { + hr = DrainEvent(false); + if (hr == MF_E_NO_EVENTS_AVAILABLE) { + Sleep(1); + continue; + } + if (FAILED(hr)) { + MF_LOG_COM(LOG_ERROR, "DrainEvent()", hr); + goto fail; + } + if (outputRequests > 0) { + hr = ProcessOutput(); + if (hr != MF_E_TRANSFORM_NEED_MORE_INPUT && FAILED(hr)) + goto fail; + } + } + } + + CHECK_HR_ERROR(ProcessInput(sample)); + + pendingRequests++; + + *status = SUCCESS; + return true; + +fail: + *status = FAILURE; + return false; +} + +bool AV1Encoder::ProcessInput_Tex(ID3D11Texture2D *pSurface, int64_t pts, Status *status) +{ + ProfileScope("AV1Encoder::ProcessInput"); + + HRESULT hr; + ComPtr sample; + ComPtr buffer; + UINT64 sampleDur; + + CHECK_HR_ERROR(MFCreateSample(&sample)); + CHECK_HR_ERROR(MFCreateDXGISurfaceBuffer(__uuidof(ID3D11Texture2D), pSurface, 0, FALSE, &buffer)); + CHECK_HR_ERROR(sample->AddBuffer(buffer.Get())); + + MFFrameRateToAverageTimePerFrame(framerateNum, framerateDen, &sampleDur); + + CHECK_HR_ERROR(sample->SetSampleTime(pts * sampleDur)); + CHECK_HR_ERROR(sample->SetSampleDuration(sampleDur)); + + if (descriptor->Async()) { + CHECK_HR_ERROR(DrainEvents()); + + while (outputRequests > 0 && (hr = ProcessOutput()) == S_OK) + ; + + if (hr != MF_E_TRANSFORM_NEED_MORE_INPUT && FAILED(hr)) { + MF_LOG_COM(LOG_ERROR, "ProcessOutput()", hr); + goto fail; + } + + while (inputRequests == 0) { + hr = DrainEvent(false); + if (hr == MF_E_NO_EVENTS_AVAILABLE) { + Sleep(1); + continue; + } + if (FAILED(hr)) { + MF_LOG_COM(LOG_ERROR, "DrainEvent()", hr); + goto fail; + } + if (outputRequests > 0) { + hr = ProcessOutput(); + if (hr != MF_E_TRANSFORM_NEED_MORE_INPUT && FAILED(hr)) + goto fail; + } + } + } + + CHECK_HR_ERROR(ProcessInput(sample)); + + pendingRequests++; + + *status = SUCCESS; + return true; + +fail: + *status = FAILURE; + return false; +} + +HRESULT AV1Encoder::ProcessOutput() +{ + HRESULT hr; + ComPtr sample; + MFT_OUTPUT_STREAM_INFO outputInfo = {0}; + + DWORD outputStatus = 0; + MFT_OUTPUT_DATA_BUFFER output = {0}; + ComPtr buffer; + BYTE *bufferData; + DWORD bufferLength; + INT64 samplePts; + INT64 sampleDts; + INT64 sampleDur; + std::unique_ptr> data(new std::vector()); + ComPtr type; + std::unique_ptr frame; + bool keyframe = false; + + if (descriptor->Async()) { + CHECK_HR_ERROR(DrainEvents()); + + if (outputRequests == 0) + return S_OK; + + outputRequests--; + } + + if (createOutputSample) { + CHECK_HR_ERROR(transform->GetOutputStreamInfo(0, &outputInfo)); + CHECK_HR_ERROR(CreateEmptySample(sample, buffer, outputInfo.cbSize)); + output.pSample = sample; + } else { + output.pSample = NULL; + } + + while (true) { + hr = transform->ProcessOutput(0, 1, &output, &outputStatus); + ComPtr events(output.pEvents); + + if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) + return hr; + + if (hr == MF_E_TRANSFORM_STREAM_CHANGE) { + CHECK_HR_ERROR(transform->GetOutputAvailableType(0, 0, &type)); + CHECK_HR_ERROR(transform->SetOutputType(0, type, 0)); + MF_LOG(LOG_INFO, "Updating output type to transform"); + LogMediaType(type); + if (descriptor->Async() && outputRequests > 0) { + outputRequests--; + continue; + } else { + return MF_E_TRANSFORM_NEED_MORE_INPUT; + } + } + + if (hr != S_OK) { + MF_LOG_COM(LOG_ERROR, "transform->ProcessOutput()", hr); + return hr; + } + + break; + } + + if (!createOutputSample) + sample.Set(output.pSample); + + CHECK_HR_ERROR(sample->GetBufferByIndex(0, &buffer)); + + keyframe = !!MFGetAttributeUINT32(sample, MFSampleExtension_CleanPoint, FALSE); + + CHECK_HR_ERROR(buffer->Lock(&bufferData, NULL, &bufferLength)); + + if (keyframe && extraData.empty()) + CHECK_HR_ERROR(InitializeExtraData()); + + data->reserve(bufferLength + extraData.size()); + + if (keyframe) + data->insert(data->end(), extraData.begin(), extraData.end()); + + data->insert(data->end(), &bufferData[0], &bufferData[bufferLength]); + CHECK_HR_ERROR(buffer->Unlock()); + + CHECK_HR_ERROR(sample->GetSampleDuration(&sampleDur)); + CHECK_HR_ERROR(sample->GetSampleTime(&samplePts)); + + sampleDts = MFGetAttributeUINT64(sample, MFSampleExtension_DecodeTimestamp, samplePts); + + frame.reset(new AV1Frame(keyframe, samplePts / sampleDur, sampleDts / sampleDur, std::move(data))); + + encodedFrames.push(std::move(frame)); + + return S_OK; + +fail: + return hr; +} + +bool AV1Encoder::ProcessOutput(UINT8 **data, UINT32 *dataLength, UINT64 *pts, UINT64 *dts, bool *keyframe, + Status *status) +{ + ProfileScope("AV1Encoder::ProcessOutput"); + + HRESULT hr; + + hr = ProcessOutput(); + + if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT || encodedFrames.empty()) { + *status = NEED_MORE_INPUT; + return true; + } + + if (FAILED(hr) && encodedFrames.empty()) { + *status = FAILURE; + return false; + } + + activeFrame = std::move(encodedFrames.front()); + encodedFrames.pop(); + + *data = activeFrame.get()->Data(); + *dataLength = activeFrame.get()->DataLength(); + *pts = activeFrame.get()->Pts(); + *dts = activeFrame.get()->Dts(); + *keyframe = activeFrame.get()->Keyframe(); + *status = SUCCESS; + + pendingRequests--; + + return true; +} diff --git a/plugins/win-mediafoundation/encoder/mf-av1-encoder.hpp b/plugins/win-mediafoundation/encoder/mf-av1-encoder.hpp new file mode 100644 index 00000000000000..8d8e5a5a5b77ad --- /dev/null +++ b/plugins/win-mediafoundation/encoder/mf-av1-encoder.hpp @@ -0,0 +1,162 @@ +#pragma once + +#include + +#define WIN32_MEAN_AND_LEAN +#include +#undef WIN32_MEAN_AND_LEAN + +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include + +#include "mf-encoder-descriptor.hpp" +#include "mf-common.hpp" + +namespace MF { +enum AV1Profile { + AV1ProfileMain, + +}; + +enum AV1RateControl { + AV1RateControlCBR, + AV1RateControlVBR, +}; + +struct AV1QP { + UINT16 defaultQp; + UINT16 i; + UINT16 p; + UINT16 b; + + UINT64 Pack(bool packDefault) + { + int shift = packDefault ? 0 : 16; + UINT64 packedQp; + if (packDefault) + packedQp = defaultQp; + + packedQp |= i << shift; + shift += 16; + packedQp |= p << shift; + shift += 16; + packedQp |= b << shift; + + return packedQp; + } +}; + +struct AV1Frame { +public: + AV1Frame(bool keyframe, UINT64 pts, UINT64 dts, std::unique_ptr> data) + : keyframe(keyframe), + pts(pts), + dts(dts), + data(std::move(data)) + { + } + bool Keyframe() { return keyframe; } + BYTE *Data() { return data.get()->data(); } + DWORD DataLength() { return (DWORD)data.get()->size(); } + INT64 Pts() { return pts; } + INT64 Dts() { return dts; } + +private: + AV1Frame(AV1Frame const &) = delete; + AV1Frame &operator=(AV1Frame const &) = delete; + +private: + bool keyframe; + INT64 pts; + INT64 dts; + std::unique_ptr> data; +}; + +class AV1Encoder { +public: + AV1Encoder(const obs_encoder_t *encoder, std::shared_ptr descriptor, UINT32 width, + UINT32 height, UINT32 framerateNum, UINT32 framerateDen, AV1Profile profile, UINT32 bitrate); + + ~AV1Encoder(); + + bool Initialize(std::function func); + bool ProcessInput(UINT8 **data, UINT32 *linesize, UINT64 pts, Status *status); + bool ProcessInput_Tex(ID3D11Texture2D *pSurface, int64_t pts, Status *status); + bool ProcessOutput(UINT8 **data, UINT32 *dataLength, UINT64 *pts, UINT64 *dts, bool *keyframe, Status *status); + bool ExtraData(UINT8 **data, UINT32 *dataLength); + + const obs_encoder_t *ObsEncoder() { return encoder; } + +public: + bool SetBitrate(UINT32 bitrate); + bool SetQP(AV1QP &qp); + bool SetMaxBitrate(UINT32 maxBitrate); + bool SetRateControl(AV1RateControl rateControl); + bool SetKeyframeInterval(UINT32 seconds); + bool SetLowLatency(bool lowLatency); + bool SetBufferSize(UINT32 bufferSize); + bool SetBFrameCount(UINT32 bFrames); + bool SetMinQP(UINT32 minQp); + bool SetMaxQP(UINT32 maxQp); + +private: + AV1Encoder(AV1Encoder const &) = delete; + AV1Encoder &operator=(AV1Encoder const &) = delete; + +private: + HRESULT InitializeEventGenerator(); + HRESULT InitializeExtraData(); + HRESULT CreateMediaTypes(ComPtr &inputType, ComPtr &outputType); + HRESULT EnsureCapacity(ComPtr &sample, DWORD length); + HRESULT CreateEmptySample(ComPtr &sample, ComPtr &buffer, DWORD length); + + HRESULT ProcessInput(ComPtr &sample); + HRESULT ProcessOutput(); + + HRESULT DrainEvent(bool block); + HRESULT DrainEvents(); + +private: + const obs_encoder_t *encoder; + std::shared_ptr descriptor; + const UINT32 width; + const UINT32 height; + const UINT32 framerateNum; + const UINT32 framerateDen; + const UINT32 initialBitrate; + const AV1Profile profile; + + bool createOutputSample; + ComPtr transform; + ComPtr codecApi; + + std::vector extraData; + + // The frame returned by ProcessOutput + // Valid until the next call to ProcessOutput + std::unique_ptr activeFrame; + + // Queued input samples that the encoder was not ready + // to process + std::queue> inputSamples; + + // Queued output samples that have not been returned from + // ProcessOutput yet + std::queue> encodedFrames; + + ComPtr eventGenerator; + std::atomic inputRequests = {0}; + std::atomic outputRequests = {0}; + std::atomic pendingRequests = {0}; +}; +} // namespace MF diff --git a/plugins/win-mediafoundation/encoder/mf-av1.cpp b/plugins/win-mediafoundation/encoder/mf-av1.cpp new file mode 100644 index 00000000000000..8a10bbd5ad7172 --- /dev/null +++ b/plugins/win-mediafoundation/encoder/mf-av1.cpp @@ -0,0 +1,615 @@ +#include +#include + +#include +#include + +#include "mf-av1-encoder.hpp" +#include "mf-encoder-descriptor.hpp" +#include + +using namespace MF; + +struct MFAV1_Encoder { + obs_encoder_t *encoder; + std::shared_ptr descriptor; + std::unique_ptr av1Encoder; + uint32_t width; + uint32_t height; + uint32_t framerateNum; + uint32_t framerateDen; + uint32_t keyint; + uint32_t bitrate; + uint32_t maxBitrate; + bool useMaxBitrate; + uint32_t bufferSize; + bool useBufferSize; + AV1Profile profile; + AV1RateControl rateControl; + AV1QP qp; + bool lowLatency; + uint32_t bFrames; + + const char *profiler_encode = nullptr; +}; + +ID3D11Device *d3D11Device_AV1 = NULL; +ID3D11DeviceContext *d3D11Ctx_AV1 = NULL; +ID3D11Texture2D *surface_AV1 = NULL; + +static const char *TEXT_LOW_LAT = obs_module_text("MF.AV1.LowLatency"); +static const char *TEXT_B_FRAMES = obs_module_text("MF.AV1.BFrames"); +static const char *TEXT_BITRATE = obs_module_text("MF.AV1.Bitrate"); +static const char *TEXT_CUSTOM_BUF = obs_module_text("MF.AV1.CustomBufsize"); +static const char *TEXT_BUF_SIZE = obs_module_text("MF.AV1.BufferSize"); +static const char *TEXT_USE_MAX_BITRATE = obs_module_text("MF.AV1.CustomMaxBitrate"); +static const char *TEXT_MAX_BITRATE = obs_module_text("MF.AV1.MaxBitrate"); +static const char *TEXT_KEYINT_SEC = obs_module_text("MF.AV1.KeyframeIntervalSec"); +static const char *TEXT_RATE_CONTROL = obs_module_text("MF.AV1.RateControl"); +static const char *TEXT_QPI = obs_module_text("MF.AV1.QPI"); +static const char *TEXT_QPP = obs_module_text("MF.AV1.QPP"); +static const char *TEXT_QPB = obs_module_text("MF.AV1.QPB"); +static const char *TEXT_PROFILE = obs_module_text("MF.AV1.Profile"); +static const char *TEXT_CBR = obs_module_text("MF.AV1.CBR"); +static const char *TEXT_VBR = obs_module_text("MF.AV1.VBR"); +static const char *TEXT_CQP = obs_module_text("MF.AV1.CQP"); + +constexpr const char *MFP_USE_LOWLAT = "mf_av1_use_low_latency"; +constexpr const char *MFP_B_FRAMES = "mf_av1_b_frames"; +constexpr const char *MFP_BITRATE = "mf_av1_bitrate"; +constexpr const char *MFP_USE_BUF_SIZE = "mf_av1_use_buf_size"; +constexpr const char *MFP_BUF_SIZE = "mf_av1_buf_size"; +constexpr const char *MFP_USE_MAX_BITRATE = "mf_av1_use_max_bitrate"; +constexpr const char *MFP_MAX_BITRATE = "mf_av1_max_bitrate"; +constexpr const char *MFP_KEY_INT = "mf_av1_key_int"; +constexpr const char *MFP_RATE_CONTROL = "mf_av1_rate_control"; +constexpr const char *MFP_MIN_QP = "mf_av1_min_qp"; +constexpr const char *MFP_MAX_QP = "mf_av1_max_qp"; +constexpr const char *MFP_QP_I = "mf_av1_qp_i"; +constexpr const char *MFP_QP_P = "mf_av1_qp_p"; +constexpr const char *MFP_QP_B = "mf_av1_qp_b"; +constexpr const char *MFP_PROFILE = "mf_av1_profile"; + +struct TypeData { + std::shared_ptr descriptor; + + inline TypeData(std::shared_ptr descriptor_) : descriptor(descriptor_) {} +}; + +namespace { +const char *MFAV1_GetName(void *type_data) +{ + TypeData &typeData = *reinterpret_cast(type_data); + return obs_module_text(typeData.descriptor->Name()); +} + +void set_visible(obs_properties_t *ppts, const char *name, bool visible) +{ + obs_property_t *p = obs_properties_get(ppts, name); + obs_property_set_visible(p, visible); +} + +bool use_bufsize_modified(obs_properties_t *ppts, obs_property_t *p, obs_data_t *settings) +{ + UNUSED_PARAMETER(p); + + bool use_bufsize = obs_data_get_bool(settings, MFP_USE_BUF_SIZE); + + set_visible(ppts, MFP_BUF_SIZE, use_bufsize); + + return true; +} + +bool use_max_bitrate_modified(obs_properties_t *ppts, obs_property_t *p, obs_data_t *settings) +{ + UNUSED_PARAMETER(p); + bool use_max_bitrate = obs_data_get_bool(settings, MFP_USE_MAX_BITRATE); + + return true; +} + +bool use_advanced_modified(obs_properties_t *ppts, obs_property_t *p, obs_data_t *settings) +{ + UNUSED_PARAMETER(p); + + AV1RateControl rateControl = (AV1RateControl)obs_data_get_int(settings, MFP_RATE_CONTROL); + + if (rateControl == AV1RateControlCBR || rateControl == AV1RateControlVBR) { + use_max_bitrate_modified(ppts, NULL, settings); + } + + return true; +} + +bool rate_control_modified(obs_properties_t *ppts, obs_property_t *p, obs_data_t *settings) +{ + UNUSED_PARAMETER(p); + + AV1RateControl rateControl = (AV1RateControl)obs_data_get_int(settings, MFP_RATE_CONTROL); + + set_visible(ppts, MFP_BITRATE, false); + set_visible(ppts, MFP_USE_BUF_SIZE, false); + set_visible(ppts, MFP_BUF_SIZE, false); + set_visible(ppts, MFP_USE_MAX_BITRATE, false); + set_visible(ppts, MFP_MAX_BITRATE, false); + set_visible(ppts, MFP_QP_I, false); + set_visible(ppts, MFP_QP_P, false); + set_visible(ppts, MFP_QP_B, false); + + switch (rateControl) { + case AV1RateControlCBR: + use_bufsize_modified(ppts, NULL, settings); + use_max_bitrate_modified(ppts, NULL, settings); + + set_visible(ppts, MFP_BITRATE, true); + set_visible(ppts, MFP_USE_BUF_SIZE, true); + + break; + case AV1RateControlVBR: + use_bufsize_modified(ppts, NULL, settings); + use_max_bitrate_modified(ppts, NULL, settings); + + set_visible(ppts, MFP_BITRATE, true); + set_visible(ppts, MFP_USE_BUF_SIZE, true); + + break; + default: + break; + } + + return true; +} + +obs_properties_t *MFAV1_GetProperties(void *) +{ + obs_properties_t *props = obs_properties_create(); + obs_property_t *p; + + obs_property_t *list = + obs_properties_add_list(props, MFP_PROFILE, TEXT_PROFILE, OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + + obs_property_list_add_int(list, "main", AV1ProfileMain); + + obs_properties_add_int(props, MFP_KEY_INT, TEXT_KEYINT_SEC, 0, 20, 1); + + list = obs_properties_add_list(props, MFP_RATE_CONTROL, TEXT_RATE_CONTROL, OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_INT); + + obs_property_list_add_int(list, TEXT_CBR, AV1RateControlCBR); + obs_property_list_add_int(list, TEXT_VBR, AV1RateControlVBR); + + obs_property_set_modified_callback(list, rate_control_modified); + + obs_properties_add_int(props, MFP_BITRATE, TEXT_BITRATE, 50, 10000000, 1); + + p = obs_properties_add_bool(props, MFP_USE_BUF_SIZE, TEXT_CUSTOM_BUF); + obs_property_set_modified_callback(p, use_bufsize_modified); + obs_properties_add_int(props, MFP_BUF_SIZE, TEXT_BUF_SIZE, 0, 10000000, 1); + + obs_properties_add_int(props, MFP_QP_I, TEXT_QPI, 0, 51, 1); + obs_properties_add_int(props, MFP_QP_P, TEXT_QPP, 0, 51, 1); + obs_properties_add_int(props, MFP_QP_B, TEXT_QPB, 0, 51, 1); + + p = obs_properties_add_bool(props, MFP_USE_MAX_BITRATE, TEXT_USE_MAX_BITRATE); + obs_property_set_modified_callback(p, use_max_bitrate_modified); + obs_properties_add_int(props, MFP_MAX_BITRATE, TEXT_MAX_BITRATE, 50, 10000000, 1); + + obs_properties_add_bool(props, MFP_USE_LOWLAT, TEXT_LOW_LAT); + obs_properties_add_int(props, MFP_B_FRAMES, TEXT_B_FRAMES, 0, 16, 1); + + return props; +} + +void MFAV1_GetDefaults(obs_data_t *settings) +{ + obs_data_set_default_int(settings, MFP_BITRATE, 6000); + obs_data_set_default_bool(settings, MFP_USE_LOWLAT, true); + obs_data_set_default_int(settings, MFP_B_FRAMES, 2); + obs_data_set_default_bool(settings, MFP_USE_BUF_SIZE, false); + obs_data_set_default_int(settings, MFP_BUF_SIZE, 2500); + obs_data_set_default_bool(settings, MFP_USE_MAX_BITRATE, false); + obs_data_set_default_int(settings, MFP_MAX_BITRATE, 6000); + obs_data_set_default_int(settings, MFP_KEY_INT, 2); + obs_data_set_default_int(settings, MFP_RATE_CONTROL, AV1RateControlCBR); + obs_data_set_default_int(settings, MFP_PROFILE, AV1ProfileMain); + obs_data_set_default_int(settings, MFP_MIN_QP, 1); + obs_data_set_default_int(settings, MFP_MAX_QP, 51); + obs_data_set_default_int(settings, MFP_QP_I, 26); + obs_data_set_default_int(settings, MFP_QP_B, 26); + obs_data_set_default_int(settings, MFP_QP_P, 26); +} + +void UpdateParams(MFAV1_Encoder *enc, obs_data_t *settings) +{ + video_t *video = obs_encoder_video(enc->encoder); + const struct video_output_info *voi = video_output_get_info(video); + TypeData &typeData = *reinterpret_cast(obs_encoder_get_type_data(enc->encoder)); + + enc->width = (uint32_t)obs_encoder_get_width(enc->encoder); + enc->height = (uint32_t)obs_encoder_get_height(enc->encoder); + enc->framerateNum = voi->fps_num; + enc->framerateDen = voi->fps_den; + + enc->descriptor = typeData.descriptor; + + enc->profile = static_cast(obs_data_get_int(settings, MFP_PROFILE)); + enc->rateControl = static_cast(obs_data_get_int(settings, MFP_RATE_CONTROL)); + enc->keyint = static_cast(obs_data_get_int(settings, MFP_KEY_INT)); + enc->bitrate = static_cast(obs_data_get_int(settings, MFP_BITRATE)); + enc->useBufferSize = obs_data_get_bool(settings, MFP_USE_BUF_SIZE); + enc->bufferSize = static_cast(obs_data_get_int(settings, MFP_BUF_SIZE)); + enc->useMaxBitrate = obs_data_get_bool(settings, MFP_USE_MAX_BITRATE); + enc->maxBitrate = static_cast(obs_data_get_int(settings, MFP_MAX_BITRATE)); + enc->qp.defaultQp = static_cast(obs_data_get_int(settings, MFP_QP_I)); + enc->qp.i = static_cast(obs_data_get_int(settings, MFP_QP_I)); + enc->qp.p = static_cast(obs_data_get_int(settings, MFP_QP_P)); + enc->qp.b = static_cast(obs_data_get_int(settings, MFP_QP_B)); + enc->lowLatency = obs_data_get_bool(settings, MFP_USE_LOWLAT); + enc->bFrames = static_cast(obs_data_get_int(settings, MFP_B_FRAMES)); +} +} // namespace + +namespace { +bool ApplyCBR(MFAV1_Encoder *enc) +{ + enc->av1Encoder->SetBitrate(enc->bitrate); + + if (enc->useMaxBitrate) + enc->av1Encoder->SetMaxBitrate(enc->maxBitrate); + else + enc->av1Encoder->SetMaxBitrate(enc->bitrate); + + if (enc->useBufferSize) + enc->av1Encoder->SetBufferSize(enc->bufferSize); + + return true; +} + +bool ApplyVBR(MFAV1_Encoder *enc) +{ + enc->av1Encoder->SetBitrate(enc->bitrate); + + if (enc->useBufferSize) + enc->av1Encoder->SetBufferSize(enc->bufferSize); + + return true; +} + +void *MFAV1_Create(obs_data_t *settings, obs_encoder_t *encoder) +{ + ProfileScope("MFAV1_Create"); + double bppf; + uint32_t minQp, maxQp; + float fps; + std::unique_ptr enc(new MFAV1_Encoder()); + enc->encoder = encoder; + + UpdateParams(enc.get(), settings); + + ProfileScope(enc->descriptor->Name()); + + enc->av1Encoder.reset(new AV1Encoder(encoder, enc->descriptor, enc->width, enc->height, enc->framerateNum, + enc->framerateDen, enc->profile, enc->bitrate)); + + auto applySettings = [&]() { + enc.get()->av1Encoder->SetRateControl(enc->rateControl); + enc.get()->av1Encoder->SetKeyframeInterval(enc->keyint); + + fps = (float)enc->framerateNum / (float)enc->framerateDen; + bppf = ComputeBppf((enc->bitrate * 1000), enc->width, enc->height, fps); + QpRange(QCOM_AV1, bppf, &minQp, &maxQp); + + enc.get()->av1Encoder->SetLowLatency(enc->lowLatency); + enc.get()->av1Encoder->SetBFrameCount(enc->bFrames); + + enc.get()->av1Encoder->SetMinQP(minQp); + enc.get()->av1Encoder->SetMaxQP(maxQp); + + switch (enc->rateControl) { + case AV1RateControlCBR: + return ApplyCBR(enc.get()); + case AV1RateControlVBR: + return ApplyVBR(enc.get()); + default: + return false; + } + }; + + if (!enc->av1Encoder->Initialize(applySettings)) + return nullptr; + + return enc.release(); +} + +void *MFAV1_Create_Tex(obs_data_t *settings, obs_encoder_t *encoder) +{ + ProfileScope("MFAV1_Create"); + + D3D11_TEXTURE2D_DESC desc = {}; + + video_t *video = obs_encoder_video(encoder); + const struct video_output_info *voi = video_output_get_info(video); + + MFT_REGISTER_TYPE_INFO rtInputInfo = {MFMediaType_Video, GetMFVideoFormat(voi->format)}; + MFT_REGISTER_TYPE_INFO rtInfo = {MFMediaType_Video, MFVideoFormat_AV1}; + IMFActivate **activate = NULL; + UINT32 count = 0; + ComPtr_Dev deviceManager; + uint32_t minQp, maxQp; + double bppf; + float fps; + + HRESULT hr = + MFTEnumEx(MFT_CATEGORY_VIDEO_ENCODER, MFT_ENUM_FLAG_HARDWARE, &rtInputInfo, &rtInfo, &activate, &count); + if (activate == NULL || hr != S_OK) { + obs_encoder_set_last_error( + encoder, obs_module_text("OBS does not support current color format using QCOM AV1 Encoder")); + return NULL; + } else { + blog(LOG_INFO, "Format is supported"); + } + + if (obs_encoder_scaling_enabled(encoder)) { + if (!obs_encoder_gpu_scaling_enabled(encoder)) { + blog(LOG_INFO, ">>> encoder CPU scaling active, fall back to old qsv encoder"); + return obs_encoder_create_rerouted(encoder, "qcom_av1"); + } + blog(LOG_INFO, ">>> encoder GPU scaling active"); + } + + hr = MF::CreateD3D11EncoderResources(voi, &d3D11Device_AV1, &d3D11Ctx_AV1, deviceManager, &surface_AV1); + + std::unique_ptr enc(new MFAV1_Encoder()); + enc->encoder = encoder; + + UpdateParams(enc.get(), settings); + + ProfileScope(enc->descriptor->Name()); + + enc->av1Encoder.reset(new AV1Encoder(encoder, enc->descriptor, enc->width, enc->height, enc->framerateNum, + enc->framerateDen, enc->profile, enc->bitrate)); + + auto applySettings = [&]() { + enc.get()->av1Encoder->SetRateControl(enc->rateControl); + enc.get()->av1Encoder->SetKeyframeInterval(enc->keyint); + + fps = (float)enc->framerateNum / (float)enc->framerateDen; + bppf = ComputeBppf((enc->bitrate * 1000), enc->width, enc->height, fps); + QpRange(QCOM_AV1, bppf, &minQp, &maxQp); + + enc.get()->av1Encoder->SetLowLatency(enc->lowLatency); + enc.get()->av1Encoder->SetBFrameCount(enc->bFrames); + + enc.get()->av1Encoder->SetMinQP(minQp); + enc.get()->av1Encoder->SetMaxQP(maxQp); + + switch (enc->rateControl) { + case AV1RateControlCBR: + return ApplyCBR(enc.get()); + case AV1RateControlVBR: + return ApplyVBR(enc.get()); + default: + return false; + } + }; + + if (!enc->av1Encoder->Initialize(applySettings)) + return nullptr; + + return enc.release(); +} + +void MFAV1_Destroy(void *data) +{ + MFAV1_Encoder *enc = static_cast(data); + delete enc; +} + +bool MFAV1_Encode(void *data, struct encoder_frame *frame, struct encoder_packet *packet, bool *received_packet) +{ + MFAV1_Encoder *enc = static_cast(data); + Status status; + + if (!enc->profiler_encode) + enc->profiler_encode = + profile_store_name(obs_get_profiler_name_store(), "MFAV1_Encode(%s)", enc->descriptor->Name()); + + ProfileScope(enc->profiler_encode); + + *received_packet = false; + + if (!enc->av1Encoder->ProcessInput(frame->data, frame->linesize, (frame->pts / packet->timebase_num), &status)) + return false; + + UINT8 *outputData; + UINT32 outputDataLength; + UINT64 outputPts; + UINT64 outputDts; + bool keyframe; + + if (!enc->av1Encoder->ProcessOutput(&outputData, &outputDataLength, &outputPts, &outputDts, &keyframe, &status)) + return false; + + // Needs more input, not a failure case + if (status == NEED_MORE_INPUT) + return true; + + packet->type = OBS_ENCODER_VIDEO; + packet->pts = outputPts * packet->timebase_num; + packet->dts = outputPts * packet->timebase_num; + packet->data = outputData; + packet->size = outputDataLength; + packet->keyframe = keyframe; + + *received_packet = true; + return true; +} + +static bool MFTAV1_Encode_Tex(void *data, struct encoder_texture *tex, int64_t pts, uint64_t lock_key, + uint64_t *next_key, struct encoder_packet *packet, bool *received_packet) +{ + MFAV1_Encoder *enc = static_cast(data); + Status status; + UINT8 *outputData; + UINT32 outputDataLength; + UINT64 outputPts; + UINT64 outputDts; + bool keyframe; + ID3D11Texture2D *pSurface = NULL; + + if (!enc->profiler_encode) + enc->profiler_encode = + profile_store_name(obs_get_profiler_name_store(), "MFAV1_Encode(%s)", enc->descriptor->Name()); + + ProfileScope(enc->profiler_encode); + + *received_packet = false; + Copy_Tex(tex, lock_key, next_key, d3D11Device_AV1, d3D11Ctx_AV1, surface_AV1); + + if (!enc->av1Encoder->ProcessInput_Tex(surface_AV1, pts / packet->timebase_num, &status)) + return false; + + if (!enc->av1Encoder->ProcessOutput(&outputData, &outputDataLength, &outputPts, &outputDts, &keyframe, &status)) + return false; + + // Needs more input, not a failure case + if (status == NEED_MORE_INPUT) + return true; + + packet->type = OBS_ENCODER_VIDEO; + packet->pts = outputPts * packet->timebase_num; + packet->dts = outputPts * packet->timebase_num; + packet->data = outputData; + packet->size = outputDataLength; + packet->keyframe = keyframe; + + *received_packet = true; + return true; +} + +bool MFAV1_GetExtraData(void *data, uint8_t **extra_data, size_t *size) +{ + MFAV1_Encoder *enc = static_cast(data); + + uint8_t *extraData; + UINT32 extraDataLength; + + if (!enc->av1Encoder->ExtraData(&extraData, &extraDataLength)) + return false; + + *extra_data = extraData; + *size = extraDataLength; + + return true; +} + +bool MFAV1_GetSEIData(void *data, uint8_t **sei_data, size_t *size) +{ + UNUSED_PARAMETER(data); + UNUSED_PARAMETER(sei_data); + UNUSED_PARAMETER(size); + + return false; +} + +void MFAV1_GetVideoInfo(void *, struct video_scale_info *info) +{ + info->format = VIDEO_FORMAT_NV12; +} + +bool MFAV1_Update(void *data, obs_data_t *settings) +{ + MFAV1_Encoder *enc = static_cast(data); + + UpdateParams(enc, settings); + + enc->av1Encoder->SetBitrate(enc->bitrate); + enc->av1Encoder->SetQP(enc->qp); + + return true; +} + +bool CanSpawnEncoder(std::shared_ptr descriptor) +{ + HRESULT hr; + ComPtr transform; + + hr = CoCreateInstance(descriptor->Guid(), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&transform)); + return hr == S_OK; +} +} // namespace + +void RegisterMFAV1Encoders() +{ + obs_encoder_info info = {0}; + info.type = OBS_ENCODER_VIDEO; + info.get_name = MFAV1_GetName; + info.create = MFAV1_Create_Tex; + info.destroy = MFAV1_Destroy; + info.encode = MFAV1_Encode; + info.update = MFAV1_Update; + info.encode_texture2 = MFTAV1_Encode_Tex; + info.get_properties = MFAV1_GetProperties; + info.get_defaults = MFAV1_GetDefaults; + info.get_extra_data = MFAV1_GetExtraData; + info.get_sei_data = MFAV1_GetSEIData; + info.get_video_info = MFAV1_GetVideoInfo; + info.codec = "av1"; + info.caps = OBS_ENCODER_CAP_PASS_TEXTURE | OBS_ENCODER_CAP_DYN_BITRATE; + + auto encoders = EncoderDescriptor::Enumerate("av1"); + for (auto e : encoders) { + + /* ignore the software encoder due to the fact that we already + * have an objectively superior software encoder available */ + if (e->Type() == EncoderType::H264_SOFTWARE) + continue; + + /* certain encoders such as quicksync will be "available" but + * not usable with certain processors */ + if (!CanSpawnEncoder(e)) + continue; + + info.id = e->Id(); + info.type_data = new TypeData(e); + info.free_type_data = [](void *type_data) { + delete reinterpret_cast(type_data); + }; + obs_register_encoder(&info); + } + + obs_encoder_info info_soft = {0}; + info_soft.type = OBS_ENCODER_VIDEO; + info_soft.get_name = MFAV1_GetName; + info_soft.create = MFAV1_Create; + info_soft.destroy = MFAV1_Destroy; + info_soft.encode = MFAV1_Encode; + info_soft.update = MFAV1_Update; + info_soft.get_properties = MFAV1_GetProperties; + info_soft.get_defaults = MFAV1_GetDefaults; + info_soft.get_extra_data = MFAV1_GetExtraData; + info_soft.get_sei_data = MFAV1_GetSEIData; + info_soft.get_video_info = MFAV1_GetVideoInfo; + info_soft.codec = "av1"; + info_soft.caps = OBS_ENCODER_CAP_DYN_BITRATE | OBS_ENCODER_CAP_INTERNAL | OBS_ENCODER_CAP_DEPRECATED; + + for (auto e : encoders) { + + /* ignore the software encoder due to the fact that we already + * have an objectively superior software encoder available */ + if (e->Type() == EncoderType::H264_SOFTWARE) + continue; + + /* certain encoders such as quicksync will be "available" but + * not usable with certain processors */ + if (!CanSpawnEncoder(e)) + continue; + + info_soft.id = "qcom_av1"; + info_soft.type_data = new TypeData(e); + info_soft.free_type_data = [](void *type_data) { + delete reinterpret_cast(type_data); + }; + obs_register_encoder(&info_soft); + } +} diff --git a/plugins/win-mediafoundation/encoder/mf-common.cpp b/plugins/win-mediafoundation/encoder/mf-common.cpp new file mode 100644 index 00000000000000..760df45ad4f5e9 --- /dev/null +++ b/plugins/win-mediafoundation/encoder/mf-common.cpp @@ -0,0 +1,603 @@ +#include "mf-common.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +using namespace std; + +namespace std { +template<> struct hash { + std::size_t operator()(const GUID &guid) const noexcept + { + const uint64_t *p = reinterpret_cast(&guid); + return std::hash()(p[0]) ^ std::hash()(p[1]); + } +}; +} // namespace std + +namespace { +void DBGMSG(PCWSTR format, ...) +{ + va_list args; + va_start(args, format); + + WCHAR msg[MAX_PATH]; + + if (SUCCEEDED(StringCbVPrintf(msg, sizeof(msg), format, args))) { + char *cmsg; + os_wcs_to_utf8_ptr(msg, 0, &cmsg); + MF::MF_LOG(LOG_INFO, "%s", cmsg); + bfree(cmsg); + } +} + +LPCWSTR GetGUIDNameConst(const GUID &guid) +{ + static const std::unordered_map> guidMap = { + {MF_MT_MAJOR_TYPE, L"MF_MT_MAJOR_TYPE"}, + {MF_MT_SUBTYPE, L"MF_MT_SUBTYPE"}, + {MF_MT_ALL_SAMPLES_INDEPENDENT, L"MF_MT_ALL_SAMPLES_INDEPENDENT"}, + {MF_MT_FIXED_SIZE_SAMPLES, L"MF_MT_FIXED_SIZE_SAMPLES"}, + {MF_MT_COMPRESSED, L"MF_MT_COMPRESSED"}, + {MF_MT_SAMPLE_SIZE, L"MF_MT_SAMPLE_SIZE"}, + {MF_MT_WRAPPED_TYPE, L"MF_MT_WRAPPED_TYPE"}, + {MF_MT_AUDIO_NUM_CHANNELS, L"MF_MT_AUDIO_NUM_CHANNELS"}, + {MF_MT_AUDIO_SAMPLES_PER_SECOND, L"MF_MT_AUDIO_SAMPLES_PER_SECOND"}, + {MF_MT_AUDIO_FLOAT_SAMPLES_PER_SECOND, L"MF_MT_AUDIO_FLOAT_SAMPLES_PER_SECOND"}, + {MF_MT_AUDIO_AVG_BYTES_PER_SECOND, L"MF_MT_AUDIO_AVG_BYTES_PER_SECOND"}, + {MF_MT_AUDIO_BLOCK_ALIGNMENT, L"MF_MT_AUDIO_BLOCK_ALIGNMENT"}, + {MF_MT_AUDIO_BITS_PER_SAMPLE, L"MF_MT_AUDIO_BITS_PER_SAMPLE"}, + {MF_MT_AUDIO_VALID_BITS_PER_SAMPLE, L"MF_MT_AUDIO_VALID_BITS_PER_SAMPLE"}, + {MF_MT_AUDIO_SAMPLES_PER_BLOCK, L"MF_MT_AUDIO_SAMPLES_PER_BLOCK"}, + {MF_MT_AUDIO_CHANNEL_MASK, L"MF_MT_AUDIO_CHANNEL_MASK"}, + {MF_MT_AUDIO_FOLDDOWN_MATRIX, L"MF_MT_AUDIO_FOLDDOWN_MATRIX"}, + {MF_MT_AUDIO_WMADRC_PEAKREF, L"MF_MT_AUDIO_WMADRC_PEAKREF"}, + {MF_MT_AUDIO_WMADRC_PEAKTARGET, L"MF_MT_AUDIO_WMADRC_PEAKTARGET"}, + {MF_MT_AUDIO_WMADRC_AVGREF, L"MF_MT_AUDIO_WMADRC_AVGREF"}, + {MF_MT_AUDIO_WMADRC_AVGTARGET, L"MF_MT_AUDIO_WMADRC_AVGTARGET"}, + {MF_MT_AUDIO_PREFER_WAVEFORMATEX, L"MF_MT_AUDIO_PREFER_WAVEFORMATEX"}, + {MF_MT_FRAME_SIZE, L"MF_MT_FRAME_SIZE"}, + {MF_MT_FRAME_RATE, L"MF_MT_FRAME_RATE"}, + {MF_MT_FRAME_RATE_RANGE_MAX, L"MF_MT_FRAME_RATE_RANGE_MAX"}, + {MF_MT_FRAME_RATE_RANGE_MIN, L"MF_MT_FRAME_RATE_RANGE_MIN"}, + {MF_MT_PIXEL_ASPECT_RATIO, L"MF_MT_PIXEL_ASPECT_RATIO"}, + {MF_MT_DRM_FLAGS, L"MF_MT_DRM_FLAGS"}, + {MF_MT_PAD_CONTROL_FLAGS, L"MF_MT_PAD_CONTROL_FLAGS"}, + {MF_MT_SOURCE_CONTENT_HINT, L"MF_MT_SOURCE_CONTENT_HINT"}, + {MF_MT_VIDEO_CHROMA_SITING, L"MF_MT_VIDEO_CHROMA_SITING"}, + {MF_MT_INTERLACE_MODE, L"MF_MT_INTERLACE_MODE"}, + {MF_MT_TRANSFER_FUNCTION, L"MF_MT_TRANSFER_FUNCTION"}, + {MF_MT_VIDEO_PRIMARIES, L"MF_MT_VIDEO_PRIMARIES"}, + {MF_MT_CUSTOM_VIDEO_PRIMARIES, L"MF_MT_CUSTOM_VIDEO_PRIMARIES"}, + {MF_MT_YUV_MATRIX, L"MF_MT_YUV_MATRIX"}, + {MF_MT_VIDEO_LIGHTING, L"MF_MT_VIDEO_LIGHTING"}, + {MF_MT_VIDEO_NOMINAL_RANGE, L"MF_MT_VIDEO_NOMINAL_RANGE"}, + {MF_MT_GEOMETRIC_APERTURE, L"MF_MT_GEOMETRIC_APERTURE"}, + {MF_MT_MINIMUM_DISPLAY_APERTURE, L"MF_MT_MINIMUM_DISPLAY_APERTURE"}, + {MF_MT_PAN_SCAN_APERTURE, L"MF_MT_PAN_SCAN_APERTURE"}, + {MF_MT_PAN_SCAN_ENABLED, L"MF_MT_PAN_SCAN_ENABLED"}, + {MF_MT_AVG_BITRATE, L"MF_MT_AVG_BITRATE"}, + {MF_MT_AVG_BIT_ERROR_RATE, L"MF_MT_AVG_BIT_ERROR_RATE"}, + {MF_MT_MAX_KEYFRAME_SPACING, L"MF_MT_MAX_KEYFRAME_SPACING"}, + {MF_MT_DEFAULT_STRIDE, L"MF_MT_DEFAULT_STRIDE"}, + {MF_MT_PALETTE, L"MF_MT_PALETTE"}, + {MF_MT_USER_DATA, L"MF_MT_USER_DATA"}, + {MF_MT_AM_FORMAT_TYPE, L"MF_MT_AM_FORMAT_TYPE"}, + {MF_MT_MPEG_START_TIME_CODE, L"MF_MT_MPEG_START_TIME_CODE"}, + {MF_MT_VIDEO_LEVEL, L"MF_MT_VIDEO_LEVEL"}, + {MF_MT_VIDEO_PROFILE, L"MF_MT_VIDEO_PROFILE"}, + {MF_MT_MPEG2_FLAGS, L"MF_MT_MPEG2_FLAGS"}, + {MF_MT_MPEG_SEQUENCE_HEADER, L"MF_MT_MPEG_SEQUENCE_HEADER"}, + {MF_MT_DV_AAUX_SRC_PACK_0, L"MF_MT_DV_AAUX_SRC_PACK_0"}, + {MF_MT_DV_AAUX_CTRL_PACK_0, L"MF_MT_DV_AAUX_CTRL_PACK_0"}, + {MF_MT_DV_AAUX_SRC_PACK_1, L"MF_MT_DV_AAUX_SRC_PACK_1"}, + {MF_MT_DV_AAUX_CTRL_PACK_1, L"MF_MT_DV_AAUX_CTRL_PACK_1"}, + {MF_MT_DV_VAUX_SRC_PACK, L"MF_MT_DV_VAUX_SRC_PACK"}, + {MF_MT_DV_VAUX_CTRL_PACK, L"MF_MT_DV_VAUX_CTRL_PACK"}, + {MF_MT_ARBITRARY_HEADER, L"MF_MT_ARBITRARY_HEADER"}, + {MF_MT_ARBITRARY_FORMAT, L"MF_MT_ARBITRARY_FORMAT"}, + {MF_MT_IMAGE_LOSS_TOLERANT, L"MF_MT_IMAGE_LOSS_TOLERANT"}, + {MF_MT_MPEG4_SAMPLE_DESCRIPTION, L"MF_MT_MPEG4_SAMPLE_DESCRIPTION"}, + {MF_MT_MPEG4_CURRENT_SAMPLE_ENTRY, L"MF_MT_MPEG4_CURRENT_SAMPLE_ENTRY"}, + {MF_MT_ORIGINAL_4CC, L"MF_MT_ORIGINAL_4CC"}, + {MF_MT_ORIGINAL_WAVE_FORMAT_TAG, L"MF_MT_ORIGINAL_WAVE_FORMAT_TAG"}, + {MFMediaType_Audio, L"MFMediaType_Audio"}, + {MFMediaType_Video, L"MFMediaType_Video"}, + {MFMediaType_Protected, L"MFMediaType_Protected"}, + {MFMediaType_SAMI, L"MFMediaType_SAMI"}, + {MFMediaType_Script, L"MFMediaType_Script"}, + {MFMediaType_Image, L"MFMediaType_Image"}, + {MFMediaType_HTML, L"MFMediaType_HTML"}, + {MFMediaType_Binary, L"MFMediaType_Binary"}, + {MFMediaType_FileTransfer, L"MFMediaType_FileTransfer"}, + {MFVideoFormat_AI44, L"MFVideoFormat_AI44"}, + {MFVideoFormat_ARGB32, L"MFVideoFormat_ARGB32"}, + {MFVideoFormat_AYUV, L"MFVideoFormat_AYUV"}, + {MFVideoFormat_DV25, L"MFVideoFormat_DV25"}, + {MFVideoFormat_DV50, L"MFVideoFormat_DV50"}, + {MFVideoFormat_DVH1, L"MFVideoFormat_DVH1"}, + {MFVideoFormat_DVSD, L"MFVideoFormat_DVSD"}, + {MFVideoFormat_DVSL, L"MFVideoFormat_DVSL"}, + {MFVideoFormat_H264, L"MFVideoFormat_H264"}, + {MFVideoFormat_HEVC, L"MFVideoFormat_HEVC"}, + {MFVideoFormat_H265, L"MFVideoFormat_H265"}, + {MFVideoFormat_I420, L"MFVideoFormat_I420"}, + {MFVideoFormat_IYUV, L"MFVideoFormat_IYUV"}, + {MFVideoFormat_M4S2, L"MFVideoFormat_M4S2"}, + {MFVideoFormat_MJPG, L"MFVideoFormat_MJPG"}, + {MFVideoFormat_MP43, L"MFVideoFormat_MP43"}, + {MFVideoFormat_MP4S, L"MFVideoFormat_MP4S"}, + {MFVideoFormat_MP4V, L"MFVideoFormat_MP4V"}, + {MFVideoFormat_MPG1, L"MFVideoFormat_MPG1"}, + {MFVideoFormat_MSS1, L"MFVideoFormat_MSS1"}, + {MFVideoFormat_MSS2, L"MFVideoFormat_MSS2"}, + {MFVideoFormat_NV11, L"MFVideoFormat_NV11"}, + {MFVideoFormat_NV12, L"MFVideoFormat_NV12"}, + {MFVideoFormat_P010, L"MFVideoFormat_P010"}, + {MFVideoFormat_P016, L"MFVideoFormat_P016"}, + {MFVideoFormat_P210, L"MFVideoFormat_P210"}, + {MFVideoFormat_P216, L"MFVideoFormat_P216"}, + {MFVideoFormat_RGB24, L"MFVideoFormat_RGB24"}, + {MFVideoFormat_RGB32, L"MFVideoFormat_RGB32"}, + {MFVideoFormat_RGB555, L"MFVideoFormat_RGB555"}, + {MFVideoFormat_RGB565, L"MFVideoFormat_RGB565"}, + {MFVideoFormat_RGB8, L"MFVideoFormat_RGB8"}, + {MFVideoFormat_UYVY, L"MFVideoFormat_UYVY"}, + {MFVideoFormat_v210, L"MFVideoFormat_v210"}, + {MFVideoFormat_v410, L"MFVideoFormat_v410"}, + {MFVideoFormat_WMV1, L"MFVideoFormat_WMV1"}, + {MFVideoFormat_WMV2, L"MFVideoFormat_WMV2"}, + {MFVideoFormat_WMV3, L"MFVideoFormat_WMV3"}, + {MFVideoFormat_WVC1, L"MFVideoFormat_WVC1"}, + {MFVideoFormat_Y210, L"MFVideoFormat_Y210"}, + {MFVideoFormat_Y216, L"MFVideoFormat_Y216"}, + {MFVideoFormat_Y410, L"MFVideoFormat_Y410"}, + {MFVideoFormat_Y416, L"MFVideoFormat_Y416"}, + {MFVideoFormat_Y41P, L"MFVideoFormat_Y41P"}, + {MFVideoFormat_Y41T, L"MFVideoFormat_Y41T"}, + {MFVideoFormat_YUY2, L"MFVideoFormat_YUY2"}, + {MFVideoFormat_YV12, L"MFVideoFormat_YV12"}, + {MFVideoFormat_YVYU, L"MFVideoFormat_YVYU"}, + {MFAudioFormat_PCM, L"MFAudioFormat_PCM"}, + {MFAudioFormat_Float, L"MFAudioFormat_Float"}, + {MFAudioFormat_DTS, L"MFAudioFormat_DTS"}, + {MFAudioFormat_Dolby_AC3_SPDIF, L"MFAudioFormat_Dolby_AC3_SPDIF"}, + {MFAudioFormat_DRM, L"MFAudioFormat_DRM"}, + {MFAudioFormat_WMAudioV8, L"MFAudioFormat_WMAudioV8"}, + {MFAudioFormat_WMAudioV9, L"MFAudioFormat_WMAudioV9"}, + {MFAudioFormat_WMAudio_Lossless, L"MFAudioFormat_WMAudio_Lossless"}, + {MFAudioFormat_WMASPDIF, L"MFAudioFormat_WMASPDIF"}, + {MFAudioFormat_MSP1, L"MFAudioFormat_MSP1"}, + {MFAudioFormat_MP3, L"MFAudioFormat_MP3"}, + {MFAudioFormat_MPEG, L"MFAudioFormat_MPEG"}, + {MFAudioFormat_ADTS, L"MFAudioFormat_ADTS"}}; + + auto it = guidMap.find(guid); + return (it != guidMap.end()) ? it->second : nullptr; +} + +float OffsetToFloat(const MFOffset &offset) +{ + return offset.value + (static_cast(offset.fract) / 65536.0f); +} + +HRESULT LogVideoArea(wstring &str, const PROPVARIANT &var) +{ + if (var.caub.cElems < sizeof(MFVideoArea)) { + return MF_E_BUFFERTOOSMALL; + } + + MFVideoArea *pArea = (MFVideoArea *)var.caub.pElems; + + str += L"("; + str += to_wstring(OffsetToFloat(pArea->OffsetX)); + str += L","; + str += to_wstring(OffsetToFloat(pArea->OffsetY)); + str += L") ("; + str += to_wstring(pArea->Area.cx); + str += L","; + str += to_wstring(pArea->Area.cy); + str += L")"; + return S_OK; +} + +HRESULT GetGUIDName(const GUID &guid, WCHAR **outGuidName) +{ + HRESULT hr = S_OK; + WCHAR *pName = NULL; + + LPCWSTR guidNameConst = GetGUIDNameConst(guid); + if (guidNameConst) { + size_t cchLength = 0; + + hr = StringCchLength(guidNameConst, STRSAFE_MAX_CCH, &cchLength); + if (FAILED(hr)) { + goto done; + } + + pName = (WCHAR *)CoTaskMemAlloc((cchLength + 1) * sizeof(WCHAR)); + + if (pName == NULL) { + hr = E_OUTOFMEMORY; + goto done; + } + + hr = StringCchCopy(pName, cchLength + 1, guidNameConst); + if (FAILED(hr)) { + goto done; + } + } else { + hr = StringFromCLSID(guid, &pName); + } + +done: + if (FAILED(hr)) { + *outGuidName = NULL; + CoTaskMemFree(pName); + } else { + *outGuidName = pName; + } + return hr; +} + +void LogUINT32AsUINT64(wstring &str, const PROPVARIANT &var) +{ + UINT32 uHigh = 0, uLow = 0; + Unpack2UINT32AsUINT64(var.uhVal.QuadPart, &uHigh, &uLow); + str += to_wstring(uHigh); + str += L" x "; + str += to_wstring(uLow); +} + +// Handle certain known special cases. +HRESULT SpecialCaseAttributeValue(wstring &str, GUID guid, const PROPVARIANT &var) +{ + if ((guid == MF_MT_FRAME_RATE) || (guid == MF_MT_FRAME_RATE_RANGE_MAX) || + (guid == MF_MT_FRAME_RATE_RANGE_MIN) || (guid == MF_MT_FRAME_SIZE) || (guid == MF_MT_PIXEL_ASPECT_RATIO)) { + + // Attributes that contain two packed 32-bit values. + LogUINT32AsUINT64(str, var); + + } else if ((guid == MF_MT_GEOMETRIC_APERTURE) || (guid == MF_MT_MINIMUM_DISPLAY_APERTURE) || + (guid == MF_MT_PAN_SCAN_APERTURE)) { + + // Attributes that an MFVideoArea structure. + return LogVideoArea(str, var); + + } else { + return S_FALSE; + } + return S_OK; +} + +HRESULT LogAttributeValueByIndex(IMFAttributes *pAttr, DWORD index) +{ + wstring str; + + WCHAR *pGuidName = NULL; + WCHAR *pGuidValName = NULL; + + GUID guid = {0}; + + PROPVARIANT var; + PropVariantInit(&var); + + HRESULT hr = pAttr->GetItemByIndex(index, &guid, &var); + if (FAILED(hr)) { + goto done; + } + + hr = GetGUIDName(guid, &pGuidName); + if (FAILED(hr)) { + goto done; + } + + str += L" "; + str += pGuidName; + str += L": "; + + hr = SpecialCaseAttributeValue(str, guid, var); + if (FAILED(hr)) { + goto done; + } + + if (hr == S_FALSE) { + switch (var.vt) { + case VT_UI4: + str += to_wstring(var.ulVal); + break; + + case VT_UI8: + str += to_wstring(var.uhVal.QuadPart); + break; + + case VT_R8: + str += to_wstring(var.dblVal); + break; + + case VT_CLSID: + hr = GetGUIDName(*var.puuid, &pGuidValName); + if (SUCCEEDED(hr)) { + str += pGuidValName; + } + break; + + case VT_LPWSTR: + str += var.pwszVal; + break; + + case VT_VECTOR | VT_UI1: + str += L"<>"; + break; + + case VT_UNKNOWN: + str += L"IUnknown"; + break; + + default: + str += L"Unexpected attribute type (vt = "; + str += to_wstring(var.vt); + str += L")"; + break; + } + } + + DBGMSG(L"%s", str.c_str()); + +done: + CoTaskMemFree(pGuidName); + CoTaskMemFree(pGuidValName); + PropVariantClear(&var); + return hr; +} +} // namespace + +void MF::MF_LOG(int level, const char *format, ...) +{ + constexpr size_t buf_size = 1024; + char buffer[buf_size]; + + va_list args; + va_start(args, format); + vsnprintf(buffer, buf_size, format, args); + va_end(args); + + blog(level, "[Media Foundation encoder]: %s", buffer); +} + +void MF::MF_LOG_COM(int level, const char *msg, HRESULT hr) +{ + MF_LOG(level, "%s failed, %S (0x%08lx)", msg, _com_error(hr).ErrorMessage(), hr); +} + +bool MF::LogMediaType(IMFMediaType *pType) +{ + UINT32 count = 0; + + HRESULT hr = pType->GetCount(&count); + if (FAILED(hr)) { + return false; + } + + if (count == 0) { + DBGMSG(L"Empty media type."); + } + + for (UINT32 i = 0; i < count; i++) { + hr = LogAttributeValueByIndex(pType, i); + if (FAILED(hr)) { + return false; + } + } + return true; +} + +const GUID &MF::GetMFVideoFormat(video_format format) +{ + switch (format) { + case VIDEO_FORMAT_P010: + return MFVideoFormat_P010; + case VIDEO_FORMAT_NV12: + return MFVideoFormat_NV12; + case VIDEO_FORMAT_YUY2: + return MFVideoFormat_YUY2; + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_I444: + return MFVideoFormat_I420; + case VIDEO_FORMAT_P216: + case VIDEO_FORMAT_P416: + case VIDEO_FORMAT_BGRA: + return MFVideoFormat_P216; + default: + return MFVideoFormat_P216; + } +} + +void MF::Copy_Tex(void *tex, uint64_t lock_key, uint64_t *next_key, ID3D11Device *d3D11Device, + ID3D11DeviceContext *d3D11Ctx, ID3D11Texture2D *surface) +{ + struct encoder_texture *ptex = (struct encoder_texture *)tex; + HRESULT hr; + D3D11_BOX SrcBox; + D3D11_TEXTURE2D_DESC desc = {0}; + + IDXGIKeyedMutex *km; + ID3D11Texture2D *input_tex = NULL; + + hr = d3D11Device->OpenSharedResource((HANDLE)(uintptr_t)ptex->handle, IID_ID3D11Texture2D, (void **)&input_tex); + + if (FAILED(hr)) + blog(LOG_INFO, "Failed to OpenSharedResource"); + + hr = input_tex->QueryInterface(IID_IDXGIKeyedMutex, (void **)&km); + if (FAILED(hr)) { + input_tex->Release(); + blog(LOG_INFO, "Failed to Query interface"); + } + + input_tex->SetEvictionPriority(DXGI_RESOURCE_PRIORITY_MAXIMUM); + + km->AcquireSync(lock_key, INFINITE); + + input_tex->GetDesc(&desc); + SrcBox = {0, 0, 0, desc.Width, desc.Height, 1}; + if (input_tex != NULL) + d3D11Ctx->CopySubresourceRegion(surface, 0, 0, 0, 0, input_tex, 0, &SrcBox); + + km->ReleaseSync(*next_key); + + km->Release(); + input_tex->Release(); +} + +HRESULT MF::CreateD3D11EncoderResources(const video_output_info *voi, ID3D11Device **device, + ID3D11DeviceContext **context, ComPtr_Dev &deviceManager, + ID3D11Texture2D **surface) +{ + if (!voi || !device || !context || !surface) + return E_INVALIDARG; + + HRESULT hr = S_OK; + UINT resetToken = 0; + + // Driver types supported + D3D_DRIVER_TYPE DriverTypes[] = { + D3D_DRIVER_TYPE_HARDWARE, + D3D_DRIVER_TYPE_WARP, + D3D_DRIVER_TYPE_REFERENCE, + }; + UINT NumDriverTypes = ARRAYSIZE(DriverTypes); + + // Feature levels supported + D3D_FEATURE_LEVEL FeatureLevels[] = {D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL_9_1}; + UINT NumFeatureLevels = ARRAYSIZE(FeatureLevels); + + D3D_FEATURE_LEVEL FeatureLevel; + + // Create device + for (UINT DriverTypeIndex = 0; DriverTypeIndex < NumDriverTypes; ++DriverTypeIndex) { + hr = D3D11CreateDevice(nullptr, DriverTypes[DriverTypeIndex], nullptr, + D3D11_CREATE_DEVICE_VIDEO_SUPPORT, FeatureLevels, NumFeatureLevels, + D3D11_SDK_VERSION, device, &FeatureLevel, context); + if (SUCCEEDED(hr)) { + blog(LOG_INFO, "D3D11CreateDevice successful"); + break; + } else { + _com_error err(hr); + blog(LOG_ERROR, "D3D11CreateDevice failed: %ls", err.ErrorMessage()); + } + } + + // Create DXGI device manager + hr = MFCreateDXGIDeviceManager(&resetToken, deviceManager.GetAddressOf()); + if (FAILED(hr)) { + blog(LOG_ERROR, "Failed to create DXGIDeviceManager hr=0x%08x", (unsigned)hr); + return hr; + } + if (device != NULL) + hr = deviceManager->ResetDevice(*device, resetToken); + + if (FAILED(hr)) { + blog(LOG_ERROR, "Failed to assign D3D device to device manager hr=0x%08x", (unsigned)hr); + return hr; + } + + // Choose format + DXGI_FORMAT format = (voi->format == VIDEO_FORMAT_NV12) ? DXGI_FORMAT_NV12 : DXGI_FORMAT_P010; + + // Create texture + D3D11_TEXTURE2D_DESC desc = {}; + desc.Width = voi->width; + desc.Height = voi->height; + desc.Format = format; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_DECODER | D3D11_BIND_SHADER_RESOURCE; + desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED; + + hr = (*device)->CreateTexture2D(&desc, nullptr, surface); + if (FAILED(hr)) { + blog(LOG_ERROR, "CreateTexture2D failed hr=0x%08x", (unsigned)hr); + return hr; + } + + return S_OK; +} + +double MF::ComputeBppf(double bitrate_bps, uint32_t width, uint32_t height, double fps) +{ + if (bitrate_bps <= 0 || width <= 0 || height <= 0 || fps <= 0.0) + return 0.0; + return bitrate_bps / (static_cast(width) * height * fps); +} + +void MF::QpRange(Codec Codec, double bppf, uint32_t *minQp, uint32_t *maxQp) +{ + // Sanity + if (!(bppf > 0.0) || !std::isfinite(bppf)) { + // Default to a moderate content density if input is invalid + bppf = 0.10; // ~“typical” real-time density + } + + // Defaults (H.264-like) + uint32_t qp_min_limit = 0; + uint32_t qp_max_limit = 51; + double codec_offset = 0.0; + + // Normalize codec properties + if (Codec == QCOM_H264) { + qp_min_limit = 0; + qp_max_limit = 51; + codec_offset = 0.0; + } else if (Codec == QCOM_H265) { + qp_min_limit = 0; + qp_max_limit = 51; + codec_offset = -2.0; + } else if (Codec == QCOM_AV1) { + qp_min_limit = 0; + qp_max_limit = 63; + codec_offset = -4.0; + } else { + // Unknown codec: be permissive. + qp_min_limit = 0; + qp_max_limit = 63; + codec_offset = 0.0; + } + + // Heuristic calibration + // Pick α, β so that: + // - bppf ~ 0.30 → low QP (good quality, high bits per pixel) + // - bppf ~ 0.10 → mid QP + // - bppf ~ 0.04 → higher QP (low bits per pixel) + // + // α = 12, β = 4 gives reasonable real-time ranges across codecs. + const double alpha = 12.0; + const double beta = 4.0; + + // base QP from the model + double base_qp_f = alpha - beta * std::log2(bppf) + codec_offset; + uint32_t base_qp = static_cast(std::lround(base_qp_f)); + + // Clamp to legal range + base_qp = std::max(qp_min_limit, std::min(base_qp, qp_max_limit)); + + // Provide a window around the base QP. + // Wider on the high side to let the encoder save bits during simple scenes. + uint32_t min_qp = std::max(qp_min_limit, base_qp - 6); + uint32_t max_qp = std::min(qp_max_limit, base_qp + 10); + + if (min_qp > max_qp) + std::swap(min_qp, max_qp); + *minQp = min_qp; + *maxQp = max_qp; +} diff --git a/plugins/win-mediafoundation/encoder/mf-common.hpp b/plugins/win-mediafoundation/encoder/mf-common.hpp new file mode 100644 index 00000000000000..b8ff40cbe7b4e0 --- /dev/null +++ b/plugins/win-mediafoundation/encoder/mf-common.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "dxguid.lib") +#pragma comment(lib, "wbemuuid.lib") +#pragma comment(lib, "setupapi.lib") +#pragma comment(lib, "D3D11.lib") +#pragma comment(lib, "mfplat.lib") +#pragma comment(lib, "mfuuid.lib") +#pragma comment(lib, "Winmm.lib") + +#ifndef CHECK_HR_ERROR +#define CHECK_HR_ERROR(r) \ + if (FAILED(hr = (r))) { \ + MF_LOG_COM(LOG_ERROR, #r, hr); \ + goto fail; \ + } +#endif + +#ifndef CHECK_HR_LEVEL +#define CHECK_HR_LEVEL(level, r) \ + if (FAILED(hr = (r))) { \ + MF_LOG_COM(level, #r, hr); \ + goto fail; \ + } +#endif + +#ifndef CHECK_HR_WARNING +#define CHECK_HR_WARNING(r) \ + if (FAILED(hr = (r))) \ + MF_LOG_COM(LOG_WARNING, #r, hr); +#endif + +namespace MF { +enum Status { FAILURE, SUCCESS, NOT_ACCEPTING, NEED_MORE_INPUT }; +enum Codec { QCOM_H264, QCOM_H265, QCOM_AV1 }; + +bool LogMediaType(IMFMediaType *mediaType); +void MF_LOG(int level, const char *format, ...); +void MF_LOG_COM(int level, const char *msg, HRESULT hr); +const GUID &GetMFVideoFormat(video_format format); +void Copy_Tex(void *tex, uint64_t lock_key, uint64_t *next_key, ID3D11Device *g_pD3D11Device_av1_1, + ID3D11DeviceContext *g_pD3D11Ctx_av1_1, ID3D11Texture2D *pSurface_av1_1); +template using ComPtr_Dev = Microsoft::WRL::ComPtr; +HRESULT CreateD3D11EncoderResources(const video_output_info *voi, ID3D11Device **ppDevice, + ID3D11DeviceContext **ppContext, ComPtr_Dev &deviceManager, + ID3D11Texture2D **ppSurface); +void QpRange(Codec codec, double bppf, uint32_t *minQp, uint32_t *maxQp); +double ComputeBppf(double bitrate_bps, uint32_t width, uint32_t height, double fps); +} // namespace MF diff --git a/plugins/win-mediafoundation/encoder/mf-encoder-descriptor.cpp b/plugins/win-mediafoundation/encoder/mf-encoder-descriptor.cpp new file mode 100644 index 00000000000000..741b7c1fdf6159 --- /dev/null +++ b/plugins/win-mediafoundation/encoder/mf-encoder-descriptor.cpp @@ -0,0 +1,197 @@ +#include +#include +#include +#include +#include + +#include "mf-encoder-descriptor.hpp" + +template class ComHeapPtr { + +protected: + T *ptr = nullptr; + + inline void Kill() + { + if (ptr) { + CoTaskMemFree(ptr); + ptr = nullptr; + } + } + + inline void Replace(T *p) + { + if (ptr != p) { + if (ptr) + ptr->Kill(); + ptr = p; + } + } + +public: + inline ComHeapPtr() : ptr(nullptr) {} + inline ComHeapPtr(T *p) : ptr(p) {} + inline ComHeapPtr(const ComHeapPtr &c) = delete; + inline ComHeapPtr(ComHeapPtr &&c) = delete; + inline ~ComHeapPtr() { Kill(); } + + inline void Clear() + { + if (ptr) { + Kill(); + ptr = nullptr; + } + } + + inline ComPtr &operator=(T *p) + { + Replace(p); + return *this; + } + + inline T *Detach() + { + T *out = ptr; + ptr = nullptr; + return out; + } + + inline T **Assign() + { + Clear(); + return &ptr; + } + inline void Set(T *p) + { + Kill(); + ptr = p; + } + + inline T *Get() const { return ptr; } + + inline T **operator&() { return Assign(); } + + inline operator T *() const { return ptr; } + inline T *operator->() const { return ptr; } + + inline bool operator==(T *p) const { return ptr == p; } + inline bool operator!=(T *p) const { return ptr != p; } + + inline bool operator!() const { return !ptr; } +}; + +struct EncoderEntry { + const char *guid; + const char *name; + const char *id; + MF::EncoderType type; +}; + +constexpr EncoderEntry guidNameMap[] = { + {"{6CA50344-051A-4DED-9779-A43305165E35}", "MF.H264.EncoderSWMicrosoft", "mf_h264_software", + MF::EncoderType::H264_SOFTWARE}, + {"{ADC9BC80-0F41-46C6-AB75-D693D793597D}", "MF.H264.EncoderHWAMD", "mf_h264_vce", MF::EncoderType::H264_VCE}, + {"{4BE8D3C0-0515-4A37-AD55-E4BAE19AF471}", "MF.H264.EncoderHWIntel", "mf_h264_qsv", MF::EncoderType::H264_QSV}, + {"{60F44560-5A20-4857-BFEF-D29773CB8040}", "MF.H264.EncoderHWNVIDIA", "mf_h264_nvenc", + MF::EncoderType::H264_NVENC}, + {"{7790EE16-08E3-426D-AADA-F96774308EA1}", "MF.H264.EncoderHWQCOM", "qcom_h264_tex", + MF::EncoderType::H264_QCOM}, + {"{5AAFFE75-4EA4-424C-89E3-4A1E3F9A570D}", "MF.HEVC.EncoderHWQCOM", "qcom_hevc_tex", + MF::EncoderType::HEVC_QCOM}, + {"{0705AB91-0EC9-4D51-90E2-00C3360F41C4}", "MF.AV1.EncoderHWQCOM", "qcom_av1_tex", MF::EncoderType ::AV1_QCOM}, +}; + +namespace { +std::string MBSToString(wchar_t *mbs) +{ + char *cstr; + os_wcs_to_utf8_ptr(mbs, 0, &cstr); + std::string str = cstr; + bfree(cstr); + return str; +} + +std::unique_ptr CreateDescriptor(ComPtr activate) +{ + UINT32 flags; + if (FAILED(activate->GetUINT32(MF_TRANSFORM_FLAGS_Attribute, &flags))) + return nullptr; + + bool isAsync = !(flags & MFT_ENUM_FLAG_SYNCMFT); + isAsync |= !!(flags & MFT_ENUM_FLAG_ASYNCMFT); + bool isHardware = !!(flags & MFT_ENUM_FLAG_HARDWARE); + + GUID guid = {0}; + + if (FAILED(activate->GetGUID(MFT_TRANSFORM_CLSID_Attribute, &guid))) + return nullptr; + + ComHeapPtr guidW; + StringFromIID(guid, &guidW); + std::string guidString = MBSToString(guidW); + + auto pred = [guidString](const EncoderEntry &name) { + return guidString == name.guid; + }; + + const EncoderEntry *entry = std::find_if(std::begin(guidNameMap), std::end(guidNameMap), pred); + + auto descriptor = std::make_unique(activate, entry->name, entry->id, guid, guidString, + isAsync, isHardware, entry->type); + + return descriptor; +} +} // namespace + +std::vector> MF::EncoderDescriptor::Enumerate(const char Codec[]) +{ + HRESULT hr; + UINT32 count = 0; + std::vector> descriptors; + + ComHeapPtr ppActivate; + MFT_REGISTER_TYPE_INFO info; + info.guidMajorType = MFMediaType_Video; + + if (std::strcmp(Codec, "h264") == 0) { + info.guidSubtype = MFVideoFormat_H264; + } + + else if (std::strcmp(Codec, "hevc") == 0) { + info.guidSubtype = MFVideoFormat_HEVC; + } + + else if (std::strcmp(Codec, "av1") == 0) { + info.guidSubtype = MFVideoFormat_AV1; + } + + UINT32 unFlags = 0; + + unFlags |= MFT_ENUM_FLAG_LOCALMFT; + unFlags |= MFT_ENUM_FLAG_TRANSCODE_ONLY; + + unFlags |= MFT_ENUM_FLAG_SYNCMFT; + unFlags |= MFT_ENUM_FLAG_ASYNCMFT; + unFlags |= MFT_ENUM_FLAG_HARDWARE; + + unFlags |= MFT_ENUM_FLAG_SORTANDFILTER; + + hr = MFTEnumEx(MFT_CATEGORY_VIDEO_ENCODER, unFlags, NULL, &info, &ppActivate, &count); + + if (SUCCEEDED(hr) && count == 0) { + return descriptors; + } + + if (SUCCEEDED(hr)) { + for (decltype(count) i = 0; i < count; ++i) { + auto p = std::move(CreateDescriptor(ppActivate[i])); + if (p) + descriptors.emplace_back(std::move(p)); + } + } + + for (UINT32 i = 0; i < count; ++i) { + ppActivate[i]->Release(); + } + return descriptors; +} diff --git a/plugins/win-mediafoundation/encoder/mf-encoder-descriptor.hpp b/plugins/win-mediafoundation/encoder/mf-encoder-descriptor.hpp new file mode 100644 index 00000000000000..efa0d6fe3be492 --- /dev/null +++ b/plugins/win-mediafoundation/encoder/mf-encoder-descriptor.hpp @@ -0,0 +1,60 @@ +#pragma once + +#define WIN32_MEAN_AND_LEAN +#include +#undef WIN32_MEAN_AND_LEAN + +#include +#include + +#include +#include + +#include + +namespace MF { +enum class EncoderType { H264_SOFTWARE, H264_QSV, H264_NVENC, H264_VCE, H264_QCOM, HEVC_QCOM, AV1_QCOM }; + +static const char *typeNames[] = {"Software", "Quicksync", "NVENC", "AMD VCE", "QCOM_H264", "QCOM_HEVC", "QCOM_AV1"}; + +class EncoderDescriptor { +public: + static std::vector> Enumerate(const char[]); + +public: + EncoderDescriptor(ComPtr activate_, const char *name_, const char *id_, GUID &guid_, + const std::string &guidString_, bool isAsync_, bool isHardware_, EncoderType type_) + : activate(activate_), + name(name_), + id(id_), + guid(guid_), + guidString(guidString_), + isAsync(isAsync_), + isHardware(isHardware_), + type(type_) + { + } + + EncoderDescriptor(const EncoderDescriptor &) = delete; + +public: + const char *Name() const { return name; } + const char *Id() const { return id; } + ComPtr &Activator() { return activate; } + GUID &Guid() { return guid; } + std::string GuidString() const { return guidString; } + bool Async() const { return isAsync; } + bool Hardware() const { return isHardware; } + EncoderType Type() const { return type; } + +private: + ComPtr activate; + const char *name; + const char *id; + GUID guid; + std::string guidString; + bool isAsync; + bool isHardware; + EncoderType type; +}; +}; // namespace MF diff --git a/plugins/win-mediafoundation/encoder/mf-h264-encoder.cpp b/plugins/win-mediafoundation/encoder/mf-h264-encoder.cpp new file mode 100644 index 00000000000000..19ba969311150f --- /dev/null +++ b/plugins/win-mediafoundation/encoder/mf-h264-encoder.cpp @@ -0,0 +1,805 @@ +#include +#include + +#include "mf-common.hpp" +#include "mf-h264-encoder.hpp" + +#include +#include + +#include +#include + +// DirectX +#include + +// Media Foundation +#include +#include +#include +#include +#include + +namespace { +eAVEncH264VProfile MapProfile(MF::H264Profile profile) +{ + switch (profile) { + case MF::H264ProfileBaseline: + return eAVEncH264VProfile_Base; + case MF::H264ProfileMain: + return eAVEncH264VProfile_Main; + case MF::H264ProfileHigh: + return eAVEncH264VProfile_High; + default: + return eAVEncH264VProfile_Base; + } +} + +eAVEncCommonRateControlMode MapRateControl(MF::H264RateControl rc) +{ + switch (rc) { + case MF::H264RateControlCBR: + return eAVEncCommonRateControlMode_CBR; + case MF::H264RateControlConstrainedVBR: + return eAVEncCommonRateControlMode_PeakConstrainedVBR; + case MF::H264RateControlVBR: + return eAVEncCommonRateControlMode_UnconstrainedVBR; + case MF::H264RateControlCQP: + return eAVEncCommonRateControlMode_Quality; + default: + return eAVEncCommonRateControlMode_CBR; + } +} + +UINT32 MapQpToQuality(MF::H264QP &qp) +{ + return 100 - (UINT32)floor(100.0 / 51.0 * qp.defaultQp + 0.5f); +} + +bool ProcessNV12(std::function func, UINT32 height) +{ + INT32 plane = 0; + + func(height, plane++); + func(height / 2, plane); + + return true; +} +} // namespace + +MF::H264Encoder::H264Encoder(const obs_encoder_t *encoder, std::shared_ptr descriptor, UINT32 width, + UINT32 height, UINT32 framerateNum, UINT32 framerateDen, H264Profile profile, + UINT32 bitrate) + : encoder(encoder), + descriptor(descriptor), + width(width), + height(height), + framerateNum(framerateNum), + framerateDen(framerateDen), + initialBitrate(bitrate), + profile(profile) +{ +} + +MF::H264Encoder::~H264Encoder() +{ + HRESULT hr; + + if (!descriptor->Async() || !eventGenerator || !pendingRequests) + return; + + // Make sure all events have finished before releasing, and drain + // all output requests until it makes an input request. + // If you do not do this, you risk it releasing while there's still + // encoder activity, which can cause a crash with certain interfaces. + + while (inputRequests == 0) { + hr = ProcessOutput(); + + if (hr != MF_E_TRANSFORM_NEED_MORE_INPUT && FAILED(hr)) { + MF_LOG_COM(LOG_ERROR, + "H264Encoder::~H264Encoder: " + "ProcessOutput()", + hr); + break; + } + + if (inputRequests == 0) + Sleep(1); + } +} + +HRESULT MF::H264Encoder::CreateMediaTypes(ComPtr &i, ComPtr &o) +{ + HRESULT hr; + CHECK_HR_ERROR(MFCreateMediaType(&i)); + CHECK_HR_ERROR(MFCreateMediaType(&o)); + + CHECK_HR_ERROR(i->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video)); + CHECK_HR_ERROR(i->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_NV12)); + CHECK_HR_ERROR(MFSetAttributeSize(i, MF_MT_FRAME_SIZE, width, height)); + CHECK_HR_ERROR(MFSetAttributeRatio(i, MF_MT_FRAME_RATE, framerateNum, framerateDen)); + CHECK_HR_ERROR(i->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlaceMode::MFVideoInterlace_Progressive)); + CHECK_HR_ERROR(MFSetAttributeRatio(i, MF_MT_PIXEL_ASPECT_RATIO, 1, 1)); + + CHECK_HR_ERROR(o->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video)); + CHECK_HR_ERROR(o->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_H264)); + CHECK_HR_ERROR(MFSetAttributeSize(o, MF_MT_FRAME_SIZE, width, height)); + CHECK_HR_ERROR(MFSetAttributeRatio(o, MF_MT_FRAME_RATE, framerateNum, framerateDen)); + CHECK_HR_ERROR(o->SetUINT32(MF_MT_AVG_BITRATE, initialBitrate * 1000)); + CHECK_HR_ERROR(o->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlaceMode::MFVideoInterlace_Progressive)); + CHECK_HR_ERROR(MFSetAttributeRatio(o, MF_MT_PIXEL_ASPECT_RATIO, 1, 1)); + CHECK_HR_ERROR(o->SetUINT32(MF_MT_VIDEO_LEVEL, (UINT32)-1)); + CHECK_HR_ERROR(o->SetUINT32(MF_MT_VIDEO_PROFILE, MapProfile(profile))); + + return S_OK; + +fail: + return hr; +} + +HRESULT MF::H264Encoder::DrainEvents() +{ + HRESULT hr; + while ((hr = DrainEvent(false)) == S_OK) + ; + if (hr == MF_E_NO_EVENTS_AVAILABLE) + hr = S_OK; + return hr; +} + +HRESULT MF::H264Encoder::DrainEvent(bool block) +{ + HRESULT hr, eventStatus; + ComPtr event; + MediaEventType type; + + hr = eventGenerator->GetEvent(block ? 0 : MF_EVENT_FLAG_NO_WAIT, &event); + + if (hr != MF_E_NO_EVENTS_AVAILABLE && FAILED(hr)) + goto fail; + if (hr == MF_E_NO_EVENTS_AVAILABLE) + return hr; + + CHECK_HR_ERROR(event->GetType(&type)); + CHECK_HR_ERROR(event->GetStatus(&eventStatus)); + + if (SUCCEEDED(eventStatus)) { + if (type == METransformNeedInput) { + inputRequests++; + } else if (type == METransformHaveOutput) { + outputRequests++; + } + } + + return S_OK; + +fail: + return hr; +} + +HRESULT MF::H264Encoder::InitializeEventGenerator() +{ + HRESULT hr; + + CHECK_HR_ERROR(transform->QueryInterface(&eventGenerator)); + + return S_OK; + +fail: + return hr; +} + +HRESULT MF::H264Encoder::InitializeExtraData() +{ + HRESULT hr; + ComPtr inputType; + UINT32 headerSize; + + extraData.clear(); + + CHECK_HR_ERROR(transform->GetOutputCurrentType(0, &inputType)); + + CHECK_HR_ERROR(inputType->GetBlobSize(MF_MT_MPEG_SEQUENCE_HEADER, &headerSize)); + + extraData.resize(headerSize); + + CHECK_HR_ERROR(inputType->GetBlob(MF_MT_MPEG_SEQUENCE_HEADER, extraData.data(), headerSize, NULL)); + + return S_OK; + +fail: + return hr; +} + +namespace { +HRESULT SetCodecProperty(ComPtr &codecApi, GUID guid, bool value) +{ + VARIANT v; + v.vt = VT_BOOL; + v.boolVal = value ? VARIANT_TRUE : VARIANT_FALSE; + return codecApi->SetValue(&guid, &v); +} + +HRESULT SetCodecProperty(ComPtr &codecApi, GUID guid, UINT32 value) +{ + VARIANT v; + v.vt = VT_UI4; + v.ulVal = value; + return codecApi->SetValue(&guid, &v); +} + +HRESULT SetCodecProperty(ComPtr &codecApi, GUID guid, UINT64 value) +{ + VARIANT v; + v.vt = VT_UI8; + v.ullVal = value; + return codecApi->SetValue(&guid, &v); +} +} // namespace + +bool MF::H264Encoder::SetBitrate(UINT32 bitrate) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, + SetCodecProperty(codecApi, CODECAPI_AVEncCommonMeanBitRate, UINT32(bitrate * 1000))); + } + + return true; + +fail: + return false; +} + +bool MF::H264Encoder::SetQP(MF::H264QP &qp) +{ + HRESULT hr; + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, + SetCodecProperty(codecApi, CODECAPI_AVEncCommonQuality, UINT32(MapQpToQuality(qp)))); + CHECK_HR_WARNING(SetCodecProperty(codecApi, CODECAPI_AVEncVideoEncodeQP, UINT64(qp.Pack(true)))); + CHECK_HR_WARNING( + SetCodecProperty(codecApi, CODECAPI_AVEncVideoEncodeFrameTypeQP, UINT64(qp.Pack(false)))); + } + + return true; + +fail: + return false; +} + +bool MF::H264Encoder::SetMinQP(UINT32 minQp) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, SetCodecProperty(codecApi, CODECAPI_AVEncVideoMinQP, UINT32(minQp))); + } + + return true; + +fail: + return false; +} + +bool MF::H264Encoder::SetMaxQP(UINT32 maxQp) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, SetCodecProperty(codecApi, CODECAPI_AVEncVideoMaxQP, UINT32(maxQp))); + } + + return true; + +fail: + return false; +} + +bool MF::H264Encoder::SetRateControl(H264RateControl rateControl) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, SetCodecProperty(codecApi, CODECAPI_AVEncCommonRateControlMode, + UINT32(MapRateControl(rateControl)))); + } + + return true; + +fail: + return false; +} + +bool MF::H264Encoder::SetKeyframeInterval(UINT32 seconds) +{ + HRESULT hr; + + if (codecApi) { + float gopSize = float(framerateNum) / framerateDen * seconds; + CHECK_HR_LEVEL(LOG_WARNING, SetCodecProperty(codecApi, CODECAPI_AVEncMPVGOPSize, UINT32(gopSize))); + } + + return true; + +fail: + return false; +} + +bool MF::H264Encoder::SetMaxBitrate(UINT32 maxBitrate) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, + SetCodecProperty(codecApi, CODECAPI_AVEncCommonMaxBitRate, UINT32(maxBitrate * 1000))); + } + + return true; + +fail: + return false; +} + +bool MF::H264Encoder::SetLowLatency(bool lowLatency) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, SetCodecProperty(codecApi, CODECAPI_AVLowLatencyMode, lowLatency)); + } + + return true; + +fail: + return false; +} + +bool MF::H264Encoder::SetBufferSize(UINT32 bufferSize) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, + SetCodecProperty(codecApi, CODECAPI_AVEncCommonBufferSize, UINT32(bufferSize * 1000))); + } + + return true; + +fail: + return false; +} + +bool MF::H264Encoder::SetBFrameCount(UINT32 bFrames) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, + SetCodecProperty(codecApi, CODECAPI_AVEncMPVDefaultBPictureCount, UINT32(bFrames))); + } + + return true; + +fail: + return false; +} + +bool MF::H264Encoder::SetEntropyEncoding(MF::H264EntropyEncoding entropyEncoding) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, SetCodecProperty(codecApi, CODECAPI_AVEncH264CABACEnable, + entropyEncoding == H264EntropyEncodingCABAC)); + } + + return true; + +fail: + return false; +} + +bool MF::H264Encoder::Initialize(std::function func) +{ + ProfileScope("H264Encoder::Initialize"); + + HRESULT hr; + + ComPtr inputType, outputType; + ComPtr transformAttributes; + MFT_OUTPUT_STREAM_INFO streamInfo = {0}; + + CHECK_HR_ERROR(CoCreateInstance(descriptor->Guid(), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&transform))); + + CHECK_HR_ERROR(CreateMediaTypes(inputType, outputType)); + + if (descriptor->Async()) { + CHECK_HR_ERROR(transform->GetAttributes(&transformAttributes)); + CHECK_HR_ERROR(transformAttributes->SetUINT32(MF_TRANSFORM_ASYNC_UNLOCK, TRUE)); + } + + CHECK_HR_ERROR(transform->QueryInterface(&codecApi)); + + if (func && !func()) { + MF_LOG(LOG_ERROR, "Failed setting custom properties"); + goto fail; + } + + MF_LOG(LOG_INFO, "Activating encoder: %s", typeNames[(int)descriptor->Type()]); + + MF_LOG(LOG_INFO, " Setting output type to transform:"); + LogMediaType(outputType.Get()); + CHECK_HR_ERROR(transform->SetOutputType(0, outputType.Get(), 0)); + + MF_LOG(LOG_INFO, " Setting input type to transform:"); + LogMediaType(inputType.Get()); + CHECK_HR_ERROR(transform->SetInputType(0, inputType.Get(), 0)); + + CHECK_HR_ERROR(transform->ProcessMessage(MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, NULL)); + + CHECK_HR_ERROR(transform->ProcessMessage(MFT_MESSAGE_NOTIFY_START_OF_STREAM, NULL)); + + if (descriptor->Async()) + CHECK_HR_ERROR(InitializeEventGenerator()); + + CHECK_HR_ERROR(transform->GetOutputStreamInfo(0, &streamInfo)); + createOutputSample = + !(streamInfo.dwFlags & (MFT_OUTPUT_STREAM_PROVIDES_SAMPLES | MFT_OUTPUT_STREAM_CAN_PROVIDE_SAMPLES)); + + return true; + +fail: + return false; +} + +bool MF::H264Encoder::ExtraData(UINT8 **data, UINT32 *dataLength) +{ + if (extraData.empty()) + return false; + + *data = extraData.data(); + *dataLength = (UINT32)extraData.size(); + + return true; +} + +HRESULT MF::H264Encoder::CreateEmptySample(ComPtr &sample, ComPtr &buffer, DWORD length) +{ + HRESULT hr; + + CHECK_HR_ERROR(MFCreateSample(&sample)); + CHECK_HR_ERROR(MFCreateMemoryBuffer(length, &buffer)); + CHECK_HR_ERROR(sample->AddBuffer(buffer.Get())); + return S_OK; + +fail: + return hr; +} + +HRESULT MF::H264Encoder::EnsureCapacity(ComPtr &sample, DWORD length) +{ + HRESULT hr; + ComPtr buffer; + DWORD currentLength; + + if (!sample) { + CHECK_HR_ERROR(CreateEmptySample(sample, buffer, length)); + } else { + CHECK_HR_ERROR(sample->GetBufferByIndex(0, &buffer)); + } + + CHECK_HR_ERROR(buffer->GetMaxLength(¤tLength)); + if (currentLength < length) { + CHECK_HR_ERROR(sample->RemoveAllBuffers()); + CHECK_HR_ERROR(MFCreateMemoryBuffer(length, &buffer)); + CHECK_HR_ERROR(sample->AddBuffer(buffer)); + } else { + buffer->SetCurrentLength(0); + } + + return S_OK; + +fail: + return hr; +} + +HRESULT MF::H264Encoder::ProcessInput(ComPtr &sample) +{ + ProfileScope("H264Encoder::ProcessInput(sample)"); + + HRESULT hr = S_OK; + if (descriptor->Async()) { + if (inputRequests == 1 && inputSamples.empty()) { + inputRequests--; + return transform->ProcessInput(0, sample, 0); + } + + inputSamples.push(sample); + + while (inputRequests > 0) { + if (inputSamples.empty()) + return hr; + ComPtr queuedSample = inputSamples.front(); + inputSamples.pop(); + inputRequests--; + CHECK_HR_ERROR(transform->ProcessInput(0, queuedSample, 0)); + } + } else { + return transform->ProcessInput(0, sample, 0); + } + +fail: + return hr; +} + +bool MF::H264Encoder::ProcessInput(UINT8 **data, UINT32 *linesize, UINT64 pts, Status *status) +{ + ProfileScope("H264Encoder::ProcessInput"); + + HRESULT hr; + ComPtr sample; + ComPtr buffer; + BYTE *bufferData; + UINT64 sampleDur; + UINT32 imageSize; + + CHECK_HR_ERROR(MFCalculateImageSize(MFVideoFormat_NV12, width, height, &imageSize)); + + CHECK_HR_ERROR(CreateEmptySample(sample, buffer, imageSize)); + + { + ProfileScope("H264EncoderCopyInputSample"); + + CHECK_HR_ERROR(buffer->Lock(&bufferData, NULL, NULL)); + + ProcessNV12( + [&, this](DWORD height, int plane) { + MFCopyImage(bufferData, width, data[plane], linesize[plane], width, height); + bufferData += width * height; + }, + height); + } + + CHECK_HR_ERROR(buffer->Unlock()); + CHECK_HR_ERROR(buffer->SetCurrentLength(imageSize)); + + MFFrameRateToAverageTimePerFrame(framerateNum, framerateDen, &sampleDur); + + CHECK_HR_ERROR(sample->SetSampleTime(pts * sampleDur)); + CHECK_HR_ERROR(sample->SetSampleDuration(sampleDur)); + + if (descriptor->Async()) { + CHECK_HR_ERROR(DrainEvents()); + + while (outputRequests > 0 && (hr = ProcessOutput()) == S_OK) + ; + + if (hr != MF_E_TRANSFORM_NEED_MORE_INPUT && FAILED(hr)) { + MF_LOG_COM(LOG_ERROR, "ProcessOutput()", hr); + goto fail; + } + + while (inputRequests == 0) { + hr = DrainEvent(false); + if (hr == MF_E_NO_EVENTS_AVAILABLE) { + Sleep(1); + continue; + } + if (FAILED(hr)) { + MF_LOG_COM(LOG_ERROR, "DrainEvent()", hr); + goto fail; + } + if (outputRequests > 0) { + hr = ProcessOutput(); + if (hr != MF_E_TRANSFORM_NEED_MORE_INPUT && FAILED(hr)) + goto fail; + } + } + } + + CHECK_HR_ERROR(ProcessInput(sample)); + + pendingRequests++; + + *status = SUCCESS; + return true; + +fail: + *status = FAILURE; + return false; +} + +bool MF::H264Encoder::ProcessInput_Tex(ID3D11Texture2D *pSurface, int64_t pts, Status *status) +{ + ProfileScope("H264Encoder::ProcessInput"); + + HRESULT hr; + ComPtr sample; + ComPtr buffer; + UINT64 sampleDur; + + CHECK_HR_ERROR(MFCreateSample(&sample)); + CHECK_HR_ERROR(MFCreateDXGISurfaceBuffer(__uuidof(ID3D11Texture2D), pSurface, 0, FALSE, &buffer)); + CHECK_HR_ERROR(sample->AddBuffer(buffer.Get())); + + MFFrameRateToAverageTimePerFrame(framerateNum, framerateDen, &sampleDur); + + CHECK_HR_ERROR(sample->SetSampleTime(pts * sampleDur)); + CHECK_HR_ERROR(sample->SetSampleDuration(sampleDur)); + + if (descriptor->Async()) { + CHECK_HR_ERROR(DrainEvents()); + + while (outputRequests > 0 && (hr = ProcessOutput()) == S_OK) + ; + + if (hr != MF_E_TRANSFORM_NEED_MORE_INPUT && FAILED(hr)) { + MF_LOG_COM(LOG_ERROR, "ProcessOutput()", hr); + goto fail; + } + + while (inputRequests == 0) { + hr = DrainEvent(false); + if (hr == MF_E_NO_EVENTS_AVAILABLE) { + Sleep(1); + continue; + } + if (FAILED(hr)) { + MF_LOG_COM(LOG_ERROR, "DrainEvent()", hr); + goto fail; + } + if (outputRequests > 0) { + hr = ProcessOutput(); + if (hr != MF_E_TRANSFORM_NEED_MORE_INPUT && FAILED(hr)) + goto fail; + } + } + } + + CHECK_HR_ERROR(ProcessInput(sample)); + + pendingRequests++; + + *status = SUCCESS; + return true; + +fail: + *status = FAILURE; + return false; +} + +HRESULT MF::H264Encoder::ProcessOutput() +{ + HRESULT hr; + ComPtr sample; + MFT_OUTPUT_STREAM_INFO outputInfo = {0}; + + DWORD outputStatus = 0; + MFT_OUTPUT_DATA_BUFFER output = {0}; + ComPtr buffer; + BYTE *bufferData; + DWORD bufferLength; + INT64 samplePts; + INT64 sampleDts; + INT64 sampleDur; + auto data = std::make_unique>(); + ComPtr type; + auto frame = std::make_unique(); + bool keyframe = false; + + if (descriptor->Async()) { + CHECK_HR_ERROR(DrainEvents()); + + if (outputRequests == 0) + return S_OK; + + outputRequests--; + } + + if (createOutputSample) { + CHECK_HR_ERROR(transform->GetOutputStreamInfo(0, &outputInfo)); + CHECK_HR_ERROR(CreateEmptySample(sample, buffer, outputInfo.cbSize)); + output.pSample = sample; + } else { + output.pSample = NULL; + } + + while (true) { + hr = transform->ProcessOutput(0, 1, &output, &outputStatus); + ComPtr events(output.pEvents); + + if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) + return hr; + + if (hr == MF_E_TRANSFORM_STREAM_CHANGE) { + CHECK_HR_ERROR(transform->GetOutputAvailableType(0, 0, &type)); + CHECK_HR_ERROR(transform->SetOutputType(0, type, 0)); + MF_LOG(LOG_INFO, "Updating output type to transform"); + LogMediaType(type); + if (descriptor->Async() && outputRequests > 0) { + outputRequests--; + continue; + } else { + return MF_E_TRANSFORM_NEED_MORE_INPUT; + } + } + + if (hr != S_OK) { + MF_LOG_COM(LOG_ERROR, "transform->ProcessOutput()", hr); + return hr; + } + + break; + } + + if (!createOutputSample) + sample.Set(output.pSample); + + CHECK_HR_ERROR(sample->GetBufferByIndex(0, &buffer)); + + keyframe = !!MFGetAttributeUINT32(sample, MFSampleExtension_CleanPoint, FALSE); + + CHECK_HR_ERROR(buffer->Lock(&bufferData, NULL, &bufferLength)); + + if (keyframe && extraData.empty()) + CHECK_HR_ERROR(InitializeExtraData()); + + data->reserve(bufferLength + extraData.size()); + + if (keyframe) + data->insert(data->end(), extraData.begin(), extraData.end()); + + data->insert(data->end(), &bufferData[0], &bufferData[bufferLength]); + CHECK_HR_ERROR(buffer->Unlock()); + + CHECK_HR_ERROR(sample->GetSampleDuration(&sampleDur)); + CHECK_HR_ERROR(sample->GetSampleTime(&samplePts)); + + sampleDts = MFGetAttributeUINT64(sample, MFSampleExtension_DecodeTimestamp, samplePts); + + frame.reset(new MF::H264Frame(keyframe, samplePts / sampleDur, sampleDts / sampleDur, std::move(data))); + + encodedFrames.push(std::move(frame)); + + return S_OK; + +fail: + return hr; +} + +bool MF::H264Encoder::ProcessOutput(UINT8 **data, UINT32 *dataLength, UINT64 *pts, UINT64 *dts, bool *keyframe, + Status *status) +{ + ProfileScope("H264Encoder::ProcessOutput"); + + HRESULT hr; + + hr = ProcessOutput(); + + if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT || encodedFrames.empty()) { + *status = NEED_MORE_INPUT; + return true; + } + + if (FAILED(hr) && encodedFrames.empty()) { + *status = FAILURE; + return false; + } + + activeFrame = std::move(encodedFrames.front()); + encodedFrames.pop(); + + *data = activeFrame.get()->Data(); + *dataLength = activeFrame.get()->DataLength(); + *pts = activeFrame.get()->Pts(); + *dts = activeFrame.get()->Dts(); + *keyframe = activeFrame.get()->Keyframe(); + *status = SUCCESS; + + pendingRequests--; + + return true; +} diff --git a/plugins/win-mediafoundation/encoder/mf-h264-encoder.hpp b/plugins/win-mediafoundation/encoder/mf-h264-encoder.hpp new file mode 100644 index 00000000000000..8a45baff64309f --- /dev/null +++ b/plugins/win-mediafoundation/encoder/mf-h264-encoder.hpp @@ -0,0 +1,161 @@ +#pragma once + +#include + +#define WIN32_MEAN_AND_LEAN +#include +#undef WIN32_MEAN_AND_LEAN + +#include +#include + +#include + +#include +#include +#include +#include + +#include + +#include "mf-encoder-descriptor.hpp" +#include "mf-common.hpp" +#include + +namespace MF { +enum H264Profile { H264ProfileBaseline, H264ProfileMain, H264ProfileHigh }; + +enum H264RateControl { H264RateControlCBR, H264RateControlConstrainedVBR, H264RateControlVBR, H264RateControlCQP }; + +struct H264QP { + UINT16 defaultQp; + UINT16 i; + UINT16 p; + UINT16 b; + + UINT64 Pack(bool packDefault) const + { + int shift = packDefault ? 0 : 16; + UINT64 packedQp; + if (packDefault) + packedQp = defaultQp; + + packedQp |= i << shift; + shift += 16; + packedQp |= p << shift; + shift += 16; + packedQp |= b << shift; + + return packedQp; + } +}; + +enum H264EntropyEncoding { H264EntropyEncodingCABLC, H264EntropyEncodingCABAC }; + +struct H264Frame { +public: + H264Frame() : keyframe(false), pts(0), dts(0), data(std::make_unique>()) {} + + H264Frame(bool keyframe, UINT64 pts, UINT64 dts, std::unique_ptr> data) + : keyframe(keyframe), + pts(pts), + dts(dts), + data(std::move(data)) + { + } + bool Keyframe() const { return keyframe; } + BYTE *Data() { return data.get()->data(); } + DWORD DataLength() { return (DWORD)data.get()->size(); } + INT64 Pts() { return pts; } + INT64 Dts() { return dts; } + +private: + H264Frame(H264Frame const &) = delete; + H264Frame &operator=(H264Frame const &) = delete; + +private: + bool keyframe; + INT64 pts; + INT64 dts; + std::unique_ptr> data; +}; + +class H264Encoder { +public: + H264Encoder(const obs_encoder_t *encoder, std::shared_ptr descriptor, UINT32 width, + UINT32 height, UINT32 framerateNum, UINT32 framerateDen, H264Profile profile, UINT32 bitrate); + + ~H264Encoder(); + + bool Initialize(std::function func); + bool ProcessInput(UINT8 **data, UINT32 *linesize, UINT64 pts, Status *status); + bool ProcessInput_Tex(ID3D11Texture2D *pSurface, int64_t pts, Status *status); + bool ProcessOutput(UINT8 **data, UINT32 *dataLength, UINT64 *pts, UINT64 *dts, bool *keyframe, Status *status); + bool ExtraData(UINT8 **data, UINT32 *dataLength); + + const obs_encoder_t *ObsEncoder() { return encoder; } + +public: + bool SetBitrate(UINT32 bitrate); + bool SetQP(H264QP &qp); + bool SetMaxBitrate(UINT32 maxBitrate); + bool SetRateControl(H264RateControl rateControl); + bool SetKeyframeInterval(UINT32 seconds); + bool SetLowLatency(bool lowLatency); + bool SetBufferSize(UINT32 bufferSize); + bool SetBFrameCount(UINT32 bFrames); + bool SetEntropyEncoding(H264EntropyEncoding entropyEncoding); + bool SetMinQP(UINT32 minQp); + bool SetMaxQP(UINT32 maxQp); + +private: + H264Encoder(H264Encoder const &) = delete; + H264Encoder &operator=(H264Encoder const &) = delete; + +private: + HRESULT InitializeEventGenerator(); + HRESULT InitializeExtraData(); + HRESULT CreateMediaTypes(ComPtr &inputType, ComPtr &outputType); + HRESULT EnsureCapacity(ComPtr &sample, DWORD length); + HRESULT CreateEmptySample(ComPtr &sample, ComPtr &buffer, DWORD length); + + HRESULT ProcessInput(ComPtr &sample); + HRESULT ProcessOutput(); + + HRESULT DrainEvent(bool block); + HRESULT DrainEvents(); + +private: + const obs_encoder_t *encoder; + std::shared_ptr descriptor; + const UINT32 width; + const UINT32 height; + const UINT32 framerateNum; + const UINT32 framerateDen; + const UINT32 initialBitrate; + const H264Profile profile; + + bool createOutputSample; + ComPtr transform; + ComPtr codecApi; + + std::vector extraData; + + // The frame returned by ProcessOutput + // Valid until the next call to ProcessOutput + std::unique_ptr activeFrame; + + // Queued input samples that the encoder was not ready + // to process + std::queue> inputSamples; + + // Queued output samples that have not been returned from + // ProcessOutput yet + std::queue> encodedFrames; + + ComPtr eventGenerator; + std::atomic inputRequests{0}; + std::atomic outputRequests{0}; + std::atomic pendingRequests{0}; +}; +} // namespace MF diff --git a/plugins/win-mediafoundation/encoder/mf-h264.cpp b/plugins/win-mediafoundation/encoder/mf-h264.cpp new file mode 100644 index 00000000000000..80ac697d661a2d --- /dev/null +++ b/plugins/win-mediafoundation/encoder/mf-h264.cpp @@ -0,0 +1,694 @@ +#include +#include + +#include +#include + +#include "mf-h264-encoder.hpp" +#include "mf-encoder-descriptor.hpp" +#include + +struct MFH264_Encoder { + obs_encoder_t *encoder; + std::shared_ptr descriptor; + std::unique_ptr h264Encoder; + uint32_t width; + uint32_t height; + uint32_t framerateNum; + uint32_t framerateDen; + uint32_t keyint; + uint32_t bitrate; + uint32_t maxBitrate; + bool useMaxBitrate; + uint32_t bufferSize; + bool useBufferSize; + MF::H264Profile profile; + MF::H264RateControl rateControl; + MF::H264QP qp; + bool lowLatency; + uint32_t bFrames; + + const char *profiler_encode = nullptr; +}; + +ID3D11Device *d3D11Device_H264 = nullptr; +ID3D11DeviceContext *d3D11Ctx_H264 = nullptr; +ID3D11Texture2D *surface_H264 = NULL; + +static const char *TEXT_LOW_LAT = obs_module_text("MF.H264.LowLatency"); +static const char *TEXT_B_FRAMES = obs_module_text("MF.H264.BFrames"); +static const char *TEXT_BITRATE = obs_module_text("MF.H264.Bitrate"); +static const char *TEXT_CUSTOM_BUF = obs_module_text("MF.H264.CustomBufsize"); +static const char *TEXT_BUF_SIZE = obs_module_text("MF.H264.BufferSize"); +static const char *TEXT_USE_MAX_BITRATE = obs_module_text("MF.H264.CustomMaxBitrate"); +static const char *TEXT_MAX_BITRATE = obs_module_text("MF.H264.MaxBitrate"); +static const char *TEXT_KEYINT_SEC = obs_module_text("MF.H264.KeyframeIntervalSec"); +static const char *TEXT_RATE_CONTROL = obs_module_text("MF.H264.RateControl"); +static const char *TEXT_QPI = obs_module_text("MF.H264.QPI"); +static const char *TEXT_QPP = obs_module_text("MF.H264.QPP"); +static const char *TEXT_QPB = obs_module_text("MF.H264.QPB"); +static const char *TEXT_PROFILE = obs_module_text("MF.H264.Profile"); +static const char *TEXT_CBR = obs_module_text("MF.H264.CBR"); +static const char *TEXT_VBR = obs_module_text("MF.H264.VBR"); +static const char *TEXT_CQP = obs_module_text("MF.H264.CQP"); + +constexpr const char *MFP_USE_LOWLAT = "mf_h264_use_low_latency"; +constexpr const char *MFP_B_FRAMES = "mf_h264_b_frames"; +constexpr const char *MFP_BITRATE = "mf_h264_bitrate"; +constexpr const char *MFP_USE_BUF_SIZE = "mf_h264_use_buf_size"; +constexpr const char *MFP_BUF_SIZE = "mf_h264_buf_size"; +constexpr const char *MFP_USE_MAX_BITRATE = "mf_h264_use_max_bitrate"; +constexpr const char *MFP_MAX_BITRATE = "mf_h264_max_bitrate"; +constexpr const char *MFP_KEY_INT = "mf_h264_key_int"; +constexpr const char *MFP_RATE_CONTROL = "mf_h264_rate_control"; +constexpr const char *MFP_QP_I = "mf_h264_qp_i"; +constexpr const char *MFP_QP_P = "mf_h264_qp_p"; +constexpr const char *MFP_QP_B = "mf_h264_qp_b"; +constexpr const char *MFP_PROFILE = "mf_h264_profile"; + +struct TypeData { + std::shared_ptr descriptor; + inline TypeData(std::shared_ptr descriptor_) : descriptor(descriptor_) {} +}; + +namespace { +const char *MFH264_GetName(void *type_data) +{ + TypeData &typeData = *reinterpret_cast(type_data); + return obs_module_text(typeData.descriptor->Name()); +} + +void set_visible(obs_properties_t *ppts, const char *name, bool visible) +{ + obs_property_t *p = obs_properties_get(ppts, name); + obs_property_set_visible(p, visible); +} + +bool use_bufsize_modified(obs_properties_t *ppts, obs_property_t *p, obs_data_t *settings) +{ + UNUSED_PARAMETER(p); + + bool use_bufsize = obs_data_get_bool(settings, MFP_USE_BUF_SIZE); + + set_visible(ppts, MFP_BUF_SIZE, use_bufsize); + + return true; +} + +bool use_max_bitrate_modified(obs_properties_t *ppts, obs_property_t *p, obs_data_t *settings) +{ + UNUSED_PARAMETER(p); + + bool use_max_bitrate = obs_data_get_bool(settings, MFP_USE_MAX_BITRATE); + + set_visible(ppts, MFP_MAX_BITRATE, use_max_bitrate); + + return true; +} + +bool use_advanced_modified(obs_properties_t *ppts, obs_property_t *p, obs_data_t *settings) +{ + UNUSED_PARAMETER(p); + + MF::H264RateControl rateControl = (MF::H264RateControl)obs_data_get_int(settings, MFP_RATE_CONTROL); + + if (rateControl == MF::H264RateControlCBR || rateControl == MF::H264RateControlVBR) { + use_max_bitrate_modified(ppts, NULL, settings); + } + + return true; +} + +bool rate_control_modified(obs_properties_t *ppts, obs_property_t *p, obs_data_t *settings) +{ + UNUSED_PARAMETER(p); + + MF::H264RateControl rateControl = (MF::H264RateControl)obs_data_get_int(settings, MFP_RATE_CONTROL); + + set_visible(ppts, MFP_BITRATE, false); + set_visible(ppts, MFP_USE_BUF_SIZE, false); + set_visible(ppts, MFP_BUF_SIZE, false); + set_visible(ppts, MFP_USE_MAX_BITRATE, false); + set_visible(ppts, MFP_MAX_BITRATE, false); + set_visible(ppts, MFP_QP_I, false); + set_visible(ppts, MFP_QP_P, false); + set_visible(ppts, MFP_QP_B, false); + + switch (rateControl) { + case MF::H264RateControlCBR: + use_bufsize_modified(ppts, NULL, settings); + use_max_bitrate_modified(ppts, NULL, settings); + + set_visible(ppts, MFP_BITRATE, true); + set_visible(ppts, MFP_USE_BUF_SIZE, true); + + break; + case MF::H264RateControlVBR: + use_bufsize_modified(ppts, NULL, settings); + use_max_bitrate_modified(ppts, NULL, settings); + + set_visible(ppts, MFP_BITRATE, true); + set_visible(ppts, MFP_USE_BUF_SIZE, true); + + break; + case MF::H264RateControlCQP: + set_visible(ppts, MFP_QP_I, true); + set_visible(ppts, MFP_QP_P, true); + set_visible(ppts, MFP_QP_B, true); + + break; + default: + break; + } + + return true; +} + +obs_properties_t *MFH264_GetProperties(void *) +{ + obs_properties_t *props = obs_properties_create(); + obs_property_t *p; + + obs_property_t *list = + obs_properties_add_list(props, MFP_PROFILE, TEXT_PROFILE, OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + + obs_property_list_add_int(list, "baseline", MF::H264ProfileBaseline); + obs_property_list_add_int(list, "main", MF::H264ProfileMain); + obs_property_list_add_int(list, "high", MF::H264ProfileHigh); + + obs_properties_add_int(props, MFP_KEY_INT, TEXT_KEYINT_SEC, 0, 20, 1); + + list = obs_properties_add_list(props, MFP_RATE_CONTROL, TEXT_RATE_CONTROL, OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_INT); + + obs_property_list_add_int(list, TEXT_CBR, MF::H264RateControlCBR); + obs_property_list_add_int(list, TEXT_VBR, MF::H264RateControlVBR); + obs_property_list_add_int(list, TEXT_CQP, MF::H264RateControlCQP); + + obs_property_set_modified_callback(list, rate_control_modified); + + obs_properties_add_int(props, MFP_BITRATE, TEXT_BITRATE, 50, 10000000, 1); + + p = obs_properties_add_bool(props, MFP_USE_BUF_SIZE, TEXT_CUSTOM_BUF); + obs_property_set_modified_callback(p, use_bufsize_modified); + obs_properties_add_int(props, MFP_BUF_SIZE, TEXT_BUF_SIZE, 0, 10000000, 1); + + obs_properties_add_int(props, MFP_QP_I, TEXT_QPI, 0, 51, 1); + obs_properties_add_int(props, MFP_QP_P, TEXT_QPP, 0, 51, 1); + obs_properties_add_int(props, MFP_QP_B, TEXT_QPB, 0, 51, 1); + + p = obs_properties_add_bool(props, MFP_USE_MAX_BITRATE, TEXT_USE_MAX_BITRATE); + obs_property_set_modified_callback(p, use_max_bitrate_modified); + obs_properties_add_int(props, MFP_MAX_BITRATE, TEXT_MAX_BITRATE, 50, 10000000, 1); + + obs_properties_add_bool(props, MFP_USE_LOWLAT, TEXT_LOW_LAT); + obs_properties_add_int(props, MFP_B_FRAMES, TEXT_B_FRAMES, 0, 16, 1); + return props; +} + +void MFH264_GetDefaults(obs_data_t *settings) +{ + obs_data_set_default_int(settings, MFP_BITRATE, 6000); + obs_data_set_default_bool(settings, MFP_USE_LOWLAT, true); + obs_data_set_default_int(settings, MFP_B_FRAMES, 2); + obs_data_set_default_bool(settings, MFP_USE_BUF_SIZE, false); + obs_data_set_default_int(settings, MFP_BUF_SIZE, 2500); + obs_data_set_default_bool(settings, MFP_USE_MAX_BITRATE, false); + obs_data_set_default_int(settings, MFP_MAX_BITRATE, 6000); + obs_data_set_default_int(settings, MFP_KEY_INT, 2); + obs_data_set_default_int(settings, MFP_RATE_CONTROL, MF::H264RateControlCBR); + obs_data_set_default_int(settings, MFP_PROFILE, MF::H264ProfileMain); + obs_data_set_default_int(settings, MFP_QP_I, 26); + obs_data_set_default_int(settings, MFP_QP_B, 26); + obs_data_set_default_int(settings, MFP_QP_P, 26); +} + +void UpdateParams(MFH264_Encoder *enc, obs_data_t *settings) +{ + video_t *video = obs_encoder_video(enc->encoder); + const struct video_output_info *voi = video_output_get_info(video); + TypeData &typeData = *reinterpret_cast(obs_encoder_get_type_data(enc->encoder)); + + enc->width = (uint32_t)obs_encoder_get_width(enc->encoder); + enc->height = (uint32_t)obs_encoder_get_height(enc->encoder); + enc->framerateNum = voi->fps_num; + enc->framerateDen = voi->fps_den; + + enc->descriptor = typeData.descriptor; + enc->profile = static_cast(obs_data_get_int(settings, MFP_PROFILE)); + enc->rateControl = static_cast(obs_data_get_int(settings, MFP_RATE_CONTROL)); + enc->keyint = static_cast(obs_data_get_int(settings, MFP_KEY_INT)); + enc->bitrate = static_cast(obs_data_get_int(settings, MFP_BITRATE)); + enc->useBufferSize = obs_data_get_bool(settings, MFP_USE_BUF_SIZE); + enc->bufferSize = static_cast(obs_data_get_int(settings, MFP_BUF_SIZE)); + enc->useMaxBitrate = obs_data_get_bool(settings, MFP_USE_MAX_BITRATE); + enc->maxBitrate = static_cast(obs_data_get_int(settings, MFP_MAX_BITRATE)); + enc->qp.defaultQp = static_cast(obs_data_get_int(settings, MFP_QP_I)); + enc->qp.i = static_cast(obs_data_get_int(settings, MFP_QP_I)); + enc->qp.p = static_cast(obs_data_get_int(settings, MFP_QP_P)); + enc->qp.b = static_cast(obs_data_get_int(settings, MFP_QP_B)); + enc->lowLatency = obs_data_get_bool(settings, MFP_USE_LOWLAT); + enc->bFrames = static_cast(obs_data_get_int(settings, MFP_B_FRAMES)); +} +} // namespace + +namespace { +bool ApplyCBR(MFH264_Encoder *enc) +{ + enc->h264Encoder->SetBitrate(enc->bitrate); + + if (enc->useMaxBitrate) + enc->h264Encoder->SetMaxBitrate(enc->maxBitrate); + else + enc->h264Encoder->SetMaxBitrate(enc->bitrate); + + if (enc->useBufferSize) + enc->h264Encoder->SetBufferSize(enc->bufferSize); + + return true; +} + +bool ApplyCVBR(MFH264_Encoder *enc) +{ + enc->h264Encoder->SetBitrate(enc->bitrate); + + if (enc->useMaxBitrate) + enc->h264Encoder->SetMaxBitrate(enc->maxBitrate); + else + enc->h264Encoder->SetMaxBitrate(enc->bitrate); + + if (enc->useBufferSize) + enc->h264Encoder->SetBufferSize(enc->bufferSize); + + return true; +} + +bool ApplyVBR(MFH264_Encoder *enc) +{ + enc->h264Encoder->SetBitrate(enc->bitrate); + + if (enc->useBufferSize) + enc->h264Encoder->SetBufferSize(enc->bufferSize); + + return true; +} + +bool ApplyCQP(MFH264_Encoder *enc) +{ + enc->h264Encoder->SetQP(enc->qp); + + return true; +} + +void *MFH264_Create(obs_data_t *settings, obs_encoder_t *encoder) +{ + ProfileScope("MFH264_Create"); + + uint32_t minQp, maxQp; + double bppf; + float fps; + auto enc = std::make_unique(); + enc->encoder = encoder; + + UpdateParams(enc.get(), settings); + + ProfileScope(enc->descriptor->Name()); + + enc->h264Encoder.reset(new MF::H264Encoder(encoder, enc->descriptor, enc->width, enc->height, enc->framerateNum, + enc->framerateDen, enc->profile, enc->bitrate)); + + auto applySettings = [&]() { + enc.get()->h264Encoder->SetRateControl(enc->rateControl); + enc.get()->h264Encoder->SetKeyframeInterval(enc->keyint); + + enc.get()->h264Encoder->SetEntropyEncoding(MF::H264EntropyEncodingCABAC); + + enc.get()->h264Encoder->SetLowLatency(enc->lowLatency); + enc.get()->h264Encoder->SetBFrameCount(enc->bFrames); + + fps = (float)enc->framerateNum / (float)enc->framerateDen; + bppf = MF::ComputeBppf((enc->bitrate * 1000), enc->width, enc->height, fps); + MF::QpRange(MF::QCOM_H264, bppf, &minQp, &maxQp); + + enc.get()->h264Encoder->SetMinQP(minQp); + enc.get()->h264Encoder->SetMaxQP(maxQp); + + if (enc->rateControl == MF::H264RateControlVBR && enc->useMaxBitrate) + enc->rateControl = MF::H264RateControlConstrainedVBR; + + switch (enc->rateControl) { + case MF::H264RateControlCBR: + return ApplyCBR(enc.get()); + case MF::H264RateControlConstrainedVBR: + return ApplyCVBR(enc.get()); + case MF::H264RateControlVBR: + return ApplyVBR(enc.get()); + case MF::H264RateControlCQP: + return ApplyCQP(enc.get()); + default: + return false; + } + }; + + if (!enc->h264Encoder->Initialize(applySettings)) + return nullptr; + + return enc.release(); +} + +void *MFH264_Create_Tex(obs_data_t *settings, obs_encoder_t *encoder) +{ + ProfileScope("MFH264_Create_Tex"); + + D3D11_TEXTURE2D_DESC desc = {}; + video_t *video = obs_encoder_video(encoder); + const struct video_output_info *voi = video_output_get_info(video); + + MFT_REGISTER_TYPE_INFO rtInputInfo = {MFMediaType_Video, MF::GetMFVideoFormat(voi->format)}; + MFT_REGISTER_TYPE_INFO rtinfo = {MFMediaType_Video, MFVideoFormat_H264}; + IMFActivate **activate = NULL; + UINT32 count = 0; + MF::ComPtr_Dev deviceManager; + uint32_t minQp, maxQp; + double bppf; + float fps; + + HRESULT hr = + MFTEnumEx(MFT_CATEGORY_VIDEO_ENCODER, MFT_ENUM_FLAG_HARDWARE, &rtInputInfo, &rtinfo, &activate, &count); + if (activate == NULL || hr != S_OK) { + obs_encoder_set_last_error( + encoder, obs_module_text("OBS does not support current color format using QCOM AVC Encoder")); + return NULL; + } else { + blog(LOG_INFO, "Format is supported"); + } + + if (obs_encoder_scaling_enabled(encoder)) { + if (!obs_encoder_gpu_scaling_enabled(encoder)) { + blog(LOG_INFO, ">>> encoder CPU scaling active, fall back to old qsv encoder"); + return obs_encoder_create_rerouted(encoder, "qcom_h264"); + } + blog(LOG_INFO, ">>> encoder GPU scaling active"); + } + + hr = MF::CreateD3D11EncoderResources(voi, &d3D11Device_H264, &d3D11Ctx_H264, deviceManager, &surface_H264); + std::unique_ptr enc(new MFH264_Encoder()); + enc->encoder = encoder; + + UpdateParams(enc.get(), settings); + + ProfileScope(enc->descriptor->Name()); + + enc->h264Encoder.reset(new MF::H264Encoder(encoder, enc->descriptor, enc->width, enc->height, enc->framerateNum, + enc->framerateDen, enc->profile, enc->bitrate)); + + auto applySettings = [&]() { + enc.get()->h264Encoder->SetRateControl(enc->rateControl); + enc.get()->h264Encoder->SetKeyframeInterval(enc->keyint); + + enc.get()->h264Encoder->SetEntropyEncoding(MF::H264EntropyEncodingCABAC); + + enc.get()->h264Encoder->SetLowLatency(enc->lowLatency); + enc.get()->h264Encoder->SetBFrameCount(enc->bFrames); + + fps = (float)enc->framerateNum / (float)enc->framerateDen; + bppf = MF::ComputeBppf((enc->bitrate * 1000), enc->width, enc->height, fps); + MF::QpRange(MF::QCOM_H264, bppf, &minQp, &maxQp); + + enc.get()->h264Encoder->SetMinQP(1); + enc.get()->h264Encoder->SetMaxQP(51); + + switch (enc->rateControl) { + case MF::H264RateControlCBR: + return ApplyCBR(enc.get()); + case MF::H264RateControlConstrainedVBR: + return ApplyCVBR(enc.get()); + case MF::H264RateControlVBR: + return ApplyVBR(enc.get()); + case MF::H264RateControlCQP: + return ApplyCQP(enc.get()); + default: + return false; + } + }; + + if (!enc->h264Encoder->Initialize(applySettings)) + return nullptr; + + return enc.release(); +} + +void MFH264_Destroy(void *data) +{ + MFH264_Encoder *enc = static_cast(data); + delete enc; +} + +bool MFH264_Encode(void *data, struct encoder_frame *frame, struct encoder_packet *packet, bool *received_packet) +{ + MFH264_Encoder *enc = static_cast(data); + MF::Status status; + + if (!enc->profiler_encode) + enc->profiler_encode = + profile_store_name(obs_get_profiler_name_store(), "MFH264_Encode(%s)", enc->descriptor->Name()); + + ProfileScope(enc->profiler_encode); + + *received_packet = false; + + if (!enc->h264Encoder->ProcessInput(frame->data, frame->linesize, (frame->pts / packet->timebase_num), &status)) + return false; + + UINT8 *outputData; + UINT32 outputDataLength; + UINT64 outputPts; + UINT64 outputDts; + bool keyframe; + + if (!enc->h264Encoder->ProcessOutput(&outputData, &outputDataLength, &outputPts, &outputDts, &keyframe, + &status)) + return false; + + // Needs more input, not a failure case + if (status == MF::NEED_MORE_INPUT) + return true; + + packet->type = OBS_ENCODER_VIDEO; + packet->pts = outputPts * packet->timebase_num; + packet->dts = outputPts * packet->timebase_num; + packet->data = outputData; + packet->size = outputDataLength; + packet->keyframe = keyframe; + + *received_packet = true; + return true; +} + +static bool MFTH264_Encode_Tex(void *data, struct encoder_texture *tex, int64_t pts, uint64_t lock_key, + uint64_t *next_key, struct encoder_packet *packet, bool *received_packet) +{ + MFH264_Encoder *enc = static_cast(data); + ID3D11Texture2D *pSurface = NULL; + MF::Status status; + UINT8 *outputData; + UINT32 outputDataLength; + UINT64 outputPts; + UINT64 outputDts; + bool keyframe; + + if (!enc->profiler_encode) + enc->profiler_encode = + profile_store_name(obs_get_profiler_name_store(), "MFH264_Encode(%s)", enc->descriptor->Name()); + + ProfileScope(enc->profiler_encode); + + *received_packet = false; + + MF::Copy_Tex(tex, lock_key, next_key, d3D11Device_H264, d3D11Ctx_H264, surface_H264); + + if (!enc->h264Encoder->ProcessInput_Tex(surface_H264, pts / packet->timebase_num, &status)) + return false; + + if (!enc->h264Encoder->ProcessOutput(&outputData, &outputDataLength, &outputPts, &outputDts, &keyframe, + &status)) + return false; + + // Needs more input, not a failure case + if (status == MF::NEED_MORE_INPUT) + return true; + + packet->type = OBS_ENCODER_VIDEO; + packet->pts = outputPts * packet->timebase_num; + packet->dts = outputPts * packet->timebase_num; + packet->data = outputData; + packet->size = outputDataLength; + packet->keyframe = keyframe; + + *received_packet = true; + return true; +} + +bool MFH264_GetExtraData(void *data, uint8_t **extra_data, size_t *size) +{ + MFH264_Encoder *enc = static_cast(data); + + uint8_t *extraData; + UINT32 extraDataLength; + + if (!enc->h264Encoder->ExtraData(&extraData, &extraDataLength)) + return false; + + *extra_data = extraData; + *size = extraDataLength; + + return true; +} + +bool MFH264_GetSEIData(void *data, uint8_t **sei_data, size_t *size) +{ + UNUSED_PARAMETER(data); + UNUSED_PARAMETER(sei_data); + UNUSED_PARAMETER(size); + + return false; +} + +void MFH264_GetVideoInfo(void *, struct video_scale_info *info) +{ + info->format = VIDEO_FORMAT_NV12; +} + +bool MFH264_Update(void *data, obs_data_t *settings) +{ + MFH264_Encoder *enc = static_cast(data); + + UpdateParams(enc, settings); + + enc->h264Encoder->SetBitrate(enc->bitrate); + enc->h264Encoder->SetQP(enc->qp); + + return true; +} + +bool CanSpawnEncoder(std::shared_ptr descriptor) +{ + HRESULT hr; + ComPtr transform; + + hr = CoCreateInstance(descriptor->Guid(), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&transform)); + return hr == S_OK; +} +} // namespace + +void RegisterMFH264Encoders(const char Codec[]) +{ + obs_encoder_info info = {0}; + info.type = OBS_ENCODER_VIDEO; + info.get_name = MFH264_GetName; + info.create = MFH264_Create; + info.destroy = MFH264_Destroy; + info.encode = MFH264_Encode; + info.update = MFH264_Update; + info.get_properties = MFH264_GetProperties; + info.get_defaults = MFH264_GetDefaults; + info.get_extra_data = MFH264_GetExtraData; + info.get_sei_data = MFH264_GetSEIData; + info.get_video_info = MFH264_GetVideoInfo; + info.codec = Codec; + + auto encoders = MF::EncoderDescriptor::Enumerate(Codec); + for (auto e : encoders) { + /* ignore the software encoder due to the fact that we already + * have an objectively superior software encoder available */ + if (e->Type() == MF::EncoderType::H264_SOFTWARE) + continue; + + /* certain encoders such as quicksync will be "available" but + * not usable with certain processors */ + if (!CanSpawnEncoder(e)) + continue; + + info.id = e->Id(); + info.type_data = new TypeData(e); + info.free_type_data = [](void *type_data) { + delete reinterpret_cast(type_data); + }; + obs_register_encoder(&info); + } +} + +void RegisterMFH264Encoders() +{ + + obs_encoder_info info = {0}; + info.type = OBS_ENCODER_VIDEO; + info.get_name = MFH264_GetName; + info.create = MFH264_Create_Tex; + info.destroy = MFH264_Destroy; + info.encode = MFH264_Encode; + info.update = MFH264_Update; + info.encode_texture2 = MFTH264_Encode_Tex; + info.get_properties = MFH264_GetProperties; + info.get_defaults = MFH264_GetDefaults; + info.get_extra_data = MFH264_GetExtraData; + info.get_sei_data = MFH264_GetSEIData; + info.get_video_info = MFH264_GetVideoInfo; + info.codec = "h264"; + info.caps = OBS_ENCODER_CAP_PASS_TEXTURE; + + auto encoders = MF::EncoderDescriptor::Enumerate("h264"); + for (auto e : encoders) { + /* ignore the software encoder due to the fact that we already + * have an objectively superior software encoder available */ + if (e->Type() == MF::EncoderType::H264_SOFTWARE) + continue; + + /* certain encoders such as quicksync will be "available" but + * not usable with certain processors */ + if (!CanSpawnEncoder(e)) + continue; + + info.id = e->Id(); + info.type_data = new TypeData(e); + info.free_type_data = [](void *type_data) { + delete reinterpret_cast(type_data); + }; + obs_register_encoder(&info); + } + + obs_encoder_info info_soft = {0}; + info_soft.type = OBS_ENCODER_VIDEO; + info_soft.get_name = MFH264_GetName; + info_soft.create = MFH264_Create; + info_soft.destroy = MFH264_Destroy; + info_soft.encode = MFH264_Encode; + info_soft.update = MFH264_Update; + info_soft.get_properties = MFH264_GetProperties; + info_soft.get_defaults = MFH264_GetDefaults; + info_soft.get_extra_data = MFH264_GetExtraData; + info_soft.get_sei_data = MFH264_GetSEIData; + info_soft.get_video_info = MFH264_GetVideoInfo; + info_soft.codec = "h264"; + info_soft.caps = OBS_ENCODER_CAP_DYN_BITRATE | OBS_ENCODER_CAP_INTERNAL | OBS_ENCODER_CAP_DEPRECATED; + + for (auto e : encoders) { + + /* ignore the software encoder due to the fact that we already + * have an objectively superior software encoder available */ + if (e->Type() == MF::EncoderType::H264_SOFTWARE) + continue; + + /* certain encoders such as quicksync will be "available" but + * not usable with certain processors */ + if (!CanSpawnEncoder(e)) + continue; + + info_soft.id = "qcom_h264"; + info_soft.type_data = new TypeData(e); + info_soft.free_type_data = [](void *type_data) { + delete reinterpret_cast(type_data); + }; + obs_register_encoder(&info_soft); + } +} diff --git a/plugins/win-mediafoundation/encoder/mf-hevc-encoder.cpp b/plugins/win-mediafoundation/encoder/mf-hevc-encoder.cpp new file mode 100644 index 00000000000000..0eb75c42cbfb04 --- /dev/null +++ b/plugins/win-mediafoundation/encoder/mf-hevc-encoder.cpp @@ -0,0 +1,780 @@ +#include +#include + +#include "mf-common.hpp" +#include "mf-hevc-encoder.hpp" + +#include +#include + +#include +#include + +// DirectX +#include + +// Media Foundation +#include +#include +#include +#include +#include + +namespace { +eAVEncH265VProfile MapProfile(MF::HEVCProfile profile) +{ + switch (profile) { + case MF::HEVCProfileMain: + return eAVEncH265VProfile_Main_420_8; + default: + return eAVEncH265VProfile_Main_420_8; + } +} + +eAVEncCommonRateControlMode MapRateControl(MF::HEVCRateControl rc) +{ + switch (rc) { + case MF::HEVCRateControlCBR: + return eAVEncCommonRateControlMode_CBR; + case MF::HEVCRateControlVBR: + return eAVEncCommonRateControlMode_UnconstrainedVBR; + default: + return eAVEncCommonRateControlMode_CBR; + } +} + +UINT32 MapQpToQuality(MF::HEVCQP &qp) +{ + return 100 - (UINT32)floor(100.0 / 51.0 * qp.defaultQp + 0.5f); +} + +bool ProcessNV12(std::function func, UINT32 height) +{ + INT32 plane = 0; + + func(height, plane++); + func(height / 2, plane); + + return true; +} +} // namespace + +MF::HEVCEncoder::HEVCEncoder(const obs_encoder_t *encoder, std::shared_ptr descriptor, UINT32 width, + UINT32 height, UINT32 framerateNum, UINT32 framerateDen, MF::HEVCProfile profile, + UINT32 bitrate) + : encoder(encoder), + descriptor(descriptor), + width(width), + height(height), + framerateNum(framerateNum), + framerateDen(framerateDen), + initialBitrate(bitrate), + profile(profile) +{ +} + +MF::HEVCEncoder::~HEVCEncoder() +{ + HRESULT hr; + + if (!descriptor->Async() || !eventGenerator || !pendingRequests) + return; + + // Make sure all events have finished before releasing, and drain + // all output requests until it makes an input request. + // If you do not do this, you risk it releasing while there's still + // encoder activity, which can cause a crash with certain interfaces. + while (inputRequests == 0) { + hr = ProcessOutput(); + if (hr != MF_E_TRANSFORM_NEED_MORE_INPUT && FAILED(hr)) { + MF::MF_LOG_COM(LOG_ERROR, + "HEVCEncoder::~HEVCEncoder: " + "ProcessOutput()", + hr); + break; + } + + if (inputRequests == 0) + Sleep(1); + } +} + +HRESULT MF::HEVCEncoder::CreateMediaTypes(ComPtr &i, ComPtr &o) +{ + HRESULT hr; + CHECK_HR_ERROR(MFCreateMediaType(&i)); + CHECK_HR_ERROR(MFCreateMediaType(&o)); + + CHECK_HR_ERROR(i->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video)); + CHECK_HR_ERROR(i->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_NV12)); + CHECK_HR_ERROR(MFSetAttributeSize(i, MF_MT_FRAME_SIZE, width, height)); + CHECK_HR_ERROR(MFSetAttributeRatio(i, MF_MT_FRAME_RATE, framerateNum, framerateDen)); + CHECK_HR_ERROR(i->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlaceMode::MFVideoInterlace_Progressive)); + CHECK_HR_ERROR(MFSetAttributeRatio(i, MF_MT_PIXEL_ASPECT_RATIO, 1, 1)); + + CHECK_HR_ERROR(o->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video)); + CHECK_HR_ERROR(o->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_HEVC)); + CHECK_HR_ERROR(MFSetAttributeSize(o, MF_MT_FRAME_SIZE, width, height)); + CHECK_HR_ERROR(MFSetAttributeRatio(o, MF_MT_FRAME_RATE, framerateNum, framerateDen)); + CHECK_HR_ERROR(o->SetUINT32(MF_MT_AVG_BITRATE, initialBitrate * 1000)); + CHECK_HR_ERROR(o->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlaceMode::MFVideoInterlace_Progressive)); + CHECK_HR_ERROR(MFSetAttributeRatio(o, MF_MT_PIXEL_ASPECT_RATIO, 1, 1)); + CHECK_HR_ERROR(o->SetUINT32(MF_MT_VIDEO_LEVEL, (UINT32)-1)); + CHECK_HR_ERROR(o->SetUINT32(MF_MT_MPEG2_PROFILE, MapProfile(profile))); + + return S_OK; + +fail: + return hr; +} + +HRESULT MF::HEVCEncoder::DrainEvents() +{ + HRESULT hr; + while ((hr = DrainEvent(false)) == S_OK) + ; + if (hr == MF_E_NO_EVENTS_AVAILABLE) + hr = S_OK; + return hr; +} + +HRESULT MF::HEVCEncoder::DrainEvent(bool block) +{ + HRESULT hr, eventStatus; + ComPtr event; + MediaEventType type; + + hr = eventGenerator->GetEvent(block ? 0 : MF_EVENT_FLAG_NO_WAIT, &event); + + if (hr != MF_E_NO_EVENTS_AVAILABLE && FAILED(hr)) + goto fail; + if (hr == MF_E_NO_EVENTS_AVAILABLE) + return hr; + + CHECK_HR_ERROR(event->GetType(&type)); + CHECK_HR_ERROR(event->GetStatus(&eventStatus)); + + if (SUCCEEDED(eventStatus)) { + if (type == METransformNeedInput) { + inputRequests++; + } else if (type == METransformHaveOutput) { + outputRequests++; + } + } + + return S_OK; + +fail: + return hr; +} + +HRESULT MF::HEVCEncoder::InitializeEventGenerator() +{ + HRESULT hr; + + CHECK_HR_ERROR(transform->QueryInterface(&eventGenerator)); + + return S_OK; + +fail: + return hr; +} + +HRESULT MF::HEVCEncoder::InitializeExtraData() +{ + HRESULT hr; + ComPtr inputType; + UINT32 headerSize; + + extraData.clear(); + + CHECK_HR_ERROR(transform->GetOutputCurrentType(0, &inputType)); + + CHECK_HR_ERROR(inputType->GetBlobSize(MF_MT_MPEG_SEQUENCE_HEADER, &headerSize)); + + extraData.resize(headerSize); + + CHECK_HR_ERROR(inputType->GetBlob(MF_MT_MPEG_SEQUENCE_HEADER, extraData.data(), headerSize, NULL)); + + return S_OK; + +fail: + return hr; +} + +namespace { +HRESULT SetCodecProperty(ComPtr &codecApi, GUID guid, bool value) +{ + VARIANT v; + v.vt = VT_BOOL; + v.boolVal = value ? VARIANT_TRUE : VARIANT_FALSE; + return codecApi->SetValue(&guid, &v); +} + +HRESULT SetCodecProperty(ComPtr &codecApi, GUID guid, UINT32 value) +{ + VARIANT v; + v.vt = VT_UI4; + v.ulVal = value; + return codecApi->SetValue(&guid, &v); +} + +HRESULT SetCodecProperty(ComPtr &codecApi, GUID guid, UINT64 value) +{ + VARIANT v; + v.vt = VT_UI8; + v.ullVal = value; + return codecApi->SetValue(&guid, &v); +} +} // namespace + +bool MF::HEVCEncoder::SetBitrate(UINT32 bitrate) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, + SetCodecProperty(codecApi, CODECAPI_AVEncCommonMeanBitRate, UINT32(bitrate * 1000))); + } + + return true; + +fail: + return false; +} + +bool MF::HEVCEncoder::SetQP(MF::HEVCQP &qp) +{ + HRESULT hr; + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, + SetCodecProperty(codecApi, CODECAPI_AVEncCommonQuality, UINT32(MapQpToQuality(qp)))); + CHECK_HR_WARNING(SetCodecProperty(codecApi, CODECAPI_AVEncVideoEncodeQP, UINT64(qp.Pack(true)))); + CHECK_HR_WARNING( + SetCodecProperty(codecApi, CODECAPI_AVEncVideoEncodeFrameTypeQP, UINT64(qp.Pack(false)))); + } + + return true; + +fail: + return false; +} + +bool MF::HEVCEncoder::SetMinQP(UINT32 minQp) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, SetCodecProperty(codecApi, CODECAPI_AVEncVideoMinQP, UINT32(minQp))); + } + + return true; + +fail: + return false; +} + +bool MF::HEVCEncoder::SetMaxQP(UINT32 maxQp) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, SetCodecProperty(codecApi, CODECAPI_AVEncVideoMaxQP, UINT32(maxQp))); + } + + return true; + +fail: + return false; +} + +bool MF::HEVCEncoder::SetRateControl(MF::HEVCRateControl rateControl) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, SetCodecProperty(codecApi, CODECAPI_AVEncCommonRateControlMode, + UINT32(MapRateControl(rateControl)))); + } + + return true; + +fail: + return false; +} + +bool MF::HEVCEncoder::SetKeyframeInterval(UINT32 seconds) +{ + HRESULT hr; + + if (codecApi) { + float gopSize = float(framerateNum) / framerateDen * seconds; + CHECK_HR_LEVEL(LOG_WARNING, SetCodecProperty(codecApi, CODECAPI_AVEncMPVGOPSize, UINT32(gopSize))); + } + + return true; + +fail: + return false; +} + +bool MF::HEVCEncoder::SetMaxBitrate(UINT32 maxBitrate) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, + SetCodecProperty(codecApi, CODECAPI_AVEncCommonMaxBitRate, UINT32(maxBitrate * 1000))); + } + + return true; + +fail: + return false; +} + +bool MF::HEVCEncoder::SetLowLatency(bool lowLatency) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, SetCodecProperty(codecApi, CODECAPI_AVEncCommonLowLatency, lowLatency)); + } + + return true; + +fail: + return false; +} + +bool MF::HEVCEncoder::SetBufferSize(UINT32 bufferSize) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, + SetCodecProperty(codecApi, CODECAPI_AVEncCommonBufferSize, UINT32(bufferSize * 1000))); + } + + return true; + +fail: + return false; +} + +bool MF::HEVCEncoder::SetBFrameCount(UINT32 bFrames) +{ + HRESULT hr; + + if (codecApi) { + CHECK_HR_LEVEL(LOG_WARNING, + SetCodecProperty(codecApi, CODECAPI_AVEncMPVDefaultBPictureCount, UINT32(bFrames))); + } + + return true; + +fail: + return false; +} + +bool MF::HEVCEncoder::Initialize(std::function func) +{ + ProfileScope("HEVCEncoder::Initialize"); + + HRESULT hr; + + ComPtr inputType, outputType; + ComPtr transformAttributes; + MFT_OUTPUT_STREAM_INFO streamInfo = {0}; + + CHECK_HR_ERROR(CoCreateInstance(descriptor->Guid(), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&transform))); + + CHECK_HR_ERROR(CreateMediaTypes(inputType, outputType)); + + if (descriptor->Async()) { + CHECK_HR_ERROR(transform->GetAttributes(&transformAttributes)); + CHECK_HR_ERROR(transformAttributes->SetUINT32(MF_TRANSFORM_ASYNC_UNLOCK, TRUE)); + } + + CHECK_HR_ERROR(transform->QueryInterface(&codecApi)); + + if (func && !func()) { + MF::MF_LOG(LOG_ERROR, "Failed setting custom properties"); + goto fail; + } + + MF::MF_LOG(LOG_INFO, "Activating encoder: %s", typeNames[(int)descriptor->Type()]); + + MF::MF_LOG(LOG_INFO, " Setting output type to transform:"); + MF::LogMediaType(outputType.Get()); + CHECK_HR_ERROR(transform->SetOutputType(0, outputType.Get(), 0)); + + MF::MF_LOG(LOG_INFO, " Setting input type to transform:"); + MF::LogMediaType(inputType.Get()); + CHECK_HR_ERROR(transform->SetInputType(0, inputType.Get(), 0)); + + CHECK_HR_ERROR(transform->ProcessMessage(MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, NULL)); + + CHECK_HR_ERROR(transform->ProcessMessage(MFT_MESSAGE_NOTIFY_START_OF_STREAM, NULL)); + + if (descriptor->Async()) + CHECK_HR_ERROR(InitializeEventGenerator()); + + CHECK_HR_ERROR(transform->GetOutputStreamInfo(0, &streamInfo)); + createOutputSample = + !(streamInfo.dwFlags & (MFT_OUTPUT_STREAM_PROVIDES_SAMPLES | MFT_OUTPUT_STREAM_CAN_PROVIDE_SAMPLES)); + + return true; + +fail: + return false; +} + +bool MF::HEVCEncoder::ExtraData(UINT8 **data, UINT32 *dataLength) +{ + if (extraData.empty()) + return false; + + *data = extraData.data(); + *dataLength = (UINT32)extraData.size(); + + return true; +} + +HRESULT MF::HEVCEncoder::CreateEmptySample(ComPtr &sample, ComPtr &buffer, DWORD length) +{ + HRESULT hr; + + CHECK_HR_ERROR(MFCreateSample(&sample)); + CHECK_HR_ERROR(MFCreateMemoryBuffer(length, &buffer)); + CHECK_HR_ERROR(sample->AddBuffer(buffer.Get())); + return S_OK; + +fail: + return hr; +} + +HRESULT MF::HEVCEncoder::EnsureCapacity(ComPtr &sample, DWORD length) +{ + HRESULT hr; + ComPtr buffer; + DWORD currentLength; + + if (!sample) { + CHECK_HR_ERROR(CreateEmptySample(sample, buffer, length)); + } else { + CHECK_HR_ERROR(sample->GetBufferByIndex(0, &buffer)); + } + + CHECK_HR_ERROR(buffer->GetMaxLength(¤tLength)); + if (currentLength < length) { + CHECK_HR_ERROR(sample->RemoveAllBuffers()); + CHECK_HR_ERROR(MFCreateMemoryBuffer(length, &buffer)); + CHECK_HR_ERROR(sample->AddBuffer(buffer)); + } else { + buffer->SetCurrentLength(0); + } + + return S_OK; + +fail: + return hr; +} + +HRESULT MF::HEVCEncoder::ProcessInput(ComPtr &sample) +{ + ProfileScope("HEVCEncoder::ProcessInput(sample)"); + + HRESULT hr = S_OK; + if (descriptor->Async()) { + if (inputRequests == 1 && inputSamples.empty()) { + inputRequests--; + return transform->ProcessInput(0, sample, 0); + } + + inputSamples.push(sample); + + while (inputRequests > 0) { + if (inputSamples.empty()) + return hr; + ComPtr queuedSample = inputSamples.front(); + inputSamples.pop(); + inputRequests--; + CHECK_HR_ERROR(transform->ProcessInput(0, queuedSample, 0)); + } + } else { + return transform->ProcessInput(0, sample, 0); + } + +fail: + return hr; +} + +bool MF::HEVCEncoder::ProcessInput(UINT8 **data, UINT32 *linesize, UINT64 pts, MF::Status *status) +{ + ProfileScope("HEVCEncoder::ProcessInput"); + + HRESULT hr; + ComPtr sample; + ComPtr buffer; + BYTE *bufferData; + UINT64 sampleDur; + UINT32 imageSize; + + CHECK_HR_ERROR(MFCalculateImageSize(MFVideoFormat_NV12, width, height, &imageSize)); + + CHECK_HR_ERROR(CreateEmptySample(sample, buffer, imageSize)); + + { + ProfileScope("HEVCEncoderCopyInputSample"); + + CHECK_HR_ERROR(buffer->Lock(&bufferData, NULL, NULL)); + + ProcessNV12( + [&, this](DWORD height, int plane) { + MFCopyImage(bufferData, width, data[plane], linesize[plane], width, height); + bufferData += width * height; + }, + height); + } + + CHECK_HR_ERROR(buffer->Unlock()); + CHECK_HR_ERROR(buffer->SetCurrentLength(imageSize)); + + MFFrameRateToAverageTimePerFrame(framerateNum, framerateDen, &sampleDur); + + CHECK_HR_ERROR(sample->SetSampleTime(pts * sampleDur)); + CHECK_HR_ERROR(sample->SetSampleDuration(sampleDur)); + + if (descriptor->Async()) { + CHECK_HR_ERROR(DrainEvents()); + + while (outputRequests > 0 && (hr = ProcessOutput()) == S_OK) + ; + + if (hr != MF_E_TRANSFORM_NEED_MORE_INPUT && FAILED(hr)) { + MF::MF_LOG_COM(LOG_ERROR, "ProcessOutput()", hr); + goto fail; + } + + while (inputRequests == 0) { + hr = DrainEvent(false); + if (hr == MF_E_NO_EVENTS_AVAILABLE) { + Sleep(1); + continue; + } + if (FAILED(hr)) { + MF::MF_LOG_COM(LOG_ERROR, "DrainEvent()", hr); + goto fail; + } + if (outputRequests > 0) { + hr = ProcessOutput(); + if (hr != MF_E_TRANSFORM_NEED_MORE_INPUT && FAILED(hr)) + goto fail; + } + } + } + + CHECK_HR_ERROR(ProcessInput(sample)); + + pendingRequests++; + + *status = SUCCESS; + return true; + +fail: + *status = FAILURE; + return false; +} + +bool MF::HEVCEncoder::ProcessInput_Tex(ID3D11Texture2D *pSurface, int64_t pts, Status *status) +{ + ProfileScope("HEVCEncoder::ProcessInput"); + + HRESULT hr; + ComPtr sample; + ComPtr buffer; + UINT64 sampleDur; + + CHECK_HR_ERROR(MFCreateSample(&sample)); + CHECK_HR_ERROR(MFCreateDXGISurfaceBuffer(__uuidof(ID3D11Texture2D), pSurface, 0, FALSE, &buffer)); + CHECK_HR_ERROR(sample->AddBuffer(buffer.Get())); + + MFFrameRateToAverageTimePerFrame(framerateNum, framerateDen, &sampleDur); + + CHECK_HR_ERROR(sample->SetSampleTime(pts * sampleDur)); + CHECK_HR_ERROR(sample->SetSampleDuration(sampleDur)); + + if (descriptor->Async()) { + CHECK_HR_ERROR(DrainEvents()); + + while (outputRequests > 0 && (hr = ProcessOutput()) == S_OK) + ; + + if (hr != MF_E_TRANSFORM_NEED_MORE_INPUT && FAILED(hr)) { + MF::MF_LOG_COM(LOG_ERROR, "ProcessOutput()", hr); + goto fail; + } + + while (inputRequests == 0) { + hr = DrainEvent(false); + if (hr == MF_E_NO_EVENTS_AVAILABLE) { + Sleep(1); + continue; + } + if (FAILED(hr)) { + MF::MF_LOG_COM(LOG_ERROR, "DrainEvent()", hr); + goto fail; + } + if (outputRequests > 0) { + hr = ProcessOutput(); + if (hr != MF_E_TRANSFORM_NEED_MORE_INPUT && FAILED(hr)) + goto fail; + } + } + } + + CHECK_HR_ERROR(ProcessInput(sample)); + + pendingRequests++; + + *status = SUCCESS; + return true; + +fail: + *status = FAILURE; + return false; +} + +HRESULT MF::HEVCEncoder::ProcessOutput() +{ + HRESULT hr; + ComPtr sample; + MFT_OUTPUT_STREAM_INFO outputInfo = {0}; + + DWORD outputStatus = 0; + MFT_OUTPUT_DATA_BUFFER output = {0}; + ComPtr buffer; + BYTE *bufferData; + DWORD bufferLength; + INT64 samplePts; + INT64 sampleDts; + INT64 sampleDur; + auto data = std::make_unique>(); + ComPtr type; + std::unique_ptr frame; + bool keyframe = false; + + if (descriptor->Async()) { + CHECK_HR_ERROR(DrainEvents()); + + if (outputRequests == 0) + return S_OK; + + outputRequests--; + } + + if (createOutputSample) { + CHECK_HR_ERROR(transform->GetOutputStreamInfo(0, &outputInfo)); + CHECK_HR_ERROR(CreateEmptySample(sample, buffer, outputInfo.cbSize)); + output.pSample = sample; + } else { + output.pSample = NULL; + } + + while (true) { + hr = transform->ProcessOutput(0, 1, &output, &outputStatus); + ComPtr events(output.pEvents); + + if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) + return hr; + + if (hr == MF_E_TRANSFORM_STREAM_CHANGE) { + CHECK_HR_ERROR(transform->GetOutputAvailableType(0, 0, &type)); + CHECK_HR_ERROR(transform->SetOutputType(0, type, 0)); + MF::MF_LOG(LOG_INFO, "Updating output type to transform"); + MF::LogMediaType(type); + if (descriptor->Async() && outputRequests > 0) { + outputRequests--; + continue; + } else { + return MF_E_TRANSFORM_NEED_MORE_INPUT; + } + } + + if (hr != S_OK) { + MF::MF_LOG_COM(LOG_ERROR, "transform->ProcessOutput()", hr); + return hr; + } + + break; + } + + if (!createOutputSample) + sample.Set(output.pSample); + + CHECK_HR_ERROR(sample->GetBufferByIndex(0, &buffer)); + + keyframe = !!MFGetAttributeUINT32(sample, MFSampleExtension_CleanPoint, FALSE); + + CHECK_HR_ERROR(buffer->Lock(&bufferData, NULL, &bufferLength)); + + if (keyframe && extraData.empty()) + CHECK_HR_ERROR(InitializeExtraData()); + + data->reserve(bufferLength + extraData.size()); + + if (keyframe) + data->insert(data->end(), extraData.begin(), extraData.end()); + + data->insert(data->end(), &bufferData[0], &bufferData[bufferLength]); + CHECK_HR_ERROR(buffer->Unlock()); + + CHECK_HR_ERROR(sample->GetSampleDuration(&sampleDur)); + CHECK_HR_ERROR(sample->GetSampleTime(&samplePts)); + + sampleDts = MFGetAttributeUINT64(sample, MFSampleExtension_DecodeTimestamp, samplePts); + + frame.reset(new MF::HEVCFrame(keyframe, samplePts / sampleDur, sampleDts / sampleDur, std::move(data))); + + encodedFrames.push(std::move(frame)); + + return S_OK; + +fail: + return hr; +} + +bool MF::HEVCEncoder::ProcessOutput(UINT8 **data, UINT32 *dataLength, UINT64 *pts, UINT64 *dts, bool *keyframe, + MF::Status *status) +{ + ProfileScope("HEVCEncoder::ProcessOutput"); + + HRESULT hr; + + hr = ProcessOutput(); + + if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT || encodedFrames.empty()) { + *status = NEED_MORE_INPUT; + return true; + } + + if (FAILED(hr) && encodedFrames.empty()) { + *status = FAILURE; + return false; + } + + activeFrame = std::move(encodedFrames.front()); + encodedFrames.pop(); + + *data = activeFrame.get()->Data(); + *dataLength = activeFrame.get()->DataLength(); + *pts = activeFrame.get()->Pts(); + *dts = activeFrame.get()->Dts(); + *keyframe = activeFrame.get()->Keyframe(); + *status = SUCCESS; + + pendingRequests--; + + return true; +} diff --git a/plugins/win-mediafoundation/encoder/mf-hevc-encoder.hpp b/plugins/win-mediafoundation/encoder/mf-hevc-encoder.hpp new file mode 100644 index 00000000000000..d1a3a4bbdcbfb3 --- /dev/null +++ b/plugins/win-mediafoundation/encoder/mf-hevc-encoder.hpp @@ -0,0 +1,163 @@ +#pragma once + +#include + +#define WIN32_MEAN_AND_LEAN +#include +#undef WIN32_MEAN_AND_LEAN + +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include + +#include "mf-encoder-descriptor.hpp" +#include "mf-common.hpp" + +namespace MF { +enum HEVCProfile { + HEVCProfileMain, + +}; + +enum HEVCRateControl { + HEVCRateControlCBR, + HEVCRateControlVBR, +}; + +struct HEVCQP { + UINT16 defaultQp; + UINT16 i; + UINT16 p; + UINT16 b; + + UINT64 Pack(bool packDefault) + { + int shift = packDefault ? 0 : 16; + UINT64 packedQp; + if (packDefault) + packedQp = defaultQp; + + packedQp |= i << shift; + shift += 16; + packedQp |= p << shift; + shift += 16; + packedQp |= b << shift; + + return packedQp; + } +}; + +struct HEVCFrame { +public: + HEVCFrame() : keyframe(false), pts(0), dts(0), data(std::make_unique>()) {} + HEVCFrame(bool keyframe, UINT64 pts, UINT64 dts, std::unique_ptr> data) + : keyframe(keyframe), + pts(pts), + dts(dts), + data(std::move(data)) + { + } + bool Keyframe() { return keyframe; } + BYTE *Data() { return data.get()->data(); } + DWORD DataLength() { return (DWORD)data.get()->size(); } + INT64 Pts() { return pts; } + INT64 Dts() { return dts; } + +private: + HEVCFrame(HEVCFrame const &) = delete; + HEVCFrame &operator=(HEVCFrame const &) = delete; + +private: + bool keyframe; + INT64 pts; + INT64 dts; + std::unique_ptr> data; +}; + +class HEVCEncoder { +public: + HEVCEncoder(const obs_encoder_t *encoder, std::shared_ptr descriptor, UINT32 width, + UINT32 height, UINT32 framerateNum, UINT32 framerateDen, HEVCProfile profile, UINT32 bitrate); + + ~HEVCEncoder(); + + bool Initialize(std::function func); + bool ProcessInput(UINT8 **data, UINT32 *linesize, UINT64 pts, Status *status); + bool ProcessInput_Tex(ID3D11Texture2D *pSurface, int64_t pts, Status *status); + bool ProcessOutput(UINT8 **data, UINT32 *dataLength, UINT64 *pts, UINT64 *dts, bool *keyframe, Status *status); + bool ExtraData(UINT8 **data, UINT32 *dataLength); + + const obs_encoder_t *ObsEncoder() { return encoder; } + +public: + bool SetBitrate(UINT32 bitrate); + bool SetQP(HEVCQP &qp); + bool SetMaxBitrate(UINT32 maxBitrate); + bool SetRateControl(HEVCRateControl rateControl); + bool SetKeyframeInterval(UINT32 seconds); + bool SetLowLatency(bool lowLatency); + bool SetBufferSize(UINT32 bufferSize); + bool SetBFrameCount(UINT32 bFrames); + bool SetMinQP(UINT32 minQp); + bool SetMaxQP(UINT32 maxQp); + +private: + HEVCEncoder(HEVCEncoder const &) = delete; + HEVCEncoder &operator=(HEVCEncoder const &) = delete; + +private: + HRESULT InitializeEventGenerator(); + HRESULT InitializeExtraData(); + HRESULT CreateMediaTypes(ComPtr &inputType, ComPtr &outputType); + HRESULT EnsureCapacity(ComPtr &sample, DWORD length); + HRESULT CreateEmptySample(ComPtr &sample, ComPtr &buffer, DWORD length); + + HRESULT ProcessInput(ComPtr &sample); + HRESULT ProcessOutput(); + + HRESULT DrainEvent(bool block); + HRESULT DrainEvents(); + +private: + const obs_encoder_t *encoder; + std::shared_ptr descriptor; + const UINT32 width; + const UINT32 height; + const UINT32 framerateNum; + const UINT32 framerateDen; + const UINT32 initialBitrate; + const HEVCProfile profile; + + bool createOutputSample; + ComPtr transform; + ComPtr codecApi; + + std::vector extraData; + + // The frame returned by ProcessOutput + // Valid until the next call to ProcessOutput + std::unique_ptr activeFrame; + + // Queued input samples that the encoder was not ready + // to process + std::queue> inputSamples; + + // Queued output samples that have not been returned from + // ProcessOutput yet + std::queue> encodedFrames; + + ComPtr eventGenerator; + std::atomic inputRequests{0}; + std::atomic outputRequests{0}; + std::atomic pendingRequests{0}; +}; +} // namespace MF diff --git a/plugins/win-mediafoundation/encoder/mf-hevc.cpp b/plugins/win-mediafoundation/encoder/mf-hevc.cpp new file mode 100644 index 00000000000000..502c0443bb27f6 --- /dev/null +++ b/plugins/win-mediafoundation/encoder/mf-hevc.cpp @@ -0,0 +1,618 @@ +#include +#include + +#include +#include + +#include "mf-hevc-encoder.hpp" +#include "mf-encoder-descriptor.hpp" +#include + +struct MFHEVC_Encoder { + obs_encoder_t *encoder; + std::shared_ptr descriptor; + std::unique_ptr hevcEncoder; + uint32_t width; + uint32_t height; + uint32_t framerateNum; + uint32_t framerateDen; + uint32_t keyint; + uint32_t bitrate; + uint32_t maxBitrate; + bool useMaxBitrate; + uint32_t bufferSize; + bool useBufferSize; + MF::HEVCProfile profile; + MF::HEVCRateControl rateControl; + MF::HEVCQP qp; + bool lowLatency; + uint32_t bFrames; + + const char *profiler_encode = nullptr; +}; + +ID3D11Device *d3D11Device_HEVC = nullptr; +ID3D11DeviceContext *d3D11Ctx_HEVC = nullptr; +ID3D11Texture2D *surface_HEVC = NULL; + +static const char *TEXT_LOW_LAT = obs_module_text("MF.HEVC.LowLatency"); +static const char *TEXT_B_FRAMES = obs_module_text("MF.HEVC.BFrames"); +static const char *TEXT_BITRATE = obs_module_text("MF.HEVC.Bitrate"); +static const char *TEXT_CUSTOM_BUF = obs_module_text("MF.HEVC.CustomBufsize"); +static const char *TEXT_BUF_SIZE = obs_module_text("MF.HEVC.BufferSize"); +static const char *TEXT_USE_MAX_BITRATE = obs_module_text("MF.HEVC.CustomMaxBitrate"); +static const char *TEXT_MAX_BITRATE = obs_module_text("MF.HEVC.MaxBitrate"); +static const char *TEXT_KEYINT_SEC = obs_module_text("MF.HEVC.KeyframeIntervalSec"); +static const char *TEXT_RATE_CONTROL = obs_module_text("MF.HEVC.RateControl"); +static const char *TEXT_QPI = obs_module_text("MF.HEVC.QPI"); +static const char *TEXT_QPP = obs_module_text("MF.HEVC.QPP"); +static const char *TEXT_QPB = obs_module_text("MF.HEVC.QPB"); +static const char *TEXT_PROFILE = obs_module_text("MF.HEVC.Profile"); +static const char *TEXT_CBR = obs_module_text("MF.HEVC.CBR"); +static const char *TEXT_VBR = obs_module_text("MF.HEVC.VBR"); + +constexpr const char *MFP_USE_LOWLAT = "mf_hevc_use_low_latency"; +constexpr const char *MFP_B_FRAMES = "mf_hevc_b_frames"; +constexpr const char *MFP_BITRATE = "mf_hevc_bitrate"; +constexpr const char *MFP_USE_BUF_SIZE = "mf_hevc_use_buf_size"; +constexpr const char *MFP_BUF_SIZE = "mf_hevc_buf_size"; +constexpr const char *MFP_USE_MAX_BITRATE = "mf_hevc_use_max_bitrate"; +constexpr const char *MFP_MAX_BITRATE = "mf_hevc_max_bitrate"; +constexpr const char *MFP_KEY_INT = "mf_hevc_key_int"; +constexpr const char *MFP_RATE_CONTROL = "mf_hevc_rate_control"; +constexpr const char *MFP_MIN_QP = "mf_hevc_min_qp"; +constexpr const char *MFP_MAX_QP = "mf_hevc_max_qp"; +constexpr const char *MFP_QP_I = "mf_hevc_qp_i"; +constexpr const char *MFP_QP_P = "mf_hevc_qp_p"; +constexpr const char *MFP_QP_B = "mf_hevc_qp_b"; +constexpr const char *MFP_PROFILE = "mf_hevc_profile"; + +struct TypeData { + std::shared_ptr descriptor; + + inline TypeData(std::shared_ptr descriptor_) : descriptor(descriptor_) {} +}; + +namespace { +const char *MFHEVC_GetName(void *type_data) +{ + TypeData &typeData = *reinterpret_cast(type_data); + return obs_module_text(typeData.descriptor->Name()); +} + +void set_visible(obs_properties_t *ppts, const char *name, bool visible) +{ + obs_property_t *p = obs_properties_get(ppts, name); + obs_property_set_visible(p, visible); +} + +bool use_bufsize_modified(obs_properties_t *ppts, obs_property_t *p, obs_data_t *settings) +{ + UNUSED_PARAMETER(p); + + bool use_bufsize = obs_data_get_bool(settings, MFP_USE_BUF_SIZE); + + set_visible(ppts, MFP_BUF_SIZE, use_bufsize); + + return true; +} + +bool use_max_bitrate_modified(obs_properties_t *ppts, obs_property_t *p, obs_data_t *settings) +{ + UNUSED_PARAMETER(p); + + bool use_max_bitrate = obs_data_get_bool(settings, MFP_USE_MAX_BITRATE); + + set_visible(ppts, MFP_MAX_BITRATE, use_max_bitrate); + + return true; +} + +bool use_advanced_modified(obs_properties_t *ppts, obs_property_t *p, obs_data_t *settings) +{ + UNUSED_PARAMETER(p); + + MF::HEVCRateControl rateControl = (MF::HEVCRateControl)obs_data_get_int(settings, MFP_RATE_CONTROL); + + if (rateControl == MF::HEVCRateControlCBR || rateControl == MF::HEVCRateControlVBR) { + use_max_bitrate_modified(ppts, NULL, settings); + } + + return true; +} + +bool rate_control_modified(obs_properties_t *ppts, obs_property_t *p, obs_data_t *settings) +{ + UNUSED_PARAMETER(p); + + MF::HEVCRateControl rateControl = (MF::HEVCRateControl)obs_data_get_int(settings, MFP_RATE_CONTROL); + + set_visible(ppts, MFP_BITRATE, false); + set_visible(ppts, MFP_USE_BUF_SIZE, false); + set_visible(ppts, MFP_BUF_SIZE, false); + set_visible(ppts, MFP_USE_MAX_BITRATE, false); + set_visible(ppts, MFP_MAX_BITRATE, false); + set_visible(ppts, MFP_QP_I, false); + set_visible(ppts, MFP_QP_P, false); + set_visible(ppts, MFP_QP_B, false); + + switch (rateControl) { + case MF::HEVCRateControlCBR: + use_bufsize_modified(ppts, NULL, settings); + use_max_bitrate_modified(ppts, NULL, settings); + + set_visible(ppts, MFP_BITRATE, true); + set_visible(ppts, MFP_USE_BUF_SIZE, true); + + break; + case MF::HEVCRateControlVBR: + use_bufsize_modified(ppts, NULL, settings); + use_max_bitrate_modified(ppts, NULL, settings); + + set_visible(ppts, MFP_BITRATE, true); + set_visible(ppts, MFP_USE_BUF_SIZE, true); + break; + default: + break; + } + + return true; +} + +obs_properties_t *MFHEVC_GetProperties(void *) +{ + obs_properties_t *props = obs_properties_create(); + obs_property_t *p; + + obs_property_t *list = + obs_properties_add_list(props, MFP_PROFILE, TEXT_PROFILE, OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + + obs_property_list_add_int(list, "main", MF::HEVCProfileMain); + + obs_properties_add_int(props, MFP_KEY_INT, TEXT_KEYINT_SEC, 0, 20, 1); + + list = obs_properties_add_list(props, MFP_RATE_CONTROL, TEXT_RATE_CONTROL, OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_INT); + + obs_property_list_add_int(list, TEXT_CBR, MF::HEVCRateControlCBR); + obs_property_list_add_int(list, TEXT_VBR, MF::HEVCRateControlVBR); + + obs_property_set_modified_callback(list, rate_control_modified); + + obs_properties_add_int(props, MFP_BITRATE, TEXT_BITRATE, 50, 10000000, 1); + + p = obs_properties_add_bool(props, MFP_USE_BUF_SIZE, TEXT_CUSTOM_BUF); + obs_property_set_modified_callback(p, use_bufsize_modified); + obs_properties_add_int(props, MFP_BUF_SIZE, TEXT_BUF_SIZE, 0, 10000000, 1); + + obs_properties_add_int(props, MFP_QP_I, TEXT_QPI, 0, 51, 1); + obs_properties_add_int(props, MFP_QP_P, TEXT_QPP, 0, 51, 1); + obs_properties_add_int(props, MFP_QP_B, TEXT_QPB, 0, 51, 1); + + p = obs_properties_add_bool(props, MFP_USE_MAX_BITRATE, TEXT_USE_MAX_BITRATE); + obs_property_set_modified_callback(p, use_max_bitrate_modified); + obs_properties_add_int(props, MFP_MAX_BITRATE, TEXT_MAX_BITRATE, 50, 10000000, 1); + + obs_properties_add_bool(props, MFP_USE_LOWLAT, TEXT_LOW_LAT); + obs_properties_add_int(props, MFP_B_FRAMES, TEXT_B_FRAMES, 0, 16, 1); + return props; +} + +void MFHEVC_GetDefaults(obs_data_t *settings) +{ + obs_data_set_default_int(settings, MFP_BITRATE, 6000); + obs_data_set_default_bool(settings, MFP_USE_LOWLAT, true); + obs_data_set_default_int(settings, MFP_B_FRAMES, 2); + obs_data_set_default_bool(settings, MFP_USE_BUF_SIZE, false); + obs_data_set_default_int(settings, MFP_BUF_SIZE, 2500); + obs_data_set_default_bool(settings, MFP_USE_MAX_BITRATE, false); + obs_data_set_default_int(settings, MFP_MAX_BITRATE, 6000); + obs_data_set_default_int(settings, MFP_KEY_INT, 2); + obs_data_set_default_int(settings, MFP_RATE_CONTROL, MF::HEVCRateControlCBR); + obs_data_set_default_int(settings, MFP_PROFILE, MF::HEVCProfileMain); + obs_data_set_default_int(settings, MFP_MIN_QP, 1); + obs_data_set_default_int(settings, MFP_MAX_QP, 51); + obs_data_set_default_int(settings, MFP_QP_I, 26); + obs_data_set_default_int(settings, MFP_QP_B, 26); + obs_data_set_default_int(settings, MFP_QP_P, 26); +} + +void UpdateParams(MFHEVC_Encoder *enc, obs_data_t *settings) +{ + video_t *video = obs_encoder_video(enc->encoder); + const struct video_output_info *voi = video_output_get_info(video); + TypeData &typeData = *reinterpret_cast(obs_encoder_get_type_data(enc->encoder)); + + enc->width = (uint32_t)obs_encoder_get_width(enc->encoder); + enc->height = (uint32_t)obs_encoder_get_height(enc->encoder); + enc->framerateNum = voi->fps_num; + enc->framerateDen = voi->fps_den; + + enc->descriptor = typeData.descriptor; + enc->profile = static_cast(obs_data_get_int(settings, MFP_PROFILE)); + enc->rateControl = static_cast(obs_data_get_int(settings, MFP_RATE_CONTROL)); + enc->keyint = static_cast(obs_data_get_int(settings, MFP_KEY_INT)); + enc->bitrate = static_cast(obs_data_get_int(settings, MFP_BITRATE)); + enc->useBufferSize = obs_data_get_bool(settings, MFP_USE_BUF_SIZE); + enc->bufferSize = static_cast(obs_data_get_int(settings, MFP_BUF_SIZE)); + enc->useMaxBitrate = obs_data_get_bool(settings, MFP_USE_MAX_BITRATE); + enc->maxBitrate = static_cast(obs_data_get_int(settings, MFP_MAX_BITRATE)); + enc->qp.defaultQp = static_cast(obs_data_get_int(settings, MFP_QP_I)); + enc->qp.i = static_cast(obs_data_get_int(settings, MFP_QP_I)); + enc->qp.p = static_cast(obs_data_get_int(settings, MFP_QP_P)); + enc->qp.b = static_cast(obs_data_get_int(settings, MFP_QP_B)); + enc->lowLatency = obs_data_get_bool(settings, MFP_USE_LOWLAT); + enc->bFrames = static_cast(obs_data_get_int(settings, MFP_B_FRAMES)); +} + +} // namespace + +//#undef MFTEXT +//#undef MFP + +namespace { +bool ApplyCBR(MFHEVC_Encoder *enc) +{ + enc->hevcEncoder->SetBitrate(enc->bitrate); + + if (enc->useMaxBitrate) + enc->hevcEncoder->SetMaxBitrate(enc->maxBitrate); + else + enc->hevcEncoder->SetMaxBitrate(enc->bitrate); + + if (enc->useBufferSize) + enc->hevcEncoder->SetBufferSize(enc->bufferSize); + + return true; +} + +bool ApplyVBR(MFHEVC_Encoder *enc) +{ + enc->hevcEncoder->SetBitrate(enc->bitrate); + + if (enc->useBufferSize) + enc->hevcEncoder->SetBufferSize(enc->bufferSize); + + return true; +} + +void *MFHEVC_Create(obs_data_t *settings, obs_encoder_t *encoder) +{ + ProfileScope("MFHEVC_Create"); + uint32_t minQp, maxQp; + double bppf; + float fps; + auto enc = std::make_unique(); + enc->encoder = encoder; + + UpdateParams(enc.get(), settings); + + ProfileScope(enc->descriptor->Name()); + + enc->hevcEncoder.reset(new MF::HEVCEncoder(encoder, enc->descriptor, enc->width, enc->height, enc->framerateNum, + enc->framerateDen, enc->profile, enc->bitrate)); + + auto applySettings = [&]() { + enc.get()->hevcEncoder->SetRateControl(enc->rateControl); + enc.get()->hevcEncoder->SetKeyframeInterval(enc->keyint); + + enc.get()->hevcEncoder->SetLowLatency(enc->lowLatency); + enc.get()->hevcEncoder->SetBFrameCount(enc->bFrames); + + fps = (float)enc->framerateNum / (float)enc->framerateDen; + bppf = MF::ComputeBppf((enc->bitrate * 1000), enc->width, enc->height, fps); + MF::QpRange(MF::QCOM_H264, bppf, &minQp, &maxQp); + + enc.get()->hevcEncoder->SetMinQP(minQp); + enc.get()->hevcEncoder->SetMaxQP(maxQp); + + switch (enc->rateControl) { + case MF::HEVCRateControlCBR: + return ApplyCBR(enc.get()); + case MF::HEVCRateControlVBR: + return ApplyVBR(enc.get()); + default: + return false; + } + }; + + if (!enc->hevcEncoder->Initialize(applySettings)) + return nullptr; + + return enc.release(); +} + +void *MFHEVC_Create_Tex(obs_data_t *settings, obs_encoder_t *encoder) +{ + ProfileScope("MFHEVC_Create"); + + D3D11_TEXTURE2D_DESC desc = {}; + video_t *video = obs_encoder_video(encoder); + const struct video_output_info *voi = video_output_get_info(video); + + MFT_REGISTER_TYPE_INFO rtInputInfo = {MFMediaType_Video, MF::GetMFVideoFormat(voi->format)}; + MFT_REGISTER_TYPE_INFO rtInfo = {MFMediaType_Video, MFVideoFormat_HEVC}; + IMFActivate **activate = NULL; + UINT32 count = 0; + MF::ComPtr_Dev deviceManager; + uint32_t minQp, maxQp; + double bppf; + float fps; + + HRESULT hr = + MFTEnumEx(MFT_CATEGORY_VIDEO_ENCODER, MFT_ENUM_FLAG_HARDWARE, &rtInputInfo, &rtInfo, &activate, &count); + if (activate == NULL || hr != S_OK) { + obs_encoder_set_last_error( + encoder, obs_module_text("OBS does not support current color format using QCOM HEVC Encoder")); + return NULL; + } else { + blog(LOG_INFO, "Format is supported"); + } + + if (obs_encoder_scaling_enabled(encoder)) { + if (!obs_encoder_gpu_scaling_enabled(encoder)) { + blog(LOG_INFO, ">>> encoder CPU scaling active, fall back to old qsv encoder"); + return obs_encoder_create_rerouted(encoder, "qcom_hevc"); + } + blog(LOG_INFO, ">>> encoder GPU scaling active"); + } + + hr = MF::CreateD3D11EncoderResources(voi, &d3D11Device_HEVC, &d3D11Ctx_HEVC, deviceManager, &surface_HEVC); + + std::unique_ptr enc(new MFHEVC_Encoder()); + enc->encoder = encoder; + + UpdateParams(enc.get(), settings); + + ProfileScope(enc->descriptor->Name()); + + enc->hevcEncoder.reset(new MF::HEVCEncoder(encoder, enc->descriptor, enc->width, enc->height, enc->framerateNum, + enc->framerateDen, enc->profile, enc->bitrate)); + + auto applySettings = [&]() { + enc.get()->hevcEncoder->SetRateControl(enc->rateControl); + enc.get()->hevcEncoder->SetKeyframeInterval(enc->keyint); + + enc.get()->hevcEncoder->SetLowLatency(enc->lowLatency); + enc.get()->hevcEncoder->SetBFrameCount(enc->bFrames); + + fps = (float)enc->framerateNum / (float)enc->framerateDen; + bppf = MF::ComputeBppf((enc->bitrate * 1000), enc->width, enc->height, fps); + MF::QpRange(MF::QCOM_H264, bppf, &minQp, &maxQp); + + enc.get()->hevcEncoder->SetMinQP(minQp); + enc.get()->hevcEncoder->SetMaxQP(maxQp); + + switch (enc->rateControl) { + case MF::HEVCRateControlCBR: + return ApplyCBR(enc.get()); + case MF::HEVCRateControlVBR: + return ApplyVBR(enc.get()); + default: + return false; + } + }; + + if (!enc->hevcEncoder->Initialize(applySettings)) + return nullptr; + + return enc.release(); +} + +void MFHEVC_Destroy(void *data) +{ + MFHEVC_Encoder *enc = static_cast(data); + delete enc; +} + +bool MFHEVC_Encode(void *data, struct encoder_frame *frame, struct encoder_packet *packet, bool *received_packet) +{ + MFHEVC_Encoder *enc = static_cast(data); + MF::Status status; + + if (!enc->profiler_encode) + enc->profiler_encode = + profile_store_name(obs_get_profiler_name_store(), "MFHEVC_Encode(%s)", enc->descriptor->Name()); + + ProfileScope(enc->profiler_encode); + + *received_packet = false; + + if (!enc->hevcEncoder->ProcessInput(frame->data, frame->linesize, (frame->pts / packet->timebase_num), &status)) + return false; + + UINT8 *outputData; + UINT32 outputDataLength; + UINT64 outputPts; + UINT64 outputDts; + bool keyframe; + + if (!enc->hevcEncoder->ProcessOutput(&outputData, &outputDataLength, &outputPts, &outputDts, &keyframe, + &status)) + return false; + + // Needs more input, not a failure case + if (status == MF::NEED_MORE_INPUT) + return true; + + packet->type = OBS_ENCODER_VIDEO; + packet->pts = outputPts * packet->timebase_num; + packet->dts = outputPts * packet->timebase_num; + packet->data = outputData; + packet->size = outputDataLength; + packet->keyframe = keyframe; + + *received_packet = true; + return true; +} + +static bool MFTHEVC_Encode_Tex(void *data, struct encoder_texture *tex, int64_t pts, uint64_t lock_key, + uint64_t *next_key, struct encoder_packet *packet, bool *received_packet) +{ + MFHEVC_Encoder *enc = static_cast(data); + MF::Status status; + ID3D11Texture2D *pSurface = NULL; + + if (!enc->profiler_encode) + enc->profiler_encode = + profile_store_name(obs_get_profiler_name_store(), "MFHEVC_Encode(%s)", enc->descriptor->Name()); + + ProfileScope(enc->profiler_encode); + + *received_packet = false; + + MF::Copy_Tex(tex, lock_key, next_key, d3D11Device_HEVC, d3D11Ctx_HEVC, surface_HEVC); + if (!enc->hevcEncoder->ProcessInput_Tex(surface_HEVC, pts / packet->timebase_num, &status)) + return false; + + UINT8 *outputData; + UINT32 outputDataLength; + UINT64 outputPts; + UINT64 outputDts; + bool keyframe; + + if (!enc->hevcEncoder->ProcessOutput(&outputData, &outputDataLength, &outputPts, &outputDts, &keyframe, + &status)) + return false; + + // Needs more input, not a failure case + if (status == MF::NEED_MORE_INPUT) + return true; + + packet->type = OBS_ENCODER_VIDEO; + packet->pts = outputPts * packet->timebase_num; + packet->dts = outputPts * packet->timebase_num; + packet->data = outputData; + packet->size = outputDataLength; + packet->keyframe = keyframe; + + *received_packet = true; + return true; +} + +bool MFHEVC_GetExtraData(void *data, uint8_t **extra_data, size_t *size) +{ + MFHEVC_Encoder *enc = static_cast(data); + + uint8_t *extraData; + UINT32 extraDataLength; + + if (!enc->hevcEncoder->ExtraData(&extraData, &extraDataLength)) + return false; + + *extra_data = extraData; + *size = extraDataLength; + + return true; +} + +bool MFHEVC_GetSEIData(void *data, uint8_t **sei_data, size_t *size) +{ + UNUSED_PARAMETER(data); + UNUSED_PARAMETER(sei_data); + UNUSED_PARAMETER(size); + + return false; +} + +void MFHEVC_GetVideoInfo(void *, struct video_scale_info *info) +{ + info->format = VIDEO_FORMAT_NV12; +} + +bool MFHEVC_Update(void *data, obs_data_t *settings) +{ + MFHEVC_Encoder *enc = static_cast(data); + + UpdateParams(enc, settings); + + enc->hevcEncoder->SetBitrate(enc->bitrate); + enc->hevcEncoder->SetQP(enc->qp); + + return true; +} + +bool CanSpawnEncoder(std::shared_ptr descriptor) +{ + HRESULT hr; + ComPtr transform; + + hr = CoCreateInstance(descriptor->Guid(), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&transform)); + return hr == S_OK; +} +} // namespace + +void RegisterMFHEVCEncoders() +{ + obs_encoder_info info = {0}; + info.type = OBS_ENCODER_VIDEO; + info.get_name = MFHEVC_GetName; + info.create = MFHEVC_Create_Tex; + info.destroy = MFHEVC_Destroy; + info.encode = MFHEVC_Encode; + info.update = MFHEVC_Update; + info.encode_texture2 = MFTHEVC_Encode_Tex; + info.get_properties = MFHEVC_GetProperties; + info.get_defaults = MFHEVC_GetDefaults; + info.get_extra_data = MFHEVC_GetExtraData; + info.get_sei_data = MFHEVC_GetSEIData; + info.get_video_info = MFHEVC_GetVideoInfo; + info.codec = "hevc"; + info.caps = OBS_ENCODER_CAP_PASS_TEXTURE; + + auto encoders = MF::EncoderDescriptor::Enumerate("hevc"); + for (auto e : encoders) { + + /* ignore the software encoder due to the fact that we already + * have an objectively superior software encoder available */ + if (e->Type() == MF::EncoderType::H264_SOFTWARE) + continue; + + /* certain encoders such as quicksync will be "available" but + * not usable with certain processors */ + if (!CanSpawnEncoder(e)) + continue; + + info.id = e->Id(); + info.type_data = new TypeData(e); + info.free_type_data = [](void *type_data) { + delete reinterpret_cast(type_data); + }; + obs_register_encoder(&info); + } + + obs_encoder_info info_soft = {0}; + info_soft.type = OBS_ENCODER_VIDEO; + info_soft.get_name = MFHEVC_GetName; + info_soft.create = MFHEVC_Create; + info_soft.destroy = MFHEVC_Destroy; + info_soft.encode = MFHEVC_Encode; + info_soft.update = MFHEVC_Update; + info_soft.get_properties = MFHEVC_GetProperties; + info_soft.get_defaults = MFHEVC_GetDefaults; + info_soft.get_extra_data = MFHEVC_GetExtraData; + info_soft.get_sei_data = MFHEVC_GetSEIData; + info_soft.get_video_info = MFHEVC_GetVideoInfo; + info_soft.codec = "hevc"; + info_soft.caps = OBS_ENCODER_CAP_DYN_BITRATE | OBS_ENCODER_CAP_INTERNAL | OBS_ENCODER_CAP_DEPRECATED; + + for (auto e : encoders) { + + /* ignore the software encoder due to the fact that we already + * have an objectively superior software encoder available */ + if (e->Type() == MF::EncoderType::H264_SOFTWARE) + continue; + + /* certain encoders such as quicksync will be "available" but + * not usable with certain processors */ + if (!CanSpawnEncoder(e)) + continue; + + info_soft.id = "qcom_hevc"; + info_soft.type_data = new TypeData(e); + info_soft.free_type_data = [](void *type_data) { + delete reinterpret_cast(type_data); + }; + obs_register_encoder(&info_soft); + } +} diff --git a/plugins/win-mediafoundation/encoder/mf-plugin.cpp b/plugins/win-mediafoundation/encoder/mf-plugin.cpp new file mode 100644 index 00000000000000..a2fe84472e9fa5 --- /dev/null +++ b/plugins/win-mediafoundation/encoder/mf-plugin.cpp @@ -0,0 +1,25 @@ +#include +#include + +#include "mf-common.hpp" + +extern void RegisterMFH264Encoders(); +extern void RegisterMFHEVCEncoders(); +extern void RegisterMFAV1Encoders(); + +extern "C" bool obs_module_load(void) +{ + MFStartup(MF_VERSION, MFSTARTUP_FULL); + RegisterMFH264Encoders(); + RegisterMFHEVCEncoders(); + RegisterMFAV1Encoders(); + return true; +} + +extern "C" void obs_module_unload(void) +{ + MFShutdown(); +} + +OBS_DECLARE_MODULE() +OBS_MODULE_USE_DEFAULT_LOCALE("win-mediafoundation-encoder", "en-US")