diff --git a/obs/audio-encoders.cpp b/obs/audio-encoders.cpp index 94ca79ef0..f689075f3 100644 --- a/obs/audio-encoders.cpp +++ b/obs/audio-encoders.cpp @@ -13,6 +13,7 @@ using namespace std; static const string encoders[] = { "ffmpeg_aac", + "mf_aac", "libfdk_aac", "CoreAudio_AAC", }; diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 2109d9b2b..aea99366b 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -8,6 +8,7 @@ if(WIN32) add_subdirectory(win-dshow) add_subdirectory(win-capture) add_subdirectory(decklink/win) + add_subdirectory(win-mf) elseif(APPLE) add_subdirectory(coreaudio-encoder) add_subdirectory(mac-avcapture) diff --git a/plugins/win-mf/CMakeLists.txt b/plugins/win-mf/CMakeLists.txt new file mode 100644 index 000000000..41ae06826 --- /dev/null +++ b/plugins/win-mf/CMakeLists.txt @@ -0,0 +1,23 @@ +project(win-mf) + +set(win-mf_SOURCES + mf-plugin.c + mf-aac.cpp + mf-aac-encoder.cpp) + +set(win-mf_HEADERS + mf-aac-encoder.hpp) + +add_library(win-mf MODULE + ${win-mf_SOURCES} + ${win-mf_HEADERS}) + +target_link_libraries(win-mf + uuid + mfplat + mfuuid + mf + wmcodecdspuuid + libobs) + +install_obs_plugin_with_data(win-mf data) diff --git a/plugins/win-mf/data/locale/en-US.ini b/plugins/win-mf/data/locale/en-US.ini new file mode 100644 index 000000000..380fad819 --- /dev/null +++ b/plugins/win-mf/data/locale/en-US.ini @@ -0,0 +1,2 @@ +MFAACEnc="Media Foundation AAC Encoder" +Bitrate="Bitrate" diff --git a/plugins/win-mf/mf-aac-encoder.cpp b/plugins/win-mf/mf-aac-encoder.cpp new file mode 100644 index 000000000..05303150f --- /dev/null +++ b/plugins/win-mf/mf-aac-encoder.cpp @@ -0,0 +1,346 @@ +#include + +#include "mf-aac-encoder.hpp" + +#include +#include +#include +#include + +#define MF_LOG_AAC(level, format, ...) \ + MF_LOG_ENCODER("AAC", ObsEncoder(), level, format, ##__VA_ARGS__) + +#define MF_LOG_COM(msg, hr) MF_LOG_AAC(LOG_ERROR, \ + msg " failed, %S (0x%08lx)", \ + _com_error(hr).ErrorMessage(), hr) + +#define HRC(r) \ + if(FAILED(hr = (r))) { \ + MF_LOG_COM(#r, hr); \ + goto fail; \ + } + +using namespace MFAAC; + +#define CONST_ARRAY(name, ...) static const UINT32 name[] = { __VA_ARGS__ }; + +CONST_ARRAY(VALID_BITRATES, 96, 128, 160, 192); +CONST_ARRAY(VALID_CHANNELS, 1, 2); +CONST_ARRAY(VALID_BITS_PER_SAMPLE, 16); +CONST_ARRAY(VALID_SAMPLERATES, 44100, 48000 ); + +#undef CONST_ARRAY + +template +static UINT32 FindBestMatch(const UINT32 (&validValues)[N], UINT32 value) +{ + for (UINT32 val : validValues) { + if (val >= value) + return val; + } + + // Only downgrade if no values are better + return validValues[N - 1]; +} + +template +static bool IsValid(const UINT32 (&validValues)[N], UINT32 value) +{ + for (UINT32 val : validValues) { + if (val == value) + return true; + } + + return false; +}; + +UINT32 MFAAC::FindBestBitrateMatch(UINT32 value) +{ + return FindBestMatch(VALID_BITRATES, value); +} + +UINT32 MFAAC::FindBestChannelsMatch(UINT32 value) +{ + return FindBestMatch(VALID_CHANNELS, value); +} + +UINT32 MFAAC::FindBestBitsPerSampleMatch(UINT32 value) +{ + return FindBestMatch(VALID_BITS_PER_SAMPLE, value); +} + +UINT32 MFAAC::FindBestSamplerateMatch(UINT32 value) +{ + return FindBestMatch(VALID_SAMPLERATES, value); +} + +bool MFAAC::BitrateValid(UINT32 value) +{ + return IsValid(VALID_BITRATES, value); +} + +bool MFAAC::ChannelsValid(UINT32 value) +{ + return IsValid(VALID_CHANNELS, value); +} + +bool MFAAC::BitsPerSampleValid(UINT32 value) +{ + return IsValid(VALID_BITS_PER_SAMPLE, value); +} + +bool MFAAC::SamplerateValid(UINT32 value) +{ + return IsValid(VALID_SAMPLERATES, value); +} + +HRESULT MFAAC::Encoder::CreateMediaTypes(ComPtr &i, + ComPtr &o) +{ + HRESULT hr; + HRC(MFCreateMediaType(&i)); + HRC(MFCreateMediaType(&o)); + + HRC(i->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio)); + HRC(i->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM)); + HRC(i->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, bitsPerSample)); + HRC(i->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, sampleRate)); + HRC(i->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, channels)); + + HRC(o->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio)); + HRC(o->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_AAC)); + HRC(o->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, bitsPerSample)); + HRC(o->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, sampleRate)); + HRC(o->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, channels)); + HRC(o->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, + (bitrate * 1000) / 8)); + + return S_OK; +fail: + return hr; +} + +void MFAAC::Encoder::InitializeExtraData() +{ + UINT16 *extraData16 = (UINT16 *)extraData; + UINT16 profile = 2; //Low Complexity +#define SWAPU16(x) (x>>8) | (x<<8) + // Profile + // XXXX X... .... .... + *extraData16 = profile << 11; + // Sample Index (3=48, 4=44.1) + // .... .XXX X... .... + *extraData16 |= sampleRate == 48000 ? 3 : 4 << 7; + // Channels + // .... .... .XXX X... + *extraData16 |= channels << 3; + *extraData16 = SWAPU16(*extraData16); + + // Extensions + extraData16++; + *extraData16 = 0x2b7 << 5; + // Profile + *extraData16 |= profile; + *extraData16 = SWAPU16(*extraData16); + + extraData[4] = 0; +#undef SWAPU16 +} + +bool MFAAC::Encoder::Initialize() +{ + HRESULT hr; + + ComPtr transform_; + ComPtr inputType, outputType; + + if (!BitrateValid(bitrate)) { + MF_LOG_AAC(LOG_WARNING, "invalid bitrate (kbps) '%d'", bitrate); + return false; + } + if (!ChannelsValid(channels)) { + MF_LOG_AAC(LOG_WARNING, "invalid channel count '%d", channels); + return false; + } + if (!SamplerateValid(sampleRate)) { + MF_LOG_AAC(LOG_WARNING, "invalid sample rate (hz) '%d", + sampleRate); + return false; + } + if (!BitsPerSampleValid(bitsPerSample)) { + MF_LOG_AAC(LOG_WARNING, "invalid bits-per-sample (bits) '%d'", + bitsPerSample); + return false; + } + + InitializeExtraData(); + + HRC(CoCreateInstance(CLSID_AACMFTEncoder, NULL, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&transform_))); + HRC(CreateMediaTypes(inputType, outputType)); + + HRC(transform_->SetInputType(0, inputType.Get(), 0)); + HRC(transform_->SetOutputType(0, outputType.Get(), 0)); + + HRC(transform_->ProcessMessage(MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, + NULL)); + HRC(transform_->ProcessMessage(MFT_MESSAGE_NOTIFY_START_OF_STREAM, + NULL)); + + MF_LOG_AAC(LOG_INFO, "encoder created\n" + "\tbitrate: %d\n" + "\tchannels: %d\n" + "\tsample rate: %d\n" + "\tbits-per-sample: %d\n", + bitrate, channels, sampleRate, bitsPerSample); + + transform = transform_; + return true; + +fail: + return false; +} + +HRESULT MFAAC::Encoder::CreateEmptySample(ComPtr &sample, + ComPtr &buffer, DWORD length) +{ + HRESULT hr; + + HRC(MFCreateSample(&sample)); + HRC(MFCreateMemoryBuffer(length, &buffer)); + HRC(sample->AddBuffer(buffer.Get())); + return S_OK; + +fail: + return hr; +} + +HRESULT MFAAC::Encoder::EnsureCapacity(ComPtr &sample, DWORD length) +{ + HRESULT hr; + ComPtr buffer; + DWORD currentLength; + + if (!sample) { + HRC(CreateEmptySample(sample, buffer, length)); + } else { + HRC(sample->GetBufferByIndex(0, &buffer)); + } + + HRC(buffer->GetMaxLength(¤tLength)); + if (currentLength < length) { + HRC(sample->RemoveAllBuffers()); + HRC(MFCreateMemoryBuffer(length, &buffer)); + HRC(sample->AddBuffer(buffer)); + } else { + buffer->SetCurrentLength(0); + } + + packetBuffer.reserve(length); + + return S_OK; + +fail: + return hr; +} + +bool MFAAC::Encoder::ProcessInput(UINT8 *data, UINT32 data_length, + UINT64 pts, Status *status) +{ + HRESULT hr; + ComPtr sample; + ComPtr buffer; + BYTE *bufferData; + INT64 samplePts; + UINT32 samples; + UINT64 sampleDur; + + HRC(CreateEmptySample(sample, buffer, data_length)); + + HRC(buffer->Lock(&bufferData, NULL, NULL)); + memcpy(bufferData, data, data_length); + HRC(buffer->Unlock()); + HRC(buffer->SetCurrentLength(data_length)); + + samples = data_length / channels / (bitsPerSample / 8); + sampleDur = (UINT64)(((float) sampleRate / channels / samples) * 10000); + samplePts = pts / 100; + + HRC(sample->SetSampleTime(samplePts)); + HRC(sample->SetSampleDuration(sampleDur)); + + hr = transform->ProcessInput(0, sample, 0); + if (hr == MF_E_NOTACCEPTING) { + *status = NOT_ACCEPTING; + return true; + } else if (FAILED(hr)) { + MF_LOG_COM("process input", hr); + return false; + } + + *status = SUCCESS; + return true; + +fail: + *status = FAILURE; + return false; +} + +bool MFAAC::Encoder::ProcessOutput(UINT8 **data, UINT32 *dataLength, + UINT64 *pts, Status *status) +{ + HRESULT hr; + + DWORD outputFlags, outputStatus; + MFT_OUTPUT_STREAM_INFO outputInfo = {0}; + MFT_OUTPUT_DATA_BUFFER output = {0}; + ComPtr outputBuffer; + BYTE *bufferData; + DWORD bufferLength; + INT64 samplePts; + + HRC(transform->GetOutputStatus(&outputFlags)); + if (outputFlags != MFT_OUTPUT_STATUS_SAMPLE_READY) { + *status = NEED_MORE_INPUT; + return true; + } + + HRC(transform->GetOutputStreamInfo(0, &outputInfo)); + EnsureCapacity(outputSample, outputInfo.cbSize); + + output.pSample = outputSample.Get(); + + hr = transform->ProcessOutput(0, 1, &output, &outputStatus); + if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) { + *status = NEED_MORE_INPUT; + return true; + } else if (FAILED(hr)) { + MF_LOG_COM("process output", hr); + return false; + } + + HRC(outputSample->GetBufferByIndex(0, &outputBuffer)); + + HRC(outputBuffer->Lock(&bufferData, NULL, &bufferLength)); + packetBuffer.assign(bufferData, bufferData + bufferLength); + HRC(outputBuffer->Unlock()); + + HRC(outputSample->GetSampleTime(&samplePts)); + + *pts = samplePts * 100; + *data = &packetBuffer[0]; + *dataLength = bufferLength; + *status = SUCCESS; + return true; + +fail: + *status = FAILURE; + return false; +} + +bool MFAAC::Encoder::ExtraData(UINT8 **extraData_, UINT32 *extraDataLength) +{ + *extraData_ = extraData; + *extraDataLength = sizeof(extraData); + return true; +} diff --git a/plugins/win-mf/mf-aac-encoder.hpp b/plugins/win-mf/mf-aac-encoder.hpp new file mode 100644 index 000000000..d9aa2d25e --- /dev/null +++ b/plugins/win-mf/mf-aac-encoder.hpp @@ -0,0 +1,91 @@ +#pragma once + +#define WIN32_MEAN_AND_LEAN +#include +#undef WIN32_MEAN_AND_LEAN + +#include +#include + +#include +#include + +#include + +#define MF_LOG(level, format, ...) \ + blog(level, "[Media Foundation encoder]: " format, ##__VA_ARGS__) +#define MF_LOG_ENCODER(format_name, encoder, level, format, ...) \ + blog(level, "[Media Foundation %s: '%s']: " format, \ + format_name, obs_encoder_get_name(encoder), \ + ##__VA_ARGS__) + +namespace MFAAC { + +enum Status { + FAILURE, + SUCCESS, + NOT_ACCEPTING, + NEED_MORE_INPUT +}; + +class Encoder { +public: + Encoder(const obs_encoder_t *encoder, UINT32 bitrate, UINT32 channels, + UINT32 sampleRate, UINT32 bitsPerSample) + : encoder(encoder), + bitrate(bitrate), + channels(channels), + sampleRate(sampleRate), + bitsPerSample(bitsPerSample) + {} + + Encoder& operator=(Encoder const&) = delete; + + bool Initialize(); + bool ProcessInput(UINT8 *data, UINT32 dataLength, + UINT64 pts, MFAAC::Status *status); + bool ProcessOutput(UINT8 **data, UINT32 *dataLength, + UINT64 *pts, MFAAC::Status *status); + bool ExtraData(UINT8 **extraData, UINT32 *extraDataLength); + + const obs_encoder_t *ObsEncoder() { return encoder; } + UINT32 Bitrate() { return bitrate; } + UINT32 Channels() { return channels; } + UINT32 SampleRate() { return sampleRate; } + UINT32 BitsPerSample() { return bitsPerSample; } + + static const UINT32 FrameSize = 1024; + +private: + void InitializeExtraData(); + HRESULT CreateMediaTypes(ComPtr &inputType, + ComPtr &outputType); + HRESULT EnsureCapacity(ComPtr &sample, DWORD length); + HRESULT CreateEmptySample(ComPtr &sample, + ComPtr &buffer, DWORD length); + +private: + const obs_encoder_t *encoder; + const UINT32 bitrate; + const UINT32 channels; + const UINT32 sampleRate; + const UINT32 bitsPerSample; + + ComPtr transform; + ComPtr outputSample; + std::vector packetBuffer; + UINT8 extraData[5]; +}; + +static const UINT32 FrameSize = 1024; + +UINT32 FindBestBitrateMatch(UINT32 value); +UINT32 FindBestChannelsMatch(UINT32 value); +UINT32 FindBestBitsPerSampleMatch(UINT32 value); +UINT32 FindBestSamplerateMatch(UINT32 value); +bool BitrateValid(UINT32 value); +bool ChannelsValid(UINT32 value); +bool BitsPerSampleValid(UINT32 value); +bool SamplerateValid(UINT32 value); + +} diff --git a/plugins/win-mf/mf-aac.cpp b/plugins/win-mf/mf-aac.cpp new file mode 100644 index 000000000..615add77c --- /dev/null +++ b/plugins/win-mf/mf-aac.cpp @@ -0,0 +1,168 @@ +#include + +#include + +#include "mf-aac-encoder.hpp" + +#include + +using namespace MFAAC; + +static const char *MFAAC_GetName() +{ + return obs_module_text("MFAACEnc"); +} + +static obs_properties_t *MFAAC_GetProperties(void *) +{ + obs_properties_t *props = obs_properties_create(); + + obs_properties_add_int(props, "bitrate", + obs_module_text("Bitrate"), 96, 192, 32); + + return props; +} + +static void MFAAC_GetDefaults(obs_data_t *settings) +{ + obs_data_set_default_int(settings, "bitrate", 128); +} + +static void *MFAAC_Create(obs_data_t *settings, obs_encoder_t *encoder) +{ + UINT32 bitrate = (UINT32)obs_data_get_int(settings, "bitrate"); + if (!bitrate) { + MF_LOG_ENCODER("AAC", encoder, LOG_ERROR, + "Invalid bitrate specified"); + return NULL; + } + + audio_t *audio = obs_encoder_audio(encoder); + UINT32 channels = (UINT32)audio_output_get_channels(audio); + UINT32 sampleRate = audio_output_get_sample_rate(audio); + UINT32 bitsPerSample = 16; + + UINT32 recommendedSampleRate = FindBestSamplerateMatch(sampleRate); + if (recommendedSampleRate != sampleRate) { + MF_LOG_ENCODER("AAC", encoder, LOG_WARNING, + "unsupported sample rate; " + "resampling to best guess '%d' instead of '%d'", + recommendedSampleRate, sampleRate); + sampleRate = recommendedSampleRate; + } + + UINT32 recommendedBitRate = FindBestBitrateMatch(bitrate); + if (recommendedBitRate != bitrate) { + MF_LOG_ENCODER("AAC", encoder, LOG_WARNING, + "unsupported bitrate; " + "resampling to best guess '%d' instead of '%d'", + recommendedBitRate, bitrate); + bitrate = recommendedBitRate; + } + + std::unique_ptr enc(new Encoder(encoder, + bitrate, channels, sampleRate, bitsPerSample)); + + audio_convert_info aci; + aci.samples_per_sec = sampleRate; + + if (!enc->Initialize()) + return nullptr; + + return enc.release(); +} + +static void MFAAC_Destroy(void *data) +{ + Encoder *enc = static_cast(data); + delete enc; +} + +static bool MFAAC_Encode(void *data, struct encoder_frame *frame, + struct encoder_packet *packet, bool *received_packet) +{ + Encoder *enc = static_cast(data); + Status status; + + if (!enc->ProcessInput(frame->data[0], frame->linesize[0], frame->pts, + &status)) + return false; + + // This shouldn't happen since we drain right after + // we process input + if (status == NOT_ACCEPTING) + return false; + + UINT8 *outputData; + UINT32 outputDataLength; + UINT64 outputPts; + + if (!enc->ProcessOutput(&outputData, &outputDataLength, &outputPts, + &status)) + return false; + + // Needs more input, not a failure case + if (status == NEED_MORE_INPUT) + return true; + + packet->pts = outputPts; + packet->dts = outputPts; + packet->data = outputData; + packet->size = outputDataLength; + packet->type = OBS_ENCODER_AUDIO; + packet->timebase_num = 1; + packet->timebase_den = enc->SampleRate(); + + return *received_packet = true; +} + +static bool MFAAC_GetExtraData(void *data, uint8_t **extra_data, size_t *size) +{ + Encoder *enc = static_cast(data); + + UINT32 length; + if (enc->ExtraData(extra_data, &length)) { + *size = length; + return true; + } + return false; +} + +static void MFAAC_GetAudioInfo(void *, struct audio_convert_info *info) +{ + info->format = AUDIO_FORMAT_16BIT; + info->samples_per_sec = FindBestSamplerateMatch(info->samples_per_sec); +} + +static size_t MFAAC_GetFrameSize(void *) +{ + return Encoder::FrameSize; +} + +extern "C" void RegisterMFAACEncoder() +{ + if (!IsWindows8OrGreater()) { + MF_LOG(LOG_WARNING, "plugin is disabled for performance " + "reasons on Windows versions less than 8"); + return; + } + + obs_encoder_info info = {}; + info.id = "mf_aac"; + info.type = OBS_ENCODER_AUDIO; + info.codec = "AAC"; + info.get_name = MFAAC_GetName; + info.create = MFAAC_Create; + info.destroy = MFAAC_Destroy; + info.encode = MFAAC_Encode; + info.get_frame_size = MFAAC_GetFrameSize; + info.get_defaults = MFAAC_GetDefaults; + info.get_properties = MFAAC_GetProperties; + info.get_extra_data = MFAAC_GetExtraData; + info.get_audio_info = MFAAC_GetAudioInfo; + + MF_LOG(LOG_INFO, "Adding Media Foundation AAC Encoder"); + + obs_register_encoder(&info); + +} diff --git a/plugins/win-mf/mf-plugin.c b/plugins/win-mf/mf-plugin.c new file mode 100644 index 000000000..d55feda2f --- /dev/null +++ b/plugins/win-mf/mf-plugin.c @@ -0,0 +1,12 @@ +#include + +extern void RegisterMFAACEncoder(); + +bool obs_module_load(void) +{ + RegisterMFAACEncoder(); + return true; +} + +OBS_DECLARE_MODULE() +OBS_MODULE_USE_DEFAULT_LOCALE("win-mf", "en-US")