1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 
5 #include "MediaKeySystemAccessManager.h"
6 #include "DecoderDoctorDiagnostics.h"
7 #include "MediaPrefs.h"
8 #include "mozilla/EMEUtils.h"
9 #include "nsServiceManagerUtils.h"
10 #include "nsComponentManagerUtils.h"
11 #include "nsIObserverService.h"
12 #include "mozilla/Services.h"
13 #include "mozilla/DetailedPromise.h"
14 #ifdef XP_WIN
15 #include "mozilla/WindowsVersion.h"
16 #endif
17 #ifdef XP_MACOSX
18 #include "nsCocoaFeatures.h"
19 #endif
20 #include "nsPrintfCString.h"
21 #include "nsContentUtils.h"
22 #include "nsIScriptError.h"
23 #include "mozilla/Unused.h"
24 #include "nsDataHashtable.h"
25 
26 namespace mozilla {
27 namespace dom {
28 
29 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeySystemAccessManager)
30   NS_INTERFACE_MAP_ENTRY(nsISupports)
31   NS_INTERFACE_MAP_ENTRY(nsIObserver)
32 NS_INTERFACE_MAP_END
33 
34 NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeySystemAccessManager)
35 NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeySystemAccessManager)
36 
37 NS_IMPL_CYCLE_COLLECTION_CLASS(MediaKeySystemAccessManager)
38 
39 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaKeySystemAccessManager)
40   NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow)
41   for (size_t i = 0; i < tmp->mRequests.Length(); i++) {
42     tmp->mRequests[i].RejectPromise(NS_LITERAL_CSTRING(
43         "Promise still outstanding at MediaKeySystemAccessManager GC"));
44     tmp->mRequests[i].CancelTimer();
45     NS_IMPL_CYCLE_COLLECTION_UNLINK(mRequests[i].mPromise)
46   }
47   tmp->mRequests.Clear();
48 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
49 
50 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MediaKeySystemAccessManager)
51   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow)
52   for (size_t i = 0; i < tmp->mRequests.Length(); i++) {
53     NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRequests[i].mPromise)
54   }
55 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
56 
MediaKeySystemAccessManager(nsPIDOMWindowInner * aWindow)57 MediaKeySystemAccessManager::MediaKeySystemAccessManager(
58     nsPIDOMWindowInner* aWindow)
59     : mWindow(aWindow), mAddedObservers(false) {}
60 
~MediaKeySystemAccessManager()61 MediaKeySystemAccessManager::~MediaKeySystemAccessManager() { Shutdown(); }
62 
Request(DetailedPromise * aPromise,const nsAString & aKeySystem,const Sequence<MediaKeySystemConfiguration> & aConfigs)63 void MediaKeySystemAccessManager::Request(
64     DetailedPromise* aPromise, const nsAString& aKeySystem,
65     const Sequence<MediaKeySystemConfiguration>& aConfigs) {
66   Request(aPromise, aKeySystem, aConfigs, RequestType::Initial);
67 }
68 
Request(DetailedPromise * aPromise,const nsAString & aKeySystem,const Sequence<MediaKeySystemConfiguration> & aConfigs,RequestType aType)69 void MediaKeySystemAccessManager::Request(
70     DetailedPromise* aPromise, const nsAString& aKeySystem,
71     const Sequence<MediaKeySystemConfiguration>& aConfigs, RequestType aType) {
72   EME_LOG("MediaKeySystemAccessManager::Request %s",
73           NS_ConvertUTF16toUTF8(aKeySystem).get());
74 
75   if (aKeySystem.IsEmpty()) {
76     aPromise->MaybeReject(NS_ERROR_DOM_TYPE_ERR,
77                           NS_LITERAL_CSTRING("Key system string is empty"));
78     // Don't notify DecoderDoctor, as there's nothing we or the user can
79     // do to fix this situation; the site is using the API wrong.
80     return;
81   }
82   if (aConfigs.IsEmpty()) {
83     aPromise->MaybeReject(
84         NS_ERROR_DOM_TYPE_ERR,
85         NS_LITERAL_CSTRING("Candidate MediaKeySystemConfigs is empty"));
86     // Don't notify DecoderDoctor, as there's nothing we or the user can
87     // do to fix this situation; the site is using the API wrong.
88     return;
89   }
90 
91   DecoderDoctorDiagnostics diagnostics;
92 
93   // Ensure keysystem is supported.
94   if (!IsWidevineKeySystem(aKeySystem) && !IsClearkeyKeySystem(aKeySystem)) {
95     // Not to inform user, because nothing to do if the keySystem is not
96     // supported.
97     aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR,
98                           NS_LITERAL_CSTRING("Key system is unsupported"));
99     diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), aKeySystem,
100                                           false, __func__);
101     return;
102   }
103 
104   if (!MediaPrefs::EMEEnabled() && !IsClearkeyKeySystem(aKeySystem)) {
105     // EME disabled by user, send notification to chrome so UI can inform user.
106     // Clearkey is allowed even when EME is disabled because we want the pref
107     // "media.eme.enabled" only taking effect on proprietary DRMs.
108     MediaKeySystemAccess::NotifyObservers(mWindow, aKeySystem,
109                                           MediaKeySystemStatus::Api_disabled);
110     aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR,
111                           NS_LITERAL_CSTRING("EME has been preffed off"));
112     diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), aKeySystem,
113                                           false, __func__);
114     return;
115   }
116 
117   nsAutoCString message;
118   MediaKeySystemStatus status =
119       MediaKeySystemAccess::GetKeySystemStatus(aKeySystem, message);
120 
121   nsPrintfCString msg(
122       "MediaKeySystemAccess::GetKeySystemStatus(%s) "
123       "result=%s msg='%s'",
124       NS_ConvertUTF16toUTF8(aKeySystem).get(),
125       MediaKeySystemStatusValues::strings[(size_t)status].value, message.get());
126   LogToBrowserConsole(NS_ConvertUTF8toUTF16(msg));
127 
128   if (status == MediaKeySystemStatus::Cdm_not_installed &&
129       IsWidevineKeySystem(aKeySystem)) {
130     // These are cases which could be resolved by downloading a new(er) CDM.
131     // When we send the status to chrome, chrome's GMPProvider will attempt to
132     // download or update the CDM. In AwaitInstall() we add listeners to wait
133     // for the update to complete, and we'll call this function again with
134     // aType==Subsequent once the download has completed and the GMPService
135     // has had a new plugin added. AwaitInstall() sets a timer to fail if the
136     // update/download takes too long or fails.
137     if (aType == RequestType::Initial &&
138         AwaitInstall(aPromise, aKeySystem, aConfigs)) {
139       // Notify chrome that we're going to wait for the CDM to download/update.
140       // Note: If we're re-trying, we don't re-send the notification,
141       // as chrome is already displaying the "we can't play, updating"
142       // notification.
143       MediaKeySystemAccess::NotifyObservers(mWindow, aKeySystem, status);
144     } else {
145       // We waited or can't wait for an update and we still can't service
146       // the request. Give up. Chrome will still be showing a "I can't play,
147       // updating" notification.
148       aPromise->MaybeReject(
149           NS_ERROR_DOM_NOT_SUPPORTED_ERR,
150           NS_LITERAL_CSTRING("Gave up while waiting for a CDM update"));
151     }
152     diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), aKeySystem,
153                                           false, __func__);
154     return;
155   }
156   if (status != MediaKeySystemStatus::Available) {
157     // Failed due to user disabling something, send a notification to
158     // chrome, so we can show some UI to explain how the user can rectify
159     // the situation.
160     MediaKeySystemAccess::NotifyObservers(mWindow, aKeySystem, status);
161     aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR, message);
162     return;
163   }
164 
165   nsCOMPtr<nsIDocument> doc = mWindow->GetExtantDoc();
166   nsDataHashtable<nsCharPtrHashKey, bool> warnings;
167   std::function<void(const char*)> deprecationWarningLogFn =
168       [&](const char* aMsgName) {
169         EME_LOG("Logging deprecation warning '%s' to WebConsole.", aMsgName);
170         warnings.Put(aMsgName, true);
171         nsString uri;
172         if (doc) {
173           Unused << doc->GetDocumentURI(uri);
174         }
175         const char16_t* params[] = {uri.get()};
176         nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
177                                         NS_LITERAL_CSTRING("Media"), doc,
178                                         nsContentUtils::eDOM_PROPERTIES,
179                                         aMsgName, params, ArrayLength(params));
180       };
181 
182   bool isPrivateBrowsing =
183       mWindow->GetExtantDoc() &&
184       mWindow->GetExtantDoc()->NodePrincipal()->GetPrivateBrowsingId() > 0;
185   MediaKeySystemConfiguration config;
186   if (MediaKeySystemAccess::GetSupportedConfig(aKeySystem, aConfigs, config,
187                                                &diagnostics, isPrivateBrowsing,
188                                                deprecationWarningLogFn)) {
189     RefPtr<MediaKeySystemAccess> access(
190         new MediaKeySystemAccess(mWindow, aKeySystem, config));
191     aPromise->MaybeResolve(access);
192     diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), aKeySystem,
193                                           true, __func__);
194 
195     // Accumulate telemetry to report whether we hit deprecation warnings.
196     if (warnings.Get("MediaEMENoCapabilitiesDeprecatedWarning")) {
197       Telemetry::Accumulate(
198           Telemetry::HistogramID::MEDIA_EME_REQUEST_DEPRECATED_WARNINGS, 1);
199       EME_LOG(
200           "MEDIA_EME_REQUEST_DEPRECATED_WARNINGS "
201           "MediaEMENoCapabilitiesDeprecatedWarning");
202     } else if (warnings.Get("MediaEMENoCodecsDeprecatedWarning")) {
203       Telemetry::Accumulate(
204           Telemetry::HistogramID::MEDIA_EME_REQUEST_DEPRECATED_WARNINGS, 2);
205       EME_LOG(
206           "MEDIA_EME_REQUEST_DEPRECATED_WARNINGS "
207           "MediaEMENoCodecsDeprecatedWarning");
208     } else {
209       Telemetry::Accumulate(
210           Telemetry::HistogramID::MEDIA_EME_REQUEST_DEPRECATED_WARNINGS, 0);
211       EME_LOG("MEDIA_EME_REQUEST_DEPRECATED_WARNINGS No warnings");
212     }
213     return;
214   }
215   // Not to inform user, because nothing to do if the corresponding keySystem
216   // configuration is not supported.
217   aPromise->MaybeReject(
218       NS_ERROR_DOM_NOT_SUPPORTED_ERR,
219       NS_LITERAL_CSTRING("Key system configuration is not supported"));
220   diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), aKeySystem,
221                                         false, __func__);
222 }
223 
PendingRequest(DetailedPromise * aPromise,const nsAString & aKeySystem,const Sequence<MediaKeySystemConfiguration> & aConfigs,nsITimer * aTimer)224 MediaKeySystemAccessManager::PendingRequest::PendingRequest(
225     DetailedPromise* aPromise, const nsAString& aKeySystem,
226     const Sequence<MediaKeySystemConfiguration>& aConfigs, nsITimer* aTimer)
227     : mPromise(aPromise),
228       mKeySystem(aKeySystem),
229       mConfigs(aConfigs),
230       mTimer(aTimer) {
231   MOZ_COUNT_CTOR(MediaKeySystemAccessManager::PendingRequest);
232 }
233 
PendingRequest(const PendingRequest & aOther)234 MediaKeySystemAccessManager::PendingRequest::PendingRequest(
235     const PendingRequest& aOther)
236     : mPromise(aOther.mPromise),
237       mKeySystem(aOther.mKeySystem),
238       mConfigs(aOther.mConfigs),
239       mTimer(aOther.mTimer) {
240   MOZ_COUNT_CTOR(MediaKeySystemAccessManager::PendingRequest);
241 }
242 
~PendingRequest()243 MediaKeySystemAccessManager::PendingRequest::~PendingRequest() {
244   MOZ_COUNT_DTOR(MediaKeySystemAccessManager::PendingRequest);
245 }
246 
CancelTimer()247 void MediaKeySystemAccessManager::PendingRequest::CancelTimer() {
248   if (mTimer) {
249     mTimer->Cancel();
250   }
251 }
252 
RejectPromise(const nsCString & aReason)253 void MediaKeySystemAccessManager::PendingRequest::RejectPromise(
254     const nsCString& aReason) {
255   if (mPromise) {
256     mPromise->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR, aReason);
257   }
258 }
259 
AwaitInstall(DetailedPromise * aPromise,const nsAString & aKeySystem,const Sequence<MediaKeySystemConfiguration> & aConfigs)260 bool MediaKeySystemAccessManager::AwaitInstall(
261     DetailedPromise* aPromise, const nsAString& aKeySystem,
262     const Sequence<MediaKeySystemConfiguration>& aConfigs) {
263   EME_LOG("MediaKeySystemAccessManager::AwaitInstall %s",
264           NS_ConvertUTF16toUTF8(aKeySystem).get());
265 
266   if (!EnsureObserversAdded()) {
267     NS_WARNING("Failed to add pref observer");
268     return false;
269   }
270 
271   nsCOMPtr<nsITimer> timer;
272   NS_NewTimerWithObserver(getter_AddRefs(timer), this, 60 * 1000,
273                           nsITimer::TYPE_ONE_SHOT);
274   if (!timer) {
275     NS_WARNING("Failed to create timer to await CDM install.");
276     return false;
277   }
278 
279   mRequests.AppendElement(
280       PendingRequest(aPromise, aKeySystem, aConfigs, timer));
281   return true;
282 }
283 
RetryRequest(PendingRequest & aRequest)284 void MediaKeySystemAccessManager::RetryRequest(PendingRequest& aRequest) {
285   aRequest.CancelTimer();
286   Request(aRequest.mPromise, aRequest.mKeySystem, aRequest.mConfigs,
287           RequestType::Subsequent);
288 }
289 
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)290 nsresult MediaKeySystemAccessManager::Observe(nsISupports* aSubject,
291                                               const char* aTopic,
292                                               const char16_t* aData) {
293   EME_LOG("MediaKeySystemAccessManager::Observe %s", aTopic);
294 
295   if (!strcmp(aTopic, "gmp-changed")) {
296     // Filter out the requests where the CDM's install-status is no longer
297     // "unavailable". This will be the CDMs which have downloaded since the
298     // initial request.
299     // Note: We don't have a way to communicate from chrome that the CDM has
300     // failed to download, so we'll just let the timeout fail us in that case.
301     nsTArray<PendingRequest> requests;
302     for (size_t i = mRequests.Length(); i-- > 0;) {
303       PendingRequest& request = mRequests[i];
304       nsAutoCString message;
305       MediaKeySystemStatus status =
306           MediaKeySystemAccess::GetKeySystemStatus(request.mKeySystem, message);
307       if (status == MediaKeySystemStatus::Cdm_not_installed) {
308         // Not yet installed, don't retry. Keep waiting until timeout.
309         continue;
310       }
311       // Status has changed, retry request.
312       requests.AppendElement(Move(request));
313       mRequests.RemoveElementAt(i);
314     }
315     // Retry all pending requests, but this time fail if the CDM is not
316     // installed.
317     for (PendingRequest& request : requests) {
318       RetryRequest(request);
319     }
320   } else if (!strcmp(aTopic, "timer-callback")) {
321     // Find the timer that expired and re-run the request for it.
322     nsCOMPtr<nsITimer> timer(do_QueryInterface(aSubject));
323     for (size_t i = 0; i < mRequests.Length(); i++) {
324       if (mRequests[i].mTimer == timer) {
325         EME_LOG("MediaKeySystemAccessManager::AwaitInstall resuming request");
326         PendingRequest request = mRequests[i];
327         mRequests.RemoveElementAt(i);
328         RetryRequest(request);
329         break;
330       }
331     }
332   }
333   return NS_OK;
334 }
335 
EnsureObserversAdded()336 bool MediaKeySystemAccessManager::EnsureObserversAdded() {
337   if (mAddedObservers) {
338     return true;
339   }
340 
341   nsCOMPtr<nsIObserverService> obsService =
342       mozilla::services::GetObserverService();
343   if (NS_WARN_IF(!obsService)) {
344     return false;
345   }
346   mAddedObservers =
347       NS_SUCCEEDED(obsService->AddObserver(this, "gmp-changed", false));
348   return mAddedObservers;
349 }
350 
Shutdown()351 void MediaKeySystemAccessManager::Shutdown() {
352   EME_LOG("MediaKeySystemAccessManager::Shutdown");
353   nsTArray<PendingRequest> requests(Move(mRequests));
354   for (PendingRequest& request : requests) {
355     // Cancel all requests; we're shutting down.
356     request.CancelTimer();
357     request.RejectPromise(NS_LITERAL_CSTRING(
358         "Promise still outstanding at MediaKeySystemAccessManager shutdown"));
359   }
360   if (mAddedObservers) {
361     nsCOMPtr<nsIObserverService> obsService =
362         mozilla::services::GetObserverService();
363     if (obsService) {
364       obsService->RemoveObserver(this, "gmp-changed");
365       mAddedObservers = false;
366     }
367   }
368 }
369 
370 }  // namespace dom
371 }  // namespace mozilla
372