/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "DecoderDoctorDiagnostics.h" #include "mozilla/dom/DecoderDoctorNotificationBinding.h" #include "mozilla/Logging.h" #include "mozilla/Preferences.h" #include "nsContentUtils.h" #include "nsGkAtoms.h" #include "nsIDocument.h" #include "nsIObserverService.h" #include "nsIScriptError.h" #include "nsITimer.h" #include "nsIWeakReference.h" #include "nsPluginHost.h" #include "nsPrintfCString.h" #include "VideoUtils.h" #if defined(MOZ_FFMPEG) #include "FFmpegRuntimeLinker.h" #endif #if defined(XP_WIN) #include "mozilla/WindowsVersion.h" #endif static mozilla::LazyLogModule sDecoderDoctorLog("DecoderDoctor"); #define DD_LOG(level, arg, ...) MOZ_LOG(sDecoderDoctorLog, level, (arg, ##__VA_ARGS__)) #define DD_DEBUG(arg, ...) DD_LOG(mozilla::LogLevel::Debug, arg, ##__VA_ARGS__) #define DD_INFO(arg, ...) DD_LOG(mozilla::LogLevel::Info, arg, ##__VA_ARGS__) #define DD_WARN(arg, ...) DD_LOG(mozilla::LogLevel::Warning, arg, ##__VA_ARGS__) namespace mozilla { struct NotificationAndReportStringId { // Notification type, handled by browser-media.js. dom::DecoderDoctorNotificationType mNotificationType; // Console message id. Key in dom/locales/.../chrome/dom/dom.properties. const char* mReportStringId; }; // Class that collects a sequence of diagnostics from the same document over a // small period of time, in order to provide a synthesized analysis. // // Referenced by the document through a nsINode property, mTimer, and // inter-task captures. // When notified that the document is dead, or when the timer expires but // nothing new happened, StopWatching() will remove the document property and // timer (if present), so no more work will happen and the watcher will be // destroyed once all references are gone. class DecoderDoctorDocumentWatcher : public nsITimerCallback { public: static already_AddRefed RetrieveOrCreate(nsIDocument* aDocument); NS_DECL_ISUPPORTS NS_DECL_NSITIMERCALLBACK void AddDiagnostics(DecoderDoctorDiagnostics&& aDiagnostics, const char* aCallSite); private: explicit DecoderDoctorDocumentWatcher(nsIDocument* aDocument); virtual ~DecoderDoctorDocumentWatcher(); // This will prevent further work from happening, watcher will deregister // itself from document (if requested) and cancel any timer, and soon die. void StopWatching(bool aRemoveProperty); // Remove property from document; will call DestroyPropertyCallback. void RemovePropertyFromDocument(); // Callback for property destructor, will be automatically called when the // document (in aObject) is being destroyed. static void DestroyPropertyCallback(void* aObject, nsIAtom* aPropertyName, void* aPropertyValue, void* aData); static const uint32_t sAnalysisPeriod_ms = 1000; void EnsureTimerIsStarted(); void SynthesizeAnalysis(); // Raw pointer to an nsIDocument. // Must be non-null during construction. // Nulled when we want to stop watching, because either: // 1. The document has been destroyed (notified through // DestroyPropertyCallback). // 2. We have not received new diagnostic information within a short time // period, so we just stop watching. // Once nulled, no more actual work will happen, and the watcher will be // destroyed soon. nsIDocument* mDocument; struct Diagnostics { Diagnostics(DecoderDoctorDiagnostics&& aDiagnostics, const char* aCallSite) : mDecoderDoctorDiagnostics(Move(aDiagnostics)) , mCallSite(aCallSite) {} Diagnostics(const Diagnostics&) = delete; Diagnostics(Diagnostics&& aOther) : mDecoderDoctorDiagnostics(Move(aOther.mDecoderDoctorDiagnostics)) , mCallSite(Move(aOther.mCallSite)) {} const DecoderDoctorDiagnostics mDecoderDoctorDiagnostics; const nsCString mCallSite; }; typedef nsTArray DiagnosticsSequence; DiagnosticsSequence mDiagnosticsSequence; nsCOMPtr mTimer; // Keep timer alive until we run. DiagnosticsSequence::size_type mDiagnosticsHandled = 0; }; NS_IMPL_ISUPPORTS(DecoderDoctorDocumentWatcher, nsITimerCallback) // static already_AddRefed DecoderDoctorDocumentWatcher::RetrieveOrCreate(nsIDocument* aDocument) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aDocument); RefPtr watcher = static_cast( aDocument->GetProperty(nsGkAtoms::decoderDoctor)); if (!watcher) { watcher = new DecoderDoctorDocumentWatcher(aDocument); if (NS_WARN_IF(NS_FAILED( aDocument->SetProperty(nsGkAtoms::decoderDoctor, watcher.get(), DestroyPropertyCallback, /*transfer*/ false)))) { DD_WARN("DecoderDoctorDocumentWatcher::RetrieveOrCreate(doc=%p) - Could not set property in document, will destroy new watcher[%p]", aDocument, watcher.get()); return nullptr; } // Document owns watcher through this property. // Released in DestroyPropertyCallback(). NS_ADDREF(watcher.get()); } return watcher.forget(); } DecoderDoctorDocumentWatcher::DecoderDoctorDocumentWatcher(nsIDocument* aDocument) : mDocument(aDocument) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mDocument); DD_DEBUG("DecoderDoctorDocumentWatcher[%p]::DecoderDoctorDocumentWatcher(doc=%p)", this, mDocument); } DecoderDoctorDocumentWatcher::~DecoderDoctorDocumentWatcher() { MOZ_ASSERT(NS_IsMainThread()); DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p <- expect 0]::~DecoderDoctorDocumentWatcher()", this, mDocument); // mDocument should have been reset through StopWatching()! MOZ_ASSERT(!mDocument); } void DecoderDoctorDocumentWatcher::RemovePropertyFromDocument() { MOZ_ASSERT(NS_IsMainThread()); DecoderDoctorDocumentWatcher* watcher = static_cast( mDocument->GetProperty(nsGkAtoms::decoderDoctor)); if (!watcher) { return; } DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::RemovePropertyFromDocument()\n", watcher, watcher->mDocument); // This will remove the property and call our DestroyPropertyCallback. mDocument->DeleteProperty(nsGkAtoms::decoderDoctor); } // Callback for property destructors. |aObject| is the object // the property is being removed for, |aPropertyName| is the property // being removed, |aPropertyValue| is the value of the property, and |aData| // is the opaque destructor data that was passed to SetProperty(). // static void DecoderDoctorDocumentWatcher::DestroyPropertyCallback(void* aObject, nsIAtom* aPropertyName, void* aPropertyValue, void*) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPropertyName == nsGkAtoms::decoderDoctor); DecoderDoctorDocumentWatcher* watcher = static_cast(aPropertyValue); MOZ_ASSERT(watcher); #ifdef DEBUG nsIDocument* document = static_cast(aObject); MOZ_ASSERT(watcher->mDocument == document); #endif DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::DestroyPropertyCallback()\n", watcher, watcher->mDocument); // 'false': StopWatching should not try and remove the property. watcher->StopWatching(false); NS_RELEASE(watcher); } void DecoderDoctorDocumentWatcher::StopWatching(bool aRemoveProperty) { MOZ_ASSERT(NS_IsMainThread()); // StopWatching() shouldn't be called twice. MOZ_ASSERT(mDocument); if (aRemoveProperty) { RemovePropertyFromDocument(); } // Forget document now, this will prevent more work from being started. mDocument = nullptr; if (mTimer) { mTimer->Cancel(); mTimer = nullptr; } } void DecoderDoctorDocumentWatcher::EnsureTimerIsStarted() { MOZ_ASSERT(NS_IsMainThread()); if (!mTimer) { mTimer = do_CreateInstance(NS_TIMER_CONTRACTID); if (NS_WARN_IF(!mTimer)) { return; } if (NS_WARN_IF(NS_FAILED( mTimer->InitWithCallback( this, sAnalysisPeriod_ms, nsITimer::TYPE_ONE_SHOT)))) { mTimer = nullptr; } } } // Note: ReportStringIds are limited to alphanumeric only. static const NotificationAndReportStringId sMediaWidevineNoWMFNoSilverlight = { dom::DecoderDoctorNotificationType::Platform_decoder_not_found, "MediaWidevineNoWMFNoSilverlight" }; static const NotificationAndReportStringId sMediaWMFNeeded = { dom::DecoderDoctorNotificationType::Platform_decoder_not_found, "MediaWMFNeeded" }; static const NotificationAndReportStringId sMediaUnsupportedBeforeWindowsVista = { dom::DecoderDoctorNotificationType::Platform_decoder_not_found, "MediaUnsupportedBeforeWindowsVista" }; static const NotificationAndReportStringId sMediaPlatformDecoderNotFound = { dom::DecoderDoctorNotificationType::Platform_decoder_not_found, "MediaPlatformDecoderNotFound" }; static const NotificationAndReportStringId sMediaCannotPlayNoDecoders = { dom::DecoderDoctorNotificationType::Cannot_play, "MediaCannotPlayNoDecoders" }; static const NotificationAndReportStringId sMediaNoDecoders = { dom::DecoderDoctorNotificationType::Can_play_but_some_missing_decoders, "MediaNoDecoders" }; static const NotificationAndReportStringId sCannotInitializePulseAudio = { dom::DecoderDoctorNotificationType::Cannot_initialize_pulseaudio, "MediaCannotInitializePulseAudio" }; static const NotificationAndReportStringId sUnsupportedLibavcodec = { dom::DecoderDoctorNotificationType::Unsupported_libavcodec, "MediaUnsupportedLibavcodec" }; static const NotificationAndReportStringId* sAllNotificationsAndReportStringIds[] = { &sMediaWidevineNoWMFNoSilverlight, &sMediaWMFNeeded, &sMediaUnsupportedBeforeWindowsVista, &sMediaPlatformDecoderNotFound, &sMediaCannotPlayNoDecoders, &sMediaNoDecoders, &sCannotInitializePulseAudio, &sUnsupportedLibavcodec, }; static void DispatchNotification(nsISupports* aSubject, const NotificationAndReportStringId& aNotification, bool aIsSolved, const nsAString& aFormats) { if (!aSubject) { return; } dom::DecoderDoctorNotification data; data.mType = aNotification.mNotificationType; data.mIsSolved = aIsSolved; data.mDecoderDoctorReportId.Assign( NS_ConvertUTF8toUTF16(aNotification.mReportStringId)); if (!aFormats.IsEmpty()) { data.mFormats.Construct(aFormats); } nsAutoString json; data.ToJSON(json); if (json.IsEmpty()) { DD_WARN("DecoderDoctorDiagnostics/DispatchEvent() - Could not create json for dispatch"); // No point in dispatching this notification without data, the front-end // wouldn't know what to display. return; } DD_DEBUG("DecoderDoctorDiagnostics/DispatchEvent() %s", NS_ConvertUTF16toUTF8(json).get()); nsCOMPtr obs = services::GetObserverService(); if (obs) { obs->NotifyObservers(aSubject, "decoder-doctor-notification", json.get()); } } static void ReportToConsole(nsIDocument* aDocument, const char* aConsoleStringId, const nsAString& aParams) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aDocument); // 'params' will only be forwarded for non-empty strings. const char16_t* params[1] = { aParams.Data() }; DD_DEBUG("DecoderDoctorDiagnostics.cpp:ReportToConsole(doc=%p) ReportToConsole - aMsg='%s' params[0]='%s'", aDocument, aConsoleStringId, aParams.IsEmpty() ? "" : NS_ConvertUTF16toUTF8(params[0]).get()); nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, NS_LITERAL_CSTRING("Media"), aDocument, nsContentUtils::eDOM_PROPERTIES, aConsoleStringId, aParams.IsEmpty() ? nullptr : params, aParams.IsEmpty() ? 0 : 1); } static void ReportAnalysis(nsIDocument* aDocument, const NotificationAndReportStringId& aNotification, bool aIsSolved, const nsAString& aParams) { MOZ_ASSERT(NS_IsMainThread()); if (!aDocument) { return; } // Report non-solved issues to console. if (!aIsSolved) { ReportToConsole(aDocument, aNotification.mReportStringId, aParams); } // "media.decoder-doctor.notifications-allowed" controls which notifications // may be dispatched to the front-end. It either contains: // - '*' -> Allow everything. // - Comma-separater list of ids -> Allow if aReportStringId (from // dom.properties) is one of them. // - Nothing (missing or empty) -> Disable everything. nsAdoptingCString filter = Preferences::GetCString("media.decoder-doctor.notifications-allowed"); filter.StripWhitespace(); if (filter.EqualsLiteral("*") || StringListContains(filter, aNotification.mReportStringId)) { DispatchNotification( aDocument->GetInnerWindow(), aNotification, aIsSolved, aParams); } } enum SilverlightPresence { eNoSilverlight, eSilverlightDisabled, eSilverlightEnabled }; static SilverlightPresence CheckSilverlight() { MOZ_ASSERT(NS_IsMainThread()); RefPtr pluginHost = nsPluginHost::GetInst(); if (!pluginHost) { return eNoSilverlight; } nsTArray> plugins; pluginHost->GetPlugins(plugins, /*aIncludeDisabled*/ true); for (const auto& plugin : plugins) { for (const auto& mime : plugin->MimeTypes()) { if (mime.LowerCaseEqualsLiteral("application/x-silverlight") || mime.LowerCaseEqualsLiteral("application/x-silverlight-2")) { return plugin->IsEnabled() ? eSilverlightEnabled : eSilverlightDisabled; } } } return eNoSilverlight; } static nsString CleanItemForFormatsList(const nsAString& aItem) { nsString item(aItem); // Remove commas from item, as commas are used to separate items. It's fine // to have a one-way mapping, it's only used for comparisons and in // console display (where formats shouldn't contain commas in the first place) item.ReplaceChar(',', ' '); item.CompressWhitespace(); return item; } static void AppendToFormatsList(nsAString& aList, const nsAString& aItem) { if (!aList.IsEmpty()) { aList += NS_LITERAL_STRING(", "); } aList += CleanItemForFormatsList(aItem); } static bool FormatsListContains(const nsAString& aList, const nsAString& aItem) { return StringListContains(aList, CleanItemForFormatsList(aItem)); } void DecoderDoctorDocumentWatcher::SynthesizeAnalysis() { MOZ_ASSERT(NS_IsMainThread()); nsAutoString playableFormats; nsAutoString unplayableFormats; // Subsets of unplayableFormats that require a specific platform decoder: #if defined(XP_WIN) nsAutoString formatsRequiringWMF; #endif #if defined(MOZ_FFMPEG) nsAutoString formatsRequiringFFMpeg; #endif nsAutoString supportedKeySystems; nsAutoString unsupportedKeySystems; DecoderDoctorDiagnostics::KeySystemIssue lastKeySystemIssue = DecoderDoctorDiagnostics::eUnset; for (const auto& diag : mDiagnosticsSequence) { switch (diag.mDecoderDoctorDiagnostics.Type()) { case DecoderDoctorDiagnostics::eFormatSupportCheck: if (diag.mDecoderDoctorDiagnostics.CanPlay()) { AppendToFormatsList(playableFormats, diag.mDecoderDoctorDiagnostics.Format()); } else { AppendToFormatsList(unplayableFormats, diag.mDecoderDoctorDiagnostics.Format()); #if defined(XP_WIN) if (diag.mDecoderDoctorDiagnostics.DidWMFFailToLoad()) { AppendToFormatsList(formatsRequiringWMF, diag.mDecoderDoctorDiagnostics.Format()); } #endif #if defined(MOZ_FFMPEG) if (diag.mDecoderDoctorDiagnostics.DidFFmpegFailToLoad()) { AppendToFormatsList(formatsRequiringFFMpeg, diag.mDecoderDoctorDiagnostics.Format()); } #endif } break; case DecoderDoctorDiagnostics::eMediaKeySystemAccessRequest: if (diag.mDecoderDoctorDiagnostics.IsKeySystemSupported()) { AppendToFormatsList(supportedKeySystems, diag.mDecoderDoctorDiagnostics.KeySystem()); } else { AppendToFormatsList(unsupportedKeySystems, diag.mDecoderDoctorDiagnostics.KeySystem()); DecoderDoctorDiagnostics::KeySystemIssue issue = diag.mDecoderDoctorDiagnostics.GetKeySystemIssue(); if (issue != DecoderDoctorDiagnostics::eUnset) { lastKeySystemIssue = issue; } } break; case DecoderDoctorDiagnostics::eEvent: MOZ_ASSERT_UNREACHABLE("Events shouldn't be stored for processing."); break; default: MOZ_ASSERT(diag.mDecoderDoctorDiagnostics.Type() == DecoderDoctorDiagnostics::eFormatSupportCheck || diag.mDecoderDoctorDiagnostics.Type() == DecoderDoctorDiagnostics::eMediaKeySystemAccessRequest); break; } } // Check if issues have been solved, by finding if some now-playable // key systems or formats were previously recorded as having issues. if (!supportedKeySystems.IsEmpty() || !playableFormats.IsEmpty()) { DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - supported key systems '%s', playable formats '%s'; See if they show issues have been solved...", this, mDocument, NS_ConvertUTF16toUTF8(supportedKeySystems).Data(), NS_ConvertUTF16toUTF8(playableFormats).get()); const nsAString* workingFormatsArray[] = { &supportedKeySystems, &playableFormats }; // For each type of notification, retrieve the pref that contains formats/ // key systems with issues. for (const NotificationAndReportStringId* id : sAllNotificationsAndReportStringIds) { nsAutoCString formatsPref("media.decoder-doctor."); formatsPref += id->mReportStringId; formatsPref += ".formats"; nsAdoptingString formatsWithIssues = Preferences::GetString(formatsPref.Data()); if (formatsWithIssues.IsEmpty()) { continue; } // See if that list of formats-with-issues contains any formats that are // now playable/supported. bool solved = false; for (const nsAString* workingFormats : workingFormatsArray) { for (const auto& workingFormat : MakeStringListRange(*workingFormats)) { if (FormatsListContains(formatsWithIssues, workingFormat)) { // This now-working format used not to work -> Report solved issue. DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - %s solved ('%s' now works, it was in pref(%s)='%s')", this, mDocument, id->mReportStringId, NS_ConvertUTF16toUTF8(workingFormat).get(), formatsPref.Data(), NS_ConvertUTF16toUTF8(formatsWithIssues).get()); ReportAnalysis(mDocument, *id, true, workingFormat); // This particular Notification&ReportId has been solved, no need // to keep looking at other keysys/formats that might solve it too. solved = true; break; } } if (solved) { break; } } if (!solved) { DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - %s not solved (pref(%s)='%s')", this, mDocument, id->mReportStringId, formatsPref.Data(), NS_ConvertUTF16toUTF8(formatsWithIssues).get()); } } } // Look at Key System issues first, as they take precedence over format checks. if (!unsupportedKeySystems.IsEmpty() && supportedKeySystems.IsEmpty()) { // No supported key systems! switch (lastKeySystemIssue) { case DecoderDoctorDiagnostics::eWidevineWithNoWMF: if (CheckSilverlight() != eSilverlightEnabled) { DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - unsupported key systems: %s, widevine without WMF nor Silverlight", this, mDocument, NS_ConvertUTF16toUTF8(unsupportedKeySystems).get()); ReportAnalysis(mDocument, sMediaWidevineNoWMFNoSilverlight, false, unsupportedKeySystems); return; } break; default: break; } } // Next, check playability of requested formats. if (!unplayableFormats.IsEmpty()) { // Some requested formats cannot be played. if (playableFormats.IsEmpty()) { // No requested formats can be played. See if we can help the user, by // going through expected decoders from most to least desirable. #if defined(XP_WIN) if (!formatsRequiringWMF.IsEmpty()) { DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - unplayable formats: %s -> Cannot play media because WMF was not found", this, mDocument, NS_ConvertUTF16toUTF8(formatsRequiringWMF).get()); ReportAnalysis(mDocument, sMediaWMFNeeded, false, formatsRequiringWMF); return; } #endif #if defined(MOZ_FFMPEG) if (!formatsRequiringFFMpeg.IsEmpty()) { switch (FFmpegRuntimeLinker::LinkStatusCode()) { case FFmpegRuntimeLinker::LinkStatus_INVALID_FFMPEG_CANDIDATE: case FFmpegRuntimeLinker::LinkStatus_UNUSABLE_LIBAV57: case FFmpegRuntimeLinker::LinkStatus_INVALID_LIBAV_CANDIDATE: case FFmpegRuntimeLinker::LinkStatus_OBSOLETE_FFMPEG: case FFmpegRuntimeLinker::LinkStatus_OBSOLETE_LIBAV: case FFmpegRuntimeLinker::LinkStatus_INVALID_CANDIDATE: DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - unplayable formats: %s -> Cannot play media because of unsupported %s (Reason: %s)", this, mDocument, NS_ConvertUTF16toUTF8(formatsRequiringFFMpeg).get(), FFmpegRuntimeLinker::LinkStatusLibraryName(), FFmpegRuntimeLinker::LinkStatusString()); ReportAnalysis(mDocument, sUnsupportedLibavcodec, false, formatsRequiringFFMpeg); return; case FFmpegRuntimeLinker::LinkStatus_INIT: MOZ_FALLTHROUGH_ASSERT("Unexpected LinkStatus_INIT"); case FFmpegRuntimeLinker::LinkStatus_SUCCEEDED: MOZ_FALLTHROUGH_ASSERT("Unexpected LinkStatus_SUCCEEDED"); case FFmpegRuntimeLinker::LinkStatus_NOT_FOUND: DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - unplayable formats: %s -> Cannot play media because platform decoder was not found (Reason: %s)", this, mDocument, NS_ConvertUTF16toUTF8(formatsRequiringFFMpeg).get(), FFmpegRuntimeLinker::LinkStatusString()); ReportAnalysis(mDocument, sMediaPlatformDecoderNotFound, false, formatsRequiringFFMpeg); return; } } #endif DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - Cannot play media, unplayable formats: %s", this, mDocument, NS_ConvertUTF16toUTF8(unplayableFormats).get()); ReportAnalysis(mDocument, sMediaCannotPlayNoDecoders, false, unplayableFormats); return; } DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - Can play media, but no decoders for some requested formats: %s", this, mDocument, NS_ConvertUTF16toUTF8(unplayableFormats).get()); if (Preferences::GetBool("media.decoder-doctor.verbose", false)) { ReportAnalysis(mDocument, sMediaNoDecoders, false, unplayableFormats); } return; } DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - Can play media, decoders available for all requested formats", this, mDocument); } void DecoderDoctorDocumentWatcher::AddDiagnostics(DecoderDoctorDiagnostics&& aDiagnostics, const char* aCallSite) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aDiagnostics.Type() != DecoderDoctorDiagnostics::eEvent); if (!mDocument) { return; } DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::AddDiagnostics(DecoderDoctorDiagnostics{%s}, call site '%s')", this, mDocument, aDiagnostics.GetDescription().Data(), aCallSite); mDiagnosticsSequence.AppendElement(Diagnostics(Move(aDiagnostics), aCallSite)); EnsureTimerIsStarted(); } NS_IMETHODIMP DecoderDoctorDocumentWatcher::Notify(nsITimer* timer) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(timer == mTimer); // Forget timer. (Assuming timer keeps itself and us alive during this call.) mTimer = nullptr; if (!mDocument) { return NS_OK; } if (mDiagnosticsSequence.Length() > mDiagnosticsHandled) { // We have new diagnostic data. mDiagnosticsHandled = mDiagnosticsSequence.Length(); SynthesizeAnalysis(); // Restart timer, to redo analysis or stop watching this document, // depending on whether anything new happens. EnsureTimerIsStarted(); } else { DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::Notify() - No new diagnostics to analyze -> Stop watching", this, mDocument); // Stop watching this document, we don't expect more diagnostics for now. // If more diagnostics come in, we'll treat them as another burst, separately. // 'true' to remove the property from the document. StopWatching(true); } return NS_OK; } void DecoderDoctorDiagnostics::StoreFormatDiagnostics(nsIDocument* aDocument, const nsAString& aFormat, bool aCanPlay, const char* aCallSite) { MOZ_ASSERT(NS_IsMainThread()); // Make sure we only store once. MOZ_ASSERT(mDiagnosticsType == eUnsaved); mDiagnosticsType = eFormatSupportCheck; if (NS_WARN_IF(!aDocument)) { DD_WARN("DecoderDoctorDiagnostics[%p]::StoreFormatDiagnostics(nsIDocument* aDocument=nullptr, format='%s', can-play=%d, call site '%s')", this, NS_ConvertUTF16toUTF8(aFormat).get(), aCanPlay, aCallSite); return; } if (NS_WARN_IF(aFormat.IsEmpty())) { DD_WARN("DecoderDoctorDiagnostics[%p]::StoreFormatDiagnostics(nsIDocument* aDocument=%p, format=, can-play=%d, call site '%s')", this, aDocument, aCanPlay, aCallSite); return; } RefPtr watcher = DecoderDoctorDocumentWatcher::RetrieveOrCreate(aDocument); if (NS_WARN_IF(!watcher)) { DD_WARN("DecoderDoctorDiagnostics[%p]::StoreFormatDiagnostics(nsIDocument* aDocument=%p, format='%s', can-play=%d, call site '%s') - Could not create document watcher", this, aDocument, NS_ConvertUTF16toUTF8(aFormat).get(), aCanPlay, aCallSite); return; } mFormat = aFormat; mCanPlay = aCanPlay; // StoreDiagnostics should only be called once, after all data is available, // so it is safe to Move() from this object. watcher->AddDiagnostics(Move(*this), aCallSite); // Even though it's moved-from, the type should stay set // (Only used to ensure that we do store only once.) MOZ_ASSERT(mDiagnosticsType == eFormatSupportCheck); } void DecoderDoctorDiagnostics::StoreMediaKeySystemAccess(nsIDocument* aDocument, const nsAString& aKeySystem, bool aIsSupported, const char* aCallSite) { MOZ_ASSERT(NS_IsMainThread()); // Make sure we only store once. MOZ_ASSERT(mDiagnosticsType == eUnsaved); mDiagnosticsType = eMediaKeySystemAccessRequest; if (NS_WARN_IF(!aDocument)) { DD_WARN("DecoderDoctorDiagnostics[%p]::StoreMediaKeySystemAccess(nsIDocument* aDocument=nullptr, keysystem='%s', supported=%d, call site '%s')", this, NS_ConvertUTF16toUTF8(aKeySystem).get(), aIsSupported, aCallSite); return; } if (NS_WARN_IF(aKeySystem.IsEmpty())) { DD_WARN("DecoderDoctorDiagnostics[%p]::StoreMediaKeySystemAccess(nsIDocument* aDocument=%p, keysystem=, supported=%d, call site '%s')", this, aDocument, aIsSupported, aCallSite); return; } RefPtr watcher = DecoderDoctorDocumentWatcher::RetrieveOrCreate(aDocument); if (NS_WARN_IF(!watcher)) { DD_WARN("DecoderDoctorDiagnostics[%p]::StoreMediaKeySystemAccess(nsIDocument* aDocument=%p, keysystem='%s', supported=%d, call site '%s') - Could not create document watcher", this, aDocument, NS_ConvertUTF16toUTF8(aKeySystem).get(), aIsSupported, aCallSite); return; } mKeySystem = aKeySystem; mIsKeySystemSupported = aIsSupported; // StoreMediaKeySystemAccess should only be called once, after all data is // available, so it is safe to Move() from this object. watcher->AddDiagnostics(Move(*this), aCallSite); // Even though it's moved-from, the type should stay set // (Only used to ensure that we do store only once.) MOZ_ASSERT(mDiagnosticsType == eMediaKeySystemAccessRequest); } void DecoderDoctorDiagnostics::StoreEvent(nsIDocument* aDocument, const DecoderDoctorEvent& aEvent, const char* aCallSite) { MOZ_ASSERT(NS_IsMainThread()); // Make sure we only store once. MOZ_ASSERT(mDiagnosticsType == eUnsaved); mDiagnosticsType = eEvent; mEvent = aEvent; if (NS_WARN_IF(!aDocument)) { DD_WARN("DecoderDoctorDiagnostics[%p]::StoreEvent(nsIDocument* aDocument=nullptr, aEvent=%s, call site '%s')", this, GetDescription().get(), aCallSite); return; } // Don't keep events for later processing, just handle them now. #ifdef MOZ_PULSEAUDIO switch (aEvent.mDomain) { case DecoderDoctorEvent::eAudioSinkStartup: if (aEvent.mResult == NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR) { DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::AddDiagnostics() - unable to initialize PulseAudio", this, aDocument); ReportAnalysis(aDocument, sCannotInitializePulseAudio, false, NS_LITERAL_STRING("*")); } else if (aEvent.mResult == NS_OK) { DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::AddDiagnostics() - now able to initialize PulseAudio", this, aDocument); ReportAnalysis(aDocument, sCannotInitializePulseAudio, true, NS_LITERAL_STRING("*")); } break; } #endif // MOZ_PULSEAUDIO } static const char* EventDomainString(DecoderDoctorEvent::Domain aDomain) { switch (aDomain) { case DecoderDoctorEvent::eAudioSinkStartup: return "audio-sink-startup"; } return "?"; } nsCString DecoderDoctorDiagnostics::GetDescription() const { nsCString s; switch (mDiagnosticsType) { case eUnsaved: s = "Unsaved diagnostics, cannot get accurate description"; break; case eFormatSupportCheck: s = "format='"; s += NS_ConvertUTF16toUTF8(mFormat).get(); s += mCanPlay ? "', can play" : "', cannot play"; if (mVideoNotSupported) { s+= ", but video format not supported"; } if (mAudioNotSupported) { s+= ", but audio format not supported"; } if (mWMFFailedToLoad) { s += ", Windows platform decoder failed to load"; } if (mFFmpegFailedToLoad) { s += ", Linux platform decoder failed to load"; } #ifdef MOZ_GMP if (mGMPPDMFailedToStartup) { s += ", GMP PDM failed to startup"; } else if (!mGMP.IsEmpty()) { s += ", Using GMP '"; s += mGMP; s += "'"; } #endif break; case eMediaKeySystemAccessRequest: s = "key system='"; s += NS_ConvertUTF16toUTF8(mKeySystem).get(); s += mIsKeySystemSupported ? "', supported" : "', not supported"; switch (mKeySystemIssue) { case eUnset: break; case eWidevineWithNoWMF: s += ", Widevine with no WMF"; break; } break; case eEvent: s = nsPrintfCString("event domain %s result=%u", EventDomainString(mEvent.mDomain), mEvent.mResult); break; default: MOZ_ASSERT_UNREACHABLE("Unexpected DiagnosticsType"); s = "?"; break; } return s; } } // namespace mozilla