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