1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sw=2 sts=2 et cindent: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "DecoderDoctorDiagnostics.h"
8 
9 #include "mozilla/dom/DecoderDoctorNotificationBinding.h"
10 #include "mozilla/Logging.h"
11 #include "mozilla/Preferences.h"
12 #include "nsContentUtils.h"
13 #include "nsGkAtoms.h"
14 #include "nsIDocument.h"
15 #include "nsIObserverService.h"
16 #include "nsIScriptError.h"
17 #include "nsITimer.h"
18 #include "nsIWeakReference.h"
19 #include "nsPluginHost.h"
20 #include "nsPrintfCString.h"
21 #include "VideoUtils.h"
22 
23 #if defined(MOZ_FFMPEG)
24 #include "FFmpegRuntimeLinker.h"
25 #endif
26 
27 #if defined(XP_WIN)
28 #include "mozilla/WindowsVersion.h"
29 #endif
30 
31 static mozilla::LazyLogModule sDecoderDoctorLog("DecoderDoctor");
32 #define DD_LOG(level, arg, ...) MOZ_LOG(sDecoderDoctorLog, level, (arg, ##__VA_ARGS__))
33 #define DD_DEBUG(arg, ...) DD_LOG(mozilla::LogLevel::Debug, arg, ##__VA_ARGS__)
34 #define DD_INFO(arg, ...) DD_LOG(mozilla::LogLevel::Info, arg, ##__VA_ARGS__)
35 #define DD_WARN(arg, ...) DD_LOG(mozilla::LogLevel::Warning, arg, ##__VA_ARGS__)
36 
37 namespace mozilla {
38 
39 struct NotificationAndReportStringId
40 {
41   // Notification type, handled by browser-media.js.
42   dom::DecoderDoctorNotificationType mNotificationType;
43   // Console message id. Key in dom/locales/.../chrome/dom/dom.properties.
44   const char* mReportStringId;
45 };
46 
47 // Class that collects a sequence of diagnostics from the same document over a
48 // small period of time, in order to provide a synthesized analysis.
49 //
50 // Referenced by the document through a nsINode property, mTimer, and
51 // inter-task captures.
52 // When notified that the document is dead, or when the timer expires but
53 // nothing new happened, StopWatching() will remove the document property and
54 // timer (if present), so no more work will happen and the watcher will be
55 // destroyed once all references are gone.
56 class DecoderDoctorDocumentWatcher : public nsITimerCallback
57 {
58 public:
59   static already_AddRefed<DecoderDoctorDocumentWatcher>
60   RetrieveOrCreate(nsIDocument* aDocument);
61 
62   NS_DECL_ISUPPORTS
63   NS_DECL_NSITIMERCALLBACK
64 
65   void AddDiagnostics(DecoderDoctorDiagnostics&& aDiagnostics,
66                       const char* aCallSite);
67 
68 private:
69   explicit DecoderDoctorDocumentWatcher(nsIDocument* aDocument);
70   virtual ~DecoderDoctorDocumentWatcher();
71 
72   // This will prevent further work from happening, watcher will deregister
73   // itself from document (if requested) and cancel any timer, and soon die.
74   void StopWatching(bool aRemoveProperty);
75 
76   // Remove property from document; will call DestroyPropertyCallback.
77   void RemovePropertyFromDocument();
78   // Callback for property destructor, will be automatically called when the
79   // document (in aObject) is being destroyed.
80   static void DestroyPropertyCallback(void* aObject,
81                                       nsIAtom* aPropertyName,
82                                       void* aPropertyValue,
83                                       void* aData);
84 
85   static const uint32_t sAnalysisPeriod_ms = 1000;
86   void EnsureTimerIsStarted();
87 
88   void SynthesizeAnalysis();
89 
90   // Raw pointer to an nsIDocument.
91   // Must be non-null during construction.
92   // Nulled when we want to stop watching, because either:
93   // 1. The document has been destroyed (notified through
94   //    DestroyPropertyCallback).
95   // 2. We have not received new diagnostic information within a short time
96   //    period, so we just stop watching.
97   // Once nulled, no more actual work will happen, and the watcher will be
98   // destroyed soon.
99   nsIDocument* mDocument;
100 
101   struct Diagnostics
102   {
Diagnosticsmozilla::DecoderDoctorDocumentWatcher::Diagnostics103     Diagnostics(DecoderDoctorDiagnostics&& aDiagnostics,
104                 const char* aCallSite)
105       : mDecoderDoctorDiagnostics(Move(aDiagnostics))
106       , mCallSite(aCallSite)
107     {}
108     Diagnostics(const Diagnostics&) = delete;
Diagnosticsmozilla::DecoderDoctorDocumentWatcher::Diagnostics109     Diagnostics(Diagnostics&& aOther)
110       : mDecoderDoctorDiagnostics(Move(aOther.mDecoderDoctorDiagnostics))
111       , mCallSite(Move(aOther.mCallSite))
112     {}
113 
114     const DecoderDoctorDiagnostics mDecoderDoctorDiagnostics;
115     const nsCString mCallSite;
116   };
117   typedef nsTArray<Diagnostics> DiagnosticsSequence;
118   DiagnosticsSequence mDiagnosticsSequence;
119 
120   nsCOMPtr<nsITimer> mTimer; // Keep timer alive until we run.
121   DiagnosticsSequence::size_type mDiagnosticsHandled = 0;
122 };
123 
124 
NS_IMPL_ISUPPORTS(DecoderDoctorDocumentWatcher,nsITimerCallback)125 NS_IMPL_ISUPPORTS(DecoderDoctorDocumentWatcher, nsITimerCallback)
126 
127 // static
128 already_AddRefed<DecoderDoctorDocumentWatcher>
129 DecoderDoctorDocumentWatcher::RetrieveOrCreate(nsIDocument* aDocument)
130 {
131   MOZ_ASSERT(NS_IsMainThread());
132   MOZ_ASSERT(aDocument);
133   RefPtr<DecoderDoctorDocumentWatcher> watcher =
134     static_cast<DecoderDoctorDocumentWatcher*>(
135       aDocument->GetProperty(nsGkAtoms::decoderDoctor));
136   if (!watcher) {
137     watcher = new DecoderDoctorDocumentWatcher(aDocument);
138     if (NS_WARN_IF(NS_FAILED(
139           aDocument->SetProperty(nsGkAtoms::decoderDoctor,
140                                  watcher.get(),
141                                  DestroyPropertyCallback,
142                                  /*transfer*/ false)))) {
143       DD_WARN("DecoderDoctorDocumentWatcher::RetrieveOrCreate(doc=%p) - Could not set property in document, will destroy new watcher[%p]",
144               aDocument, watcher.get());
145       return nullptr;
146     }
147     // Document owns watcher through this property.
148     // Released in DestroyPropertyCallback().
149     NS_ADDREF(watcher.get());
150   }
151   return watcher.forget();
152 }
153 
DecoderDoctorDocumentWatcher(nsIDocument * aDocument)154 DecoderDoctorDocumentWatcher::DecoderDoctorDocumentWatcher(nsIDocument* aDocument)
155   : mDocument(aDocument)
156 {
157   MOZ_ASSERT(NS_IsMainThread());
158   MOZ_ASSERT(mDocument);
159   DD_DEBUG("DecoderDoctorDocumentWatcher[%p]::DecoderDoctorDocumentWatcher(doc=%p)",
160            this, mDocument);
161 }
162 
~DecoderDoctorDocumentWatcher()163 DecoderDoctorDocumentWatcher::~DecoderDoctorDocumentWatcher()
164 {
165   MOZ_ASSERT(NS_IsMainThread());
166   DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p <- expect 0]::~DecoderDoctorDocumentWatcher()",
167            this, mDocument);
168   // mDocument should have been reset through StopWatching()!
169   MOZ_ASSERT(!mDocument);
170 }
171 
172 void
RemovePropertyFromDocument()173 DecoderDoctorDocumentWatcher::RemovePropertyFromDocument()
174 {
175   MOZ_ASSERT(NS_IsMainThread());
176   DecoderDoctorDocumentWatcher* watcher =
177     static_cast<DecoderDoctorDocumentWatcher*>(
178       mDocument->GetProperty(nsGkAtoms::decoderDoctor));
179   if (!watcher) {
180     return;
181   }
182   DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::RemovePropertyFromDocument()\n",
183            watcher, watcher->mDocument);
184   // This will remove the property and call our DestroyPropertyCallback.
185   mDocument->DeleteProperty(nsGkAtoms::decoderDoctor);
186 }
187 
188 // Callback for property destructors. |aObject| is the object
189 // the property is being removed for, |aPropertyName| is the property
190 // being removed, |aPropertyValue| is the value of the property, and |aData|
191 // is the opaque destructor data that was passed to SetProperty().
192 // static
193 void
DestroyPropertyCallback(void * aObject,nsIAtom * aPropertyName,void * aPropertyValue,void *)194 DecoderDoctorDocumentWatcher::DestroyPropertyCallback(void* aObject,
195                                                       nsIAtom* aPropertyName,
196                                                       void* aPropertyValue,
197                                                       void*)
198 {
199   MOZ_ASSERT(NS_IsMainThread());
200   MOZ_ASSERT(aPropertyName == nsGkAtoms::decoderDoctor);
201   DecoderDoctorDocumentWatcher* watcher =
202     static_cast<DecoderDoctorDocumentWatcher*>(aPropertyValue);
203   MOZ_ASSERT(watcher);
204 #ifdef DEBUG
205   nsIDocument* document = static_cast<nsIDocument*>(aObject);
206   MOZ_ASSERT(watcher->mDocument == document);
207 #endif
208   DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::DestroyPropertyCallback()\n",
209            watcher, watcher->mDocument);
210   // 'false': StopWatching should not try and remove the property.
211   watcher->StopWatching(false);
212   NS_RELEASE(watcher);
213 }
214 
215 void
StopWatching(bool aRemoveProperty)216 DecoderDoctorDocumentWatcher::StopWatching(bool aRemoveProperty)
217 {
218   MOZ_ASSERT(NS_IsMainThread());
219   // StopWatching() shouldn't be called twice.
220   MOZ_ASSERT(mDocument);
221 
222   if (aRemoveProperty) {
223     RemovePropertyFromDocument();
224   }
225 
226   // Forget document now, this will prevent more work from being started.
227   mDocument = nullptr;
228 
229   if (mTimer) {
230     mTimer->Cancel();
231     mTimer = nullptr;
232   }
233 }
234 
235 void
EnsureTimerIsStarted()236 DecoderDoctorDocumentWatcher::EnsureTimerIsStarted()
237 {
238   MOZ_ASSERT(NS_IsMainThread());
239 
240   if (!mTimer) {
241     mTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
242     if (NS_WARN_IF(!mTimer)) {
243       return;
244     }
245     if (NS_WARN_IF(NS_FAILED(
246           mTimer->InitWithCallback(
247             this, sAnalysisPeriod_ms, nsITimer::TYPE_ONE_SHOT)))) {
248       mTimer = nullptr;
249     }
250   }
251 }
252 
253 // Note: ReportStringIds are limited to alphanumeric only.
254 static const NotificationAndReportStringId sMediaWidevineNoWMFNoSilverlight =
255   { dom::DecoderDoctorNotificationType::Platform_decoder_not_found,
256     "MediaWidevineNoWMFNoSilverlight" };
257 static const NotificationAndReportStringId sMediaWMFNeeded =
258   { dom::DecoderDoctorNotificationType::Platform_decoder_not_found,
259     "MediaWMFNeeded" };
260 static const NotificationAndReportStringId sMediaUnsupportedBeforeWindowsVista =
261   { dom::DecoderDoctorNotificationType::Platform_decoder_not_found,
262     "MediaUnsupportedBeforeWindowsVista" };
263 static const NotificationAndReportStringId sMediaPlatformDecoderNotFound =
264   { dom::DecoderDoctorNotificationType::Platform_decoder_not_found,
265     "MediaPlatformDecoderNotFound" };
266 static const NotificationAndReportStringId sMediaCannotPlayNoDecoders =
267   { dom::DecoderDoctorNotificationType::Cannot_play,
268     "MediaCannotPlayNoDecoders" };
269 static const NotificationAndReportStringId sMediaNoDecoders =
270   { dom::DecoderDoctorNotificationType::Can_play_but_some_missing_decoders,
271     "MediaNoDecoders" };
272 static const NotificationAndReportStringId sCannotInitializePulseAudio =
273   { dom::DecoderDoctorNotificationType::Cannot_initialize_pulseaudio,
274     "MediaCannotInitializePulseAudio" };
275 static const NotificationAndReportStringId sUnsupportedLibavcodec =
276   { dom::DecoderDoctorNotificationType::Unsupported_libavcodec,
277     "MediaUnsupportedLibavcodec" };
278 
279 static const NotificationAndReportStringId*
280 sAllNotificationsAndReportStringIds[] =
281 {
282   &sMediaWidevineNoWMFNoSilverlight,
283   &sMediaWMFNeeded,
284   &sMediaUnsupportedBeforeWindowsVista,
285   &sMediaPlatformDecoderNotFound,
286   &sMediaCannotPlayNoDecoders,
287   &sMediaNoDecoders,
288   &sCannotInitializePulseAudio,
289   &sUnsupportedLibavcodec,
290 };
291 
292 static void
DispatchNotification(nsISupports * aSubject,const NotificationAndReportStringId & aNotification,bool aIsSolved,const nsAString & aFormats)293 DispatchNotification(nsISupports* aSubject,
294                      const NotificationAndReportStringId& aNotification,
295                      bool aIsSolved,
296                      const nsAString& aFormats)
297 {
298   if (!aSubject) {
299     return;
300   }
301   dom::DecoderDoctorNotification data;
302   data.mType = aNotification.mNotificationType;
303   data.mIsSolved = aIsSolved;
304   data.mDecoderDoctorReportId.Assign(
305     NS_ConvertUTF8toUTF16(aNotification.mReportStringId));
306   if (!aFormats.IsEmpty()) {
307     data.mFormats.Construct(aFormats);
308   }
309   nsAutoString json;
310   data.ToJSON(json);
311   if (json.IsEmpty()) {
312     DD_WARN("DecoderDoctorDiagnostics/DispatchEvent() - Could not create json for dispatch");
313     // No point in dispatching this notification without data, the front-end
314     // wouldn't know what to display.
315     return;
316   }
317   DD_DEBUG("DecoderDoctorDiagnostics/DispatchEvent() %s", NS_ConvertUTF16toUTF8(json).get());
318   nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
319   if (obs) {
320     obs->NotifyObservers(aSubject, "decoder-doctor-notification", json.get());
321   }
322 }
323 
324 static void
ReportToConsole(nsIDocument * aDocument,const char * aConsoleStringId,const nsAString & aParams)325 ReportToConsole(nsIDocument* aDocument,
326                 const char* aConsoleStringId,
327                 const nsAString& aParams)
328 {
329   MOZ_ASSERT(NS_IsMainThread());
330   MOZ_ASSERT(aDocument);
331 
332   // 'params' will only be forwarded for non-empty strings.
333   const char16_t* params[1] = { aParams.Data() };
334   DD_DEBUG("DecoderDoctorDiagnostics.cpp:ReportToConsole(doc=%p) ReportToConsole - aMsg='%s' params[0]='%s'",
335            aDocument, aConsoleStringId,
336            aParams.IsEmpty() ? "<no params>" : NS_ConvertUTF16toUTF8(params[0]).get());
337   nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
338                                   NS_LITERAL_CSTRING("Media"),
339                                   aDocument,
340                                   nsContentUtils::eDOM_PROPERTIES,
341                                   aConsoleStringId,
342                                   aParams.IsEmpty() ? nullptr : params,
343                                   aParams.IsEmpty() ? 0 : 1);
344 }
345 
346 static void
ReportAnalysis(nsIDocument * aDocument,const NotificationAndReportStringId & aNotification,bool aIsSolved,const nsAString & aParams)347 ReportAnalysis(nsIDocument* aDocument,
348                const NotificationAndReportStringId& aNotification,
349                bool aIsSolved,
350                const nsAString& aParams)
351 {
352   MOZ_ASSERT(NS_IsMainThread());
353 
354   if (!aDocument) {
355     return;
356   }
357 
358   // Report non-solved issues to console.
359   if (!aIsSolved) {
360     ReportToConsole(aDocument, aNotification.mReportStringId, aParams);
361   }
362 
363   // "media.decoder-doctor.notifications-allowed" controls which notifications
364   // may be dispatched to the front-end. It either contains:
365   // - '*' -> Allow everything.
366   // - Comma-separater list of ids -> Allow if aReportStringId (from
367   //                                  dom.properties) is one of them.
368   // - Nothing (missing or empty) -> Disable everything.
369   nsAdoptingCString filter =
370     Preferences::GetCString("media.decoder-doctor.notifications-allowed");
371   filter.StripWhitespace();
372   if (filter.EqualsLiteral("*")
373       || StringListContains(filter, aNotification.mReportStringId)) {
374     DispatchNotification(
375       aDocument->GetInnerWindow(), aNotification, aIsSolved, aParams);
376   }
377 }
378 
379 enum SilverlightPresence {
380   eNoSilverlight,
381   eSilverlightDisabled,
382   eSilverlightEnabled
383 };
384 static SilverlightPresence
CheckSilverlight()385 CheckSilverlight()
386 {
387   MOZ_ASSERT(NS_IsMainThread());
388   RefPtr<nsPluginHost> pluginHost = nsPluginHost::GetInst();
389   if (!pluginHost) {
390     return eNoSilverlight;
391   }
392   nsTArray<nsCOMPtr<nsIInternalPluginTag>> plugins;
393   pluginHost->GetPlugins(plugins, /*aIncludeDisabled*/ true);
394   for (const auto& plugin : plugins) {
395     for (const auto& mime : plugin->MimeTypes()) {
396       if (mime.LowerCaseEqualsLiteral("application/x-silverlight")
397           || mime.LowerCaseEqualsLiteral("application/x-silverlight-2")) {
398         return plugin->IsEnabled() ? eSilverlightEnabled : eSilverlightDisabled;
399       }
400     }
401   }
402 
403   return eNoSilverlight;
404 }
405 
406 static nsString
CleanItemForFormatsList(const nsAString & aItem)407 CleanItemForFormatsList(const nsAString& aItem)
408 {
409   nsString item(aItem);
410   // Remove commas from item, as commas are used to separate items. It's fine
411   // to have a one-way mapping, it's only used for comparisons and in
412   // console display (where formats shouldn't contain commas in the first place)
413   item.ReplaceChar(',', ' ');
414   item.CompressWhitespace();
415   return item;
416 }
417 
418 static void
AppendToFormatsList(nsAString & aList,const nsAString & aItem)419 AppendToFormatsList(nsAString& aList, const nsAString& aItem)
420 {
421   if (!aList.IsEmpty()) {
422     aList += NS_LITERAL_STRING(", ");
423   }
424   aList += CleanItemForFormatsList(aItem);
425 }
426 
427 static bool
FormatsListContains(const nsAString & aList,const nsAString & aItem)428 FormatsListContains(const nsAString& aList, const nsAString& aItem)
429 {
430   return StringListContains(aList, CleanItemForFormatsList(aItem));
431 }
432 
433 void
SynthesizeAnalysis()434 DecoderDoctorDocumentWatcher::SynthesizeAnalysis()
435 {
436   MOZ_ASSERT(NS_IsMainThread());
437 
438   nsAutoString playableFormats;
439   nsAutoString unplayableFormats;
440   // Subsets of unplayableFormats that require a specific platform decoder:
441 #if defined(XP_WIN)
442   nsAutoString formatsRequiringWMF;
443 #endif
444 #if defined(MOZ_FFMPEG)
445   nsAutoString formatsRequiringFFMpeg;
446 #endif
447   nsAutoString supportedKeySystems;
448   nsAutoString unsupportedKeySystems;
449   DecoderDoctorDiagnostics::KeySystemIssue lastKeySystemIssue =
450     DecoderDoctorDiagnostics::eUnset;
451 
452   for (const auto& diag : mDiagnosticsSequence) {
453     switch (diag.mDecoderDoctorDiagnostics.Type()) {
454       case DecoderDoctorDiagnostics::eFormatSupportCheck:
455         if (diag.mDecoderDoctorDiagnostics.CanPlay()) {
456           AppendToFormatsList(playableFormats,
457                               diag.mDecoderDoctorDiagnostics.Format());
458         } else {
459           AppendToFormatsList(unplayableFormats,
460                               diag.mDecoderDoctorDiagnostics.Format());
461 #if defined(XP_WIN)
462           if (diag.mDecoderDoctorDiagnostics.DidWMFFailToLoad()) {
463             AppendToFormatsList(formatsRequiringWMF,
464                                 diag.mDecoderDoctorDiagnostics.Format());
465           }
466 #endif
467 #if defined(MOZ_FFMPEG)
468           if (diag.mDecoderDoctorDiagnostics.DidFFmpegFailToLoad()) {
469             AppendToFormatsList(formatsRequiringFFMpeg,
470                                 diag.mDecoderDoctorDiagnostics.Format());
471           }
472 #endif
473         }
474         break;
475       case DecoderDoctorDiagnostics::eMediaKeySystemAccessRequest:
476         if (diag.mDecoderDoctorDiagnostics.IsKeySystemSupported()) {
477           AppendToFormatsList(supportedKeySystems,
478                               diag.mDecoderDoctorDiagnostics.KeySystem());
479         } else {
480           AppendToFormatsList(unsupportedKeySystems,
481                               diag.mDecoderDoctorDiagnostics.KeySystem());
482           DecoderDoctorDiagnostics::KeySystemIssue issue =
483             diag.mDecoderDoctorDiagnostics.GetKeySystemIssue();
484           if (issue != DecoderDoctorDiagnostics::eUnset) {
485             lastKeySystemIssue = issue;
486           }
487         }
488         break;
489       case DecoderDoctorDiagnostics::eEvent:
490         MOZ_ASSERT_UNREACHABLE("Events shouldn't be stored for processing.");
491         break;
492       default:
493         MOZ_ASSERT(diag.mDecoderDoctorDiagnostics.Type()
494                      == DecoderDoctorDiagnostics::eFormatSupportCheck
495                    || diag.mDecoderDoctorDiagnostics.Type()
496                         == DecoderDoctorDiagnostics::eMediaKeySystemAccessRequest);
497         break;
498     }
499   }
500 
501   // Check if issues have been solved, by finding if some now-playable
502   // key systems or formats were previously recorded as having issues.
503   if (!supportedKeySystems.IsEmpty() || !playableFormats.IsEmpty()) {
504     DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - supported key systems '%s', playable formats '%s'; See if they show issues have been solved...",
505              this, mDocument,
506              NS_ConvertUTF16toUTF8(supportedKeySystems).Data(),
507              NS_ConvertUTF16toUTF8(playableFormats).get());
508     const nsAString* workingFormatsArray[] =
509       { &supportedKeySystems, &playableFormats };
510     // For each type of notification, retrieve the pref that contains formats/
511     // key systems with issues.
512     for (const NotificationAndReportStringId* id :
513            sAllNotificationsAndReportStringIds) {
514       nsAutoCString formatsPref("media.decoder-doctor.");
515       formatsPref += id->mReportStringId;
516       formatsPref += ".formats";
517       nsAdoptingString formatsWithIssues =
518         Preferences::GetString(formatsPref.Data());
519       if (formatsWithIssues.IsEmpty()) {
520         continue;
521       }
522       // See if that list of formats-with-issues contains any formats that are
523       // now playable/supported.
524       bool solved = false;
525       for (const nsAString* workingFormats : workingFormatsArray) {
526         for (const auto& workingFormat : MakeStringListRange(*workingFormats)) {
527           if (FormatsListContains(formatsWithIssues, workingFormat)) {
528             // This now-working format used not to work -> Report solved issue.
529             DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - %s solved ('%s' now works, it was in pref(%s)='%s')",
530                     this, mDocument, id->mReportStringId,
531                     NS_ConvertUTF16toUTF8(workingFormat).get(),
532                     formatsPref.Data(),
533                     NS_ConvertUTF16toUTF8(formatsWithIssues).get());
534             ReportAnalysis(mDocument, *id, true, workingFormat);
535             // This particular Notification&ReportId has been solved, no need
536             // to keep looking at other keysys/formats that might solve it too.
537             solved = true;
538             break;
539           }
540         }
541         if (solved) {
542           break;
543         }
544       }
545       if (!solved) {
546         DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - %s not solved (pref(%s)='%s')",
547                  this, mDocument, id->mReportStringId, formatsPref.Data(),
548                  NS_ConvertUTF16toUTF8(formatsWithIssues).get());
549       }
550     }
551   }
552 
553   // Look at Key System issues first, as they take precedence over format checks.
554   if (!unsupportedKeySystems.IsEmpty() && supportedKeySystems.IsEmpty()) {
555     // No supported key systems!
556     switch (lastKeySystemIssue) {
557       case DecoderDoctorDiagnostics::eWidevineWithNoWMF:
558         if (CheckSilverlight() != eSilverlightEnabled) {
559           DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - unsupported key systems: %s, widevine without WMF nor Silverlight",
560                   this, mDocument, NS_ConvertUTF16toUTF8(unsupportedKeySystems).get());
561           ReportAnalysis(mDocument, sMediaWidevineNoWMFNoSilverlight,
562                          false, unsupportedKeySystems);
563           return;
564         }
565         break;
566       default:
567         break;
568     }
569   }
570 
571   // Next, check playability of requested formats.
572   if (!unplayableFormats.IsEmpty()) {
573     // Some requested formats cannot be played.
574     if (playableFormats.IsEmpty()) {
575       // No requested formats can be played. See if we can help the user, by
576       // going through expected decoders from most to least desirable.
577 #if defined(XP_WIN)
578       if (!formatsRequiringWMF.IsEmpty()) {
579         if (IsVistaOrLater()) {
580           DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - unplayable formats: %s -> Cannot play media because WMF was not found",
581                   this, mDocument, NS_ConvertUTF16toUTF8(formatsRequiringWMF).get());
582           ReportAnalysis(mDocument, sMediaWMFNeeded, false, formatsRequiringWMF);
583         } else {
584           DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - unplayable formats: %s -> Cannot play media before Windows Vista",
585                   this, mDocument, NS_ConvertUTF16toUTF8(formatsRequiringWMF).get());
586           ReportAnalysis(mDocument, sMediaUnsupportedBeforeWindowsVista,
587                          false, formatsRequiringWMF);
588         }
589         return;
590       }
591 #endif
592 #if defined(MOZ_FFMPEG)
593       if (!formatsRequiringFFMpeg.IsEmpty()) {
594         switch (FFmpegRuntimeLinker::LinkStatusCode()) {
595           case FFmpegRuntimeLinker::LinkStatus_INVALID_FFMPEG_CANDIDATE:
596           case FFmpegRuntimeLinker::LinkStatus_UNUSABLE_LIBAV57:
597           case FFmpegRuntimeLinker::LinkStatus_INVALID_LIBAV_CANDIDATE:
598           case FFmpegRuntimeLinker::LinkStatus_OBSOLETE_FFMPEG:
599           case FFmpegRuntimeLinker::LinkStatus_OBSOLETE_LIBAV:
600           case FFmpegRuntimeLinker::LinkStatus_INVALID_CANDIDATE:
601             DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - unplayable formats: %s -> Cannot play media because of unsupported %s (Reason: %s)",
602                     this, mDocument,
603                     NS_ConvertUTF16toUTF8(formatsRequiringFFMpeg).get(),
604                     FFmpegRuntimeLinker::LinkStatusLibraryName(),
605                     FFmpegRuntimeLinker::LinkStatusString());
606             ReportAnalysis(mDocument, sUnsupportedLibavcodec,
607                            false, formatsRequiringFFMpeg);
608             return;
609           case FFmpegRuntimeLinker::LinkStatus_INIT:
610             MOZ_FALLTHROUGH_ASSERT("Unexpected LinkStatus_INIT");
611           case FFmpegRuntimeLinker::LinkStatus_SUCCEEDED:
612             MOZ_FALLTHROUGH_ASSERT("Unexpected LinkStatus_SUCCEEDED");
613           case FFmpegRuntimeLinker::LinkStatus_NOT_FOUND:
614             DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - unplayable formats: %s -> Cannot play media because platform decoder was not found (Reason: %s)",
615                     this, mDocument,
616                     NS_ConvertUTF16toUTF8(formatsRequiringFFMpeg).get(),
617                     FFmpegRuntimeLinker::LinkStatusString());
618             ReportAnalysis(mDocument, sMediaPlatformDecoderNotFound,
619                            false, formatsRequiringFFMpeg);
620             return;
621         }
622       }
623 #endif
624       DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - Cannot play media, unplayable formats: %s",
625               this, mDocument, NS_ConvertUTF16toUTF8(unplayableFormats).get());
626       ReportAnalysis(mDocument, sMediaCannotPlayNoDecoders,
627                      false, unplayableFormats);
628       return;
629     }
630 
631     DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - Can play media, but no decoders for some requested formats: %s",
632             this, mDocument, NS_ConvertUTF16toUTF8(unplayableFormats).get());
633     if (Preferences::GetBool("media.decoder-doctor.verbose", false)) {
634       ReportAnalysis(mDocument, sMediaNoDecoders, false, unplayableFormats);
635     }
636     return;
637   }
638   DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - Can play media, decoders available for all requested formats",
639            this, mDocument);
640 }
641 
642 void
AddDiagnostics(DecoderDoctorDiagnostics && aDiagnostics,const char * aCallSite)643 DecoderDoctorDocumentWatcher::AddDiagnostics(DecoderDoctorDiagnostics&& aDiagnostics,
644                                              const char* aCallSite)
645 {
646   MOZ_ASSERT(NS_IsMainThread());
647   MOZ_ASSERT(aDiagnostics.Type() != DecoderDoctorDiagnostics::eEvent);
648 
649   if (!mDocument) {
650     return;
651   }
652 
653   DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::AddDiagnostics(DecoderDoctorDiagnostics{%s}, call site '%s')",
654            this, mDocument, aDiagnostics.GetDescription().Data(), aCallSite);
655   mDiagnosticsSequence.AppendElement(Diagnostics(Move(aDiagnostics), aCallSite));
656   EnsureTimerIsStarted();
657 }
658 
659 NS_IMETHODIMP
Notify(nsITimer * timer)660 DecoderDoctorDocumentWatcher::Notify(nsITimer* timer)
661 {
662   MOZ_ASSERT(NS_IsMainThread());
663   MOZ_ASSERT(timer == mTimer);
664 
665   // Forget timer. (Assuming timer keeps itself and us alive during this call.)
666   mTimer = nullptr;
667 
668   if (!mDocument) {
669     return NS_OK;
670   }
671 
672   if (mDiagnosticsSequence.Length() > mDiagnosticsHandled) {
673     // We have new diagnostic data.
674     mDiagnosticsHandled = mDiagnosticsSequence.Length();
675 
676     SynthesizeAnalysis();
677 
678     // Restart timer, to redo analysis or stop watching this document,
679     // depending on whether anything new happens.
680     EnsureTimerIsStarted();
681   } else {
682     DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::Notify() - No new diagnostics to analyze -> Stop watching",
683              this, mDocument);
684     // Stop watching this document, we don't expect more diagnostics for now.
685     // If more diagnostics come in, we'll treat them as another burst, separately.
686     // 'true' to remove the property from the document.
687     StopWatching(true);
688   }
689 
690   return NS_OK;
691 }
692 
693 
694 void
StoreFormatDiagnostics(nsIDocument * aDocument,const nsAString & aFormat,bool aCanPlay,const char * aCallSite)695 DecoderDoctorDiagnostics::StoreFormatDiagnostics(nsIDocument* aDocument,
696                                                  const nsAString& aFormat,
697                                                  bool aCanPlay,
698                                                  const char* aCallSite)
699 {
700   MOZ_ASSERT(NS_IsMainThread());
701   // Make sure we only store once.
702   MOZ_ASSERT(mDiagnosticsType == eUnsaved);
703   mDiagnosticsType = eFormatSupportCheck;
704 
705   if (NS_WARN_IF(!aDocument)) {
706     DD_WARN("DecoderDoctorDiagnostics[%p]::StoreFormatDiagnostics(nsIDocument* aDocument=nullptr, format='%s', can-play=%d, call site '%s')",
707             this, NS_ConvertUTF16toUTF8(aFormat).get(), aCanPlay, aCallSite);
708     return;
709   }
710   if (NS_WARN_IF(aFormat.IsEmpty())) {
711     DD_WARN("DecoderDoctorDiagnostics[%p]::StoreFormatDiagnostics(nsIDocument* aDocument=%p, format=<empty>, can-play=%d, call site '%s')",
712             this, aDocument, aCanPlay, aCallSite);
713     return;
714   }
715 
716   RefPtr<DecoderDoctorDocumentWatcher> watcher =
717     DecoderDoctorDocumentWatcher::RetrieveOrCreate(aDocument);
718 
719   if (NS_WARN_IF(!watcher)) {
720     DD_WARN("DecoderDoctorDiagnostics[%p]::StoreFormatDiagnostics(nsIDocument* aDocument=%p, format='%s', can-play=%d, call site '%s') - Could not create document watcher",
721             this, aDocument, NS_ConvertUTF16toUTF8(aFormat).get(), aCanPlay, aCallSite);
722     return;
723   }
724 
725   mFormat = aFormat;
726   mCanPlay = aCanPlay;
727 
728   // StoreDiagnostics should only be called once, after all data is available,
729   // so it is safe to Move() from this object.
730   watcher->AddDiagnostics(Move(*this), aCallSite);
731   // Even though it's moved-from, the type should stay set
732   // (Only used to ensure that we do store only once.)
733   MOZ_ASSERT(mDiagnosticsType == eFormatSupportCheck);
734 }
735 
736 void
StoreMediaKeySystemAccess(nsIDocument * aDocument,const nsAString & aKeySystem,bool aIsSupported,const char * aCallSite)737 DecoderDoctorDiagnostics::StoreMediaKeySystemAccess(nsIDocument* aDocument,
738                                                     const nsAString& aKeySystem,
739                                                     bool aIsSupported,
740                                                     const char* aCallSite)
741 {
742   MOZ_ASSERT(NS_IsMainThread());
743   // Make sure we only store once.
744   MOZ_ASSERT(mDiagnosticsType == eUnsaved);
745   mDiagnosticsType = eMediaKeySystemAccessRequest;
746 
747   if (NS_WARN_IF(!aDocument)) {
748     DD_WARN("DecoderDoctorDiagnostics[%p]::StoreMediaKeySystemAccess(nsIDocument* aDocument=nullptr, keysystem='%s', supported=%d, call site '%s')",
749             this, NS_ConvertUTF16toUTF8(aKeySystem).get(), aIsSupported, aCallSite);
750     return;
751   }
752   if (NS_WARN_IF(aKeySystem.IsEmpty())) {
753     DD_WARN("DecoderDoctorDiagnostics[%p]::StoreMediaKeySystemAccess(nsIDocument* aDocument=%p, keysystem=<empty>, supported=%d, call site '%s')",
754             this, aDocument, aIsSupported, aCallSite);
755     return;
756   }
757 
758   RefPtr<DecoderDoctorDocumentWatcher> watcher =
759     DecoderDoctorDocumentWatcher::RetrieveOrCreate(aDocument);
760 
761   if (NS_WARN_IF(!watcher)) {
762     DD_WARN("DecoderDoctorDiagnostics[%p]::StoreMediaKeySystemAccess(nsIDocument* aDocument=%p, keysystem='%s', supported=%d, call site '%s') - Could not create document watcher",
763             this, aDocument, NS_ConvertUTF16toUTF8(aKeySystem).get(), aIsSupported, aCallSite);
764     return;
765   }
766 
767   mKeySystem = aKeySystem;
768   mIsKeySystemSupported = aIsSupported;
769 
770   // StoreMediaKeySystemAccess should only be called once, after all data is
771   // available, so it is safe to Move() from this object.
772   watcher->AddDiagnostics(Move(*this), aCallSite);
773   // Even though it's moved-from, the type should stay set
774   // (Only used to ensure that we do store only once.)
775   MOZ_ASSERT(mDiagnosticsType == eMediaKeySystemAccessRequest);
776 }
777 
778 void
StoreEvent(nsIDocument * aDocument,const DecoderDoctorEvent & aEvent,const char * aCallSite)779 DecoderDoctorDiagnostics::StoreEvent(nsIDocument* aDocument,
780                                      const DecoderDoctorEvent& aEvent,
781                                      const char* aCallSite)
782 {
783   MOZ_ASSERT(NS_IsMainThread());
784   // Make sure we only store once.
785   MOZ_ASSERT(mDiagnosticsType == eUnsaved);
786   mDiagnosticsType = eEvent;
787   mEvent = aEvent;
788 
789   if (NS_WARN_IF(!aDocument)) {
790     DD_WARN("DecoderDoctorDiagnostics[%p]::StoreEvent(nsIDocument* aDocument=nullptr, aEvent=%s, call site '%s')",
791             this, GetDescription().get(), aCallSite);
792     return;
793   }
794 
795   // Don't keep events for later processing, just handle them now.
796 #ifdef MOZ_PULSEAUDIO
797   switch (aEvent.mDomain) {
798     case DecoderDoctorEvent::eAudioSinkStartup:
799       if (aEvent.mResult == NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR) {
800         DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::AddDiagnostics() - unable to initialize PulseAudio",
801                 this, aDocument);
802         ReportAnalysis(aDocument, sCannotInitializePulseAudio,
803                        false, NS_LITERAL_STRING("*"));
804       } else if (aEvent.mResult == NS_OK) {
805         DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::AddDiagnostics() - now able to initialize PulseAudio",
806                 this, aDocument);
807         ReportAnalysis(aDocument, sCannotInitializePulseAudio,
808                        true, NS_LITERAL_STRING("*"));
809       }
810       break;
811   }
812 #endif // MOZ_PULSEAUDIO
813 }
814 
815 static const char*
EventDomainString(DecoderDoctorEvent::Domain aDomain)816 EventDomainString(DecoderDoctorEvent::Domain aDomain)
817 {
818   switch (aDomain) {
819     case DecoderDoctorEvent::eAudioSinkStartup:
820       return "audio-sink-startup";
821   }
822   return "?";
823 }
824 
825 nsCString
GetDescription() const826 DecoderDoctorDiagnostics::GetDescription() const
827 {
828   nsCString s;
829   switch (mDiagnosticsType) {
830     case eUnsaved:
831       s = "Unsaved diagnostics, cannot get accurate description";
832       break;
833     case eFormatSupportCheck:
834       s = "format='";
835       s += NS_ConvertUTF16toUTF8(mFormat).get();
836       s += mCanPlay ? "', can play" : "', cannot play";
837       if (mVideoNotSupported) {
838         s+= ", but video format not supported";
839       }
840       if (mAudioNotSupported) {
841         s+= ", but audio format not supported";
842       }
843       if (mWMFFailedToLoad) {
844         s += ", Windows platform decoder failed to load";
845       }
846       if (mFFmpegFailedToLoad) {
847         s += ", Linux platform decoder failed to load";
848       }
849       if (mGMPPDMFailedToStartup) {
850         s += ", GMP PDM failed to startup";
851       } else if (!mGMP.IsEmpty()) {
852         s += ", Using GMP '";
853         s += mGMP;
854         s += "'";
855       }
856       break;
857     case eMediaKeySystemAccessRequest:
858       s = "key system='";
859       s += NS_ConvertUTF16toUTF8(mKeySystem).get();
860       s += mIsKeySystemSupported ? "', supported" : "', not supported";
861       switch (mKeySystemIssue) {
862         case eUnset:
863           break;
864         case eWidevineWithNoWMF:
865           s += ", Widevine with no WMF";
866           break;
867       }
868       break;
869     case eEvent:
870       s = nsPrintfCString("event domain %s result=%u",
871                           EventDomainString(mEvent.mDomain), mEvent.mResult);
872       break;
873     default:
874       MOZ_ASSERT_UNREACHABLE("Unexpected DiagnosticsType");
875       s = "?";
876       break;
877   }
878   return s;
879 }
880 
881 } // namespace mozilla
882