From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../eme/mediadrm/MediaDrmCDMCallbackProxy.cpp | 115 ++++++ dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h | 63 +++ dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp | 453 +++++++++++++++++++++ dom/media/eme/mediadrm/MediaDrmCDMProxy.h | 186 +++++++++ dom/media/eme/mediadrm/MediaDrmProxySupport.cpp | 272 +++++++++++++ dom/media/eme/mediadrm/MediaDrmProxySupport.h | 68 ++++ dom/media/eme/mediadrm/moz.build | 19 + 7 files changed, 1176 insertions(+) create mode 100644 dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.cpp create mode 100644 dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h create mode 100644 dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp create mode 100644 dom/media/eme/mediadrm/MediaDrmCDMProxy.h create mode 100644 dom/media/eme/mediadrm/MediaDrmProxySupport.cpp create mode 100644 dom/media/eme/mediadrm/MediaDrmProxySupport.h create mode 100644 dom/media/eme/mediadrm/moz.build (limited to 'dom/media/eme/mediadrm') diff --git a/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.cpp b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.cpp new file mode 100644 index 0000000000..e5431f50fd --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.cpp @@ -0,0 +1,115 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "MediaDrmCDMCallbackProxy.h" +#include "MediaDrmCDMProxy.h" +#include "nsString.h" +#include "mozilla/dom/MediaKeys.h" +#include "mozilla/dom/MediaKeySession.h" +#include "nsContentCID.h" +#include "nsServiceManagerUtils.h" +#include "MainThreadUtils.h" +#include "mozilla/EMEUtils.h" + +namespace mozilla { + +MediaDrmCDMCallbackProxy::MediaDrmCDMCallbackProxy(MediaDrmCDMProxy* aProxy) + : mProxy(aProxy) {} + +MediaDrmCDMCallbackProxy::~MediaDrmCDMCallbackProxy() = default; + +void MediaDrmCDMCallbackProxy::SetSessionId(uint32_t aToken, + const nsCString& aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + mProxy->OnSetSessionId(aToken, NS_ConvertUTF8toUTF16(aSessionId)); +} + +void MediaDrmCDMCallbackProxy::ResolveLoadSessionPromise(uint32_t aPromiseId, + bool aSuccess) { + MOZ_ASSERT(NS_IsMainThread()); + mProxy->OnResolveLoadSessionPromise(aPromiseId, aSuccess); +} + +void MediaDrmCDMCallbackProxy::ResolvePromise(uint32_t aPromiseId) { + // Note: CDMProxy proxies this from non-main threads to main thread. + mProxy->ResolvePromise(aPromiseId); +} + +void MediaDrmCDMCallbackProxy::RejectPromise(uint32_t aPromiseId, + ErrorResult&& aException, + const nsCString& aMessage) { + MOZ_ASSERT(NS_IsMainThread()); + mProxy->OnRejectPromise(aPromiseId, std::move(aException), aMessage); +} + +void MediaDrmCDMCallbackProxy::SessionMessage( + const nsCString& aSessionId, dom::MediaKeyMessageType aMessageType, + const nsTArray& aMessage) { + MOZ_ASSERT(NS_IsMainThread()); + // For removing constness + nsTArray message(aMessage.Clone()); + mProxy->OnSessionMessage(NS_ConvertUTF8toUTF16(aSessionId), aMessageType, + message); +} + +void MediaDrmCDMCallbackProxy::ExpirationChange(const nsCString& aSessionId, + UnixTime aExpiryTime) { + MOZ_ASSERT(NS_IsMainThread()); + mProxy->OnExpirationChange(NS_ConvertUTF8toUTF16(aSessionId), aExpiryTime); +} + +void MediaDrmCDMCallbackProxy::SessionClosed(const nsCString& aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + bool keyStatusesChange = false; + { + auto caps = mProxy->Capabilites().Lock(); + keyStatusesChange = + caps->RemoveKeysForSession(NS_ConvertUTF8toUTF16(aSessionId)); + } + if (keyStatusesChange) { + mProxy->OnKeyStatusesChange(NS_ConvertUTF8toUTF16(aSessionId)); + } + mProxy->OnSessionClosed(NS_ConvertUTF8toUTF16(aSessionId)); +} + +void MediaDrmCDMCallbackProxy::SessionError(const nsCString& aSessionId, + nsresult aException, + uint32_t aSystemCode, + const nsCString& aMessage) { + MOZ_ASSERT(NS_IsMainThread()); + mProxy->OnSessionError(NS_ConvertUTF8toUTF16(aSessionId), aException, + aSystemCode, NS_ConvertUTF8toUTF16(aMessage)); +} + +void MediaDrmCDMCallbackProxy::BatchedKeyStatusChanged( + const nsCString& aSessionId, const nsTArray& aKeyInfos) { + MOZ_ASSERT(NS_IsMainThread()); + BatchedKeyStatusChangedInternal(aSessionId, aKeyInfos); +} + +void MediaDrmCDMCallbackProxy::BatchedKeyStatusChangedInternal( + const nsCString& aSessionId, const nsTArray& aKeyInfos) { + bool keyStatusesChange = false; + { + auto caps = mProxy->Capabilites().Lock(); + for (size_t i = 0; i < aKeyInfos.Length(); i++) { + keyStatusesChange |= caps->SetKeyStatus(aKeyInfos[i].mKeyId, + NS_ConvertUTF8toUTF16(aSessionId), + aKeyInfos[i].mStatus); + } + } + if (keyStatusesChange) { + mProxy->OnKeyStatusesChange(NS_ConvertUTF8toUTF16(aSessionId)); + } +} + +void MediaDrmCDMCallbackProxy::Decrypted( + uint32_t aId, DecryptStatus aResult, + const nsTArray& aDecryptedData) { + MOZ_ASSERT_UNREACHABLE("Fennec could not handle decrypted event"); +} + +} // namespace mozilla diff --git a/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h new file mode 100644 index 0000000000..e963e21e11 --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef MediaDrmCDMCallbackProxy_h_ +#define MediaDrmCDMCallbackProxy_h_ + +#include "mozilla/CDMProxy.h" +#include "mozilla/DecryptorProxyCallback.h" + +namespace mozilla { +class CDMProxy; +class ErrorResult; +class MediaDrmCDMProxy; + +// Proxies call backs from the MediaDrmProxy -> MediaDrmProxySupport back to the +// MediaKeys object on the main thread. We used annotation calledFrom = "gecko" +// to ensure running on main thread. +class MediaDrmCDMCallbackProxy final : public DecryptorProxyCallback { + public: + void SetSessionId(uint32_t aCreateSessionToken, + const nsCString& aSessionId) override; + + void ResolveLoadSessionPromise(uint32_t aPromiseId, bool aSuccess) override; + + void ResolvePromise(uint32_t aPromiseId) override; + + void RejectPromise(uint32_t aPromiseId, ErrorResult&& aException, + const nsCString& aSessionId) override; + + void SessionMessage(const nsCString& aSessionId, + dom::MediaKeyMessageType aMessageType, + const nsTArray& aMessage) override; + + void ExpirationChange(const nsCString& aSessionId, + UnixTime aExpiryTime) override; + + void SessionClosed(const nsCString& aSessionId) override; + + void SessionError(const nsCString& aSessionId, nsresult aException, + uint32_t aSystemCode, const nsCString& aMessage) override; + + void Decrypted(uint32_t aId, DecryptStatus aResult, + const nsTArray& aDecryptedData) override; + + void BatchedKeyStatusChanged(const nsCString& aSessionId, + const nsTArray& aKeyInfos) override; + + ~MediaDrmCDMCallbackProxy(); + + private: + friend class MediaDrmCDMProxy; + explicit MediaDrmCDMCallbackProxy(MediaDrmCDMProxy* aProxy); + + void BatchedKeyStatusChangedInternal(const nsCString& aSessionId, + const nsTArray& aKeyInfos); + const RefPtr mProxy; +}; + +} // namespace mozilla +#endif diff --git a/dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp b/dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp new file mode 100644 index 0000000000..07b7bfe3a0 --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp @@ -0,0 +1,453 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "mozilla/dom/MediaKeySession.h" +#include "mozilla/MediaDrmCDMProxy.h" +#include "MediaDrmCDMCallbackProxy.h" + +namespace mozilla { + +MediaDrmSessionType ToMediaDrmSessionType( + dom::MediaKeySessionType aSessionType) { + switch (aSessionType) { + case dom::MediaKeySessionType::Temporary: + return kKeyStreaming; + case dom::MediaKeySessionType::Persistent_license: + return kKeyOffline; + default: + return kKeyStreaming; + }; +} + +MediaDrmCDMProxy::MediaDrmCDMProxy(dom::MediaKeys* aKeys, + const nsAString& aKeySystem, + bool aDistinctiveIdentifierRequired, + bool aPersistentStateRequired) + : CDMProxy(aKeys, aKeySystem, aDistinctiveIdentifierRequired, + aPersistentStateRequired), + mCDM(nullptr), + mShutdownCalled(false) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_COUNT_CTOR(MediaDrmCDMProxy); +} + +MediaDrmCDMProxy::~MediaDrmCDMProxy() { MOZ_COUNT_DTOR(MediaDrmCDMProxy); } + +void MediaDrmCDMProxy::Init(PromiseId aPromiseId, const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, + const nsAString& aName) { + MOZ_ASSERT(NS_IsMainThread()); + NS_ENSURE_TRUE_VOID(!mKeys.IsNull()); + + EME_LOG("MediaDrmCDMProxy::Init (%s, %s) %s", + NS_ConvertUTF16toUTF8(aOrigin).get(), + NS_ConvertUTF16toUTF8(aTopLevelOrigin).get(), + NS_ConvertUTF16toUTF8(aName).get()); + + // Create a thread to work with cdm. + if (!mOwnerThread) { + nsresult rv = + NS_NewNamedThread("MDCDMThread", getter_AddRefs(mOwnerThread)); + if (NS_FAILED(rv)) { + RejectPromiseWithStateError( + aPromiseId, nsLiteralCString( + "Couldn't create CDM thread MediaDrmCDMProxy::Init")); + return; + } + } + + mCDM = mozilla::MakeUnique(mKeySystem); + nsCOMPtr task( + NewRunnableMethod("MediaDrmCDMProxy::md_Init", this, + &MediaDrmCDMProxy::md_Init, aPromiseId)); + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); +} + +void MediaDrmCDMProxy::CreateSession(uint32_t aCreateSessionToken, + MediaKeySessionType aSessionType, + PromiseId aPromiseId, + const nsAString& aInitDataType, + nsTArray& aInitData) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mOwnerThread); + + UniquePtr data(new CreateSessionData()); + data->mSessionType = aSessionType; + data->mCreateSessionToken = aCreateSessionToken; + data->mPromiseId = aPromiseId; + data->mInitDataType = NS_ConvertUTF16toUTF8(aInitDataType); + data->mInitData = std::move(aInitData); + + nsCOMPtr task(NewRunnableMethod&&>( + "MediaDrmCDMProxy::md_CreateSession", this, + &MediaDrmCDMProxy::md_CreateSession, std::move(data))); + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); +} + +void MediaDrmCDMProxy::LoadSession(PromiseId aPromiseId, + dom::MediaKeySessionType aSessionType, + const nsAString& aSessionId) { + // TODO: Implement LoadSession. + RejectPromiseWithStateError( + aPromiseId, "Currently Fennec does not support LoadSession"_ns); +} + +void MediaDrmCDMProxy::SetServerCertificate(PromiseId aPromiseId, + nsTArray& aCert) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mOwnerThread); + + mOwnerThread->Dispatch(NewRunnableMethod>( + "MediaDrmCDMProxy::md_SetServerCertificate", this, + &MediaDrmCDMProxy::md_SetServerCertificate, + aPromiseId, std::move(aCert)), + NS_DISPATCH_NORMAL); +} + +void MediaDrmCDMProxy::UpdateSession(const nsAString& aSessionId, + PromiseId aPromiseId, + nsTArray& aResponse) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mOwnerThread); + NS_ENSURE_TRUE_VOID(!mKeys.IsNull()); + + UniquePtr data(new UpdateSessionData()); + data->mPromiseId = aPromiseId; + data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId); + data->mResponse = std::move(aResponse); + + nsCOMPtr task(NewRunnableMethod&&>( + "MediaDrmCDMProxy::md_UpdateSession", this, + &MediaDrmCDMProxy::md_UpdateSession, std::move(data))); + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); +} + +void MediaDrmCDMProxy::CloseSession(const nsAString& aSessionId, + PromiseId aPromiseId) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mOwnerThread); + NS_ENSURE_TRUE_VOID(!mKeys.IsNull()); + + UniquePtr data(new SessionOpData()); + data->mPromiseId = aPromiseId; + data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId); + + nsCOMPtr task(NewRunnableMethod&&>( + "MediaDrmCDMProxy::md_CloseSession", this, + &MediaDrmCDMProxy::md_CloseSession, std::move(data))); + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); +} + +void MediaDrmCDMProxy::RemoveSession(const nsAString& aSessionId, + PromiseId aPromiseId) { + // TODO: Implement RemoveSession. + RejectPromiseWithStateError( + aPromiseId, "Currently Fennec does not support RemoveSession"_ns); +} + +void MediaDrmCDMProxy::QueryOutputProtectionStatus() { + // TODO(bryce): determine if this is needed for Android and implement as + // needed. See also `NotifyOutputProtectionStatus`. +} + +void MediaDrmCDMProxy::NotifyOutputProtectionStatus( + OutputProtectionCheckStatus aCheckStatus, + OutputProtectionCaptureStatus aCaptureStatus) { + // TODO(bryce): determine if this is needed for Android and implement as + // needed. See also `QueryOutputProtectionStatus`. +} + +void MediaDrmCDMProxy::Shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mOwnerThread); + nsCOMPtr task(NewRunnableMethod( + "MediaDrmCDMProxy::md_Shutdown", this, &MediaDrmCDMProxy::md_Shutdown)); + + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); + mOwnerThread->Shutdown(); + mOwnerThread = nullptr; + mKeys.Clear(); +} + +void MediaDrmCDMProxy::Terminated() { + // TODO: Implement Terminated. + // Should find a way to handle the case when remote side MediaDrm crashed. +} + +void MediaDrmCDMProxy::OnSetSessionId(uint32_t aCreateSessionToken, + const nsAString& aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + + RefPtr session( + mKeys->GetPendingSession(aCreateSessionToken)); + if (session) { + session->SetSessionId(aSessionId); + } +} + +void MediaDrmCDMProxy::OnResolveLoadSessionPromise(uint32_t aPromiseId, + bool aSuccess) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + mKeys->OnSessionLoaded(aPromiseId, aSuccess); +} + +void MediaDrmCDMProxy::OnSessionMessage(const nsAString& aSessionId, + dom::MediaKeyMessageType aMessageType, + const nsTArray& aMessage) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr session(mKeys->GetSession(aSessionId)); + if (session) { + session->DispatchKeyMessage(aMessageType, aMessage); + } +} + +void MediaDrmCDMProxy::OnExpirationChange(const nsAString& aSessionId, + UnixTime aExpiryTime) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr session(mKeys->GetSession(aSessionId)); + if (session) { + session->SetExpiration(static_cast(aExpiryTime)); + } +} + +void MediaDrmCDMProxy::OnSessionClosed(const nsAString& aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr session(mKeys->GetSession(aSessionId)); + if (session) { + session->OnClosed(); + } +} + +void MediaDrmCDMProxy::OnSessionError(const nsAString& aSessionId, + nsresult aException, uint32_t aSystemCode, + const nsAString& aMsg) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr session(mKeys->GetSession(aSessionId)); + if (session) { + session->DispatchKeyError(aSystemCode); + } +} + +void MediaDrmCDMProxy::OnRejectPromise(uint32_t aPromiseId, + ErrorResult&& aException, + const nsCString& aMsg) { + MOZ_ASSERT(NS_IsMainThread()); + RejectPromise(aPromiseId, std::move(aException), aMsg); +} + +RefPtr MediaDrmCDMProxy::Decrypt(MediaRawData* aSample) { + MOZ_ASSERT_UNREACHABLE("Fennec could not handle decrypting individually"); + return nullptr; +} + +void MediaDrmCDMProxy::OnDecrypted(uint32_t aId, DecryptStatus aResult, + const nsTArray& aDecryptedData) { + MOZ_ASSERT_UNREACHABLE("Fennec could not handle decrypted event"); +} + +void MediaDrmCDMProxy::RejectPromise(PromiseId aId, ErrorResult&& aException, + const nsCString& aReason) { + if (NS_IsMainThread()) { + if (!mKeys.IsNull()) { + mKeys->RejectPromise(aId, std::move(aException), aReason); + } + } else { + nsCOMPtr task( + new RejectPromiseTask(this, aId, std::move(aException), aReason)); + mMainThread->Dispatch(task.forget(), NS_DISPATCH_NORMAL); + } +} + +void MediaDrmCDMProxy::RejectPromiseWithStateError(PromiseId aId, + const nsCString& aReason) { + ErrorResult rv; + rv.ThrowInvalidStateError(aReason); + RejectPromise(aId, std::move(rv), aReason); +} + +void MediaDrmCDMProxy::ResolvePromise(PromiseId aId) { + if (NS_IsMainThread()) { + if (!mKeys.IsNull()) { + mKeys->ResolvePromise(aId); + } else { + NS_WARNING("MediaDrmCDMProxy unable to resolve promise!"); + } + } else { + nsCOMPtr task; + task = + NewRunnableMethod("MediaDrmCDMProxy::ResolvePromise", this, + &MediaDrmCDMProxy::ResolvePromise, aId); + mMainThread->Dispatch(task.forget(), NS_DISPATCH_NORMAL); + } +} + +template +void MediaDrmCDMProxy::ResolvePromiseWithResult(PromiseId aId, + const T& aResult) { + if (NS_IsMainThread()) { + if (!mKeys.IsNull()) { + mKeys->ResolvePromiseWithResult(aId, aResult); + } else { + NS_WARNING("MediaDrmCDMProxy unable to resolve promise!"); + } + return; + } + + nsCOMPtr task; + task = NewRunnableMethod( + "MediaDrmCDMProxy::ResolvePromiseWithResult", this, + &MediaDrmCDMProxy::ResolvePromiseWithResult, aId, aResult); + mMainThread->Dispatch(task.forget(), NS_DISPATCH_NORMAL); +} + +void MediaDrmCDMProxy::OnKeyStatusesChange(const nsAString& aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr session(mKeys->GetSession(aSessionId)); + if (session) { + session->DispatchKeyStatusesChange(); + } +} + +void MediaDrmCDMProxy::GetStatusForPolicy(PromiseId aPromiseId, + const nsAString& aMinHdcpVersion) { + // TODO: Implement GetStatusForPolicy. + constexpr auto err = + "Currently Fennec does not support GetStatusForPolicy"_ns; + + ErrorResult rv; + rv.ThrowNotSupportedError(err); + RejectPromise(aPromiseId, std::move(rv), err); +} + +#ifdef DEBUG +bool MediaDrmCDMProxy::IsOnOwnerThread() { + return NS_GetCurrentThread() == mOwnerThread; +} +#endif + +const nsString& MediaDrmCDMProxy::GetMediaDrmStubId() const { + MOZ_ASSERT(mCDM); + return mCDM->GetMediaDrmStubId(); +} + +void MediaDrmCDMProxy::OnCDMCreated(uint32_t aPromiseId) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + + if (mCDM) { + mKeys->OnCDMCreated(aPromiseId, 0); + return; + } + + // No CDM? Just reject the promise. + constexpr auto err = "Null CDM in OnCDMCreated()"_ns; + ErrorResult rv; + rv.ThrowInvalidStateError(err); + mKeys->RejectPromise(aPromiseId, std::move(rv), err); +} + +void MediaDrmCDMProxy::md_Init(uint32_t aPromiseId) { + MOZ_ASSERT(IsOnOwnerThread()); + MOZ_ASSERT(mCDM); + + UniquePtr callback( + new MediaDrmCDMCallbackProxy(this)); + mCDM->Init(std::move(callback)); + nsCOMPtr task( + NewRunnableMethod("MediaDrmCDMProxy::OnCDMCreated", this, + &MediaDrmCDMProxy::OnCDMCreated, aPromiseId)); + mMainThread->Dispatch(task.forget(), NS_DISPATCH_NORMAL); +} + +void MediaDrmCDMProxy::md_CreateSession(UniquePtr&& aData) { + MOZ_ASSERT(IsOnOwnerThread()); + + if (!mCDM) { + RejectPromiseWithStateError(aData->mPromiseId, + "Null CDM in md_CreateSession"_ns); + return; + } + + mCDM->CreateSession(aData->mCreateSessionToken, aData->mPromiseId, + aData->mInitDataType, aData->mInitData, + ToMediaDrmSessionType(aData->mSessionType)); +} + +void MediaDrmCDMProxy::md_SetServerCertificate(PromiseId aPromiseId, + const nsTArray& aCert) { + MOZ_ASSERT(IsOnOwnerThread()); + + if (!mCDM) { + RejectPromiseWithStateError(aPromiseId, + "Null CDM in md_SetServerCertificate"_ns); + return; + } + + if (mCDM->SetServerCertificate(aCert)) { + ResolvePromiseWithResult(aPromiseId, true); + } else { + RejectPromiseWithStateError( + aPromiseId, "MediaDrmCDMProxy unable to set server certificate"_ns); + } +} + +void MediaDrmCDMProxy::md_UpdateSession(UniquePtr&& aData) { + MOZ_ASSERT(IsOnOwnerThread()); + + if (!mCDM) { + RejectPromiseWithStateError(aData->mPromiseId, + "Null CDM in md_UpdateSession"_ns); + return; + } + mCDM->UpdateSession(aData->mPromiseId, aData->mSessionId, aData->mResponse); +} + +void MediaDrmCDMProxy::md_CloseSession(UniquePtr&& aData) { + MOZ_ASSERT(IsOnOwnerThread()); + + if (!mCDM) { + RejectPromiseWithStateError(aData->mPromiseId, + "Null CDM in md_CloseSession"_ns); + return; + } + mCDM->CloseSession(aData->mPromiseId, aData->mSessionId); +} + +void MediaDrmCDMProxy::md_Shutdown() { + MOZ_ASSERT(IsOnOwnerThread()); + MOZ_ASSERT(mCDM); + if (mShutdownCalled) { + return; + } + mShutdownCalled = true; + mCDM->Shutdown(); + mCDM = nullptr; +} + +} // namespace mozilla diff --git a/dom/media/eme/mediadrm/MediaDrmCDMProxy.h b/dom/media/eme/mediadrm/MediaDrmCDMProxy.h new file mode 100644 index 0000000000..60a541ece9 --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmCDMProxy.h @@ -0,0 +1,186 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef MediaDrmCDMProxy_h_ +#define MediaDrmCDMProxy_h_ + +#include +#include "mozilla/jni/Types.h" +#include "mozilla/CDMProxy.h" +#include "mozilla/CDMCaps.h" +#include "mozilla/dom/MediaKeys.h" +#include "mozilla/dom/MediaKeySession.h" +#include "mozilla/MediaDrmProxySupport.h" +#include "mozilla/UniquePtr.h" + +#include "MediaCodec.h" +#include "nsString.h" + +namespace mozilla { + +class MediaDrmCDMCallbackProxy; +class MediaDrmCDMProxy final : public CDMProxy { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaDrmCDMProxy, override) + + MediaDrmCDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem, + bool aDistinctiveIdentifierRequired, + bool aPersistentStateRequired); + + void Init(PromiseId aPromiseId, const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, + const nsAString& aGMPName) override; + + void CreateSession(uint32_t aCreateSessionToken, + MediaKeySessionType aSessionType, PromiseId aPromiseId, + const nsAString& aInitDataType, + nsTArray& aInitData) override; + + void LoadSession(PromiseId aPromiseId, dom::MediaKeySessionType aSessionType, + const nsAString& aSessionId) override; + + void SetServerCertificate(PromiseId aPromiseId, + nsTArray& aCert) override; + + void UpdateSession(const nsAString& aSessionId, PromiseId aPromiseId, + nsTArray& aResponse) override; + + void CloseSession(const nsAString& aSessionId, PromiseId aPromiseId) override; + + void RemoveSession(const nsAString& aSessionId, + PromiseId aPromiseId) override; + + void QueryOutputProtectionStatus() override; + + void NotifyOutputProtectionStatus( + OutputProtectionCheckStatus aCheckStatus, + OutputProtectionCaptureStatus aCaptureStatus) override; + + void Shutdown() override; + + void Terminated() override; + + void OnSetSessionId(uint32_t aCreateSessionToken, + const nsAString& aSessionId) override; + + void OnResolveLoadSessionPromise(uint32_t aPromiseId, bool aSuccess) override; + + void OnSessionMessage(const nsAString& aSessionId, + dom::MediaKeyMessageType aMessageType, + const nsTArray& aMessage) override; + + void OnExpirationChange(const nsAString& aSessionId, + UnixTime aExpiryTime) override; + + void OnSessionClosed(const nsAString& aSessionId) override; + + void OnSessionError(const nsAString& aSessionId, nsresult aException, + uint32_t aSystemCode, const nsAString& aMsg) override; + + void OnRejectPromise(uint32_t aPromiseId, ErrorResult&& aException, + const nsCString& aMsg) override; + + RefPtr Decrypt(MediaRawData* aSample) override; + void OnDecrypted(uint32_t aId, DecryptStatus aResult, + const nsTArray& aDecryptedData) override; + + void RejectPromise(PromiseId aId, ErrorResult&& aException, + const nsCString& aReason) override; + // Reject promise with an InvalidStateError and the given message. + void RejectPromiseWithStateError(PromiseId aId, const nsCString& aReason); + + // Resolves promise with "undefined". + // Can be called from any thread. + void ResolvePromise(PromiseId aId) override; + + void OnKeyStatusesChange(const nsAString& aSessionId) override; + + void GetStatusForPolicy(PromiseId aPromiseId, + const nsAString& aMinHdcpVersion) override; + +#ifdef DEBUG + bool IsOnOwnerThread() override; +#endif + + const nsString& GetMediaDrmStubId() const; + + private: + virtual ~MediaDrmCDMProxy(); + + void OnCDMCreated(uint32_t aPromiseId); + + template + void ResolvePromiseWithResult(PromiseId aId, const T& aResult); + + struct CreateSessionData { + MediaKeySessionType mSessionType; + uint32_t mCreateSessionToken; + PromiseId mPromiseId; + nsCString mInitDataType; + nsTArray mInitData; + }; + + struct UpdateSessionData { + PromiseId mPromiseId; + nsCString mSessionId; + nsTArray mResponse; + }; + + struct SessionOpData { + PromiseId mPromiseId; + nsCString mSessionId; + }; + + class RejectPromiseTask : public Runnable { + public: + RejectPromiseTask(MediaDrmCDMProxy* aProxy, PromiseId aId, + ErrorResult&& aException, const nsCString& aReason) + : Runnable("RejectPromiseTask"), + mProxy(aProxy), + mId(aId), + mException(std::move(aException)), + mReason(aReason) {} + NS_IMETHOD Run() override { + // Moving into or out of a non-copyable ErrorResult will assert that both + // ErorResults are from our current thread. Avoid the assertion by moving + // into a current-thread CopyableErrorResult first. Note that this is + // safe, because CopyableErrorResult never holds state that can't move + // across threads. + CopyableErrorResult rv(std::move(mException)); + mProxy->RejectPromise(mId, std::move(rv), mReason); + return NS_OK; + } + + private: + RefPtr mProxy; + PromiseId mId; + // We use a CopyableErrorResult here, because we're going to dispatch to a + // different thread and normal ErrorResult doesn't support that. + // CopyableErrorResult ensures that it only stores values that are safe to + // move across threads. + CopyableErrorResult mException; + nsCString mReason; + }; + + nsCString mNodeId; + UniquePtr mCDM; + bool mShutdownCalled; + + // ===================================================================== + // For MediaDrmProxySupport + void md_Init(uint32_t aPromiseId); + void md_CreateSession(UniquePtr&& aData); + void md_SetServerCertificate(PromiseId aPromiseId, + const nsTArray& aCert); + void md_UpdateSession(UniquePtr&& aData); + void md_CloseSession(UniquePtr&& aData); + void md_Shutdown(); + // ===================================================================== +}; + +} // namespace mozilla + +#endif // MediaDrmCDMProxy_h_ diff --git a/dom/media/eme/mediadrm/MediaDrmProxySupport.cpp b/dom/media/eme/mediadrm/MediaDrmProxySupport.cpp new file mode 100644 index 0000000000..717a57df35 --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmProxySupport.cpp @@ -0,0 +1,272 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "MediaDrmProxySupport.h" +#include "MediaDrmCDMCallbackProxy.h" +#include "mozilla/EMEUtils.h" +#include "mozilla/java/MediaDrmProxyNatives.h" +#include "mozilla/java/SessionKeyInfoWrappers.h" +#include "MediaCodec.h" // For MediaDrm::KeyStatus + +namespace mozilla { + +LogModule* GetMDRMNLog() { + static LazyLogModule log("MediaDrmProxySupport"); + return log; +} + +class MediaDrmJavaCallbacksSupport + : public java::MediaDrmProxy::NativeMediaDrmProxyCallbacks::Natives< + MediaDrmJavaCallbacksSupport> { + public: + typedef java::MediaDrmProxy::NativeMediaDrmProxyCallbacks::Natives< + MediaDrmJavaCallbacksSupport> + MediaDrmProxyNativeCallbacks; + using MediaDrmProxyNativeCallbacks::AttachNative; + using MediaDrmProxyNativeCallbacks::DisposeNative; + + explicit MediaDrmJavaCallbacksSupport( + UniquePtr&& aDecryptorProxyCallback) + : mDecryptorProxyCallback(std::move(aDecryptorProxyCallback)) { + MOZ_ASSERT(mDecryptorProxyCallback); + } + /* + * Native implementation, called by Java. + */ + void OnSessionCreated(int aCreateSessionToken, int aPromiseId, + jni::ByteArray::Param aSessionId, + jni::ByteArray::Param aRequest); + + void OnSessionUpdated(int aPromiseId, jni::ByteArray::Param aSessionId); + + void OnSessionClosed(int aPromiseId, jni::ByteArray::Param aSessionId); + + void OnSessionMessage( + jni::ByteArray::Param aSessionId, + int /*mozilla::dom::MediaKeyMessageType*/ aSessionMessageType, + jni::ByteArray::Param aRequest); + + void OnSessionError(jni::ByteArray::Param aSessionId, + jni::String::Param aMessage); + + void OnSessionBatchedKeyChanged(jni::ByteArray::Param, + jni::ObjectArray::Param); + + void OnRejectPromise(int aPromiseId, jni::String::Param aMessage); + + private: + UniquePtr mDecryptorProxyCallback; +}; // MediaDrmJavaCallbacksSupport + +void MediaDrmJavaCallbacksSupport::OnSessionCreated( + int aCreateSessionToken, int aPromiseId, jni::ByteArray::Param aSessionId, + jni::ByteArray::Param aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + auto reqDataArray = aRequest->GetElements(); + nsCString sessionId( + reinterpret_cast(aSessionId->GetElements().Elements()), + aSessionId->Length()); + MDRMN_LOG("SessionId(%s) closed", sessionId.get()); + + mDecryptorProxyCallback->SetSessionId(aCreateSessionToken, sessionId); + mDecryptorProxyCallback->ResolvePromise(aPromiseId); +} + +void MediaDrmJavaCallbacksSupport::OnSessionUpdated( + int aPromiseId, jni::ByteArray::Param aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + MDRMN_LOG( + "SessionId(%s) closed", + nsCString(reinterpret_cast(aSessionId->GetElements().Elements()), + aSessionId->Length()) + .get()); + mDecryptorProxyCallback->ResolvePromise(aPromiseId); +} + +void MediaDrmJavaCallbacksSupport::OnSessionClosed( + int aPromiseId, jni::ByteArray::Param aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + nsCString sessionId( + reinterpret_cast(aSessionId->GetElements().Elements()), + aSessionId->Length()); + MDRMN_LOG("SessionId(%s) closed", sessionId.get()); + mDecryptorProxyCallback->ResolvePromise(aPromiseId); + mDecryptorProxyCallback->SessionClosed(sessionId); +} + +void MediaDrmJavaCallbacksSupport::OnSessionMessage( + jni::ByteArray::Param aSessionId, + int /*mozilla::dom::MediaKeyMessageType*/ aMessageType, + jni::ByteArray::Param aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + nsCString sessionId( + reinterpret_cast(aSessionId->GetElements().Elements()), + aSessionId->Length()); + auto reqDataArray = aRequest->GetElements(); + + nsTArray retRequest; + retRequest.AppendElements(reinterpret_cast(reqDataArray.Elements()), + reqDataArray.Length()); + + mDecryptorProxyCallback->SessionMessage( + sessionId, static_cast(aMessageType), + retRequest); +} + +void MediaDrmJavaCallbacksSupport::OnSessionError( + jni::ByteArray::Param aSessionId, jni::String::Param aMessage) { + MOZ_ASSERT(NS_IsMainThread()); + nsCString sessionId( + reinterpret_cast(aSessionId->GetElements().Elements()), + aSessionId->Length()); + nsCString errorMessage = aMessage->ToCString(); + MDRMN_LOG("SessionId(%s)", sessionId.get()); + // TODO: We cannot get system error code from media drm API. + // Currently use -1 as an error code. + mDecryptorProxyCallback->SessionError( + sessionId, NS_ERROR_DOM_INVALID_STATE_ERR, -1, errorMessage); +} + +// TODO: MediaDrm.KeyStatus defined the status code not included +// dom::MediaKeyStatus::Released and dom::MediaKeyStatus::Output_downscaled. +// Should keep tracking for this if it will be changed in the future. +static dom::MediaKeyStatus MediaDrmKeyStatusToMediaKeyStatus(int aStatusCode) { + using mozilla::java::sdk::MediaDrm; + switch (aStatusCode) { + case MediaDrm::KeyStatus::STATUS_USABLE: + return dom::MediaKeyStatus::Usable; + case MediaDrm::KeyStatus::STATUS_EXPIRED: + return dom::MediaKeyStatus::Expired; + case MediaDrm::KeyStatus::STATUS_OUTPUT_NOT_ALLOWED: + return dom::MediaKeyStatus::Output_restricted; + case MediaDrm::KeyStatus::STATUS_INTERNAL_ERROR: + return dom::MediaKeyStatus::Internal_error; + case MediaDrm::KeyStatus::STATUS_PENDING: + return dom::MediaKeyStatus::Status_pending; + default: + return dom::MediaKeyStatus::Internal_error; + } +} + +void MediaDrmJavaCallbacksSupport::OnSessionBatchedKeyChanged( + jni::ByteArray::Param aSessionId, jni::ObjectArray::Param aKeyInfos) { + MOZ_ASSERT(NS_IsMainThread()); + nsCString sessionId( + reinterpret_cast(aSessionId->GetElements().Elements()), + aSessionId->Length()); + nsTArray keyInfosObjectArray(aKeyInfos->GetElements()); + + nsTArray keyInfosArray; + + for (auto&& keyInfoObject : keyInfosObjectArray) { + java::SessionKeyInfo::LocalRef keyInfo(std::move(keyInfoObject)); + mozilla::jni::ByteArray::LocalRef keyIdByteArray = keyInfo->KeyId(); + nsTArray keyIdInt8Array = keyIdByteArray->GetElements(); + // Cast nsTArray to nsTArray + nsTArray* keyId = + reinterpret_cast*>(&keyIdInt8Array); + auto keyStatus = keyInfo->Status(); // int32_t + keyInfosArray.AppendElement( + CDMKeyInfo(*keyId, dom::Optional( + MediaDrmKeyStatusToMediaKeyStatus(keyStatus)))); + } + + mDecryptorProxyCallback->BatchedKeyStatusChanged(sessionId, keyInfosArray); +} + +void MediaDrmJavaCallbacksSupport::OnRejectPromise( + int aPromiseId, jni::String::Param aMessage) { + MOZ_ASSERT(NS_IsMainThread()); + nsCString reason = aMessage->ToCString(); + MDRMN_LOG("OnRejectPromise aMessage(%s) ", reason.get()); + // Current implementation assume all the reject from MediaDrm is due to + // invalid state. Other cases should be handled before calling into + // MediaDrmProxy API. + ErrorResult rv; + rv.ThrowInvalidStateError(reason); + mDecryptorProxyCallback->RejectPromise(aPromiseId, std::move(rv), reason); +} + +MediaDrmProxySupport::MediaDrmProxySupport(const nsAString& aKeySystem) + : mKeySystem(aKeySystem), mDestroyed(false) { + mJavaCallbacks = java::MediaDrmProxy::NativeMediaDrmProxyCallbacks::New(); + + mBridgeProxy = java::MediaDrmProxy::Create(mKeySystem, mJavaCallbacks); + + MOZ_ASSERT(mBridgeProxy, "mBridgeProxy should not be null"); + mMediaDrmStubId = mBridgeProxy->GetStubId()->ToString(); +} + +MediaDrmProxySupport::~MediaDrmProxySupport() { + MOZ_ASSERT(mDestroyed, "Shutdown() should be called before !!"); + MediaDrmJavaCallbacksSupport::DisposeNative(mJavaCallbacks); +} + +nsresult MediaDrmProxySupport::Init( + UniquePtr&& aCallback) { + MOZ_ASSERT(mJavaCallbacks); + + MediaDrmJavaCallbacksSupport::AttachNative( + mJavaCallbacks, + mozilla::MakeUnique(std::move(aCallback))); + return mBridgeProxy != nullptr ? NS_OK : NS_ERROR_FAILURE; +} + +void MediaDrmProxySupport::CreateSession(uint32_t aCreateSessionToken, + uint32_t aPromiseId, + const nsCString& aInitDataType, + const nsTArray& aInitData, + MediaDrmSessionType aSessionType) { + MOZ_ASSERT(mBridgeProxy); + + auto initDataBytes = mozilla::jni::ByteArray::New( + reinterpret_cast(&aInitData[0]), aInitData.Length()); + // TODO: aSessionType is not used here. + // Refer to + // http://androidxref.com/5.1.1_r6/xref/external/chromium_org/media/base/android/java/src/org/chromium/media/MediaDrmBridge.java#420 + // it is hard code to streaming type. + mBridgeProxy->CreateSession(aCreateSessionToken, aPromiseId, + NS_ConvertUTF8toUTF16(aInitDataType), + initDataBytes); +} + +void MediaDrmProxySupport::UpdateSession(uint32_t aPromiseId, + const nsCString& aSessionId, + const nsTArray& aResponse) { + MOZ_ASSERT(mBridgeProxy); + + auto response = mozilla::jni::ByteArray::New( + reinterpret_cast(aResponse.Elements()), + aResponse.Length()); + mBridgeProxy->UpdateSession(aPromiseId, NS_ConvertUTF8toUTF16(aSessionId), + response); +} + +void MediaDrmProxySupport::CloseSession(uint32_t aPromiseId, + const nsCString& aSessionId) { + MOZ_ASSERT(mBridgeProxy); + + mBridgeProxy->CloseSession(aPromiseId, NS_ConvertUTF8toUTF16(aSessionId)); +} + +void MediaDrmProxySupport::Shutdown() { + MOZ_ASSERT(mBridgeProxy); + + if (mDestroyed) { + return; + } + mBridgeProxy->Destroy(); + mDestroyed = true; +} + +bool MediaDrmProxySupport::SetServerCertificate( + const nsTArray& aCert) { + jni::ByteArray::LocalRef cert = jni::ByteArray::New( + reinterpret_cast(aCert.Elements()), aCert.Length()); + return mBridgeProxy->SetServerCertificate(cert); +} + +} // namespace mozilla diff --git a/dom/media/eme/mediadrm/MediaDrmProxySupport.h b/dom/media/eme/mediadrm/MediaDrmProxySupport.h new file mode 100644 index 0000000000..e994036f69 --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmProxySupport.h @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef MediaDrmProxySupport_H +#define MediaDrmProxySupport_H + +#include "mozilla/DecryptorProxyCallback.h" +#include "mozilla/java/MediaDrmProxyWrappers.h" +#include "mozilla/Logging.h" +#include "mozilla/UniquePtr.h" +#include "nsString.h" + +namespace mozilla { + +enum MediaDrmSessionType { + kKeyStreaming = 1, + kKeyOffline = 2, + kKeyRelease = 3, +}; + +#ifndef MDRMN_LOG +LogModule* GetMDRMNLog(); +# define MDRMN_LOG(x, ...) \ + MOZ_LOG(GetMDRMNLog(), mozilla::LogLevel::Debug, \ + ("[MediaDrmProxySupport][%s]" x, __FUNCTION__, ##__VA_ARGS__)) +#endif + +class MediaDrmCDMCallbackProxy; + +class MediaDrmProxySupport final { + public: + explicit MediaDrmProxySupport(const nsAString& aKeySystem); + ~MediaDrmProxySupport(); + + /* + * APIs to act as GMPDecryptorAPI, discarding unnecessary calls. + */ + nsresult Init(UniquePtr&& aCallback); + + void CreateSession(uint32_t aCreateSessionToken, uint32_t aPromiseId, + const nsCString& aInitDataType, + const nsTArray& aInitData, + MediaDrmSessionType aSessionType); + + void UpdateSession(uint32_t aPromiseId, const nsCString& aSessionId, + const nsTArray& aResponse); + + void CloseSession(uint32_t aPromiseId, const nsCString& aSessionId); + + void Shutdown(); + + const nsString& GetMediaDrmStubId() const { return mMediaDrmStubId; } + + bool SetServerCertificate(const nsTArray& aCert); + + private: + const nsString mKeySystem; + java::MediaDrmProxy::GlobalRef mBridgeProxy; + java::MediaDrmProxy::NativeMediaDrmProxyCallbacks::GlobalRef mJavaCallbacks; + bool mDestroyed; + nsString mMediaDrmStubId; +}; + +} // namespace mozilla +#endif // MediaDrmProxySupport_H diff --git a/dom/media/eme/mediadrm/moz.build b/dom/media/eme/mediadrm/moz.build new file mode 100644 index 0000000000..c8f433f25f --- /dev/null +++ b/dom/media/eme/mediadrm/moz.build @@ -0,0 +1,19 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +EXPORTS.mozilla += [ + "MediaDrmCDMCallbackProxy.h", + "MediaDrmCDMProxy.h", + "MediaDrmProxySupport.h", +] + +UNIFIED_SOURCES += [ + "MediaDrmCDMCallbackProxy.cpp", + "MediaDrmCDMProxy.cpp", + "MediaDrmProxySupport.cpp", +] + +FINAL_LIBRARY = "xul" -- cgit v1.2.3