/* 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 #ifdef XP_MACOSX #include "nsCocoaFeatures.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& aConfigs) { Request(aPromise, aKeySystem, aConfigs, RequestType::Initial); } void MediaKeySystemAccessManager::Request(DetailedPromise* aPromise, const nsAString& aKeySystem, const Sequence& 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) && !IsPrimetimeKeySystem(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 && (IsPrimetimeKeySystem(aKeySystem) || 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 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& 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& aConfigs) { EME_LOG("MediaKeySystemAccessManager::AwaitInstall %s", NS_ConvertUTF16toUTF8(aKeySystem).get()); if (!EnsureObserversAdded()) { NS_WARNING("Failed to add pref observer"); return false; } nsCOMPtr 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 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 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 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 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 obsService = mozilla::services::GetObserverService(); if (obsService) { obsService->RemoveObserver(this, "gmp-changed"); mAddedObservers = false; } } } } // namespace dom } // namespace mozilla