How do I play protected content with Media Foundation?

247 views Asked by At

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);
1

There are 1 answers

1
Luke On

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.