diff options
Diffstat (limited to '')
-rw-r--r-- | dom/media/gmp/ChromiumCDMParent.cpp | 1319 |
1 files changed, 1319 insertions, 0 deletions
diff --git a/dom/media/gmp/ChromiumCDMParent.cpp b/dom/media/gmp/ChromiumCDMParent.cpp new file mode 100644 index 0000000000..ddb1e38d33 --- /dev/null +++ b/dom/media/gmp/ChromiumCDMParent.cpp @@ -0,0 +1,1319 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "ChromiumCDMParent.h" + +#include "ChromiumCDMCallback.h" +#include "ChromiumCDMCallbackProxy.h" +#include "ChromiumCDMProxy.h" +#include "content_decryption_module.h" +#include "GMPContentChild.h" +#include "GMPContentParent.h" +#include "GMPLog.h" +#include "GMPService.h" +#include "GMPUtils.h" +#include "VideoUtils.h" +#include "mozilla/dom/MediaKeyMessageEventBinding.h" +#include "mozilla/gmp/GMPTypes.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/Unused.h" +#include "AnnexB.h" +#include "H264.h" + +#define NS_DispatchToMainThread(...) CompileError_UseAbstractMainThreadInstead + +namespace mozilla::gmp { + +using namespace eme; + +ChromiumCDMParent::ChromiumCDMParent(GMPContentParent* aContentParent, + uint32_t aPluginId) + : mPluginId(aPluginId), + mContentParent(aContentParent), + mVideoShmemLimit(StaticPrefs::media_eme_chromium_api_video_shmems()) +#ifdef DEBUG + , + mGMPThread(aContentParent->GMPEventTarget()) +#endif +{ + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::ChromiumCDMParent(this=%p, contentParent=%p, " + "id=%" PRIu32 ")", + this, aContentParent, aPluginId); +} + +RefPtr<ChromiumCDMParent::InitPromise> ChromiumCDMParent::Init( + ChromiumCDMCallback* aCDMCallback, bool aAllowDistinctiveIdentifier, + bool aAllowPersistentState, nsIEventTarget* aMainThread) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::Init(this=%p) shutdown=%s abormalShutdown=%s " + "actorDestroyed=%s", + this, mIsShutdown ? "true" : "false", + mAbnormalShutdown ? "true" : "false", mActorDestroyed ? "true" : "false"); + if (!aCDMCallback || !aMainThread) { + GMP_LOG_DEBUG( + "ChromiumCDMParent::Init(this=%p) failed " + "nullCallback=%s nullMainThread=%s", + this, !aCDMCallback ? "true" : "false", + !aMainThread ? "true" : "false"); + + return ChromiumCDMParent::InitPromise::CreateAndReject( + MediaResult(NS_ERROR_FAILURE, + nsPrintfCString("ChromiumCDMParent::Init() failed " + "nullCallback=%s nullMainThread=%s", + !aCDMCallback ? "true" : "false", + !aMainThread ? "true" : "false")), + __func__); + } + + RefPtr<ChromiumCDMParent::InitPromise> promise = + mInitPromise.Ensure(__func__); + RefPtr<ChromiumCDMParent> self = this; + SendInit(aAllowDistinctiveIdentifier, aAllowPersistentState) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self, aCDMCallback](bool aSuccess) { + if (!aSuccess) { + GMP_LOG_DEBUG( + "ChromiumCDMParent::Init() failed with callback from " + "child indicating CDM failed init"); + self->mInitPromise.RejectIfExists( + MediaResult(NS_ERROR_FAILURE, + "ChromiumCDMParent::Init() failed with callback " + "from child indicating CDM failed init"), + __func__); + return; + } + GMP_LOG_DEBUG( + "ChromiumCDMParent::Init() succeeded with callback from child"); + self->mCDMCallback = aCDMCallback; + self->mInitPromise.ResolveIfExists(true /* unused */, __func__); + }, + [self](ResponseRejectReason&& aReason) { + RefPtr<gmp::GeckoMediaPluginService> service = + gmp::GeckoMediaPluginService::GetGeckoMediaPluginService(); + bool xpcomWillShutdown = + service && service->XPCOMWillShutdownReceived(); + GMP_LOG_DEBUG( + "ChromiumCDMParent::Init(this=%p) failed " + "shutdown=%s cdmCrash=%s actorDestroyed=%s " + "browserShutdown=%s promiseRejectReason=%d", + self.get(), self->mIsShutdown ? "true" : "false", + self->mAbnormalShutdown ? "true" : "false", + self->mActorDestroyed ? "true" : "false", + xpcomWillShutdown ? "true" : "false", + static_cast<int>(aReason)); + self->mInitPromise.RejectIfExists( + MediaResult( + NS_ERROR_FAILURE, + nsPrintfCString("ChromiumCDMParent::Init() failed " + "shutdown=%s cdmCrash=%s actorDestroyed=%s " + "browserShutdown=%s promiseRejectReason=%d", + self->mIsShutdown ? "true" : "false", + self->mAbnormalShutdown ? "true" : "false", + self->mActorDestroyed ? "true" : "false", + xpcomWillShutdown ? "true" : "false", + static_cast<int>(aReason))), + __func__); + }); + return promise; +} + +void ChromiumCDMParent::CreateSession(uint32_t aCreateSessionToken, + uint32_t aSessionType, + uint32_t aInitDataType, + uint32_t aPromiseId, + const nsTArray<uint8_t>& aInitData) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::CreateSession(this=%p)", this); + if (mIsShutdown) { + RejectPromiseShutdown(aPromiseId); + return; + } + if (!SendCreateSessionAndGenerateRequest(aPromiseId, aSessionType, + aInitDataType, aInitData)) { + RejectPromiseWithStateError( + aPromiseId, "Failed to send generateRequest to CDM process."_ns); + return; + } + mPromiseToCreateSessionToken.InsertOrUpdate(aPromiseId, aCreateSessionToken); +} + +void ChromiumCDMParent::LoadSession(uint32_t aPromiseId, uint32_t aSessionType, + nsString aSessionId) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::LoadSession(this=%p, pid=%" PRIu32 + ", type=%" PRIu32 ", sid=%s)", + this, aPromiseId, aSessionType, + NS_ConvertUTF16toUTF8(aSessionId).get()); + if (mIsShutdown) { + RejectPromiseShutdown(aPromiseId); + return; + } + if (!SendLoadSession(aPromiseId, aSessionType, + NS_ConvertUTF16toUTF8(aSessionId))) { + RejectPromiseWithStateError( + aPromiseId, "Failed to send loadSession to CDM process."_ns); + return; + } +} + +void ChromiumCDMParent::SetServerCertificate(uint32_t aPromiseId, + const nsTArray<uint8_t>& aCert) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::SetServerCertificate(this=%p)", this); + if (mIsShutdown) { + RejectPromiseShutdown(aPromiseId); + return; + } + if (!SendSetServerCertificate(aPromiseId, aCert)) { + RejectPromiseWithStateError( + aPromiseId, "Failed to send setServerCertificate to CDM process"_ns); + } +} + +void ChromiumCDMParent::UpdateSession(const nsCString& aSessionId, + uint32_t aPromiseId, + const nsTArray<uint8_t>& aResponse) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::UpdateSession(this=%p)", this); + if (mIsShutdown) { + RejectPromiseShutdown(aPromiseId); + return; + } + if (!SendUpdateSession(aPromiseId, aSessionId, aResponse)) { + RejectPromiseWithStateError( + aPromiseId, "Failed to send updateSession to CDM process"_ns); + } +} + +void ChromiumCDMParent::CloseSession(const nsCString& aSessionId, + uint32_t aPromiseId) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::CloseSession(this=%p)", this); + if (mIsShutdown) { + RejectPromiseShutdown(aPromiseId); + return; + } + if (!SendCloseSession(aPromiseId, aSessionId)) { + RejectPromiseWithStateError( + aPromiseId, "Failed to send closeSession to CDM process"_ns); + } +} + +void ChromiumCDMParent::RemoveSession(const nsCString& aSessionId, + uint32_t aPromiseId) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RemoveSession(this=%p)", this); + if (mIsShutdown) { + RejectPromiseShutdown(aPromiseId); + return; + } + if (!SendRemoveSession(aPromiseId, aSessionId)) { + RejectPromiseWithStateError( + aPromiseId, "Failed to send removeSession to CDM process"_ns); + } +} + +void ChromiumCDMParent::NotifyOutputProtectionStatus(bool aSuccess, + uint32_t aLinkMask, + uint32_t aProtectionMask) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::NotifyOutputProtectionStatus(this=%p)", + this); + if (mIsShutdown) { + return; + } + const bool haveCachedValue = mOutputProtectionLinkMask.isSome(); + if (mAwaitingOutputProtectionInformation && !aSuccess) { + MOZ_DIAGNOSTIC_ASSERT( + !haveCachedValue, + "Should not have a cached value if we're still awaiting infomation"); + // We're awaiting info and don't yet have a cached value, and the check + // failed, don't cache the result, just forward the failure. + CompleteQueryOutputProtectionStatus(false, aLinkMask, aProtectionMask); + return; + } + if (!mAwaitingOutputProtectionInformation && haveCachedValue && !aSuccess) { + // We're not awaiting info, already have a cached value, and the check + // failed. Ignore this, we'll update our info from any future successful + // checks. + return; + } + MOZ_ASSERT(aSuccess, "Failed checks should be handled by this point"); + // Update our protection information. + mOutputProtectionLinkMask = Some(aLinkMask); + + if (mAwaitingOutputProtectionInformation) { + // If we have an outstanding query, service that query with this + // information. + mAwaitingOutputProtectionInformation = false; + MOZ_ASSERT(!haveCachedValue, + "If we were waiting on information, we shouldn't have yet " + "cached a value"); + CompleteQueryOutputProtectionStatus(true, mOutputProtectionLinkMask.value(), + aProtectionMask); + } +} + +void ChromiumCDMParent::CompleteQueryOutputProtectionStatus( + bool aSuccess, uint32_t aLinkMask, uint32_t aProtectionMask) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::CompleteQueryOutputProtectionStatus(this=%p) " + "mIsShutdown=%s aSuccess=%s aLinkMask=%" PRIu32, + this, mIsShutdown ? "true" : "false", aSuccess ? "true" : "false", + aLinkMask); + if (mIsShutdown) { + return; + } + Unused << SendCompleteQueryOutputProtectionStatus(aSuccess, aLinkMask, + aProtectionMask); +} + +// See +// https://cs.chromium.org/chromium/src/media/blink/webcontentdecryptionmodule_impl.cc?l=33-66&rcl=d49aa59ac8c2925d5bec229f3f1906537b6b4547 +static Result<cdm::HdcpVersion, nsresult> ToCDMHdcpVersion( + const nsCString& aMinHdcpVersion) { + if (aMinHdcpVersion.IsEmpty()) { + return cdm::HdcpVersion::kHdcpVersionNone; + } + if (aMinHdcpVersion.EqualsIgnoreCase("1.0")) { + return cdm::HdcpVersion::kHdcpVersion1_0; + } + if (aMinHdcpVersion.EqualsIgnoreCase("1.1")) { + return cdm::HdcpVersion::kHdcpVersion1_1; + } + if (aMinHdcpVersion.EqualsIgnoreCase("1.2")) { + return cdm::HdcpVersion::kHdcpVersion1_2; + } + if (aMinHdcpVersion.EqualsIgnoreCase("1.3")) { + return cdm::HdcpVersion::kHdcpVersion1_3; + } + if (aMinHdcpVersion.EqualsIgnoreCase("1.4")) { + return cdm::HdcpVersion::kHdcpVersion1_4; + } + if (aMinHdcpVersion.EqualsIgnoreCase("2.0")) { + return cdm::HdcpVersion::kHdcpVersion2_0; + } + if (aMinHdcpVersion.EqualsIgnoreCase("2.1")) { + return cdm::HdcpVersion::kHdcpVersion2_1; + } + if (aMinHdcpVersion.EqualsIgnoreCase("2.2")) { + return cdm::HdcpVersion::kHdcpVersion2_2; + } + // When adding another version remember to update GMPMessageUtils so that we + // can serialize it correctly and have correct bounds on the enum! + + // Invalid hdcp version string. + return Err(NS_ERROR_INVALID_ARG); +} + +void ChromiumCDMParent::GetStatusForPolicy(uint32_t aPromiseId, + const nsCString& aMinHdcpVersion) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::GetStatusForPolicy(this=%p)", this); + if (mIsShutdown) { + RejectPromiseShutdown(aPromiseId); + return; + } + auto hdcpVersionResult = ToCDMHdcpVersion(aMinHdcpVersion); + if (hdcpVersionResult.isErr()) { + ErrorResult rv; + // XXXbz there's no spec for this yet, and + // <https://github.com/WICG/hdcp-detection/blob/master/explainer.md> + // does not define what exceptions get thrown. Let's assume + // TypeError for invalid args, as usual. + constexpr auto err = + "getStatusForPolicy failed due to bad hdcp version argument"_ns; + rv.ThrowTypeError(err); + RejectPromise(aPromiseId, std::move(rv), err); + return; + } + + if (!SendGetStatusForPolicy(aPromiseId, hdcpVersionResult.unwrap())) { + RejectPromiseWithStateError( + aPromiseId, "Failed to send getStatusForPolicy to CDM process"_ns); + } +} + +bool ChromiumCDMParent::InitCDMInputBuffer(gmp::CDMInputBuffer& aBuffer, + MediaRawData* aSample) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + const CryptoSample& crypto = aSample->mCrypto; + if (crypto.mEncryptedSizes.Length() != crypto.mPlainSizes.Length()) { + GMP_LOG_DEBUG("InitCDMInputBuffer clear/cipher subsamples don't match"); + return false; + } + + Shmem shmem; + if (!AllocShmem(aSample->Size(), &shmem)) { + return false; + } + memcpy(shmem.get<uint8_t>(), aSample->Data(), aSample->Size()); + cdm::EncryptionScheme encryptionScheme = cdm::EncryptionScheme::kUnencrypted; + switch (crypto.mCryptoScheme) { + case CryptoScheme::None: + break; // Default to none + case CryptoScheme::Cenc: + encryptionScheme = cdm::EncryptionScheme::kCenc; + break; + case CryptoScheme::Cbcs: + encryptionScheme = cdm::EncryptionScheme::kCbcs; + break; + default: + GMP_LOG_DEBUG( + "InitCDMInputBuffer got unexpected encryption scheme with " + "value of %" PRIu8 ". Treating as no encryption.", + static_cast<uint8_t>(crypto.mCryptoScheme)); + MOZ_ASSERT_UNREACHABLE("Should not have unrecognized encryption type"); + break; + } + + const nsTArray<uint8_t>& iv = encryptionScheme != cdm::EncryptionScheme::kCbcs + ? crypto.mIV + : crypto.mConstantIV; + aBuffer = gmp::CDMInputBuffer( + shmem, crypto.mKeyId, iv, aSample->mTime.ToMicroseconds(), + aSample->mDuration.ToMicroseconds(), crypto.mPlainSizes, + crypto.mEncryptedSizes, crypto.mCryptByteBlock, crypto.mSkipByteBlock, + encryptionScheme); + return true; +} + +bool ChromiumCDMParent::SendBufferToCDM(uint32_t aSizeInBytes) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::SendBufferToCDM() size=%" PRIu32, + aSizeInBytes); + Shmem shmem; + if (!AllocShmem(aSizeInBytes, &shmem)) { + return false; + } + if (!SendGiveBuffer(std::move(shmem))) { + DeallocShmem(shmem); + return false; + } + return true; +} + +RefPtr<DecryptPromise> ChromiumCDMParent::Decrypt(MediaRawData* aSample) { + if (mIsShutdown) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample), + __func__); + } + CDMInputBuffer buffer; + if (!InitCDMInputBuffer(buffer, aSample)) { + return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample), + __func__); + } + // Send a buffer to the CDM to store the output. The CDM will either fill + // it with the decrypted sample and return it, or deallocate it on failure. + if (!SendBufferToCDM(aSample->Size())) { + DeallocShmem(buffer.mData()); + return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample), + __func__); + } + + RefPtr<DecryptJob> job = new DecryptJob(aSample); + if (!SendDecrypt(job->mId, buffer)) { + GMP_LOG_DEBUG( + "ChromiumCDMParent::Decrypt(this=%p) failed to send decrypt message", + this); + DeallocShmem(buffer.mData()); + return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample), + __func__); + } + RefPtr<DecryptPromise> promise = job->Ensure(); + mDecrypts.AppendElement(job); + return promise; +} + +ipc::IPCResult ChromiumCDMParent::Recv__delete__() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + MOZ_ASSERT(mIsShutdown); + GMP_LOG_DEBUG("ChromiumCDMParent::Recv__delete__(this=%p)", this); + if (mContentParent) { + mContentParent->ChromiumCDMDestroyed(this); + mContentParent = nullptr; + } + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvOnResolvePromiseWithKeyStatus( + const uint32_t& aPromiseId, const uint32_t& aKeyStatus) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::RecvOnResolvePromiseWithKeyStatus(this=%p, " + "pid=%" PRIu32 ", keystatus=%" PRIu32 ")", + this, aPromiseId, aKeyStatus); + if (!mCDMCallback || mIsShutdown) { + return IPC_OK(); + } + + mCDMCallback->ResolvePromiseWithKeyStatus(aPromiseId, aKeyStatus); + + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvOnResolveNewSessionPromise( + const uint32_t& aPromiseId, const nsCString& aSessionId) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::RecvOnResolveNewSessionPromise(this=%p, pid=%" PRIu32 + ", sid=%s)", + this, aPromiseId, aSessionId.get()); + if (!mCDMCallback || mIsShutdown) { + return IPC_OK(); + } + + Maybe<uint32_t> token = mPromiseToCreateSessionToken.Extract(aPromiseId); + if (token.isNothing()) { + RejectPromiseWithStateError(aPromiseId, + "Lost session token for new session."_ns); + return IPC_OK(); + } + + mCDMCallback->SetSessionId(token.value(), aSessionId); + + ResolvePromise(aPromiseId); + + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvResolveLoadSessionPromise( + const uint32_t& aPromiseId, const bool& aSuccessful) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::RecvResolveLoadSessionPromise(this=%p, pid=%" PRIu32 + ", successful=%d)", + this, aPromiseId, aSuccessful); + if (!mCDMCallback || mIsShutdown) { + return IPC_OK(); + } + + mCDMCallback->ResolveLoadSessionPromise(aPromiseId, aSuccessful); + + return IPC_OK(); +} + +void ChromiumCDMParent::ResolvePromise(uint32_t aPromiseId) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::ResolvePromise(this=%p, pid=%" PRIu32 ")", + this, aPromiseId); + + // Note: The MediaKeys rejects all pending DOM promises when it + // initiates shutdown. + if (!mCDMCallback || mIsShutdown) { + return; + } + + mCDMCallback->ResolvePromise(aPromiseId); +} + +ipc::IPCResult ChromiumCDMParent::RecvOnResolvePromise( + const uint32_t& aPromiseId) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + ResolvePromise(aPromiseId); + return IPC_OK(); +} + +void ChromiumCDMParent::RejectPromise(uint32_t aPromiseId, + ErrorResult&& aException, + const nsCString& aErrorMessage) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RejectPromise(this=%p, pid=%" PRIu32 ")", + this, aPromiseId); + // Note: The MediaKeys rejects all pending DOM promises when it + // initiates shutdown. + if (!mCDMCallback || mIsShutdown) { + // Suppress the exception as it will not be explicitly handled due to the + // early return. + aException.SuppressException(); + return; + } + + mCDMCallback->RejectPromise(aPromiseId, std::move(aException), aErrorMessage); +} + +void ChromiumCDMParent::RejectPromiseShutdown(uint32_t aPromiseId) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + RejectPromiseWithStateError(aPromiseId, "CDM is shutdown"_ns); +} + +void ChromiumCDMParent::RejectPromiseWithStateError( + uint32_t aPromiseId, const nsCString& aErrorMessage) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + ErrorResult rv; + rv.ThrowInvalidStateError(aErrorMessage); + RejectPromise(aPromiseId, std::move(rv), aErrorMessage); +} + +static ErrorResult ToErrorResult(uint32_t aException, + const nsCString& aErrorMessage) { + // XXXbz could we have a CopyableErrorResult sent to us with a better error + // message? + ErrorResult rv; + switch (static_cast<cdm::Exception>(aException)) { + case cdm::Exception::kExceptionNotSupportedError: + rv.ThrowNotSupportedError(aErrorMessage); + break; + case cdm::Exception::kExceptionInvalidStateError: + rv.ThrowInvalidStateError(aErrorMessage); + break; + case cdm::Exception::kExceptionTypeError: + rv.ThrowTypeError(aErrorMessage); + break; + case cdm::Exception::kExceptionQuotaExceededError: + rv.ThrowQuotaExceededError(aErrorMessage); + break; + default: + MOZ_ASSERT_UNREACHABLE("Invalid cdm::Exception enum value."); + // Note: Unique placeholder. + rv.ThrowTimeoutError(aErrorMessage); + }; + return rv; +} + +ipc::IPCResult ChromiumCDMParent::RecvOnRejectPromise( + const uint32_t& aPromiseId, const uint32_t& aException, + const uint32_t& aSystemCode, const nsCString& aErrorMessage) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + RejectPromise(aPromiseId, ToErrorResult(aException, aErrorMessage), + aErrorMessage); + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvOnSessionMessage( + const nsCString& aSessionId, const uint32_t& aMessageType, + nsTArray<uint8_t>&& aMessage) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvOnSessionMessage(this=%p, sid=%s)", + this, aSessionId.get()); + if (!mCDMCallback || mIsShutdown) { + return IPC_OK(); + } + + mCDMCallback->SessionMessage(aSessionId, aMessageType, std::move(aMessage)); + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvOnSessionKeysChange( + const nsCString& aSessionId, nsTArray<CDMKeyInformation>&& aKeysInfo) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvOnSessionKeysChange(this=%p)", this); + if (!mCDMCallback || mIsShutdown) { + return IPC_OK(); + } + + mCDMCallback->SessionKeysChange(aSessionId, std::move(aKeysInfo)); + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvOnExpirationChange( + const nsCString& aSessionId, const double& aSecondsSinceEpoch) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvOnExpirationChange(this=%p) time=%lf", + this, aSecondsSinceEpoch); + if (!mCDMCallback || mIsShutdown) { + return IPC_OK(); + } + + mCDMCallback->ExpirationChange(aSessionId, aSecondsSinceEpoch); + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvOnSessionClosed( + const nsCString& aSessionId) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvOnSessionClosed(this=%p)", this); + if (!mCDMCallback || mIsShutdown) { + return IPC_OK(); + } + + mCDMCallback->SessionClosed(aSessionId); + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvOnQueryOutputProtectionStatus() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::RecvOnQueryOutputProtectionStatus(this=%p) " + "mIsShutdown=%s mCDMCallback=%s mAwaitingOutputProtectionInformation=%s", + this, mIsShutdown ? "true" : "false", mCDMCallback ? "true" : "false", + mAwaitingOutputProtectionInformation ? "true" : "false"); + if (mIsShutdown) { + // We're shutdown, don't try to service the query. + return IPC_OK(); + } + if (!mCDMCallback) { + // We don't have a callback. We're not yet outputting anything so can report + // we're safe. + CompleteQueryOutputProtectionStatus(true, uint32_t{}, uint32_t{}); + return IPC_OK(); + } + + if (mOutputProtectionLinkMask.isSome()) { + MOZ_DIAGNOSTIC_ASSERT( + !mAwaitingOutputProtectionInformation, + "If we have a cached value we should not be awaiting information"); + // We have a cached value, use that. + CompleteQueryOutputProtectionStatus(true, mOutputProtectionLinkMask.value(), + uint32_t{}); + return IPC_OK(); + } + + // We need to call up the stack to get the info. The CDM proxy will finish + // the request via `NotifyOutputProtectionStatus`. + mAwaitingOutputProtectionInformation = true; + mCDMCallback->QueryOutputProtectionStatus(); + return IPC_OK(); +} + +DecryptStatus ToDecryptStatus(uint32_t aStatus) { + switch (static_cast<cdm::Status>(aStatus)) { + case cdm::kSuccess: + return DecryptStatus::Ok; + case cdm::kNoKey: + return DecryptStatus::NoKeyErr; + default: + return DecryptStatus::GenericErr; + } +} + +ipc::IPCResult ChromiumCDMParent::RecvDecryptFailed(const uint32_t& aId, + const uint32_t& aStatus) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvDecryptFailed(this=%p, id=%" PRIu32 + ", status=%" PRIu32 ")", + this, aId, aStatus); + + if (mIsShutdown) { + MOZ_ASSERT(mDecrypts.IsEmpty()); + return IPC_OK(); + } + + for (size_t i = 0; i < mDecrypts.Length(); i++) { + if (mDecrypts[i]->mId == aId) { + mDecrypts[i]->PostResult(ToDecryptStatus(aStatus)); + mDecrypts.RemoveElementAt(i); + break; + } + } + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvDecrypted(const uint32_t& aId, + const uint32_t& aStatus, + ipc::Shmem&& aShmem) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvDecrypted(this=%p, id=%" PRIu32 + ", status=%" PRIu32 ")", + this, aId, aStatus); + + // We must deallocate the shmem once we've copied the result out of it + // in PostResult below. + auto autoDeallocateShmem = MakeScopeExit([&, this] { DeallocShmem(aShmem); }); + + if (mIsShutdown) { + MOZ_ASSERT(mDecrypts.IsEmpty()); + return IPC_OK(); + } + for (size_t i = 0; i < mDecrypts.Length(); i++) { + if (mDecrypts[i]->mId == aId) { + mDecrypts[i]->PostResult( + ToDecryptStatus(aStatus), + Span<const uint8_t>(aShmem.get<uint8_t>(), aShmem.Size<uint8_t>())); + mDecrypts.RemoveElementAt(i); + break; + } + } + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvIncreaseShmemPoolSize() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("%s(this=%p) limit=%" PRIu32 " active=%" PRIu32, __func__, this, + mVideoShmemLimit, mVideoShmemsActive); + + // Put an upper limit on the number of shmems we tolerate the CDM asking + // for, to prevent a memory blow-out. In practice, we expect the CDM to + // need less than 5, but some encodings require more. + // We'd expect CDMs to not have video frames larger than 720p-1080p + // (due to DRM robustness requirements), which is about 1.5MB-3MB per + // frame. + if (mVideoShmemLimit > 50) { + mDecodePromise.RejectIfExists( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("Failled to ensure CDM has enough shmems.")), + __func__); + Shutdown(); + return IPC_OK(); + } + mVideoShmemLimit++; + + EnsureSufficientShmems(mVideoFrameBufferSize); + + return IPC_OK(); +} + +bool ChromiumCDMParent::PurgeShmems() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::PurgeShmems(this=%p) frame_size=%zu" + " limit=%" PRIu32 " active=%" PRIu32, + this, mVideoFrameBufferSize, mVideoShmemLimit, mVideoShmemsActive); + + if (mVideoShmemsActive == 0) { + // We haven't allocated any shmems, nothing to do here. + return true; + } + if (!SendPurgeShmems()) { + return false; + } + mVideoShmemsActive = 0; + return true; +} + +bool ChromiumCDMParent::EnsureSufficientShmems(size_t aVideoFrameSize) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::EnsureSufficientShmems(this=%p) " + "size=%zu expected_size=%zu limit=%" PRIu32 " active=%" PRIu32, + this, aVideoFrameSize, mVideoFrameBufferSize, mVideoShmemLimit, + mVideoShmemsActive); + + // The Chromium CDM API requires us to implement a synchronous + // interface to supply buffers to the CDM for it to write decrypted samples + // into. We want our buffers to be backed by shmems, in order to reduce + // the overhead of transferring decoded frames. However due to sandboxing + // restrictions, the CDM process cannot allocate shmems itself. + // We don't want to be doing synchronous IPC to request shmems from the + // content process, nor do we want to have to do intr IPC or make async + // IPC conform to the sync allocation interface. So instead we have the + // content process pre-allocate a set of shmems and give them to the CDM + // process in advance of them being needed. + // + // When the CDM needs to allocate a buffer for storing a decoded video + // frame, the CDM host gives it one of these shmems' buffers. When this + // is sent back to the content process, we upload it to a GPU surface, + // and send the shmem back to the CDM process so it can reuse it. + // + // Normally the CDM won't allocate more than one buffer at once, but + // we've seen cases where it allocates multiple buffers, returns one and + // holds onto the rest. So we need to ensure we have several extra + // shmems pre-allocated for the CDM. This threshold is set by the pref + // media.eme.chromium-api.video-shmems. + // + // We also have a failure recovery mechanism; if the CDM asks for more + // buffers than we have shmem's available, ChromiumCDMChild gives the + // CDM a non-shared memory buffer, and returns the frame to the parent + // in an nsTArray<uint8_t> instead of a shmem. The child then sends a + // message to the parent asking it to increase the number of shmems in + // the pool. Via this mechanism we should recover from incorrectly + // predicting how many shmems to pre-allocate. + // + // At decoder start up, we guess how big the shmems need to be based on + // the video frame dimensions. If we guess wrong, the CDM will follow + // the non-shmem path, and we'll re-create the shmems of the correct size. + // This meanns we can recover from guessing the shmem size wrong. + // We must re-take this path after every decoder de-init/re-init, as the + // frame sizes should change every time we switch video stream. + + if (mVideoFrameBufferSize < aVideoFrameSize) { + if (!PurgeShmems()) { + return false; + } + mVideoFrameBufferSize = aVideoFrameSize; + } + + while (mVideoShmemsActive < mVideoShmemLimit) { + if (!SendBufferToCDM(mVideoFrameBufferSize)) { + return false; + } + mVideoShmemsActive++; + } + + return true; +} + +ipc::IPCResult ChromiumCDMParent::RecvDecodedData(const CDMVideoFrame& aFrame, + nsTArray<uint8_t>&& aData) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvDecodedData(this=%p) time=%" PRId64, + this, aFrame.mTimestamp()); + + if (mIsShutdown || mDecodePromise.IsEmpty()) { + return IPC_OK(); + } + + if (!EnsureSufficientShmems(aData.Length())) { + mDecodePromise.RejectIfExists( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("Failled to ensure CDM has enough shmems.")), + __func__); + return IPC_OK(); + } + + RefPtr<VideoData> v = CreateVideoFrame(aFrame, aData); + if (!v) { + mDecodePromise.RejectIfExists( + MediaResult(NS_ERROR_OUT_OF_MEMORY, + RESULT_DETAIL("Can't create VideoData")), + __func__); + return IPC_OK(); + } + + ReorderAndReturnOutput(std::move(v)); + + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvDecodedShmem(const CDMVideoFrame& aFrame, + ipc::Shmem&& aShmem) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvDecodedShmem(this=%p) time=%" PRId64 + " duration=%" PRId64, + this, aFrame.mTimestamp(), aFrame.mDuration()); + + // On failure we need to deallocate the shmem we're to return to the + // CDM. On success we return it to the CDM to be reused. + auto autoDeallocateShmem = + MakeScopeExit([&, this] { this->DeallocShmem(aShmem); }); + + if (mIsShutdown || mDecodePromise.IsEmpty()) { + return IPC_OK(); + } + + RefPtr<VideoData> v = CreateVideoFrame( + aFrame, Span<uint8_t>(aShmem.get<uint8_t>(), aShmem.Size<uint8_t>())); + if (!v) { + mDecodePromise.RejectIfExists( + MediaResult(NS_ERROR_OUT_OF_MEMORY, + RESULT_DETAIL("Can't create VideoData")), + __func__); + return IPC_OK(); + } + + // Return the shmem to the CDM so the shmem can be reused to send us + // another frame. + if (!SendGiveBuffer(std::move(aShmem))) { + mDecodePromise.RejectIfExists( + MediaResult(NS_ERROR_OUT_OF_MEMORY, + RESULT_DETAIL("Can't return shmem to CDM process")), + __func__); + return IPC_OK(); + } + + // Don't need to deallocate the shmem since the CDM process is responsible + // for it again. + autoDeallocateShmem.release(); + + ReorderAndReturnOutput(std::move(v)); + + return IPC_OK(); +} + +void ChromiumCDMParent::ReorderAndReturnOutput(RefPtr<VideoData>&& aFrame) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + if (mMaxRefFrames == 0) { + mDecodePromise.ResolveIfExists( + MediaDataDecoder::DecodedData({std::move(aFrame)}), __func__); + return; + } + mReorderQueue.Push(std::move(aFrame)); + MediaDataDecoder::DecodedData results; + while (mReorderQueue.Length() > mMaxRefFrames) { + results.AppendElement(mReorderQueue.Pop()); + } + mDecodePromise.Resolve(std::move(results), __func__); +} + +already_AddRefed<VideoData> ChromiumCDMParent::CreateVideoFrame( + const CDMVideoFrame& aFrame, Span<uint8_t> aData) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + VideoData::YCbCrBuffer b; + MOZ_ASSERT(aData.Length() > 0); + + // Since we store each plane separately we can just roll the offset + // into our pointer to that plane and store that. + b.mPlanes[0].mData = aData.Elements() + aFrame.mYPlane().mPlaneOffset(); + b.mPlanes[0].mWidth = aFrame.mImageWidth(); + b.mPlanes[0].mHeight = aFrame.mImageHeight(); + b.mPlanes[0].mStride = aFrame.mYPlane().mStride(); + b.mPlanes[0].mSkip = 0; + + b.mPlanes[1].mData = aData.Elements() + aFrame.mUPlane().mPlaneOffset(); + b.mPlanes[1].mWidth = (aFrame.mImageWidth() + 1) / 2; + b.mPlanes[1].mHeight = (aFrame.mImageHeight() + 1) / 2; + b.mPlanes[1].mStride = aFrame.mUPlane().mStride(); + b.mPlanes[1].mSkip = 0; + + b.mPlanes[2].mData = aData.Elements() + aFrame.mVPlane().mPlaneOffset(); + b.mPlanes[2].mWidth = (aFrame.mImageWidth() + 1) / 2; + b.mPlanes[2].mHeight = (aFrame.mImageHeight() + 1) / 2; + b.mPlanes[2].mStride = aFrame.mVPlane().mStride(); + b.mPlanes[2].mSkip = 0; + + b.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT; + + // We unfortunately can't know which colorspace the video is using at this + // stage. + b.mYUVColorSpace = + DefaultColorSpace({aFrame.mImageWidth(), aFrame.mImageHeight()}); + + gfx::IntRect pictureRegion(0, 0, aFrame.mImageWidth(), aFrame.mImageHeight()); + RefPtr<VideoData> v = VideoData::CreateAndCopyData( + mVideoInfo, mImageContainer, mLastStreamOffset, + media::TimeUnit::FromMicroseconds(aFrame.mTimestamp()), + media::TimeUnit::FromMicroseconds(aFrame.mDuration()), b, false, + media::TimeUnit::FromMicroseconds(-1), pictureRegion, mKnowsCompositor); + + if (!v || !v->mImage) { + NS_WARNING("Failed to decode video frame."); + return v.forget(); + } + + // This is a DRM image. + v->mImage->SetIsDRM(true); + + return v.forget(); +} + +ipc::IPCResult ChromiumCDMParent::RecvDecodeFailed(const uint32_t& aStatus) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvDecodeFailed(this=%p status=%" PRIu32 + ")", + this, aStatus); + if (mIsShutdown) { + MOZ_ASSERT(mDecodePromise.IsEmpty()); + return IPC_OK(); + } + + if (aStatus == cdm::kNeedMoreData) { + mDecodePromise.ResolveIfExists(nsTArray<RefPtr<MediaData>>(), __func__); + return IPC_OK(); + } + + mDecodePromise.RejectIfExists( + MediaResult( + NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL( + "ChromiumCDMParent::RecvDecodeFailed with status %s (%" PRIu32 + ")", + CdmStatusToString(aStatus), aStatus)), + __func__); + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvShutdown() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvShutdown(this=%p)", this); + Shutdown(); + return IPC_OK(); +} + +void ChromiumCDMParent::ActorDestroy(ActorDestroyReason aWhy) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::ActorDestroy(this=%p, reason=%d)", this, + aWhy); + MOZ_ASSERT(!mActorDestroyed); + mActorDestroyed = true; + // Shutdown() will clear mCDMCallback, so let's keep a reference for later + // use. + auto callback = mCDMCallback; + if (!mIsShutdown) { + // Plugin crash. + MOZ_ASSERT(aWhy == AbnormalShutdown); + Shutdown(); + } + MOZ_ASSERT(mIsShutdown); + RefPtr<ChromiumCDMParent> kungFuDeathGrip(this); + if (mContentParent) { + mContentParent->ChromiumCDMDestroyed(this); + mContentParent = nullptr; + } + mAbnormalShutdown = (aWhy == AbnormalShutdown); + if (mAbnormalShutdown && callback) { + callback->Terminated(); + } + MaybeDisconnect(mAbnormalShutdown); +} + +RefPtr<MediaDataDecoder::InitPromise> ChromiumCDMParent::InitializeVideoDecoder( + const gmp::CDMVideoDecoderConfig& aConfig, const VideoInfo& aInfo, + RefPtr<layers::ImageContainer> aImageContainer, + RefPtr<layers::KnowsCompositor> aKnowsCompositor) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + if (mIsShutdown) { + return MediaDataDecoder::InitPromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("ChromiumCDMParent is shutdown")), + __func__); + } + + // The Widevine CDM version 1.4.8.970 and above contain a video decoder that + // does not optimally allocate video frames; it requests buffers much larger + // than required. The exact formula the CDM uses to calculate their frame + // sizes isn't obvious, but they normally request around or slightly more + // than 1.5X the optimal amount. So pad the size of buffers we allocate so + // that we're likely to have buffers big enough to accomodate the CDM's weird + // frame size calculation. + const size_t bufferSize = + 1.7 * I420FrameBufferSizePadded(aInfo.mImage.width, aInfo.mImage.height); + if (bufferSize <= 0) { + return MediaDataDecoder::InitPromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("Video frame buffer size is invalid.")), + __func__); + } + + if (!EnsureSufficientShmems(bufferSize)) { + return MediaDataDecoder::InitPromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("Failed to init shmems for video decoder")), + __func__); + } + + if (!SendInitializeVideoDecoder(aConfig)) { + return MediaDataDecoder::InitPromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("Failed to send init video decoder to CDM")), + __func__); + } + + mMaxRefFrames = (aConfig.mCodec() == cdm::VideoCodec::kCodecH264) + ? H264::HasSPS(aInfo.mExtraData) + ? H264::ComputeMaxRefFrames(aInfo.mExtraData) + : 16 + : 0; + + mVideoDecoderInitialized = true; + mImageContainer = aImageContainer; + mKnowsCompositor = aKnowsCompositor; + mVideoInfo = aInfo; + mVideoFrameBufferSize = bufferSize; + + return mInitVideoDecoderPromise.Ensure(__func__); +} + +ipc::IPCResult ChromiumCDMParent::RecvOnDecoderInitDone( + const uint32_t& aStatus) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::RecvOnDecoderInitDone(this=%p, status=%" PRIu32 ")", + this, aStatus); + if (mIsShutdown) { + MOZ_ASSERT(mInitVideoDecoderPromise.IsEmpty()); + return IPC_OK(); + } + if (aStatus == static_cast<uint32_t>(cdm::kSuccess)) { + mInitVideoDecoderPromise.ResolveIfExists(TrackInfo::kVideoTrack, __func__); + } else { + mVideoDecoderInitialized = false; + mInitVideoDecoderPromise.RejectIfExists( + MediaResult( + NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("CDM init decode failed with status %s (%" PRIu32 ")", + CdmStatusToString(aStatus), aStatus)), + __func__); + } + return IPC_OK(); +} + +RefPtr<MediaDataDecoder::DecodePromise> +ChromiumCDMParent::DecryptAndDecodeFrame(MediaRawData* aSample) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + if (mIsShutdown) { + return MediaDataDecoder::DecodePromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("ChromiumCDMParent is shutdown")), + __func__); + } + + GMP_LOG_DEBUG("ChromiumCDMParent::DecryptAndDecodeFrame t=%" PRId64, + aSample->mTime.ToMicroseconds()); + + CDMInputBuffer buffer; + + if (!InitCDMInputBuffer(buffer, aSample)) { + return MediaDataDecoder::DecodePromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "Failed to init CDM buffer."), + __func__); + } + + mLastStreamOffset = aSample->mOffset; + + if (!SendDecryptAndDecodeFrame(buffer)) { + GMP_LOG_DEBUG( + "ChromiumCDMParent::Decrypt(this=%p) failed to send decrypt message.", + this); + DeallocShmem(buffer.mData()); + return MediaDataDecoder::DecodePromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + "Failed to send decrypt to CDM process."), + __func__); + } + + return mDecodePromise.Ensure(__func__); +} + +RefPtr<MediaDataDecoder::FlushPromise> ChromiumCDMParent::FlushVideoDecoder() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + if (mIsShutdown) { + MOZ_ASSERT(mReorderQueue.IsEmpty()); + return MediaDataDecoder::FlushPromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("ChromiumCDMParent is shutdown")), + __func__); + } + + mReorderQueue.Clear(); + + mDecodePromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__); + if (!SendResetVideoDecoder()) { + return MediaDataDecoder::FlushPromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + "Failed to send flush to CDM."), + __func__); + } + return mFlushDecoderPromise.Ensure(__func__); +} + +ipc::IPCResult ChromiumCDMParent::RecvResetVideoDecoderComplete() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + MOZ_ASSERT(mReorderQueue.IsEmpty()); + if (mIsShutdown) { + MOZ_ASSERT(mFlushDecoderPromise.IsEmpty()); + return IPC_OK(); + } + mFlushDecoderPromise.ResolveIfExists(true, __func__); + return IPC_OK(); +} + +RefPtr<MediaDataDecoder::DecodePromise> ChromiumCDMParent::Drain() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + MOZ_ASSERT(mDecodePromise.IsEmpty(), "Must wait for decoding to complete"); + if (mIsShutdown) { + return MediaDataDecoder::DecodePromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("ChromiumCDMParent is shutdown")), + __func__); + } + + RefPtr<MediaDataDecoder::DecodePromise> p = mDecodePromise.Ensure(__func__); + if (!SendDrain()) { + mDecodePromise.Resolve(MediaDataDecoder::DecodedData(), __func__); + } + return p; +} + +ipc::IPCResult ChromiumCDMParent::RecvDrainComplete() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + if (mIsShutdown) { + MOZ_ASSERT(mDecodePromise.IsEmpty()); + return IPC_OK(); + } + + MediaDataDecoder::DecodedData samples; + while (!mReorderQueue.IsEmpty()) { + samples.AppendElement(mReorderQueue.Pop()); + } + + mDecodePromise.ResolveIfExists(std::move(samples), __func__); + return IPC_OK(); +} +RefPtr<ShutdownPromise> ChromiumCDMParent::ShutdownVideoDecoder() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + if (mIsShutdown || !mVideoDecoderInitialized) { + return ShutdownPromise::CreateAndResolve(true, __func__); + } + mInitVideoDecoderPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, + __func__); + mDecodePromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__); + MOZ_ASSERT(mFlushDecoderPromise.IsEmpty()); + if (!SendDeinitializeVideoDecoder()) { + return ShutdownPromise::CreateAndResolve(true, __func__); + } + mVideoDecoderInitialized = false; + + GMP_LOG_DEBUG("ChromiumCDMParent::~ShutdownVideoDecoder(this=%p) ", this); + + // The ChromiumCDMChild will purge its shmems, so if the decoder is + // reinitialized the shmems need to be re-allocated, and they may need + // to be a different size. + mVideoShmemsActive = 0; + mVideoFrameBufferSize = 0; + return ShutdownPromise::CreateAndResolve(true, __func__); +} + +void ChromiumCDMParent::Shutdown() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::Shutdown(this=%p)", this); + + if (mIsShutdown) { + return; + } + mIsShutdown = true; + + // If we're shutting down due to the plugin shutting down due to application + // shutdown, we should tell the CDM proxy to also shutdown. Otherwise the + // proxy will shutdown when the owning MediaKeys is destroyed during cycle + // collection, and that will not shut down cleanly as the GMP thread will be + // shutdown by then. + if (mCDMCallback) { + mCDMCallback->Shutdown(); + } + + // We may be called from a task holding the last reference to the CDM + // callback, so let's clear our local weak pointer to ensure it will not be + // used afterward (including from an already-queued task, e.g.: ActorDestroy). + mCDMCallback = nullptr; + + mReorderQueue.Clear(); + + for (RefPtr<DecryptJob>& decrypt : mDecrypts) { + decrypt->PostResult(eme::AbortedErr); + } + mDecrypts.Clear(); + + if (mVideoDecoderInitialized && !mActorDestroyed) { + Unused << SendDeinitializeVideoDecoder(); + mVideoDecoderInitialized = false; + } + + // Note: MediaKeys rejects all outstanding promises when it initiates + // shutdown. + mPromiseToCreateSessionToken.Clear(); + + mInitPromise.RejectIfExists( + MediaResult(NS_ERROR_DOM_ABORT_ERR, + RESULT_DETAIL("ChromiumCDMParent is shutdown")), + __func__); + + mInitVideoDecoderPromise.RejectIfExists( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("ChromiumCDMParent is shutdown")), + __func__); + mDecodePromise.RejectIfExists( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("ChromiumCDMParent is shutdown")), + __func__); + mFlushDecoderPromise.RejectIfExists( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("ChromiumCDMParent is shutdown")), + __func__); + + if (!mActorDestroyed) { + Unused << SendDestroy(); + } +} + +} // namespace mozilla::gmp + +#undef NS_DispatchToMainThread |