1

I'm trying to playback mp4 files using Media Foundation. I am using this documentation as a reference. Using that logic I can playback unprotected files but cannot playback protected files. In order to playback protected files, you need to do some additional work as documented here. I am using the first two mp4 test files published here, because surely Media Foundation works with Microsoft's own DRM system. The problem is that no matter what I do, any protected mp4 file fails playback with MF_E_UNSUPPORTED_BYTESTREAM_TYPE. The failure occurs during the MESessionTopologySet event, and my implementation of IMFContentProtectionManager is not invoked for anything other than QueryInterface(). This is all on Windows 10. So how do I playback protected mp4 files using Media Foundation?

// initialization
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
hr = MFStartup(MF_VERSION);

// load source
IMFSourceResolver* SourceResolver = NULL;
hr = MFCreateSourceResolver(&SourceResolver);
MF_OBJECT_TYPE ObjectType = MF_OBJECT_INVALID;
IUnknown* Object = NULL;
hr = SourceResolver->CreateObjectFromURL(L"tearsofsteel_1080p_60s_24fps.6000kbps.1920x1080.h264-8b.2ch.128kbps.aac.avsep.cenc.mp4", MF_RESOLUTION_MEDIASOURCE | MF_RESOLUTION_READ, NULL, &ObjectType, &Object);
IMFMediaSource* MediaSource = NULL;
hr = Object->QueryInterface(&MediaSource);
IMFPresentationDescriptor* PresentationDescriptor = NULL;
hr = MediaSource->CreatePresentationDescriptor(&PresentationDescriptor);
hr = MFRequireProtectedEnvironment(PresentationDescriptor);
wprintf_s(L"MFRequireProtectedEnvironment() hr=0x%.08X\n", hr);

// get first stream
IMFStreamDescriptor* StreamDescriptor = NULL;
BOOL Selected = FALSE;
hr = PresentationDescriptor->GetStreamDescriptorByIndex(0, &Selected, &StreamDescriptor);
IMFMediaTypeHandler* MediaTypeHandler = NULL;
hr = StreamDescriptor->GetMediaTypeHandler(&MediaTypeHandler);
GUID MajorType = GUID_NULL;
hr = MediaTypeHandler->GetMajorType(&MajorType);

// create appropriate renderer
IMFActivate* Activate = NULL;
if (MajorType == MFMediaType_Video)
{
    hr = MFCreateVideoRendererActivate(NULL, &Activate);
}
else if (MajorType == MFMediaType_Audio)
{
    hr = MFCreateAudioRendererActivate(&Activate);
}

// build the topology
IMFTopology* Topology = NULL;
hr = MFCreateTopology(&Topology);

// build the source node
IMFTopologyNode* SourceTopologyNode = NULL;
hr = MFCreateTopologyNode(MF_TOPOLOGY_SOURCESTREAM_NODE, &SourceTopologyNode);
hr = SourceTopologyNode->SetUnknown(MF_TOPONODE_SOURCE, MediaSource);
hr = SourceTopologyNode->SetUnknown(MF_TOPONODE_PRESENTATION_DESCRIPTOR, PresentationDescriptor);
hr = SourceTopologyNode->SetUnknown(MF_TOPONODE_STREAM_DESCRIPTOR, StreamDescriptor);
hr = Topology->AddNode(SourceTopologyNode);

// build the output node
IMFTopologyNode* OutputTopologyNode = NULL;
hr = MFCreateTopologyNode(MF_TOPOLOGY_OUTPUT_NODE, &OutputTopologyNode);
hr = OutputTopologyNode->SetObject(Activate);
hr = OutputTopologyNode->SetUINT32(MF_TOPONODE_STREAMID, 0);
hr = OutputTopologyNode->SetUINT32(MF_TOPONODE_NOSHUTDOWN_ON_REMOVE, FALSE);
hr = Topology->AddNode(OutputTopologyNode);
hr = SourceTopologyNode->ConnectOutput(0, OutputTopologyNode, 0);

// create the protected session
IMFMediaSession* MediaSession = NULL;
IMFAttributes* Configuration = NULL;
hr = MFCreateAttributes(&Configuration, 1);
IUnknown* ContentProtectionManager = new ContentProtectionManagerImpl();
Configuration->SetUnknown(MF_SESSION_CONTENT_PROTECTION_MANAGER, ContentProtectionManager);
IMFActivate* EnablerActivate = NULL;
hr = MFCreatePMPMediaSession(0, Configuration, &MediaSession, &EnablerActivate);

// set the topology
hr = MediaSession->SetTopology(0, Topology);

// get the event status
IMFMediaEvent* Event = NULL;
hr = MediaSession->GetEvent(0, &Event);
MediaEventType Type = MEUnknown;
hr = Event->GetType(&Type);
HRESULT Status = S_OK;
hr = Event->GetStatus(&Status);
wprintf_s(L"Event Type=%d Status=0x%.08X\n", Type, Status);
Roman R.
  • 68,205
  • 6
  • 94
  • 158
Luke
  • 11,211
  • 2
  • 27
  • 38
  • 1
    I don't think this can work w/o having async implementations (IMFAsyncCallback) and maybe using a windows message pump in the thread. The (old) official sample https://learn.microsoft.com/en-us/windows/win32/medfound/protectedplayback-sample uses async and a Windows app. And it does receive calls to IMFContentProtectionManager using the protected sample avsep.cenc.mp4 you link to (and then it fails probably because the LA_URL is invalid and needs to be fixed, but that's another story). At least this sample should be the starting point. – Simon Mourier Jul 19 '23 at 17:00
  • Thanks a ton for the sample, will take a look tomorrow and see what I'm missing. – Luke Jul 19 '23 at 17:18
  • @SimonMourier Are you saying that the MF_ProtectedPlayback sample project successfully plays the tearsofsteel_1080p_60s_24fps.6000kbps.1920x1080.h264-8b.2ch.128kbps.aac.avsep.cenc.mp4 file? That's not happening for me; I'm getting a message box with error code 0xC00D36C4 = MF_E_UNSUPPORTED_BYTESTREAM_TYPE. – Luke Jul 19 '23 at 20:46
  • It heavily depends on your setup, Windows, graphic card, drivers, screen. As is the sample cannot work (I have another error 0x80070241), but it should call IMFContentProtectionManager methods. – Simon Mourier Jul 19 '23 at 21:14
  • @SimonMourier Ok, so how do I diagnose this? I've run mftrace.exe against the sample app using both files, and the logs are identical to the point the clear file starts playing and the encrypted file doesn't. I also tried the various Media Foundation event logs and they don't provide any useful information. – Luke Jul 20 '23 at 09:16
  • Is your IMFContentProtectionManager called? Is it called in the official sample? – Simon Mourier Jul 20 '23 at 09:52
  • No. The manager is created and gets called for QueryInterface() many times, but none of the IMFContentProtectionManager methods are called. It seems like the parser is failing before it even tries to process any protection information. – Luke Jul 20 '23 at 10:15
  • I think this kind of Media Foundation playback for DRM content simply went out of support. Microsoft is notorious for not updated documentation for this APIs, both not mentioning new functionality and deprecating old. – Roman R. Jul 20 '23 at 17:37
  • I find that hard to believe, but assuming that's true I guess the ultimate question is this: how can I play protected content with **any** Windows API? Surely there must exist a way to do this outside of a browser. – Luke Jul 21 '23 at 10:13
  • A possible lead: https://github.com/chromium/chromium/blob/main/media/renderers/win/media_foundation_protection_manager.cc#L70 – Luke Jul 21 '23 at 10:44
  • `1` https://learn.microsoft.com/en-us/samples/microsoft/windows-universal-samples/playready/ still works `2` this one should also be good https://github.com/microsoft/media-foundation/tree/master/samples/MediaEngineEMEUWPSample - it's active and EME assumes protected content `3` Chromiun source code is also good source (I am pretty sure), but I did not have time/reason to check myself. – Roman R. Jul 21 '23 at 11:57
  • Ah, the 2nd one looks like what I'm looking for and is very similar to the chromium source as well. Thanks, I'll have to play around with it a bit. – Luke Jul 21 '23 at 12:55
  • 1
    I had this working in a UWP app but I really wanted it in a Win32 app. I found the secret sauce via google: https://github.com/microsoft/media-foundation/issues/37 – Luke Jul 21 '23 at 18:25
  • @luke - have you got something working? Can you answer the question with sample code or should we go on trying. – Simon Mourier Jul 25 '23 at 10:14
  • I do but it's quite ugly. I intend to come back and post an answer at some point. – Luke Jul 25 '23 at 11:11

1 Answers1

1

I managed to get this working but it's kind of a mess. Rather than post a complete code example, I'll break it down into different components. This is based on the UWP sample code. Error handling and cleanup is omitted for brevity.

First, you need to create and configure an instance of an IMFContentDecryptionModule object:

LPCWSTR KeySystem = L"com.microsoft.playready";

IMFMediaEngineClassFactory4* MediaEngineClassFactory4 = NULL;
CoCreateInstance(CLSID_MFMediaEngineClassFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&MediaEngineClassFactory4));

IMFContentDecryptionModuleFactory* ContentDecryptionModuleFactory = NULL;
MediaEngineClassFactory4->CreateContentDecryptionModuleFactory(KeySystem, IID_PPV_ARGS(&ContentDecryptionModuleFactory));

IPropertyStore* ContentDecryptionModuleAccess_PropertyStore = NULL;
PSCreateMemoryPropertyStore(IID_PPV_ARGS(&ContentDecryptionModuleAccess_PropertyStore));

// set the following properties in the property store (not sure if all are strictly required):
// MF_EME_INITDATATYPES = (VT_VECTOR | VT_BSTR) [ "cenc" ]
// MF_EME_AUDIOCAPABILITIES = (VT_VECTOR | VT_VARIANT) []
// MF_EME_VIDEOCAPABILITIES = (VT_VECTOR | VT_VARIANT) []
// MF_EME_DISTINCTIVEID = (VT_UI4) MF_MEDIAKEYS_REQUIREMENT_REQUIRED
// MF_EME_PERSISTEDSTATE = (VT_UI4) MF_MEDIAKEYS_REQUIREMENT_OPTIONAL
// MF_EME_SESSIONTYPES = (VT_VECTOR | VT_UI4) []

IMFContentDecryptionModuleAccess* ContentDecryptionModuleAccess = NULL;
ContentDecryptionModuleFactory->CreateContentDecryptionModuleAccess(KeySystem, &ContentDecryptionModuleAccess_PropertyStore, 1, &ContentDecryptionModuleAccess);

IPropertyStore* ContentDecryptionModule_PropertyStore = NULL;
PSCreateMemoryPropertyStore(IID_PPV_ARGS(&ContentDecryptionModule_PropertyStore));

// set the following properties in the property store
// MF_EME_CDM_STOREPATH = (VT_BSTR) "some path for the CDM to store its data" (NOTE: path must exist or subsequent calls will fail)

IMFContentDecryptionModule* ContentDecryptionModule = NULL;
ContentDecryptionModuleAccess->CreateContentDecryptionModule(ContentDecryptionModule_PropertyStore, &ContentDecryptionModule);

Ok, we now have a content decryption module that we can use to playback PlayReady content. Next, because we're not running in a UWP app we need to provide our own instance of a IMFPMPHostApp object as described here.

IMFPMPHost* Host = NULL;
MFGetService(ContentDecryptionModule, MF_CONTENTDECRYPTIONMODULE_SERVICE, IID_PPV_ARGS(&Host));
PMPHostAppImpl* PMPHostApp = new PMPHostAppImpl(Host);
ContentDecryptionModule->SetPMPHostApp(PMPHostApp);

The methods you need to implement are as follows:

STDMETHODIMP PMPHostAppImpl::LockProcess()
{
    return E_NOTIMPL;
}

STDMETHODIMP PMPHostAppImpl::UnlockProcess()
{
    return E_NOTIMPL;
}

STDMETHODIMP PMPHostAppImpl::ActivateClassById(LPCWSTR id, IStream* pStream, REFIID riid, void** ppv)
{
    IMFAttributes* Attributes = NULL;
    MFCreateAttributes(&Attributes, 3);

    Attributes->SetString(GUID_ClassName, id);

    if (pStream)
    {
        STATSTG statstg = { 0 };
        pStream->Stat(&statstg, STATFLAG_NOOPEN | STATFLAG_NONAME);
        std::vector<uint8_t> StreamBlob(statstg.cbSize.LowPart);
        ULONG BytesRead = 0;
        pStream->Read(&StreamBlob[0], (ULONG)StreamBlob.size(), &BytesRead);
        Attributes->SetBlob(GUID_ObjectStream, &StreamBlob[0], BytesRead);
    }

    IStream* OutputStream = NULL;
    CreateStreamOnHGlobal(NULL, TRUE, &OutputStream);
    MFSerializeAttributesToStream(Attributes, 0, OutputStream);
    OutputStream->Seek({}, STREAM_SEEK_SET, NULL);

    IMFActivate* Activator = NULL;
    m_Host->CreateObjectByCLSID(CLSID_EMEStoreActivate, OutputStream, IID_PPV_ARGS(&Activator));
    Activator->ActivateObject(riid, ppv);

    return S_OK;
}

Now we need to create a content decryption session and generate a license request:

ContentDecryptionModuleSessionCallbacksImpl* ContentDecryptionModuleSessionCallbacks = new ContentDecryptionModuleSessionCallbacksImpl();

IMFContentDecryptionModuleSession* ContentDecryptionModuleSession = NULL;
ContentDecryptionModule->CreateSession(MF_MEDIAKEYSESSION_TYPE_TEMPORARY, ContentDecryptionModuleSessionCallbacks, &ContentDecryptionModuleSession);

ContentDecryptionModuleSessionCallbacks->SetContentDecryptionModuleSession(ContentDecryptionModuleSession);

ContentDecryptionModuleSession->GenerateRequest(L"cenc", InitDataBuffer, InitDataSize);

InitDataBuffer and InitDataSize contain the PlayReady PSSH box from the MP4 file. ContentDecryptionModuleSessionCallbacksImpl needs to implement the following methods:

STDMETHODIMP KeyMessage(MF_MEDIAKEYSESSION_MESSAGETYPE messageType, const BYTE* message, DWORD messageSize, LPCWSTR destinationURL)
{
    std::wstring XmlText((wchar_t*)message, messageSize / sizeof(wchar_t));
    // pseudocode
    XmlDocument Document = new XmlDocument(XmlText);
    Challenge = Document["PlayReadyKeyMessage"]["LicenseAcquisition"]["Challenge"];
    HttpHeaders = Document["PlayReadyKeyMessage"]["LicenseAcquisition"]["HttpHeaders"];
    ResponseBody = HttpPost(Url=destinationURL, RequestHeaders=HttpHeaders, RequestBody=Base64Decode(Challenge));
    m_ContentDecryptionModuleSession->Update(ResponseBody.Buffer, ResponseBody.Size);
    return S_OK;
}

STDMETHODIMP KeyStatusChanged()
{
    MFMediaKeyStatus* KeyStatusList = NULL;
    UINT KeyStatusCount = 0;
    m_ContentDecryptionModuleSession->GetKeyStatuses(&KeyStatusList, &KeyStatusCount);
    for (UINT KeyStatusIndex = 0; KeyStatusIndex < KeyStatusCount; KeyStatusIndex++)
    {
        BOOL IsKeyValid = (KeyStatusList[KeyStatusIndex].eMediaKeyStatus == MF_MEDIAKEY_STATUS_USABLE);
    }
    return S_OK;
}

Now we create our media source:

IMFSourceResolver* SourceResolver = NULL;
MFCreateSourceResolver(&SourceResolver);

MF_OBJECT_TYPE ObjectType = MF_OBJECT_INVALID;
IUnknown* Unknown = NULL;
SourceResolver->CreateObjectFromURL(pwszURL, MF_RESOLUTION_MEDIASOURCE | MF_RESOLUTION_READ, NULL, &ObjectType, &Unknown);

IMFMediaSource* MediaSource = NULL;
Unknown->QueryInterface(&MediaSource);

Then we create our media engine extension, which is required to load our media source (otherwise we'll get MF_E_UNSUPPORTED_BYTESTREAM_TYPE):

MediaEngineExtensionImpl* MediaEngineExtension = new MediaEngineExtensionImpl();

Methods we need to implement:

STDMETHODIMP MediaEngineExtensionImpl::CanPlayType(BOOL AudioOnly, BSTR MimeType, MF_MEDIA_ENGINE_CANPLAY* pAnswer)
{
    return E_NOTIMPL;
}

STDMETHODIMP MediaEngineExtensionImpl::BeginCreateObject(BSTR bstrURL, IMFByteStream* pByteStream, MF_OBJECT_TYPE type, IUnknown** ppIUnknownCancelCookie, IMFAsyncCallback* pCallback, IUnknown* punkState)
{
    if (lstrcmpW(bstrURL, L"CustomSource") == 0)
    {
        if (type == MF_OBJECT_MEDIASOURCE)
        {
            IMFAsyncResult* AsyncResult = NULL;
            MFCreateAsyncResult(m_MediaSource, pCallback, punkState, &AsyncResult);
            AsyncResult->SetStatus(S_OK);
            pCallback->Invoke(AsyncResult);
            return S_OK;
        }
    }
    return E_UNEXPECTED;
}

STDMETHODIMP MediaEngineExtensionImpl::CancelObjectCreation(IUnknown* pIUnknownCancelCookie)
{
    return E_NOTIMPL;
}

STDMETHODIMP MediaEngineExtensionImpl::EndCreateObject(IMFAsyncResult* pResult, IUnknown** ppObject)
{
    pResult->GetObject(ppObject);
    return S_OK;
}

Then we can create the media engine object:

IMFAttributes* Attributes = NULL;
MFCreateAttributes(&Attributes, 5);

MediaEngineNotifyImpl* MediaEngineNotify = new MediaEngineNotifyImpl();

Attributes->SetUnknown(MF_MEDIA_ENGINE_CALLBACK, MediaEngineNotify);
Attributes->SetUINT32(MF_MEDIA_ENGINE_CONTENT_PROTECTION_FLAGS, MF_MEDIA_ENGINE_ENABLE_PROTECTED_CONTENT);
Attributes->SetGUID(MF_MEDIA_ENGINE_BROWSER_COMPATIBILITY_MODE, MF_MEDIA_ENGINE_BROWSER_COMPATIBILITY_MODE_IE_EDGE);
Attributes->SetUnknown(MF_MEDIA_ENGINE_EXTENSION, MediaEngineExtension);
Attributes->SetUINT64(MF_MEDIA_ENGINE_PLAYBACK_HWND, (UINT64)WindowHandle);

IMFMediaEngineClassFactory* MediaEngineClassFactory = NULL;
CoCreateInstance(CLSID_MFMediaEngineClassFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&MediaEngineClassFactory));

IMFMediaEngine* MediaEngine = NULL;
MediaEngineClassFactory->CreateInstance(0, Attributes, &MediaEngine);

Now we can configure the content protection manager:

IMFMediaEngineProtectedContent* MediaEngineProtectedContent = NULL;
MediaEngine->QueryInterface(&MediaEngineProtectedContent);

ABI::Windows::Media::Protection::IMediaProtectionPMPServer* MediaProtectionPMPServer = NULL;
MFGetService(ContentDecryptionModule, MF_CONTENTDECRYPTIONMODULE_SERVICE, IID_PPV_ARGS(&MediaProtectionPMPServer));

IInspectable* PropertySet_Inspectable = NULL;
RoActivateInstance(Microsoft::WRL::Wrappers::HStringReference(RuntimeClass_Windows_Foundation_Collections_PropertySet).Get(), &PropertySet_Inspectable);

ABI::Windows::Foundation::Collections::IMap<HSTRING, IInspectable*>* PropertyMap = NULL;
PropertySet_Inspectable->QueryInterface(&PropertyMap);

boolean Replaced = false;
PropertyMap->Insert(Microsoft::WRL::Wrappers::HStringReference(L"Windows.Media.Protection.MediaProtectionPMPServer").Get(), MediaProtectionPMPServer, &Replaced);

ABI::Windows::Foundation::Collections::IPropertySet* PropertySet = NULL;
PropertySet_Inspectable->QueryInterface(&PropertySet);

ContentProtectionManagerImpl* ContentProtectionManager = new ContentProtectionManagerImpl(PropertySet);
MediaEngineProtectedContent->SetContentProtectionManager(ContentProtectionManager);

The only method we need to implement:

STDMETHODIMP ContentProtectionManagerImpl::get_Properties(ABI::Windows::Foundation::Collections::IPropertySet** value)
{
    *value = m_PropertySet;
    m_PropertySet->AddRef();
    return S_OK;
}

Then we can create a wrapper for the media source to deal with content protection:

IMFTrustedInput* TrustedInput = NULL;
ContentDecryptionModule->CreateTrustedInput(NULL, 0, &TrustedInput);

ProtectedMediaSourceImpl* ProtectedMediaSource = new ProtectedMediaSourceImpl(MediaSource, TrustedInput);

MediaEngineExtension->SetSource(ProtectedMediaSource);

Where we need to implement:

STDMETHODIMP ProtectedMediaSourceImpl::GetInputTrustAuthority(DWORD dwStreamID, REFIID riid, IUnknown** ppunkObject)
{
    IUnknown* TrustAuthority = m_TrustAuthorities[dwStreamID];
    if (!TrustAuthority)
    {
        m_TrustedInput->GetInputTrustAuthority(dwStreamID, riid, &TrustAuthority);
        m_TrustAuthorities[dwStreamID] = TrustAuthority;
    }
    TrustAuthority->AddRef();
    *ppunkObject = TrustAuthority;
    return S_OK;
}

Then finally we can set the media source via our extension:

IMFMediaEngineEx* MediaEngineEx = NULL;
MediaEngine->QueryInterface(&MediaEngineEx);

MediaEngineEx->SetSource(SysAllocString(L"CustomSource"));

And assuming all of that works, the media playback will start.

Yes, it is that simple!

I'm sure I either glossed over something or forgot to include something. Let me know if anything's missing and I'll update accordingly.

Luke
  • 11,211
  • 2
  • 27
  • 38
  • Another point I struggled with. I have to do some custom processing with the PlayReady request/response. The `IMFContentDecryptionModuleSessionCallbacks::KeyMessage()` method gives you XML in UTF-16, but the `IMFContentDecryptionModuleSession::Update()` method expects XML in UTF-8. That was fun trying to figure out. – Luke Jul 28 '23 at 11:34