diff options
author | Matt A. Tobin <email@mattatobin.com> | 2022-04-20 12:37:57 -0500 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2022-04-20 12:37:57 -0500 |
commit | 7c598437c237a3a6da4f74f611bc2f043c32abc7 (patch) | |
tree | d6352487c1d32b06928f43344d41ba1ca1b0fac7 /dom | |
parent | 87dabd4e0b724bb81db3eaeefa09cfd2c5545a36 (diff) | |
download | aura-central-7c598437c237a3a6da4f74f611bc2f043c32abc7.tar.gz |
Issue #1 - Restore EME (but not the configure logic to enable it)
Diffstat (limited to 'dom')
94 files changed, 11487 insertions, 15 deletions
diff --git a/dom/base/Navigator.cpp b/dom/base/Navigator.cpp index e72e95a61..11fe91b9a 100644 --- a/dom/base/Navigator.cpp +++ b/dom/base/Navigator.cpp @@ -88,6 +88,11 @@ #endif #include "mozilla/dom/ContentChild.h" +#ifdef MOZ_EME +#include "mozilla/EMEUtils.h" +#include "mozilla/DetailedPromise.h" +#endif + namespace mozilla { namespace dom { @@ -190,6 +195,9 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Navigator) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mServiceWorkerContainer) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow) +#ifdef MOZ_EME + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaKeySystemAccessManager) +#endif #ifdef MOZ_GAMEPAD NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGamepadServiceTest) #endif @@ -250,6 +258,13 @@ Navigator::Invalidate() mServiceWorkerContainer = nullptr; +#ifdef MOZ_EME + if (mMediaKeySystemAccessManager) { + mMediaKeySystemAccessManager->Shutdown(); + mMediaKeySystemAccessManager = nullptr; + } +#endif + #ifdef MOZ_GAMEPAD if (mGamepadServiceTest) { mGamepadServiceTest->Shutdown(); @@ -1483,5 +1498,131 @@ Navigator::GetUserAgent(nsPIDOMWindowInner* aWindow, nsIURI* aURI, return siteSpecificUA->GetUserAgentForURIAndWindow(aURI, aWindow, aUserAgent); } +#ifdef MOZ_EME +static nsCString +ToCString(const nsString& aString) +{ + nsCString str("'"); + str.Append(NS_ConvertUTF16toUTF8(aString)); + str.AppendLiteral("'"); + return str; +} + +static nsCString +ToCString(const MediaKeysRequirement aValue) +{ + nsCString str("'"); + str.Append(nsDependentCString(MediaKeysRequirementValues::strings[static_cast<uint32_t>(aValue)].value)); + str.AppendLiteral("'"); + return str; +} + +static nsCString +ToCString(const MediaKeySystemMediaCapability& aValue) +{ + nsCString str; + str.AppendLiteral("{contentType="); + str.Append(ToCString(aValue.mContentType)); + str.AppendLiteral(", robustness="); + str.Append(ToCString(aValue.mRobustness)); + str.AppendLiteral("}"); + return str; +} + +template<class Type> +static nsCString +ToCString(const Sequence<Type>& aSequence) +{ + nsCString str; + str.AppendLiteral("["); + for (size_t i = 0; i < aSequence.Length(); i++) { + if (i != 0) { + str.AppendLiteral(","); + } + str.Append(ToCString(aSequence[i])); + } + str.AppendLiteral("]"); + return str; +} + +template<class Type> +static nsCString +ToCString(const Optional<Sequence<Type>>& aOptional) +{ + nsCString str; + if (aOptional.WasPassed()) { + str.Append(ToCString(aOptional.Value())); + } else { + str.AppendLiteral("[]"); + } + return str; +} + +static nsCString +ToCString(const MediaKeySystemConfiguration& aConfig) +{ + nsCString str; + str.AppendLiteral("{label="); + str.Append(ToCString(aConfig.mLabel)); + + str.AppendLiteral(", initDataTypes="); + str.Append(ToCString(aConfig.mInitDataTypes)); + + str.AppendLiteral(", audioCapabilities="); + str.Append(ToCString(aConfig.mAudioCapabilities)); + + str.AppendLiteral(", videoCapabilities="); + str.Append(ToCString(aConfig.mVideoCapabilities)); + + str.AppendLiteral(", distinctiveIdentifier="); + str.Append(ToCString(aConfig.mDistinctiveIdentifier)); + + str.AppendLiteral(", persistentState="); + str.Append(ToCString(aConfig.mPersistentState)); + + str.AppendLiteral(", sessionTypes="); + str.Append(ToCString(aConfig.mSessionTypes)); + + str.AppendLiteral("}"); + + return str; +} + +static nsCString +RequestKeySystemAccessLogString(const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfigs) +{ + nsCString str; + str.AppendPrintf("Navigator::RequestMediaKeySystemAccess(keySystem='%s' options=", + NS_ConvertUTF16toUTF8(aKeySystem).get()); + str.Append(ToCString(aConfigs)); + str.AppendLiteral(")"); + return str; +} + +already_AddRefed<Promise> +Navigator::RequestMediaKeySystemAccess(const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfigs, + ErrorResult& aRv) +{ + EME_LOG("%s", RequestKeySystemAccessLogString(aKeySystem, aConfigs).get()); + + nsCOMPtr<nsIGlobalObject> go = do_QueryInterface(mWindow); + RefPtr<DetailedPromise> promise = + DetailedPromise::Create(go, aRv, + NS_LITERAL_CSTRING("navigator.requestMediaKeySystemAccess")); + if (aRv.Failed()) { + return nullptr; + } + + if (!mMediaKeySystemAccessManager) { + mMediaKeySystemAccessManager = new MediaKeySystemAccessManager(mWindow); + } + + mMediaKeySystemAccessManager->Request(promise, aKeySystem, aConfigs); + return promise.forget(); +} +#endif + } // namespace dom } // namespace mozilla diff --git a/dom/base/Navigator.h b/dom/base/Navigator.h index a1c4794c4..2915b5069 100644 --- a/dom/base/Navigator.h +++ b/dom/base/Navigator.h @@ -17,6 +17,9 @@ #include "nsString.h" #include "nsTArray.h" #include "nsWeakPtr.h" +#ifdef MOZ_EME +#include "mozilla/dom/MediaKeySystemAccessManager.h" +#endif class nsPluginArray; class nsMimeTypeArray; @@ -235,6 +238,15 @@ public: // any, else null. static already_AddRefed<nsPIDOMWindowInner> GetWindowFromGlobal(JSObject* aGlobal); +#ifdef MOZ_EME + already_AddRefed<Promise> + RequestMediaKeySystemAccess(const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfig, + ErrorResult& aRv); +private: + RefPtr<MediaKeySystemAccessManager> mMediaKeySystemAccessManager; +#endif + private: virtual ~Navigator(); diff --git a/dom/base/nsDocument.cpp b/dom/base/nsDocument.cpp index 08c78c88d..52113f4d8 100644 --- a/dom/base/nsDocument.cpp +++ b/dom/base/nsDocument.cpp @@ -4289,6 +4289,32 @@ nsDocument::SetScopeObject(nsIGlobalObject* aGlobal) } } +#ifdef MOZ_EME +static void +CheckIfContainsEMEContent(nsISupports* aSupports, void* aContainsEME) +{ + nsCOMPtr<nsIDOMHTMLMediaElement> domMediaElem(do_QueryInterface(aSupports)); + if (domMediaElem) { + nsCOMPtr<nsIContent> content(do_QueryInterface(domMediaElem)); + MOZ_ASSERT(content, "aSupports is not a content"); + HTMLMediaElement* mediaElem = static_cast<HTMLMediaElement*>(content.get()); + bool* contains = static_cast<bool*>(aContainsEME); + if (mediaElem->GetMediaKeys()) { + *contains = true; + } + } +} + +bool +nsDocument::ContainsEMEContent() +{ + bool containsEME = false; + EnumerateActivityObservers(CheckIfContainsEMEContent, + static_cast<void*>(&containsEME)); + return containsEME; +} +#endif // MOZ_EME + static void CheckIfContainsMSEContent(nsISupports* aSupports, void* aContainsMSE) { @@ -8065,6 +8091,14 @@ nsDocument::CanSavePresentation(nsIRequest *aNewRequest) return false; } +#ifdef MOZ_EME + // Don't save presentations for documents containing EME content, so that + // CDMs reliably shutdown upon user navigation. + if (ContainsEMEContent()) { + return false; + } +#endif + // Don't save presentations for documents containing MSE content, to // reduce memory usage. if (ContainsMSEContent()) { diff --git a/dom/base/nsDocument.h b/dom/base/nsDocument.h index 024aaaa7e..010f95ae2 100644 --- a/dom/base/nsDocument.h +++ b/dom/base/nsDocument.h @@ -1115,6 +1115,10 @@ public: js::ExpandoAndGeneration mExpandoAndGeneration; +#ifdef MOZ_EME + bool ContainsEMEContent(); +#endif + bool ContainsMSEContent(); protected: diff --git a/dom/canvas/CanvasRenderingContext2D.cpp b/dom/canvas/CanvasRenderingContext2D.cpp index 7d00b2cb2..3965d40f8 100644 --- a/dom/canvas/CanvasRenderingContext2D.cpp +++ b/dom/canvas/CanvasRenderingContext2D.cpp @@ -5008,6 +5008,13 @@ CanvasRenderingContext2D::DrawImage(const CanvasImageSource& aImage, return; } +#ifdef MOZ_EME + if (video->ContainsRestrictedContent()) { + aError.Throw(NS_ERROR_NOT_AVAILABLE); + return; + } +#endif + uint16_t readyState; if (NS_SUCCEEDED(video->GetReadyState(&readyState)) && readyState < nsIDOMHTMLMediaElement::HAVE_CURRENT_DATA) { diff --git a/dom/html/HTMLMediaElement.cpp b/dom/html/HTMLMediaElement.cpp index 9b3b725c6..6774504a4 100644 --- a/dom/html/HTMLMediaElement.cpp +++ b/dom/html/HTMLMediaElement.cpp @@ -12,6 +12,9 @@ #include "mozilla/ArrayUtils.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/AsyncEventDispatcher.h" +#ifdef MOZ_EME +#include "mozilla/dom/MediaEncryptedEvent.h" +#endif #include "base/basictypes.h" #include "nsIDOMHTMLMediaElement.h" @@ -874,6 +877,9 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLMediaElement, nsGenericHTM NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTextTrackManager) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioTrackList) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVideoTrackList) +#ifdef MOZ_EME + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaKeys) +#endif NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectedVideoStreamTrack) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingPlayPromises) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END @@ -899,6 +905,9 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLMediaElement, nsGenericHTMLE NS_IMPL_CYCLE_COLLECTION_UNLINK(mTextTrackManager) NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioTrackList) NS_IMPL_CYCLE_COLLECTION_UNLINK(mVideoTrackList) +#ifdef MOZ_EME + NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaKeys) +#endif NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectedVideoStreamTrack) NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingPlayPromises) NS_IMPL_CYCLE_COLLECTION_UNLINK_END @@ -1151,6 +1160,9 @@ void HTMLMediaElement::AbortExistingLoads() mDownloadSuspendedByCache = false; mMediaInfo = MediaInfo(); mIsEncrypted = false; +#ifdef MOZ_EME + mPendingEncryptedInitData.mInitDatas.Clear(); +#endif mWaitingForKey = NOT_WAITING_FOR_KEY; mSourcePointer = nullptr; @@ -1839,6 +1851,20 @@ nsresult HTMLMediaElement::LoadResource() // Set the media element's CORS mode only when loading a resource mCORSMode = AttrValueToCORSMode(GetParsedAttr(nsGkAtoms::crossorigin)); +#ifdef MOZ_EME + bool isBlob = false; + if (mMediaKeys && + Preferences::GetBool("media.eme.mse-only", true) && + // We only want mediaSource URLs, but they are BlobURL, so we have to + // check the schema and abort if they are not MediaStream or real Blob. + (NS_FAILED(mLoadingSrc->SchemeIs(BLOBURI_SCHEME, &isBlob)) || + !isBlob || + IsMediaStreamURI(mLoadingSrc) || + IsBlobURI(mLoadingSrc))) { + return NS_ERROR_DOM_NOT_SUPPORTED_ERR; + } +#endif + HTMLMediaElement* other = LookupMediaElementURITable(mLoadingSrc); if (other && other->mDecoder) { // Clone it. @@ -2754,6 +2780,11 @@ HTMLMediaElement::CaptureStreamInternal(bool aFinishWhenEnded, if (!window) { return nullptr; } +#ifdef MOZ_EME + if (ContainsRestrictedContent()) { + return nullptr; + } +#endif if (!mOutputStreams.IsEmpty() && aGraph != mOutputStreams[0].mStream->GetInputStream()->Graph()) { @@ -3950,6 +3981,25 @@ nsresult HTMLMediaElement::FinishDecoderSetup(MediaDecoder* aDecoder, ms.mFinishWhenEnded); } +#ifdef MOZ_EME + if (mMediaKeys) { + if (mMediaKeys->GetCDMProxy()) { + mDecoder->SetCDMProxy(mMediaKeys->GetCDMProxy()); + } else { + // CDM must have crashed. + ShutdownDecoder(); + return NS_ERROR_FAILURE; + } + } + + MediaEventSource<void>* waitingForKeyProducer = mDecoder->WaitingForKeyEvent(); + // Not every decoder will produce waitingForKey events, only add ones that can + if (waitingForKeyProducer) { + mWaitingForKeyListener = waitingForKeyProducer->Connect( + AbstractThread::MainThread(), this, &HTMLMediaElement::CannotDecryptWaitingForKey); + } +#endif + if (mChannelLoader) { mChannelLoader->Done(); mChannelLoader = nullptr; @@ -4404,7 +4454,11 @@ void HTMLMediaElement::MetadataLoaded(const MediaInfo* aInfo, SetMediaInfo(*aInfo); - mIsEncrypted = aInfo->IsEncrypted(); + mIsEncrypted = aInfo->IsEncrypted() +#ifdef MOZ_EME + || mPendingEncryptedInitData.IsEncrypted() +#endif + ; mTags = aTags.forget(); mLoadedDataFired = false; ChangeReadyState(nsIDOMHTMLMediaElement::HAVE_METADATA); @@ -4429,6 +4483,14 @@ void HTMLMediaElement::MetadataLoaded(const MediaInfo* aInfo, "Encrypted content not supported outside of MSE")); return; } + +#ifdef MOZ_EME + // Dispatch a distinct 'encrypted' event for each initData we have. + for (const auto& initData : mPendingEncryptedInitData.mInitDatas) { + DispatchEncrypted(initData.mInitData, initData.mType); + } + mPendingEncryptedInitData.mInitDatas.Clear(); +#endif } mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal); @@ -5362,12 +5424,29 @@ void HTMLMediaElement::SuspendOrResumeElement(bool aPauseElement, bool aSuspendE UpdateSrcMediaStreamPlaying(); UpdateAudioChannelPlayingState(); if (aPauseElement) { +#ifdef MOZ_EME + // For EME content, we may force destruction of the CDM client (and CDM + // instance if this is the last client for that CDM instance) and + // the CDM's decoder. This ensures the CDM gets reliable and prompt + // shutdown notifications, as it may have book-keeping it needs + // to do on shutdown. + if (mMediaKeys) { + mMediaKeys->Shutdown(); + mMediaKeys = nullptr; + if (mDecoder) { + ShutdownDecoder(); + } + } +#endif if (mDecoder) { mDecoder->Pause(); mDecoder->Suspend(); } mEventDeliveryPaused = aSuspendEvents; } else { +#ifdef MOZ_EME + MOZ_ASSERT(!mMediaKeys); +#endif if (mDecoder) { mDecoder->Resume(); if (!mPaused && !mDecoder->IsEnded()) { @@ -5404,6 +5483,17 @@ void HTMLMediaElement::NotifyOwnerDocumentActivityChanged() bool pauseElement = ShouldElementBePaused(); SuspendOrResumeElement(pauseElement, !IsActive()); +#ifdef MOZ_EME + // If the owning document has become inactive we should shutdown the CDM. + if (!OwnerDoc()->IsCurrentActiveDocument() && mMediaKeys) { + mMediaKeys->Shutdown(); + mMediaKeys = nullptr; + if (mDecoder) { + ShutdownDecoder(); + } + } +#endif + AddRemoveSelfReference(); } @@ -6164,6 +6254,236 @@ HTMLMediaElement::OnVisibilityChange(Visibility aNewVisibility) } +#ifdef MOZ_EME +MediaKeys* +HTMLMediaElement::GetMediaKeys() const +{ + return mMediaKeys; +} + +bool +HTMLMediaElement::ContainsRestrictedContent() +{ + return GetMediaKeys() != nullptr; +} + +already_AddRefed<Promise> +HTMLMediaElement::SetMediaKeys(mozilla::dom::MediaKeys* aMediaKeys, + ErrorResult& aRv) +{ + LOG(LogLevel::Debug, ("%p SetMediaKeys(%p) mMediaKeys=%p mDecoder=%p", + this, aMediaKeys, mMediaKeys.get(), mDecoder.get())); + + if (MozAudioCaptured()) { + aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return nullptr; + } + + nsCOMPtr<nsIGlobalObject> global = + do_QueryInterface(OwnerDoc()->GetInnerWindow()); + if (!global) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + RefPtr<DetailedPromise> promise = DetailedPromise::Create(global, aRv, + NS_LITERAL_CSTRING("HTMLMediaElement.setMediaKeys")); + if (aRv.Failed()) { + return nullptr; + } + + // 1. If mediaKeys and the mediaKeys attribute are the same object, + // return a resolved promise. + if (mMediaKeys == aMediaKeys) { + promise->MaybeResolveWithUndefined(); + return promise.forget(); + } + + // Note: Our attaching code is synchronous, so we can skip the following steps. + + // 2. If this object's attaching media keys value is true, return a + // promise rejected with a new DOMException whose name is InvalidStateError. + // 3. Let this object's attaching media keys value be true. + // 4. Let promise be a new promise. + // 5. Run the following steps in parallel: + + // 5.1 If mediaKeys is not null, CDM instance represented by mediaKeys is + // already in use by another media element, and the user agent is unable + // to use it with this element, let this object's attaching media keys + // value be false and reject promise with a new DOMException whose name + // is QuotaExceededError. + if (aMediaKeys && aMediaKeys->IsBoundToMediaElement()) { + promise->MaybeReject(NS_ERROR_DOM_QUOTA_EXCEEDED_ERR, + NS_LITERAL_CSTRING("MediaKeys object is already bound to another HTMLMediaElement")); + return promise.forget(); + } + + // 5.2 If the mediaKeys attribute is not null, run the following steps: + if (mMediaKeys) { + // 5.2.1 If the user agent or CDM do not support removing the association, + // let this object's attaching media keys value be false and reject promise + // with a new DOMException whose name is NotSupportedError. + + // 5.2.2 If the association cannot currently be removed, let this object's + // attaching media keys value be false and reject promise with a new + // DOMException whose name is InvalidStateError. + if (mDecoder) { + // We don't support swapping out the MediaKeys once we've started to + // setup the playback pipeline. Note this also means we don't need to worry + // about handling disassociating the MediaKeys from the MediaDecoder. + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Can't change MediaKeys on HTMLMediaElement after load has started")); + return promise.forget(); + } + + // 5.2.3 Stop using the CDM instance represented by the mediaKeys attribute + // to decrypt media data and remove the association with the media element. + mMediaKeys->Unbind(); + mMediaKeys = nullptr; + + // 5.2.4 If the preceding step failed, let this object's attaching media + // keys value be false and reject promise with a new DOMException whose + // name is the appropriate error name. + } + + // 5.3. If mediaKeys is not null, run the following steps: + if (aMediaKeys) { + if (!aMediaKeys->GetCDMProxy()) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("CDM crashed before binding MediaKeys object to HTMLMediaElement")); + return promise.forget(); + } + + // 5.3.1 Associate the CDM instance represented by mediaKeys with the + // media element for decrypting media data. + if (NS_FAILED(aMediaKeys->Bind(this))) { + // 5.3.2 If the preceding step failed, run the following steps: + // 5.3.2.1 Set the mediaKeys attribute to null. + mMediaKeys = nullptr; + // 5.3.2.2 Let this object's attaching media keys value be false. + // 5.3.2.3 Reject promise with a new DOMException whose name is + // the appropriate error name. + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Failed to bind MediaKeys object to HTMLMediaElement")); + return promise.forget(); + } + // 5.3.3 Queue a task to run the "Attempt to Resume Playback If Necessary" + // algorithm on the media element. + // Note: Setting the CDMProxy on the MediaDecoder will unblock playback. + if (mDecoder) { + mDecoder->SetCDMProxy(aMediaKeys->GetCDMProxy()); + } + } + + // 5.4 Set the mediaKeys attribute to mediaKeys. + mMediaKeys = aMediaKeys; + + // 5.5 Let this object's attaching media keys value be false. + + // 5.6 Resolve promise. + promise->MaybeResolveWithUndefined(); + + // 6. Return promise. + return promise.forget(); +} + +EventHandlerNonNull* +HTMLMediaElement::GetOnencrypted() +{ + return EventTarget::GetEventHandler(nsGkAtoms::onencrypted, EmptyString()); +} + +void +HTMLMediaElement::SetOnencrypted(EventHandlerNonNull* aCallback) +{ + EventTarget::SetEventHandler(nsGkAtoms::onencrypted, EmptyString(), aCallback); +} + +EventHandlerNonNull* +HTMLMediaElement::GetOnwaitingforkey() +{ + return EventTarget::GetEventHandler(nsGkAtoms::onwaitingforkey, EmptyString()); +} + +void +HTMLMediaElement::SetOnwaitingforkey(EventHandlerNonNull* aCallback) +{ + EventTarget::SetEventHandler(nsGkAtoms::onwaitingforkey, EmptyString(), aCallback); +} + +void +HTMLMediaElement::DispatchEncrypted(const nsTArray<uint8_t>& aInitData, + const nsAString& aInitDataType) +{ + LOG(LogLevel::Debug, + ("%p DispatchEncrypted initDataType='%s'", + this, NS_ConvertUTF16toUTF8(aInitDataType).get())); + + if (mReadyState == nsIDOMHTMLMediaElement::HAVE_NOTHING) { + // Ready state not HAVE_METADATA (yet), don't dispatch encrypted now. + // Queueing for later dispatch in MetadataLoaded. + mPendingEncryptedInitData.AddInitData(aInitDataType, aInitData); + return; + } + + RefPtr<MediaEncryptedEvent> event; + if (IsCORSSameOrigin()) { + event = MediaEncryptedEvent::Constructor(this, aInitDataType, aInitData); + } else { + event = MediaEncryptedEvent::Constructor(this); + } + + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(this, event); + asyncDispatcher->PostDOMEvent(); +} + +bool +HTMLMediaElement::IsEventAttributeName(nsIAtom* aName) +{ + return aName == nsGkAtoms::onencrypted || + nsGenericHTMLElement::IsEventAttributeName(aName); +} + +already_AddRefed<nsIPrincipal> +HTMLMediaElement::GetTopLevelPrincipal() +{ + RefPtr<nsIPrincipal> principal; + nsCOMPtr<nsPIDOMWindowInner> window = OwnerDoc()->GetInnerWindow(); + if (!window) { + return nullptr; + } + // XXXkhuey better hope we always have an outer ... + nsCOMPtr<nsPIDOMWindowOuter> top = window->GetOuterWindow()->GetTop(); + if (!top) { + return nullptr; + } + nsIDocument* doc = top->GetExtantDoc(); + if (!doc) { + return nullptr; + } + principal = doc->NodePrincipal(); + return principal.forget(); +} + +void +HTMLMediaElement::CannotDecryptWaitingForKey() +{ + LOG(LogLevel::Debug, ("%p, CannotDecryptWaitingForKey()", this)); + + // http://w3c.github.io/encrypted-media/#wait-for-key + // 7.3.4 Queue a "waitingforkey" Event + // 1. Let the media element be the specified HTMLMediaElement object. + // 2. If the media element's waiting for key value is true, abort these steps. + if (mWaitingForKey == NOT_WAITING_FOR_KEY) { + // 3. Set the media element's waiting for key value to true. + // Note: algorithm continues in UpdateReadyStateInternal() when all decoded + // data enqueued in the MDSM is consumed. + mWaitingForKey = WAITING_FOR_KEY; + UpdateReadyStateInternal(); + } +} +#endif //MOZ_EME + NS_IMETHODIMP HTMLMediaElement::WindowAudioCaptureChanged(bool aCapture) { MOZ_ASSERT(mAudioChannelAgent); diff --git a/dom/html/HTMLMediaElement.h b/dom/html/HTMLMediaElement.h index 23799b574..bda9924a6 100644 --- a/dom/html/HTMLMediaElement.h +++ b/dom/html/HTMLMediaElement.h @@ -18,6 +18,9 @@ #include "mozilla/dom/TextTrackManager.h" #include "mozilla/WeakPtr.h" #include "MediaDecoder.h" +#ifdef MOZ_EME +#include "mozilla/dom/MediaKeys.h" +#endif #include "mozilla/StateWatching.h" #include "nsGkAtoms.h" #include "PrincipalChangeObserver.h" @@ -613,6 +616,30 @@ public: // XPCOM MozPreservesPitch() is OK +#ifdef MOZ_EME + MediaKeys* GetMediaKeys() const; + + already_AddRefed<Promise> SetMediaKeys(MediaKeys* mediaKeys, + ErrorResult& aRv); + + mozilla::dom::EventHandlerNonNull* GetOnencrypted(); + void SetOnencrypted(mozilla::dom::EventHandlerNonNull* aCallback); + + mozilla::dom::EventHandlerNonNull* GetOnwaitingforkey(); + void SetOnwaitingforkey(mozilla::dom::EventHandlerNonNull* aCallback); + + void DispatchEncrypted(const nsTArray<uint8_t>& aInitData, + const nsAString& aInitDataType) override; + + bool IsEventAttributeName(nsIAtom* aName) override; + + // Returns the principal of the "top level" document; the origin displayed + // in the URL bar of the browser window. + already_AddRefed<nsIPrincipal> GetTopLevelPrincipal(); + + bool ContainsRestrictedContent(); +#endif // MOZ_EME + void CannotDecryptWaitingForKey(); bool MozAutoplayEnabled() const @@ -1447,6 +1474,11 @@ protected: // Timer used for updating progress events. nsCOMPtr<nsITimer> mProgressTimer; +#ifdef MOZ_EME + // Encrypted Media Extension media keys. + RefPtr<MediaKeys> mMediaKeys; +#endif + // Stores the time at the start of the current 'played' range. double mCurrentPlayRangeStart; @@ -1600,6 +1632,11 @@ protected: // Listens for waitingForKey events from the owned decoder. MediaEventListener mWaitingForKeyListener; +#ifdef MOZ_EME + // Init Data that needs to be sent in 'encrypted' events in MetadataLoaded(). + EncryptionInfo mPendingEncryptedInitData; +#endif + // True if the media's channel's download has been suspended. Watchable<bool> mDownloadSuspendedByCache; diff --git a/dom/media/AbstractMediaDecoder.h b/dom/media/AbstractMediaDecoder.h index 01722f8b1..6babcce17 100644 --- a/dom/media/AbstractMediaDecoder.h +++ b/dom/media/AbstractMediaDecoder.h @@ -1,4 +1,5 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ /* 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/. */ @@ -30,6 +31,9 @@ class MediaResource; class ReentrantMonitor; class VideoFrameContainer; class MediaDecoderOwner; +#ifdef MOZ_EME +class CDMProxy; +#endif typedef nsDataHashtable<nsCStringHashKey, nsCString> MetadataTags; diff --git a/dom/media/MediaDecoder.cpp b/dom/media/MediaDecoder.cpp index ebc694b47..10b97f7e6 100644 --- a/dom/media/MediaDecoder.cpp +++ b/dom/media/MediaDecoder.cpp @@ -386,6 +386,9 @@ MediaDecoder::MediaDecoder(MediaDecoderOwner* aOwner) , mLogicalPosition(0.0) , mDuration(std::numeric_limits<double>::quiet_NaN()) , mResourceCallback(new ResourceCallback()) +#ifdef MOZ_EME + , mCDMProxyPromise(mCDMProxyPromiseHolder.Ensure(__func__)) +#endif , mIgnoreProgressData(false) , mInfiniteStream(false) , mOwner(aOwner) @@ -470,6 +473,10 @@ MediaDecoder::Shutdown() mResourceCallback->Disconnect(); +#ifdef MOZ_EME + mCDMProxyPromiseHolder.RejectIfExists(true, __func__); +#endif + DiscardOngoingSeekIfExists(); // This changes the decoder state to SHUTDOWN and does other things @@ -1540,6 +1547,23 @@ MediaDecoder::CanPlayThrough() return GetStatistics().CanPlayThrough(); } +#ifdef MOZ_EME +RefPtr<MediaDecoder::CDMProxyPromise> +MediaDecoder::RequestCDMProxy() const +{ + return mCDMProxyPromise; +} + +void +MediaDecoder::SetCDMProxy(CDMProxy* aProxy) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aProxy); + + mCDMProxyPromiseHolder.ResolveIfExists(aProxy, __func__); +} +#endif + bool MediaDecoder::IsOpusEnabled() { diff --git a/dom/media/MediaDecoder.h b/dom/media/MediaDecoder.h index 5bc9e5000..298552433 100644 --- a/dom/media/MediaDecoder.h +++ b/dom/media/MediaDecoder.h @@ -1,4 +1,5 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ /* 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/. */ @@ -8,6 +9,10 @@ #include "mozilla/Atomics.h" +#ifdef MOZ_EME +#include "mozilla/CDMProxy.h" +#endif + #include "mozilla/MozPromise.h" #include "mozilla/ReentrantMonitor.h" #include "mozilla/StateMirroring.h" @@ -431,6 +436,16 @@ private: MediaDecoderOwner* GetOwner() const override; +#ifdef MOZ_EME + typedef MozPromise<RefPtr<CDMProxy>, bool /* aIgnored */, /* IsExclusive = */ true> CDMProxyPromise; + + // Resolved when a CDMProxy is available and the capabilities are known or + // rejected when this decoder is about to shut down. + RefPtr<CDMProxyPromise> RequestCDMProxy() const; + + void SetCDMProxy(CDMProxy* aProxy); +#endif + static bool IsOggEnabled(); static bool IsOpusEnabled(); static bool IsWaveEnabled(); @@ -580,6 +595,11 @@ private: RefPtr<ResourceCallback> mResourceCallback; +#ifdef MOZ_EME + MozPromiseHolder<CDMProxyPromise> mCDMProxyPromiseHolder; + RefPtr<CDMProxyPromise> mCDMProxyPromise; +#endif + protected: // The promise resolving/rejection is queued as a "micro-task" which will be // handled immediately after the current JS task and before any pending JS diff --git a/dom/media/MediaDecoderOwner.h b/dom/media/MediaDecoderOwner.h index 649d2996f..f993b4324 100644 --- a/dom/media/MediaDecoderOwner.h +++ b/dom/media/MediaDecoderOwner.h @@ -142,6 +142,14 @@ public: // The decoder owner should call Shutdown() on the decoder and drop the // reference to the decoder to prevent further calls into the decoder. virtual void NotifyXPCOMShutdown() = 0; + +#ifdef MOZ_EME + // Dispatches a "encrypted" event to the HTMLMediaElement, with the + // provided init data. Actual dispatch may be delayed until HAVE_METADATA. + // Main thread only. + virtual void DispatchEncrypted(const nsTArray<uint8_t>& aInitData, + const nsAString& aInitDataType) = 0; +#endif }; } // namespace mozilla diff --git a/dom/media/MediaDecoderReader.h b/dom/media/MediaDecoderReader.h index 3a3e2ceb8..dd406ed90 100644 --- a/dom/media/MediaDecoderReader.h +++ b/dom/media/MediaDecoderReader.h @@ -24,6 +24,9 @@ namespace mozilla { +#ifdef MOZ_EME +class CDMProxy; +#endif class MediaDecoderReader; struct WaitForDataRejectValue @@ -185,6 +188,10 @@ public: // when to call SetIdle(). virtual void SetIdle() {} +#ifdef MOZ_EME + virtual void SetCDMProxy(CDMProxy* aProxy) {} +#endif + // Tell the reader that the data decoded are not for direct playback, so it // can accept more files, in particular those which have more channels than // available in the audio output. diff --git a/dom/media/MediaDecoderReaderWrapper.h b/dom/media/MediaDecoderReaderWrapper.h index c85c93d44..a0f845538 100644 --- a/dom/media/MediaDecoderReaderWrapper.h +++ b/dom/media/MediaDecoderReaderWrapper.h @@ -112,6 +112,10 @@ public: return mReader->CanonicalBuffered(); } +#ifdef MOZ_EME + void SetCDMProxy(CDMProxy* aProxy) { mReader->SetCDMProxy(aProxy); } +#endif + void SetVideoBlankDecode(bool aIsBlankDecode); private: diff --git a/dom/media/MediaDecoderStateMachine.cpp b/dom/media/MediaDecoderStateMachine.cpp index 04916d2f5..c5113bed4 100644 --- a/dom/media/MediaDecoderStateMachine.cpp +++ b/dom/media/MediaDecoderStateMachine.cpp @@ -1,4 +1,5 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ /* 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/. */ @@ -1227,7 +1228,33 @@ DecodeMetadataState::OnMetadataRead(MetadataHolder* aMetadata) mMaster->GetAmpleVideoFrames()); } - SetState<DecodingFirstFrameState>(SeekJob{}); + // In general, we wait until we know the duration before notifying the decoder. + // However, we notify unconditionally in this case without waiting for the start + // time, since the caller might be waiting on metadataloaded to be fired before + // feeding in the CDM, which we need to decode the first frame (and + // thus get the metadata). We could fix this if we could compute the start + // time by demuxing without necessaring decoding. + bool waitingForCDM = +#ifdef MOZ_EME + mMaster->Info().IsEncrypted() && !mMaster->mCDMProxy; +#else + false; +#endif + + mMaster->mNotifyMetadataBeforeFirstFrame = + mMaster->mDuration.Ref().isSome() || waitingForCDM; + + if (mMaster->mNotifyMetadataBeforeFirstFrame) { + mMaster->EnqueueLoadedMetadataEvent(); + } + + if (waitingForCDM) { + // Metadata parsing was successful but we're still waiting for CDM caps + // to become available so that we can build the correct decryptor/decoder. + SetState<WaitForCDMState>(); + } else { + SetState<DecodingFirstFrameState>(SeekJob{}); + } } void @@ -1236,6 +1263,9 @@ DormantState::HandlePlayStateChanged(MediaDecoder::PlayState aPlayState) { if (aPlayState == MediaDecoder::PLAY_STATE_PLAYING) { // Exit dormant when the user wants to play. +#ifdef MOZ_EME + MOZ_ASSERT(!Info().IsEncrypted() || mMaster->mCDMProxy); +#endif MOZ_ASSERT(mMaster->mSentFirstFrameLoadedEvent); SetState<SeekingState>(Move(mPendingSeek), EventVisibility::Suppressed); } @@ -1548,6 +1578,10 @@ ShutdownState::Enter() // dispose of the timer. master->mVideoDecodeSuspendTimer.Reset(); +#ifdef MOZ_EME + master->mCDMProxyPromise.DisconnectIfExists(); +#endif + if (master->IsPlaying()) { master->StopPlayback(); } @@ -2100,6 +2134,13 @@ nsresult MediaDecoderStateMachine::Init(MediaDecoder* aDecoder) mMediaSink = CreateMediaSink(mAudioCaptured); +#ifdef MOZ_EME + mCDMProxyPromise.Begin(aDecoder->RequestCDMProxy()->Then( + OwnerThread(), __func__, this, + &MediaDecoderStateMachine::OnCDMProxyReady, + &MediaDecoderStateMachine::OnCDMProxyNotReady)); +#endif + nsresult rv = mReader->Init(); NS_ENSURE_SUCCESS(rv, rv); @@ -3074,6 +3115,25 @@ void MediaDecoderStateMachine::OnMediaSinkAudioError(nsresult aResult) DecodeError(MediaResult(NS_ERROR_DOM_MEDIA_MEDIASINK_ERR, __func__)); } +#ifdef MOZ_EME +void +MediaDecoderStateMachine::OnCDMProxyReady(RefPtr<CDMProxy> aProxy) +{ + MOZ_ASSERT(OnTaskQueue()); + mCDMProxyPromise.Complete(); + mCDMProxy = aProxy; + mReader->SetCDMProxy(aProxy); + mStateObj->HandleCDMProxyReady(); +} + +void +MediaDecoderStateMachine::OnCDMProxyNotReady() +{ + MOZ_ASSERT(OnTaskQueue()); + mCDMProxyPromise.Complete(); +} +#endif + void MediaDecoderStateMachine::SetAudioCaptured(bool aCaptured) { diff --git a/dom/media/MediaDecoderStateMachine.h b/dom/media/MediaDecoderStateMachine.h index 6391c8574..f04f34983 100644 --- a/dom/media/MediaDecoderStateMachine.h +++ b/dom/media/MediaDecoderStateMachine.h @@ -1,4 +1,5 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ /* 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/. */ @@ -767,6 +768,13 @@ private: // Playback will not start when audio is offloading. bool mAudioOffloading; +#ifdef MOZ_EME + void OnCDMProxyReady(RefPtr<CDMProxy> aProxy); + void OnCDMProxyNotReady(); + RefPtr<CDMProxy> mCDMProxy; + MozPromiseRequestHolder<MediaDecoder::CDMProxyPromise> mCDMProxyPromise; +#endif + private: // The buffered range. Mirrored from the decoder thread. Mirror<media::TimeIntervals> mBuffered; diff --git a/dom/media/MediaFormatReader.cpp b/dom/media/MediaFormatReader.cpp index 7775f0d9b..98ba57a6e 100644 --- a/dom/media/MediaFormatReader.cpp +++ b/dom/media/MediaFormatReader.cpp @@ -3,6 +3,10 @@ * 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/. */ +#ifdef MOZ_EME +#include "mozilla/CDMProxy.h" +#endif + #include "mozilla/ClearOnShutdown.h" #include "mozilla/dom/HTMLMediaElement.h" #include "mozilla/Preferences.h" @@ -347,7 +351,12 @@ MediaFormatReader::DecoderFactory::DoCreateDecoder(TrackType aTrack) if (!mOwner->mPlatform) { mOwner->mPlatform = new PDMFactory(); if (mOwner->IsEncrypted()) { +#ifdef MOZ_EME + MOZ_ASSERT(mOwner->mCDMProxy); + mOwner->mPlatform->SetCDMProxy(mOwner->mCDMProxy); +#else return MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "EME not supported"); +#endif } } @@ -577,10 +586,54 @@ MediaFormatReader::InitInternal() return NS_OK; } +#ifdef MOZ_EME +class DispatchKeyNeededEvent : public Runnable { +public: + DispatchKeyNeededEvent(AbstractMediaDecoder* aDecoder, + nsTArray<uint8_t>& aInitData, + const nsString& aInitDataType) + : mDecoder(aDecoder) + , mInitData(aInitData) + , mInitDataType(aInitDataType) + { + } + NS_IMETHOD Run() override { + // Note: Null check the owner, as the decoder could have been shutdown + // since this event was dispatched. + MediaDecoderOwner* owner = mDecoder->GetOwner(); + if (owner) { + owner->DispatchEncrypted(mInitData, mInitDataType); + } + mDecoder = nullptr; + return NS_OK; + } +private: + RefPtr<AbstractMediaDecoder> mDecoder; + nsTArray<uint8_t> mInitData; + nsString mInitDataType; +}; + +void +MediaFormatReader::SetCDMProxy(CDMProxy* aProxy) +{ + RefPtr<CDMProxy> proxy = aProxy; + RefPtr<MediaFormatReader> self = this; + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction([=] () { + MOZ_ASSERT(self->OnTaskQueue()); + self->mCDMProxy = proxy; + }); + OwnerThread()->Dispatch(r.forget()); +} +#endif // MOZ_EME + bool MediaFormatReader::IsWaitingOnCDMResource() { - /* STUB */ + MOZ_ASSERT(OnTaskQueue()); +#ifdef MOZ_EME + return IsEncrypted() && !mCDMProxy; +#else return false; +#endif } RefPtr<MediaDecoderReader::MetadataPromise> @@ -687,6 +740,13 @@ MediaFormatReader::OnDemuxerInitDone(nsresult) UniquePtr<EncryptionInfo> crypto = mDemuxer->GetCrypto(); if (mDecoder && crypto && crypto->IsEncrypted()) { +#ifdef MOZ_EME + // Try and dispatch 'encrypted'. Won't go if ready state still HAVE_NOTHING. + for (uint32_t i = 0; i < crypto->mInitDatas.Length(); i++) { + NS_DispatchToMainThread( + new DispatchKeyNeededEvent(mDecoder, crypto->mInitDatas[i].mInitData, crypto->mInitDatas[i].mType)); + } +#endif mInfo.mCrypto = *crypto; } diff --git a/dom/media/MediaFormatReader.h b/dom/media/MediaFormatReader.h index 3da080450..be0b7cd17 100644 --- a/dom/media/MediaFormatReader.h +++ b/dom/media/MediaFormatReader.h @@ -1,4 +1,5 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ /* 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/. */ @@ -19,6 +20,10 @@ namespace mozilla { +#ifdef MOZ_EME +class CDMProxy; +#endif + class MediaFormatReader final : public MediaDecoderReader { typedef TrackInfo::TrackType TrackType; @@ -88,6 +93,10 @@ public: return mTrackDemuxersMayBlock; } +#ifdef MOZ_EME + void SetCDMProxy(CDMProxy* aProxy) override; +#endif + // Returns a string describing the state of the decoder data. // Used for debugging purposes. void GetMozDebugReaderData(nsAString& aString); @@ -579,6 +588,10 @@ private: RefPtr<VideoFrameContainer> mVideoFrameContainer; layers::ImageContainer* GetImageContainer(); +#ifdef MOZ_EME + RefPtr<CDMProxy> mCDMProxy; +#endif + RefPtr<GMPCrashHelper> mCrashHelper; void SetBlankDecode(TrackType aTrack, bool aIsBlankDecode); diff --git a/dom/media/eme/CDMCaps.cpp b/dom/media/eme/CDMCaps.cpp new file mode 100644 index 000000000..a30fb59ac --- /dev/null +++ b/dom/media/eme/CDMCaps.cpp @@ -0,0 +1,169 @@ +/* -*- Mode: C++; tab-width: 8; 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 "mozilla/CDMCaps.h" +#include "mozilla/EMEUtils.h" +#include "nsThreadUtils.h" +#include "SamplesWaitingForKey.h" + +namespace mozilla { + +CDMCaps::CDMCaps() + : mMonitor("CDMCaps") +{ +} + +CDMCaps::~CDMCaps() +{ +} + +void +CDMCaps::Lock() +{ + mMonitor.Lock(); +} + +void +CDMCaps::Unlock() +{ + mMonitor.Unlock(); +} + +CDMCaps::AutoLock::AutoLock(CDMCaps& aInstance) + : mData(aInstance) +{ + mData.Lock(); +} + +CDMCaps::AutoLock::~AutoLock() +{ + mData.Unlock(); +} + +// Keys with MediaKeyStatus::Usable, MediaKeyStatus::Output_downscaled, +// or MediaKeyStatus::Output_restricted status can be used by the CDM +// to decrypt or decrypt-and-decode samples. +static bool +IsUsableStatus(dom::MediaKeyStatus aStatus) +{ + return aStatus == dom::MediaKeyStatus::Usable || + aStatus == dom::MediaKeyStatus::Output_restricted || + aStatus == dom::MediaKeyStatus::Output_downscaled; +} + +bool +CDMCaps::AutoLock::IsKeyUsable(const CencKeyId& aKeyId) +{ + mData.mMonitor.AssertCurrentThreadOwns(); + for (const KeyStatus& keyStatus : mData.mKeyStatuses) { + if (keyStatus.mId == aKeyId) { + return IsUsableStatus(keyStatus.mStatus); + } + } + return false; +} + +bool +CDMCaps::AutoLock::SetKeyStatus(const CencKeyId& aKeyId, + const nsString& aSessionId, + const dom::Optional<dom::MediaKeyStatus>& aStatus) +{ + mData.mMonitor.AssertCurrentThreadOwns(); + + if (!aStatus.WasPassed()) { + // Called from ForgetKeyStatus. + // Return true if the element is found to notify key changes. + return mData.mKeyStatuses.RemoveElement(KeyStatus(aKeyId, + aSessionId, + dom::MediaKeyStatus::Internal_error)); + } + + KeyStatus key(aKeyId, aSessionId, aStatus.Value()); + auto index = mData.mKeyStatuses.IndexOf(key); + if (index != mData.mKeyStatuses.NoIndex) { + if (mData.mKeyStatuses[index].mStatus == aStatus.Value()) { + // No change. + return false; + } + auto oldStatus = mData.mKeyStatuses[index].mStatus; + mData.mKeyStatuses[index].mStatus = aStatus.Value(); + // The old key status was one for which we can decrypt media. We don't + // need to do the "notify usable" step below, as it should be impossible + // for us to have anything waiting on this key to become usable, since it + // was already usable. + if (IsUsableStatus(oldStatus)) { + return true; + } + } else { + mData.mKeyStatuses.AppendElement(key); + } + + // Only call NotifyUsable() for a key when we are going from non-usable + // to usable state. + if (!IsUsableStatus(aStatus.Value())) { + return true; + } + + auto& waiters = mData.mWaitForKeys; + size_t i = 0; + while (i < waiters.Length()) { + auto& w = waiters[i]; + if (w.mKeyId == aKeyId) { + w.mListener->NotifyUsable(aKeyId); + waiters.RemoveElementAt(i); + } else { + i++; + } + } + return true; +} + +void +CDMCaps::AutoLock::NotifyWhenKeyIdUsable(const CencKeyId& aKey, + SamplesWaitingForKey* aListener) +{ + mData.mMonitor.AssertCurrentThreadOwns(); + MOZ_ASSERT(!IsKeyUsable(aKey)); + MOZ_ASSERT(aListener); + mData.mWaitForKeys.AppendElement(WaitForKeys(aKey, aListener)); +} + +void +CDMCaps::AutoLock::GetKeyStatusesForSession(const nsAString& aSessionId, + nsTArray<KeyStatus>& aOutKeyStatuses) +{ + for (const KeyStatus& keyStatus : mData.mKeyStatuses) { + if (keyStatus.mSessionId.Equals(aSessionId)) { + aOutKeyStatuses.AppendElement(keyStatus); + } + } +} + +void +CDMCaps::AutoLock::GetSessionIdsForKeyId(const CencKeyId& aKeyId, + nsTArray<nsCString>& aOutSessionIds) +{ + for (const KeyStatus& keyStatus : mData.mKeyStatuses) { + if (keyStatus.mId == aKeyId) { + aOutSessionIds.AppendElement(NS_ConvertUTF16toUTF8(keyStatus.mSessionId)); + } + } +} + +bool +CDMCaps::AutoLock::RemoveKeysForSession(const nsString& aSessionId) +{ + bool changed = false; + nsTArray<KeyStatus> statuses; + GetKeyStatusesForSession(aSessionId, statuses); + for (const KeyStatus& status : statuses) { + changed |= SetKeyStatus(status.mId, + aSessionId, + dom::Optional<dom::MediaKeyStatus>()); + } + return changed; +} + +} // namespace mozilla diff --git a/dom/media/eme/CDMCaps.h b/dom/media/eme/CDMCaps.h new file mode 100644 index 000000000..cb4b5e291 --- /dev/null +++ b/dom/media/eme/CDMCaps.h @@ -0,0 +1,112 @@ +/* -*- Mode: C++; tab-width: 8; 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/. */ + +#ifndef CDMCaps_h_ +#define CDMCaps_h_ + +#include "gmp-decryption.h" +#include "nsIThread.h" +#include "nsTArray.h" +#include "nsString.h" +#include "SamplesWaitingForKey.h" + +#include "mozilla/Monitor.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/MediaKeyStatusMapBinding.h" // For MediaKeyStatus +#include "mozilla/dom/BindingDeclarations.h" // For Optional + +namespace mozilla { + +// CDM capabilities; what keys a CDMProxy can use. +// Must be locked to access state. +class CDMCaps { +public: + CDMCaps(); + ~CDMCaps(); + + struct KeyStatus { + KeyStatus(const CencKeyId& aId, + const nsString& aSessionId, + dom::MediaKeyStatus aStatus) + : mId(aId) + , mSessionId(aSessionId) + , mStatus(aStatus) + {} + KeyStatus(const KeyStatus& aOther) + : mId(aOther.mId) + , mSessionId(aOther.mSessionId) + , mStatus(aOther.mStatus) + {} + bool operator==(const KeyStatus& aOther) const { + return mId == aOther.mId && + mSessionId == aOther.mSessionId; + }; + + CencKeyId mId; + nsString mSessionId; + dom::MediaKeyStatus mStatus; + }; + + // Locks the CDMCaps. It must be locked to access its shared state. + // Threadsafe when locked. + class MOZ_STACK_CLASS AutoLock { + public: + explicit AutoLock(CDMCaps& aKeyCaps); + ~AutoLock(); + + bool IsKeyUsable(const CencKeyId& aKeyId); + + // Returns true if key status changed, + // i.e. the key status changed from usable to expired. + bool SetKeyStatus(const CencKeyId& aKeyId, + const nsString& aSessionId, + const dom::Optional<dom::MediaKeyStatus>& aStatus); + + void GetKeyStatusesForSession(const nsAString& aSessionId, + nsTArray<KeyStatus>& aOutKeyStatuses); + + void GetSessionIdsForKeyId(const CencKeyId& aKeyId, + nsTArray<nsCString>& aOutSessionIds); + + // Ensures all keys for a session are marked as 'unknown', i.e. removed. + // Returns true if a key status was changed. + bool RemoveKeysForSession(const nsString& aSessionId); + + // Notifies the SamplesWaitingForKey when key become usable. + void NotifyWhenKeyIdUsable(const CencKeyId& aKey, + SamplesWaitingForKey* aSamplesWaiting); + private: + // Not taking a strong ref, since this should be allocated on the stack. + CDMCaps& mData; + }; + +private: + void Lock(); + void Unlock(); + + struct WaitForKeys { + WaitForKeys(const CencKeyId& aKeyId, + SamplesWaitingForKey* aListener) + : mKeyId(aKeyId) + , mListener(aListener) + {} + CencKeyId mKeyId; + RefPtr<SamplesWaitingForKey> mListener; + }; + + Monitor mMonitor; + + nsTArray<KeyStatus> mKeyStatuses; + + nsTArray<WaitForKeys> mWaitForKeys; + + // It is not safe to copy this object. + CDMCaps(const CDMCaps&) = delete; + CDMCaps& operator=(const CDMCaps&) = delete; +}; + +} // namespace mozilla + +#endif diff --git a/dom/media/eme/CDMProxy.h b/dom/media/eme/CDMProxy.h new file mode 100644 index 000000000..a9e783f50 --- /dev/null +++ b/dom/media/eme/CDMProxy.h @@ -0,0 +1,278 @@ +/* -*- Mode: C++; tab-width: 8; 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/. */ + +#ifndef CDMProxy_h_ +#define CDMProxy_h_ + +#include "mozilla/CDMCaps.h" +#include "mozilla/MozPromise.h" + +#include "mozilla/dom/MediaKeyMessageEvent.h" +#include "mozilla/dom/MediaKeys.h" + +#include "nsIThread.h" + +namespace mozilla { +class MediaRawData; + +enum DecryptStatus { + Ok = 0, + GenericErr = 1, + NoKeyErr = 2, + AbortedErr = 3, +}; + +struct DecryptResult { + DecryptResult(DecryptStatus aStatus, MediaRawData* aSample) + : mStatus(aStatus) + , mSample(aSample) + {} + DecryptStatus mStatus; + RefPtr<MediaRawData> mSample; +}; + +class CDMKeyInfo { +public: + explicit CDMKeyInfo(const nsTArray<uint8_t>& aKeyId) + : mKeyId(aKeyId) + , mStatus() + {} + + CDMKeyInfo(const nsTArray<uint8_t>& aKeyId, + const dom::Optional<dom::MediaKeyStatus>& aStatus) + : mKeyId(aKeyId) + , mStatus(aStatus.Value()) + {} + + // The copy-ctor and copy-assignment operator for Optional<T> are declared as + // delete, so override CDMKeyInfo copy-ctor for nsTArray operations. + CDMKeyInfo(const CDMKeyInfo& aKeyInfo) + { + mKeyId = aKeyInfo.mKeyId; + if (aKeyInfo.mStatus.WasPassed()) { + mStatus.Construct(aKeyInfo.mStatus.Value()); + } + } + + nsTArray<uint8_t> mKeyId; + dom::Optional<dom::MediaKeyStatus> mStatus; +}; + +typedef int64_t UnixTime; + +// Proxies calls CDM, and proxies calls back. +// Note: Promises are passed in via a PromiseId, so that the ID can be +// passed via IPC to the CDM, which can then signal when to reject or +// resolve the promise using its PromiseId. +class CDMProxy { +protected: + typedef dom::PromiseId PromiseId; + typedef dom::MediaKeySessionType MediaKeySessionType; +public: + + NS_IMETHOD_(MozExternalRefCountType) AddRef(void) = 0; + NS_IMETHOD_(MozExternalRefCountType) Release(void) = 0; + + typedef MozPromise<DecryptResult, DecryptResult, /* IsExclusive = */ true> DecryptPromise; + + // Main thread only. + CDMProxy(dom::MediaKeys* aKeys, + const nsAString& aKeySystem, + bool aDistinctiveIdentifierRequired, + bool aPersistentStateRequired) + : mKeys(aKeys) + , mKeySystem(aKeySystem) + , mDistinctiveIdentifierRequired(aDistinctiveIdentifierRequired) + , mPersistentStateRequired(aPersistentStateRequired) + {} + + // Main thread only. + // Loads the CDM corresponding to mKeySystem. + // Calls MediaKeys::OnCDMCreated() when the CDM is created. + virtual void Init(PromiseId aPromiseId, + const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, + const nsAString& aName, + bool aInPrivateBrowsing) = 0; + + virtual void OnSetDecryptorId(uint32_t aId) {} + + // Main thread only. + // Uses the CDM to create a key session. + // Calls MediaKeys::OnSessionActivated() when session is created. + // Assumes ownership of (Move()s) aInitData's contents. + virtual void CreateSession(uint32_t aCreateSessionToken, + MediaKeySessionType aSessionType, + PromiseId aPromiseId, + const nsAString& aInitDataType, + nsTArray<uint8_t>& aInitData) = 0; + + // Main thread only. + // Uses the CDM to load a presistent session stored on disk. + // Calls MediaKeys::OnSessionActivated() when session is loaded. + virtual void LoadSession(PromiseId aPromiseId, + const nsAString& aSessionId) = 0; + + // Main thread only. + // Sends a new certificate to the CDM. + // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has + // processed the request. + // Assumes ownership of (Move()s) aCert's contents. + virtual void SetServerCertificate(PromiseId aPromiseId, + nsTArray<uint8_t>& aCert) = 0; + + // Main thread only. + // Sends an update to the CDM. + // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has + // processed the request. + // Assumes ownership of (Move()s) aResponse's contents. + virtual void UpdateSession(const nsAString& aSessionId, + PromiseId aPromiseId, + nsTArray<uint8_t>& aResponse) = 0; + + // Main thread only. + // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has + // processed the request. + // If processing this operation results in the session actually closing, + // we also call MediaKeySession::OnClosed(), which in turn calls + // MediaKeys::OnSessionClosed(). + virtual void CloseSession(const nsAString& aSessionId, + PromiseId aPromiseId) = 0; + + // Main thread only. + // Removes all data for a persisent session. + // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has + // processed the request. + virtual void RemoveSession(const nsAString& aSessionId, + PromiseId aPromiseId) = 0; + + // Main thread only. + virtual void Shutdown() = 0; + + // Main thread only. + virtual void Terminated() = 0; + + // Threadsafe. + virtual const nsCString& GetNodeId() const = 0; + + // Main thread only. + virtual void OnSetSessionId(uint32_t aCreateSessionToken, + const nsAString& aSessionId) = 0; + + // Main thread only. + virtual void OnResolveLoadSessionPromise(uint32_t aPromiseId, + bool aSuccess) = 0; + + // Main thread only. + virtual void OnSessionMessage(const nsAString& aSessionId, + dom::MediaKeyMessageType aMessageType, + nsTArray<uint8_t>& aMessage) = 0; + + // Main thread only. + virtual void OnExpirationChange(const nsAString& aSessionId, + UnixTime aExpiryTime) = 0; + + // Main thread only. + virtual void OnSessionClosed(const nsAString& aSessionId) = 0; + + // Main thread only. + virtual void OnSessionError(const nsAString& aSessionId, + nsresult aException, + uint32_t aSystemCode, + const nsAString& aMsg) = 0; + + // Main thread only. + virtual void OnRejectPromise(uint32_t aPromiseId, + nsresult aDOMException, + const nsCString& aMsg) = 0; + + virtual RefPtr<DecryptPromise> Decrypt(MediaRawData* aSample) = 0; + + // Owner thread only. + virtual void OnDecrypted(uint32_t aId, + DecryptStatus aResult, + const nsTArray<uint8_t>& aDecryptedData) = 0; + + // Reject promise with DOMException corresponding to aExceptionCode. + // Can be called from any thread. + virtual void RejectPromise(PromiseId aId, + nsresult aExceptionCode, + const nsCString& aReason) = 0; + + // Resolves promise with "undefined". + // Can be called from any thread. + virtual void ResolvePromise(PromiseId aId) = 0; + + // Threadsafe. + virtual const nsString& KeySystem() const = 0; + + virtual CDMCaps& Capabilites() = 0; + + // Main thread only. + virtual void OnKeyStatusesChange(const nsAString& aSessionId) = 0; + + virtual void GetSessionIdsForKeyId(const nsTArray<uint8_t>& aKeyId, + nsTArray<nsCString>& aSessionIds) = 0; + +#ifdef DEBUG + virtual bool IsOnOwnerThread() = 0; +#endif + + virtual uint32_t GetDecryptorId() { return 0; } + +protected: + virtual ~CDMProxy() {} + + // Helper to enforce that a raw pointer is only accessed on the main thread. + template<class Type> + class MainThreadOnlyRawPtr { + public: + explicit MainThreadOnlyRawPtr(Type* aPtr) + : mPtr(aPtr) + { + MOZ_ASSERT(NS_IsMainThread()); + } + + bool IsNull() const { + MOZ_ASSERT(NS_IsMainThread()); + return !mPtr; + } + + void Clear() { + MOZ_ASSERT(NS_IsMainThread()); + mPtr = nullptr; + } + + Type* operator->() const MOZ_NO_ADDREF_RELEASE_ON_RETURN { + MOZ_ASSERT(NS_IsMainThread()); + return mPtr; + } + private: + Type* mPtr; + }; + + // Our reference back to the MediaKeys object. + // WARNING: This is a non-owning reference that is cleared by MediaKeys + // destructor. only use on main thread, and always nullcheck before using! + MainThreadOnlyRawPtr<dom::MediaKeys> mKeys; + + const nsString mKeySystem; + + // Onwer specified thread. e.g. Gecko Media Plugin thread. + // All interactions with the out-of-process EME plugin must come from this thread. + RefPtr<nsIThread> mOwnerThread; + + nsCString mNodeId; + + CDMCaps mCapabilites; + + const bool mDistinctiveIdentifierRequired; + const bool mPersistentStateRequired; +}; + + +} // namespace mozilla + +#endif // CDMProxy_h_ diff --git a/dom/media/eme/DecryptorProxyCallback.h b/dom/media/eme/DecryptorProxyCallback.h new file mode 100644 index 000000000..c1fcb49a4 --- /dev/null +++ b/dom/media/eme/DecryptorProxyCallback.h @@ -0,0 +1,54 @@ +/* -*- 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/. */ + +#ifndef DecryptorProxyCallback_h_ +#define DecryptorProxyCallback_h_ + +#include "mozilla/dom/MediaKeyStatusMapBinding.h" // For MediaKeyStatus +#include "mozilla/dom/MediaKeyMessageEventBinding.h" // For MediaKeyMessageType +#include "mozilla/CDMProxy.h" + +class DecryptorProxyCallback { +public: + + virtual ~DecryptorProxyCallback() {} + + virtual void SetDecryptorId(uint32_t aId) = 0; + + virtual void SetSessionId(uint32_t aCreateSessionId, + const nsCString& aSessionId) = 0; + + virtual void ResolveLoadSessionPromise(uint32_t aPromiseId, + bool aSuccess) = 0; + + virtual void ResolvePromise(uint32_t aPromiseId) = 0; + + virtual void RejectPromise(uint32_t aPromiseId, + nsresult aException, + const nsCString& aSessionId) = 0; + + virtual void SessionMessage(const nsCString& aSessionId, + mozilla::dom::MediaKeyMessageType aMessageType, + const nsTArray<uint8_t>& aMessage) = 0; + + virtual void ExpirationChange(const nsCString& aSessionId, + mozilla::UnixTime aExpiryTime) = 0; + + virtual void SessionClosed(const nsCString& aSessionId) = 0; + + virtual void SessionError(const nsCString& aSessionId, + nsresult aException, + uint32_t aSystemCode, + const nsCString& aMessage) = 0; + + virtual void Decrypted(uint32_t aId, + mozilla::DecryptStatus aResult, + const nsTArray<uint8_t>& aDecryptedData) = 0; + + virtual void BatchedKeyStatusChanged(const nsCString& aSessionId, + const nsTArray<mozilla::CDMKeyInfo>& aKeyInfos) = 0; +}; + +#endif diff --git a/dom/media/eme/DetailedPromise.cpp b/dom/media/eme/DetailedPromise.cpp new file mode 100644 index 000000000..1aa83bef5 --- /dev/null +++ b/dom/media/eme/DetailedPromise.cpp @@ -0,0 +1,57 @@ +/* -*- 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 "DetailedPromise.h" +#include "mozilla/dom/DOMException.h" +#include "nsPrintfCString.h" + +namespace mozilla { +namespace dom { + +DetailedPromise::DetailedPromise(nsIGlobalObject* aGlobal, + const nsACString& aName) + : Promise(aGlobal) + , mName(aName) + , mResponded(false) + , mStartTime(TimeStamp::Now()) +{ +} + +DetailedPromise::~DetailedPromise() +{ +} + +void +DetailedPromise::MaybeReject(nsresult aArg, const nsACString& aReason) +{ + nsPrintfCString msg("%s promise rejected 0x%x '%s'", mName.get(), aArg, + PromiseFlatCString(aReason).get()); + EME_LOG(msg.get()); + + LogToBrowserConsole(NS_ConvertUTF8toUTF16(msg)); + + ErrorResult rv; + rv.ThrowDOMException(aArg, aReason); + Promise::MaybeReject(rv); +} + +void +DetailedPromise::MaybeReject(ErrorResult&, const nsACString& aReason) +{ + NS_NOTREACHED("nsresult expected in MaybeReject()"); +} + +/* static */ already_AddRefed<DetailedPromise> +DetailedPromise::Create(nsIGlobalObject* aGlobal, + ErrorResult& aRv, + const nsACString& aName) +{ + RefPtr<DetailedPromise> promise = new DetailedPromise(aGlobal, aName); + promise->CreateWrapper(nullptr, aRv); + return aRv.Failed() ? nullptr : promise.forget(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/eme/DetailedPromise.h b/dom/media/eme/DetailedPromise.h new file mode 100644 index 000000000..f7f10aa40 --- /dev/null +++ b/dom/media/eme/DetailedPromise.h @@ -0,0 +1,57 @@ +/* -*- 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/. */ + +#ifndef __DetailedPromise_h__ +#define __DetailedPromise_h__ + +#include "mozilla/dom/Promise.h" +#include "EMEUtils.h" + +namespace mozilla { +namespace dom { + +/* + * This is pretty horrible; bug 1160445. + * Extend Promise to add custom DOMException messages on rejection. + * Get rid of this once we've ironed out EME errors in the wild. + */ +class DetailedPromise : public Promise +{ +public: + static already_AddRefed<DetailedPromise> + Create(nsIGlobalObject* aGlobal, + ErrorResult& aRv, + const nsACString& aName); + + template <typename T> + void MaybeResolve(const T& aArg) + { + EME_LOG("%s promise resolved", mName.get()); + Promise::MaybeResolve<T>(aArg); + } + + void MaybeReject(nsresult aArg) = delete; + void MaybeReject(nsresult aArg, const nsACString& aReason); + + void MaybeReject(ErrorResult& aArg) = delete; + void MaybeReject(ErrorResult&, const nsACString& aReason); + +private: + explicit DetailedPromise(nsIGlobalObject* aGlobal, + const nsACString& aName); + + virtual ~DetailedPromise(); + + enum Status { Succeeded, Failed }; + + nsCString mName; + bool mResponded; + TimeStamp mStartTime; +}; + +} // namespace dom +} // namespace mozilla + +#endif // __DetailedPromise_h__ diff --git a/dom/media/eme/EMEUtils.cpp b/dom/media/eme/EMEUtils.cpp new file mode 100644 index 000000000..68ef52d83 --- /dev/null +++ b/dom/media/eme/EMEUtils.cpp @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 8; 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 "mozilla/EMEUtils.h" + +#include "jsfriendapi.h" // for AutoCheckCannotGC +#include "mozilla/dom/UnionTypes.h" + +namespace mozilla { + +LogModule* GetEMELog() { + static LazyLogModule log("EME"); + return log; +} + +LogModule* GetEMEVerboseLog() { + static LazyLogModule log("EMEV"); + return log; +} + +ArrayData +GetArrayBufferViewOrArrayBufferData(const dom::ArrayBufferViewOrArrayBuffer& aBufferOrView) +{ + MOZ_ASSERT(aBufferOrView.IsArrayBuffer() || aBufferOrView.IsArrayBufferView()); + JS::AutoCheckCannotGC nogc; + if (aBufferOrView.IsArrayBuffer()) { + const dom::ArrayBuffer& buffer = aBufferOrView.GetAsArrayBuffer(); + buffer.ComputeLengthAndData(); + return ArrayData(buffer.Data(), buffer.Length()); + } else if (aBufferOrView.IsArrayBufferView()) { + const dom::ArrayBufferView& bufferview = aBufferOrView.GetAsArrayBufferView(); + bufferview.ComputeLengthAndData(); + return ArrayData(bufferview.Data(), bufferview.Length()); + } + return ArrayData(nullptr, 0); +} + +void +CopyArrayBufferViewOrArrayBufferData(const dom::ArrayBufferViewOrArrayBuffer& aBufferOrView, + nsTArray<uint8_t>& aOutData) +{ + JS::AutoCheckCannotGC nogc; + ArrayData data = GetArrayBufferViewOrArrayBufferData(aBufferOrView); + aOutData.Clear(); + if (!data.IsValid()) { + return; + } + aOutData.AppendElements(data.mData, data.mLength); +} + +void CopyArrayBufferViewOrArrayBufferData(const dom::ArrayBuffer& aBuffer, + nsTArray<uint8_t>& aOutData) { + JS::AutoCheckCannotGC nogc; + aBuffer.ComputeLengthAndData(); + aOutData.Clear(); + aOutData.AppendElements(aBuffer.Data(), aBuffer.Length()); +} + +bool +IsClearkeyKeySystem(const nsAString& aKeySystem) +{ + return !CompareUTF8toUTF16(kEMEKeySystemClearkey, aKeySystem); +} + +bool +IsWidevineKeySystem(const nsAString& aKeySystem) +{ + return !CompareUTF8toUTF16(kEMEKeySystemWidevine, aKeySystem); +} + +nsString +KeySystemToGMPName(const nsAString& aKeySystem) +{ + if (IsClearkeyKeySystem(aKeySystem)) { + return NS_LITERAL_STRING("gmp-clearkey"); + } + if (IsWidevineKeySystem(aKeySystem)) { + return NS_LITERAL_STRING("gmp-widevinecdm"); + } + MOZ_ASSERT(false, "We should only call this for known GMPs"); + return EmptyString(); +} + +} // namespace mozilla diff --git a/dom/media/eme/EMEUtils.h b/dom/media/eme/EMEUtils.h new file mode 100644 index 000000000..aef157482 --- /dev/null +++ b/dom/media/eme/EMEUtils.h @@ -0,0 +1,104 @@ +/* -*- Mode: C++; tab-width: 8; 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/. */ + +#ifndef EME_LOG_H_ +#define EME_LOG_H_ + +#include "VideoUtils.h" +#include "mozilla/Logging.h" +#include "nsString.h" +#include "nsTArray.h" +#include "mozilla/dom/TypedArray.h" + +namespace mozilla { + +namespace dom { +class ArrayBufferViewOrArrayBuffer; +} + +#ifndef EME_LOG + LogModule* GetEMELog(); + #define EME_LOG(...) MOZ_LOG(GetEMELog(), mozilla::LogLevel::Debug, (__VA_ARGS__)) + #define EME_LOG_ENABLED() MOZ_LOG_TEST(GetEMELog(), mozilla::LogLevel::Debug) +#endif + +#ifndef EME_VERBOSE_LOG + LogModule* GetEMEVerboseLog(); + #define EME_VERBOSE_LOG(...) MOZ_LOG(GetEMEVerboseLog(), mozilla::LogLevel::Debug, (__VA_ARGS__)) +#else + #ifndef EME_LOG + #define EME_LOG(...) + #endif + + #ifndef EME_VERBOSE_LOG + #define EME_VERBOSE_LOG(...) + #endif +#endif + +// Helper function to extract a copy of data coming in from JS in an +// (ArrayBuffer or ArrayBufferView) IDL typed function argument. +// +// Only call this on a properly initialized ArrayBufferViewOrArrayBuffer. +void +CopyArrayBufferViewOrArrayBufferData(const dom::ArrayBufferViewOrArrayBuffer& aBufferOrView, + nsTArray<uint8_t>& aOutData); + +// Overload for ArrayBuffer +void CopyArrayBufferViewOrArrayBufferData(const dom::ArrayBuffer& aBufferOrView, + nsTArray<uint8_t>& aOutData); + +struct ArrayData { + explicit ArrayData(const uint8_t* aData, size_t aLength) + : mData(aData) + , mLength(aLength) + { + } + const uint8_t* mData; + const size_t mLength; + bool IsValid() const { + return mData != nullptr && mLength != 0; + } + bool operator== (const nsTArray<uint8_t>& aOther) const { + return mLength == aOther.Length() && + memcmp(mData, aOther.Elements(), mLength) == 0; + } +}; + +// Helper function to extract data coming in from JS in an +// (ArrayBuffer or ArrayBufferView) IDL typed function argument. +// +// Be *very* careful with this! +// +// Only use returned ArrayData inside the lifetime of the +// ArrayBufferViewOrArrayBuffer; the ArrayData struct does not contain +// a copy of the data! +// +// And do *not* call out to anything that could call into JavaScript, +// while the ArrayData is live, as then all bets about the data not changing +// are off! No calls into JS, no calls into JS-implemented WebIDL or XPIDL, +// nothing. Beware! +// +// Only call this on a properly initialized ArrayBufferViewOrArrayBuffer. +ArrayData +GetArrayBufferViewOrArrayBufferData(const dom::ArrayBufferViewOrArrayBuffer& aBufferOrView); + +nsString +KeySystemToGMPName(const nsAString& aKeySystem); + +bool +IsClearkeyKeySystem(const nsAString& aKeySystem); + +bool +IsWidevineKeySystem(const nsAString& aKeySystem); + +enum CDMType { + eClearKey = 0, + eWidevine = 2, + eUnknown = 3 +}; + +} // namespace mozilla + +#endif // EME_LOG_H_ diff --git a/dom/media/eme/MediaEncryptedEvent.cpp b/dom/media/eme/MediaEncryptedEvent.cpp new file mode 100644 index 000000000..a9c83291e --- /dev/null +++ b/dom/media/eme/MediaEncryptedEvent.cpp @@ -0,0 +1,128 @@ +/* -*- Mode: C++; tab-width: 8; 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 "MediaEncryptedEvent.h" +#include "mozilla/dom/MediaEncryptedEventBinding.h" +#include "nsContentUtils.h" +#include "jsfriendapi.h" +#include "nsINode.h" +#include "mozilla/dom/MediaKeys.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(MediaEncryptedEvent) + +NS_IMPL_ADDREF_INHERITED(MediaEncryptedEvent, Event) +NS_IMPL_RELEASE_INHERITED(MediaEncryptedEvent, Event) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaEncryptedEvent, Event) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MediaEncryptedEvent, Event) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mInitData) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MediaEncryptedEvent, Event) + tmp->mInitData = nullptr; + mozilla::DropJSObjects(this); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(MediaEncryptedEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +MediaEncryptedEvent::MediaEncryptedEvent(EventTarget* aOwner) + : Event(aOwner, nullptr, nullptr) +{ + mozilla::HoldJSObjects(this); +} + +MediaEncryptedEvent::~MediaEncryptedEvent() +{ + mInitData = nullptr; + mozilla::DropJSObjects(this); +} + +JSObject* +MediaEncryptedEvent::WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return MediaEncryptedEventBinding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed<MediaEncryptedEvent> +MediaEncryptedEvent::Constructor(EventTarget* aOwner) +{ + RefPtr<MediaEncryptedEvent> e = new MediaEncryptedEvent(aOwner); + e->InitEvent(NS_LITERAL_STRING("encrypted"), false, false); + e->SetTrusted(true); + return e.forget(); +} + +already_AddRefed<MediaEncryptedEvent> +MediaEncryptedEvent::Constructor(EventTarget* aOwner, + const nsAString& aInitDataType, + const nsTArray<uint8_t>& aInitData) +{ + RefPtr<MediaEncryptedEvent> e = new MediaEncryptedEvent(aOwner); + e->InitEvent(NS_LITERAL_STRING("encrypted"), false, false); + e->mInitDataType = aInitDataType; + e->mRawInitData = aInitData; + e->SetTrusted(true); + return e.forget(); +} + +already_AddRefed<MediaEncryptedEvent> +MediaEncryptedEvent::Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const MediaKeyNeededEventInit& aEventInitDict, + ErrorResult& aRv) +{ + nsCOMPtr<EventTarget> owner = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<MediaEncryptedEvent> e = new MediaEncryptedEvent(owner); + bool trusted = e->Init(owner); + e->InitEvent(aType, aEventInitDict.mBubbles, aEventInitDict.mCancelable); + e->mInitDataType = aEventInitDict.mInitDataType; + if (!aEventInitDict.mInitData.IsNull()) { + const auto& a = aEventInitDict.mInitData.Value(); + nsTArray<uint8_t> initData; + CopyArrayBufferViewOrArrayBufferData(a, initData); + e->mInitData = ArrayBuffer::Create(aGlobal.Context(), initData.Length(), + initData.Elements()); + if (!e->mInitData) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + } + e->SetTrusted(trusted); + return e.forget(); +} + +void +MediaEncryptedEvent::GetInitDataType(nsString& aRetVal) const +{ + aRetVal = mInitDataType; +} + +void +MediaEncryptedEvent::GetInitData(JSContext* cx, + JS::MutableHandle<JSObject*> aData, + ErrorResult& aRv) +{ + if (mRawInitData.Length()) { + mInitData = ArrayBuffer::Create(cx, + this, + mRawInitData.Length(), + mRawInitData.Elements()); + if (!mInitData) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + mRawInitData.Clear(); + } + aData.set(mInitData); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/eme/MediaEncryptedEvent.h b/dom/media/eme/MediaEncryptedEvent.h new file mode 100644 index 000000000..c2ac56061 --- /dev/null +++ b/dom/media/eme/MediaEncryptedEvent.h @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 8; 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/. */ + +#ifndef mozilla_dom_MediaKeyNeededEvent_h__ +#define mozilla_dom_MediaKeyNeededEvent_h__ + +#include "mozilla/dom/MediaEncryptedEventBinding.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" +#include "nsCOMPtr.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/BindingUtils.h" +#include "js/TypeDecls.h" + +namespace mozilla { +namespace dom { + +class MediaEncryptedEvent final : public Event +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(MediaEncryptedEvent, Event) +protected: + virtual ~MediaEncryptedEvent(); + explicit MediaEncryptedEvent(EventTarget* aOwner); + + nsString mInitDataType; + JS::Heap<JSObject*> mInitData; + +public: + + JSObject* WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<MediaEncryptedEvent> + Constructor(EventTarget* aOwner); + + static already_AddRefed<MediaEncryptedEvent> + Constructor(EventTarget* aOwner, + const nsAString& aInitDataType, + const nsTArray<uint8_t>& aInitData); + + static already_AddRefed<MediaEncryptedEvent> + Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const MediaKeyNeededEventInit& aEventInitDict, + ErrorResult& aRv); + + void GetInitDataType(nsString& aRetVal) const; + + void GetInitData(JSContext* cx, + JS::MutableHandle<JSObject*> aData, + ErrorResult& aRv); +private: + nsTArray<uint8_t> mRawInitData; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MediaKeyNeededEvent_h__ diff --git a/dom/media/eme/MediaKeyError.cpp b/dom/media/eme/MediaKeyError.cpp new file mode 100644 index 000000000..635c34364 --- /dev/null +++ b/dom/media/eme/MediaKeyError.cpp @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "MediaKeyError.h" +#include "mozilla/dom/MediaKeyErrorBinding.h" +#include "nsContentUtils.h" + +namespace mozilla { +namespace dom { + +MediaKeyError::MediaKeyError(EventTarget* aOwner, uint32_t aSystemCode) + : Event(aOwner, nullptr, nullptr) + , mSystemCode(aSystemCode) +{ + InitEvent(NS_LITERAL_STRING("error"), false, false); +} + +MediaKeyError::~MediaKeyError() +{ +} + +uint32_t +MediaKeyError::SystemCode() const +{ + return mSystemCode; +} + +JSObject* +MediaKeyError::WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return MediaKeyErrorBinding::Wrap(aCx, this, aGivenProto); +} + + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/eme/MediaKeyError.h b/dom/media/eme/MediaKeyError.h new file mode 100644 index 000000000..df79b2c95 --- /dev/null +++ b/dom/media/eme/MediaKeyError.h @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 mozilla_dom_MediaKeyError_h +#define mozilla_dom_MediaKeyError_h + +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "nsWrapperCache.h" +#include "mozilla/dom/Event.h" +#include "js/TypeDecls.h" + +namespace mozilla { +namespace dom { + +class MediaKeyError final : public Event +{ +public: + NS_FORWARD_TO_EVENT + + MediaKeyError(EventTarget* aOwner, uint32_t aSystemCode); + ~MediaKeyError(); + + JSObject* WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + uint32_t SystemCode() const; + +private: + uint32_t mSystemCode; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/media/eme/MediaKeyMessageEvent.cpp b/dom/media/eme/MediaKeyMessageEvent.cpp new file mode 100644 index 000000000..ee13f8d26 --- /dev/null +++ b/dom/media/eme/MediaKeyMessageEvent.cpp @@ -0,0 +1,122 @@ +/* -*- Mode: C++; tab-width: 8; 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 "mozilla/dom/MediaKeyMessageEvent.h" +#include "mozilla/dom/MediaKeyMessageEventBinding.h" +#include "js/GCAPI.h" +#include "jsfriendapi.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/PrimitiveConversions.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/TypedArray.h" +#include "nsContentUtils.h" +#include "mozilla/dom/MediaKeys.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(MediaKeyMessageEvent) + +NS_IMPL_ADDREF_INHERITED(MediaKeyMessageEvent, Event) +NS_IMPL_RELEASE_INHERITED(MediaKeyMessageEvent, Event) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaKeyMessageEvent, Event) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MediaKeyMessageEvent, Event) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mMessage) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MediaKeyMessageEvent, Event) + tmp->mMessage = nullptr; + mozilla::DropJSObjects(this); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(MediaKeyMessageEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +MediaKeyMessageEvent::MediaKeyMessageEvent(EventTarget* aOwner) + : Event(aOwner, nullptr, nullptr) +{ + mozilla::HoldJSObjects(this); +} + +MediaKeyMessageEvent::~MediaKeyMessageEvent() +{ + mMessage = nullptr; + mozilla::DropJSObjects(this); +} + +MediaKeyMessageEvent* +MediaKeyMessageEvent::AsMediaKeyMessageEvent() +{ + return this; +} + +JSObject* +MediaKeyMessageEvent::WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return MediaKeyMessageEventBinding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed<MediaKeyMessageEvent> +MediaKeyMessageEvent::Constructor(EventTarget* aOwner, + MediaKeyMessageType aMessageType, + const nsTArray<uint8_t>& aMessage) +{ + RefPtr<MediaKeyMessageEvent> e = new MediaKeyMessageEvent(aOwner); + e->InitEvent(NS_LITERAL_STRING("message"), false, false); + e->mMessageType = aMessageType; + e->mRawMessage = aMessage; + e->SetTrusted(true); + return e.forget(); +} + +already_AddRefed<MediaKeyMessageEvent> +MediaKeyMessageEvent::Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const MediaKeyMessageEventInit& aEventInitDict, + ErrorResult& aRv) +{ + nsCOMPtr<EventTarget> owner = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<MediaKeyMessageEvent> e = new MediaKeyMessageEvent(owner); + bool trusted = e->Init(owner); + e->InitEvent(aType, aEventInitDict.mBubbles, aEventInitDict.mCancelable); + nsTArray<uint8_t> initData; + CopyArrayBufferViewOrArrayBufferData(aEventInitDict.mMessage, initData); + e->mMessage = ArrayBuffer::Create(aGlobal.Context(), + initData.Length(), + initData.Elements()); + if (!e->mMessage) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + e->mMessageType = aEventInitDict.mMessageType; + e->SetTrusted(trusted); + e->SetComposed(aEventInitDict.mComposed); + return e.forget(); +} + +void +MediaKeyMessageEvent::GetMessage(JSContext* cx, + JS::MutableHandle<JSObject*> aMessage, + ErrorResult& aRv) +{ + if (!mMessage) { + mMessage = ArrayBuffer::Create(cx, + this, + mRawMessage.Length(), + mRawMessage.Elements()); + if (!mMessage) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + mRawMessage.Clear(); + } + aMessage.set(mMessage); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/eme/MediaKeyMessageEvent.h b/dom/media/eme/MediaKeyMessageEvent.h new file mode 100644 index 000000000..2ec50f4f0 --- /dev/null +++ b/dom/media/eme/MediaKeyMessageEvent.h @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 8; 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/. */ + +#ifndef mozilla_dom_MediaKeyMessageEvent_h__ +#define mozilla_dom_MediaKeyMessageEvent_h__ + +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" +#include "nsCOMPtr.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/TypedArray.h" +#include "js/TypeDecls.h" +#include "mozilla/dom/MediaKeyMessageEventBinding.h" + +namespace mozilla { +namespace dom { + +struct MediaKeyMessageEventInit; + +class MediaKeyMessageEvent final : public Event +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(MediaKeyMessageEvent, Event) +protected: + virtual ~MediaKeyMessageEvent(); + explicit MediaKeyMessageEvent(EventTarget* aOwner); + + MediaKeyMessageType mMessageType; + JS::Heap<JSObject*> mMessage; + +public: + virtual MediaKeyMessageEvent* AsMediaKeyMessageEvent(); + + JSObject* WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<MediaKeyMessageEvent> + Constructor(EventTarget* aOwner, + MediaKeyMessageType aMessageType, + const nsTArray<uint8_t>& aMessage); + + static already_AddRefed<MediaKeyMessageEvent> + Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const MediaKeyMessageEventInit& aEventInitDict, + ErrorResult& aRv); + + MediaKeyMessageType MessageType() const { return mMessageType; } + + void GetMessage(JSContext* cx, + JS::MutableHandle<JSObject*> aMessage, + ErrorResult& aRv); + +private: + nsTArray<uint8_t> mRawMessage; +}; + + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MediaKeyMessageEvent_h__ diff --git a/dom/media/eme/MediaKeySession.cpp b/dom/media/eme/MediaKeySession.cpp new file mode 100644 index 000000000..b5f3fe498 --- /dev/null +++ b/dom/media/eme/MediaKeySession.cpp @@ -0,0 +1,670 @@ +/* -*- Mode: C++; tab-width: 8; 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 "mozilla/dom/HTMLMediaElement.h" +#include "mozilla/dom/MediaKeySession.h" +#include "mozilla/dom/MediaKeyError.h" +#include "mozilla/dom/MediaKeyMessageEvent.h" +#include "mozilla/dom/MediaEncryptedEvent.h" +#include "mozilla/dom/MediaKeyStatusMap.h" +#include "mozilla/dom/MediaKeySystemAccess.h" +#include "mozilla/dom/KeyIdsInitDataBinding.h" +#include "nsCycleCollectionParticipant.h" +#include "mozilla/CDMProxy.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/Move.h" +#include "nsContentUtils.h" +#include "mozilla/EMEUtils.h" +#include "GMPUtils.h" +#include "nsPrintfCString.h" +#include "psshparser/PsshParser.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaKeySession, + DOMEventTargetHelper, + mMediaKeyError, + mKeys, + mKeyStatusMap, + mClosed) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(MediaKeySession) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(MediaKeySession, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(MediaKeySession, DOMEventTargetHelper) + +// Count of number of instances. Used to give each instance a +// unique token. +static uint32_t sMediaKeySessionNum = 0; + +// Max length of keyId in EME "keyIds" or WebM init data format, as enforced +// by web platform tests. +static const uint32_t MAX_KEY_ID_LENGTH = 512; + +// Max length of CENC PSSH init data tolerated, as enforced by web +// platform tests. +static const uint32_t MAX_CENC_INIT_DATA_LENGTH = 64 * 1024; + + +MediaKeySession::MediaKeySession(JSContext* aCx, + nsPIDOMWindowInner* aParent, + MediaKeys* aKeys, + const nsAString& aKeySystem, + MediaKeySessionType aSessionType, + ErrorResult& aRv) + : DOMEventTargetHelper(aParent) + , mKeys(aKeys) + , mKeySystem(aKeySystem) + , mSessionType(aSessionType) + , mToken(sMediaKeySessionNum++) + , mIsClosed(false) + , mUninitialized(true) + , mKeyStatusMap(new MediaKeyStatusMap(aParent)) + , mExpiration(JS::GenericNaN()) +{ + EME_LOG("MediaKeySession[%p,''] ctor", this); + + MOZ_ASSERT(aParent); + if (aRv.Failed()) { + return; + } + mClosed = MakePromise(aRv, NS_LITERAL_CSTRING("MediaKeys.createSession")); +} + +void MediaKeySession::SetSessionId(const nsAString& aSessionId) +{ + EME_LOG("MediaKeySession[%p,'%s'] session Id set", + this, NS_ConvertUTF16toUTF8(aSessionId).get()); + + if (NS_WARN_IF(!mSessionId.IsEmpty())) { + return; + } + mSessionId = aSessionId; + mKeys->OnSessionIdReady(this); +} + +MediaKeySession::~MediaKeySession() +{ +} + +MediaKeyError* +MediaKeySession::GetError() const +{ + return mMediaKeyError; +} + +void +MediaKeySession::GetKeySystem(nsString& aOutKeySystem) const +{ + aOutKeySystem.Assign(mKeySystem); +} + +void +MediaKeySession::GetSessionId(nsString& aSessionId) const +{ + aSessionId = GetSessionId(); +} + +const nsString& +MediaKeySession::GetSessionId() const +{ + return mSessionId; +} + +JSObject* +MediaKeySession::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return MediaKeySessionBinding::Wrap(aCx, this, aGivenProto); +} + +double +MediaKeySession::Expiration() const +{ + return mExpiration; +} + +Promise* +MediaKeySession::Closed() const +{ + return mClosed; +} + +void +MediaKeySession::UpdateKeyStatusMap() +{ + MOZ_ASSERT(!IsClosed()); + if (!mKeys->GetCDMProxy()) { + return; + } + + nsTArray<CDMCaps::KeyStatus> keyStatuses; + { + CDMCaps::AutoLock caps(mKeys->GetCDMProxy()->Capabilites()); + caps.GetKeyStatusesForSession(mSessionId, keyStatuses); + } + + mKeyStatusMap->Update(keyStatuses); + + if (EME_LOG_ENABLED()) { + nsAutoCString message( + nsPrintfCString("MediaKeySession[%p,'%s'] key statuses change {", + this, NS_ConvertUTF16toUTF8(mSessionId).get())); + using IntegerType = typename std::underlying_type<MediaKeyStatus>::type; + for (const CDMCaps::KeyStatus& status : keyStatuses) { + message.Append(nsPrintfCString(" (%s,%s)", ToBase64(status.mId).get(), + MediaKeyStatusValues::strings[static_cast<IntegerType>(status.mStatus)].value)); + } + message.Append(" }"); + EME_LOG(message.get()); + } +} + +MediaKeyStatusMap* +MediaKeySession::KeyStatuses() const +{ + return mKeyStatusMap; +} + +// The user agent MUST thoroughly validate the Initialization Data before +// passing it to the CDM. This includes verifying that the length and +// values of fields are reasonable, verifying that values are within +// reasonable limits, and stripping irrelevant, unsupported, or unknown +// data or fields. It is RECOMMENDED that user agents pre-parse, sanitize, +// and/or generate a fully sanitized version of the Initialization Data. +// If the Initialization Data format specified by initDataType supports +// multiple entries, the user agent SHOULD remove entries that are not +// needed by the CDM. The user agent MUST NOT re-order entries within +// the Initialization Data. +static bool +ValidateInitData(const nsTArray<uint8_t>& aInitData, const nsAString& aInitDataType) +{ + if (aInitDataType.LowerCaseEqualsLiteral("webm")) { + // WebM initData consists of a single keyId. Ensure it's of reasonable length. + return aInitData.Length() <= MAX_KEY_ID_LENGTH; + } else if (aInitDataType.LowerCaseEqualsLiteral("cenc")) { + // Limit initData to less than 64KB. + if (aInitData.Length() > MAX_CENC_INIT_DATA_LENGTH) { + return false; + } + std::vector<std::vector<uint8_t>> keyIds; + return ParseCENCInitData(aInitData.Elements(), aInitData.Length(), keyIds); + } else if (aInitDataType.LowerCaseEqualsLiteral("keyids")) { + if (aInitData.Length() > MAX_KEY_ID_LENGTH) { + return false; + } + // Ensure that init data matches the expected JSON format. + mozilla::dom::KeyIdsInitData keyIds; + nsString json; + nsDependentCSubstring raw(reinterpret_cast<const char*>(aInitData.Elements()), aInitData.Length()); + if (NS_FAILED(nsContentUtils::ConvertStringFromEncoding(NS_LITERAL_CSTRING("UTF-8"), raw, json))) { + return false; + } + if (!keyIds.Init(json)) { + return false; + } + if (keyIds.mKids.Length() == 0) { + return false; + } + for (const auto& kid : keyIds.mKids) { + if (kid.IsEmpty()) { + return false; + } + } + } + return true; +} + +// Generates a license request based on the initData. A message of type +// "license-request" or "individualization-request" will always be queued +// if the algorithm succeeds and the promise is resolved. +already_AddRefed<Promise> +MediaKeySession::GenerateRequest(const nsAString& aInitDataType, + const ArrayBufferViewOrArrayBuffer& aInitData, + ErrorResult& aRv) +{ + RefPtr<DetailedPromise> promise(MakePromise(aRv, + NS_LITERAL_CSTRING("MediaKeySession.generateRequest"))); + if (aRv.Failed()) { + return nullptr; + } + + // If this object is closed, return a promise rejected with an InvalidStateError. + if (IsClosed()) { + EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() failed, closed", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Session is closed in MediaKeySession.generateRequest()")); + return promise.forget(); + } + + // If this object's uninitialized value is false, return a promise rejected + // with an InvalidStateError. + if (!mUninitialized) { + EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() failed, uninitialized", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Session is already initialized in MediaKeySession.generateRequest()")); + return promise.forget(); + } + + // Let this object's uninitialized value be false. + mUninitialized = false; + + // If initDataType is the empty string, return a promise rejected + // with a newly created TypeError. + if (aInitDataType.IsEmpty()) { + promise->MaybeReject(NS_ERROR_DOM_TYPE_ERR, + NS_LITERAL_CSTRING("Empty initDataType passed to MediaKeySession.generateRequest()")); + EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() failed, empty initDataType", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + return promise.forget(); + } + + // If initData is an empty array, return a promise rejected with + // a newly created TypeError. + nsTArray<uint8_t> data; + CopyArrayBufferViewOrArrayBufferData(aInitData, data); + if (data.IsEmpty()) { + promise->MaybeReject(NS_ERROR_DOM_TYPE_ERR, + NS_LITERAL_CSTRING("Empty initData passed to MediaKeySession.generateRequest()")); + EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() failed, empty initData", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + return promise.forget(); + } + + // If the Key System implementation represented by this object's + // cdm implementation value does not support initDataType as an + // Initialization Data Type, return a promise rejected with a + // NotSupportedError. String comparison is case-sensitive. + if (!MediaKeySystemAccess::KeySystemSupportsInitDataType(mKeySystem, aInitDataType)) { + promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR, + NS_LITERAL_CSTRING("Unsupported initDataType passed to MediaKeySession.generateRequest()")); + EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() failed, unsupported initDataType", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + return promise.forget(); + } + + // Let init data be a copy of the contents of the initData parameter. + // Note: Handled by the CopyArrayBufferViewOrArrayBufferData call above. + + // Let session type be this object's session type. + + // Let promise be a new promise. + + // Run the following steps in parallel: + + // If the init data is not valid for initDataType, reject promise with + // a newly created TypeError. + if (!ValidateInitData(data, aInitDataType)) { + // If the preceding step failed, reject promise with a newly created TypeError. + promise->MaybeReject(NS_ERROR_DOM_TYPE_ERR, + NS_LITERAL_CSTRING("initData sanitization failed in MediaKeySession.generateRequest()")); + EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() initData sanitization failed", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + return promise.forget(); + } + + // Let sanitized init data be a validated and sanitized version of init data. + + // If sanitized init data is empty, reject promise with a NotSupportedError. + + // Note: Remaining steps of generateRequest method continue in CDM. + + // Convert initData to base64 for easier logging. + // Note: CreateSession() Move()s the data out of the array, so we have + // to copy it here. + nsAutoCString base64InitData(ToBase64(data)); + PromiseId pid = mKeys->StorePromise(promise); + mKeys->ConnectPendingPromiseIdWithToken(pid, Token()); + mKeys->GetCDMProxy()->CreateSession(Token(), + mSessionType, + pid, + aInitDataType, data); + + EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() sent, " + "promiseId=%d initData(base64)='%s' initDataType='%s'", + this, + NS_ConvertUTF16toUTF8(mSessionId).get(), + pid, + base64InitData.get(), + NS_ConvertUTF16toUTF8(aInitDataType).get()); + + return promise.forget(); +} + +already_AddRefed<Promise> +MediaKeySession::Load(const nsAString& aSessionId, ErrorResult& aRv) +{ + RefPtr<DetailedPromise> promise(MakePromise(aRv, + NS_LITERAL_CSTRING("MediaKeySession.load"))); + if (aRv.Failed()) { + return nullptr; + } + + // 1. If this object is closed, return a promise rejected with an InvalidStateError. + if (IsClosed()) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Session is closed in MediaKeySession.load()")); + EME_LOG("MediaKeySession[%p,'%s'] Load() failed, closed", + this, NS_ConvertUTF16toUTF8(aSessionId).get()); + return promise.forget(); + } + + // 2.If this object's uninitialized value is false, return a promise rejected + // with an InvalidStateError. + if (!mUninitialized) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Session is already initialized in MediaKeySession.load()")); + EME_LOG("MediaKeySession[%p,'%s'] Load() failed, uninitialized", + this, NS_ConvertUTF16toUTF8(aSessionId).get()); + return promise.forget(); + } + + // 3.Let this object's uninitialized value be false. + mUninitialized = false; + + // 4. If sessionId is the empty string, return a promise rejected with a newly created TypeError. + if (aSessionId.IsEmpty()) { + promise->MaybeReject(NS_ERROR_DOM_TYPE_ERR, + NS_LITERAL_CSTRING("Trying to load a session with empty session ID")); + // "The sessionId parameter is empty." + EME_LOG("MediaKeySession[%p,''] Load() failed, no sessionId", this); + return promise.forget(); + } + + // 5. If the result of running the Is persistent session type? algorithm + // on this object's session type is false, return a promise rejected with + // a newly created TypeError. + if (mSessionType == MediaKeySessionType::Temporary) { + promise->MaybeReject(NS_ERROR_DOM_TYPE_ERR, + NS_LITERAL_CSTRING("Trying to load() into a non-persistent session")); + EME_LOG("MediaKeySession[%p,''] Load() failed, can't load in a non-persistent session", this); + return promise.forget(); + } + + // Note: We don't support persistent sessions in any keysystem, so all calls + // to Load() should reject with a TypeError in the preceding check. Omitting + // implementing the rest of the specified MediaKeySession::Load() algorithm. + + // We now know the sessionId being loaded into this session. Remove the + // session from its owning MediaKey's set of sessions awaiting a sessionId. + RefPtr<MediaKeySession> session(mKeys->GetPendingSession(Token())); + MOZ_ASSERT(session == this, "Session should be awaiting id on its own token"); + + // Associate with the known sessionId. + SetSessionId(aSessionId); + + PromiseId pid = mKeys->StorePromise(promise); + mKeys->GetCDMProxy()->LoadSession(pid, aSessionId); + + EME_LOG("MediaKeySession[%p,'%s'] Load() sent to CDM, promiseId=%d", + this, NS_ConvertUTF16toUTF8(mSessionId).get(), pid); + + return promise.forget(); +} + +already_AddRefed<Promise> +MediaKeySession::Update(const ArrayBufferViewOrArrayBuffer& aResponse, ErrorResult& aRv) +{ + RefPtr<DetailedPromise> promise(MakePromise(aRv, + NS_LITERAL_CSTRING("MediaKeySession.update"))); + if (aRv.Failed()) { + return nullptr; + } + + if (!IsCallable()) { + // If this object's callable value is false, return a promise rejected + // with a new DOMException whose name is InvalidStateError. + EME_LOG("MediaKeySession[%p,''] Update() called before sessionId set by CDM", this); + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("MediaKeySession.Update() called before sessionId set by CDM")); + return promise.forget(); + } + + nsTArray<uint8_t> data; + if (IsClosed() || !mKeys->GetCDMProxy()) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Session is closed or was not properly initialized")); + EME_LOG("MediaKeySession[%p,'%s'] Update() failed, session is closed or was not properly initialised.", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + return promise.forget(); + } + CopyArrayBufferViewOrArrayBufferData(aResponse, data); + if (data.IsEmpty()) { + promise->MaybeReject(NS_ERROR_DOM_TYPE_ERR, + NS_LITERAL_CSTRING("Empty response buffer passed to MediaKeySession.update()")); + EME_LOG("MediaKeySession[%p,'%s'] Update() failed, empty response buffer", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + return promise.forget(); + } + + + // Convert response to base64 for easier logging. + // Note: UpdateSession() Move()s the data out of the array, so we have + // to copy it here. + nsAutoCString base64Response(ToBase64(data)); + + PromiseId pid = mKeys->StorePromise(promise); + mKeys->GetCDMProxy()->UpdateSession(mSessionId, + pid, + data); + + EME_LOG("MediaKeySession[%p,'%s'] Update() sent to CDM, " + "promiseId=%d Response(base64)='%s'", + this, + NS_ConvertUTF16toUTF8(mSessionId).get(), + pid, + base64Response.get()); + + return promise.forget(); +} + +already_AddRefed<Promise> +MediaKeySession::Close(ErrorResult& aRv) +{ + RefPtr<DetailedPromise> promise(MakePromise(aRv, + NS_LITERAL_CSTRING("MediaKeySession.close"))); + if (aRv.Failed()) { + return nullptr; + } + // 1. Let session be the associated MediaKeySession object. + // 2. If session is closed, return a resolved promise. + if (IsClosed()) { + EME_LOG("MediaKeySession[%p,'%s'] Close() already closed", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + promise->MaybeResolveWithUndefined(); + return promise.forget(); + } + // 3. If session's callable value is false, return a promise rejected + // with an InvalidStateError. + if (!IsCallable()) { + EME_LOG("MediaKeySession[%p,''] Close() called before sessionId set by CDM", this); + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("MediaKeySession.Close() called before sessionId set by CDM")); + return promise.forget(); + } + if (!mKeys->GetCDMProxy()) { + EME_LOG("MediaKeySession[%p,'%s'] Close() null CDMProxy", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("MediaKeySession.Close() lost reference to CDM")); + return promise.forget(); + } + // 4. Let promise be a new promise. + PromiseId pid = mKeys->StorePromise(promise); + // 5. Run the following steps in parallel: + // 5.1 Let cdm be the CDM instance represented by session's cdm instance value. + // 5.2 Use cdm to close the session associated with session. + mKeys->GetCDMProxy()->CloseSession(mSessionId, pid); + + EME_LOG("MediaKeySession[%p,'%s'] Close() sent to CDM, promiseId=%d", + this, NS_ConvertUTF16toUTF8(mSessionId).get(), pid); + + // Session Closed algorithm is run when CDM causes us to run OnSessionClosed(). + + // 6. Return promise. + return promise.forget(); +} + +void +MediaKeySession::OnClosed() +{ + if (IsClosed()) { + return; + } + EME_LOG("MediaKeySession[%p,'%s'] session close operation complete.", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + mIsClosed = true; + mKeys->OnSessionClosed(this); + mKeys = nullptr; + mClosed->MaybeResolveWithUndefined(); +} + +bool +MediaKeySession::IsClosed() const +{ + return mIsClosed; +} + +already_AddRefed<Promise> +MediaKeySession::Remove(ErrorResult& aRv) +{ + RefPtr<DetailedPromise> promise(MakePromise(aRv, + NS_LITERAL_CSTRING("MediaKeySession.remove"))); + if (aRv.Failed()) { + return nullptr; + } + if (!IsCallable()) { + // If this object's callable value is false, return a promise rejected + // with a new DOMException whose name is InvalidStateError. + EME_LOG("MediaKeySession[%p,''] Remove() called before sessionId set by CDM", this); + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("MediaKeySession.Remove() called before sessionId set by CDM")); + return promise.forget(); + } + if (mSessionType != MediaKeySessionType::Persistent_license) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR, + NS_LITERAL_CSTRING("Calling MediaKeySession.remove() on non-persistent session")); + // "The operation is not supported on session type sessions." + EME_LOG("MediaKeySession[%p,'%s'] Remove() failed, sesion not persisrtent.", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + return promise.forget(); + } + if (IsClosed() || !mKeys->GetCDMProxy()) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("MediaKeySesison.remove() called but session is not active")); + // "The session is closed." + EME_LOG("MediaKeySession[%p,'%s'] Remove() failed, already session closed.", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + return promise.forget(); + } + PromiseId pid = mKeys->StorePromise(promise); + mKeys->GetCDMProxy()->RemoveSession(mSessionId, pid); + EME_LOG("MediaKeySession[%p,'%s'] Remove() sent to CDM, promiseId=%d.", + this, NS_ConvertUTF16toUTF8(mSessionId).get(), pid); + + return promise.forget(); +} + +void +MediaKeySession::DispatchKeyMessage(MediaKeyMessageType aMessageType, + const nsTArray<uint8_t>& aMessage) +{ + if (EME_LOG_ENABLED()) { + EME_LOG("MediaKeySession[%p,'%s'] DispatchKeyMessage() type=%s message(base64)='%s'", + this, NS_ConvertUTF16toUTF8(mSessionId).get(), + MediaKeyMessageTypeValues::strings[uint32_t(aMessageType)].value, + ToBase64(aMessage).get()); + } + + RefPtr<MediaKeyMessageEvent> event( + MediaKeyMessageEvent::Constructor(this, aMessageType, aMessage)); + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(this, event); + asyncDispatcher->PostDOMEvent(); +} + +void +MediaKeySession::DispatchKeyError(uint32_t aSystemCode) +{ + EME_LOG("MediaKeySession[%p,'%s'] DispatchKeyError() systemCode=%u.", + this, NS_ConvertUTF16toUTF8(mSessionId).get(), aSystemCode); + + RefPtr<MediaKeyError> event(new MediaKeyError(this, aSystemCode)); + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(this, event); + asyncDispatcher->PostDOMEvent(); +} + +void +MediaKeySession::DispatchKeyStatusesChange() +{ + if (IsClosed()) { + return; + } + + UpdateKeyStatusMap(); + + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(this, NS_LITERAL_STRING("keystatuseschange"), false); + asyncDispatcher->PostDOMEvent(); +} + +uint32_t +MediaKeySession::Token() const +{ + return mToken; +} + +already_AddRefed<DetailedPromise> +MediaKeySession::MakePromise(ErrorResult& aRv, const nsACString& aName) +{ + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject()); + if (!global) { + NS_WARNING("Passed non-global to MediaKeys ctor!"); + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + return DetailedPromise::Create(global, aRv, aName); +} + +void +MediaKeySession::SetExpiration(double aExpiration) +{ + EME_LOG("MediaKeySession[%p,'%s'] SetExpiry(%lf)", + this, + NS_ConvertUTF16toUTF8(mSessionId).get(), + aExpiration); + mExpiration = aExpiration; +} + +EventHandlerNonNull* +MediaKeySession::GetOnkeystatuseschange() +{ + return GetEventHandler(nsGkAtoms::onkeystatuseschange, EmptyString()); +} + +void +MediaKeySession::SetOnkeystatuseschange(EventHandlerNonNull* aCallback) +{ + SetEventHandler(nsGkAtoms::onkeystatuseschange, EmptyString(), aCallback); +} + +EventHandlerNonNull* +MediaKeySession::GetOnmessage() +{ + return GetEventHandler(nsGkAtoms::onmessage, EmptyString()); +} + +void +MediaKeySession::SetOnmessage(EventHandlerNonNull* aCallback) +{ + SetEventHandler(nsGkAtoms::onmessage, EmptyString(), aCallback); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/eme/MediaKeySession.h b/dom/media/eme/MediaKeySession.h new file mode 100644 index 000000000..6b7137046 --- /dev/null +++ b/dom/media/eme/MediaKeySession.h @@ -0,0 +1,136 @@ +/* -*- Mode: C++; tab-width: 8; 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/. */ + +#ifndef mozilla_dom_MediaKeySession_h +#define mozilla_dom_MediaKeySession_h + +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "nsCycleCollectionParticipant.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "nsCOMPtr.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/Mutex.h" +#include "mozilla/dom/Date.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/DetailedPromise.h" +#include "mozilla/dom/MediaKeySessionBinding.h" +#include "mozilla/dom/MediaKeysBinding.h" +#include "mozilla/dom/MediaKeyMessageEventBinding.h" + +struct JSContext; + +namespace mozilla { +namespace dom { + +class ArrayBufferViewOrArrayBuffer; +class MediaKeyError; +class MediaKeyStatusMap; + +class MediaKeySession final : public DOMEventTargetHelper +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaKeySession, + DOMEventTargetHelper) +public: + MediaKeySession(JSContext* aCx, + nsPIDOMWindowInner* aParent, + MediaKeys* aKeys, + const nsAString& aKeySystem, + MediaKeySessionType aSessionType, + ErrorResult& aRv); + + void SetSessionId(const nsAString& aSessionId); + + JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + // Mark this as resultNotAddRefed to return raw pointers + MediaKeyError* GetError() const; + + MediaKeyStatusMap* KeyStatuses() const; + + void GetKeySystem(nsString& aRetval) const; + + void GetSessionId(nsString& aRetval) const; + + const nsString& GetSessionId() const; + + // Number of ms since epoch at which expiration occurs, or NaN if unknown. + // TODO: The type of this attribute is still under contention. + // https://www.w3.org/Bugs/Public/show_bug.cgi?id=25902 + double Expiration() const; + + Promise* Closed() const; + + already_AddRefed<Promise> GenerateRequest(const nsAString& aInitDataType, + const ArrayBufferViewOrArrayBuffer& aInitData, + ErrorResult& aRv); + + already_AddRefed<Promise> Load(const nsAString& aSessionId, + ErrorResult& aRv); + + already_AddRefed<Promise> Update(const ArrayBufferViewOrArrayBuffer& response, + ErrorResult& aRv); + + already_AddRefed<Promise> Close(ErrorResult& aRv); + + already_AddRefed<Promise> Remove(ErrorResult& aRv); + + void DispatchKeyMessage(MediaKeyMessageType aMessageType, + const nsTArray<uint8_t>& aMessage); + + void DispatchKeyError(uint32_t system_code); + + void DispatchKeyStatusesChange(); + + void OnClosed(); + + bool IsClosed() const; + + void SetExpiration(double aExpiry); + + mozilla::dom::EventHandlerNonNull* GetOnkeystatuseschange(); + void SetOnkeystatuseschange(mozilla::dom::EventHandlerNonNull* aCallback); + + mozilla::dom::EventHandlerNonNull* GetOnmessage(); + void SetOnmessage(mozilla::dom::EventHandlerNonNull* aCallback); + + // Process-unique identifier. + uint32_t Token() const; + +private: + ~MediaKeySession(); + + void UpdateKeyStatusMap(); + + bool IsCallable() const { + // The EME spec sets the "callable value" to true whenever the CDM sets + // the sessionId. When the session is initialized, sessionId is empty and + // callable is thus false. + return !mSessionId.IsEmpty(); + } + + already_AddRefed<DetailedPromise> MakePromise(ErrorResult& aRv, + const nsACString& aName); + + RefPtr<DetailedPromise> mClosed; + + RefPtr<MediaKeyError> mMediaKeyError; + RefPtr<MediaKeys> mKeys; + const nsString mKeySystem; + nsString mSessionId; + const MediaKeySessionType mSessionType; + const uint32_t mToken; + bool mIsClosed; + bool mUninitialized; + RefPtr<MediaKeyStatusMap> mKeyStatusMap; + double mExpiration; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/media/eme/MediaKeyStatusMap.cpp b/dom/media/eme/MediaKeyStatusMap.cpp new file mode 100644 index 000000000..677fd0db2 --- /dev/null +++ b/dom/media/eme/MediaKeyStatusMap.cpp @@ -0,0 +1,120 @@ +/* -*- Mode: C++; tab-width: 8; 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 "mozilla/dom/MediaKeyStatusMap.h" +#include "nsPIDOMWindow.h" +#include "mozilla/dom/UnionTypes.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/EMEUtils.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeyStatusMap) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeyStatusMap) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeyStatusMap) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaKeyStatusMap, mParent) + +MediaKeyStatusMap::MediaKeyStatusMap(nsPIDOMWindowInner* aParent) + : mParent(aParent) +{ +} + +MediaKeyStatusMap::~MediaKeyStatusMap() +{ +} + +JSObject* +MediaKeyStatusMap::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return MediaKeyStatusMapBinding::Wrap(aCx, this, aGivenProto); +} + +nsPIDOMWindowInner* +MediaKeyStatusMap::GetParentObject() const +{ + return mParent; +} + +void +MediaKeyStatusMap::Get(JSContext* aCx, + const ArrayBufferViewOrArrayBuffer& aKey, + JS::MutableHandle<JS::Value> aOutValue, + ErrorResult& aOutRv) const +{ + ArrayData keyId = GetArrayBufferViewOrArrayBufferData(aKey); + if (!keyId.IsValid()) { + aOutValue.setUndefined(); + return; + } + for (const KeyStatus& status : mStatuses) { + if (keyId == status.mKeyId) { + bool ok = ToJSValue(aCx, status.mStatus, aOutValue);
+ if (!ok) {
+ aOutRv.NoteJSContextException(aCx);
+ }
+ return; + } + } + aOutValue.setUndefined(); +} + +bool +MediaKeyStatusMap::Has(const ArrayBufferViewOrArrayBuffer& aKey) const +{ + ArrayData keyId = GetArrayBufferViewOrArrayBufferData(aKey); + if (!keyId.IsValid()) { + return false; + } + + for (const KeyStatus& status : mStatuses) { + if (keyId == status.mKeyId) { + return true; + } + } + + return false; +} + +uint32_t +MediaKeyStatusMap::GetIterableLength() const +{ + return mStatuses.Length(); +} + +TypedArrayCreator<ArrayBuffer> +MediaKeyStatusMap::GetKeyAtIndex(uint32_t aIndex) const +{ + MOZ_ASSERT(aIndex < GetIterableLength()); + return TypedArrayCreator<ArrayBuffer>(mStatuses[aIndex].mKeyId); +} + +MediaKeyStatus +MediaKeyStatusMap::GetValueAtIndex(uint32_t aIndex) const +{ + MOZ_ASSERT(aIndex < GetIterableLength()); + return mStatuses[aIndex].mStatus; +} + +uint32_t +MediaKeyStatusMap::Size() const +{ + return mStatuses.Length(); +} + +void +MediaKeyStatusMap::Update(const nsTArray<CDMCaps::KeyStatus>& aKeys) +{ + mStatuses.Clear(); + for (const auto& key : aKeys) { + mStatuses.InsertElementSorted(KeyStatus(key.mId, key.mStatus)); + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/eme/MediaKeyStatusMap.h b/dom/media/eme/MediaKeyStatusMap.h new file mode 100644 index 000000000..8bfbd4d07 --- /dev/null +++ b/dom/media/eme/MediaKeyStatusMap.h @@ -0,0 +1,96 @@ +/* -*- Mode: C++; tab-width: 8; 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/. */ + +#ifndef mozilla_dom_MediaKeyStatuses_h +#define mozilla_dom_MediaKeyStatuses_h + +#include "mozilla/ErrorResult.h" +#include "mozilla/Attributes.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +#include "mozilla/dom/TypedArray.h" +#include "mozilla/dom/MediaKeyStatusMapBinding.h" +#include "mozilla/CDMCaps.h" + +class nsPIDOMWindowInner; + +namespace mozilla { +namespace dom { + +class ArrayBufferViewOrArrayBuffer; + +// The MediaKeyStatusMap WebIDL interface; maps a keyId to its status. +// Note that the underlying "map" is stored in an array, since we assume +// that a MediaKeySession won't have many key statuses to report. +class MediaKeyStatusMap final : public nsISupports, + public nsWrapperCache +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(MediaKeyStatusMap) + +public: + explicit MediaKeyStatusMap(nsPIDOMWindowInner* aParent); + +protected: + ~MediaKeyStatusMap(); + +public: + nsPIDOMWindowInner* GetParentObject() const; + + JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + void Get(JSContext* aCx, + const ArrayBufferViewOrArrayBuffer& aKey, + JS::MutableHandle<JS::Value> aOutValue, + ErrorResult& aOutRv) const; + bool Has(const ArrayBufferViewOrArrayBuffer& aKey) const; + uint32_t Size() const; + + uint32_t GetIterableLength() const; + TypedArrayCreator<ArrayBuffer> GetKeyAtIndex(uint32_t aIndex) const; + MediaKeyStatus GetValueAtIndex(uint32_t aIndex) const; + + void Update(const nsTArray<CDMCaps::KeyStatus>& keys); + +private: + + nsCOMPtr<nsPIDOMWindowInner> mParent; + + struct KeyStatus { + KeyStatus(const nsTArray<uint8_t>& aKeyId, + MediaKeyStatus aStatus) + : mKeyId(aKeyId) + , mStatus(aStatus) + { + } + bool operator== (const KeyStatus& aOther) const { + return aOther.mKeyId == mKeyId; + } + bool operator<(const KeyStatus& aOther) const { + // Copy chromium and compare keys' bytes. + // Update once https://github.com/w3c/encrypted-media/issues/69 + // is resolved. + const nsTArray<uint8_t>& other = aOther.mKeyId; + const nsTArray<uint8_t>& self = mKeyId; + size_t length = std::min<size_t>(other.Length(), self.Length()); + int cmp = memcmp(self.Elements(), other.Elements(), length); + if (cmp != 0) { + return cmp < 0; + } + return self.Length() <= other.Length(); + } + nsTArray<uint8_t> mKeyId; + MediaKeyStatus mStatus; + }; + + nsTArray<KeyStatus> mStatuses; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/media/eme/MediaKeySystemAccess.cpp b/dom/media/eme/MediaKeySystemAccess.cpp new file mode 100644 index 000000000..c65c57ed0 --- /dev/null +++ b/dom/media/eme/MediaKeySystemAccess.cpp @@ -0,0 +1,1042 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/MediaKeySystemAccess.h" +#include "mozilla/dom/MediaKeySystemAccessBinding.h" +#include "mozilla/Preferences.h" +#include "MediaPrefs.h" +#include "nsContentTypeParser.h" +#ifdef MOZ_FMP4 +#include "MP4Decoder.h" +#endif +#ifdef XP_WIN +#include "mozilla/WindowsVersion.h" +#include "WMFDecoderModule.h" +#endif +#include "nsContentCID.h" +#include "nsServiceManagerUtils.h" +#include "mozIGeckoMediaPluginService.h" +#include "VideoUtils.h" +#include "mozilla/Services.h" +#include "nsIObserverService.h" +#include "mozilla/EMEUtils.h" +#include "GMPUtils.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "nsXULAppAPI.h" +#include "gmp-audio-decode.h" +#include "gmp-video-decode.h" +#include "DecoderDoctorDiagnostics.h" +#include "WebMDecoder.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/ClearOnShutdown.h" +#include "nsUnicharUtils.h" +#include "mozilla/dom/MediaSource.h" +#include "DecoderTraits.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaKeySystemAccess, + mParent) +NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeySystemAccess) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeySystemAccess) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeySystemAccess) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +MediaKeySystemAccess::MediaKeySystemAccess(nsPIDOMWindowInner* aParent, + const nsAString& aKeySystem, + const MediaKeySystemConfiguration& aConfig) + : mParent(aParent) + , mKeySystem(aKeySystem) + , mConfig(aConfig) +{ +} + +MediaKeySystemAccess::~MediaKeySystemAccess() +{ +} + +JSObject* +MediaKeySystemAccess::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return MediaKeySystemAccessBinding::Wrap(aCx, this, aGivenProto); +} + +nsPIDOMWindowInner* +MediaKeySystemAccess::GetParentObject() const +{ + return mParent; +} + +void +MediaKeySystemAccess::GetKeySystem(nsString& aOutKeySystem) const +{ + aOutKeySystem.Assign(mKeySystem); +} + +void +MediaKeySystemAccess::GetConfiguration(MediaKeySystemConfiguration& aConfig) +{ + aConfig = mConfig; +} + +already_AddRefed<Promise> +MediaKeySystemAccess::CreateMediaKeys(ErrorResult& aRv) +{ + RefPtr<MediaKeys> keys(new MediaKeys(mParent, + mKeySystem, + mConfig)); + return keys->Init(aRv); +} + +static bool +HavePluginForKeySystem(const nsCString& aKeySystem) +{ + bool havePlugin = HaveGMPFor(NS_LITERAL_CSTRING(GMP_API_DECRYPTOR), + { aKeySystem }); + return havePlugin; +} + +static MediaKeySystemStatus +EnsureCDMInstalled(const nsAString& aKeySystem, + nsACString& aOutMessage) +{ + if (!HavePluginForKeySystem(NS_ConvertUTF16toUTF8(aKeySystem))) { + aOutMessage = NS_LITERAL_CSTRING("CDM is not installed"); + return MediaKeySystemStatus::Cdm_not_installed; + } + + return MediaKeySystemStatus::Available; +} + +/* static */ +MediaKeySystemStatus +MediaKeySystemAccess::GetKeySystemStatus(const nsAString& aKeySystem, + nsACString& aOutMessage) +{ + MOZ_ASSERT(MediaPrefs::EMEEnabled() || IsClearkeyKeySystem(aKeySystem)); + + if (IsClearkeyKeySystem(aKeySystem)) { + return EnsureCDMInstalled(aKeySystem, aOutMessage); + } + + if (IsWidevineKeySystem(aKeySystem)) { + if (Preferences::GetBool("media.gmp-widevinecdm.visible", false)) { + if (!Preferences::GetBool("media.gmp-widevinecdm.enabled", false)) { + aOutMessage = NS_LITERAL_CSTRING("Widevine EME disabled"); + return MediaKeySystemStatus::Cdm_disabled; + } + return EnsureCDMInstalled(aKeySystem, aOutMessage); + } + } + + return MediaKeySystemStatus::Cdm_not_supported; +} + +typedef nsCString EMECodecString; + +static NS_NAMED_LITERAL_CSTRING(EME_CODEC_AAC, "aac"); +static NS_NAMED_LITERAL_CSTRING(EME_CODEC_OPUS, "opus"); +static NS_NAMED_LITERAL_CSTRING(EME_CODEC_VORBIS, "vorbis"); +static NS_NAMED_LITERAL_CSTRING(EME_CODEC_H264, "h264"); +static NS_NAMED_LITERAL_CSTRING(EME_CODEC_VP8, "vp8"); +static NS_NAMED_LITERAL_CSTRING(EME_CODEC_VP9, "vp9"); + +EMECodecString +ToEMEAPICodecString(const nsString& aCodec) +{ + if (IsAACCodecString(aCodec)) { + return EME_CODEC_AAC; + } + if (aCodec.EqualsLiteral("opus")) { + return EME_CODEC_OPUS; + } + if (aCodec.EqualsLiteral("vorbis")) { + return EME_CODEC_VORBIS; + } + if (IsH264CodecString(aCodec)) { + return EME_CODEC_H264; + } + if (IsVP8CodecString(aCodec)) { + return EME_CODEC_VP8; + } + if (IsVP9CodecString(aCodec)) { + return EME_CODEC_VP9; + } + return EmptyCString(); +} + +// A codec can be decrypted-and-decoded by the CDM, or only decrypted +// by the CDM and decoded by Gecko. Not both. +struct KeySystemContainerSupport +{ + bool IsSupported() const + { + return !mCodecsDecoded.IsEmpty() || !mCodecsDecrypted.IsEmpty(); + } + + // CDM decrypts and decodes using a DRM robust decoder, and passes decoded + // samples back to Gecko for rendering. + bool DecryptsAndDecodes(EMECodecString aCodec) const + { + return mCodecsDecoded.Contains(aCodec); + } + + // CDM decrypts and passes the decrypted samples back to Gecko for decoding. + bool Decrypts(EMECodecString aCodec) const + { + return mCodecsDecrypted.Contains(aCodec); + } + + void SetCanDecryptAndDecode(EMECodecString aCodec) + { + // Can't both decrypt and decrypt-and-decode a codec. + MOZ_ASSERT(!Decrypts(aCodec)); + // Prevent duplicates. + MOZ_ASSERT(!DecryptsAndDecodes(aCodec)); + mCodecsDecoded.AppendElement(aCodec); + } + + void SetCanDecrypt(EMECodecString aCodec) + { + // Prevent duplicates. + MOZ_ASSERT(!Decrypts(aCodec)); + // Can't both decrypt and decrypt-and-decode a codec. + MOZ_ASSERT(!DecryptsAndDecodes(aCodec)); + mCodecsDecrypted.AppendElement(aCodec); + } + +private: + nsTArray<EMECodecString> mCodecsDecoded; + nsTArray<EMECodecString> mCodecsDecrypted; +}; + +enum class KeySystemFeatureSupport +{ + Prohibited = 1, + Requestable = 2, + Required = 3, +}; + +struct KeySystemConfig +{ + nsString mKeySystem; + nsTArray<nsString> mInitDataTypes; + KeySystemFeatureSupport mPersistentState = KeySystemFeatureSupport::Prohibited; + KeySystemFeatureSupport mDistinctiveIdentifier = KeySystemFeatureSupport::Prohibited; + nsTArray<MediaKeySessionType> mSessionTypes; + nsTArray<nsString> mVideoRobustness; + nsTArray<nsString> mAudioRobustness; + KeySystemContainerSupport mMP4; + KeySystemContainerSupport mWebM; +}; + +static nsTArray<KeySystemConfig> +GetSupportedKeySystems() +{ + nsTArray<KeySystemConfig> keySystemConfigs; + + { + if (HavePluginForKeySystem(kEMEKeySystemClearkey)) { + KeySystemConfig clearkey; + clearkey.mKeySystem = NS_ConvertUTF8toUTF16(kEMEKeySystemClearkey); + clearkey.mInitDataTypes.AppendElement(NS_LITERAL_STRING("cenc")); + clearkey.mInitDataTypes.AppendElement(NS_LITERAL_STRING("keyids")); + clearkey.mInitDataTypes.AppendElement(NS_LITERAL_STRING("webm")); + clearkey.mPersistentState = KeySystemFeatureSupport::Requestable; + clearkey.mDistinctiveIdentifier = KeySystemFeatureSupport::Prohibited; + clearkey.mSessionTypes.AppendElement(MediaKeySessionType::Temporary); + if (MediaPrefs::ClearKeyPersistentLicenseEnabled()) { + clearkey.mSessionTypes.AppendElement(MediaKeySessionType::Persistent_license); + } +#if defined(XP_WIN) + // Clearkey CDM uses WMF decoders on Windows. + if (WMFDecoderModule::HasAAC()) { + clearkey.mMP4.SetCanDecryptAndDecode(EME_CODEC_AAC); + } else { + clearkey.mMP4.SetCanDecrypt(EME_CODEC_AAC); + } + if (WMFDecoderModule::HasH264()) { + clearkey.mMP4.SetCanDecryptAndDecode(EME_CODEC_H264); + } else { + clearkey.mMP4.SetCanDecrypt(EME_CODEC_H264); + } +#else + clearkey.mMP4.SetCanDecrypt(EME_CODEC_AAC); + clearkey.mMP4.SetCanDecrypt(EME_CODEC_H264); +#endif + clearkey.mWebM.SetCanDecrypt(EME_CODEC_VORBIS); + clearkey.mWebM.SetCanDecrypt(EME_CODEC_OPUS); + clearkey.mWebM.SetCanDecrypt(EME_CODEC_VP8); + clearkey.mWebM.SetCanDecrypt(EME_CODEC_VP9); + keySystemConfigs.AppendElement(Move(clearkey)); + } + } + { + if (HavePluginForKeySystem(kEMEKeySystemWidevine)) { + KeySystemConfig widevine; + widevine.mKeySystem = NS_ConvertUTF8toUTF16(kEMEKeySystemWidevine); + widevine.mInitDataTypes.AppendElement(NS_LITERAL_STRING("cenc")); + widevine.mInitDataTypes.AppendElement(NS_LITERAL_STRING("keyids")); + widevine.mInitDataTypes.AppendElement(NS_LITERAL_STRING("webm")); + widevine.mPersistentState = KeySystemFeatureSupport::Requestable; + widevine.mDistinctiveIdentifier = KeySystemFeatureSupport::Prohibited; + widevine.mSessionTypes.AppendElement(MediaKeySessionType::Temporary); + widevine.mAudioRobustness.AppendElement(NS_LITERAL_STRING("SW_SECURE_CRYPTO")); + widevine.mVideoRobustness.AppendElement(NS_LITERAL_STRING("SW_SECURE_DECODE")); +#if defined(XP_WIN) + // Widevine CDM doesn't include an AAC decoder. So if WMF can't + // decode AAC, and a codec wasn't specified, be conservative + // and reject the MediaKeys request, since our policy is to prevent + // the Adobe GMP's unencrypted AAC decoding path being used to + // decode content decrypted by the Widevine CDM. + if (WMFDecoderModule::HasAAC()) { + widevine.mMP4.SetCanDecrypt(EME_CODEC_AAC); + } +#endif + + widevine.mMP4.SetCanDecryptAndDecode(EME_CODEC_H264); + widevine.mWebM.SetCanDecrypt(EME_CODEC_VORBIS); + widevine.mWebM.SetCanDecrypt(EME_CODEC_OPUS); + widevine.mWebM.SetCanDecryptAndDecode(EME_CODEC_VP8); + widevine.mWebM.SetCanDecryptAndDecode(EME_CODEC_VP9); + keySystemConfigs.AppendElement(Move(widevine)); + } + } + + return keySystemConfigs; +} + +static bool +GetKeySystemConfig(const nsAString& aKeySystem, KeySystemConfig& aOutKeySystemConfig) +{ + for (auto&& config : GetSupportedKeySystems()) { + if (config.mKeySystem.Equals(aKeySystem)) { + aOutKeySystemConfig = mozilla::Move(config); + return true; + } + } + // No matching key system found. + return false; +} + +/* static */ +bool +MediaKeySystemAccess::KeySystemSupportsInitDataType(const nsAString& aKeySystem, + const nsAString& aInitDataType) +{ + KeySystemConfig implementation; + return GetKeySystemConfig(aKeySystem, implementation) && + implementation.mInitDataTypes.Contains(aInitDataType); +} + +enum CodecType +{ + Audio, + Video, + Invalid +}; + +static bool +CanDecryptAndDecode(const nsString& aKeySystem, + const nsString& aContentType, + CodecType aCodecType, + const KeySystemContainerSupport& aContainerSupport, + const nsTArray<EMECodecString>& aCodecs, + DecoderDoctorDiagnostics* aDiagnostics) +{ + MOZ_ASSERT(aCodecType != Invalid); + for (const EMECodecString& codec : aCodecs) { + MOZ_ASSERT(!codec.IsEmpty()); + + if (aContainerSupport.DecryptsAndDecodes(codec)) { + // GMP can decrypt-and-decode this codec. + continue; + } + + if (aContainerSupport.Decrypts(codec) && + NS_SUCCEEDED(MediaSource::IsTypeSupported(aContentType, aDiagnostics))) { + // GMP can decrypt and is allowed to return compressed samples to + // Gecko to decode, and Gecko has a decoder. + continue; + } + + // Neither the GMP nor Gecko can both decrypt and decode. We don't + // support this codec. + +#if defined(XP_WIN) + // Widevine CDM doesn't include an AAC decoder. So if WMF can't + // decode AAC, and a codec wasn't specified, be conservative + // and reject the MediaKeys request, since our policy is to prevent + // the Adobe GMP's unencrypted AAC decoding path being used to + // decode content decrypted by the Widevine CDM. + if (codec == EME_CODEC_AAC && + IsWidevineKeySystem(aKeySystem) && + !WMFDecoderModule::HasAAC()) { + if (aDiagnostics) { + aDiagnostics->SetKeySystemIssue( + DecoderDoctorDiagnostics::eWidevineWithNoWMF); + } + } +#endif + return false; + } + return true; +} + +static bool +ToSessionType(const nsAString& aSessionType, MediaKeySessionType& aOutType) +{ + using MediaKeySessionTypeValues::strings; + const char* temporary = + strings[static_cast<uint32_t>(MediaKeySessionType::Temporary)].value; + if (aSessionType.EqualsASCII(temporary)) { + aOutType = MediaKeySessionType::Temporary; + return true; + } + const char* persistentLicense = + strings[static_cast<uint32_t>(MediaKeySessionType::Persistent_license)].value; + if (aSessionType.EqualsASCII(persistentLicense)) { + aOutType = MediaKeySessionType::Persistent_license; + return true; + } + return false; +} + +// 5.2.1 Is persistent session type? +static bool +IsPersistentSessionType(MediaKeySessionType aSessionType) +{ + return aSessionType == MediaKeySessionType::Persistent_license; +} + +CodecType +GetMajorType(const nsAString& aContentType) +{ + if (CaseInsensitiveFindInReadable(NS_LITERAL_STRING("audio/"), aContentType)) { + return Audio; + } + if (CaseInsensitiveFindInReadable(NS_LITERAL_STRING("video/"), aContentType)) { + return Video; + } + return Invalid; +} + +static CodecType +GetCodecType(const EMECodecString& aCodec) +{ + if (aCodec.Equals(EME_CODEC_AAC) || + aCodec.Equals(EME_CODEC_OPUS) || + aCodec.Equals(EME_CODEC_VORBIS)) { + return Audio; + } + if (aCodec.Equals(EME_CODEC_H264) || + aCodec.Equals(EME_CODEC_VP8) || + aCodec.Equals(EME_CODEC_VP9)) { + return Video; + } + return Invalid; +} + +static bool +AllCodecsOfType(const nsTArray<EMECodecString>& aCodecs, const CodecType aCodecType) +{ + for (const EMECodecString& codec : aCodecs) { + if (GetCodecType(codec) != aCodecType) { + return false; + } + } + return true; +} + +static bool +IsParameterUnrecognized(const nsAString& aContentType) +{ + nsAutoString contentType(aContentType); + contentType.StripWhitespace(); + + nsTArray<nsString> params; + nsAString::const_iterator start, end, semicolon, equalSign; + contentType.BeginReading(start); + contentType.EndReading(end); + semicolon = start; + // Find any substring between ';' & '='. + while (semicolon != end) { + if (FindCharInReadable(';', semicolon, end)) { + equalSign = ++semicolon; + if (FindCharInReadable('=', equalSign, end)) { + params.AppendElement(Substring(semicolon, equalSign)); + semicolon = equalSign; + } + } + } + + for (auto param : params) { + if (!param.LowerCaseEqualsLiteral("codecs") && + !param.LowerCaseEqualsLiteral("profiles")) { + return true; + } + } + return false; +} + +// 3.1.2.3 Get Supported Capabilities for Audio/Video Type +static Sequence<MediaKeySystemMediaCapability> +GetSupportedCapabilities(const CodecType aCodecType, + const nsTArray<MediaKeySystemMediaCapability>& aRequestedCapabilities, + const MediaKeySystemConfiguration& aPartialConfig, + const KeySystemConfig& aKeySystem, + DecoderDoctorDiagnostics* aDiagnostics) +{ + // Let local accumulated configuration be a local copy of partial configuration. + // (Note: It's not necessary for us to maintain a local copy, as we don't need + // to test whether capabilites from previous calls to this algorithm work with + // the capabilities currently being considered in this call. ) + + // Let supported media capabilities be an empty sequence of + // MediaKeySystemMediaCapability dictionaries. + Sequence<MediaKeySystemMediaCapability> supportedCapabilities; + + // For each requested media capability in requested media capabilities: + for (const MediaKeySystemMediaCapability& capabilities : aRequestedCapabilities) { + // Let content type be requested media capability's contentType member. + const nsString& contentType = capabilities.mContentType; + // Let robustness be requested media capability's robustness member. + const nsString& robustness = capabilities.mRobustness; + // If content type is the empty string, return null. + if (contentType.IsEmpty()) { + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') rejected; " + "audio or video capability has empty contentType.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).get()); + return Sequence<MediaKeySystemMediaCapability>(); + } + // If content type is an invalid or unrecognized MIME type, continue + // to the next iteration. + nsAutoString container; + nsTArray<nsString> codecStrings; + if (!ParseMIMETypeString(contentType, container, codecStrings)) { + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') unsupported; " + "failed to parse contentType as MIME type.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).get()); + continue; + } + bool invalid = false; + nsTArray<EMECodecString> codecs; + for (const nsString& codecString : codecStrings) { + EMECodecString emeCodec = ToEMEAPICodecString(codecString); + if (emeCodec.IsEmpty()) { + invalid = true; + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') unsupported; " + "'%s' is an invalid codec string.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).get(), + NS_ConvertUTF16toUTF8(codecString).get()); + break; + } + codecs.AppendElement(emeCodec); + } + if (invalid) { + continue; + } + + // If the user agent does not support container, continue to the next iteration. + // The case-sensitivity of string comparisons is determined by the appropriate RFC. + // (Note: Per RFC 6838 [RFC6838], "Both top-level type and subtype names are + // case-insensitive."'. We're using nsContentTypeParser and that is + // case-insensitive and converts all its parameter outputs to lower case.) + NS_ConvertUTF16toUTF8 container_utf8(container); + const bool isMP4 = DecoderTraits::IsMP4TypeAndEnabled(container_utf8, aDiagnostics); + if (isMP4 && !aKeySystem.mMP4.IsSupported()) { + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') unsupported; " + "MP4 requested but unsupported.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).get()); + continue; + } + const bool isWebM = DecoderTraits::IsWebMTypeAndEnabled(container_utf8); + if (isWebM && !aKeySystem.mWebM.IsSupported()) { + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') unsupported; " + "WebM requested but unsupported.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).get()); + continue; + } + if (!isMP4 && !isWebM) { + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') unsupported; " + "Unsupported or unrecognized container requested.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).get()); + continue; + } + + // Let parameters be the RFC 6381[RFC6381] parameters, if any, specified by + // content type. + // If the user agent does not recognize one or more parameters, continue to + // the next iteration. + if (IsParameterUnrecognized(contentType)) { + continue; + } + + // Let media types be the set of codecs and codec constraints specified by + // parameters. The case-sensitivity of string comparisons is determined by + // the appropriate RFC or other specification. + // (Note: codecs array is 'parameter'). + + // If media types is empty: + if (codecs.IsEmpty()) { + // If container normatively implies a specific set of codecs and codec constraints: + // Let parameters be that set. + if (isMP4) { + if (aCodecType == Audio) { + codecs.AppendElement(EME_CODEC_AAC); + } else if (aCodecType == Video) { + codecs.AppendElement(EME_CODEC_H264); + } + } else if (isWebM) { + if (aCodecType == Audio) { + codecs.AppendElement(EME_CODEC_VORBIS); + } else if (aCodecType == Video) { + codecs.AppendElement(EME_CODEC_VP8); + } + } + // Otherwise: Continue to the next iteration. + // (Note: all containers we support have implied codecs, so don't continue here.) + } + + // If content type is not strictly a audio/video type, continue to the next iteration. + const auto majorType = GetMajorType(container); + if (majorType == Invalid) { + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') unsupported; " + "MIME type is not an audio or video MIME type.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).get()); + continue; + } + if (majorType != aCodecType || !AllCodecsOfType(codecs, aCodecType)) { + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') unsupported; " + "MIME type mixes audio codecs in video capabilities " + "or video codecs in audio capabilities.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).get()); + continue; + } + // If robustness is not the empty string and contains an unrecognized + // value or a value not supported by implementation, continue to the + // next iteration. String comparison is case-sensitive. + if (!robustness.IsEmpty()) { + if (majorType == Audio && !aKeySystem.mAudioRobustness.Contains(robustness)) { + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') unsupported; " + "unsupported robustness string.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).get()); + continue; + } + if (majorType == Video && !aKeySystem.mVideoRobustness.Contains(robustness)) { + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') unsupported; " + "unsupported robustness string.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).get()); + continue; + } + // Note: specified robustness requirements are satisfied. + } + + // If the user agent and implementation definitely support playback of + // encrypted media data for the combination of container, media types, + // robustness and local accumulated configuration in combination with + // restrictions... + const auto& containerSupport = isMP4 ? aKeySystem.mMP4 : aKeySystem.mWebM; + if (!CanDecryptAndDecode(aKeySystem.mKeySystem, + contentType, + majorType, + containerSupport, + codecs, + aDiagnostics)) { + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') unsupported; " + "codec unsupported by CDM requested.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).get()); + continue; + } + + // ... add requested media capability to supported media capabilities. + if (!supportedCapabilities.AppendElement(capabilities, mozilla::fallible)) { + NS_WARNING("GetSupportedCapabilities: Malloc failure"); + return Sequence<MediaKeySystemMediaCapability>(); + } + + // Note: omitting steps 3.13.2, our robustness is not sophisticated enough + // to require considering all requirements together. + } + return Move(supportedCapabilities); +} + +// "Get Supported Configuration and Consent" algorithm, steps 4-7 for +// distinctive identifier, and steps 8-11 for persistent state. The steps +// are the same for both requirements/features, so we factor them out into +// a single function. +static bool +CheckRequirement(const MediaKeysRequirement aRequirement, + const KeySystemFeatureSupport aFeatureSupport, + MediaKeysRequirement& aOutRequirement) +{ + // Let requirement be the value of candidate configuration's member. + MediaKeysRequirement requirement = aRequirement; + // If requirement is "optional" and feature is not allowed according to + // restrictions, set requirement to "not-allowed". + if (aRequirement == MediaKeysRequirement::Optional && + aFeatureSupport == KeySystemFeatureSupport::Prohibited) { + requirement = MediaKeysRequirement::Not_allowed; + } + + // Follow the steps for requirement from the following list: + switch (requirement) { + case MediaKeysRequirement::Required: { + // If the implementation does not support use of requirement in combination + // with accumulated configuration and restrictions, return NotSupported. + if (aFeatureSupport == KeySystemFeatureSupport::Prohibited) { + return false; + } + break; + } + case MediaKeysRequirement::Optional: { + // Continue with the following steps. + break; + } + case MediaKeysRequirement::Not_allowed: { + // If the implementation requires use of feature in combination with + // accumulated configuration and restrictions, return NotSupported. + if (aFeatureSupport == KeySystemFeatureSupport::Required) { + return false; + } + break; + } + default: { + return false; + } + } + + // Set the requirement member of accumulated configuration to equal + // calculated requirement. + aOutRequirement = requirement; + + return true; +} + +// 3.1.2.2, step 12 +// Follow the steps for the first matching condition from the following list: +// If the sessionTypes member is present in candidate configuration. +// Let session types be candidate configuration's sessionTypes member. +// Otherwise let session types be ["temporary"]. +// Note: This returns an empty array on malloc failure. +static Sequence<nsString> +UnboxSessionTypes(const Optional<Sequence<nsString>>& aSessionTypes) +{ + Sequence<nsString> sessionTypes; + if (aSessionTypes.WasPassed()) { + sessionTypes = aSessionTypes.Value(); + } else { + using MediaKeySessionTypeValues::strings; + const char* temporary = strings[static_cast<uint32_t>(MediaKeySessionType::Temporary)].value; + // Note: fallible. Results in an empty array. + sessionTypes.AppendElement(NS_ConvertUTF8toUTF16(nsDependentCString(temporary)), mozilla::fallible); + } + return sessionTypes; +} + +// 3.1.2.2 Get Supported Configuration and Consent +static bool +GetSupportedConfig(const KeySystemConfig& aKeySystem, + const MediaKeySystemConfiguration& aCandidate, + MediaKeySystemConfiguration& aOutConfig, + DecoderDoctorDiagnostics* aDiagnostics) +{ + // Let accumulated configuration be a new MediaKeySystemConfiguration dictionary. + MediaKeySystemConfiguration config; + // Set the label member of accumulated configuration to equal the label member of + // candidate configuration. + config.mLabel = aCandidate.mLabel; + // If the initDataTypes member of candidate configuration is non-empty, run the + // following steps: + if (!aCandidate.mInitDataTypes.IsEmpty()) { + // Let supported types be an empty sequence of DOMStrings. + nsTArray<nsString> supportedTypes; + // For each value in candidate configuration's initDataTypes member: + for (const nsString& initDataType : aCandidate.mInitDataTypes) { + // Let initDataType be the value. + // If the implementation supports generating requests based on initDataType, + // add initDataType to supported types. String comparison is case-sensitive. + // The empty string is never supported. + if (aKeySystem.mInitDataTypes.Contains(initDataType)) { + supportedTypes.AppendElement(initDataType); + } + } + // If supported types is empty, return NotSupported. + if (supportedTypes.IsEmpty()) { + EME_LOG("MediaKeySystemConfiguration (label='%s') rejected; " + "no supported initDataTypes provided.", + NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); + return false; + } + // Set the initDataTypes member of accumulated configuration to supported types. + if (!config.mInitDataTypes.Assign(supportedTypes)) { + return false; + } + } + + if (!CheckRequirement(aCandidate.mDistinctiveIdentifier, + aKeySystem.mDistinctiveIdentifier, + config.mDistinctiveIdentifier)) { + EME_LOG("MediaKeySystemConfiguration (label='%s') rejected; " + "distinctiveIdentifier requirement not satisfied.", + NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); + return false; + } + + if (!CheckRequirement(aCandidate.mPersistentState, + aKeySystem.mPersistentState, + config.mPersistentState)) { + EME_LOG("MediaKeySystemConfiguration (label='%s') rejected; " + "persistentState requirement not satisfied.", + NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); + return false; + } + + Sequence<nsString> sessionTypes(UnboxSessionTypes(aCandidate.mSessionTypes)); + if (sessionTypes.IsEmpty()) { + // Malloc failure. + return false; + } + + // For each value in session types: + for (const auto& sessionTypeString : sessionTypes) { + // Let session type be the value. + MediaKeySessionType sessionType; + if (!ToSessionType(sessionTypeString, sessionType)) { + // (Assume invalid sessionType is unsupported as per steps below). + EME_LOG("MediaKeySystemConfiguration (label='%s') rejected; " + "invalid session type specified.", + NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); + return false; + } + // If accumulated configuration's persistentState value is "not-allowed" + // and the Is persistent session type? algorithm returns true for session + // type return NotSupported. + if (config.mPersistentState == MediaKeysRequirement::Not_allowed && + IsPersistentSessionType(sessionType)) { + EME_LOG("MediaKeySystemConfiguration (label='%s') rejected; " + "persistent session requested but keysystem doesn't" + "support persistent state.", + NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); + return false; + } + // If the implementation does not support session type in combination + // with accumulated configuration and restrictions for other reasons, + // return NotSupported. + if (!aKeySystem.mSessionTypes.Contains(sessionType)) { + EME_LOG("MediaKeySystemConfiguration (label='%s') rejected; " + "session type '%s' unsupported by keySystem.", + NS_ConvertUTF16toUTF8(aCandidate.mLabel).get(), + NS_ConvertUTF16toUTF8(sessionTypeString).get()); + return false; + } + // If accumulated configuration's persistentState value is "optional" + // and the result of running the Is persistent session type? algorithm + // on session type is true, change accumulated configuration's + // persistentState value to "required". + if (config.mPersistentState == MediaKeysRequirement::Optional && + IsPersistentSessionType(sessionType)) { + config.mPersistentState = MediaKeysRequirement::Required; + } + } + // Set the sessionTypes member of accumulated configuration to session types. + config.mSessionTypes.Construct(Move(sessionTypes)); + + // If the videoCapabilities and audioCapabilities members in candidate + // configuration are both empty, return NotSupported. + // TODO: Most sites using EME still don't pass capabilities, so we + // can't reject on it yet without breaking them. So add this later. + + // If the videoCapabilities member in candidate configuration is non-empty: + if (!aCandidate.mVideoCapabilities.IsEmpty()) { + // Let video capabilities be the result of executing the Get Supported + // Capabilities for Audio/Video Type algorithm on Video, candidate + // configuration's videoCapabilities member, accumulated configuration, + // and restrictions. + Sequence<MediaKeySystemMediaCapability> caps = + GetSupportedCapabilities(Video, + aCandidate.mVideoCapabilities, + config, + aKeySystem, + aDiagnostics); + // If video capabilities is null, return NotSupported. + if (caps.IsEmpty()) { + EME_LOG("MediaKeySystemConfiguration (label='%s') rejected; " + "no supported video capabilities.", + NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); + return false; + } + // Set the videoCapabilities member of accumulated configuration to video capabilities. + config.mVideoCapabilities = Move(caps); + } else { + // Otherwise: + // Set the videoCapabilities member of accumulated configuration to an empty sequence. + } + + // If the audioCapabilities member in candidate configuration is non-empty: + if (!aCandidate.mAudioCapabilities.IsEmpty()) { + // Let audio capabilities be the result of executing the Get Supported Capabilities + // for Audio/Video Type algorithm on Audio, candidate configuration's audioCapabilities + // member, accumulated configuration, and restrictions. + Sequence<MediaKeySystemMediaCapability> caps = + GetSupportedCapabilities(Audio, + aCandidate.mAudioCapabilities, + config, + aKeySystem, + aDiagnostics); + // If audio capabilities is null, return NotSupported. + if (caps.IsEmpty()) { + EME_LOG("MediaKeySystemConfiguration (label='%s') rejected; " + "no supported audio capabilities.", + NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); + return false; + } + // Set the audioCapabilities member of accumulated configuration to audio capabilities. + config.mAudioCapabilities = Move(caps); + } else { + // Otherwise: + // Set the audioCapabilities member of accumulated configuration to an empty sequence. + } + + // If accumulated configuration's distinctiveIdentifier value is "optional", follow the + // steps for the first matching condition from the following list: + if (config.mDistinctiveIdentifier == MediaKeysRequirement::Optional) { + // If the implementation requires use Distinctive Identifier(s) or + // Distinctive Permanent Identifier(s) for any of the combinations + // in accumulated configuration + if (aKeySystem.mDistinctiveIdentifier == KeySystemFeatureSupport::Required) { + // Change accumulated configuration's distinctiveIdentifier value to "required". + config.mDistinctiveIdentifier = MediaKeysRequirement::Required; + } else { + // Otherwise, change accumulated configuration's distinctiveIdentifier + // value to "not-allowed". + config.mDistinctiveIdentifier = MediaKeysRequirement::Not_allowed; + } + } + + // If accumulated configuration's persistentState value is "optional", follow the + // steps for the first matching condition from the following list: + if (config.mPersistentState == MediaKeysRequirement::Optional) { + // If the implementation requires persisting state for any of the combinations + // in accumulated configuration + if (aKeySystem.mPersistentState == KeySystemFeatureSupport::Required) { + // Change accumulated configuration's persistentState value to "required". + config.mPersistentState = MediaKeysRequirement::Required; + } else { + // Otherwise, change accumulated configuration's persistentState + // value to "not-allowed". + config.mPersistentState = MediaKeysRequirement::Not_allowed; + } + } + + // Note: Omitting steps 20-22. We don't ask for consent. + +#if defined(XP_WIN) + // Widevine CDM doesn't include an AAC decoder. So if WMF can't decode AAC, + // and a codec wasn't specified, be conservative and reject the MediaKeys request. + if (IsWidevineKeySystem(aKeySystem.mKeySystem) && + (aCandidate.mAudioCapabilities.IsEmpty() || + aCandidate.mVideoCapabilities.IsEmpty()) && + !WMFDecoderModule::HasAAC()) { + if (aDiagnostics) { + aDiagnostics->SetKeySystemIssue( + DecoderDoctorDiagnostics::eWidevineWithNoWMF); + } + EME_LOG("MediaKeySystemConfiguration (label='%s') rejected; " + "WMF required for Widevine decoding, but it's not available.", + NS_ConvertUTF16toUTF8(aCandidate.mLabel).get()); + return false; + } +#endif + + // Return accumulated configuration. + aOutConfig = config; + + return true; +} + +/* static */ +bool +MediaKeySystemAccess::GetSupportedConfig(const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfigs, + MediaKeySystemConfiguration& aOutConfig, + DecoderDoctorDiagnostics* aDiagnostics) +{ + KeySystemConfig implementation; + if (!GetKeySystemConfig(aKeySystem, implementation)) { + return false; + } + for (const MediaKeySystemConfiguration& candidate : aConfigs) { + if (mozilla::dom::GetSupportedConfig(implementation, + candidate, + aOutConfig, + aDiagnostics)) { + return true; + } + } + + return false; +} + + +/* static */ +void +MediaKeySystemAccess::NotifyObservers(nsPIDOMWindowInner* aWindow, + const nsAString& aKeySystem, + MediaKeySystemStatus aStatus) +{ + RequestMediaKeySystemAccessNotification data; + data.mKeySystem = aKeySystem; + data.mStatus = aStatus; + nsAutoString json; + data.ToJSON(json); + EME_LOG("MediaKeySystemAccess::NotifyObservers() %s", NS_ConvertUTF16toUTF8(json).get()); + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (obs) { + obs->NotifyObservers(aWindow, "mediakeys-request", json.get()); + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/eme/MediaKeySystemAccess.h b/dom/media/eme/MediaKeySystemAccess.h new file mode 100644 index 000000000..a166ac22e --- /dev/null +++ b/dom/media/eme/MediaKeySystemAccess.h @@ -0,0 +1,81 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 mozilla_dom_MediaKeySystemAccess_h +#define mozilla_dom_MediaKeySystemAccess_h + +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/MediaKeySystemAccessBinding.h" +#include "mozilla/dom/MediaKeysRequestStatusBinding.h" + +#include "js/TypeDecls.h" + +namespace mozilla { + +class DecoderDoctorDiagnostics; + +namespace dom { + +class MediaKeySystemAccess final : public nsISupports, + public nsWrapperCache +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(MediaKeySystemAccess) + +public: + explicit MediaKeySystemAccess(nsPIDOMWindowInner* aParent, + const nsAString& aKeySystem, + const MediaKeySystemConfiguration& aConfig); + +protected: + ~MediaKeySystemAccess(); + +public: + nsPIDOMWindowInner* GetParentObject() const; + + JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + void GetKeySystem(nsString& aRetVal) const; + + void GetConfiguration(MediaKeySystemConfiguration& aConfig); + + already_AddRefed<Promise> CreateMediaKeys(ErrorResult& aRv); + + static MediaKeySystemStatus GetKeySystemStatus(const nsAString& aKeySystem, + nsACString& aOutExceptionMessage); + + static bool IsSupported(const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfigs, + DecoderDoctorDiagnostics* aDiagnostics); + + static void NotifyObservers(nsPIDOMWindowInner* aWindow, + const nsAString& aKeySystem, + MediaKeySystemStatus aStatus); + + static bool GetSupportedConfig(const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfigs, + MediaKeySystemConfiguration& aOutConfig, + DecoderDoctorDiagnostics* aDiagnostics); + + static bool KeySystemSupportsInitDataType(const nsAString& aKeySystem, + const nsAString& aInitDataType); + +private: + nsCOMPtr<nsPIDOMWindowInner> mParent; + const nsString mKeySystem; + const MediaKeySystemConfiguration mConfig; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MediaKeySystemAccess_h diff --git a/dom/media/eme/MediaKeySystemAccessManager.cpp b/dom/media/eme/MediaKeySystemAccessManager.cpp new file mode 100644 index 000000000..a1e1254ad --- /dev/null +++ b/dom/media/eme/MediaKeySystemAccessManager.cpp @@ -0,0 +1,336 @@ +/* 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 "MediaKeySystemAccessManager.h" +#include "DecoderDoctorDiagnostics.h" +#include "MediaPrefs.h" +#include "mozilla/EMEUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsIObserverService.h" +#include "mozilla/Services.h" +#include "mozilla/DetailedPromise.h" +#ifdef XP_WIN +#include "mozilla/WindowsVersion.h" +#endif +#include "nsPrintfCString.h" + +namespace mozilla { +namespace dom { + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeySystemAccessManager) + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY(nsIObserver) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeySystemAccessManager) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeySystemAccessManager) + +NS_IMPL_CYCLE_COLLECTION_CLASS(MediaKeySystemAccessManager) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaKeySystemAccessManager) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow) + for (size_t i = 0; i < tmp->mRequests.Length(); i++) { + tmp->mRequests[i].RejectPromise(NS_LITERAL_CSTRING("Promise still outstanding at MediaKeySystemAccessManager GC")); + tmp->mRequests[i].CancelTimer(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mRequests[i].mPromise) + } + tmp->mRequests.Clear(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MediaKeySystemAccessManager) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow) + for (size_t i = 0; i < tmp->mRequests.Length(); i++) { + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRequests[i].mPromise) + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +MediaKeySystemAccessManager::MediaKeySystemAccessManager(nsPIDOMWindowInner* aWindow) + : mWindow(aWindow) + , mAddedObservers(false) +{ +} + +MediaKeySystemAccessManager::~MediaKeySystemAccessManager() +{ + Shutdown(); +} + +void +MediaKeySystemAccessManager::Request(DetailedPromise* aPromise, + const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfigs) +{ + Request(aPromise, aKeySystem, aConfigs, RequestType::Initial); +} + +void +MediaKeySystemAccessManager::Request(DetailedPromise* aPromise, + const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfigs, + RequestType aType) +{ + EME_LOG("MediaKeySystemAccessManager::Request %s", NS_ConvertUTF16toUTF8(aKeySystem).get()); + + if (aKeySystem.IsEmpty()) { + aPromise->MaybeReject(NS_ERROR_DOM_TYPE_ERR, + NS_LITERAL_CSTRING("Key system string is empty")); + // Don't notify DecoderDoctor, as there's nothing we or the user can + // do to fix this situation; the site is using the API wrong. + return; + } + if (aConfigs.IsEmpty()) { + aPromise->MaybeReject(NS_ERROR_DOM_TYPE_ERR, + NS_LITERAL_CSTRING("Candidate MediaKeySystemConfigs is empty")); + // Don't notify DecoderDoctor, as there's nothing we or the user can + // do to fix this situation; the site is using the API wrong. + return; + } + + DecoderDoctorDiagnostics diagnostics; + + // Ensure keysystem is supported. + if (!IsWidevineKeySystem(aKeySystem) && + !IsClearkeyKeySystem(aKeySystem)) { + // Not to inform user, because nothing to do if the keySystem is not + // supported. + aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR, + NS_LITERAL_CSTRING("Key system is unsupported")); + diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), + aKeySystem, false, __func__); + return; + } + + if (!MediaPrefs::EMEEnabled() && !IsClearkeyKeySystem(aKeySystem)) { + // EME disabled by user, send notification to chrome so UI can inform user. + // Clearkey is allowed even when EME is disabled because we want the pref + // "media.eme.enabled" only taking effect on proprietary DRMs. + MediaKeySystemAccess::NotifyObservers(mWindow, + aKeySystem, + MediaKeySystemStatus::Api_disabled); + aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR, + NS_LITERAL_CSTRING("EME has been preffed off")); + diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), + aKeySystem, false, __func__); + return; + } + + nsAutoCString message; + MediaKeySystemStatus status = + MediaKeySystemAccess::GetKeySystemStatus(aKeySystem, message); + + nsPrintfCString msg("MediaKeySystemAccess::GetKeySystemStatus(%s) " + "result=%s msg='%s'", + NS_ConvertUTF16toUTF8(aKeySystem).get(), + MediaKeySystemStatusValues::strings[(size_t)status].value, + message.get()); + LogToBrowserConsole(NS_ConvertUTF8toUTF16(msg)); + + if (status == MediaKeySystemStatus::Cdm_not_installed && + IsWidevineKeySystem(aKeySystem)) { + // These are cases which could be resolved by downloading a new(er) CDM. + // When we send the status to chrome, chrome's GMPProvider will attempt to + // download or update the CDM. In AwaitInstall() we add listeners to wait + // for the update to complete, and we'll call this function again with + // aType==Subsequent once the download has completed and the GMPService + // has had a new plugin added. AwaitInstall() sets a timer to fail if the + // update/download takes too long or fails. + if (aType == RequestType::Initial && + AwaitInstall(aPromise, aKeySystem, aConfigs)) { + // Notify chrome that we're going to wait for the CDM to download/update. + // Note: If we're re-trying, we don't re-send the notification, + // as chrome is already displaying the "we can't play, updating" + // notification. + MediaKeySystemAccess::NotifyObservers(mWindow, aKeySystem, status); + } else { + // We waited or can't wait for an update and we still can't service + // the request. Give up. Chrome will still be showing a "I can't play, + // updating" notification. + aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR, + NS_LITERAL_CSTRING("Gave up while waiting for a CDM update")); + } + diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), + aKeySystem, false, __func__); + return; + } + if (status != MediaKeySystemStatus::Available) { + // Failed due to user disabling something, send a notification to + // chrome, so we can show some UI to explain how the user can rectify + // the situation. + MediaKeySystemAccess::NotifyObservers(mWindow, aKeySystem, status); + aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR, message); + return; + } + + MediaKeySystemConfiguration config; + if (MediaKeySystemAccess::GetSupportedConfig(aKeySystem, aConfigs, config, &diagnostics)) { + RefPtr<MediaKeySystemAccess> access( + new MediaKeySystemAccess(mWindow, aKeySystem, config)); + aPromise->MaybeResolve(access); + diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), + aKeySystem, true, __func__); + return; + } + // Not to inform user, because nothing to do if the corresponding keySystem + // configuration is not supported. + aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR, + NS_LITERAL_CSTRING("Key system configuration is not supported")); + diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), + aKeySystem, false, __func__); +} + +MediaKeySystemAccessManager::PendingRequest::PendingRequest(DetailedPromise* aPromise, + const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfigs, + nsITimer* aTimer) + : mPromise(aPromise) + , mKeySystem(aKeySystem) + , mConfigs(aConfigs) + , mTimer(aTimer) +{ + MOZ_COUNT_CTOR(MediaKeySystemAccessManager::PendingRequest); +} + +MediaKeySystemAccessManager::PendingRequest::PendingRequest(const PendingRequest& aOther) + : mPromise(aOther.mPromise) + , mKeySystem(aOther.mKeySystem) + , mConfigs(aOther.mConfigs) + , mTimer(aOther.mTimer) +{ + MOZ_COUNT_CTOR(MediaKeySystemAccessManager::PendingRequest); +} + +MediaKeySystemAccessManager::PendingRequest::~PendingRequest() +{ + MOZ_COUNT_DTOR(MediaKeySystemAccessManager::PendingRequest); +} + +void +MediaKeySystemAccessManager::PendingRequest::CancelTimer() +{ + if (mTimer) { + mTimer->Cancel(); + } +} + +void +MediaKeySystemAccessManager::PendingRequest::RejectPromise(const nsCString& aReason) +{ + if (mPromise) { + mPromise->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR, aReason); + } +} + +bool +MediaKeySystemAccessManager::AwaitInstall(DetailedPromise* aPromise, + const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfigs) +{ + EME_LOG("MediaKeySystemAccessManager::AwaitInstall %s", NS_ConvertUTF16toUTF8(aKeySystem).get()); + + if (!EnsureObserversAdded()) { + NS_WARNING("Failed to add pref observer"); + return false; + } + + nsCOMPtr<nsITimer> timer(do_CreateInstance("@mozilla.org/timer;1")); + if (!timer || NS_FAILED(timer->Init(this, 60 * 1000, nsITimer::TYPE_ONE_SHOT))) { + NS_WARNING("Failed to create timer to await CDM install."); + return false; + } + + mRequests.AppendElement(PendingRequest(aPromise, aKeySystem, aConfigs, timer)); + return true; +} + +void +MediaKeySystemAccessManager::RetryRequest(PendingRequest& aRequest) +{ + aRequest.CancelTimer(); + Request(aRequest.mPromise, aRequest.mKeySystem, aRequest.mConfigs, RequestType::Subsequent); +} + +nsresult +MediaKeySystemAccessManager::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + EME_LOG("MediaKeySystemAccessManager::Observe %s", aTopic); + + if (!strcmp(aTopic, "gmp-changed")) { + // Filter out the requests where the CDM's install-status is no longer + // "unavailable". This will be the CDMs which have downloaded since the + // initial request. + // Note: We don't have a way to communicate from chrome that the CDM has + // failed to download, so we'll just let the timeout fail us in that case. + nsTArray<PendingRequest> requests; + for (size_t i = mRequests.Length(); i-- > 0; ) { + PendingRequest& request = mRequests[i]; + nsAutoCString message; + MediaKeySystemStatus status = + MediaKeySystemAccess::GetKeySystemStatus(request.mKeySystem, message); + if (status == MediaKeySystemStatus::Cdm_not_installed) { + // Not yet installed, don't retry. Keep waiting until timeout. + continue; + } + // Status has changed, retry request. + requests.AppendElement(Move(request)); + mRequests.RemoveElementAt(i); + } + // Retry all pending requests, but this time fail if the CDM is not installed. + for (PendingRequest& request : requests) { + RetryRequest(request); + } + } else if (!strcmp(aTopic, "timer-callback")) { + // Find the timer that expired and re-run the request for it. + nsCOMPtr<nsITimer> timer(do_QueryInterface(aSubject)); + for (size_t i = 0; i < mRequests.Length(); i++) { + if (mRequests[i].mTimer == timer) { + EME_LOG("MediaKeySystemAccessManager::AwaitInstall resuming request"); + PendingRequest request = mRequests[i]; + mRequests.RemoveElementAt(i); + RetryRequest(request); + break; + } + } + } + return NS_OK; +} + +bool +MediaKeySystemAccessManager::EnsureObserversAdded() +{ + if (mAddedObservers) { + return true; + } + + nsCOMPtr<nsIObserverService> obsService = mozilla::services::GetObserverService(); + if (NS_WARN_IF(!obsService)) { + return false; + } + mAddedObservers = NS_SUCCEEDED(obsService->AddObserver(this, "gmp-changed", false)); + return mAddedObservers; +} + +void +MediaKeySystemAccessManager::Shutdown() +{ + EME_LOG("MediaKeySystemAccessManager::Shutdown"); + nsTArray<PendingRequest> requests(Move(mRequests)); + for (PendingRequest& request : requests) { + // Cancel all requests; we're shutting down. + request.CancelTimer(); + request.RejectPromise(NS_LITERAL_CSTRING("Promise still outstanding at MediaKeySystemAccessManager shutdown")); + } + if (mAddedObservers) { + nsCOMPtr<nsIObserverService> obsService = mozilla::services::GetObserverService(); + if (obsService) { + obsService->RemoveObserver(this, "gmp-changed"); + mAddedObservers = false; + } + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/eme/MediaKeySystemAccessManager.h b/dom/media/eme/MediaKeySystemAccessManager.h new file mode 100644 index 000000000..9c092248e --- /dev/null +++ b/dom/media/eme/MediaKeySystemAccessManager.h @@ -0,0 +1,83 @@ +/* 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 mozilla_dom_MediaKeySystemAccessManager_h +#define mozilla_dom_MediaKeySystemAccessManager_h + +#include "mozilla/dom/MediaKeySystemAccess.h" +#include "nsIObserver.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupportsImpl.h" +#include "nsITimer.h" + +namespace mozilla { +namespace dom { + +class DetailedPromise; +class TestGMPVideoDecoder; + +class MediaKeySystemAccessManager final : public nsIObserver +{ +public: + + explicit MediaKeySystemAccessManager(nsPIDOMWindowInner* aWindow); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(MediaKeySystemAccessManager, nsIObserver) + NS_DECL_NSIOBSERVER + + void Request(DetailedPromise* aPromise, + const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfig); + + void Shutdown(); + + struct PendingRequest { + PendingRequest(DetailedPromise* aPromise, + const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfig, + nsITimer* aTimer); + PendingRequest(const PendingRequest& aOther); + ~PendingRequest(); + void CancelTimer(); + void RejectPromise(const nsCString& aReason); + + RefPtr<DetailedPromise> mPromise; + const nsString mKeySystem; + const Sequence<MediaKeySystemConfiguration> mConfigs; + nsCOMPtr<nsITimer> mTimer; + }; + +private: + + enum RequestType { + Initial, + Subsequent + }; + + void Request(DetailedPromise* aPromise, + const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfig, + RequestType aType); + + ~MediaKeySystemAccessManager(); + + bool EnsureObserversAdded(); + + bool AwaitInstall(DetailedPromise* aPromise, + const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfig); + + void RetryRequest(PendingRequest& aRequest); + + nsTArray<PendingRequest> mRequests; + + nsCOMPtr<nsPIDOMWindowInner> mWindow; + bool mAddedObservers; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/media/eme/MediaKeys.cpp b/dom/media/eme/MediaKeys.cpp new file mode 100644 index 000000000..862100757 --- /dev/null +++ b/dom/media/eme/MediaKeys.cpp @@ -0,0 +1,579 @@ +/* -*- Mode: C++; tab-width: 8; 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 "mozilla/dom/MediaKeys.h" +#include "GMPService.h" +#include "mozilla/dom/HTMLMediaElement.h" +#include "mozilla/dom/MediaKeysBinding.h" +#include "mozilla/dom/MediaKeyMessageEvent.h" +#include "mozilla/dom/MediaKeyError.h" +#include "mozilla/dom/MediaKeySession.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/UnionTypes.h" +#include "GMPCDMProxy.h" +#include "mozilla/EMEUtils.h" +#include "nsContentUtils.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsContentTypeParser.h" +#ifdef MOZ_FMP4 +#include "MP4Decoder.h" +#endif +#ifdef XP_WIN +#include "mozilla/WindowsVersion.h" +#endif +#include "nsContentCID.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/dom/MediaKeySystemAccess.h" +#include "nsPrintfCString.h" + +namespace mozilla { + +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaKeys, + mElement, + mParent, + mKeySessions, + mPromises, + mPendingSessions); +NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeys) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeys) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeys) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +MediaKeys::MediaKeys(nsPIDOMWindowInner* aParent, + const nsAString& aKeySystem, + const MediaKeySystemConfiguration& aConfig) + : mParent(aParent) + , mKeySystem(aKeySystem) + , mCreatePromiseId(0) + , mConfig(aConfig) +{ + EME_LOG("MediaKeys[%p] constructed keySystem=%s", + this, NS_ConvertUTF16toUTF8(mKeySystem).get()); +} + +MediaKeys::~MediaKeys() +{ + Shutdown(); + EME_LOG("MediaKeys[%p] destroyed", this); +} + +void +MediaKeys::Terminated() +{ + EME_LOG("MediaKeys[%p] CDM crashed unexpectedly", this); + + KeySessionHashMap keySessions; + // Remove entries during iteration will screw it. Make a copy first. + for (auto iter = mKeySessions.Iter(); !iter.Done(); iter.Next()) { + RefPtr<MediaKeySession>& session = iter.Data(); + keySessions.Put(session->GetSessionId(), session); + } + for (auto iter = keySessions.Iter(); !iter.Done(); iter.Next()) { + RefPtr<MediaKeySession>& session = iter.Data(); + session->OnClosed(); + } + keySessions.Clear(); + MOZ_ASSERT(mKeySessions.Count() == 0); + + // Notify the element about that CDM has terminated. + if (mElement) { + mElement->DecodeError(NS_ERROR_DOM_MEDIA_CDM_ERR); + } + + Shutdown(); +} + +void +MediaKeys::Shutdown() +{ + if (mProxy) { + mProxy->Shutdown(); + mProxy = nullptr; + } + + RefPtr<MediaKeys> kungFuDeathGrip = this; + + for (auto iter = mPromises.Iter(); !iter.Done(); iter.Next()) { + RefPtr<dom::DetailedPromise>& promise = iter.Data(); + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Promise still outstanding at MediaKeys shutdown")); + Release(); + } + mPromises.Clear(); +} + +nsPIDOMWindowInner* +MediaKeys::GetParentObject() const +{ + return mParent; +} + +JSObject* +MediaKeys::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return MediaKeysBinding::Wrap(aCx, this, aGivenProto); +} + +void +MediaKeys::GetKeySystem(nsString& aOutKeySystem) const +{ + aOutKeySystem.Assign(mKeySystem); +} + +already_AddRefed<DetailedPromise> +MediaKeys::SetServerCertificate(const ArrayBufferViewOrArrayBuffer& aCert, ErrorResult& aRv) +{ + RefPtr<DetailedPromise> promise(MakePromise(aRv, + NS_LITERAL_CSTRING("MediaKeys.setServerCertificate"))); + if (aRv.Failed()) { + return nullptr; + } + + if (!mProxy) { + NS_WARNING("Tried to use a MediaKeys without a CDM"); + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Null CDM in MediaKeys.setServerCertificate()")); + return promise.forget(); + } + + nsTArray<uint8_t> data; + CopyArrayBufferViewOrArrayBufferData(aCert, data); + if (data.IsEmpty()) { + promise->MaybeReject(NS_ERROR_DOM_TYPE_ERR, + NS_LITERAL_CSTRING("Empty certificate passed to MediaKeys.setServerCertificate()")); + return promise.forget(); + } + + mProxy->SetServerCertificate(StorePromise(promise), data); + return promise.forget(); +} + +already_AddRefed<DetailedPromise> +MediaKeys::MakePromise(ErrorResult& aRv, const nsACString& aName) +{ + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject()); + if (!global) { + NS_WARNING("Passed non-global to MediaKeys ctor!"); + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + return DetailedPromise::Create(global, aRv, aName); +} + +PromiseId +MediaKeys::StorePromise(DetailedPromise* aPromise) +{ + static uint32_t sEMEPromiseCount = 1; + MOZ_ASSERT(aPromise); + uint32_t id = sEMEPromiseCount++; + + EME_LOG("MediaKeys[%p]::StorePromise() id=%d", this, id); + + // Keep MediaKeys alive for the lifetime of its promises. Any still-pending + // promises are rejected in Shutdown(). + AddRef(); + +#ifdef DEBUG + // We should not have already stored this promise! + for (auto iter = mPromises.ConstIter(); !iter.Done(); iter.Next()) { + MOZ_ASSERT(iter.Data() != aPromise); + } +#endif + + mPromises.Put(id, aPromise); + return id; +} + +void +MediaKeys::ConnectPendingPromiseIdWithToken(PromiseId aId, uint32_t aToken) +{ + // Should only be called from MediaKeySession::GenerateRequest. + mPromiseIdToken.Put(aId, aToken); + EME_LOG("MediaKeys[%p]::ConnectPendingPromiseIdWithToken() id=%u => token(%u)", + this, aId, aToken); +} + +already_AddRefed<DetailedPromise> +MediaKeys::RetrievePromise(PromiseId aId) +{ + if (!mPromises.Contains(aId)) { + NS_WARNING(nsPrintfCString("Tried to retrieve a non-existent promise id=%d", aId).get()); + return nullptr; + } + RefPtr<DetailedPromise> promise; + mPromises.Remove(aId, getter_AddRefs(promise)); + Release(); + return promise.forget(); +} + +void +MediaKeys::RejectPromise(PromiseId aId, nsresult aExceptionCode, + const nsCString& aReason) +{ + EME_LOG("MediaKeys[%p]::RejectPromise(%d, 0x%x)", this, aId, aExceptionCode); + + RefPtr<DetailedPromise> promise(RetrievePromise(aId)); + if (!promise) { + return; + } + + // This promise could be a createSession or loadSession promise, + // so we might have a pending session waiting to be resolved into + // the promise on success. We've been directed to reject to promise, + // so we can throw away the corresponding session object. + uint32_t token = 0; + if (mPromiseIdToken.Get(aId, &token)) { + MOZ_ASSERT(mPendingSessions.Contains(token)); + mPendingSessions.Remove(token); + mPromiseIdToken.Remove(aId); + } + + MOZ_ASSERT(NS_FAILED(aExceptionCode)); + promise->MaybeReject(aExceptionCode, aReason); + + if (mCreatePromiseId == aId) { + // Note: This will probably destroy the MediaKeys object! + Release(); + } +} + +void +MediaKeys::OnSessionIdReady(MediaKeySession* aSession) +{ + if (!aSession) { + NS_WARNING("Invalid MediaKeySession passed to OnSessionIdReady()"); + return; + } + if (mKeySessions.Contains(aSession->GetSessionId())) { + NS_WARNING("MediaKeySession's made ready multiple times!"); + return; + } + if (mPendingSessions.Contains(aSession->Token())) { + NS_WARNING("MediaKeySession made ready when it wasn't waiting to be ready!"); + return; + } + if (aSession->GetSessionId().IsEmpty()) { + NS_WARNING("MediaKeySession with invalid sessionId passed to OnSessionIdReady()"); + return; + } + mKeySessions.Put(aSession->GetSessionId(), aSession); +} + +void +MediaKeys::ResolvePromise(PromiseId aId) +{ + EME_LOG("MediaKeys[%p]::ResolvePromise(%d)", this, aId); + + RefPtr<DetailedPromise> promise(RetrievePromise(aId)); + MOZ_ASSERT(!mPromises.Contains(aId)); + if (!promise) { + return; + } + + uint32_t token = 0; + if (!mPromiseIdToken.Get(aId, &token)) { + promise->MaybeResolveWithUndefined(); + return; + } else if (!mPendingSessions.Contains(token)) { + // Pending session for CreateSession() should be removed when sessionId + // is ready. + promise->MaybeResolveWithUndefined(); + mPromiseIdToken.Remove(aId); + return; + } + mPromiseIdToken.Remove(aId); + + // We should only resolve LoadSession calls via this path, + // not CreateSession() promises. + RefPtr<MediaKeySession> session; + mPendingSessions.Remove(token, getter_AddRefs(session)); + if (!session || session->GetSessionId().IsEmpty()) { + NS_WARNING("Received activation for non-existent session!"); + promise->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR, + NS_LITERAL_CSTRING("CDM LoadSession() returned a different session ID than requested")); + return; + } + mKeySessions.Put(session->GetSessionId(), session); + promise->MaybeResolve(session); +} + +class MediaKeysGMPCrashHelper : public GMPCrashHelper +{ +public: + explicit MediaKeysGMPCrashHelper(MediaKeys* aMediaKeys) + : mMediaKeys(aMediaKeys) + { + MOZ_ASSERT(NS_IsMainThread()); // WeakPtr isn't thread safe. + } + already_AddRefed<nsPIDOMWindowInner> + GetPluginCrashedEventTarget() override + { + MOZ_ASSERT(NS_IsMainThread()); // WeakPtr isn't thread safe. + EME_LOG("MediaKeysGMPCrashHelper::GetPluginCrashedEventTarget()"); + return (mMediaKeys && mMediaKeys->GetParentObject()) ? + do_AddRef(mMediaKeys->GetParentObject()) : nullptr; + } +private: + WeakPtr<MediaKeys> mMediaKeys; +}; + +already_AddRefed<CDMProxy> +MediaKeys::CreateCDMProxy() +{ + RefPtr<CDMProxy> proxy; + { + proxy = new GMPCDMProxy(this, + mKeySystem, + new MediaKeysGMPCrashHelper(this), + mConfig.mDistinctiveIdentifier == MediaKeysRequirement::Required, + mConfig.mPersistentState == MediaKeysRequirement::Required); + } + return proxy.forget(); +} + +already_AddRefed<DetailedPromise> +MediaKeys::Init(ErrorResult& aRv) +{ + RefPtr<DetailedPromise> promise(MakePromise(aRv, + NS_LITERAL_CSTRING("MediaKeys::Init()"))); + if (aRv.Failed()) { + return nullptr; + } + + mProxy = CreateCDMProxy(); + + // Determine principal (at creation time) of the MediaKeys object. + nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(GetParentObject()); + if (!sop) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Couldn't get script principal in MediaKeys::Init")); + return promise.forget(); + } + mPrincipal = sop->GetPrincipal(); + + // Determine principal of the "top-level" window; the principal of the + // page that will display in the URL bar. + nsCOMPtr<nsPIDOMWindowInner> window = GetParentObject(); + if (!window) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Couldn't get top-level window in MediaKeys::Init")); + return promise.forget(); + } + nsCOMPtr<nsPIDOMWindowOuter> top = window->GetOuterWindow()->GetTop(); + if (!top || !top->GetExtantDoc()) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Couldn't get document in MediaKeys::Init")); + return promise.forget(); + } + + mTopLevelPrincipal = top->GetExtantDoc()->NodePrincipal(); + + if (!mPrincipal || !mTopLevelPrincipal) { + NS_WARNING("Failed to get principals when creating MediaKeys"); + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Couldn't get principal(s) in MediaKeys::Init")); + return promise.forget(); + } + + nsAutoCString origin; + nsresult rv = mPrincipal->GetOrigin(origin); + if (NS_FAILED(rv)) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Couldn't get principal origin string in MediaKeys::Init")); + return promise.forget(); + } + nsAutoCString topLevelOrigin; + rv = mTopLevelPrincipal->GetOrigin(topLevelOrigin); + if (NS_FAILED(rv)) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Couldn't get top-level principal origin string in MediaKeys::Init")); + return promise.forget(); + } + + nsIDocument* doc = window->GetExtantDoc(); + const bool inPrivateBrowsing = nsContentUtils::IsInPrivateBrowsing(doc); + + EME_LOG("MediaKeys[%p]::Create() (%s, %s), %s", + this, + origin.get(), + topLevelOrigin.get(), + (inPrivateBrowsing ? "PrivateBrowsing" : "NonPrivateBrowsing")); + + // The CDMProxy's initialization is asynchronous. The MediaKeys is + // refcounted, and its instance is returned to JS by promise once + // it's been initialized. No external refs exist to the MediaKeys while + // we're waiting for the promise to be resolved, so we must hold a + // reference to the new MediaKeys object until it's been created, + // or its creation has failed. Store the id of the promise returned + // here, and hold a self-reference until that promise is resolved or + // rejected. + MOZ_ASSERT(!mCreatePromiseId, "Should only be created once!"); + mCreatePromiseId = StorePromise(promise); + AddRef(); + mProxy->Init(mCreatePromiseId, + NS_ConvertUTF8toUTF16(origin), + NS_ConvertUTF8toUTF16(topLevelOrigin), + KeySystemToGMPName(mKeySystem), + inPrivateBrowsing); + + return promise.forget(); +} + +void +MediaKeys::OnCDMCreated(PromiseId aId, const nsACString& aNodeId, const uint32_t aPluginId) +{ + RefPtr<DetailedPromise> promise(RetrievePromise(aId)); + if (!promise) { + return; + } + mNodeId = aNodeId; + RefPtr<MediaKeys> keys(this); + EME_LOG("MediaKeys[%p]::OnCDMCreated() resolve promise id=%d", this, aId); + promise->MaybeResolve(keys); + if (mCreatePromiseId == aId) { + Release(); + } + + MediaKeySystemAccess::NotifyObservers(mParent, + mKeySystem, + MediaKeySystemStatus::Cdm_created); + +} + +static bool +IsSessionTypeSupported(const MediaKeySessionType aSessionType, + const MediaKeySystemConfiguration& aConfig) +{ + if (aSessionType == MediaKeySessionType::Temporary) { + // Temporary is always supported. + return true; + } + if (!aConfig.mSessionTypes.WasPassed()) { + // No other session types supported. + return false; + } + using MediaKeySessionTypeValues::strings; + const char* sessionType = strings[static_cast<uint32_t>(aSessionType)].value; + for (const nsString& s : aConfig.mSessionTypes.Value()) { + if (s.EqualsASCII(sessionType)) { + return true; + } + } + return false; +} + +already_AddRefed<MediaKeySession> +MediaKeys::CreateSession(JSContext* aCx, + MediaKeySessionType aSessionType, + ErrorResult& aRv) +{ + if (!IsSessionTypeSupported(aSessionType, mConfig)) { + EME_LOG("MediaKeys[%p,'%s'] CreateSession() failed, unsupported session type", this); + aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return nullptr; + } + + if (!mProxy) { + NS_WARNING("Tried to use a MediaKeys which lost its CDM"); + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + EME_LOG("MediaKeys[%p] Creating session", this); + + RefPtr<MediaKeySession> session = new MediaKeySession(aCx, + GetParentObject(), + this, + mKeySystem, + aSessionType, + aRv); + + if (aRv.Failed()) { + return nullptr; + } + + // Add session to the set of sessions awaiting their sessionId being ready. + mPendingSessions.Put(session->Token(), session); + + return session.forget(); +} + +void +MediaKeys::OnSessionLoaded(PromiseId aId, bool aSuccess) +{ + RefPtr<DetailedPromise> promise(RetrievePromise(aId)); + if (!promise) { + return; + } + EME_LOG("MediaKeys[%p]::OnSessionLoaded() resolve promise id=%d", this, aId); + + promise->MaybeResolve(aSuccess); +} + +void +MediaKeys::OnSessionClosed(MediaKeySession* aSession) +{ + nsAutoString id; + aSession->GetSessionId(id); + mKeySessions.Remove(id); +} + +already_AddRefed<MediaKeySession> +MediaKeys::GetSession(const nsAString& aSessionId) +{ + RefPtr<MediaKeySession> session; + mKeySessions.Get(aSessionId, getter_AddRefs(session)); + return session.forget(); +} + +already_AddRefed<MediaKeySession> +MediaKeys::GetPendingSession(uint32_t aToken) +{ + RefPtr<MediaKeySession> session; + mPendingSessions.Get(aToken, getter_AddRefs(session)); + mPendingSessions.Remove(aToken); + return session.forget(); +} + +const nsCString& +MediaKeys::GetNodeId() const +{ + MOZ_ASSERT(NS_IsMainThread()); + return mNodeId; +} + +bool +MediaKeys::IsBoundToMediaElement() const +{ + MOZ_ASSERT(NS_IsMainThread()); + return mElement != nullptr; +} + +nsresult +MediaKeys::Bind(HTMLMediaElement* aElement) +{ + MOZ_ASSERT(NS_IsMainThread()); + if (IsBoundToMediaElement()) { + return NS_ERROR_FAILURE; + } + + mElement = aElement; + + return NS_OK; +} + +void +MediaKeys::Unbind() +{ + MOZ_ASSERT(NS_IsMainThread()); + mElement = nullptr; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/eme/MediaKeys.h b/dom/media/eme/MediaKeys.h new file mode 100644 index 000000000..491963934 --- /dev/null +++ b/dom/media/eme/MediaKeys.h @@ -0,0 +1,167 @@ +/* -*- Mode: C++; tab-width: 8; 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/. */ + +#ifndef mozilla_dom_mediakeys_h__ +#define mozilla_dom_mediakeys_h__ + +#include "nsWrapperCache.h" +#include "nsISupports.h" +#include "mozilla/Attributes.h" +#include "mozilla/RefPtr.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsRefPtrHashtable.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/MediaKeysBinding.h" +#include "mozilla/dom/MediaKeySystemAccessBinding.h" +#include "mozIGeckoMediaPluginService.h" +#include "mozilla/DetailedPromise.h" +#include "mozilla/WeakPtr.h" + +namespace mozilla { + +class CDMProxy; + +namespace dom { + +class ArrayBufferViewOrArrayBuffer; +class MediaKeySession; +class HTMLMediaElement; + +typedef nsRefPtrHashtable<nsStringHashKey, MediaKeySession> KeySessionHashMap; +typedef nsRefPtrHashtable<nsUint32HashKey, dom::DetailedPromise> PromiseHashMap; +typedef nsRefPtrHashtable<nsUint32HashKey, MediaKeySession> PendingKeySessionsHashMap; +typedef nsDataHashtable<nsUint32HashKey, uint32_t> PendingPromiseIdTokenHashMap; +typedef uint32_t PromiseId; + +// This class is used on the main thread only. +// Note: its addref/release is not (and can't be) thread safe! +class MediaKeys final : public nsISupports, + public nsWrapperCache, + public SupportsWeakPtr<MediaKeys> +{ + ~MediaKeys(); + +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(MediaKeys) + MOZ_DECLARE_WEAKREFERENCE_TYPENAME(MediaKeys) + + MediaKeys(nsPIDOMWindowInner* aParentWindow, + const nsAString& aKeySystem, + const MediaKeySystemConfiguration& aConfig); + + already_AddRefed<DetailedPromise> Init(ErrorResult& aRv); + + nsPIDOMWindowInner* GetParentObject() const; + + JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + nsresult Bind(HTMLMediaElement* aElement); + void Unbind(); + + // Javascript: readonly attribute DOMString keySystem; + void GetKeySystem(nsString& retval) const; + + // JavaScript: MediaKeys.createSession() + already_AddRefed<MediaKeySession> CreateSession(JSContext* aCx, + MediaKeySessionType aSessionType, + ErrorResult& aRv); + + // JavaScript: MediaKeys.SetServerCertificate() + already_AddRefed<DetailedPromise> + SetServerCertificate(const ArrayBufferViewOrArrayBuffer& aServerCertificate, + ErrorResult& aRv); + + already_AddRefed<MediaKeySession> GetSession(const nsAString& aSessionId); + + // Removes and returns MediaKeySession from the set of sessions awaiting + // their sessionId to be assigned. + already_AddRefed<MediaKeySession> GetPendingSession(uint32_t aToken); + + // Called once a Init() operation succeeds. + void OnCDMCreated(PromiseId aId, + const nsACString& aNodeId, const uint32_t aPluginId); + + // Called once the CDM generates a sessionId while servicing a + // MediaKeySession.generateRequest() or MediaKeySession.load() call, + // once the sessionId of a MediaKeySession is known. + void OnSessionIdReady(MediaKeySession* aSession); + + // Called once a LoadSession succeeds. + void OnSessionLoaded(PromiseId aId, bool aSuccess); + + // Called once a session has closed. + void OnSessionClosed(MediaKeySession* aSession); + + CDMProxy* GetCDMProxy() { return mProxy; } + + // Makes a new promise, or nullptr on failure. + already_AddRefed<DetailedPromise> MakePromise(ErrorResult& aRv, + const nsACString& aName); + // Stores promise in mPromises, returning an ID that can be used to retrieve + // it later. The ID is passed to the CDM, so that it can signal specific + // promises to be resolved. + PromiseId StorePromise(DetailedPromise* aPromise); + + // Stores a map from promise id to pending session token. Using this + // mapping, when a promise is rejected via its ID, we can check if the + // promise corresponds to a pending session and retrieve that session + // via the mapped-to token, and remove the pending session from the + // list of sessions awaiting a session id. + void ConnectPendingPromiseIdWithToken(PromiseId aId, uint32_t aToken); + + // Reject promise with DOMException corresponding to aExceptionCode. + void RejectPromise(PromiseId aId, nsresult aExceptionCode, + const nsCString& aReason); + // Resolves promise with "undefined". + void ResolvePromise(PromiseId aId); + + const nsCString& GetNodeId() const; + + void Shutdown(); + + // Called by CDMProxy when CDM crashes or shuts down. It is different from + // Shutdown which is called from the script/dom side. + void Terminated(); + + // Returns true if this MediaKeys has been bound to a media element. + bool IsBoundToMediaElement() const; + +private: + + // Instantiate CDMProxy instance. + // It could be MediaDrmCDMProxy (Widevine on Fennec) or GMPCDMProxy (the rest). + already_AddRefed<CDMProxy> CreateCDMProxy(); + + // Removes promise from mPromises, and returns it. + already_AddRefed<DetailedPromise> RetrievePromise(PromiseId aId); + + // Owning ref to proxy. The proxy has a weak reference back to the MediaKeys, + // and the MediaKeys destructor clears the proxy's reference to the MediaKeys. + RefPtr<CDMProxy> mProxy; + + RefPtr<HTMLMediaElement> mElement; + + nsCOMPtr<nsPIDOMWindowInner> mParent; + const nsString mKeySystem; + nsCString mNodeId; + KeySessionHashMap mKeySessions; + PromiseHashMap mPromises; + PendingKeySessionsHashMap mPendingSessions; + PromiseId mCreatePromiseId; + + RefPtr<nsIPrincipal> mPrincipal; + RefPtr<nsIPrincipal> mTopLevelPrincipal; + + const MediaKeySystemConfiguration mConfig; + + PendingPromiseIdTokenHashMap mPromiseIdToken; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_mediakeys_h__ diff --git a/dom/media/eme/moz.build b/dom/media/eme/moz.build new file mode 100644 index 000000000..7b1ad9d84 --- /dev/null +++ b/dom/media/eme/moz.build @@ -0,0 +1,41 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# 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.dom += [ + 'MediaEncryptedEvent.h', + 'MediaKeyError.h', + 'MediaKeyMessageEvent.h', + 'MediaKeys.h', + 'MediaKeySession.h', + 'MediaKeyStatusMap.h', + 'MediaKeySystemAccess.h', + 'MediaKeySystemAccessManager.h', +] + +EXPORTS.mozilla += [ + 'CDMCaps.h', + 'CDMProxy.h', + 'DecryptorProxyCallback.h', + 'DetailedPromise.h', + 'EMEUtils.h', +] + +SOURCES += [ + 'CDMCaps.cpp', + 'DetailedPromise.cpp', + 'EMEUtils.cpp', + 'MediaEncryptedEvent.cpp', + 'MediaKeyError.cpp', + 'MediaKeyMessageEvent.cpp', + 'MediaKeys.cpp', + 'MediaKeySession.cpp', + 'MediaKeyStatusMap.cpp', + 'MediaKeySystemAccess.cpp', + 'MediaKeySystemAccessManager.cpp', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' diff --git a/dom/media/fmp4/MP4Decoder.cpp b/dom/media/fmp4/MP4Decoder.cpp index 5ad690625..7a340d829 100644 --- a/dom/media/fmp4/MP4Decoder.cpp +++ b/dom/media/fmp4/MP4Decoder.cpp @@ -1,4 +1,5 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ /* 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/. */ @@ -9,6 +10,9 @@ #include "MP4Demuxer.h" #include "mozilla/Preferences.h" #include "nsCharSeparatedTokenizer.h" +#ifdef MOZ_EME +#include "mozilla/CDMProxy.h" +#endif #include "mozilla/Logging.h" #include "mozilla/SharedThreadPool.h" #include "nsMimeTypes.h" diff --git a/dom/media/gmp/GMPCDMCallbackProxy.cpp b/dom/media/gmp/GMPCDMCallbackProxy.cpp new file mode 100644 index 000000000..a0b490849 --- /dev/null +++ b/dom/media/gmp/GMPCDMCallbackProxy.cpp @@ -0,0 +1,250 @@ +/* -*- Mode: C++; tab-width: 8; 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 "GMPCDMCallbackProxy.h" +#include "mozilla/CDMProxy.h" +#include "nsString.h" +#include "mozilla/dom/MediaKeys.h" +#include "mozilla/dom/MediaKeySession.h" +#include "mozIGeckoMediaPluginService.h" +#include "nsContentCID.h" +#include "nsServiceManagerUtils.h" +#include "MainThreadUtils.h" +#include "mozilla/EMEUtils.h" + +namespace mozilla { + +GMPCDMCallbackProxy::GMPCDMCallbackProxy(CDMProxy* aProxy) + : mProxy(aProxy) +{} + +void +GMPCDMCallbackProxy::SetDecryptorId(uint32_t aId) +{ + MOZ_ASSERT(mProxy->IsOnOwnerThread()); + + RefPtr<CDMProxy> proxy = mProxy; + NS_DispatchToMainThread( + NS_NewRunnableFunction([proxy, aId] () + { + proxy->OnSetDecryptorId(aId); + }) + );} + +void +GMPCDMCallbackProxy::SetSessionId(uint32_t aToken, + const nsCString& aSessionId) +{ + MOZ_ASSERT(mProxy->IsOnOwnerThread()); + + RefPtr<CDMProxy> proxy = mProxy; + auto sid = NS_ConvertUTF8toUTF16(aSessionId); + NS_DispatchToMainThread( + NS_NewRunnableFunction([proxy, + aToken, + sid] () + { + proxy->OnSetSessionId(aToken, sid); + }) + ); +} + +void +GMPCDMCallbackProxy::ResolveLoadSessionPromise(uint32_t aPromiseId, + bool aSuccess) +{ + MOZ_ASSERT(mProxy->IsOnOwnerThread()); + + RefPtr<CDMProxy> proxy = mProxy; + NS_DispatchToMainThread( + NS_NewRunnableFunction([proxy, aPromiseId, aSuccess] () + { + proxy->OnResolveLoadSessionPromise(aPromiseId, aSuccess); + }) + ); +} + +void +GMPCDMCallbackProxy::ResolvePromise(uint32_t aPromiseId) +{ + MOZ_ASSERT(mProxy->IsOnOwnerThread()); + + // Note: CDMProxy proxies this from non-main threads to main thread. + mProxy->ResolvePromise(aPromiseId); +} + +void +GMPCDMCallbackProxy::RejectPromise(uint32_t aPromiseId, + nsresult aException, + const nsCString& aMessage) +{ + MOZ_ASSERT(mProxy->IsOnOwnerThread()); + + RefPtr<CDMProxy> proxy = mProxy; + NS_DispatchToMainThread( + NS_NewRunnableFunction([proxy, + aPromiseId, + aException, + aMessage] () + { + proxy->OnRejectPromise(aPromiseId, aException, aMessage); + }) + ); +} + +void +GMPCDMCallbackProxy::SessionMessage(const nsCString& aSessionId, + dom::MediaKeyMessageType aMessageType, + const nsTArray<uint8_t>& aMessage) +{ + MOZ_ASSERT(mProxy->IsOnOwnerThread()); + + RefPtr<CDMProxy> proxy = mProxy; + auto sid = NS_ConvertUTF8toUTF16(aSessionId); + nsTArray<uint8_t> msg(aMessage); + NS_DispatchToMainThread( + NS_NewRunnableFunction([proxy, + sid, + aMessageType, + msg] () mutable + { + proxy->OnSessionMessage(sid, aMessageType, msg); + }) + ); +} + +void +GMPCDMCallbackProxy::ExpirationChange(const nsCString& aSessionId, + GMPTimestamp aExpiryTime) +{ + MOZ_ASSERT(mProxy->IsOnOwnerThread()); + + RefPtr<CDMProxy> proxy = mProxy; + auto sid = NS_ConvertUTF8toUTF16(aSessionId); + NS_DispatchToMainThread( + NS_NewRunnableFunction([proxy, + sid, + aExpiryTime] () + { + proxy->OnExpirationChange(sid, aExpiryTime); + }) + ); +} + +void +GMPCDMCallbackProxy::SessionClosed(const nsCString& aSessionId) +{ + MOZ_ASSERT(mProxy->IsOnOwnerThread()); + + bool keyStatusesChange = false; + auto sid = NS_ConvertUTF8toUTF16(aSessionId); + { + CDMCaps::AutoLock caps(mProxy->Capabilites()); + keyStatusesChange = caps.RemoveKeysForSession(NS_ConvertUTF8toUTF16(aSessionId)); + } + if (keyStatusesChange) { + RefPtr<CDMProxy> proxy = mProxy; + NS_DispatchToMainThread( + NS_NewRunnableFunction([proxy, sid] () + { + proxy->OnKeyStatusesChange(sid); + }) + ); + } + + RefPtr<CDMProxy> proxy = mProxy; + NS_DispatchToMainThread( + NS_NewRunnableFunction([proxy, sid] () + { + proxy->OnSessionClosed(sid); + }) + ); +} + +void +GMPCDMCallbackProxy::SessionError(const nsCString& aSessionId, + nsresult aException, + uint32_t aSystemCode, + const nsCString& aMessage) +{ + MOZ_ASSERT(mProxy->IsOnOwnerThread()); + + RefPtr<CDMProxy> proxy = mProxy; + auto sid = NS_ConvertUTF8toUTF16(aSessionId); + auto msg = NS_ConvertUTF8toUTF16(aMessage); + NS_DispatchToMainThread( + NS_NewRunnableFunction([proxy, + sid, + aException, + aSystemCode, + msg] () + { + proxy->OnSessionError(sid, + aException, + aSystemCode, + msg); + }) + ); +} + +void +GMPCDMCallbackProxy::BatchedKeyStatusChanged(const nsCString& aSessionId, + const nsTArray<CDMKeyInfo>& aKeyInfos) +{ + MOZ_ASSERT(mProxy->IsOnOwnerThread()); + BatchedKeyStatusChangedInternal(aSessionId, aKeyInfos); +} + +void +GMPCDMCallbackProxy::BatchedKeyStatusChangedInternal(const nsCString& aSessionId, + const nsTArray<CDMKeyInfo>& aKeyInfos) +{ + bool keyStatusesChange = false; + { + CDMCaps::AutoLock caps(mProxy->Capabilites()); + for (size_t i = 0; i < aKeyInfos.Length(); i++) { + keyStatusesChange |= + caps.SetKeyStatus(aKeyInfos[i].mKeyId, + NS_ConvertUTF8toUTF16(aSessionId), + aKeyInfos[i].mStatus); + } + } + if (keyStatusesChange) { + RefPtr<CDMProxy> proxy = mProxy; + auto sid = NS_ConvertUTF8toUTF16(aSessionId); + NS_DispatchToMainThread( + NS_NewRunnableFunction([proxy, sid] () + { + proxy->OnKeyStatusesChange(sid); + }) + ); + } +} + +void +GMPCDMCallbackProxy::Decrypted(uint32_t aId, + DecryptStatus aResult, + const nsTArray<uint8_t>& aDecryptedData) +{ + MOZ_ASSERT(mProxy->IsOnOwnerThread()); + + mProxy->OnDecrypted(aId, aResult, aDecryptedData); +} + +void +GMPCDMCallbackProxy::Terminated() +{ + MOZ_ASSERT(mProxy->IsOnOwnerThread()); + + RefPtr<CDMProxy> proxy = mProxy; + NS_DispatchToMainThread( + NS_NewRunnableFunction([proxy] () + { + proxy->Terminated(); + }) + ); +} + +} // namespace mozilla diff --git a/dom/media/gmp/GMPCDMCallbackProxy.h b/dom/media/gmp/GMPCDMCallbackProxy.h new file mode 100644 index 000000000..d2cc80682 --- /dev/null +++ b/dom/media/gmp/GMPCDMCallbackProxy.h @@ -0,0 +1,71 @@ +/* -*- Mode: C++; tab-width: 8; 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/. */ + +#ifndef GMPCDMCallbackProxy_h_ +#define GMPCDMCallbackProxy_h_ + +#include "mozilla/CDMProxy.h" +#include "gmp-decryption.h" +#include "GMPDecryptorProxy.h" + +namespace mozilla { + +// Proxies call backs from the CDM on the GMP thread back to the MediaKeys +// object on the main thread. +class GMPCDMCallbackProxy : public GMPDecryptorProxyCallback { +public: + + void SetDecryptorId(uint32_t aId) override; + + 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, + nsresult aException, + const nsCString& aSessionId) override; + + void SessionMessage(const nsCString& aSessionId, + dom::MediaKeyMessageType aMessageType, + const nsTArray<uint8_t>& 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<uint8_t>& aDecryptedData) override; + + void BatchedKeyStatusChanged(const nsCString& aSessionId, + const nsTArray<CDMKeyInfo>& aKeyInfos) override; + + void Terminated() override; + + ~GMPCDMCallbackProxy() {} + +private: + friend class GMPCDMProxy; + explicit GMPCDMCallbackProxy(CDMProxy* aProxy); + + void BatchedKeyStatusChangedInternal(const nsCString& aSessionId, + const nsTArray<CDMKeyInfo>& aKeyInfos); + // Warning: Weak ref. + CDMProxy* mProxy; +}; + +} // namespace mozilla + +#endif // GMPCDMCallbackProxy_h_ diff --git a/dom/media/gmp/GMPCDMProxy.cpp b/dom/media/gmp/GMPCDMProxy.cpp new file mode 100644 index 000000000..0f1958632 --- /dev/null +++ b/dom/media/gmp/GMPCDMProxy.cpp @@ -0,0 +1,798 @@ +/* -*- Mode: C++; tab-width: 8; 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 "GMPCDMProxy.h" +#include "mozilla/EMEUtils.h" +#include "mozilla/PodOperations.h" + +#include "mozilla/dom/MediaKeys.h" +#include "mozilla/dom/MediaKeySession.h" + +#include "mozIGeckoMediaPluginService.h" +#include "nsContentCID.h" +#include "nsIConsoleService.h" +#include "nsPrintfCString.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "prenv.h" +#include "GMPCDMCallbackProxy.h" +#include "GMPService.h" +#include "MainThreadUtils.h" +#include "MediaData.h" + +namespace mozilla { + +GMPCDMProxy::GMPCDMProxy(dom::MediaKeys* aKeys, + const nsAString& aKeySystem, + GMPCrashHelper* aCrashHelper, + bool aDistinctiveIdentifierRequired, + bool aPersistentStateRequired) + : CDMProxy(aKeys, + aKeySystem, + aDistinctiveIdentifierRequired, + aPersistentStateRequired) + , mCrashHelper(aCrashHelper) + , mCDM(nullptr) + , mDecryptionJobCount(0) + , mShutdownCalled(false) + , mDecryptorId(0) + , mCreatePromiseId(0) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_COUNT_CTOR(GMPCDMProxy); +} + +GMPCDMProxy::~GMPCDMProxy() +{ + MOZ_COUNT_DTOR(GMPCDMProxy); +} + +void +GMPCDMProxy::Init(PromiseId aPromiseId, + const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, + const nsAString& aGMPName, + bool aInPrivateBrowsing) +{ + MOZ_ASSERT(NS_IsMainThread()); + NS_ENSURE_TRUE_VOID(!mKeys.IsNull()); + + EME_LOG("GMPCDMProxy::Init (%s, %s) %s", + NS_ConvertUTF16toUTF8(aOrigin).get(), + NS_ConvertUTF16toUTF8(aTopLevelOrigin).get(), + (aInPrivateBrowsing ? "PrivateBrowsing" : "NonPrivateBrowsing")); + + nsCString pluginVersion; + if (!mOwnerThread) { + nsCOMPtr<mozIGeckoMediaPluginService> mps = + do_GetService("@mozilla.org/gecko-media-plugin-service;1"); + if (!mps) { + RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Couldn't get MediaPluginService in GMPCDMProxy::Init")); + return; + } + mps->GetThread(getter_AddRefs(mOwnerThread)); + if (!mOwnerThread) { + RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Couldn't get GMP thread GMPCDMProxy::Init")); + return; + } + } + + if (aGMPName.IsEmpty()) { + RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + nsPrintfCString("Unknown GMP for keysystem '%s'", NS_ConvertUTF16toUTF8(mKeySystem).get())); + return; + } + + nsAutoPtr<InitData> data(new InitData()); + data->mPromiseId = aPromiseId; + data->mOrigin = aOrigin; + data->mTopLevelOrigin = aTopLevelOrigin; + data->mGMPName = aGMPName; + data->mInPrivateBrowsing = aInPrivateBrowsing; + data->mCrashHelper = mCrashHelper; + nsCOMPtr<nsIRunnable> task( + NewRunnableMethod<nsAutoPtr<InitData>>(this, + &GMPCDMProxy::gmp_Init, + Move(data))); + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); +} + +#ifdef DEBUG +bool +GMPCDMProxy::IsOnOwnerThread() +{ + return NS_GetCurrentThread() == mOwnerThread; +} +#endif + +void +GMPCDMProxy::gmp_InitDone(GMPDecryptorProxy* aCDM, nsAutoPtr<InitData>&& aData) +{ + EME_LOG("GMPCDMProxy::gmp_InitDone"); + if (mShutdownCalled) { + if (aCDM) { + aCDM->Close(); + } + RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("GMPCDMProxy was shut down before init could complete")); + return; + } + if (!aCDM) { + RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("GetGMPDecryptor failed to return a CDM")); + return; + } + + mCDM = aCDM; + mCallback = new GMPCDMCallbackProxy(this); + mCDM->Init(mCallback, + mDistinctiveIdentifierRequired, + mPersistentStateRequired); + + // Await the OnSetDecryptorId callback. + mCreatePromiseId = aData->mPromiseId; +} + +void GMPCDMProxy::OnSetDecryptorId(uint32_t aId) +{ + MOZ_ASSERT(mCreatePromiseId); + mDecryptorId = aId; + nsCOMPtr<nsIRunnable> task( + NewRunnableMethod<uint32_t>(this, + &GMPCDMProxy::OnCDMCreated, + mCreatePromiseId)); + NS_DispatchToMainThread(task); +} + +class gmp_InitDoneCallback : public GetGMPDecryptorCallback +{ +public: + gmp_InitDoneCallback(GMPCDMProxy* aGMPCDMProxy, + nsAutoPtr<GMPCDMProxy::InitData>&& aData) + : mGMPCDMProxy(aGMPCDMProxy), + mData(Move(aData)) + { + } + + void Done(GMPDecryptorProxy* aCDM) + { + mGMPCDMProxy->gmp_InitDone(aCDM, Move(mData)); + } + +private: + RefPtr<GMPCDMProxy> mGMPCDMProxy; + nsAutoPtr<GMPCDMProxy::InitData> mData; +}; + +class gmp_InitGetGMPDecryptorCallback : public GetNodeIdCallback +{ +public: + gmp_InitGetGMPDecryptorCallback(GMPCDMProxy* aGMPCDMProxy, + nsAutoPtr<GMPCDMProxy::InitData>&& aData) + : mGMPCDMProxy(aGMPCDMProxy), + mData(aData) + { + } + + void Done(nsresult aResult, const nsACString& aNodeId) + { + mGMPCDMProxy->gmp_InitGetGMPDecryptor(aResult, aNodeId, Move(mData)); + } + +private: + RefPtr<GMPCDMProxy> mGMPCDMProxy; + nsAutoPtr<GMPCDMProxy::InitData> mData; +}; + +void +GMPCDMProxy::gmp_Init(nsAutoPtr<InitData>&& aData) +{ + MOZ_ASSERT(IsOnOwnerThread()); + + nsCOMPtr<mozIGeckoMediaPluginService> mps = + do_GetService("@mozilla.org/gecko-media-plugin-service;1"); + if (!mps) { + RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Couldn't get MediaPluginService in GMPCDMProxy::gmp_Init")); + return; + } + + // Make a copy before we transfer ownership of aData to the + // gmp_InitGetGMPDecryptorCallback. + InitData data(*aData); + UniquePtr<GetNodeIdCallback> callback( + new gmp_InitGetGMPDecryptorCallback(this, Move(aData))); + nsresult rv = mps->GetNodeId(data.mOrigin, + data.mTopLevelOrigin, + data.mGMPName, + data.mInPrivateBrowsing, + Move(callback)); + if (NS_FAILED(rv)) { + RejectPromise(data.mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Call to GetNodeId() failed early")); + } +} + +void +GMPCDMProxy::gmp_InitGetGMPDecryptor(nsresult aResult, + const nsACString& aNodeId, + nsAutoPtr<InitData>&& aData) +{ + uint32_t promiseID = aData->mPromiseId; + if (NS_FAILED(aResult)) { + RejectPromise(promiseID, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("GetNodeId() called back, but with a failure result")); + return; + } + + mNodeId = aNodeId; + MOZ_ASSERT(!GetNodeId().IsEmpty()); + + nsCOMPtr<mozIGeckoMediaPluginService> mps = + do_GetService("@mozilla.org/gecko-media-plugin-service;1"); + if (!mps) { + RejectPromise(promiseID, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Couldn't get MediaPluginService in GMPCDMProxy::gmp_InitGetGMPDecryptor")); + return; + } + + EME_LOG("GMPCDMProxy::gmp_Init (%s, %s) %s NodeId=%s", + NS_ConvertUTF16toUTF8(aData->mOrigin).get(), + NS_ConvertUTF16toUTF8(aData->mTopLevelOrigin).get(), + (aData->mInPrivateBrowsing ? "PrivateBrowsing" : "NonPrivateBrowsing"), + GetNodeId().get()); + + nsTArray<nsCString> tags; + tags.AppendElement(NS_ConvertUTF16toUTF8(mKeySystem)); + + // Note: must capture helper refptr here, before the Move() + // when we create the GetGMPDecryptorCallback below. + RefPtr<GMPCrashHelper> crashHelper = Move(aData->mCrashHelper); + UniquePtr<GetGMPDecryptorCallback> callback(new gmp_InitDoneCallback(this, + Move(aData))); + nsresult rv = mps->GetGMPDecryptor(crashHelper, + &tags, + GetNodeId(), + Move(callback)); + if (NS_FAILED(rv)) { + RejectPromise(promiseID, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Call to GetGMPDecryptor() failed early")); + } +} + +void +GMPCDMProxy::OnCDMCreated(uint32_t aPromiseId) +{ + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + MOZ_ASSERT(!GetNodeId().IsEmpty()); + if (mCDM) { + mKeys->OnCDMCreated(aPromiseId, GetNodeId(), mCDM->GetPluginId()); + } else { + // No CDM? Just reject the promise. + mKeys->RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Null CDM in OnCDMCreated()")); + } +} + +void +GMPCDMProxy::CreateSession(uint32_t aCreateSessionToken, + dom::MediaKeySessionType aSessionType, + PromiseId aPromiseId, + const nsAString& aInitDataType, + nsTArray<uint8_t>& aInitData) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mOwnerThread); + + nsAutoPtr<CreateSessionData> data(new CreateSessionData()); + data->mSessionType = aSessionType; + data->mCreateSessionToken = aCreateSessionToken; + data->mPromiseId = aPromiseId; + data->mInitDataType = NS_ConvertUTF16toUTF8(aInitDataType); + data->mInitData = Move(aInitData); + + nsCOMPtr<nsIRunnable> task( + NewRunnableMethod<nsAutoPtr<CreateSessionData>>(this, &GMPCDMProxy::gmp_CreateSession, data)); + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); +} + +GMPSessionType +ToGMPSessionType(dom::MediaKeySessionType aSessionType) { + switch (aSessionType) { + case dom::MediaKeySessionType::Temporary: return kGMPTemporySession; + case dom::MediaKeySessionType::Persistent_license: return kGMPPersistentSession; + default: return kGMPTemporySession; + }; +}; + +void +GMPCDMProxy::gmp_CreateSession(nsAutoPtr<CreateSessionData> aData) +{ + MOZ_ASSERT(IsOnOwnerThread()); + if (!mCDM) { + RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Null CDM in gmp_CreateSession")); + return; + } + mCDM->CreateSession(aData->mCreateSessionToken, + aData->mPromiseId, + aData->mInitDataType, + aData->mInitData, + ToGMPSessionType(aData->mSessionType)); +} + +void +GMPCDMProxy::LoadSession(PromiseId aPromiseId, + const nsAString& aSessionId) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mOwnerThread); + + nsAutoPtr<SessionOpData> data(new SessionOpData()); + data->mPromiseId = aPromiseId; + data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId); + nsCOMPtr<nsIRunnable> task( + NewRunnableMethod<nsAutoPtr<SessionOpData>>(this, &GMPCDMProxy::gmp_LoadSession, data)); + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); +} + +void +GMPCDMProxy::gmp_LoadSession(nsAutoPtr<SessionOpData> aData) +{ + MOZ_ASSERT(IsOnOwnerThread()); + + if (!mCDM) { + RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Null CDM in gmp_LoadSession")); + return; + } + mCDM->LoadSession(aData->mPromiseId, aData->mSessionId); +} + +void +GMPCDMProxy::SetServerCertificate(PromiseId aPromiseId, + nsTArray<uint8_t>& aCert) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mOwnerThread); + + nsAutoPtr<SetServerCertificateData> data(new SetServerCertificateData()); + data->mPromiseId = aPromiseId; + data->mCert = Move(aCert); + nsCOMPtr<nsIRunnable> task( + NewRunnableMethod<nsAutoPtr<SetServerCertificateData>>(this, &GMPCDMProxy::gmp_SetServerCertificate, data)); + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); +} + +void +GMPCDMProxy::gmp_SetServerCertificate(nsAutoPtr<SetServerCertificateData> aData) +{ + MOZ_ASSERT(IsOnOwnerThread()); + if (!mCDM) { + RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Null CDM in gmp_SetServerCertificate")); + return; + } + mCDM->SetServerCertificate(aData->mPromiseId, aData->mCert); +} + +void +GMPCDMProxy::UpdateSession(const nsAString& aSessionId, + PromiseId aPromiseId, + nsTArray<uint8_t>& aResponse) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mOwnerThread); + NS_ENSURE_TRUE_VOID(!mKeys.IsNull()); + + nsAutoPtr<UpdateSessionData> data(new UpdateSessionData()); + data->mPromiseId = aPromiseId; + data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId); + data->mResponse = Move(aResponse); + nsCOMPtr<nsIRunnable> task( + NewRunnableMethod<nsAutoPtr<UpdateSessionData>>(this, &GMPCDMProxy::gmp_UpdateSession, data)); + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); +} + +void +GMPCDMProxy::gmp_UpdateSession(nsAutoPtr<UpdateSessionData> aData) +{ + MOZ_ASSERT(IsOnOwnerThread()); + if (!mCDM) { + RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Null CDM in gmp_UpdateSession")); + return; + } + mCDM->UpdateSession(aData->mPromiseId, + aData->mSessionId, + aData->mResponse); +} + +void +GMPCDMProxy::CloseSession(const nsAString& aSessionId, + PromiseId aPromiseId) +{ + MOZ_ASSERT(NS_IsMainThread()); + NS_ENSURE_TRUE_VOID(!mKeys.IsNull()); + + nsAutoPtr<SessionOpData> data(new SessionOpData()); + data->mPromiseId = aPromiseId; + data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId); + nsCOMPtr<nsIRunnable> task( + NewRunnableMethod<nsAutoPtr<SessionOpData>>(this, &GMPCDMProxy::gmp_CloseSession, data)); + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); +} + +void +GMPCDMProxy::gmp_CloseSession(nsAutoPtr<SessionOpData> aData) +{ + MOZ_ASSERT(IsOnOwnerThread()); + if (!mCDM) { + RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Null CDM in gmp_CloseSession")); + return; + } + mCDM->CloseSession(aData->mPromiseId, aData->mSessionId); +} + +void +GMPCDMProxy::RemoveSession(const nsAString& aSessionId, + PromiseId aPromiseId) +{ + MOZ_ASSERT(NS_IsMainThread()); + NS_ENSURE_TRUE_VOID(!mKeys.IsNull()); + + nsAutoPtr<SessionOpData> data(new SessionOpData()); + data->mPromiseId = aPromiseId; + data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId); + nsCOMPtr<nsIRunnable> task( + NewRunnableMethod<nsAutoPtr<SessionOpData>>(this, &GMPCDMProxy::gmp_RemoveSession, data)); + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); +} + +void +GMPCDMProxy::gmp_RemoveSession(nsAutoPtr<SessionOpData> aData) +{ + MOZ_ASSERT(IsOnOwnerThread()); + if (!mCDM) { + RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Null CDM in gmp_RemoveSession")); + return; + } + mCDM->RemoveSession(aData->mPromiseId, aData->mSessionId); +} + +void +GMPCDMProxy::Shutdown() +{ + MOZ_ASSERT(NS_IsMainThread()); + mKeys.Clear(); + // Note: This may end up being the last owning reference to the GMPCDMProxy. + nsCOMPtr<nsIRunnable> task(NewRunnableMethod(this, &GMPCDMProxy::gmp_Shutdown)); + if (mOwnerThread) { + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); + } +} + +void +GMPCDMProxy::gmp_Shutdown() +{ + MOZ_ASSERT(IsOnOwnerThread()); + + mShutdownCalled = true; + + // Abort any pending decrypt jobs, to awaken any clients waiting on a job. + for (size_t i = 0; i < mDecryptionJobs.Length(); i++) { + DecryptJob* job = mDecryptionJobs[i]; + job->PostResult(AbortedErr); + } + mDecryptionJobs.Clear(); + + if (mCDM) { + mCDM->Close(); + mCDM = nullptr; + } +} + +void +GMPCDMProxy::RejectPromise(PromiseId aId, nsresult aCode, + const nsCString& aReason) +{ + if (NS_IsMainThread()) { + if (!mKeys.IsNull()) { + mKeys->RejectPromise(aId, aCode, aReason); + } + } else { + nsCOMPtr<nsIRunnable> task(new RejectPromiseTask(this, aId, aCode, + aReason)); + NS_DispatchToMainThread(task); + } +} + +void +GMPCDMProxy::ResolvePromise(PromiseId aId) +{ + if (NS_IsMainThread()) { + if (!mKeys.IsNull()) { + mKeys->ResolvePromise(aId); + } else { + NS_WARNING("GMPCDMProxy unable to resolve promise!"); + } + } else { + nsCOMPtr<nsIRunnable> task; + task = NewRunnableMethod<PromiseId>(this, + &GMPCDMProxy::ResolvePromise, + aId); + NS_DispatchToMainThread(task); + } +} + +const nsCString& +GMPCDMProxy::GetNodeId() const +{ + return mNodeId; +} + +void +GMPCDMProxy::OnSetSessionId(uint32_t aCreateSessionToken, + const nsAString& aSessionId) +{ + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + + RefPtr<dom::MediaKeySession> session(mKeys->GetPendingSession(aCreateSessionToken)); + if (session) { + session->SetSessionId(aSessionId); + } +} + +void +GMPCDMProxy::OnResolveLoadSessionPromise(uint32_t aPromiseId, bool aSuccess) +{ + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + mKeys->OnSessionLoaded(aPromiseId, aSuccess); +} + +void +GMPCDMProxy::OnSessionMessage(const nsAString& aSessionId, + dom::MediaKeyMessageType aMessageType, + nsTArray<uint8_t>& aMessage) +{ + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId)); + if (session) { + session->DispatchKeyMessage(aMessageType, aMessage); + } +} + +void +GMPCDMProxy::OnKeyStatusesChange(const nsAString& aSessionId) +{ + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId)); + if (session) { + session->DispatchKeyStatusesChange(); + } +} + +void +GMPCDMProxy::OnExpirationChange(const nsAString& aSessionId, + GMPTimestamp aExpiryTime) +{ + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId)); + if (session) { + session->SetExpiration(static_cast<double>(aExpiryTime)); + } +} + +void +GMPCDMProxy::OnSessionClosed(const nsAString& aSessionId) +{ + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId)); + if (session) { + session->OnClosed(); + } +} + +void +GMPCDMProxy::OnDecrypted(uint32_t aId, + DecryptStatus aResult, + const nsTArray<uint8_t>& aDecryptedData) +{ + MOZ_ASSERT(IsOnOwnerThread()); + gmp_Decrypted(aId, aResult, aDecryptedData); +} + +static void +LogToConsole(const nsAString& aMsg) +{ + nsCOMPtr<nsIConsoleService> console( + do_GetService("@mozilla.org/consoleservice;1")); + if (!console) { + NS_WARNING("Failed to log message to console."); + return; + } + nsAutoString msg(aMsg); + console->LogStringMessage(msg.get()); +} + +void +GMPCDMProxy::OnSessionError(const nsAString& aSessionId, + nsresult aException, + uint32_t aSystemCode, + const nsAString& aMsg) +{ + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId)); + if (session) { + session->DispatchKeyError(aSystemCode); + } + LogToConsole(aMsg); +} + +void +GMPCDMProxy::OnRejectPromise(uint32_t aPromiseId, + nsresult aDOMException, + const nsCString& aMsg) +{ + MOZ_ASSERT(NS_IsMainThread()); + RejectPromise(aPromiseId, aDOMException, aMsg); +} + +const nsString& +GMPCDMProxy::KeySystem() const +{ + return mKeySystem; +} + +CDMCaps& +GMPCDMProxy::Capabilites() { + return mCapabilites; +} + +RefPtr<GMPCDMProxy::DecryptPromise> +GMPCDMProxy::Decrypt(MediaRawData* aSample) +{ + RefPtr<DecryptJob> job(new DecryptJob(aSample)); + RefPtr<DecryptPromise> promise(job->Ensure()); + + nsCOMPtr<nsIRunnable> task( + NewRunnableMethod<RefPtr<DecryptJob>>(this, &GMPCDMProxy::gmp_Decrypt, job)); + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); + return promise; +} + +void +GMPCDMProxy::gmp_Decrypt(RefPtr<DecryptJob> aJob) +{ + MOZ_ASSERT(IsOnOwnerThread()); + + if (!mCDM) { + aJob->PostResult(AbortedErr); + return; + } + + aJob->mId = ++mDecryptionJobCount; + nsTArray<uint8_t> data; + data.AppendElements(aJob->mSample->Data(), aJob->mSample->Size()); + mCDM->Decrypt(aJob->mId, aJob->mSample->mCrypto, data); + mDecryptionJobs.AppendElement(aJob.forget()); +} + +void +GMPCDMProxy::gmp_Decrypted(uint32_t aId, + DecryptStatus aResult, + const nsTArray<uint8_t>& aDecryptedData) +{ + MOZ_ASSERT(IsOnOwnerThread()); +#ifdef DEBUG + bool jobIdFound = false; +#endif + for (size_t i = 0; i < mDecryptionJobs.Length(); i++) { + DecryptJob* job = mDecryptionJobs[i]; + if (job->mId == aId) { +#ifdef DEBUG + jobIdFound = true; +#endif + job->PostResult(aResult, aDecryptedData); + mDecryptionJobs.RemoveElementAt(i); + } + } +#ifdef DEBUG + if (!jobIdFound) { + NS_WARNING("GMPDecryptorChild returned incorrect job ID"); + } +#endif +} + +void +GMPCDMProxy::DecryptJob::PostResult(DecryptStatus aResult) +{ + nsTArray<uint8_t> empty; + PostResult(aResult, empty); +} + +void +GMPCDMProxy::DecryptJob::PostResult(DecryptStatus aResult, + const nsTArray<uint8_t>& aDecryptedData) +{ + if (aDecryptedData.Length() != mSample->Size()) { + NS_WARNING("CDM returned incorrect number of decrypted bytes"); + } + if (aResult == Ok) { + nsAutoPtr<MediaRawDataWriter> writer(mSample->CreateWriter()); + PodCopy(writer->Data(), + aDecryptedData.Elements(), + std::min<size_t>(aDecryptedData.Length(), mSample->Size())); + } else if (aResult == NoKeyErr) { + NS_WARNING("CDM returned NoKeyErr"); + // We still have the encrypted sample, so we can re-enqueue it to be + // decrypted again once the key is usable again. + } else { + nsAutoCString str("CDM returned decode failure DecryptStatus="); + str.AppendInt(aResult); + NS_WARNING(str.get()); + } + mPromise.Resolve(DecryptResult(aResult, mSample), __func__); +} + +void +GMPCDMProxy::GetSessionIdsForKeyId(const nsTArray<uint8_t>& aKeyId, + nsTArray<nsCString>& aSessionIds) +{ + CDMCaps::AutoLock caps(Capabilites()); + caps.GetSessionIdsForKeyId(aKeyId, aSessionIds); +} + +void +GMPCDMProxy::Terminated() +{ + MOZ_ASSERT(NS_IsMainThread()); + NS_WARNING("CDM terminated"); + if (mCreatePromiseId) { + RejectPromise(mCreatePromiseId, + NS_ERROR_DOM_MEDIA_FATAL_ERR, + NS_LITERAL_CSTRING("Crashed waiting for CDM to initialize")); + mCreatePromiseId = 0; + } + if (!mKeys.IsNull()) { + mKeys->Terminated(); + } +} + +uint32_t +GMPCDMProxy::GetDecryptorId() +{ + return mDecryptorId; +} + +} // namespace mozilla diff --git a/dom/media/gmp/GMPCDMProxy.h b/dom/media/gmp/GMPCDMProxy.h new file mode 100644 index 000000000..a7fae235b --- /dev/null +++ b/dom/media/gmp/GMPCDMProxy.h @@ -0,0 +1,265 @@ +/* -*- Mode: C++; tab-width: 8; 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/. */ + +#ifndef GMPCDMProxy_h_ +#define GMPCDMProxy_h_ + +#include "mozilla/CDMProxy.h" +#include "GMPCDMCallbackProxy.h" +#include "GMPDecryptorProxy.h" + +namespace mozilla { +class MediaRawData; + +// Implementation of CDMProxy which is based on GMP architecture. +class GMPCDMProxy : public CDMProxy { +public: + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPCDMProxy, override) + + typedef MozPromise<DecryptResult, DecryptResult, /* IsExclusive = */ true> DecryptPromise; + + GMPCDMProxy(dom::MediaKeys* aKeys, + const nsAString& aKeySystem, + GMPCrashHelper* aCrashHelper, + bool aDistinctiveIdentifierRequired, + bool aPersistentStateRequired); + + void Init(PromiseId aPromiseId, + const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, + const nsAString& aGMPName, + bool aInPrivateBrowsing) override; + + void OnSetDecryptorId(uint32_t aId) override; + + void CreateSession(uint32_t aCreateSessionToken, + dom::MediaKeySessionType aSessionType, + PromiseId aPromiseId, + const nsAString& aInitDataType, + nsTArray<uint8_t>& aInitData) override; + + void LoadSession(PromiseId aPromiseId, + const nsAString& aSessionId) override; + + void SetServerCertificate(PromiseId aPromiseId, + nsTArray<uint8_t>& aCert) override; + + void UpdateSession(const nsAString& aSessionId, + PromiseId aPromiseId, + nsTArray<uint8_t>& aResponse) override; + + void CloseSession(const nsAString& aSessionId, + PromiseId aPromiseId) override; + + void RemoveSession(const nsAString& aSessionId, + PromiseId aPromiseId) override; + + void Shutdown() override; + + void Terminated() override; + + const nsCString& GetNodeId() const 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, + nsTArray<uint8_t>& aMessage) override; + + void OnExpirationChange(const nsAString& aSessionId, + GMPTimestamp 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, + nsresult aDOMException, + const nsCString& aMsg) override; + + RefPtr<DecryptPromise> Decrypt(MediaRawData* aSample) override; + + void OnDecrypted(uint32_t aId, + DecryptStatus aResult, + const nsTArray<uint8_t>& aDecryptedData) override; + + void RejectPromise(PromiseId aId, nsresult aExceptionCode, + const nsCString& aReason) override; + + void ResolvePromise(PromiseId aId) override; + + const nsString& KeySystem() const override; + + CDMCaps& Capabilites() override; + + void OnKeyStatusesChange(const nsAString& aSessionId) override; + + void GetSessionIdsForKeyId(const nsTArray<uint8_t>& aKeyId, + nsTArray<nsCString>& aSessionIds) override; + +#ifdef DEBUG + bool IsOnOwnerThread() override; +#endif + + uint32_t GetDecryptorId() override; + +private: + friend class gmp_InitDoneCallback; + friend class gmp_InitGetGMPDecryptorCallback; + + struct InitData { + uint32_t mPromiseId; + nsString mOrigin; + nsString mTopLevelOrigin; + nsString mGMPName; + RefPtr<GMPCrashHelper> mCrashHelper; + bool mInPrivateBrowsing; + }; + + // GMP thread only. + void gmp_Init(nsAutoPtr<InitData>&& aData); + void gmp_InitDone(GMPDecryptorProxy* aCDM, nsAutoPtr<InitData>&& aData); + void gmp_InitGetGMPDecryptor(nsresult aResult, + const nsACString& aNodeId, + nsAutoPtr<InitData>&& aData); + + // GMP thread only. + void gmp_Shutdown(); + + // Main thread only. + void OnCDMCreated(uint32_t aPromiseId); + + struct CreateSessionData { + dom::MediaKeySessionType mSessionType; + uint32_t mCreateSessionToken; + PromiseId mPromiseId; + nsCString mInitDataType; + nsTArray<uint8_t> mInitData; + }; + // GMP thread only. + void gmp_CreateSession(nsAutoPtr<CreateSessionData> aData); + + struct SessionOpData { + PromiseId mPromiseId; + nsCString mSessionId; + }; + // GMP thread only. + void gmp_LoadSession(nsAutoPtr<SessionOpData> aData); + + struct SetServerCertificateData { + PromiseId mPromiseId; + nsTArray<uint8_t> mCert; + }; + // GMP thread only. + void gmp_SetServerCertificate(nsAutoPtr<SetServerCertificateData> aData); + + struct UpdateSessionData { + PromiseId mPromiseId; + nsCString mSessionId; + nsTArray<uint8_t> mResponse; + }; + // GMP thread only. + void gmp_UpdateSession(nsAutoPtr<UpdateSessionData> aData); + + // GMP thread only. + void gmp_CloseSession(nsAutoPtr<SessionOpData> aData); + + // GMP thread only. + void gmp_RemoveSession(nsAutoPtr<SessionOpData> aData); + + class DecryptJob { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DecryptJob) + + explicit DecryptJob(MediaRawData* aSample) + : mId(0) + , mSample(aSample) + { + } + + void PostResult(DecryptStatus aResult, + const nsTArray<uint8_t>& aDecryptedData); + void PostResult(DecryptStatus aResult); + + RefPtr<DecryptPromise> Ensure() { + return mPromise.Ensure(__func__); + } + + uint32_t mId; + RefPtr<MediaRawData> mSample; + private: + ~DecryptJob() {} + MozPromiseHolder<DecryptPromise> mPromise; + }; + // GMP thread only. + void gmp_Decrypt(RefPtr<DecryptJob> aJob); + + // GMP thread only. + void gmp_Decrypted(uint32_t aId, + DecryptStatus aResult, + const nsTArray<uint8_t>& aDecryptedData); + + class RejectPromiseTask : public Runnable { + public: + RejectPromiseTask(GMPCDMProxy* aProxy, + PromiseId aId, + nsresult aCode, + const nsCString& aReason) + : mProxy(aProxy) + , mId(aId) + , mCode(aCode) + , mReason(aReason) + { + } + NS_IMETHOD Run() override { + mProxy->RejectPromise(mId, mCode, mReason); + return NS_OK; + } + private: + RefPtr<GMPCDMProxy> mProxy; + PromiseId mId; + nsresult mCode; + nsCString mReason; + }; + + ~GMPCDMProxy(); + + GMPCrashHelper* mCrashHelper; + + GMPDecryptorProxy* mCDM; + + nsAutoPtr<GMPCDMCallbackProxy> mCallback; + + // Decryption jobs sent to CDM, awaiting result. + // GMP thread only. + nsTArray<RefPtr<DecryptJob>> mDecryptionJobs; + + // Number of buffers we've decrypted. Used to uniquely identify + // decryption jobs sent to CDM. Note we can't just use the length of + // mDecryptionJobs as that shrinks as jobs are completed and removed + // from it. + // GMP thread only. + uint32_t mDecryptionJobCount; + + // True if GMPCDMProxy::gmp_Shutdown was called. + // GMP thread only. + bool mShutdownCalled; + + uint32_t mDecryptorId; + + PromiseId mCreatePromiseId; +}; + + +} // namespace mozilla + +#endif // GMPCDMProxy_h_ diff --git a/dom/media/gmp/GMPChild.cpp b/dom/media/gmp/GMPChild.cpp index 7941183b5..eb1803736 100644 --- a/dom/media/gmp/GMPChild.cpp +++ b/dom/media/gmp/GMPChild.cpp @@ -22,6 +22,9 @@ #include "GMPUtils.h" #include "prio.h" #include "base/task.h" +#ifdef MOZ_EME +#include "widevine-adapter/WidevineAdapter.h" +#endif using namespace mozilla::ipc; @@ -251,7 +254,13 @@ GMPChild::AnswerStartPlugin(const nsString& aAdapter) return false; } +#ifdef MOZ_EME + bool isWidevine = aAdapter.EqualsLiteral("widevine"); + + GMPAdapter* adapter = (isWidevine) ? new WidevineAdapter() : nullptr; +#else GMPAdapter* adapter = nullptr; +#endif if (!mGMPLoader->Load(libPath.get(), libPath.Length(), mNodeId.BeginWriting(), diff --git a/dom/media/gmp/GMPDecryptorParent.cpp b/dom/media/gmp/GMPDecryptorParent.cpp index f2a5d2431..56474e973 100644 --- a/dom/media/gmp/GMPDecryptorParent.cpp +++ b/dom/media/gmp/GMPDecryptorParent.cpp @@ -45,7 +45,13 @@ GMPDecryptorParent::~GMPDecryptorParent() bool GMPDecryptorParent::RecvSetDecryptorId(const uint32_t& aId) { - /* STUB */ +#ifdef MOZ_EME + if (!mIsOpen) { + NS_WARNING("Trying to use a dead GMP decrypter!"); + return false; + } + mCallback->SetDecryptorId(aId); +#endif return true; } @@ -200,7 +206,16 @@ bool GMPDecryptorParent::RecvSetSessionId(const uint32_t& aCreateSessionId, const nsCString& aSessionId) { - /* STUB */ +#ifdef MOZ_EME + LOGD(("GMPDecryptorParent[%p]::RecvSetSessionId(token=%u, sessionId='%s')", + this, aCreateSessionId, aSessionId.get())); + + if (!mIsOpen) { + NS_WARNING("Trying to use a dead GMP decrypter!"); + return false; + } + mCallback->SetSessionId(aCreateSessionId, aSessionId); +#endif return true; } @@ -208,14 +223,32 @@ bool GMPDecryptorParent::RecvResolveLoadSessionPromise(const uint32_t& aPromiseId, const bool& aSuccess) { - /* STUB */ +#ifdef MOZ_EME + LOGD(("GMPDecryptorParent[%p]::RecvResolveLoadSessionPromise(promiseId=%u)", + this, aPromiseId)); + + if (!mIsOpen) { + NS_WARNING("Trying to use a dead GMP decrypter!"); + return false; + } + mCallback->ResolveLoadSessionPromise(aPromiseId, aSuccess); +#endif return true; } bool GMPDecryptorParent::RecvResolvePromise(const uint32_t& aPromiseId) { - /* STUB */ +#ifdef MOZ_EME + LOGD(("GMPDecryptorParent[%p]::RecvResolvePromise(promiseId=%u)", + this, aPromiseId)); + + if (!mIsOpen) { + NS_WARNING("Trying to use a dead GMP decrypter!"); + return false; + } + mCallback->ResolvePromise(aPromiseId); +#endif return true; } @@ -243,16 +276,47 @@ GMPDecryptorParent::RecvRejectPromise(const uint32_t& aPromiseId, const GMPDOMException& aException, const nsCString& aMessage) { - /* STUB */ +#ifdef MOZ_EME + LOGD(("GMPDecryptorParent[%p]::RecvRejectPromise(promiseId=%u, exception=%d, msg='%s')", + this, aPromiseId, aException, aMessage.get())); + + if (!mIsOpen) { + NS_WARNING("Trying to use a dead GMP decrypter!"); + return false; + } + mCallback->RejectPromise(aPromiseId, GMPExToNsresult(aException), aMessage); +#endif return true; } +#ifdef MOZ_EME +static dom::MediaKeyMessageType +ToMediaKeyMessageType(GMPSessionMessageType aMessageType) { + switch (aMessageType) { + case kGMPLicenseRequest: return dom::MediaKeyMessageType::License_request; + case kGMPLicenseRenewal: return dom::MediaKeyMessageType::License_renewal; + case kGMPLicenseRelease: return dom::MediaKeyMessageType::License_release; + case kGMPIndividualizationRequest: return dom::MediaKeyMessageType::Individualization_request; + default: return dom::MediaKeyMessageType::License_request; + }; +}; +#endif + bool GMPDecryptorParent::RecvSessionMessage(const nsCString& aSessionId, const GMPSessionMessageType& aMessageType, nsTArray<uint8_t>&& aMessage) { - /* STUB */ +#ifdef MOZ_EME + LOGD(("GMPDecryptorParent[%p]::RecvSessionMessage(sessionId='%s', type=%d, msg='%s')", + this, aSessionId.get(), aMessageType, ToBase64(aMessage).get())); + + if (!mIsOpen) { + NS_WARNING("Trying to use a dead GMP decrypter!"); + return false; + } + mCallback->SessionMessage(aSessionId, ToMediaKeyMessageType(aMessageType), aMessage); +#endif return true; } @@ -260,14 +324,32 @@ bool GMPDecryptorParent::RecvExpirationChange(const nsCString& aSessionId, const double& aExpiryTime) { - /* STUB */ +#ifdef MOZ_EME + LOGD(("GMPDecryptorParent[%p]::RecvExpirationChange(sessionId='%s', expiry=%lf)", + this, aSessionId.get(), aExpiryTime)); + + if (!mIsOpen) { + NS_WARNING("Trying to use a dead GMP decrypter!"); + return false; + } + mCallback->ExpirationChange(aSessionId, aExpiryTime); +#endif return true; } bool GMPDecryptorParent::RecvSessionClosed(const nsCString& aSessionId) { - /* STUB */ +#ifdef MOZ_EME + LOGD(("GMPDecryptorParent[%p]::RecvSessionClosed(sessionId='%s')", + this, aSessionId.get())); + + if (!mIsOpen) { + NS_WARNING("Trying to use a dead GMP decrypter!"); + return false; + } + mCallback->SessionClosed(aSessionId); +#endif return true; } @@ -277,24 +359,95 @@ GMPDecryptorParent::RecvSessionError(const nsCString& aSessionId, const uint32_t& aSystemCode, const nsCString& aMessage) { - /* STUB */ +#ifdef MOZ_EME + LOGD(("GMPDecryptorParent[%p]::RecvSessionError(sessionId='%s', exception=%d, sysCode=%d, msg='%s')", + this, aSessionId.get(), + aException, aSystemCode, aMessage.get())); + + if (!mIsOpen) { + NS_WARNING("Trying to use a dead GMP decrypter!"); + return false; + } + mCallback->SessionError(aSessionId, + GMPExToNsresult(aException), + aSystemCode, + aMessage); +#endif return true; } +#ifdef MOZ_EME +static dom::MediaKeyStatus +ToMediaKeyStatus(GMPMediaKeyStatus aStatus) { + switch (aStatus) { + case kGMPUsable: return dom::MediaKeyStatus::Usable; + case kGMPExpired: return dom::MediaKeyStatus::Expired; + case kGMPOutputDownscaled: return dom::MediaKeyStatus::Output_downscaled; + case kGMPOutputRestricted: return dom::MediaKeyStatus::Output_restricted; + case kGMPInternalError: return dom::MediaKeyStatus::Internal_error; + case kGMPReleased: return dom::MediaKeyStatus::Released; + case kGMPStatusPending: return dom::MediaKeyStatus::Status_pending; + default: return dom::MediaKeyStatus::Internal_error; + } +} +#endif + bool GMPDecryptorParent::RecvBatchedKeyStatusChanged(const nsCString& aSessionId, InfallibleTArray<GMPKeyInformation>&& aKeyInfos) { - /* STUB */ +#ifdef MOZ_EME + LOGD(("GMPDecryptorParent[%p]::RecvBatchedKeyStatusChanged(sessionId='%s', KeyInfos len='%d')", + this, aSessionId.get(), aKeyInfos.Length())); + + if (mIsOpen) { + nsTArray<CDMKeyInfo> cdmKeyInfos(aKeyInfos.Length()); + for (uint32_t i = 0; i < aKeyInfos.Length(); i++) { + LOGD(("GMPDecryptorParent[%p]::RecvBatchedKeyStatusChanged(keyId=%s, gmp-status=%d)", + this, ToBase64(aKeyInfos[i].keyId()).get(), aKeyInfos[i].status())); + // If the status is kGMPUnknown, we're going to forget(remove) that key info. + if (aKeyInfos[i].status() != kGMPUnknown) { + auto status = ToMediaKeyStatus(aKeyInfos[i].status()); + cdmKeyInfos.AppendElement(CDMKeyInfo(aKeyInfos[i].keyId(), + dom::Optional<dom::MediaKeyStatus>(status))); + } else { + cdmKeyInfos.AppendElement(CDMKeyInfo(aKeyInfos[i].keyId())); + } + } + mCallback->BatchedKeyStatusChanged(aSessionId, cdmKeyInfos); + } +#endif return true; } +#ifdef MOZ_EME +DecryptStatus +ToDecryptStatus(GMPErr aError) +{ + switch (aError) { + case GMPNoErr: return Ok; + case GMPNoKeyErr: return NoKeyErr; + case GMPAbortedErr: return AbortedErr; + default: return GenericErr; + } +} +#endif + bool GMPDecryptorParent::RecvDecrypted(const uint32_t& aId, const GMPErr& aErr, InfallibleTArray<uint8_t>&& aBuffer) { - /* STUB */ +#ifdef MOZ_EME + LOGV(("GMPDecryptorParent[%p]::RecvDecrypted(id=%d, err=%d)", + this, aId, aErr)); + + if (!mIsOpen) { + NS_WARNING("Trying to use a dead GMP decrypter!"); + return false; + } + mCallback->Decrypted(aId, ToDecryptStatus(aErr), aBuffer); +#endif return true; } diff --git a/dom/media/gmp/GMPDecryptorProxy.h b/dom/media/gmp/GMPDecryptorProxy.h index f9e34a45f..ed16755f8 100644 --- a/dom/media/gmp/GMPDecryptorProxy.h +++ b/dom/media/gmp/GMPDecryptorProxy.h @@ -6,6 +6,9 @@ #ifndef GMPDecryptorProxy_h_ #define GMPDecryptorProxy_h_ +#ifdef MOZ_EME +#include "mozilla/DecryptorProxyCallback.h" +#endif #include "GMPCallbackBase.h" #include "gmp-decryption.h" #include "nsString.h" @@ -14,7 +17,12 @@ namespace mozilla { class CryptoSample; } // namespace mozilla +#ifdef MOZ_EME +class GMPDecryptorProxyCallback : public DecryptorProxyCallback, + public GMPCallbackBase { +#else class GMPDecryptorProxyCallback : public GMPCallbackBase { +#endif public: virtual ~GMPDecryptorProxyCallback() {} diff --git a/dom/media/gmp/GMPParent.cpp b/dom/media/gmp/GMPParent.cpp index 11dbc6b96..e6f797264 100644 --- a/dom/media/gmp/GMPParent.cpp +++ b/dom/media/gmp/GMPParent.cpp @@ -28,6 +28,11 @@ using mozilla::ipc::GeckoChildProcessHost; #include "WMFDecoderModule.h" #endif +#ifdef MOZ_EME +#include "mozilla/dom/WidevineCDMManifestBinding.h" +#include "widevine-adapter/WidevineAdapter.h" +#endif + namespace mozilla { #undef LOG @@ -649,7 +654,18 @@ GMPParent::ReadGMPMetaData() return ReadGMPInfoFile(infoFile); } +#ifdef MOZ_EME + // Maybe this is the Widevine adapted plugin? + nsCOMPtr<nsIFile> manifestFile; + rv = mDirectory->Clone(getter_AddRefs(manifestFile)); + if (NS_FAILED(rv)) { + return GenericPromise::CreateAndReject(rv, __func__); + } + manifestFile->AppendRelativePath(NS_LITERAL_STRING("manifest.json")); + return ReadChromiumManifestFile(manifestFile); +#else return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); +#endif } RefPtr<GenericPromise> @@ -742,9 +758,48 @@ GMPParent::ReadChromiumManifestFile(nsIFile* aFile) RefPtr<GenericPromise> GMPParent::ParseChromiumManifest(nsString aJSON) { - // TODO: Remove This. +#ifdef MOZ_EME + LOGD("%s: for '%s'", __FUNCTION__, NS_LossyConvertUTF16toASCII(aJSON).get()); + + MOZ_ASSERT(NS_IsMainThread()); + mozilla::dom::WidevineCDMManifest m; + if (!m.Init(aJSON)) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + nsresult ignored; // Note: ToInteger returns 0 on failure. + if (!WidevineAdapter::Supports(m.mX_cdm_module_versions.ToInteger(&ignored), + m.mX_cdm_interface_versions.ToInteger(&ignored), + m.mX_cdm_host_versions.ToInteger(&ignored))) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + mDisplayName = NS_ConvertUTF16toUTF8(m.mName); + mDescription = NS_ConvertUTF16toUTF8(m.mDescription); + mVersion = NS_ConvertUTF16toUTF8(m.mVersion); + + GMPCapability video(NS_LITERAL_CSTRING(GMP_API_VIDEO_DECODER)); + video.mAPITags.AppendElement(NS_LITERAL_CSTRING("h264")); + video.mAPITags.AppendElement(NS_LITERAL_CSTRING("vp8")); + video.mAPITags.AppendElement(NS_LITERAL_CSTRING("vp9")); + video.mAPITags.AppendElement(kEMEKeySystemWidevine); + mCapabilities.AppendElement(Move(video)); + + GMPCapability decrypt(NS_LITERAL_CSTRING(GMP_API_DECRYPTOR)); + decrypt.mAPITags.AppendElement(kEMEKeySystemWidevine); + mCapabilities.AppendElement(Move(decrypt)); + + MOZ_ASSERT(mName.EqualsLiteral("widevinecdm")); + mAdapter = NS_LITERAL_STRING("widevine"); +#ifdef XP_WIN + mLibs = NS_LITERAL_CSTRING("dxva2.dll"); +#endif + + return GenericPromise::CreateAndResolve(true, __func__); +#else // !MOZ_EME MOZ_ASSERT_UNREACHABLE("don't call me if EME isn't enabled"); return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); +#endif // !MOZ_EME } bool diff --git a/dom/media/gmp/moz.build b/dom/media/gmp/moz.build index 3ff90b2c8..52dc673cd 100644 --- a/dom/media/gmp/moz.build +++ b/dom/media/gmp/moz.build @@ -70,6 +70,12 @@ EXPORTS += [ 'GMPVideoPlaneImpl.h', ] +if CONFIG['MOZ_EME']: + EXPORTS += [ + 'GMPCDMCallbackProxy.h', + 'GMPCDMProxy.h', + ] + SOURCES += [ 'GMPAudioDecoderChild.cpp', 'GMPAudioDecoderParent.cpp', @@ -105,8 +111,17 @@ SOURCES += [ 'GMPVideoPlaneImpl.cpp', ] +if CONFIG['MOZ_EME']: + SOURCES += [ + 'GMPCDMCallbackProxy.cpp', + 'GMPCDMProxy.cpp', + ] + DIRS += ['rlz'] +if CONFIG['MOZ_EME']: + DIRS += ['widevine-adapter'] + IPDL_SOURCES += [ 'GMPTypes.ipdlh', 'PGMP.ipdl', diff --git a/dom/media/gmp/widevine-adapter/WidevineAdapter.cpp b/dom/media/gmp/widevine-adapter/WidevineAdapter.cpp new file mode 100644 index 000000000..57d4ecec2 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/WidevineAdapter.cpp @@ -0,0 +1,168 @@ +/* -*- 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 "WidevineAdapter.h" +#include "content_decryption_module.h" +#include "VideoUtils.h" +#include "WidevineDecryptor.h" +#include "WidevineUtils.h" +#include "WidevineVideoDecoder.h" +#include "gmp-api/gmp-entrypoints.h" +#include "gmp-api/gmp-decryption.h" +#include "gmp-api/gmp-video-codec.h" +#include "gmp-api/gmp-platform.h" + +static const GMPPlatformAPI* sPlatform = nullptr; + +namespace mozilla { + +GMPErr GMPGetCurrentTime(GMPTimestamp* aOutTime) { + return sPlatform->getcurrenttime(aOutTime); +} + +// Call on main thread only. +GMPErr GMPSetTimerOnMainThread(GMPTask* aTask, int64_t aTimeoutMS) { + return sPlatform->settimer(aTask, aTimeoutMS); +} + +GMPErr GMPCreateRecord(const char* aRecordName, + uint32_t aRecordNameSize, + GMPRecord** aOutRecord, + GMPRecordClient* aClient) +{ + return sPlatform->createrecord(aRecordName, aRecordNameSize, aOutRecord, aClient); +} + +void +WidevineAdapter::SetAdaptee(PRLibrary* aLib) +{ + mLib = aLib; +} + +void* GetCdmHost(int aHostInterfaceVersion, void* aUserData) +{ + Log("GetCdmHostFunc(%d, %p)", aHostInterfaceVersion, aUserData); + WidevineDecryptor* decryptor = reinterpret_cast<WidevineDecryptor*>(aUserData); + MOZ_ASSERT(decryptor); + return static_cast<cdm::Host_9*>(decryptor); +} + +#define STRINGIFY(s) _STRINGIFY(s) +#define _STRINGIFY(s) #s + +GMPErr +WidevineAdapter::GMPInit(const GMPPlatformAPI* aPlatformAPI) +{ +#ifdef ENABLE_WIDEVINE_LOG + if (getenv("GMP_LOG_FILE")) { + // Clear log file. + FILE* f = fopen(getenv("GMP_LOG_FILE"), "w"); + if (f) { + fclose(f); + } + } +#endif + + sPlatform = aPlatformAPI; + if (!mLib) { + return GMPGenericErr; + } + + auto init = reinterpret_cast<decltype(::INITIALIZE_CDM_MODULE)*>( + PR_FindFunctionSymbol(mLib, STRINGIFY(INITIALIZE_CDM_MODULE))); + if (!init) { + return GMPGenericErr; + } + + Log(STRINGIFY(INITIALIZE_CDM_MODULE)"()"); + init(); + + return GMPNoErr; +} + +GMPErr +WidevineAdapter::GMPGetAPI(const char* aAPIName, + void* aHostAPI, + void** aPluginAPI, + uint32_t aDecryptorId) +{ + Log("WidevineAdapter::GMPGetAPI(%s, 0x%p, 0x%p, %u) this=0x%p", + aAPIName, aHostAPI, aPluginAPI, this, aDecryptorId); + if (!strcmp(aAPIName, GMP_API_DECRYPTOR)) { + if (WidevineDecryptor::GetInstance(aDecryptorId)) { + // We only support one CDM instance per PGMPDecryptor. Fail! + Log("WidevineAdapter::GMPGetAPI() Tried to create more than once CDM per IPDL actor! FAIL!"); + return GMPQuotaExceededErr; + } + auto create = reinterpret_cast<decltype(::CreateCdmInstance)*>( + PR_FindFunctionSymbol(mLib, "CreateCdmInstance")); + if (!create) { + Log("WidevineAdapter::GMPGetAPI(%s, 0x%p, 0x%p, %u) this=0x%p FAILED to find CreateCdmInstance", + aAPIName, aHostAPI, aPluginAPI, this, aDecryptorId); + return GMPGenericErr; + } + + WidevineDecryptor* decryptor = new WidevineDecryptor(); + + auto cdm = reinterpret_cast<cdm::ContentDecryptionModule_9*>( + create(cdm::ContentDecryptionModule_9::kVersion, + kEMEKeySystemWidevine.get(), + kEMEKeySystemWidevine.Length(), + &GetCdmHost, + decryptor)); + if (!cdm) { + Log("WidevineAdapter::GMPGetAPI(%s, 0x%p, 0x%p, %u) this=0x%p FAILED to create cdm", + aAPIName, aHostAPI, aPluginAPI, this, aDecryptorId); + return GMPGenericErr; + } + Log("cdm: 0x%x", cdm); + RefPtr<CDMWrapper> wrapper(new CDMWrapper(cdm, decryptor)); + decryptor->SetCDM(wrapper, aDecryptorId); + *aPluginAPI = decryptor; + + } else if (!strcmp(aAPIName, GMP_API_VIDEO_DECODER)) { + RefPtr<CDMWrapper> wrapper = WidevineDecryptor::GetInstance(aDecryptorId); + if (!wrapper) { + Log("WidevineAdapter::GMPGetAPI(%s, 0x%p, 0x%p, %u) this=0x%p No cdm for video decoder", + aAPIName, aHostAPI, aPluginAPI, thiss, aDecryptorId); + return GMPGenericErr; + } + *aPluginAPI = new WidevineVideoDecoder(static_cast<GMPVideoHost*>(aHostAPI), + wrapper); + } + return *aPluginAPI ? GMPNoErr : GMPNotImplementedErr; +} + +void +WidevineAdapter::GMPShutdown() +{ + Log("WidevineAdapter::GMPShutdown()"); + + decltype(::DeinitializeCdmModule)* deinit; + deinit = (decltype(deinit))(PR_FindFunctionSymbol(mLib, "DeinitializeCdmModule")); + if (deinit) { + Log("DeinitializeCdmModule()"); + deinit(); + } +} + +void +WidevineAdapter::GMPSetNodeId(const char* aNodeId, uint32_t aLength) +{ + +} + +/* static */ +bool +WidevineAdapter::Supports(int32_t aModuleVersion, + int32_t aInterfaceVersion, + int32_t aHostVersion) +{ + return aModuleVersion == CDM_MODULE_VERSION && + aInterfaceVersion == cdm::ContentDecryptionModule_9::kVersion && + aHostVersion == cdm::Host_9::kVersion; +} + +} // namespace mozilla diff --git a/dom/media/gmp/widevine-adapter/WidevineAdapter.h b/dom/media/gmp/widevine-adapter/WidevineAdapter.h new file mode 100644 index 000000000..714e041be --- /dev/null +++ b/dom/media/gmp/widevine-adapter/WidevineAdapter.h @@ -0,0 +1,59 @@ +/* -*- 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/. */ + +#ifndef WidevineAdapter_h_ +#define WidevineAdapter_h_ + +#include "GMPLoader.h" +#include "prlink.h" +#include "GMPUtils.h" + +struct GMPPlatformAPI; + +namespace mozilla { + +class WidevineAdapter : public gmp::GMPAdapter { +public: + + void SetAdaptee(PRLibrary* aLib) override; + + // These are called in place of the corresponding GMP API functions. + GMPErr GMPInit(const GMPPlatformAPI* aPlatformAPI) override; + GMPErr GMPGetAPI(const char* aAPIName, + void* aHostAPI, + void** aPluginAPI, + uint32_t aDecryptorId) override; + void GMPShutdown() override; + void GMPSetNodeId(const char* aNodeId, uint32_t aLength) override; + + static bool Supports(int32_t aModuleVersion, + int32_t aInterfaceVersion, + int32_t aHostVersion); + +private: + PRLibrary* mLib = nullptr; +}; + +GMPErr GMPCreateThread(GMPThread** aThread); +GMPErr GMPRunOnMainThread(GMPTask* aTask); +GMPErr GMPCreateMutex(GMPMutex** aMutex); + +// Call on main thread only. +GMPErr GMPCreateRecord(const char* aRecordName, + uint32_t aRecordNameSize, + GMPRecord** aOutRecord, + GMPRecordClient* aClient); + +// Call on main thread only. +GMPErr GMPSetTimerOnMainThread(GMPTask* aTask, int64_t aTimeoutMS); + +GMPErr GMPGetCurrentTime(GMPTimestamp* aOutTime); + +GMPErr GMPCreateRecordIterator(RecvGMPRecordIteratorPtr aRecvIteratorFunc, + void* aUserArg); + +} // namespace mozilla + +#endif // WidevineAdapter_h_ diff --git a/dom/media/gmp/widevine-adapter/WidevineDecryptor.cpp b/dom/media/gmp/widevine-adapter/WidevineDecryptor.cpp new file mode 100644 index 000000000..4d3408804 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/WidevineDecryptor.cpp @@ -0,0 +1,554 @@ +/* -*- 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 "WidevineDecryptor.h" + +#include "WidevineAdapter.h" +#include "WidevineUtils.h" +#include "WidevineFileIO.h" +#include <mozilla/SizePrintfMacros.h> +#include <stdarg.h> +#include "base/time.h" + +using namespace cdm; +using namespace std; + +namespace mozilla { + +static map<uint32_t, RefPtr<CDMWrapper>> sDecryptors; + +/* static */ +RefPtr<CDMWrapper> +WidevineDecryptor::GetInstance(uint32_t aInstanceId) +{ + auto itr = sDecryptors.find(aInstanceId); + if (itr != sDecryptors.end()) { + return itr->second; + } + return nullptr; +} + + +WidevineDecryptor::WidevineDecryptor() + : mCallback(nullptr) +{ + Log("WidevineDecryptor created this=%p", this); + AddRef(); // Released in DecryptingComplete(). +} + +WidevineDecryptor::~WidevineDecryptor() +{ + Log("WidevineDecryptor destroyed this=%p", this); +} + +void +WidevineDecryptor::SetCDM(RefPtr<CDMWrapper> aCDM, uint32_t aInstanceId) +{ + mCDM = aCDM; + mInstanceId = aInstanceId; + sDecryptors[mInstanceId] = aCDM; +} + +void +WidevineDecryptor::Init(GMPDecryptorCallback* aCallback, + bool aDistinctiveIdentifierRequired, + bool aPersistentStateRequired) +{ + Log("WidevineDecryptor::Init() this=%p distinctiveId=%d persistentState=%d", + this, aDistinctiveIdentifierRequired, aPersistentStateRequired); + MOZ_ASSERT(aCallback); + mCallback = aCallback; + MOZ_ASSERT(mCDM); + mDistinctiveIdentifierRequired = aDistinctiveIdentifierRequired; + mPersistentStateRequired = aPersistentStateRequired; + if (CDM()) { + CDM()->Initialize(aDistinctiveIdentifierRequired, + aPersistentStateRequired); + } +} + +static SessionType +ToCDMSessionType(GMPSessionType aSessionType) +{ + switch (aSessionType) { + case kGMPTemporySession: return kTemporary; + case kGMPPersistentSession: return kPersistentLicense; + case kGMPSessionInvalid: return kTemporary; + // TODO: kPersistentKeyRelease + } + MOZ_ASSERT(false); // Not supposed to get here. + return kTemporary; +} + +void +WidevineDecryptor::CreateSession(uint32_t aCreateSessionToken, + uint32_t aPromiseId, + const char* aInitDataType, + uint32_t aInitDataTypeSize, + const uint8_t* aInitData, + uint32_t aInitDataSize, + GMPSessionType aSessionType) +{ + Log("Decryptor::CreateSession(token=%d, pid=%d)", aCreateSessionToken, aPromiseId); + InitDataType initDataType; + if (!strcmp(aInitDataType, "cenc")) { + initDataType = kCenc; + } else if (!strcmp(aInitDataType, "webm")) { + initDataType = kWebM; + } else if (!strcmp(aInitDataType, "keyids")) { + initDataType = kKeyIds; + } else { + // Invalid init data type + const char* errorMsg = "Invalid init data type when creating session."; + OnRejectPromise(aPromiseId, kExceptionNotSupportedError, 0, errorMsg, sizeof(errorMsg)); + return; + } + mPromiseIdToNewSessionTokens[aPromiseId] = aCreateSessionToken; + CDM()->CreateSessionAndGenerateRequest(aPromiseId, + ToCDMSessionType(aSessionType), + initDataType, + aInitData, aInitDataSize); +} + +void +WidevineDecryptor::LoadSession(uint32_t aPromiseId, + const char* aSessionId, + uint32_t aSessionIdLength) +{ + Log("Decryptor::LoadSession(pid=%d, %s)", aPromiseId, aSessionId); + // TODO: session type?? + CDM()->LoadSession(aPromiseId, kPersistentLicense, aSessionId, aSessionIdLength); +} + +void +WidevineDecryptor::UpdateSession(uint32_t aPromiseId, + const char* aSessionId, + uint32_t aSessionIdLength, + const uint8_t* aResponse, + uint32_t aResponseSize) +{ + Log("Decryptor::UpdateSession(pid=%d, session=%s)", aPromiseId, aSessionId); + CDM()->UpdateSession(aPromiseId, aSessionId, aSessionIdLength, aResponse, aResponseSize); +} + +void +WidevineDecryptor::CloseSession(uint32_t aPromiseId, + const char* aSessionId, + uint32_t aSessionIdLength) +{ + Log("Decryptor::CloseSession(pid=%d, session=%s)", aPromiseId, aSessionId); + CDM()->CloseSession(aPromiseId, aSessionId, aSessionIdLength); +} + +void +WidevineDecryptor::RemoveSession(uint32_t aPromiseId, + const char* aSessionId, + uint32_t aSessionIdLength) +{ + Log("Decryptor::RemoveSession(%s)", aSessionId); + CDM()->RemoveSession(aPromiseId, aSessionId, aSessionIdLength); +} + +void +WidevineDecryptor::SetServerCertificate(uint32_t aPromiseId, + const uint8_t* aServerCert, + uint32_t aServerCertSize) +{ + Log("Decryptor::SetServerCertificate()"); + CDM()->SetServerCertificate(aPromiseId, aServerCert, aServerCertSize); +} + +class WidevineDecryptedBlock : public cdm::DecryptedBlock { +public: + + WidevineDecryptedBlock() + : mBuffer(nullptr) + , mTimestamp(0) + { + } + + ~WidevineDecryptedBlock() { + if (mBuffer) { + mBuffer->Destroy(); + mBuffer = nullptr; + } + } + + void SetDecryptedBuffer(cdm::Buffer* aBuffer) override { + mBuffer = aBuffer; + } + + cdm::Buffer* DecryptedBuffer() override { + return mBuffer; + } + + void SetTimestamp(int64_t aTimestamp) override { + mTimestamp = aTimestamp; + } + + int64_t Timestamp() const override { + return mTimestamp; + } + +private: + cdm::Buffer* mBuffer; + int64_t mTimestamp; +}; + +void +WidevineDecryptor::Decrypt(GMPBuffer* aBuffer, + GMPEncryptedBufferMetadata* aMetadata) +{ + if (!mCallback) { + Log("WidevineDecryptor::Decrypt() this=%p FAIL; !mCallback", this); + return; + } + const GMPEncryptedBufferMetadata* crypto = aMetadata; + InputBuffer sample; + nsTArray<SubsampleEntry> subsamples; + InitInputBuffer(crypto, aBuffer->Id(), aBuffer->Data(), aBuffer->Size(), sample, subsamples); + WidevineDecryptedBlock decrypted; + Status rv = CDM()->Decrypt(sample, &decrypted); + Log("Decryptor::Decrypt(timestamp=%lld) rv=%d sz=%d", + sample.timestamp, rv, decrypted.DecryptedBuffer()->Size()); + if (rv == kSuccess) { + aBuffer->Resize(decrypted.DecryptedBuffer()->Size()); + memcpy(aBuffer->Data(), + decrypted.DecryptedBuffer()->Data(), + decrypted.DecryptedBuffer()->Size()); + } + mCallback->Decrypted(aBuffer, ToGMPErr(rv)); +} + +void +WidevineDecryptor::DecryptingComplete() +{ + Log("WidevineDecryptor::DecryptingComplete() this=%p", this); + // Drop our references to the CDMWrapper. When any other references + // held elsewhere are dropped (for example references held by a + // WidevineVideoDecoder, or a runnable), the CDMWrapper destroys + // the CDM. + mCDM = nullptr; + sDecryptors.erase(mInstanceId); + mCallback = nullptr; + Release(); +} + +class WidevineBuffer : public cdm::Buffer { +public: + explicit WidevineBuffer(size_t aSize) { + Log("WidevineBuffer(size=" PRIuSIZE ") created", aSize); + mBuffer.SetLength(aSize); + } + ~WidevineBuffer() { + Log("WidevineBuffer(size=" PRIuSIZE ") destroyed", Size()); + } + void Destroy() override { delete this; } + uint32_t Capacity() const override { return mBuffer.Length(); }; + uint8_t* Data() override { return mBuffer.Elements(); } + void SetSize(uint32_t aSize) override { mBuffer.SetLength(aSize); } + uint32_t Size() const override { return mBuffer.Length(); } + +private: + WidevineBuffer(const WidevineBuffer&); + void operator=(const WidevineBuffer&); + + nsTArray<uint8_t> mBuffer; +}; + +Buffer* +WidevineDecryptor::Allocate(uint32_t aCapacity) +{ + Log("Decryptor::Allocate(capacity=%u)", aCapacity); + return new WidevineBuffer(aCapacity); +} + +class TimerTask : public GMPTask { +public: + TimerTask(WidevineDecryptor* aDecryptor, + RefPtr<CDMWrapper> aCDM, + void* aContext) + : mDecryptor(aDecryptor) + , mCDM(aCDM) + , mContext(aContext) + { + } + ~TimerTask() override {} + void Run() override { + mCDM->GetCDM()->TimerExpired(mContext); + } + void Destroy() override { delete this; } +private: + RefPtr<WidevineDecryptor> mDecryptor; + RefPtr<CDMWrapper> mCDM; + void* mContext; +}; + +void +WidevineDecryptor::SetTimer(int64_t aDelayMs, void* aContext) +{ + Log("Decryptor::SetTimer(delay_ms=%lld, context=0x%x)", aDelayMs, aContext); + if (mCDM) { + GMPSetTimerOnMainThread(new TimerTask(this, mCDM, aContext), aDelayMs); + } +} + +Time +WidevineDecryptor::GetCurrentWallTime() +{ + return base::Time::Now().ToDoubleT(); +} + +void +WidevineDecryptor::OnResolveKeyStatusPromise(uint32_t aPromiseId, + cdm::KeyStatus aKeyStatus) { + //TODO: The callback of GetStatusForPolicy. See Mozilla bug 1404230. +} + +void +WidevineDecryptor::OnResolveNewSessionPromise(uint32_t aPromiseId, + const char* aSessionId, + uint32_t aSessionIdSize) +{ + if (!mCallback) { + Log("Decryptor::OnResolveNewSessionPromise(aPromiseId=0x%d) FAIL; !mCallback", aPromiseId); + return; + } + Log("Decryptor::OnResolveNewSessionPromise(aPromiseId=0x%d)", aPromiseId); + auto iter = mPromiseIdToNewSessionTokens.find(aPromiseId); + if (iter == mPromiseIdToNewSessionTokens.end()) { + Log("FAIL: Decryptor::OnResolveNewSessionPromise(aPromiseId=%d) unknown aPromiseId", aPromiseId); + return; + } + mCallback->SetSessionId(iter->second, aSessionId, aSessionIdSize); + mCallback->ResolvePromise(aPromiseId); + mPromiseIdToNewSessionTokens.erase(iter); +} + +void +WidevineDecryptor::OnResolvePromise(uint32_t aPromiseId) +{ + if (!mCallback) { + Log("Decryptor::OnResolvePromise(aPromiseId=0x%d) FAIL; !mCallback", aPromiseId); + return; + } + Log("Decryptor::OnResolvePromise(aPromiseId=%d)", aPromiseId); + mCallback->ResolvePromise(aPromiseId); +} + +static GMPDOMException +ConvertCDMExceptionToGMPDOMException(cdm::Exception aException) +{ + switch (aException) { + case kExceptionNotSupportedError: return kGMPNotSupportedError; + case kExceptionInvalidStateError: return kGMPInvalidStateError; + case kExceptionTypeError: return kGMPTypeError; + case kExceptionQuotaExceededError: return kGMPQuotaExceededError; + case kUnknownError: return kGMPInvalidModificationError; // Note: Unique placeholder. + case kClientError: return kGMPAbortError; // Note: Unique placeholder. + case kOutputError: return kGMPSecurityError; // Note: Unique placeholder. + }; + return kGMPInvalidStateError; // Note: Unique placeholder. +} + +// Align with spec, the Exceptions used by CDM to reject promises . +// https://w3c.github.io/encrypted-media/#exceptions +cdm::Exception +ConvertCDMErrorToCDMException(cdm::Error error) { + switch (error) { + case cdm::kNotSupportedError: + return cdm::Exception::kExceptionNotSupportedError; + case cdm::kInvalidStateError: + return cdm::Exception::kExceptionInvalidStateError; + case cdm::kInvalidAccessError: + return cdm::Exception::kExceptionTypeError; + case cdm::kQuotaExceededError: + return cdm::Exception::kExceptionQuotaExceededError; + + case cdm::kUnknownError: + case cdm::kClientError: + case cdm::kOutputError: + break; + } + + return cdm::Exception::kExceptionInvalidStateError; +} + +void +WidevineDecryptor::OnRejectPromise(uint32_t aPromiseId, + cdm::Exception aException, + uint32_t aSystemCode, + const char* aErrorMessage, + uint32_t aErrorMessageSize) +{ + if (!mCallback) { + Log("Decryptor::OnRejectPromise(aPromiseId=%d, err=%d, sysCode=%u, msg=%s) FAIL; !mCallback", + aPromiseId, (int)aException, aSystemCode, aErrorMessage); + return; + } + Log("Decryptor::OnRejectPromise(aPromiseId=%d, err=%d, sysCode=%u, msg=%s)", + aPromiseId, (int)aException, aSystemCode, aErrorMessage); + mCallback->RejectPromise(aPromiseId, + ConvertCDMExceptionToGMPDOMException(aException), + !aErrorMessageSize ? "" : aErrorMessage, + aErrorMessageSize); +} + +static GMPSessionMessageType +ToGMPMessageType(MessageType message_type) +{ + switch (message_type) { + case kLicenseRequest: return kGMPLicenseRequest; + case kLicenseRenewal: return kGMPLicenseRenewal; + case kLicenseRelease: return kGMPLicenseRelease; + } + return kGMPMessageInvalid; +} + +void +WidevineDecryptor::OnSessionMessage(const char* aSessionId, + uint32_t aSessionIdSize, + cdm::MessageType aMessageType, + const char* aMessage, + uint32_t aMessageSize) +{ + if (!mCallback) { + Log("Decryptor::OnSessionMessage() FAIL; !mCallback"); + return; + } + Log("Decryptor::OnSessionMessage()"); + mCallback->SessionMessage(aSessionId, + aSessionIdSize, + ToGMPMessageType(aMessageType), + reinterpret_cast<const uint8_t*>(aMessage), + aMessageSize); +} + +static GMPMediaKeyStatus +ToGMPKeyStatus(KeyStatus aStatus) +{ + switch (aStatus) { + case kUsable: return kGMPUsable; + case kInternalError: return kGMPInternalError; + case kExpired: return kGMPExpired; + case kOutputRestricted: return kGMPOutputRestricted; + case kOutputDownscaled: return kGMPOutputDownscaled; + case kStatusPending: return kGMPStatusPending; + case kReleased: return kGMPReleased; + } + return kGMPUnknown; +} + +void +WidevineDecryptor::OnSessionKeysChange(const char* aSessionId, + uint32_t aSessionIdSize, + bool aHasAdditionalUsableKey, + const KeyInformation* aKeysInfo, + uint32_t aKeysInfoCount) +{ + if (!mCallback) { + Log("Decryptor::OnSessionKeysChange() FAIL; !mCallback"); + return; + } + Log("Decryptor::OnSessionKeysChange()"); + + nsTArray<GMPMediaKeyInfo> key_infos; + for (uint32_t i = 0; i < aKeysInfoCount; i++) { + key_infos.AppendElement(GMPMediaKeyInfo(aKeysInfo[i].key_id, + aKeysInfo[i].key_id_size, + ToGMPKeyStatus(aKeysInfo[i].status))); + } + mCallback->BatchedKeyStatusChanged(aSessionId, aSessionIdSize, + key_infos.Elements(), key_infos.Length()); +} + +static GMPTimestamp +ToGMPTime(Time aCDMTime) +{ + return static_cast<GMPTimestamp>(aCDMTime * 1000); +} + +void +WidevineDecryptor::OnExpirationChange(const char* aSessionId, + uint32_t aSessionIdSize, + Time aNewExpiryTime) +{ + if (!mCallback) { + Log("Decryptor::OnExpirationChange(sid=%s) t=%lf FAIL; !mCallback", + aSessionId, aNewExpiryTime); + return; + } + Log("Decryptor::OnExpirationChange(sid=%s) t=%lf", aSessionId, aNewExpiryTime); + GMPTimestamp expiry = ToGMPTime(aNewExpiryTime); + if (aNewExpiryTime == 0) { + return; + } + mCallback->ExpirationChange(aSessionId, aSessionIdSize, expiry); +} + +void +WidevineDecryptor::OnSessionClosed(const char* aSessionId, + uint32_t aSessionIdSize) +{ + if (!mCallback) { + Log("Decryptor::OnSessionClosed(sid=%s) FAIL; !mCallback", aSessionId); + return; + } + Log("Decryptor::OnSessionClosed(sid=%s)", aSessionId); + mCallback->SessionClosed(aSessionId, aSessionIdSize); +} + +void +WidevineDecryptor::SendPlatformChallenge(const char* aServiceId, + uint32_t aServiceIdSize, + const char* aChallenge, + uint32_t aChallengeSize) +{ + Log("Decryptor::SendPlatformChallenge(service_id=%s)", aServiceId); +} + +void +WidevineDecryptor::EnableOutputProtection(uint32_t aDesiredProtectionMask) +{ + Log("Decryptor::EnableOutputProtection(mask=0x%x)", aDesiredProtectionMask); +} + +void +WidevineDecryptor::QueryOutputProtectionStatus() +{ + Log("Decryptor::QueryOutputProtectionStatus()"); +} + +void +WidevineDecryptor::OnDeferredInitializationDone(StreamType aStreamType, + Status aDecoderStatus) +{ + Log("Decryptor::OnDeferredInitializationDone()"); +} + +FileIO* +WidevineDecryptor::CreateFileIO(FileIOClient* aClient) +{ + Log("Decryptor::CreateFileIO()"); + if (!mPersistentStateRequired) { + return nullptr; + } + return new WidevineFileIO(aClient); +} + +void +WidevineDecryptor::RequestStorageId(uint32_t aVersion) +{ + Log("Decryptor::RequestStorageId() aVersion = %u", aVersion); + if (aVersion >= 0x80000000) { + mCDM->OnStorageId(aVersion, nullptr, 0); + return; + } + + //TODO: Need to provide a menaingful buffer instead of a dummy one. + mCDM->OnStorageId(aVersion, new uint8_t[1024*1024], 1024 * 1024); +} + +} // namespace mozilla diff --git a/dom/media/gmp/widevine-adapter/WidevineDecryptor.h b/dom/media/gmp/widevine-adapter/WidevineDecryptor.h new file mode 100644 index 000000000..f291c321d --- /dev/null +++ b/dom/media/gmp/widevine-adapter/WidevineDecryptor.h @@ -0,0 +1,132 @@ +/* -*- 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/. */ + +#ifndef WidevineDecryptor_h_ +#define WidevineDecryptor_h_ + +#include "stddef.h" +#include "content_decryption_module.h" +#include "gmp-api/gmp-decryption.h" +#include "mozilla/RefPtr.h" +#include "WidevineUtils.h" +#include <map> + +namespace mozilla { + +class WidevineDecryptor : public GMPDecryptor + , public cdm::Host_9 +{ +public: + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WidevineDecryptor) + + WidevineDecryptor(); + + void SetCDM(RefPtr<CDMWrapper> aCDM, uint32_t aDecryptorId); + + static RefPtr<CDMWrapper> GetInstance(uint32_t aDecryptorId); + + // GMPDecryptor + void Init(GMPDecryptorCallback* aCallback, + bool aDistinctiveIdentifierRequired, + bool aPersistentStateRequired) override; + + void CreateSession(uint32_t aCreateSessionToken, + uint32_t aPromiseId, + const char* aInitDataType, + uint32_t aInitDataTypeSize, + const uint8_t* aInitData, + uint32_t aInitDataSize, + GMPSessionType aSessionType) override; + + void LoadSession(uint32_t aPromiseId, + const char* aSessionId, + uint32_t aSessionIdLength) override; + + void UpdateSession(uint32_t aPromiseId, + const char* aSessionId, + uint32_t aSessionIdLength, + const uint8_t* aResponse, + uint32_t aResponseSize) override; + + void CloseSession(uint32_t aPromiseId, + const char* aSessionId, + uint32_t aSessionIdLength) override; + + void RemoveSession(uint32_t aPromiseId, + const char* aSessionId, + uint32_t aSessionIdLength) override; + + void SetServerCertificate(uint32_t aPromiseId, + const uint8_t* aServerCert, + uint32_t aServerCertSize) override; + + void Decrypt(GMPBuffer* aBuffer, + GMPEncryptedBufferMetadata* aMetadata) override; + + void DecryptingComplete() override; + + + // cdm::Host_9 implementation + cdm::Buffer* Allocate(uint32_t aCapacity) override; + void SetTimer(int64_t aDelayMs, void* aContext) override; + cdm::Time GetCurrentWallTime() override; + // cdm::Host_9 interface + void OnResolveKeyStatusPromise(uint32_t aPromiseId, + cdm::KeyStatus aKeyStatus) override; + void OnResolveNewSessionPromise(uint32_t aPromiseId, + const char* aSessionId, + uint32_t aSessionIdSize) override; + void OnResolvePromise(uint32_t aPromiseId) override; + void OnRejectPromise(uint32_t aPromiseId, + cdm::Exception aException, + uint32_t aSystemCode, + const char* aErrorMessage, + uint32_t aErrorMessageSize) override; + void OnSessionMessage(const char* aSessionId, + uint32_t aSessionIdSize, + cdm::MessageType aMessageType, + const char* aMessage, + uint32_t aMessageSize) override; + void OnSessionKeysChange(const char* aSessionId, + uint32_t aSessionIdSize, + bool aHasAdditionalUsableKey, + const cdm::KeyInformation* aKeysInfo, + uint32_t aKeysInfoCount) override; + void OnExpirationChange(const char* aSessionId, + uint32_t aSessionIdSize, + cdm::Time aNewExpiryTime) override; + void OnSessionClosed(const char* aSessionId, + uint32_t aSessionIdSize) override; + void SendPlatformChallenge(const char* aServiceId, + uint32_t aServiceIdSize, + const char* aChallenge, + uint32_t aChallengeSize) override; + void EnableOutputProtection(uint32_t aDesiredProtectionMask) override; + void QueryOutputProtectionStatus() override; + void OnDeferredInitializationDone(cdm::StreamType aStreamType, + cdm::Status aDecoderStatus) override; + // cdm::Host_9 interface + // NOTE: the interface has changed upstream. + void RequestStorageId(uint32_t aVersion) override; + cdm::FileIO* CreateFileIO(cdm::FileIOClient* aClient) override; + + GMPDecryptorCallback* Callback() const { return mCallback; } + RefPtr<CDMWrapper> GetCDMWrapper() const { return mCDM; } +private: + ~WidevineDecryptor(); + RefPtr<CDMWrapper> mCDM; + cdm::ContentDecryptionModule_9* CDM() { return mCDM->GetCDM(); } + + GMPDecryptorCallback* mCallback; + std::map<uint32_t, uint32_t> mPromiseIdToNewSessionTokens; + bool mDistinctiveIdentifierRequired = false; + bool mPersistentStateRequired = false; + uint32_t mInstanceId = 0; +}; + +} // namespace mozilla + +#endif // WidevineDecryptor_h_ diff --git a/dom/media/gmp/widevine-adapter/WidevineFileIO.cpp b/dom/media/gmp/widevine-adapter/WidevineFileIO.cpp new file mode 100644 index 000000000..b5fb1d705 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/WidevineFileIO.cpp @@ -0,0 +1,97 @@ +#include "WidevineFileIO.h" +#include "WidevineUtils.h" +#include "WidevineAdapter.h" + +using namespace cdm; + +namespace mozilla { + +void +WidevineFileIO::Open(const char* aFilename, uint32_t aFilenameLength) +{ + mName = std::string(aFilename, aFilename + aFilenameLength); + GMPRecord* record = nullptr; + GMPErr err = GMPCreateRecord(aFilename, aFilenameLength, &record, static_cast<GMPRecordClient*>(this)); + if (GMP_FAILED(err)) { + Log("WidevineFileIO::Open() '%s' GMPCreateRecord failed", mName.c_str()); + mClient->OnOpenComplete(FileIOClient::kError); + return; + } + if (GMP_FAILED(record->Open())) { + Log("WidevineFileIO::Open() '%s' record open failed", mName.c_str()); + mClient->OnOpenComplete(FileIOClient::kError); + return; + } + + Log("WidevineFileIO::Open() '%s'", mName.c_str()); + mRecord = record; +} + +void +WidevineFileIO::Read() +{ + if (!mRecord) { + Log("WidevineFileIO::Read() '%s' used uninitialized!", mName.c_str()); + mClient->OnReadComplete(FileIOClient::kError, nullptr, 0); + return; + } + Log("WidevineFileIO::Read() '%s'", mName.c_str()); + mRecord->Read(); +} + +void +WidevineFileIO::Write(const uint8_t* aData, uint32_t aDataSize) +{ + if (!mRecord) { + Log("WidevineFileIO::Write() '%s' used uninitialized!", mName.c_str()); + mClient->OnWriteComplete(FileIOClient::kError); + return; + } + mRecord->Write(aData, aDataSize); +} + +void +WidevineFileIO::Close() +{ + Log("WidevineFileIO::Close() '%s'", mName.c_str()); + if (mRecord) { + mRecord->Close(); + mRecord = nullptr; + } + delete this; +} + +static FileIOClient::Status +GMPToWidevineFileStatus(GMPErr aStatus) +{ + switch (aStatus) { + case GMPRecordInUse: return FileIOClient::kInUse; + case GMPNoErr: return FileIOClient::kSuccess; + default: return FileIOClient::kError; + } +} + +void +WidevineFileIO::OpenComplete(GMPErr aStatus) +{ + Log("WidevineFileIO::OpenComplete() '%s' status=%d", mName.c_str(), aStatus); + mClient->OnOpenComplete(GMPToWidevineFileStatus(aStatus)); +} + +void +WidevineFileIO::ReadComplete(GMPErr aStatus, + const uint8_t* aData, + uint32_t aDataSize) +{ + Log("WidevineFileIO::OnReadComplete() '%s' status=%d", mName.c_str(), aStatus); + mClient->OnReadComplete(GMPToWidevineFileStatus(aStatus), aData, aDataSize); +} + +void +WidevineFileIO::WriteComplete(GMPErr aStatus) +{ + Log("WidevineFileIO::WriteComplete() '%s' status=%d", mName.c_str(), aStatus); + mClient->OnWriteComplete(GMPToWidevineFileStatus(aStatus)); +} + +} // namespace mozilla diff --git a/dom/media/gmp/widevine-adapter/WidevineFileIO.h b/dom/media/gmp/widevine-adapter/WidevineFileIO.h new file mode 100644 index 000000000..63003d9b6 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/WidevineFileIO.h @@ -0,0 +1,46 @@ +/* -*- 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/. */ + +#ifndef WidevineFileIO_h_ +#define WidevineFileIO_h_ + +#include <stddef.h> +#include "content_decryption_module.h" +#include "gmp-api/gmp-storage.h" +#include <string> + +namespace mozilla { + +class WidevineFileIO : public cdm::FileIO + , public GMPRecordClient +{ +public: + explicit WidevineFileIO(cdm::FileIOClient* aClient) + : mClient(aClient) + , mRecord(nullptr) + {} + + // cdm::FileIO + void Open(const char* aFilename, uint32_t aFilenameLength) override; + void Read() override; + void Write(const uint8_t* aData, uint32_t aDataSize) override; + void Close() override; + + // GMPRecordClient + void OpenComplete(GMPErr aStatus) override; + void ReadComplete(GMPErr aStatus, + const uint8_t* aData, + uint32_t aDataSize) override; + void WriteComplete(GMPErr aStatus) override; + +private: + cdm::FileIOClient* mClient; + GMPRecord* mRecord; + std::string mName; +}; + +} // namespace mozilla + +#endif // WidevineFileIO_h_
\ No newline at end of file diff --git a/dom/media/gmp/widevine-adapter/WidevineUtils.cpp b/dom/media/gmp/widevine-adapter/WidevineUtils.cpp new file mode 100644 index 000000000..10c6c2e18 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/WidevineUtils.cpp @@ -0,0 +1,95 @@ +/* -*- 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 "WidevineUtils.h" +#include "WidevineDecryptor.h" + +#include "gmp-api/gmp-errors.h" +#include <stdarg.h> +#include <stdio.h> + +namespace mozilla { + +#ifdef ENABLE_WIDEVINE_LOG +void +Log(const char* aFormat, ...) +{ + va_list ap; + va_start(ap, aFormat); + const size_t len = 1024; + char buf[len]; + vsnprintf(buf, len, aFormat, ap); + va_end(ap); + if (getenv("GMP_LOG_FILE")) { + FILE* f = fopen(getenv("GMP_LOG_FILE"), "a"); + if (f) { + fprintf(f, "%s\n", buf); + fflush(f); + fclose(f); + f = nullptr; + } + } else { + printf("LOG: %s\n", buf); + } +} +#endif // ENABLE_WIDEVINE_LOG + +GMPErr +ToGMPErr(cdm::Status aStatus) +{ + switch (aStatus) { + case cdm::kSuccess: return GMPNoErr; + case cdm::kNeedMoreData: return GMPGenericErr; + case cdm::kNoKey: return GMPNoKeyErr; + case cdm::kInitializationError: return GMPGenericErr; + case cdm::kDecryptError: return GMPCryptoErr; + case cdm::kDecodeError: return GMPDecodeErr; + case cdm::kDeferredInitialization: return GMPGenericErr; + default: return GMPGenericErr; + } +} + +void InitInputBuffer(const GMPEncryptedBufferMetadata* aCrypto, + int64_t aTimestamp, + const uint8_t* aData, + size_t aDataSize, + cdm::InputBuffer &aInputBuffer, + nsTArray<cdm::SubsampleEntry> &aSubsamples) +{ + if (aCrypto) { + aInputBuffer.key_id = aCrypto->KeyId(); + aInputBuffer.key_id_size = aCrypto->KeyIdSize(); + aInputBuffer.iv = aCrypto->IV(); + aInputBuffer.iv_size = aCrypto->IVSize(); + aInputBuffer.num_subsamples = aCrypto->NumSubsamples(); + aSubsamples.SetCapacity(aInputBuffer.num_subsamples); + const uint16_t* clear = aCrypto->ClearBytes(); + const uint32_t* cipher = aCrypto->CipherBytes(); + for (size_t i = 0; i < aCrypto->NumSubsamples(); i++) { + aSubsamples.AppendElement(cdm::SubsampleEntry(clear[i], cipher[i])); + } + } + aInputBuffer.data = aData; + aInputBuffer.data_size = aDataSize; + aInputBuffer.subsamples = aSubsamples.Elements(); + aInputBuffer.timestamp = aTimestamp; +} + +CDMWrapper::CDMWrapper(cdm::ContentDecryptionModule_9* aCDM, + WidevineDecryptor* aDecryptor) + : mCDM(aCDM) + , mDecryptor(aDecryptor) +{ + MOZ_ASSERT(mCDM); +} + +CDMWrapper::~CDMWrapper() +{ + Log("CDMWrapper destroying CDM=%p", mCDM); + mCDM->Destroy(); + mCDM = nullptr; +} + +} // namespace mozilla diff --git a/dom/media/gmp/widevine-adapter/WidevineUtils.h b/dom/media/gmp/widevine-adapter/WidevineUtils.h new file mode 100644 index 000000000..2f6137fe3 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/WidevineUtils.h @@ -0,0 +1,73 @@ +/* -*- 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/. */ + +#ifndef WidevineUtils_h_ +#define WidevineUtils_h_ + +#include "stddef.h" +#include "content_decryption_module.h" +#include "gmp-api/gmp-decryption.h" +#include "gmp-api/gmp-platform.h" +#include "nsISupportsImpl.h" +#include "nsTArray.h" + +namespace mozilla { + +// Uncomment for logging... +//#define ENABLE_WIDEVINE_LOG 1 +#ifdef ENABLE_WIDEVINE_LOG +void +Log(const char* aFormat, ...); +#else +#define Log(...) +#endif // ENABLE_WIDEVINE_LOG + + +#define ENSURE_TRUE(condition, rv) { \ + if (!(condition)) {\ + Log("ENSURE_TRUE FAILED %s:%d", __FILE__, __LINE__); \ + return rv; \ + } \ +} \ + +#define ENSURE_GMP_SUCCESS(err, rv) { \ + if (GMP_FAILED(err)) {\ + Log("ENSURE_GMP_SUCCESS FAILED %s:%d", __FILE__, __LINE__); \ + return rv; \ + } \ +} \ + +GMPErr +ToGMPErr(cdm::Status aStatus); + +class WidevineDecryptor; + +class CDMWrapper { +public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CDMWrapper) + + explicit CDMWrapper(cdm::ContentDecryptionModule_9* aCDM, + WidevineDecryptor* aDecryptor); + cdm::ContentDecryptionModule_9* GetCDM() const { return mCDM; } + void OnStorageId(uint32_t aVersion, const uint8_t* aStorageId, + uint32_t aStorageIdSize) { + mCDM->OnStorageId(aVersion, aStorageId, aStorageIdSize); + } +private: + ~CDMWrapper(); + cdm::ContentDecryptionModule_9* mCDM; + RefPtr<WidevineDecryptor> mDecryptor; +}; + +void InitInputBuffer(const GMPEncryptedBufferMetadata* aCrypto, + int64_t aTimestamp, + const uint8_t* aData, + size_t aDataSize, + cdm::InputBuffer &aInputBuffer, + nsTArray<cdm::SubsampleEntry> &aSubsamples); + +} // namespace mozilla + +#endif // WidevineUtils_h_ diff --git a/dom/media/gmp/widevine-adapter/WidevineVideoDecoder.cpp b/dom/media/gmp/widevine-adapter/WidevineVideoDecoder.cpp new file mode 100644 index 000000000..70d2fd8e0 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/WidevineVideoDecoder.cpp @@ -0,0 +1,400 @@ +/* -*- 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 "WidevineVideoDecoder.h" + +#include "mp4_demuxer/AnnexB.h" +#include "WidevineUtils.h" +#include "WidevineVideoFrame.h" +#include "mozilla/Move.h" + +using namespace cdm; + +namespace mozilla { + +WidevineVideoDecoder::WidevineVideoDecoder(GMPVideoHost* aVideoHost, + RefPtr<CDMWrapper> aCDMWrapper) + : mVideoHost(aVideoHost) + , mCDMWrapper(Move(aCDMWrapper)) + , mExtraData(new MediaByteBuffer()) + , mSentInput(false) + , mCodecType(kGMPVideoCodecInvalid) + , mReturnOutputCallDepth(0) + , mDrainPending(false) + , mResetInProgress(false) +{ + // Expect to start with a CDM wrapper, will release it in DecodingComplete(). + MOZ_ASSERT(mCDMWrapper); + Log("WidevineVideoDecoder created this=%p", this); + + // Corresponding Release is in DecodingComplete(). + AddRef(); +} + +WidevineVideoDecoder::~WidevineVideoDecoder() +{ + Log("WidevineVideoDecoder destroyed this=%p", this); +} + +static +VideoDecoderConfig::VideoCodecProfile +ToCDMH264Profile(uint8_t aProfile) +{ + switch (aProfile) { + case 66: return VideoDecoderConfig::kH264ProfileBaseline; + case 77: return VideoDecoderConfig::kH264ProfileMain; + case 88: return VideoDecoderConfig::kH264ProfileExtended; + case 100: return VideoDecoderConfig::kH264ProfileHigh; + case 110: return VideoDecoderConfig::kH264ProfileHigh10; + case 122: return VideoDecoderConfig::kH264ProfileHigh422; + case 144: return VideoDecoderConfig::kH264ProfileHigh444Predictive; + } + return VideoDecoderConfig::kUnknownVideoCodecProfile; +} + +void +WidevineVideoDecoder::InitDecode(const GMPVideoCodec& aCodecSettings, + const uint8_t* aCodecSpecific, + uint32_t aCodecSpecificLength, + GMPVideoDecoderCallback* aCallback, + int32_t aCoreCount) +{ + mCallback = aCallback; + VideoDecoderConfig config; + mCodecType = aCodecSettings.mCodecType; + if (mCodecType == kGMPVideoCodecH264) { + config.codec = VideoDecoderConfig::kCodecH264; + const GMPVideoCodecH264* h264 = (const GMPVideoCodecH264*)(aCodecSpecific); + config.profile = ToCDMH264Profile(h264->mAVCC.mProfile); + } else if (mCodecType == kGMPVideoCodecVP8) { + config.codec = VideoDecoderConfig::kCodecVp8; + config.profile = VideoDecoderConfig::kProfileNotNeeded; + } else if (mCodecType == kGMPVideoCodecVP9) { + config.codec = VideoDecoderConfig::kCodecVp9; + config.profile = VideoDecoderConfig::kProfileNotNeeded; + } else { + mCallback->Error(GMPInvalidArgErr); + return; + } + config.format = kYv12; + config.coded_size = Size(aCodecSettings.mWidth, aCodecSettings.mHeight); + mExtraData->AppendElements(aCodecSpecific + 1, aCodecSpecificLength); + config.extra_data = mExtraData->Elements(); + config.extra_data_size = mExtraData->Length(); + Status rv = CDM()->InitializeVideoDecoder(config); + if (rv != kSuccess) { + mCallback->Error(ToGMPErr(rv)); + return; + } + Log("WidevineVideoDecoder::InitDecode() rv=%d", rv); + mAnnexB = mp4_demuxer::AnnexB::ConvertExtraDataToAnnexB(mExtraData); +} + +void +WidevineVideoDecoder::Decode(GMPVideoEncodedFrame* aInputFrame, + bool aMissingFrames, + const uint8_t* aCodecSpecificInfo, + uint32_t aCodecSpecificInfoLength, + int64_t aRenderTimeMs) +{ + // We should not be given new input if a drain has been initiated + MOZ_ASSERT(!mDrainPending); + // We may not get the same out of the CDM decoder as we put in, and there + // may be some latency, i.e. we may need to input (say) 30 frames before + // we receive output. So we need to store the durations of the frames input, + // and retrieve them on output. + mFrameDurations[aInputFrame->TimeStamp()] = aInputFrame->Duration(); + + mSentInput = true; + InputBuffer sample; + + RefPtr<MediaRawData> raw( + new MediaRawData(aInputFrame->Buffer(), aInputFrame->Size())); + if (aInputFrame->Size() && !raw->Data()) { + // OOM. + mCallback->Error(GMPAllocErr); + return; + } + raw->mExtraData = mExtraData; + raw->mKeyframe = (aInputFrame->FrameType() == kGMPKeyFrame); + if (mCodecType == kGMPVideoCodecH264) { + // Convert input from AVCC, which GMPAPI passes in, to AnnexB, which + // Chromium uses internally. + mp4_demuxer::AnnexB::ConvertSampleToAnnexB(raw); + } + + const GMPEncryptedBufferMetadata* crypto = aInputFrame->GetDecryptionData(); + nsTArray<SubsampleEntry> subsamples; + InitInputBuffer(crypto, aInputFrame->TimeStamp(), raw->Data(), raw->Size(), sample, subsamples); + + // For keyframes, ConvertSampleToAnnexB will stick the AnnexB extra data + // at the start of the input. So we need to account for that as clear data + // in the subsamples. + if (raw->mKeyframe && !subsamples.IsEmpty() && mCodecType == kGMPVideoCodecH264) { + subsamples[0].clear_bytes += mAnnexB->Length(); + } + + WidevineVideoFrame frame; + Status rv = CDM()->DecryptAndDecodeFrame(sample, &frame); + Log("WidevineVideoDecoder::Decode(timestamp=%lld) rv=%d", sample.timestamp, rv); + + // Destroy frame, so that the shmem is now free to be used to return + // output to the Gecko process. + aInputFrame->Destroy(); + aInputFrame = nullptr; + + if (rv == kSuccess) { + if (!ReturnOutput(frame)) { + Log("WidevineVideoDecoder::Decode() Failed in ReturnOutput()"); + mCallback->Error(GMPDecodeErr); + return; + } + // A reset should only be started at most at level mReturnOutputCallDepth 1, + // and if it's started it should be finished by that call by the time + // the it returns, so it should always be false by this point. + MOZ_ASSERT(!mResetInProgress); + // Only request more data if we don't have pending samples. + if (mFrameAllocationQueue.empty()) { + MOZ_ASSERT(mCDMWrapper); + mCallback->InputDataExhausted(); + } + } else if (rv == kNeedMoreData) { + MOZ_ASSERT(mCDMWrapper); + mCallback->InputDataExhausted(); + } else { + mCallback->Error(ToGMPErr(rv)); + } + // Finish a drain if pending and we have no pending ReturnOutput calls on the stack. + if (mDrainPending && mReturnOutputCallDepth == 0) { + Drain(); + } +} + +// Util class to assist with counting mReturnOutputCallDepth. +class CounterHelper { +public: + // RAII, increment counter + explicit CounterHelper(int32_t& counter) + : mCounter(counter) + { + mCounter++; + } + + // RAII, decrement counter + ~CounterHelper() + { + mCounter--; + } + +private: + int32_t& mCounter; +}; + +// Util class to make sure GMP frames are freed. Holds a GMPVideoi420Frame* +// and will destroy it when the helper is destroyed unless the held frame +// if forgotten with ForgetFrame. +class FrameDestroyerHelper { +public: + explicit FrameDestroyerHelper(GMPVideoi420Frame*& frame) + : frame(frame) + { + } + + // RAII, destroy frame if held. + ~FrameDestroyerHelper() + { + if (frame) { + frame->Destroy(); + } + frame = nullptr; + } + + // Forget the frame without destroying it. + void ForgetFrame() + { + frame = nullptr; + } + +private: + GMPVideoi420Frame* frame; +}; + + +// Special handing is needed around ReturnOutput as it spins the IPC message +// queue when creating an empty frame and can end up with reentrant calls into +// the class methods. +bool +WidevineVideoDecoder::ReturnOutput(WidevineVideoFrame& aCDMFrame) +{ + MOZ_ASSERT(mReturnOutputCallDepth >= 0); + CounterHelper counterHelper(mReturnOutputCallDepth); + mFrameAllocationQueue.push_back(Move(aCDMFrame)); + if (mReturnOutputCallDepth > 1) { + // In a reentrant call. + return true; + } + while (!mFrameAllocationQueue.empty()) { + MOZ_ASSERT(mReturnOutputCallDepth == 1); + // If we're at call level 1 a reset should not have been started. A + // reset may be received during CreateEmptyFrame below, but we should not + // be in a reset at this stage -- this would indicate receiving decode + // messages before completing our reset, which we should not. + MOZ_ASSERT(!mResetInProgress); + WidevineVideoFrame currentCDMFrame = Move(mFrameAllocationQueue.front()); + mFrameAllocationQueue.pop_front(); + GMPVideoFrame* f = nullptr; + auto err = mVideoHost->CreateFrame(kGMPI420VideoFrame, &f); + if (GMP_FAILED(err) || !f) { + Log("Failed to create i420 frame!\n"); + return false; + } + auto gmpFrame = static_cast<GMPVideoi420Frame*>(f); + FrameDestroyerHelper frameDestroyerHelper(gmpFrame); + Size size = currentCDMFrame.Size(); + const int32_t yStride = currentCDMFrame.Stride(VideoFrame::kYPlane); + const int32_t uStride = currentCDMFrame.Stride(VideoFrame::kUPlane); + const int32_t vStride = currentCDMFrame.Stride(VideoFrame::kVPlane); + const int32_t halfHeight = size.height / 2; + // This call can cause a shmem alloc, during this alloc other calls + // may be made to this class and placed on the stack. ***WARNING***: + // other IPC calls can happen during this call, resulting in calls + // being made to the CDM. After this call state can have changed, + // and should be reevaluated. + err = gmpFrame->CreateEmptyFrame(size.width, + size.height, + yStride, + uStride, + vStride); + // Assert possible reentrant calls or resets haven't altered level unexpectedly. + MOZ_ASSERT(mReturnOutputCallDepth == 1); + ENSURE_GMP_SUCCESS(err, false); + + // If a reset started we need to dump the current frame and complete the reset. + if (mResetInProgress) { + MOZ_ASSERT(mCDMWrapper); + MOZ_ASSERT(mFrameAllocationQueue.empty()); + CompleteReset(); + return true; + } + + err = gmpFrame->SetWidth(size.width); + ENSURE_GMP_SUCCESS(err, false); + + err = gmpFrame->SetHeight(size.height); + ENSURE_GMP_SUCCESS(err, false); + + Buffer* buffer = currentCDMFrame.FrameBuffer(); + uint8_t* outBuffer = gmpFrame->Buffer(kGMPYPlane); + ENSURE_TRUE(outBuffer != nullptr, false); + MOZ_ASSERT(gmpFrame->AllocatedSize(kGMPYPlane) >= yStride*size.height); + memcpy(outBuffer, + buffer->Data() + currentCDMFrame.PlaneOffset(VideoFrame::kYPlane), + yStride * size.height); + + outBuffer = gmpFrame->Buffer(kGMPUPlane); + ENSURE_TRUE(outBuffer != nullptr, false); + MOZ_ASSERT(gmpFrame->AllocatedSize(kGMPUPlane) >= uStride * halfHeight); + memcpy(outBuffer, + buffer->Data() + currentCDMFrame.PlaneOffset(VideoFrame::kUPlane), + uStride * halfHeight); + + outBuffer = gmpFrame->Buffer(kGMPVPlane); + ENSURE_TRUE(outBuffer != nullptr, false); + MOZ_ASSERT(gmpFrame->AllocatedSize(kGMPVPlane) >= vStride * halfHeight); + memcpy(outBuffer, + buffer->Data() + currentCDMFrame.PlaneOffset(VideoFrame::kVPlane), + vStride * halfHeight); + + gmpFrame->SetTimestamp(currentCDMFrame.Timestamp()); + + auto d = mFrameDurations.find(currentCDMFrame.Timestamp()); + if (d != mFrameDurations.end()) { + gmpFrame->SetDuration(d->second); + mFrameDurations.erase(d); + } + + // Forget frame so it's not deleted, call back taking ownership. + frameDestroyerHelper.ForgetFrame(); + mCallback->Decoded(gmpFrame); + } + + return true; +} + +void +WidevineVideoDecoder::Reset() +{ + Log("WidevineVideoDecoder::Reset() mSentInput=%d", mSentInput); + // We shouldn't reset if a drain is pending. + MOZ_ASSERT(!mDrainPending); + mResetInProgress = true; + if (mSentInput) { + CDM()->ResetDecoder(kStreamTypeVideo); + } + // Remove queued frames, but do not reset mReturnOutputCallDepth, let the + // ReturnOutput calls unwind and decrement the counter as needed. + mFrameAllocationQueue.clear(); + mFrameDurations.clear(); + // Only if no ReturnOutput calls are in progress can we complete, otherwise + // ReturnOutput needs to finalize the reset. + if (mReturnOutputCallDepth == 0) { + CompleteReset(); + } +} + +void +WidevineVideoDecoder::CompleteReset() +{ + mCallback->ResetComplete(); + mSentInput = false; + mResetInProgress = false; +} + +void +WidevineVideoDecoder::Drain() +{ + Log("WidevineVideoDecoder::Drain()"); + if (mReturnOutputCallDepth > 0) { + Log("Drain call is reentrant, postponing drain"); + mDrainPending = true; + return; + } + + Status rv = kSuccess; + while (rv == kSuccess) { + WidevineVideoFrame frame; + InputBuffer sample; + Status rv = CDM()->DecryptAndDecodeFrame(sample, &frame); + Log("WidevineVideoDecoder::Drain(); DecryptAndDecodeFrame() rv=%d", rv); + if (frame.Format() == kUnknownVideoFormat) { + break; + } + if (rv == kSuccess) { + if (!ReturnOutput(frame)) { + Log("WidevineVideoDecoder::Decode() Failed in ReturnOutput()"); + } + } + } + // Shouldn't be reset while draining. + MOZ_ASSERT(!mResetInProgress); + + CDM()->ResetDecoder(kStreamTypeVideo); + mDrainPending = false; + mCallback->DrainComplete(); +} + +void +WidevineVideoDecoder::DecodingComplete() +{ + Log("WidevineVideoDecoder::DecodingComplete()"); + if (mCDMWrapper) { + CDM()->DeinitializeDecoder(kStreamTypeVideo); + mCDMWrapper = nullptr; + } + // Release that corresponds to AddRef() in constructor. + Release(); +} + +} // namespace mozilla diff --git a/dom/media/gmp/widevine-adapter/WidevineVideoDecoder.h b/dom/media/gmp/widevine-adapter/WidevineVideoDecoder.h new file mode 100644 index 000000000..f5e63519b --- /dev/null +++ b/dom/media/gmp/widevine-adapter/WidevineVideoDecoder.h @@ -0,0 +1,80 @@ +/* -*- 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/. */ + +#ifndef WidevineVideoDecoder_h_ +#define WidevineVideoDecoder_h_ + +#include "stddef.h" +#include "content_decryption_module.h" +#include "gmp-api/gmp-video-decode.h" +#include "gmp-api/gmp-video-host.h" +#include "MediaData.h" +#include "nsISupportsImpl.h" +#include "nsTArray.h" +#include "WidevineDecryptor.h" +#include "WidevineVideoFrame.h" +#include <map> +#include <deque> + +namespace mozilla { + +class WidevineVideoDecoder : public GMPVideoDecoder { +public: + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WidevineVideoDecoder) + + WidevineVideoDecoder(GMPVideoHost* aVideoHost, + RefPtr<CDMWrapper> aCDMWrapper); + void InitDecode(const GMPVideoCodec& aCodecSettings, + const uint8_t* aCodecSpecific, + uint32_t aCodecSpecificLength, + GMPVideoDecoderCallback* aCallback, + int32_t aCoreCount) override; + void Decode(GMPVideoEncodedFrame* aInputFrame, + bool aMissingFrames, + const uint8_t* aCodecSpecificInfo, + uint32_t aCodecSpecificInfoLength, + int64_t aRenderTimeMs = -1) override; + void Reset() override; + void Drain() override; + void DecodingComplete() override; + +private: + + ~WidevineVideoDecoder(); + + cdm::ContentDecryptionModule_9* CDM() const { + // CDM should only be accessed before 'DecodingComplete'. + MOZ_ASSERT(mCDMWrapper); + // CDMWrapper ensure the CDM is non-null, no need to check again. + return mCDMWrapper->GetCDM(); + } + + bool ReturnOutput(WidevineVideoFrame& aFrame); + void CompleteReset(); + + GMPVideoHost* mVideoHost; + RefPtr<CDMWrapper> mCDMWrapper; + RefPtr<MediaByteBuffer> mExtraData; + RefPtr<MediaByteBuffer> mAnnexB; + GMPVideoDecoderCallback* mCallback; + std::map<uint64_t, uint64_t> mFrameDurations; + bool mSentInput; + GMPVideoCodecType mCodecType; + // Frames waiting on allocation + std::deque<WidevineVideoFrame> mFrameAllocationQueue; + // Number of calls of ReturnOutput currently in progress. + int32_t mReturnOutputCallDepth; + // If we're waiting to drain. Used to prevent drain completing while + // ReturnOutput calls are still on the stack. + bool mDrainPending; + // If a reset is being performed. Used to track if ReturnOutput should + // dump current frame. + bool mResetInProgress; +}; + +} // namespace mozilla + +#endif // WidevineVideoDecoder_h_ diff --git a/dom/media/gmp/widevine-adapter/WidevineVideoFrame.cpp b/dom/media/gmp/widevine-adapter/WidevineVideoFrame.cpp new file mode 100644 index 000000000..4221bf15b --- /dev/null +++ b/dom/media/gmp/widevine-adapter/WidevineVideoFrame.cpp @@ -0,0 +1,126 @@ +/* -*- 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 "WidevineVideoFrame.h" + +#include "WidevineUtils.h" + +using namespace cdm; + +namespace mozilla { + +WidevineVideoFrame::WidevineVideoFrame() + : mFormat(kUnknownVideoFormat) + , mSize(0,0) + , mBuffer(nullptr) + , mTimestamp(0) +{ + Log("WidevineVideoFrame::WidevineVideoFrame() this=%p", this); + memset(mPlaneOffsets, 0, sizeof(mPlaneOffsets)); + memset(mPlaneStrides, 0, sizeof(mPlaneStrides)); +} + +WidevineVideoFrame::WidevineVideoFrame(WidevineVideoFrame&& aOther) + : mFormat(aOther.mFormat) + , mSize(aOther.mSize) + , mBuffer(aOther.mBuffer) + , mTimestamp(aOther.mTimestamp) +{ + Log("WidevineVideoFrame::WidevineVideoFrame(WidevineVideoFrame&&) this=%p, other=%p", + this, &aOther); + memcpy(mPlaneOffsets, aOther.mPlaneOffsets, sizeof(mPlaneOffsets)); + memcpy(mPlaneStrides, aOther.mPlaneStrides, sizeof(mPlaneStrides)); + aOther.mBuffer = nullptr; +} + +WidevineVideoFrame::~WidevineVideoFrame() +{ + if (mBuffer) { + mBuffer->Destroy(); + mBuffer = nullptr; + } +} + +void +WidevineVideoFrame::SetFormat(cdm::VideoFormat aFormat) +{ + Log("WidevineVideoFrame::SetFormat(%d) this=%p", aFormat, this); + mFormat = aFormat; +} + +cdm::VideoFormat +WidevineVideoFrame::Format() const +{ + return mFormat; +} + +void +WidevineVideoFrame::SetSize(cdm::Size aSize) +{ + Log("WidevineVideoFrame::SetSize(%d,%d) this=%p", aSize.width, aSize.height, this); + mSize.width = aSize.width; + mSize.height = aSize.height; +} + +cdm::Size +WidevineVideoFrame::Size() const +{ + return mSize; +} + +void +WidevineVideoFrame::SetFrameBuffer(cdm::Buffer* aFrameBuffer) +{ + Log("WidevineVideoFrame::SetFrameBuffer(%p) this=%p", aFrameBuffer, this); + MOZ_ASSERT(!mBuffer); + mBuffer = aFrameBuffer; +} + +cdm::Buffer* +WidevineVideoFrame::FrameBuffer() +{ + return mBuffer; +} + +void +WidevineVideoFrame::SetPlaneOffset(cdm::VideoFrame::VideoPlane aPlane, uint32_t aOffset) +{ + Log("WidevineVideoFrame::SetPlaneOffset(%d, %d) this=%p", aPlane, aOffset, this); + mPlaneOffsets[aPlane] = aOffset; +} + +uint32_t +WidevineVideoFrame::PlaneOffset(cdm::VideoFrame::VideoPlane aPlane) +{ + return mPlaneOffsets[aPlane]; +} + +void +WidevineVideoFrame::SetStride(cdm::VideoFrame::VideoPlane aPlane, uint32_t aStride) +{ + Log("WidevineVideoFrame::SetStride(%d, %d) this=%p", aPlane, aStride, this); + mPlaneStrides[aPlane] = aStride; +} + +uint32_t +WidevineVideoFrame::Stride(cdm::VideoFrame::VideoPlane aPlane) +{ + return mPlaneStrides[aPlane]; +} + +void +WidevineVideoFrame::SetTimestamp(int64_t timestamp) +{ + Log("WidevineVideoFrame::SetTimestamp(%lld) this=%p", timestamp, this); + mTimestamp = timestamp; +} + +int64_t +WidevineVideoFrame::Timestamp() const +{ + return mTimestamp; +} + +} // namespace mozilla diff --git a/dom/media/gmp/widevine-adapter/WidevineVideoFrame.h b/dom/media/gmp/widevine-adapter/WidevineVideoFrame.h new file mode 100644 index 000000000..96d4f20f8 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/WidevineVideoFrame.h @@ -0,0 +1,50 @@ +/* -*- 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/. */ + +#ifndef WidevineVideoFrame_h_ +#define WidevineVideoFrame_h_ + +#include "stddef.h" +#include "content_decryption_module.h" +#include <vector> + +namespace mozilla { + +class WidevineVideoFrame : public cdm::VideoFrame { +public: + WidevineVideoFrame(); + WidevineVideoFrame(WidevineVideoFrame&& other); + ~WidevineVideoFrame(); + + void SetFormat(cdm::VideoFormat aFormat) override; + cdm::VideoFormat Format() const override; + + void SetSize(cdm::Size aSize) override; + cdm::Size Size() const override; + + void SetFrameBuffer(cdm::Buffer* aFrameBuffer) override; + cdm::Buffer* FrameBuffer() override; + + void SetPlaneOffset(cdm::VideoFrame::VideoPlane aPlane, uint32_t aOffset) override; + uint32_t PlaneOffset(cdm::VideoFrame::VideoPlane aPlane) override; + + void SetStride(cdm::VideoFrame::VideoPlane aPlane, uint32_t aStride) override; + uint32_t Stride(cdm::VideoFrame::VideoPlane aPlane) override; + + void SetTimestamp(int64_t aTimestamp) override; + int64_t Timestamp() const override; + +protected: + cdm::VideoFormat mFormat; + cdm::Size mSize; + cdm::Buffer* mBuffer; + uint32_t mPlaneOffsets[kMaxPlanes]; + uint32_t mPlaneStrides[kMaxPlanes]; + int64_t mTimestamp; +}; + +} // namespace mozilla + +#endif diff --git a/dom/media/gmp/widevine-adapter/content_decryption_module.h b/dom/media/gmp/widevine-adapter/content_decryption_module.h new file mode 100644 index 000000000..0539135fb --- /dev/null +++ b/dom/media/gmp/widevine-adapter/content_decryption_module.h @@ -0,0 +1,1278 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CDM_CONTENT_DECRYPTION_MODULE_H_ +#define CDM_CONTENT_DECRYPTION_MODULE_H_ + +#include "content_decryption_module_export.h" + +#if defined(_MSC_VER) +typedef unsigned char uint8_t; +typedef unsigned int uint32_t; +typedef int int32_t; +typedef __int64 int64_t; +#else +#include <stdint.h> +#endif + +// Define CDM_CLASS_API to export class types. We have to add visibility +// attributes to make sure virtual tables in CDM consumer and CDM implementation +// are the same. Generally, it was always a good idea, as there're no guarantees +// about that for the internal symbols, but it has only become a practical issue +// after introduction of LTO devirtualization. See more details on +// https://crbug.com/609564#c35 +#if defined(_WIN32) +#if defined(__clang__) +#define CDM_CLASS_API [[clang::lto_visibility_public]] +#else +#define CDM_CLASS_API +#endif +#else // defined(_WIN32) +#define CDM_CLASS_API __attribute__((visibility("default"))) +#endif // defined(_WIN32) + +// The version number must be rolled when the exported functions are updated! +// If the CDM and the adapter use different versions of these functions, the +// adapter will fail to load or crash! +#define CDM_MODULE_VERSION 4 + +// Build the versioned entrypoint name. +// The extra macros are necessary to expand version to an actual value. +#define INITIALIZE_CDM_MODULE \ + BUILD_ENTRYPOINT(InitializeCdmModule, CDM_MODULE_VERSION) +#define BUILD_ENTRYPOINT(name, version) \ + BUILD_ENTRYPOINT_NO_EXPANSION(name, version) +#define BUILD_ENTRYPOINT_NO_EXPANSION(name, version) name##_##version + +extern "C" { +CDM_API void INITIALIZE_CDM_MODULE(); + +CDM_API void DeinitializeCdmModule(); + +// Returns a pointer to the requested CDM Host interface upon success. +// Returns NULL if the requested CDM Host interface is not supported. +// The caller should cast the returned pointer to the type matching +// |host_interface_version|. +typedef void* (*GetCdmHostFunc)(int host_interface_version, void* user_data); + +// Returns a pointer to the requested CDM upon success. +// Returns NULL if an error occurs or the requested |cdm_interface_version| or +// |key_system| is not supported or another error occurs. +// The caller should cast the returned pointer to the type matching +// |cdm_interface_version|. +// Caller retains ownership of arguments and must call Destroy() on the returned +// object. +CDM_API void* CreateCdmInstance( + int cdm_interface_version, + const char* key_system, uint32_t key_system_size, + GetCdmHostFunc get_cdm_host_func, void* user_data); + +CDM_API const char* GetCdmVersion(); +} + +namespace cdm { + +class CDM_CLASS_API AudioFrames; +class CDM_CLASS_API DecryptedBlock; +class CDM_CLASS_API VideoFrame; + +class CDM_CLASS_API Host_8; +class CDM_CLASS_API Host_9; + +enum Status { + kSuccess = 0, + kNeedMoreData, // Decoder needs more data to produce a decoded frame/sample. + kNoKey, // The required decryption key is not available. + kInitializationError, // Initialization error. + kDecryptError, // Decryption failed. + kDecodeError, // Error decoding audio or video. + kDeferredInitialization // Decoder is not ready for initialization. +}; + +// This must at least contain the exceptions defined in the spec: +// https://w3c.github.io/encrypted-media/#exceptions +// The following starts with the list of DOM4 exceptions from: +// http://www.w3.org/TR/dom/#domexception +// Some DOM4 exceptions are not included as they are not expected to be used. +// Should only be used on Host_8 and before. +enum Error { + kNotSupportedError = 9, + kInvalidStateError = 11, + kInvalidAccessError = 15, + kQuotaExceededError = 22, + + // Additional exceptions that do not have assigned codes. + // There are other non-EME-specific values, not included in this list. + kUnknownError = 30, + + // Additional values from previous EME versions. They currently have no + // matching DOMException. + kClientError = 100, + kOutputError = 101 +}; + +// Exceptions used by the CDM to reject promises. +// https://w3c.github.io/encrypted-media/#exceptions +enum Exception { + kExceptionTypeError, + kExceptionNotSupportedError, + kExceptionInvalidStateError, + kExceptionQuotaExceededError +}; + +// Time is defined as the number of seconds since the Epoch +// (00:00:00 UTC, January 1, 1970), not including any added leap second. +// Also see Time definition in spec: https://w3c.github.io/encrypted-media/#time +// Note that Time is defined in millisecond accuracy in the spec but in second +// accuracy here. +typedef double Time; + +// An input buffer can be split into several continuous subsamples. +// A SubsampleEntry specifies the number of clear and cipher bytes in each +// subsample. For example, the following buffer has three subsamples: +// +// |<----- subsample1 ----->|<----- subsample2 ----->|<----- subsample3 ----->| +// | clear1 | cipher1 | clear2 | cipher2 | clear3 | cipher3 | +// +// For decryption, all of the cipher bytes in a buffer should be concatenated +// (in the subsample order) into a single logical stream. The clear bytes should +// not be considered as part of decryption. +// +// Stream to decrypt: | cipher1 | cipher2 | cipher3 | +// Decrypted stream: | decrypted1| decrypted2 | decrypted3 | +// +// After decryption, the decrypted bytes should be copied over the position +// of the corresponding cipher bytes in the original buffer to form the output +// buffer. Following the above example, the decrypted buffer should be: +// +// |<----- subsample1 ----->|<----- subsample2 ----->|<----- subsample3 ----->| +// | clear1 | decrypted1| clear2 | decrypted2 | clear3 | decrypted3 | +// +struct SubsampleEntry { + SubsampleEntry(uint32_t clear_bytes, uint32_t cipher_bytes) + : clear_bytes(clear_bytes), cipher_bytes(cipher_bytes) {} + + uint32_t clear_bytes; + uint32_t cipher_bytes; +}; + +// Represents an input buffer to be decrypted (and possibly decoded). It does +// not own any pointers in this struct. If |iv_size| = 0, the data is +// unencrypted. +struct InputBuffer { + InputBuffer() + : data(nullptr), + data_size(0), + key_id(nullptr), + key_id_size(0), + iv(nullptr), + iv_size(0), + subsamples(nullptr), + num_subsamples(0), + timestamp(0) {} + + const uint8_t* data; // Pointer to the beginning of the input data. + uint32_t data_size; // Size (in bytes) of |data|. + + const uint8_t* key_id; // Key ID to identify the decryption key. + uint32_t key_id_size; // Size (in bytes) of |key_id|. + + const uint8_t* iv; // Initialization vector. + uint32_t iv_size; // Size (in bytes) of |iv|. + + const struct SubsampleEntry* subsamples; + uint32_t num_subsamples; // Number of subsamples in |subsamples|. + + int64_t timestamp; // Presentation timestamp in microseconds. +}; + +struct AudioDecoderConfig { + enum AudioCodec { + kUnknownAudioCodec = 0, + kCodecVorbis, + kCodecAac + }; + + AudioDecoderConfig() + : codec(kUnknownAudioCodec), + channel_count(0), + bits_per_channel(0), + samples_per_second(0), + extra_data(nullptr), + extra_data_size(0) {} + + AudioCodec codec; + int32_t channel_count; + int32_t bits_per_channel; + int32_t samples_per_second; + + // Optional byte data required to initialize audio decoders, such as the + // vorbis setup header. + uint8_t* extra_data; + uint32_t extra_data_size; +}; + +// Supported sample formats for AudioFrames. +enum AudioFormat { + kUnknownAudioFormat = 0, // Unknown format value. Used for error reporting. + kAudioFormatU8, // Interleaved unsigned 8-bit w/ bias of 128. + kAudioFormatS16, // Interleaved signed 16-bit. + kAudioFormatS32, // Interleaved signed 32-bit. + kAudioFormatF32, // Interleaved float 32-bit. + kAudioFormatPlanarS16, // Signed 16-bit planar. + kAudioFormatPlanarF32, // Float 32-bit planar. +}; + +// Surface formats based on FOURCC labels, see: http://www.fourcc.org/yuv.php +// Values are chosen to be consistent with Chromium's VideoPixelFormat values. +enum VideoFormat { + kUnknownVideoFormat = 0, // Unknown format value. Used for error reporting. + kYv12 = 1, // 12bpp YVU planar 1x1 Y, 2x2 VU samples. + kI420 = 2, // 12bpp YUV planar 1x1 Y, 2x2 UV samples. + + // In the following formats, each sample uses 16-bit in storage, while the + // sample value is stored in the least significant N bits where N is + // specified by the number after "P". For example, for YUV420P9, each Y, U, + // and V sample is stored in the least significant 9 bits in a 2-byte block. + kYUV420P9 = 16, + kYUV420P10 = 17, + kYUV422P9 = 18, + kYUV422P10 = 19, + kYUV444P9 = 20, + kYUV444P10 = 21, + kYUV420P12 = 22, + kYUV422P12 = 23, + kYUV444P12 = 24, +}; + +struct Size { + Size() : width(0), height(0) {} + Size(int32_t width, int32_t height) : width(width), height(height) {} + + int32_t width; + int32_t height; +}; + +struct VideoDecoderConfig { + enum VideoCodec { + kUnknownVideoCodec = 0, + kCodecVp8, + kCodecH264, + kCodecVp9 + }; + + enum VideoCodecProfile { + kUnknownVideoCodecProfile = 0, + kProfileNotNeeded, + kH264ProfileBaseline, + kH264ProfileMain, + kH264ProfileExtended, + kH264ProfileHigh, + kH264ProfileHigh10, + kH264ProfileHigh422, + kH264ProfileHigh444Predictive, + // VP9 Profiles are only passed in starting from CDM_9. + kVP9Profile0, + kVP9Profile1, + kVP9Profile2, + kVP9Profile3 + }; + + VideoDecoderConfig() + : codec(kUnknownVideoCodec), + profile(kUnknownVideoCodecProfile), + format(kUnknownVideoFormat), + extra_data(nullptr), + extra_data_size(0) {} + + VideoCodec codec; + VideoCodecProfile profile; + VideoFormat format; + + // Width and height of video frame immediately post-decode. Not all pixels + // in this region are valid. + Size coded_size; + + // Optional byte data required to initialize video decoders, such as H.264 + // AAVC data. + uint8_t* extra_data; + uint32_t extra_data_size; +}; + +enum StreamType { + kStreamTypeAudio = 0, + kStreamTypeVideo = 1 +}; + +// Structure provided to ContentDecryptionModule::OnPlatformChallengeResponse() +// after a platform challenge was initiated via Host::SendPlatformChallenge(). +// All values will be NULL / zero in the event of a challenge failure. +struct PlatformChallengeResponse { + // |challenge| provided during Host::SendPlatformChallenge() combined with + // nonce data and signed with the platform's private key. + const uint8_t* signed_data; + uint32_t signed_data_length; + + // RSASSA-PKCS1-v1_5-SHA256 signature of the |signed_data| block. + const uint8_t* signed_data_signature; + uint32_t signed_data_signature_length; + + // X.509 device specific certificate for the |service_id| requested. + const uint8_t* platform_key_certificate; + uint32_t platform_key_certificate_length; +}; + +// Used when passing arrays of binary data. Does not own the referenced data. +struct BinaryData { + BinaryData() : data(nullptr), length(0) {} + const uint8_t* data; + uint32_t length; +}; + +// The current status of the associated key. The valid types are defined in the +// spec: https://w3c.github.io/encrypted-media/#idl-def-MediaKeyStatus +enum KeyStatus { + kUsable = 0, + kInternalError = 1, + kExpired = 2, + kOutputRestricted = 3, + kOutputDownscaled = 4, + kStatusPending = 5, + kReleased = 6 +}; + +// Used when passing arrays of key information. Does not own the referenced +// data. |system_code| is an additional error code for unusable keys and +// should be 0 when |status| == kUsable. +struct KeyInformation { + KeyInformation() + : key_id(nullptr), + key_id_size(0), + status(kInternalError), + system_code(0) {} + const uint8_t* key_id; + uint32_t key_id_size; + KeyStatus status; + uint32_t system_code; +}; + +// Supported output protection methods for use with EnableOutputProtection() and +// returned by OnQueryOutputProtectionStatus(). +enum OutputProtectionMethods { + kProtectionNone = 0, + kProtectionHDCP = 1 << 0 +}; + +// Connected output link types returned by OnQueryOutputProtectionStatus(). +enum OutputLinkTypes { + kLinkTypeNone = 0, + kLinkTypeUnknown = 1 << 0, + kLinkTypeInternal = 1 << 1, + kLinkTypeVGA = 1 << 2, + kLinkTypeHDMI = 1 << 3, + kLinkTypeDVI = 1 << 4, + kLinkTypeDisplayPort = 1 << 5, + kLinkTypeNetwork = 1 << 6 +}; + +// Result of the QueryOutputProtectionStatus() call. +enum QueryResult { + kQuerySucceeded = 0, + kQueryFailed +}; + +// The Initialization Data Type. The valid types are defined in the spec: +// http://w3c.github.io/encrypted-media/initdata-format-registry.html#registry +enum InitDataType { + kCenc = 0, + kKeyIds = 1, + kWebM = 2 +}; + +// The type of session to create. The valid types are defined in the spec: +// https://w3c.github.io/encrypted-media/#idl-def-SessionType +enum SessionType { + kTemporary = 0, + kPersistentLicense = 1, + kPersistentKeyRelease = 2 +}; + +// The type of the message event. The valid types are defined in the spec: +// https://w3c.github.io/encrypted-media/#idl-def-MediaKeyMessageType +enum MessageType { + kLicenseRequest = 0, + kLicenseRenewal = 1, + kLicenseRelease = 2 +}; + +enum HdcpVersion { + kHdcpVersionNone, + kHdcpVersion1_0, + kHdcpVersion1_1, + kHdcpVersion1_2, + kHdcpVersion1_3, + kHdcpVersion1_4, + kHdcpVersion2_0, + kHdcpVersion2_1, + kHdcpVersion2_2 +}; + +struct Policy { + Policy() : min_hdcp_version(kHdcpVersionNone) {} + + HdcpVersion min_hdcp_version; +}; + +// FileIO interface provides a way for the CDM to store data in a file in +// persistent storage. This interface aims only at providing basic read/write +// capabilities and should not be used as a full fledged file IO API. +// Each CDM and origin (e.g. HTTPS, "foo.example.com", 443) combination has +// its own persistent storage. All instances of a given CDM associated with a +// given origin share the same persistent storage. +// Note to implementors of this interface: +// Per-origin storage and the ability for users to clear it are important. +// See http://www.w3.org/TR/encrypted-media/#privacy-storedinfo. +class CDM_CLASS_API FileIO { + public: + // Opens the file with |file_name| for read and write. + // FileIOClient::OnOpenComplete() will be called after the opening + // operation finishes. + // - When the file is opened by a CDM instance, it will be classified as "in + // use". In this case other CDM instances in the same domain may receive + // kInUse status when trying to open it. + // - |file_name| must only contain letters (A-Za-z), digits(0-9), or "._-". + // It must not start with an underscore ('_'), and must be at least 1 + // character and no more than 256 characters long. + virtual void Open(const char* file_name, uint32_t file_name_size) = 0; + + // Reads the contents of the file. FileIOClient::OnReadComplete() will be + // called with the read status. Read() should not be called if a previous + // Read() or Write() call is still pending; otherwise OnReadComplete() will + // be called with kInUse. + virtual void Read() = 0; + + // Writes |data_size| bytes of |data| into the file. + // FileIOClient::OnWriteComplete() will be called with the write status. + // All existing contents in the file will be overwritten. Calling Write() with + // NULL |data| will clear all contents in the file. Write() should not be + // called if a previous Write() or Read() call is still pending; otherwise + // OnWriteComplete() will be called with kInUse. + virtual void Write(const uint8_t* data, uint32_t data_size) = 0; + + // Closes the file if opened, destroys this FileIO object and releases any + // resources allocated. The CDM must call this method when it finished using + // this object. A FileIO object must not be used after Close() is called. + virtual void Close() = 0; + + protected: + FileIO() {} + virtual ~FileIO() {} +}; + +// Responses to FileIO calls. All responses will be called asynchronously. +// When kError is returned, the FileIO object could be in an error state. All +// following calls (other than Close()) could return kError. The CDM should +// still call Close() to destroy the FileIO object. +class CDM_CLASS_API FileIOClient { + public: + enum Status { + kSuccess = 0, + kInUse, + kError + }; + + // Response to a FileIO::Open() call with the open |status|. + virtual void OnOpenComplete(Status status) = 0; + + // Response to a FileIO::Read() call to provide |data_size| bytes of |data| + // read from the file. + // - kSuccess indicates that all contents of the file has been successfully + // read. In this case, 0 |data_size| means that the file is empty. + // - kInUse indicates that there are other read/write operations pending. + // - kError indicates read failure, e.g. the storage is not open or cannot be + // fully read. + virtual void OnReadComplete(Status status, + const uint8_t* data, uint32_t data_size) = 0; + + // Response to a FileIO::Write() call. + // - kSuccess indicates that all the data has been written into the file + // successfully. + // - kInUse indicates that there are other read/write operations pending. + // - kError indicates write failure, e.g. the storage is not open or cannot be + // fully written. Upon write failure, the contents of the file should be + // regarded as corrupt and should not used. + virtual void OnWriteComplete(Status status) = 0; + + protected: + FileIOClient() {} + virtual ~FileIOClient() {} +}; + +// ContentDecryptionModule interface that all CDMs need to implement. +// The interface is versioned for backward compatibility. +// Note: ContentDecryptionModule implementations must use the allocator +// provided in CreateCdmInstance() to allocate any Buffer that needs to +// be passed back to the caller. Implementations must call Buffer::Destroy() +// when a Buffer is created that will never be returned to the caller. +class CDM_CLASS_API ContentDecryptionModule_8 { + public: + static const int kVersion = 8; + typedef Host_8 Host; + + // Initializes the CDM instance, providing information about permitted + // functionalities. + // If |allow_distinctive_identifier| is false, messages from the CDM, + // such as message events, must not contain a Distinctive Identifier, + // even in an encrypted form. + // If |allow_persistent_state| is false, the CDM must not attempt to + // persist state. Calls to CreateFileIO() will fail. + virtual void Initialize(bool allow_distinctive_identifier, + bool allow_persistent_state) = 0; + + // SetServerCertificate(), CreateSessionAndGenerateRequest(), LoadSession(), + // UpdateSession(), CloseSession(), and RemoveSession() all accept a + // |promise_id|, which must be passed to the completion Host method + // (e.g. Host::OnResolveNewSessionPromise()). + + // Provides a server certificate to be used to encrypt messages to the + // license server. The CDM must respond by calling either + // Host::OnResolvePromise() or Host::OnRejectPromise(). + virtual void SetServerCertificate(uint32_t promise_id, + const uint8_t* server_certificate_data, + uint32_t server_certificate_data_size) = 0; + + // Creates a session given |session_type|, |init_data_type|, and |init_data|. + // The CDM must respond by calling either Host::OnResolveNewSessionPromise() + // or Host::OnRejectPromise(). + virtual void CreateSessionAndGenerateRequest(uint32_t promise_id, + SessionType session_type, + InitDataType init_data_type, + const uint8_t* init_data, + uint32_t init_data_size) = 0; + + // Loads the session of type |session_type| specified by |session_id|. + // The CDM must respond by calling either Host::OnResolveNewSessionPromise() + // or Host::OnRejectPromise(). If the session is not found, call + // Host::OnResolveNewSessionPromise() with session_id = NULL. + virtual void LoadSession(uint32_t promise_id, + SessionType session_type, + const char* session_id, + uint32_t session_id_size) = 0; + + // Updates the session with |response|. The CDM must respond by calling + // either Host::OnResolvePromise() or Host::OnRejectPromise(). + virtual void UpdateSession(uint32_t promise_id, + const char* session_id, + uint32_t session_id_size, + const uint8_t* response, + uint32_t response_size) = 0; + + // Requests that the CDM close the session. The CDM must respond by calling + // either Host::OnResolvePromise() or Host::OnRejectPromise() when the request + // has been processed. This may be before the session is closed. Once the + // session is closed, Host::OnSessionClosed() must also be called. + virtual void CloseSession(uint32_t promise_id, + const char* session_id, + uint32_t session_id_size) = 0; + + // Removes any stored session data associated with this session. Will only be + // called for persistent sessions. The CDM must respond by calling either + // Host::OnResolvePromise() or Host::OnRejectPromise() when the request has + // been processed. + virtual void RemoveSession(uint32_t promise_id, + const char* session_id, + uint32_t session_id_size) = 0; + + // Performs scheduled operation with |context| when the timer fires. + virtual void TimerExpired(void* context) = 0; + + // Decrypts the |encrypted_buffer|. + // + // Returns kSuccess if decryption succeeded, in which case the callee + // should have filled the |decrypted_buffer| and passed the ownership of + // |data| in |decrypted_buffer| to the caller. + // Returns kNoKey if the CDM did not have the necessary decryption key + // to decrypt. + // Returns kDecryptError if any other error happened. + // If the return value is not kSuccess, |decrypted_buffer| should be ignored + // by the caller. + virtual Status Decrypt(const InputBuffer& encrypted_buffer, + DecryptedBlock* decrypted_buffer) = 0; + + // Initializes the CDM audio decoder with |audio_decoder_config|. This + // function must be called before DecryptAndDecodeSamples() is called. + // + // Returns kSuccess if the |audio_decoder_config| is supported and the CDM + // audio decoder is successfully initialized. + // Returns kSessionError if |audio_decoder_config| is not supported. The CDM + // may still be able to do Decrypt(). + // Returns kDeferredInitialization if the CDM is not ready to initialize the + // decoder at this time. Must call Host::OnDeferredInitializationDone() once + // initialization is complete. + virtual Status InitializeAudioDecoder( + const AudioDecoderConfig& audio_decoder_config) = 0; + + // Initializes the CDM video decoder with |video_decoder_config|. This + // function must be called before DecryptAndDecodeFrame() is called. + // + // Returns kSuccess if the |video_decoder_config| is supported and the CDM + // video decoder is successfully initialized. + // Returns kSessionError if |video_decoder_config| is not supported. The CDM + // may still be able to do Decrypt(). + // Returns kDeferredInitialization if the CDM is not ready to initialize the + // decoder at this time. Must call Host::OnDeferredInitializationDone() once + // initialization is complete. + virtual Status InitializeVideoDecoder( + const VideoDecoderConfig& video_decoder_config) = 0; + + // De-initializes the CDM decoder and sets it to an uninitialized state. The + // caller can initialize the decoder again after this call to re-initialize + // it. This can be used to reconfigure the decoder if the configuration + // changes. + virtual void DeinitializeDecoder(StreamType decoder_type) = 0; + + // Resets the CDM decoder to an initialized clean state. All internal buffers + // MUST be flushed. + virtual void ResetDecoder(StreamType decoder_type) = 0; + + // Decrypts the |encrypted_buffer| and decodes the decrypted buffer into a + // |video_frame|. Upon end-of-stream, the caller should call this function + // repeatedly with empty |encrypted_buffer| (|data| == NULL) until only empty + // |video_frame| (|format| == kEmptyVideoFrame) is produced. + // + // Returns kSuccess if decryption and decoding both succeeded, in which case + // the callee will have filled the |video_frame| and passed the ownership of + // |frame_buffer| in |video_frame| to the caller. + // Returns kNoKey if the CDM did not have the necessary decryption key + // to decrypt. + // Returns kNeedMoreData if more data was needed by the decoder to generate + // a decoded frame (e.g. during initialization and end-of-stream). + // Returns kDecryptError if any decryption error happened. + // Returns kDecodeError if any decoding error happened. + // If the return value is not kSuccess, |video_frame| should be ignored by + // the caller. + virtual Status DecryptAndDecodeFrame(const InputBuffer& encrypted_buffer, + VideoFrame* video_frame) = 0; + + // Decrypts the |encrypted_buffer| and decodes the decrypted buffer into + // |audio_frames|. Upon end-of-stream, the caller should call this function + // repeatedly with empty |encrypted_buffer| (|data| == NULL) until only empty + // |audio_frames| is produced. + // + // Returns kSuccess if decryption and decoding both succeeded, in which case + // the callee will have filled |audio_frames| and passed the ownership of + // |data| in |audio_frames| to the caller. + // Returns kNoKey if the CDM did not have the necessary decryption key + // to decrypt. + // Returns kNeedMoreData if more data was needed by the decoder to generate + // audio samples (e.g. during initialization and end-of-stream). + // Returns kDecryptError if any decryption error happened. + // Returns kDecodeError if any decoding error happened. + // If the return value is not kSuccess, |audio_frames| should be ignored by + // the caller. + virtual Status DecryptAndDecodeSamples(const InputBuffer& encrypted_buffer, + AudioFrames* audio_frames) = 0; + + // Called by the host after a platform challenge was initiated via + // Host::SendPlatformChallenge(). + virtual void OnPlatformChallengeResponse( + const PlatformChallengeResponse& response) = 0; + + // Called by the host after a call to Host::QueryOutputProtectionStatus(). The + // |link_mask| is a bit mask of OutputLinkTypes and |output_protection_mask| + // is a bit mask of OutputProtectionMethods. If |result| is kQueryFailed, + // then |link_mask| and |output_protection_mask| are undefined and should + // be ignored. + virtual void OnQueryOutputProtectionStatus( + QueryResult result, + uint32_t link_mask, + uint32_t output_protection_mask) = 0; + + // Destroys the object in the same context as it was created. + virtual void Destroy() = 0; + + protected: + ContentDecryptionModule_8() {} + virtual ~ContentDecryptionModule_8() {} +}; + +// ContentDecryptionModule interface that all CDMs need to implement. +// The interface is versioned for backward compatibility. +// Note: ContentDecryptionModule implementations must use the allocator +// provided in CreateCdmInstance() to allocate any Buffer that needs to +// be passed back to the caller. Implementations must call Buffer::Destroy() +// when a Buffer is created that will never be returned to the caller. +class CDM_CLASS_API ContentDecryptionModule_9 { + public: + static const int kVersion = 9; + typedef Host_9 Host; + + // Initializes the CDM instance, providing information about permitted + // functionalities. + // If |allow_distinctive_identifier| is false, messages from the CDM, + // such as message events, must not contain a Distinctive Identifier, + // even in an encrypted form. + // If |allow_persistent_state| is false, the CDM must not attempt to + // persist state. Calls to CreateFileIO() will fail. + virtual void Initialize(bool allow_distinctive_identifier, + bool allow_persistent_state) = 0; + + // Gets the key status if the CDM has a hypothetical key with the |policy|. + // The CDM must respond by calling either Host::OnResolveKeyStatusPromise() + // with the result key status or Host::OnRejectPromise() if an unexpected + // error happened or this method is not supported. + virtual void GetStatusForPolicy(uint32_t promise_id, + const Policy& policy) = 0; + + // SetServerCertificate(), CreateSessionAndGenerateRequest(), LoadSession(), + // UpdateSession(), CloseSession(), and RemoveSession() all accept a + // |promise_id|, which must be passed to the completion Host method + // (e.g. Host::OnResolveNewSessionPromise()). + + // Provides a server certificate to be used to encrypt messages to the + // license server. The CDM must respond by calling either + // Host::OnResolvePromise() or Host::OnRejectPromise(). + virtual void SetServerCertificate(uint32_t promise_id, + const uint8_t* server_certificate_data, + uint32_t server_certificate_data_size) = 0; + + // Creates a session given |session_type|, |init_data_type|, and |init_data|. + // The CDM must respond by calling either Host::OnResolveNewSessionPromise() + // or Host::OnRejectPromise(). + virtual void CreateSessionAndGenerateRequest(uint32_t promise_id, + SessionType session_type, + InitDataType init_data_type, + const uint8_t* init_data, + uint32_t init_data_size) = 0; + + // Loads the session of type |session_type| specified by |session_id|. + // The CDM must respond by calling either Host::OnResolveNewSessionPromise() + // or Host::OnRejectPromise(). If the session is not found, call + // Host::OnResolveNewSessionPromise() with session_id = NULL. + virtual void LoadSession(uint32_t promise_id, + SessionType session_type, + const char* session_id, + uint32_t session_id_size) = 0; + + // Updates the session with |response|. The CDM must respond by calling + // either Host::OnResolvePromise() or Host::OnRejectPromise(). + virtual void UpdateSession(uint32_t promise_id, + const char* session_id, + uint32_t session_id_size, + const uint8_t* response, + uint32_t response_size) = 0; + + // Requests that the CDM close the session. The CDM must respond by calling + // either Host::OnResolvePromise() or Host::OnRejectPromise() when the request + // has been processed. This may be before the session is closed. Once the + // session is closed, Host::OnSessionClosed() must also be called. + virtual void CloseSession(uint32_t promise_id, + const char* session_id, + uint32_t session_id_size) = 0; + + // Removes any stored session data associated with this session. Will only be + // called for persistent sessions. The CDM must respond by calling either + // Host::OnResolvePromise() or Host::OnRejectPromise() when the request has + // been processed. + virtual void RemoveSession(uint32_t promise_id, + const char* session_id, + uint32_t session_id_size) = 0; + + // Performs scheduled operation with |context| when the timer fires. + virtual void TimerExpired(void* context) = 0; + + // Decrypts the |encrypted_buffer|. + // + // Returns kSuccess if decryption succeeded, in which case the callee + // should have filled the |decrypted_buffer| and passed the ownership of + // |data| in |decrypted_buffer| to the caller. + // Returns kNoKey if the CDM did not have the necessary decryption key + // to decrypt. + // Returns kDecryptError if any other error happened. + // If the return value is not kSuccess, |decrypted_buffer| should be ignored + // by the caller. + virtual Status Decrypt(const InputBuffer& encrypted_buffer, + DecryptedBlock* decrypted_buffer) = 0; + + // Initializes the CDM audio decoder with |audio_decoder_config|. This + // function must be called before DecryptAndDecodeSamples() is called. + // + // Returns kSuccess if the |audio_decoder_config| is supported and the CDM + // audio decoder is successfully initialized. + // Returns kInitializationError if |audio_decoder_config| is not supported. + // The CDM may still be able to do Decrypt(). + // Returns kDeferredInitialization if the CDM is not ready to initialize the + // decoder at this time. Must call Host::OnDeferredInitializationDone() once + // initialization is complete. + virtual Status InitializeAudioDecoder( + const AudioDecoderConfig& audio_decoder_config) = 0; + + // Initializes the CDM video decoder with |video_decoder_config|. This + // function must be called before DecryptAndDecodeFrame() is called. + // + // Returns kSuccess if the |video_decoder_config| is supported and the CDM + // video decoder is successfully initialized. + // Returns kInitializationError if |video_decoder_config| is not supported. + // The CDM may still be able to do Decrypt(). + // Returns kDeferredInitialization if the CDM is not ready to initialize the + // decoder at this time. Must call Host::OnDeferredInitializationDone() once + // initialization is complete. + virtual Status InitializeVideoDecoder( + const VideoDecoderConfig& video_decoder_config) = 0; + + // De-initializes the CDM decoder and sets it to an uninitialized state. The + // caller can initialize the decoder again after this call to re-initialize + // it. This can be used to reconfigure the decoder if the configuration + // changes. + virtual void DeinitializeDecoder(StreamType decoder_type) = 0; + + // Resets the CDM decoder to an initialized clean state. All internal buffers + // MUST be flushed. + virtual void ResetDecoder(StreamType decoder_type) = 0; + + // Decrypts the |encrypted_buffer| and decodes the decrypted buffer into a + // |video_frame|. Upon end-of-stream, the caller should call this function + // repeatedly with empty |encrypted_buffer| (|data| == NULL) until only empty + // |video_frame| (|format| == kEmptyVideoFrame) is produced. + // + // Returns kSuccess if decryption and decoding both succeeded, in which case + // the callee will have filled the |video_frame| and passed the ownership of + // |frame_buffer| in |video_frame| to the caller. + // Returns kNoKey if the CDM did not have the necessary decryption key + // to decrypt. + // Returns kNeedMoreData if more data was needed by the decoder to generate + // a decoded frame (e.g. during initialization and end-of-stream). + // Returns kDecryptError if any decryption error happened. + // Returns kDecodeError if any decoding error happened. + // If the return value is not kSuccess, |video_frame| should be ignored by + // the caller. + virtual Status DecryptAndDecodeFrame(const InputBuffer& encrypted_buffer, + VideoFrame* video_frame) = 0; + + // Decrypts the |encrypted_buffer| and decodes the decrypted buffer into + // |audio_frames|. Upon end-of-stream, the caller should call this function + // repeatedly with empty |encrypted_buffer| (|data| == NULL) until only empty + // |audio_frames| is produced. + // + // Returns kSuccess if decryption and decoding both succeeded, in which case + // the callee will have filled |audio_frames| and passed the ownership of + // |data| in |audio_frames| to the caller. + // Returns kNoKey if the CDM did not have the necessary decryption key + // to decrypt. + // Returns kNeedMoreData if more data was needed by the decoder to generate + // audio samples (e.g. during initialization and end-of-stream). + // Returns kDecryptError if any decryption error happened. + // Returns kDecodeError if any decoding error happened. + // If the return value is not kSuccess, |audio_frames| should be ignored by + // the caller. + virtual Status DecryptAndDecodeSamples(const InputBuffer& encrypted_buffer, + AudioFrames* audio_frames) = 0; + + // Called by the host after a platform challenge was initiated via + // Host::SendPlatformChallenge(). + virtual void OnPlatformChallengeResponse( + const PlatformChallengeResponse& response) = 0; + + // Called by the host after a call to Host::QueryOutputProtectionStatus(). The + // |link_mask| is a bit mask of OutputLinkTypes and |output_protection_mask| + // is a bit mask of OutputProtectionMethods. If |result| is kQueryFailed, + // then |link_mask| and |output_protection_mask| are undefined and should + // be ignored. + virtual void OnQueryOutputProtectionStatus( + QueryResult result, + uint32_t link_mask, + uint32_t output_protection_mask) = 0; + + // Called by the host after a call to Host::RequestStorageId(). If the + // version of the storage ID requested is available, |storage_id| and + // |storage_id_size| are set appropriately. |version| will be the same as + // what was requested, unless 0 (latest) was requested, in which case + // |version| will be the actual version number for the |storage_id| returned. + // If the requested version is not available, null/zero will be provided as + // |storage_id| and |storage_id_size|, respectively, and |version| should be + // ignored. + virtual void OnStorageId(uint32_t version, + const uint8_t* storage_id, + uint32_t storage_id_size) = 0; + + // Destroys the object in the same context as it was created. + virtual void Destroy() = 0; + + protected: + ContentDecryptionModule_9() {} + virtual ~ContentDecryptionModule_9() {} +}; + +typedef ContentDecryptionModule_9 ContentDecryptionModule; + +// Represents a buffer created by Allocator implementations. +class CDM_CLASS_API Buffer { + public: + // Destroys the buffer in the same context as it was created. + virtual void Destroy() = 0; + + virtual uint32_t Capacity() const = 0; + virtual uint8_t* Data() = 0; + virtual void SetSize(uint32_t size) = 0; + virtual uint32_t Size() const = 0; + + protected: + Buffer() {} + virtual ~Buffer() {} + + private: + Buffer(const Buffer&); + void operator=(const Buffer&); +}; + +class CDM_CLASS_API Host_8 { + public: + static const int kVersion = 8; + + // Returns a Buffer* containing non-zero members upon success, or NULL on + // failure. The caller owns the Buffer* after this call. The buffer is not + // guaranteed to be zero initialized. The capacity of the allocated Buffer + // is guaranteed to be not less than |capacity|. + virtual Buffer* Allocate(uint32_t capacity) = 0; + + // Requests the host to call ContentDecryptionModule::TimerFired() |delay_ms| + // from now with |context|. + virtual void SetTimer(int64_t delay_ms, void* context) = 0; + + // Returns the current wall time. + virtual Time GetCurrentWallTime() = 0; + + // Called by the CDM when a session is created or loaded and the value for the + // MediaKeySession's sessionId attribute is available (|session_id|). + // This must be called before OnSessionMessage() or + // OnSessionKeysChange() is called for the same session. |session_id_size| + // should not include null termination. + // When called in response to LoadSession(), the |session_id| must be the + // same as the |session_id| passed in LoadSession(), or NULL if the + // session could not be loaded. + virtual void OnResolveNewSessionPromise(uint32_t promise_id, + const char* session_id, + uint32_t session_id_size) = 0; + + // Called by the CDM when a session is updated or released. + virtual void OnResolvePromise(uint32_t promise_id) = 0; + + // Called by the CDM when an error occurs as a result of one of the + // ContentDecryptionModule calls that accept a |promise_id|. + // |error| must be specified, |error_message| and |system_code| + // are optional. |error_message_size| should not include null termination. + virtual void OnRejectPromise(uint32_t promise_id, + Error error, + uint32_t system_code, + const char* error_message, + uint32_t error_message_size) = 0; + + // Called by the CDM when it has a message for session |session_id|. + // Size parameters should not include null termination. + // |legacy_destination_url| is only for supporting the prefixed EME API and + // is ignored by unprefixed EME. It should only be non-null if |message_type| + // is kLicenseRenewal. + virtual void OnSessionMessage(const char* session_id, + uint32_t session_id_size, + MessageType message_type, + const char* message, + uint32_t message_size, + const char* legacy_destination_url, + uint32_t legacy_destination_url_length) = 0; + + // Called by the CDM when there has been a change in keys or their status for + // session |session_id|. |has_additional_usable_key| should be set if a + // key is newly usable (e.g. new key available, previously expired key has + // been renewed, etc.) and the browser should attempt to resume playback. + // |key_ids| is the list of key ids for this session along with their + // current status. |key_ids_count| is the number of entries in |key_ids|. + // Size parameter for |session_id| should not include null termination. + virtual void OnSessionKeysChange(const char* session_id, + uint32_t session_id_size, + bool has_additional_usable_key, + const KeyInformation* keys_info, + uint32_t keys_info_count) = 0; + + // Called by the CDM when there has been a change in the expiration time for + // session |session_id|. This can happen as the result of an Update() call + // or some other event. If this happens as a result of a call to Update(), + // it must be called before resolving the Update() promise. |new_expiry_time| + // represents the time after which the key(s) in the session will no longer + // be usable for decryption. It can be 0 if no such time exists or if the + // license explicitly never expires. Size parameter should not include null + // termination. + virtual void OnExpirationChange(const char* session_id, + uint32_t session_id_size, + Time new_expiry_time) = 0; + + // Called by the CDM when session |session_id| is closed. Size + // parameter should not include null termination. + virtual void OnSessionClosed(const char* session_id, + uint32_t session_id_size) = 0; + + // Called by the CDM when an error occurs in session |session_id| + // unrelated to one of the ContentDecryptionModule calls that accept a + // |promise_id|. |error| must be specified, |error_message| and + // |system_code| are optional. Length parameters should not include null + // termination. + // Note: + // - This method is only for supporting prefixed EME API. + // - This method will be ignored by unprefixed EME. All errors reported + // in this method should probably also be reported by one of other methods. + virtual void OnLegacySessionError( + const char* session_id, uint32_t session_id_length, + Error error, + uint32_t system_code, + const char* error_message, uint32_t error_message_length) = 0; + + // The following are optional methods that may not be implemented on all + // platforms. + + // Sends a platform challenge for the given |service_id|. |challenge| is at + // most 256 bits of data to be signed. Once the challenge has been completed, + // the host will call ContentDecryptionModule::OnPlatformChallengeResponse() + // with the signed challenge response and platform certificate. Size + // parameters should not include null termination. + virtual void SendPlatformChallenge(const char* service_id, + uint32_t service_id_size, + const char* challenge, + uint32_t challenge_size) = 0; + + // Attempts to enable output protection (e.g. HDCP) on the display link. The + // |desired_protection_mask| is a bit mask of OutputProtectionMethods. No + // status callback is issued, the CDM must call QueryOutputProtectionStatus() + // periodically to ensure the desired protections are applied. + virtual void EnableOutputProtection(uint32_t desired_protection_mask) = 0; + + // Requests the current output protection status. Once the host has the status + // it will call ContentDecryptionModule::OnQueryOutputProtectionStatus(). + virtual void QueryOutputProtectionStatus() = 0; + + // Must be called by the CDM if it returned kDeferredInitialization during + // InitializeAudioDecoder() or InitializeVideoDecoder(). + virtual void OnDeferredInitializationDone(StreamType stream_type, + Status decoder_status) = 0; + + // Creates a FileIO object from the host to do file IO operation. Returns NULL + // if a FileIO object cannot be obtained. Once a valid FileIO object is + // returned, |client| must be valid until FileIO::Close() is called. The + // CDM can call this method multiple times to operate on different files. + virtual FileIO* CreateFileIO(FileIOClient* client) = 0; + + protected: + Host_8() {} + virtual ~Host_8() {} +}; + +class CDM_CLASS_API Host_9 { + public: + static const int kVersion = 9; + + // Returns a Buffer* containing non-zero members upon success, or NULL on + // failure. The caller owns the Buffer* after this call. The buffer is not + // guaranteed to be zero initialized. The capacity of the allocated Buffer + // is guaranteed to be not less than |capacity|. + virtual Buffer* Allocate(uint32_t capacity) = 0; + + // Requests the host to call ContentDecryptionModule::TimerFired() |delay_ms| + // from now with |context|. + virtual void SetTimer(int64_t delay_ms, void* context) = 0; + + // Returns the current wall time. + virtual Time GetCurrentWallTime() = 0; + + // Called by the CDM when a key status is available in response to + // GetStatusForPolicy(). + virtual void OnResolveKeyStatusPromise(uint32_t promise_id, + KeyStatus key_status) = 0; + + // Called by the CDM when a session is created or loaded and the value for the + // MediaKeySession's sessionId attribute is available (|session_id|). + // This must be called before OnSessionMessage() or + // OnSessionKeysChange() is called for the same session. |session_id_size| + // should not include null termination. + // When called in response to LoadSession(), the |session_id| must be the + // same as the |session_id| passed in LoadSession(), or NULL if the + // session could not be loaded. + virtual void OnResolveNewSessionPromise(uint32_t promise_id, + const char* session_id, + uint32_t session_id_size) = 0; + + // Called by the CDM when a session is updated or released. + virtual void OnResolvePromise(uint32_t promise_id) = 0; + + // Called by the CDM when an error occurs as a result of one of the + // ContentDecryptionModule calls that accept a |promise_id|. + // |exception| must be specified. |error_message| and |system_code| + // are optional. |error_message_size| should not include null termination. + virtual void OnRejectPromise(uint32_t promise_id, + Exception exception, + uint32_t system_code, + const char* error_message, + uint32_t error_message_size) = 0; + + // Called by the CDM when it has a message for session |session_id|. + // Size parameters should not include null termination. + virtual void OnSessionMessage(const char* session_id, + uint32_t session_id_size, + MessageType message_type, + const char* message, + uint32_t message_size) = 0; + + // Called by the CDM when there has been a change in keys or their status for + // session |session_id|. |has_additional_usable_key| should be set if a + // key is newly usable (e.g. new key available, previously expired key has + // been renewed, etc.) and the browser should attempt to resume playback. + // |key_ids| is the list of key ids for this session along with their + // current status. |key_ids_count| is the number of entries in |key_ids|. + // Size parameter for |session_id| should not include null termination. + virtual void OnSessionKeysChange(const char* session_id, + uint32_t session_id_size, + bool has_additional_usable_key, + const KeyInformation* keys_info, + uint32_t keys_info_count) = 0; + + // Called by the CDM when there has been a change in the expiration time for + // session |session_id|. This can happen as the result of an Update() call + // or some other event. If this happens as a result of a call to Update(), + // it must be called before resolving the Update() promise. |new_expiry_time| + // represents the time after which the key(s) in the session will no longer + // be usable for decryption. It can be 0 if no such time exists or if the + // license explicitly never expires. Size parameter should not include null + // termination. + virtual void OnExpirationChange(const char* session_id, + uint32_t session_id_size, + Time new_expiry_time) = 0; + + // Called by the CDM when session |session_id| is closed. Size + // parameter should not include null termination. + virtual void OnSessionClosed(const char* session_id, + uint32_t session_id_size) = 0; + + // The following are optional methods that may not be implemented on all + // platforms. + + // Sends a platform challenge for the given |service_id|. |challenge| is at + // most 256 bits of data to be signed. Once the challenge has been completed, + // the host will call ContentDecryptionModule::OnPlatformChallengeResponse() + // with the signed challenge response and platform certificate. Size + // parameters should not include null termination. + virtual void SendPlatformChallenge(const char* service_id, + uint32_t service_id_size, + const char* challenge, + uint32_t challenge_size) = 0; + + // Attempts to enable output protection (e.g. HDCP) on the display link. The + // |desired_protection_mask| is a bit mask of OutputProtectionMethods. No + // status callback is issued, the CDM must call QueryOutputProtectionStatus() + // periodically to ensure the desired protections are applied. + virtual void EnableOutputProtection(uint32_t desired_protection_mask) = 0; + + // Requests the current output protection status. Once the host has the status + // it will call ContentDecryptionModule::OnQueryOutputProtectionStatus(). + virtual void QueryOutputProtectionStatus() = 0; + + // Must be called by the CDM if it returned kDeferredInitialization during + // InitializeAudioDecoder() or InitializeVideoDecoder(). + virtual void OnDeferredInitializationDone(StreamType stream_type, + Status decoder_status) = 0; + + // Creates a FileIO object from the host to do file IO operation. Returns NULL + // if a FileIO object cannot be obtained. Once a valid FileIO object is + // returned, |client| must be valid until FileIO::Close() is called. The + // CDM can call this method multiple times to operate on different files. + virtual FileIO* CreateFileIO(FileIOClient* client) = 0; + + // Requests a specific version of the storage ID. A storage ID is a stable, + // device specific ID used by the CDM to securely store persistent data. The + // ID will be returned by the host via ContentDecryptionModule::OnStorageId(). + // If |version| is 0, the latest version will be returned. All |version|s + // that are greater than or equal to 0x80000000 are reserved for the CDM and + // should not be supported or returned by the host. The CDM must not expose + // the ID outside the client device, even in encrypted form. + virtual void RequestStorageId(uint32_t version) = 0; + + protected: + Host_9() {} + virtual ~Host_9() {} +}; + +// Represents a decrypted block that has not been decoded. +class CDM_CLASS_API DecryptedBlock { + public: + virtual void SetDecryptedBuffer(Buffer* buffer) = 0; + virtual Buffer* DecryptedBuffer() = 0; + + // TODO(tomfinegan): Figure out if timestamp is really needed. If it is not, + // we can just pass Buffer pointers around. + virtual void SetTimestamp(int64_t timestamp) = 0; + virtual int64_t Timestamp() const = 0; + + protected: + DecryptedBlock() {} + virtual ~DecryptedBlock() {} +}; + +class CDM_CLASS_API VideoFrame { + public: + enum VideoPlane { + kYPlane = 0, + kUPlane = 1, + kVPlane = 2, + kMaxPlanes = 3, + }; + + virtual void SetFormat(VideoFormat format) = 0; + virtual VideoFormat Format() const = 0; + + virtual void SetSize(cdm::Size size) = 0; + virtual cdm::Size Size() const = 0; + + virtual void SetFrameBuffer(Buffer* frame_buffer) = 0; + virtual Buffer* FrameBuffer() = 0; + + virtual void SetPlaneOffset(VideoPlane plane, uint32_t offset) = 0; + virtual uint32_t PlaneOffset(VideoPlane plane) = 0; + + virtual void SetStride(VideoPlane plane, uint32_t stride) = 0; + virtual uint32_t Stride(VideoPlane plane) = 0; + + virtual void SetTimestamp(int64_t timestamp) = 0; + virtual int64_t Timestamp() const = 0; + + protected: + VideoFrame() {} + virtual ~VideoFrame() {} +}; + +// Represents decrypted and decoded audio frames. AudioFrames can contain +// multiple audio output buffers, which are serialized into this format: +// +// |<------------------- serialized audio buffer ------------------->| +// | int64_t timestamp | int64_t length | length bytes of audio data | +// +// For example, with three audio output buffers, the AudioFrames will look +// like this: +// +// |<----------------- AudioFrames ------------------>| +// | audio buffer 0 | audio buffer 1 | audio buffer 2 | +class CDM_CLASS_API AudioFrames { + public: + virtual void SetFrameBuffer(Buffer* buffer) = 0; + virtual Buffer* FrameBuffer() = 0; + + // The CDM must call this method, providing a valid format, when providing + // frame buffers. Planar data should be stored end to end; e.g., + // |ch1 sample1||ch1 sample2|....|ch1 sample_last||ch2 sample1|... + virtual void SetFormat(AudioFormat format) = 0; + virtual AudioFormat Format() const = 0; + + protected: + AudioFrames() {} + virtual ~AudioFrames() {} +}; + +} // namespace cdm + +#endif // CDM_CONTENT_DECRYPTION_MODULE_H_ diff --git a/dom/media/gmp/widevine-adapter/content_decryption_module_export.h b/dom/media/gmp/widevine-adapter/content_decryption_module_export.h new file mode 100644 index 000000000..51d485892 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/content_decryption_module_export.h @@ -0,0 +1,22 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CDM_CONTENT_DECRYPTION_MODULE_EXPORT_H_ +#define CDM_CONTENT_DECRYPTION_MODULE_EXPORT_H_ + +// Define CDM_API so that functionality implemented by the CDM module +// can be exported to consumers. +#if defined(_WIN32) + +#if defined(CDM_IMPLEMENTATION) +#define CDM_API __declspec(dllexport) +#else +#define CDM_API __declspec(dllimport) +#endif // defined(CDM_IMPLEMENTATION) + +#else // defined(_WIN32) +#define CDM_API __attribute__((visibility("default"))) +#endif // defined(_WIN32) + +#endif // CDM_CONTENT_DECRYPTION_MODULE_EXPORT_H_ diff --git a/dom/media/gmp/widevine-adapter/content_decryption_module_ext.h b/dom/media/gmp/widevine-adapter/content_decryption_module_ext.h new file mode 100644 index 000000000..5df8344e6 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/content_decryption_module_ext.h @@ -0,0 +1,64 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CDM_CONTENT_DECRYPTION_MODULE_EXT_H_ +#define CDM_CONTENT_DECRYPTION_MODULE_EXT_H_ + +#if defined(_WIN32) +#include <windows.h> +#endif + +#include "content_decryption_module_export.h" + +#if defined(_MSC_VER) +typedef unsigned int uint32_t; +#else +#include <stdint.h> +#endif + +namespace cdm { + +#if defined(_WIN32) +typedef wchar_t FilePathCharType; +typedef HANDLE PlatformFile; +const PlatformFile kInvalidPlatformFile = INVALID_HANDLE_VALUE; +#else +typedef char FilePathCharType; +typedef int PlatformFile; +const PlatformFile kInvalidPlatformFile = -1; +#endif // defined(_WIN32) + +struct HostFile { + HostFile(const FilePathCharType* file_path, + PlatformFile file, + PlatformFile sig_file) + : file_path(file_path), file(file), sig_file(sig_file) {} + + // File that is part of the host of the CDM. + const FilePathCharType* file_path = nullptr; + PlatformFile file = kInvalidPlatformFile; + + // Signature file for |file|. + PlatformFile sig_file = kInvalidPlatformFile; +}; + +} // namespace cdm + +extern "C" { + +// Functions in this file are dynamically retrieved by their versioned function +// names. Increment the version number for any backward incompatible API +// changes. + +// Verifies CDM host. All files in |host_files| are opened in read-only mode. +// +// Returns false and closes all files if there is an immediate failure. +// Otherwise returns true as soon as possible and processes the files +// asynchronously. All files MUST be closed by the CDM after this one-time +// processing is finished. +CDM_API bool VerifyCdmHost_0(const cdm::HostFile* host_files, + uint32_t num_files); +} + +#endif // CDM_CONTENT_DECRYPTION_MODULE_EXT_H_ diff --git a/dom/media/gmp/widevine-adapter/moz.build b/dom/media/gmp/widevine-adapter/moz.build new file mode 100644 index 000000000..4a3a079ce --- /dev/null +++ b/dom/media/gmp/widevine-adapter/moz.build @@ -0,0 +1,24 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# 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/. + +SOURCES += [ + 'WidevineAdapter.cpp', + 'WidevineDecryptor.cpp', + 'WidevineFileIO.cpp', + 'WidevineUtils.cpp', + 'WidevineVideoDecoder.cpp', + 'WidevineVideoFrame.cpp', +] + +FINAL_LIBRARY = 'xul' + +LOCAL_INCLUDES += [ + '/dom/media/gmp', +] + +if CONFIG['CLANG_CXX']: + CXXFLAGS += ['-Wno-error=shadow'] + +include('/ipc/chromium/chromium-config.mozbuild') diff --git a/dom/media/mediasource/TrackBuffersManager.cpp b/dom/media/mediasource/TrackBuffersManager.cpp index da21e0b39..8663c1a8f 100644 --- a/dom/media/mediasource/TrackBuffersManager.cpp +++ b/dom/media/mediasource/TrackBuffersManager.cpp @@ -58,6 +58,34 @@ AppendStateToStr(SourceBufferAttributes::AppendState aState) static Atomic<uint32_t> sStreamSourceID(0u); +#ifdef MOZ_EME +class DispatchKeyNeededEvent : public Runnable { +public: + DispatchKeyNeededEvent(AbstractMediaDecoder* aDecoder, + nsTArray<uint8_t>& aInitData, + const nsString& aInitDataType) + : mDecoder(aDecoder) + , mInitData(aInitData) + , mInitDataType(aInitDataType) + { + } + NS_IMETHOD Run() override { + // Note: Null check the owner, as the decoder could have been shutdown + // since this event was dispatched. + MediaDecoderOwner* owner = mDecoder->GetOwner(); + if (owner) { + owner->DispatchEncrypted(mInitData, mInitDataType); + } + mDecoder = nullptr; + return NS_OK; + } +private: + RefPtr<AbstractMediaDecoder> mDecoder; + nsTArray<uint8_t> mInitData; + nsString mInitDataType; +}; +#endif // MOZ_EME + TrackBuffersManager::TrackBuffersManager(MediaSourceDecoder* aParentDecoder, const nsACString& aType) : mInputBuffer(new MediaByteBuffer) @@ -1073,6 +1101,14 @@ TrackBuffersManager::OnDemuxerInitDone(nsresult) UniquePtr<EncryptionInfo> crypto = mInputDemuxer->GetCrypto(); if (crypto && crypto->IsEncrypted()) { +#ifdef MOZ_EME + // Try and dispatch 'encrypted'. Won't go if ready state still HAVE_NOTHING. + for (uint32_t i = 0; i < crypto->mInitDatas.Length(); i++) { + NS_DispatchToMainThread( + new DispatchKeyNeededEvent(mParentDecoder, crypto->mInitDatas[i].mInitData, + crypto->mInitDatas[i].mType)); + } +#endif info.mCrypto = *crypto; // We clear our crypto init data array, so the MediaFormatReader will // not emit an encrypted event for the same init data again. diff --git a/dom/media/moz.build b/dom/media/moz.build index ec36dfaf2..fb162be81 100644 --- a/dom/media/moz.build +++ b/dom/media/moz.build @@ -31,6 +31,9 @@ DIRS += [ if CONFIG['MOZ_FMP4']: DIRS += ['fmp4'] +if CONFIG['MOZ_EME']: + DIRS += ['eme'] + TEST_DIRS += [ 'gtest', ] diff --git a/dom/media/platforms/PDMFactory.cpp b/dom/media/platforms/PDMFactory.cpp index b8082d1ff..214fc80c8 100644 --- a/dom/media/platforms/PDMFactory.cpp +++ b/dom/media/platforms/PDMFactory.cpp @@ -1,4 +1,5 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ /* 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/. */ @@ -30,6 +31,11 @@ #include "AgnosticDecoderModule.h" +#ifdef MOZ_EME +#include "mozilla/CDMProxy.h" +#include "EMEDecoderModule.h" +#endif + #include "DecoderDoctorDiagnostics.h" #include "MP4Decoder.h" @@ -421,4 +427,13 @@ PDMFactory::GetDecoder(const TrackInfo& aTrackInfo, return pdm.forget(); } +#ifdef MOZ_EME +void +PDMFactory::SetCDMProxy(CDMProxy* aProxy) +{ + RefPtr<PDMFactory> m = new PDMFactory(); + mEMEPDM = new EMEDecoderModule(aProxy, m); +} +#endif + } // namespace mozilla diff --git a/dom/media/platforms/PDMFactory.h b/dom/media/platforms/PDMFactory.h index a42436477..a13c99ac0 100644 --- a/dom/media/platforms/PDMFactory.h +++ b/dom/media/platforms/PDMFactory.h @@ -1,4 +1,5 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ /* 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/. */ @@ -10,6 +11,10 @@ #include "mozilla/Function.h" #include "mozilla/StaticMutex.h" +#ifdef MOZ_EME +class CDMProxy; +#endif + namespace mozilla { class DecoderDoctorDiagnostics; @@ -35,6 +40,15 @@ public: bool Supports(const TrackInfo& aTrackInfo, DecoderDoctorDiagnostics* aDiagnostics) const; +#ifdef MOZ_EME + // Creates a PlatformDecoderModule that uses a CDMProxy to decrypt or + // decrypt-and-decode EME encrypted content. If the CDM only decrypts and + // does not decode, we create a PDM and use that to create MediaDataDecoders + // that we use on on aTaskQueue to decode the decrypted stream. + // This is called on the decode task queue. + void SetCDMProxy(CDMProxy* aProxy); +#endif + static const int kYUV400 = 0; static const int kYUV420 = 1; static const int kYUV422 = 2; diff --git a/dom/media/platforms/agnostic/eme/EMEAudioDecoder.cpp b/dom/media/platforms/agnostic/eme/EMEAudioDecoder.cpp new file mode 100644 index 000000000..25e502025 --- /dev/null +++ b/dom/media/platforms/agnostic/eme/EMEAudioDecoder.cpp @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "EMEAudioDecoder.h" +#include "mozilla/CDMProxy.h" + +namespace mozilla { + +void +EMEAudioCallbackAdapter::Error(GMPErr aErr) +{ + if (aErr == GMPNoKeyErr) { + // The GMP failed to decrypt a frame due to not having a key. This can + // happen if a key expires or a session is closed during playback. + NS_WARNING("GMP failed to decrypt due to lack of key"); + return; + } + AudioCallbackAdapter::Error(aErr); +} + +EMEAudioDecoder::EMEAudioDecoder(CDMProxy* aProxy, + const GMPAudioDecoderParams& aParams) + : GMPAudioDecoder(GMPAudioDecoderParams(aParams).WithAdapter( + new EMEAudioCallbackAdapter(aParams.mCallback))) + , mProxy(aProxy) +{} + +void +EMEAudioDecoder::InitTags(nsTArray<nsCString>& aTags) +{ + aTags.AppendElement(NS_LITERAL_CSTRING("aac")); + aTags.AppendElement(NS_ConvertUTF16toUTF8(mProxy->KeySystem())); +} + +nsCString +EMEAudioDecoder::GetNodeId() +{ + return mProxy->GetNodeId(); +} + +} // namespace mozilla diff --git a/dom/media/platforms/agnostic/eme/EMEAudioDecoder.h b/dom/media/platforms/agnostic/eme/EMEAudioDecoder.h new file mode 100644 index 000000000..c5944cf8c --- /dev/null +++ b/dom/media/platforms/agnostic/eme/EMEAudioDecoder.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 EMEAudioDecoder_h_ +#define EMEAudioDecoder_h_ + +#include "GMPAudioDecoder.h" +#include "PlatformDecoderModule.h" + +namespace mozilla { + +class EMEAudioCallbackAdapter : public AudioCallbackAdapter { +public: + explicit EMEAudioCallbackAdapter(MediaDataDecoderCallbackProxy* aCallback) + : AudioCallbackAdapter(aCallback) + {} + + void Error(GMPErr aErr) override; +}; + +class EMEAudioDecoder : public GMPAudioDecoder { +public: + EMEAudioDecoder(CDMProxy* aProxy, const GMPAudioDecoderParams& aParams); + +private: + void InitTags(nsTArray<nsCString>& aTags) override; + nsCString GetNodeId() override; + + RefPtr<CDMProxy> mProxy; +}; + +} // namespace mozilla + +#endif diff --git a/dom/media/platforms/agnostic/eme/EMEDecoderModule.cpp b/dom/media/platforms/agnostic/eme/EMEDecoderModule.cpp new file mode 100644 index 000000000..7d523d926 --- /dev/null +++ b/dom/media/platforms/agnostic/eme/EMEDecoderModule.cpp @@ -0,0 +1,307 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "EMEDecoderModule.h" +#include "EMEAudioDecoder.h" +#include "EMEVideoDecoder.h" +#include "MediaDataDecoderProxy.h" +#include "mozIGeckoMediaPluginService.h" +#include "mozilla/CDMProxy.h" +#include "mozilla/Unused.h" +#include "nsAutoPtr.h" +#include "nsServiceManagerUtils.h" +#include "MediaInfo.h" +#include "nsClassHashtable.h" +#include "GMPDecoderModule.h" +#include "MP4Decoder.h" + +namespace mozilla { + +typedef MozPromiseRequestHolder<CDMProxy::DecryptPromise> DecryptPromiseRequestHolder; + +class EMEDecryptor : public MediaDataDecoder { + +public: + + EMEDecryptor(MediaDataDecoder* aDecoder, + MediaDataDecoderCallback* aCallback, + CDMProxy* aProxy, + TaskQueue* aDecodeTaskQueue) + : mDecoder(aDecoder) + , mCallback(aCallback) + , mTaskQueue(aDecodeTaskQueue) + , mProxy(aProxy) + , mSamplesWaitingForKey(new SamplesWaitingForKey(this, this->mCallback, + mTaskQueue, mProxy)) + , mIsShutdown(false) + { + } + + RefPtr<InitPromise> Init() override { + MOZ_ASSERT(!mIsShutdown); + return mDecoder->Init(); + } + + void Input(MediaRawData* aSample) override { + MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn()); + if (mIsShutdown) { + NS_WARNING("EME encrypted sample arrived after shutdown"); + return; + } + if (mSamplesWaitingForKey->WaitIfKeyNotUsable(aSample)) { + return; + } + + nsAutoPtr<MediaRawDataWriter> writer(aSample->CreateWriter()); + mProxy->GetSessionIdsForKeyId(aSample->mCrypto.mKeyId, + writer->mCrypto.mSessionIds); + + mDecrypts.Put(aSample, new DecryptPromiseRequestHolder()); + mDecrypts.Get(aSample)->Begin(mProxy->Decrypt(aSample)->Then( + mTaskQueue, __func__, this, + &EMEDecryptor::Decrypted, + &EMEDecryptor::Decrypted)); + return; + } + + void Decrypted(const DecryptResult& aDecrypted) { + MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn()); + MOZ_ASSERT(aDecrypted.mSample); + + nsAutoPtr<DecryptPromiseRequestHolder> holder; + mDecrypts.RemoveAndForget(aDecrypted.mSample, holder); + if (holder) { + holder->Complete(); + } else { + // Decryption is not in the list of decrypt operations waiting + // for a result. It must have been flushed or drained. Ignore result. + return; + } + + if (mIsShutdown) { + NS_WARNING("EME decrypted sample arrived after shutdown"); + return; + } + + if (aDecrypted.mStatus == NoKeyErr) { + // Key became unusable after we sent the sample to CDM to decrypt. + // Call Input() again, so that the sample is enqueued for decryption + // if the key becomes usable again. + Input(aDecrypted.mSample); + } else if (aDecrypted.mStatus != Ok) { + if (mCallback) { + mCallback->Error(MediaResult( + NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("decrypted.mStatus=%u", uint32_t(aDecrypted.mStatus)))); + } + } else { + MOZ_ASSERT(!mIsShutdown); + // The Adobe GMP AAC decoder gets confused if we pass it non-encrypted + // samples with valid crypto data. So clear the crypto data, since the + // sample should be decrypted now anyway. If we don't do this and we're + // using the Adobe GMP for unencrypted decoding of data that is decrypted + // by gmp-clearkey, decoding will fail. + UniquePtr<MediaRawDataWriter> writer(aDecrypted.mSample->CreateWriter()); + writer->mCrypto = CryptoSample(); + mDecoder->Input(aDecrypted.mSample); + } + } + + void Flush() override { + MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn()); + MOZ_ASSERT(!mIsShutdown); + for (auto iter = mDecrypts.Iter(); !iter.Done(); iter.Next()) { + nsAutoPtr<DecryptPromiseRequestHolder>& holder = iter.Data(); + holder->DisconnectIfExists(); + iter.Remove(); + } + mDecoder->Flush(); + mSamplesWaitingForKey->Flush(); + } + + void Drain() override { + MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn()); + MOZ_ASSERT(!mIsShutdown); + for (auto iter = mDecrypts.Iter(); !iter.Done(); iter.Next()) { + nsAutoPtr<DecryptPromiseRequestHolder>& holder = iter.Data(); + holder->DisconnectIfExists(); + iter.Remove(); + } + mDecoder->Drain(); + } + + void Shutdown() override { + MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn()); + MOZ_ASSERT(!mIsShutdown); + mIsShutdown = true; + mDecoder->Shutdown(); + mSamplesWaitingForKey->BreakCycles(); + mSamplesWaitingForKey = nullptr; + mDecoder = nullptr; + mProxy = nullptr; + mCallback = nullptr; + } + + const char* GetDescriptionName() const override { + return mDecoder->GetDescriptionName(); + } + +private: + + RefPtr<MediaDataDecoder> mDecoder; + MediaDataDecoderCallback* mCallback; + RefPtr<TaskQueue> mTaskQueue; + RefPtr<CDMProxy> mProxy; + nsClassHashtable<nsRefPtrHashKey<MediaRawData>, DecryptPromiseRequestHolder> mDecrypts; + RefPtr<SamplesWaitingForKey> mSamplesWaitingForKey; + bool mIsShutdown; +}; + +class EMEMediaDataDecoderProxy : public MediaDataDecoderProxy { +public: + EMEMediaDataDecoderProxy(already_AddRefed<AbstractThread> aProxyThread, + MediaDataDecoderCallback* aCallback, + CDMProxy* aProxy, + TaskQueue* aTaskQueue) + : MediaDataDecoderProxy(Move(aProxyThread), aCallback) + , mSamplesWaitingForKey(new SamplesWaitingForKey(this, aCallback, + aTaskQueue, aProxy)) + , mProxy(aProxy) + { + } + + void Input(MediaRawData* aSample) override; + void Shutdown() override; + +private: + RefPtr<SamplesWaitingForKey> mSamplesWaitingForKey; + RefPtr<CDMProxy> mProxy; +}; + +void +EMEMediaDataDecoderProxy::Input(MediaRawData* aSample) +{ + if (mSamplesWaitingForKey->WaitIfKeyNotUsable(aSample)) { + return; + } + + nsAutoPtr<MediaRawDataWriter> writer(aSample->CreateWriter()); + mProxy->GetSessionIdsForKeyId(aSample->mCrypto.mKeyId, + writer->mCrypto.mSessionIds); + + MediaDataDecoderProxy::Input(aSample); +} + +void +EMEMediaDataDecoderProxy::Shutdown() +{ + MediaDataDecoderProxy::Shutdown(); + + mSamplesWaitingForKey->BreakCycles(); + mSamplesWaitingForKey = nullptr; + mProxy = nullptr; +} + +EMEDecoderModule::EMEDecoderModule(CDMProxy* aProxy, PDMFactory* aPDM) + : mProxy(aProxy) + , mPDM(aPDM) +{ +} + +EMEDecoderModule::~EMEDecoderModule() +{ +} + +static already_AddRefed<MediaDataDecoderProxy> +CreateDecoderWrapper(MediaDataDecoderCallback* aCallback, CDMProxy* aProxy, TaskQueue* aTaskQueue) +{ + RefPtr<gmp::GeckoMediaPluginService> s(gmp::GeckoMediaPluginService::GetGeckoMediaPluginService()); + if (!s) { + return nullptr; + } + RefPtr<AbstractThread> thread(s->GetAbstractGMPThread()); + if (!thread) { + return nullptr; + } + RefPtr<MediaDataDecoderProxy> decoder( + new EMEMediaDataDecoderProxy(thread.forget(), aCallback, aProxy, aTaskQueue)); + return decoder.forget(); +} + +already_AddRefed<MediaDataDecoder> +EMEDecoderModule::CreateVideoDecoder(const CreateDecoderParams& aParams) +{ + MOZ_ASSERT(aParams.mConfig.mCrypto.mValid); + + if (SupportsMimeType(aParams.mConfig.mMimeType, nullptr)) { + // GMP decodes. Assume that means it can decrypt too. + RefPtr<MediaDataDecoderProxy> wrapper = + CreateDecoderWrapper(aParams.mCallback, mProxy, aParams.mTaskQueue); + auto params = GMPVideoDecoderParams(aParams).WithCallback(wrapper); + wrapper->SetProxyTarget(new EMEVideoDecoder(mProxy, params)); + return wrapper.forget(); + } + + MOZ_ASSERT(mPDM); + RefPtr<MediaDataDecoder> decoder(mPDM->CreateDecoder(aParams)); + if (!decoder) { + return nullptr; + } + + RefPtr<MediaDataDecoder> emeDecoder(new EMEDecryptor(decoder, + aParams.mCallback, + mProxy, + AbstractThread::GetCurrent()->AsTaskQueue())); + return emeDecoder.forget(); +} + +already_AddRefed<MediaDataDecoder> +EMEDecoderModule::CreateAudioDecoder(const CreateDecoderParams& aParams) +{ + MOZ_ASSERT(aParams.mConfig.mCrypto.mValid); + + if (SupportsMimeType(aParams.mConfig.mMimeType, nullptr)) { + // GMP decodes. Assume that means it can decrypt too. + RefPtr<MediaDataDecoderProxy> wrapper = + CreateDecoderWrapper(aParams.mCallback, mProxy, aParams.mTaskQueue); + auto gmpParams = GMPAudioDecoderParams(aParams).WithCallback(wrapper); + wrapper->SetProxyTarget(new EMEAudioDecoder(mProxy, gmpParams)); + return wrapper.forget(); + } + + MOZ_ASSERT(mPDM); + RefPtr<MediaDataDecoder> decoder(mPDM->CreateDecoder(aParams)); + if (!decoder) { + return nullptr; + } + + RefPtr<MediaDataDecoder> emeDecoder(new EMEDecryptor(decoder, + aParams.mCallback, + mProxy, + AbstractThread::GetCurrent()->AsTaskQueue())); + return emeDecoder.forget(); +} + +PlatformDecoderModule::ConversionRequired +EMEDecoderModule::DecoderNeedsConversion(const TrackInfo& aConfig) const +{ + if (aConfig.IsVideo() && MP4Decoder::IsH264(aConfig.mMimeType)) { + return ConversionRequired::kNeedAVCC; + } else { + return ConversionRequired::kNeedNone; + } +} + +bool +EMEDecoderModule::SupportsMimeType(const nsACString& aMimeType, + DecoderDoctorDiagnostics* aDiagnostics) const +{ + Maybe<nsCString> gmp; + gmp.emplace(NS_ConvertUTF16toUTF8(mProxy->KeySystem())); + return GMPDecoderModule::SupportsMimeType(aMimeType, gmp); +} + +} // namespace mozilla diff --git a/dom/media/platforms/agnostic/eme/EMEDecoderModule.h b/dom/media/platforms/agnostic/eme/EMEDecoderModule.h new file mode 100644 index 000000000..32074ae8c --- /dev/null +++ b/dom/media/platforms/agnostic/eme/EMEDecoderModule.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#if !defined(EMEDecoderModule_h_) +#define EMEDecoderModule_h_ + +#include "PlatformDecoderModule.h" +#include "PDMFactory.h" +#include "gmp-decryption.h" + +namespace mozilla { + +class CDMProxy; + +class EMEDecoderModule : public PlatformDecoderModule { +private: + +public: + EMEDecoderModule(CDMProxy* aProxy, PDMFactory* aPDM); + + virtual ~EMEDecoderModule(); + +protected: + // Decode thread. + already_AddRefed<MediaDataDecoder> + CreateVideoDecoder(const CreateDecoderParams& aParams) override; + + // Decode thread. + already_AddRefed<MediaDataDecoder> + CreateAudioDecoder(const CreateDecoderParams& aParams) override; + + ConversionRequired + DecoderNeedsConversion(const TrackInfo& aConfig) const override; + + bool + SupportsMimeType(const nsACString& aMimeType, + DecoderDoctorDiagnostics* aDiagnostics) const override; + +private: + RefPtr<CDMProxy> mProxy; + // Will be null if CDM has decoding capability. + RefPtr<PDMFactory> mPDM; + // We run the PDM on its own task queue. + RefPtr<TaskQueue> mTaskQueue; +}; + +} // namespace mozilla + +#endif // EMEDecoderModule_h_ diff --git a/dom/media/platforms/agnostic/eme/EMEVideoDecoder.cpp b/dom/media/platforms/agnostic/eme/EMEVideoDecoder.cpp new file mode 100644 index 000000000..bddb89660 --- /dev/null +++ b/dom/media/platforms/agnostic/eme/EMEVideoDecoder.cpp @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "EMEVideoDecoder.h" +#include "GMPVideoEncodedFrameImpl.h" +#include "mozilla/CDMProxy.h" +#include "MediaData.h" +#include "MP4Decoder.h" +#include "VPXDecoder.h" + +namespace mozilla { + +void +EMEVideoCallbackAdapter::Error(GMPErr aErr) +{ + if (aErr == GMPNoKeyErr) { + // The GMP failed to decrypt a frame due to not having a key. This can + // happen if a key expires or a session is closed during playback. + NS_WARNING("GMP failed to decrypt due to lack of key"); + return; + } + VideoCallbackAdapter::Error(aErr); +} + +EMEVideoDecoder::EMEVideoDecoder(CDMProxy* aProxy, + const GMPVideoDecoderParams& aParams) + : GMPVideoDecoder(GMPVideoDecoderParams(aParams).WithAdapter( + new EMEVideoCallbackAdapter(aParams.mCallback, + VideoInfo(aParams.mConfig.mDisplay), + aParams.mImageContainer))) + , mProxy(aProxy) + , mDecryptorId(aProxy->GetDecryptorId()) +{} + +void +EMEVideoDecoder::InitTags(nsTArray<nsCString>& aTags) +{ + VideoInfo config = GetConfig(); + if (MP4Decoder::IsH264(config.mMimeType)) { + aTags.AppendElement(NS_LITERAL_CSTRING("h264")); + } else if (VPXDecoder::IsVP8(config.mMimeType)) { + aTags.AppendElement(NS_LITERAL_CSTRING("vp8")); + } else if (VPXDecoder::IsVP9(config.mMimeType)) { + aTags.AppendElement(NS_LITERAL_CSTRING("vp9")); + } + aTags.AppendElement(NS_ConvertUTF16toUTF8(mProxy->KeySystem())); +} + +nsCString +EMEVideoDecoder::GetNodeId() +{ + return mProxy->GetNodeId(); +} + +GMPUniquePtr<GMPVideoEncodedFrame> +EMEVideoDecoder::CreateFrame(MediaRawData* aSample) +{ + GMPUniquePtr<GMPVideoEncodedFrame> frame = GMPVideoDecoder::CreateFrame(aSample); + if (frame && aSample->mCrypto.mValid) { + static_cast<gmp::GMPVideoEncodedFrameImpl*>(frame.get())->InitCrypto(aSample->mCrypto); + } + return frame; +} + +} // namespace mozilla diff --git a/dom/media/platforms/agnostic/eme/EMEVideoDecoder.h b/dom/media/platforms/agnostic/eme/EMEVideoDecoder.h new file mode 100644 index 000000000..a0f23f867 --- /dev/null +++ b/dom/media/platforms/agnostic/eme/EMEVideoDecoder.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 EMEVideoDecoder_h_ +#define EMEVideoDecoder_h_ + +#include "GMPVideoDecoder.h" +#include "PlatformDecoderModule.h" + +namespace mozilla { + +class CDMProxy; +class TaskQueue; + +class EMEVideoCallbackAdapter : public VideoCallbackAdapter { +public: + EMEVideoCallbackAdapter(MediaDataDecoderCallbackProxy* aCallback, + VideoInfo aVideoInfo, + layers::ImageContainer* aImageContainer) + : VideoCallbackAdapter(aCallback, aVideoInfo, aImageContainer) + {} + + void Error(GMPErr aErr) override; +}; + +class EMEVideoDecoder : public GMPVideoDecoder { +public: + EMEVideoDecoder(CDMProxy* aProxy, const GMPVideoDecoderParams& aParams); + +private: + void InitTags(nsTArray<nsCString>& aTags) override; + nsCString GetNodeId() override; + uint32_t DecryptorId() const override { return mDecryptorId; } + GMPUniquePtr<GMPVideoEncodedFrame> CreateFrame(MediaRawData* aSample) override; + + RefPtr<CDMProxy> mProxy; + uint32_t mDecryptorId; +}; + +} // namespace mozilla + +#endif // EMEVideoDecoder_h_ diff --git a/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.cpp b/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.cpp new file mode 100644 index 000000000..532fc63c0 --- /dev/null +++ b/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.cpp @@ -0,0 +1,85 @@ +/* -*- Mode: C++; tab-width: 8; 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 "SamplesWaitingForKey.h" +#include "mozilla/CDMProxy.h" +#include "mozilla/CDMCaps.h" +#include "MediaData.h" + +namespace mozilla { + +SamplesWaitingForKey::SamplesWaitingForKey(MediaDataDecoder* aDecoder, + MediaDataDecoderCallback* aCallback, + TaskQueue* aTaskQueue, + CDMProxy* aProxy) + : mMutex("SamplesWaitingForKey") + , mDecoder(aDecoder) + , mDecoderCallback(aCallback) + , mTaskQueue(aTaskQueue) + , mProxy(aProxy) +{ +} + +SamplesWaitingForKey::~SamplesWaitingForKey() +{ +} + +bool +SamplesWaitingForKey::WaitIfKeyNotUsable(MediaRawData* aSample) +{ + if (!aSample || !aSample->mCrypto.mValid || !mProxy) { + return false; + } + CDMCaps::AutoLock caps(mProxy->Capabilites()); + const auto& keyid = aSample->mCrypto.mKeyId; + if (!caps.IsKeyUsable(keyid)) { + { + MutexAutoLock lock(mMutex); + mSamples.AppendElement(aSample); + } + mDecoderCallback->WaitingForKey(); + caps.NotifyWhenKeyIdUsable(aSample->mCrypto.mKeyId, this); + return true; + } + return false; +} + +void +SamplesWaitingForKey::NotifyUsable(const CencKeyId& aKeyId) +{ + MutexAutoLock lock(mMutex); + size_t i = 0; + while (i < mSamples.Length()) { + if (aKeyId == mSamples[i]->mCrypto.mKeyId) { + RefPtr<nsIRunnable> task; + task = NewRunnableMethod<RefPtr<MediaRawData>>(mDecoder, + &MediaDataDecoder::Input, + RefPtr<MediaRawData>(mSamples[i])); + mSamples.RemoveElementAt(i); + mTaskQueue->Dispatch(task.forget()); + } else { + i++; + } + } +} + +void +SamplesWaitingForKey::Flush() +{ + MutexAutoLock lock(mMutex); + mSamples.Clear(); +} + +void +SamplesWaitingForKey::BreakCycles() +{ + MutexAutoLock lock(mMutex); + mDecoder = nullptr; + mTaskQueue = nullptr; + mProxy = nullptr; + mSamples.Clear(); +} + +} // namespace mozilla diff --git a/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.h b/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.h new file mode 100644 index 000000000..65bb14403 --- /dev/null +++ b/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.h @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 SamplesWaitingForKey_h_ +#define SamplesWaitingForKey_h_ + +#include "mozilla/TaskQueue.h" + +#include "PlatformDecoderModule.h" + +namespace mozilla { + +typedef nsTArray<uint8_t> CencKeyId; + +class CDMProxy; + +// Encapsulates the task of waiting for the CDMProxy to have the necessary +// keys to decrypt a given sample. +class SamplesWaitingForKey { +public: + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SamplesWaitingForKey) + + explicit SamplesWaitingForKey(MediaDataDecoder* aDecoder, + MediaDataDecoderCallback* aCallback, + TaskQueue* aTaskQueue, + CDMProxy* aProxy); + + // Returns true if we need to wait for a key to become usable. + // Will callback MediaDataDecoder::Input(aSample) on mDecoder once the + // sample is ready to be decrypted. The order of input samples is + // preserved. + bool WaitIfKeyNotUsable(MediaRawData* aSample); + + void NotifyUsable(const CencKeyId& aKeyId); + + void Flush(); + + void BreakCycles(); + +protected: + ~SamplesWaitingForKey(); + +private: + Mutex mMutex; + RefPtr<MediaDataDecoder> mDecoder; + MediaDataDecoderCallback* mDecoderCallback; + RefPtr<TaskQueue> mTaskQueue; + RefPtr<CDMProxy> mProxy; + nsTArray<RefPtr<MediaRawData>> mSamples; +}; + +} // namespace mozilla + +#endif // SamplesWaitingForKey_h_ diff --git a/dom/media/platforms/agnostic/eme/moz.build b/dom/media/platforms/agnostic/eme/moz.build new file mode 100644 index 000000000..97156f33a --- /dev/null +++ b/dom/media/platforms/agnostic/eme/moz.build @@ -0,0 +1,22 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# 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 += [ + 'EMEAudioDecoder.h', + 'EMEDecoderModule.h', + 'EMEVideoDecoder.h', + 'SamplesWaitingForKey.h', +] + +SOURCES += [ + 'EMEAudioDecoder.cpp', + 'EMEDecoderModule.cpp', + 'EMEVideoDecoder.cpp', + 'SamplesWaitingForKey.cpp', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' diff --git a/dom/media/platforms/moz.build b/dom/media/platforms/moz.build index 5da113e77..2d295bb1e 100644 --- a/dom/media/platforms/moz.build +++ b/dom/media/platforms/moz.build @@ -33,6 +33,9 @@ DIRS += [ 'omx' ] +if CONFIG['MOZ_EME']: + DIRS += ['agnostic/eme'] + if CONFIG['MOZ_WMF']: DIRS += [ 'wmf' ]; diff --git a/dom/media/webaudio/AudioContext.cpp b/dom/media/webaudio/AudioContext.cpp index 9f807d01f..3c7958349 100755 --- a/dom/media/webaudio/AudioContext.cpp +++ b/dom/media/webaudio/AudioContext.cpp @@ -1,4 +1,5 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ /* 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/. */ @@ -370,6 +371,13 @@ AudioContext::CreateMediaElementSource(HTMLMediaElement& aMediaElement, return nullptr; } +#ifdef MOZ_EME + if (aMediaElement.ContainsRestrictedContent()) { + aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return nullptr; + } +#endif + if (CheckClosed(aRv)) { return nullptr; } diff --git a/dom/webidl/HTMLMediaElement.webidl b/dom/webidl/HTMLMediaElement.webidl index 7f0f9f857..ad31f38cb 100644 --- a/dom/webidl/HTMLMediaElement.webidl +++ b/dom/webidl/HTMLMediaElement.webidl @@ -153,6 +153,24 @@ partial interface HTMLMediaElement { attribute EventHandler onmozinterruptend; }; +#ifdef MOZ_EME +// Encrypted Media Extensions +partial interface HTMLMediaElement { + [Pref="media.eme.apiVisible"] + readonly attribute MediaKeys? mediaKeys; + + // void, not any: https://www.w3.org/Bugs/Public/show_bug.cgi?id=26457 + [Pref="media.eme.apiVisible", NewObject] + Promise<void> setMediaKeys(MediaKeys? mediaKeys); + + [Pref="media.eme.apiVisible"] + attribute EventHandler onencrypted; + + [Pref="media.eme.apiVisible"] + attribute EventHandler onwaitingforkey; +}; +#endif + // This is just for testing partial interface HTMLMediaElement { [Pref="media.useAudioChannelService.testing"] diff --git a/dom/webidl/MediaEncryptedEvent.webidl b/dom/webidl/MediaEncryptedEvent.webidl new file mode 100644 index 000000000..28d7a17b7 --- /dev/null +++ b/dom/webidl/MediaEncryptedEvent.webidl @@ -0,0 +1,23 @@ +/* -*- Mode: IDL; 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/. + * + * The origin of this IDL file is + * https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html + * + * Copyright © 2014 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. + * W3C liability, trademark and document use rules apply. + */ + +[Pref="media.eme.apiVisible", Constructor(DOMString type, optional MediaKeyNeededEventInit eventInitDict)] +interface MediaEncryptedEvent : Event { + readonly attribute DOMString initDataType; + [Throws] + readonly attribute ArrayBuffer? initData; +}; + +dictionary MediaKeyNeededEventInit : EventInit { + DOMString initDataType = ""; + ArrayBuffer? initData = null; +}; diff --git a/dom/webidl/MediaKeyError.webidl b/dom/webidl/MediaKeyError.webidl new file mode 100644 index 000000000..d0dde2032 --- /dev/null +++ b/dom/webidl/MediaKeyError.webidl @@ -0,0 +1,19 @@ +/* -*- Mode: IDL; 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/. + * + * The origin of this IDL file is + * https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html + * + * Copyright © 2014 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. + * W3C liability, trademark and document use rules apply. + */ + +// According to the spec, "The future of error events and MediaKeyError +// is uncertain." +// https://www.w3.org/Bugs/Public/show_bug.cgi?id=21798 +[Pref="media.eme.apiVisible"] +interface MediaKeyError : Event { + readonly attribute unsigned long systemCode; +}; diff --git a/dom/webidl/MediaKeyMessageEvent.webidl b/dom/webidl/MediaKeyMessageEvent.webidl new file mode 100644 index 000000000..057924bb7 --- /dev/null +++ b/dom/webidl/MediaKeyMessageEvent.webidl @@ -0,0 +1,30 @@ +/* -*- Mode: IDL; 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/. + * + * The origin of this IDL file is + * https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html + * + * Copyright © 2014 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. + * W3C liability, trademark and document use rules apply. + */ + +enum MediaKeyMessageType { + "license-request", + "license-renewal", + "license-release", + "individualization-request" +}; + +[Pref="media.eme.apiVisible", Constructor(DOMString type, MediaKeyMessageEventInit eventInitDict)] +interface MediaKeyMessageEvent : Event { + readonly attribute MediaKeyMessageType messageType; + [Throws] + readonly attribute ArrayBuffer message; +}; + +dictionary MediaKeyMessageEventInit : EventInit { + required MediaKeyMessageType messageType; + required ArrayBuffer message; +}; diff --git a/dom/webidl/MediaKeySession.webidl b/dom/webidl/MediaKeySession.webidl new file mode 100644 index 000000000..8ca5745c4 --- /dev/null +++ b/dom/webidl/MediaKeySession.webidl @@ -0,0 +1,47 @@ +/* -*- Mode: IDL; 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/. + * + * The origin of this IDL file is + * https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html + * + * Copyright © 2014 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. + * W3C liability, trademark and document use rules apply. + */ + +[Pref="media.eme.apiVisible"] +interface MediaKeySession : EventTarget { + // error state + readonly attribute MediaKeyError? error; + + // session properties + readonly attribute DOMString keySystem; + readonly attribute DOMString sessionId; + + readonly attribute unrestricted double expiration; + + readonly attribute Promise<void> closed; + + readonly attribute MediaKeyStatusMap keyStatuses; + + attribute EventHandler onkeystatuseschange; + + attribute EventHandler onmessage; + + [NewObject] + Promise<void> generateRequest(DOMString initDataType, BufferSource initData); + + [NewObject] + Promise<boolean> load(DOMString sessionId); + + // session operations + [NewObject] + Promise<void> update(BufferSource response); + + [NewObject] + Promise<void> close(); + + [NewObject] + Promise<void> remove(); +}; diff --git a/dom/webidl/MediaKeyStatusMap.webidl b/dom/webidl/MediaKeyStatusMap.webidl new file mode 100644 index 000000000..1f34b5dc7 --- /dev/null +++ b/dom/webidl/MediaKeyStatusMap.webidl @@ -0,0 +1,30 @@ +/* -*- Mode: IDL; 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/. + * + * The origin of this IDL file is + * https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html + * + * Copyright © 2014 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. + * W3C liability, trademark and document use rules apply. + */ + +enum MediaKeyStatus { + "usable", + "expired", + "released", + "output-restricted", + "output-downscaled", + "status-pending", + "internal-error" +}; + +[Pref="media.eme.apiVisible"] +interface MediaKeyStatusMap { + iterable<ArrayBuffer,MediaKeyStatus>; + readonly attribute unsigned long size; + boolean has (BufferSource keyId); + [Throws] + any get (BufferSource keyId); +}; diff --git a/dom/webidl/MediaKeySystemAccess.webidl b/dom/webidl/MediaKeySystemAccess.webidl new file mode 100644 index 000000000..01552e449 --- /dev/null +++ b/dom/webidl/MediaKeySystemAccess.webidl @@ -0,0 +1,41 @@ +/* -*- Mode: IDL; 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/. + * + * The origin of this IDL file is + * https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html + * + * Copyright © 2014 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. + * W3C liability, trademark and document use rules apply. + */ + +enum MediaKeysRequirement { + "required", + "optional", + "not-allowed" +}; + +dictionary MediaKeySystemMediaCapability { + DOMString contentType = ""; + DOMString robustness = ""; +}; + +dictionary MediaKeySystemConfiguration { + DOMString label = ""; + sequence<DOMString> initDataTypes = []; + sequence<MediaKeySystemMediaCapability> audioCapabilities = []; + sequence<MediaKeySystemMediaCapability> videoCapabilities = []; + MediaKeysRequirement distinctiveIdentifier = "optional"; + MediaKeysRequirement persistentState = "optional"; + sequence<DOMString> sessionTypes; +}; + +[Pref="media.eme.apiVisible"] +interface MediaKeySystemAccess { + readonly attribute DOMString keySystem; + [NewObject] + MediaKeySystemConfiguration getConfiguration(); + [NewObject] + Promise<MediaKeys> createMediaKeys(); +}; diff --git a/dom/webidl/MediaKeys.webidl b/dom/webidl/MediaKeys.webidl new file mode 100644 index 000000000..cb84cdab6 --- /dev/null +++ b/dom/webidl/MediaKeys.webidl @@ -0,0 +1,30 @@ +/* -*- Mode: IDL; 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/. + * + * The origin of this IDL file is + * https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html + * + * Copyright © 2014 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. + * W3C liability, trademark and document use rules apply. + */ + +// Note: "persistent-usage-record" session type is unsupported yet, as +// it's marked as "at risk" in the spec, and Chrome doesn't support it. +enum MediaKeySessionType { + "temporary", + "persistent-license", + // persistent-usage-record, +}; + +[Pref="media.eme.apiVisible"] +interface MediaKeys { + readonly attribute DOMString keySystem; + + [NewObject, Throws] + MediaKeySession createSession(optional MediaKeySessionType sessionType = "temporary"); + + [NewObject] + Promise<void> setServerCertificate((ArrayBufferView or ArrayBuffer) serverCertificate); +}; diff --git a/dom/webidl/MediaKeysRequestStatus.webidl b/dom/webidl/MediaKeysRequestStatus.webidl new file mode 100644 index 000000000..737372f66 --- /dev/null +++ b/dom/webidl/MediaKeysRequestStatus.webidl @@ -0,0 +1,23 @@ +/* -*- Mode: IDL; 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/. + */ + +enum MediaKeySystemStatus { + "available", + "api-disabled", + "cdm-disabled", + "cdm-not-supported", + "cdm-not-installed", + "cdm-created", +}; + +/* Note: This dictionary and enum is only used by Gecko to convey messages + * to chrome JS code. It is not exposed to the web. + */ + +dictionary RequestMediaKeySystemAccessNotification { + required DOMString keySystem; + required MediaKeySystemStatus status; +}; diff --git a/dom/webidl/WidevineCDMManifest.webidl b/dom/webidl/WidevineCDMManifest.webidl new file mode 100644 index 000000000..83e14e0b0 --- /dev/null +++ b/dom/webidl/WidevineCDMManifest.webidl @@ -0,0 +1,15 @@ +/* -*- Mode: IDL; 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/.
+ */
+
+dictionary WidevineCDMManifest {
+ required DOMString name;
+ required DOMString description;
+ required DOMString version;
+ required DOMString x-cdm-module-versions;
+ required DOMString x-cdm-interface-versions;
+ required DOMString x-cdm-host-versions;
+ required DOMString x-cdm-codecs;
+};
diff --git a/dom/webidl/moz.build b/dom/webidl/moz.build index fca26152b..02e903849 100644 --- a/dom/webidl/moz.build +++ b/dom/webidl/moz.build @@ -572,6 +572,19 @@ WEBIDL_FILES = [ 'XULElement.webidl', ] +if CONFIG['MOZ_EME']: + WEBIDL_FILES += [ + 'MediaEncryptedEvent.webidl', + 'MediaKeyError.webidl', + 'MediaKeyMessageEvent.webidl', + 'MediaKeys.webidl', + 'MediaKeySession.webidl', + 'MediaKeysRequestStatus.webidl', + 'MediaKeyStatusMap.webidl', + 'MediaKeySystemAccess.webidl', + 'WidevineCDMManifest.webidl', + ] + if CONFIG['MOZ_AUDIO_CHANNEL_MANAGER']: WEBIDL_FILES += [ 'AudioChannelManager.webidl', |