diff options
Diffstat (limited to 'dom/media/DecoderDoctorDiagnostics.cpp')
-rw-r--r-- | dom/media/DecoderDoctorDiagnostics.cpp | 881 |
1 files changed, 881 insertions, 0 deletions
diff --git a/dom/media/DecoderDoctorDiagnostics.cpp b/dom/media/DecoderDoctorDiagnostics.cpp new file mode 100644 index 000000000..91c2d8dfb --- /dev/null +++ b/dom/media/DecoderDoctorDiagnostics.cpp @@ -0,0 +1,881 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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<DecoderDoctorDocumentWatcher> + 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<Diagnostics> DiagnosticsSequence; + DiagnosticsSequence mDiagnosticsSequence; + + nsCOMPtr<nsITimer> mTimer; // Keep timer alive until we run. + DiagnosticsSequence::size_type mDiagnosticsHandled = 0; +}; + + +NS_IMPL_ISUPPORTS(DecoderDoctorDocumentWatcher, nsITimerCallback) + +// static +already_AddRefed<DecoderDoctorDocumentWatcher> +DecoderDoctorDocumentWatcher::RetrieveOrCreate(nsIDocument* aDocument) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aDocument); + RefPtr<DecoderDoctorDocumentWatcher> watcher = + static_cast<DecoderDoctorDocumentWatcher*>( + 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<DecoderDoctorDocumentWatcher*>( + 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<DecoderDoctorDocumentWatcher*>(aPropertyValue); + MOZ_ASSERT(watcher); +#ifdef DEBUG + nsIDocument* document = static_cast<nsIDocument*>(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<nsIObserverService> 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() ? "<no params>" : 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<nsPluginHost> pluginHost = nsPluginHost::GetInst(); + if (!pluginHost) { + return eNoSilverlight; + } + nsTArray<nsCOMPtr<nsIInternalPluginTag>> 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()) { + if (IsVistaOrLater()) { + 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); + } else { + DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - unplayable formats: %s -> Cannot play media before Windows Vista", + this, mDocument, NS_ConvertUTF16toUTF8(formatsRequiringWMF).get()); + ReportAnalysis(mDocument, sMediaUnsupportedBeforeWindowsVista, + 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=<empty>, can-play=%d, call site '%s')", + this, aDocument, aCanPlay, aCallSite); + return; + } + + RefPtr<DecoderDoctorDocumentWatcher> 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=<empty>, supported=%d, call site '%s')", + this, aDocument, aIsSupported, aCallSite); + return; + } + + RefPtr<DecoderDoctorDocumentWatcher> 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"; + } + if (mGMPPDMFailedToStartup) { + s += ", GMP PDM failed to startup"; + } else if (!mGMP.IsEmpty()) { + s += ", Using GMP '"; + s += mGMP; + s += "'"; + } + 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 |