/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "MFCDMSession.h" #include #include #include #include "MFMediaEngineUtils.h" #include "GMPUtils.h" // ToHexString #include "mozilla/EMEUtils.h" #include "mozilla/dom/MediaKeyMessageEventBinding.h" #include "mozilla/dom/MediaKeyStatusMapBinding.h" #include "nsThreadUtils.h" namespace mozilla { using Microsoft::WRL::ComPtr; using Microsoft::WRL::MakeAndInitialize; #define LOG(msg, ...) EME_LOG("MFCDMSession=%p, " msg, this, ##__VA_ARGS__) static inline MF_MEDIAKEYSESSION_TYPE ConvertSessionType( KeySystemConfig::SessionType aType) { switch (aType) { case KeySystemConfig::SessionType::Temporary: return MF_MEDIAKEYSESSION_TYPE_TEMPORARY; case KeySystemConfig::SessionType::PersistentLicense: return MF_MEDIAKEYSESSION_TYPE_PERSISTENT_LICENSE; } } static inline LPCWSTR InitDataTypeToString(const nsAString& aInitDataType) { // The strings are defined in https://www.w3.org/TR/eme-initdata-registry/ if (aInitDataType.EqualsLiteral("webm")) { return L"webm"; } else if (aInitDataType.EqualsLiteral("cenc")) { return L"cenc"; } else if (aInitDataType.EqualsLiteral("keyids")) { return L"keyids"; } else { return L"unknown"; } } // The callback interface which IMFContentDecryptionModuleSession uses for // communicating with the session. class MFCDMSession::SessionCallbacks final : public Microsoft::WRL::RuntimeClass< Microsoft::WRL::RuntimeClassFlags, IMFContentDecryptionModuleSessionCallbacks> { public: SessionCallbacks() = default; SessionCallbacks(const SessionCallbacks&) = delete; SessionCallbacks& operator=(const SessionCallbacks&) = delete; ~SessionCallbacks() = default; HRESULT RuntimeClassInitialize() { return S_OK; } // IMFContentDecryptionModuleSessionCallbacks STDMETHODIMP KeyMessage(MF_MEDIAKEYSESSION_MESSAGETYPE aType, const BYTE* aMessage, DWORD aMessageSize, LPCWSTR aUrl) final { CopyableTArray msg{static_cast(aMessage), aMessageSize}; mKeyMessageEvent.Notify(aType, std::move(msg)); return S_OK; } STDMETHODIMP KeyStatusChanged() final { mKeyChangeEvent.Notify(); return S_OK; } MediaEventSource>& KeyMessageEvent() { return mKeyMessageEvent; } MediaEventSource& KeyChangeEvent() { return mKeyChangeEvent; } private: MediaEventProducer> mKeyMessageEvent; MediaEventProducer mKeyChangeEvent; }; /* static*/ MFCDMSession* MFCDMSession::Create(KeySystemConfig::SessionType aSessionType, IMFContentDecryptionModule* aCdm, nsISerialEventTarget* aManagerThread) { MOZ_ASSERT(aCdm); MOZ_ASSERT(aManagerThread); ComPtr callbacks; RETURN_PARAM_IF_FAILED(MakeAndInitialize(&callbacks), nullptr); ComPtr session; RETURN_PARAM_IF_FAILED(aCdm->CreateSession(ConvertSessionType(aSessionType), callbacks.Get(), &session), nullptr); return new MFCDMSession(session.Get(), callbacks.Get(), aManagerThread); } MFCDMSession::MFCDMSession(IMFContentDecryptionModuleSession* aSession, SessionCallbacks* aCallback, nsISerialEventTarget* aManagerThread) : mSession(aSession), mManagerThread(aManagerThread), mExpiredTimeMilliSecondsSinceEpoch( std::numeric_limits::quiet_NaN()) { MOZ_ASSERT(aSession); MOZ_ASSERT(aCallback); MOZ_ASSERT(aManagerThread); mKeyMessageListener = aCallback->KeyMessageEvent().Connect( mManagerThread, this, &MFCDMSession::OnSessionKeyMessage); mKeyChangeListener = aCallback->KeyChangeEvent().Connect( mManagerThread, this, &MFCDMSession::OnSessionKeysChange); } MFCDMSession::~MFCDMSession() { // TODO : maybe disconnect them in `Close()`? mKeyChangeListener.DisconnectIfExists(); mKeyMessageListener.DisconnectIfExists(); } HRESULT MFCDMSession::GenerateRequest(const nsAString& aInitDataType, const uint8_t* aInitData, uint32_t aInitDataSize) { AssertOnManagerThread(); LOG("GenerateRequest for %s (init sz=%u)", NS_ConvertUTF16toUTF8(aInitDataType).get(), aInitDataSize); RETURN_IF_FAILED(mSession->GenerateRequest( InitDataTypeToString(aInitDataType), aInitData, aInitDataSize)); Unused << RetrieveSessionId(); return S_OK; } HRESULT MFCDMSession::Load(const nsAString& aSessionId) { AssertOnManagerThread(); // TODO : do we need to implement this? Chromium doesn't implement this one. // Also, how do we know is this given session ID is equal to the session Id // asked from CDM session or not? BOOL rv = FALSE; mSession->Load(char16ptr_t(aSessionId.BeginReading()), &rv); LOG("Load, id=%s, rv=%s", NS_ConvertUTF16toUTF8(aSessionId).get(), rv ? "success" : "fail"); return rv ? S_OK : S_FALSE; } HRESULT MFCDMSession::Update(const nsTArray& aMessage) { AssertOnManagerThread(); LOG("Update"); RETURN_IF_FAILED(mSession->Update( static_cast(aMessage.Elements()), aMessage.Length())); RETURN_IF_FAILED(UpdateExpirationIfNeeded()); return S_OK; } HRESULT MFCDMSession::Close() { AssertOnManagerThread(); LOG("Close"); RETURN_IF_FAILED(mSession->Close()); return S_OK; } HRESULT MFCDMSession::Remove() { AssertOnManagerThread(); LOG("Remove"); RETURN_IF_FAILED(mSession->Remove()); RETURN_IF_FAILED(UpdateExpirationIfNeeded()); return S_OK; } bool MFCDMSession::RetrieveSessionId() { AssertOnManagerThread(); if (mSessionId) { return true; } ScopedCoMem sessionId; if (FAILED(mSession->GetSessionId(&sessionId)) || !sessionId) { LOG("Can't get session id or empty session ID!"); return false; } LOG("Set session Id %ls", sessionId.Get()); mSessionId = Some(sessionId.Get()); return true; } void MFCDMSession::OnSessionKeysChange() { AssertOnManagerThread(); LOG("OnSessionKeysChange"); if (!mSessionId) { LOG("Unexpected session keys change ignored"); return; } ScopedCoMem keyStatuses; UINT count = 0; RETURN_VOID_IF_FAILED(mSession->GetKeyStatuses(&keyStatuses, &count)); static auto ToMediaKeyStatus = [](MF_MEDIAKEY_STATUS aStatus) { // https://learn.microsoft.com/en-us/windows/win32/api/mfidl/ne-mfidl-mf_mediakey_status switch (aStatus) { case MF_MEDIAKEY_STATUS_USABLE: return dom::MediaKeyStatus::Usable; case MF_MEDIAKEY_STATUS_EXPIRED: return dom::MediaKeyStatus::Expired; case MF_MEDIAKEY_STATUS_OUTPUT_DOWNSCALED: return dom::MediaKeyStatus::Output_downscaled; // This is for legacy use and should not happen in normal cases. Map it to // internal error in case it happens. case MF_MEDIAKEY_STATUS_OUTPUT_NOT_ALLOWED: return dom::MediaKeyStatus::Internal_error; case MF_MEDIAKEY_STATUS_STATUS_PENDING: return dom::MediaKeyStatus::Status_pending; case MF_MEDIAKEY_STATUS_INTERNAL_ERROR: return dom::MediaKeyStatus::Internal_error; case MF_MEDIAKEY_STATUS_RELEASED: return dom::MediaKeyStatus::Released; case MF_MEDIAKEY_STATUS_OUTPUT_RESTRICTED: return dom::MediaKeyStatus::Output_restricted; } MOZ_ASSERT_UNREACHABLE("Invalid MF_MEDIAKEY_STATUS enum value"); return dom::MediaKeyStatus::Internal_error; }; CopyableTArray keyInfos; for (uint32_t idx = 0; idx < count; idx++) { const MFMediaKeyStatus& keyStatus = keyStatuses[idx]; if (keyStatus.cbKeyId != sizeof(GUID)) { LOG("Key ID with unsupported size ignored"); continue; } CopyableTArray keyId; ByteArrayFromGUID(reinterpret_cast(keyStatus.pbKeyId), keyId); nsAutoCString keyIdString(ToHexString(keyId)); LOG("Append keyid-sz=%u, keyid=%s, status=%s", keyStatus.cbKeyId, keyIdString.get(), ToMediaKeyStatusStr(ToMediaKeyStatus(keyStatus.eMediaKeyStatus))); keyInfos.AppendElement(MFCDMKeyInformation{ std::move(keyId), ToMediaKeyStatus(keyStatus.eMediaKeyStatus)}); } LOG("Notify 'keychange' for %s", NS_ConvertUTF16toUTF8(*mSessionId).get()); mKeyChangeEvent.Notify( MFCDMKeyStatusChange{*mSessionId, std::move(keyInfos)}); // ScopedCoMem only releases memory for |keyStatuses|. We // need to manually release memory for |pbKeyId| here. for (size_t idx = 0; idx < count; idx++) { if (const auto& keyStatus = keyStatuses[idx]; keyStatus.pbKeyId) { CoTaskMemFree(keyStatus.pbKeyId); } } } HRESULT MFCDMSession::UpdateExpirationIfNeeded() { AssertOnManagerThread(); MOZ_ASSERT(mSessionId); // The msdn document doesn't mention the unit for the expiration time, // follow chromium's implementation to treat them as millisecond. double newExpiredEpochTimeMs = 0.0; RETURN_IF_FAILED(mSession->GetExpiration(&newExpiredEpochTimeMs)); if (newExpiredEpochTimeMs == mExpiredTimeMilliSecondsSinceEpoch || (std::isnan(newExpiredEpochTimeMs) && std::isnan(mExpiredTimeMilliSecondsSinceEpoch))) { return S_OK; } LOG("Session expiration change from %f to %f, notify 'expiration' for %s", mExpiredTimeMilliSecondsSinceEpoch, newExpiredEpochTimeMs, NS_ConvertUTF16toUTF8(*mSessionId).get()); mExpiredTimeMilliSecondsSinceEpoch = newExpiredEpochTimeMs; mExpirationEvent.Notify( MFCDMKeyExpiration{*mSessionId, mExpiredTimeMilliSecondsSinceEpoch}); return S_OK; } void MFCDMSession::OnSessionKeyMessage( const MF_MEDIAKEYSESSION_MESSAGETYPE& aType, const nsTArray& aMessage) { AssertOnManagerThread(); // Only send key message after the session Id is ready. if (!RetrieveSessionId()) { return; } static auto ToMediaKeyMessageType = [](MF_MEDIAKEYSESSION_MESSAGETYPE aType) { switch (aType) { case MF_MEDIAKEYSESSION_MESSAGETYPE_LICENSE_REQUEST: return dom::MediaKeyMessageType::License_request; case MF_MEDIAKEYSESSION_MESSAGETYPE_LICENSE_RENEWAL: return dom::MediaKeyMessageType::License_renewal; case MF_MEDIAKEYSESSION_MESSAGETYPE_LICENSE_RELEASE: return dom::MediaKeyMessageType::License_release; case MF_MEDIAKEYSESSION_MESSAGETYPE_INDIVIDUALIZATION_REQUEST: return dom::MediaKeyMessageType::Individualization_request; default: MOZ_ASSERT_UNREACHABLE("Unknown session message type"); return dom::MediaKeyMessageType::EndGuard_; } }; LOG("Notify 'keymessage' for %s", NS_ConvertUTF16toUTF8(*mSessionId).get()); mKeyMessageEvent.Notify(MFCDMKeyMessage{ *mSessionId, ToMediaKeyMessageType(aType), std::move(aMessage)}); } #undef LOG } // namespace mozilla