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.