diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /dom/media/doctor/DecoderDoctorDiagnostics.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream/115.8.0esr.tar.xz firefox-esr-upstream/115.8.0esr.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/media/doctor/DecoderDoctorDiagnostics.cpp')
-rw-r--r-- | dom/media/doctor/DecoderDoctorDiagnostics.cpp | 1319 |
1 files changed, 1319 insertions, 0 deletions
diff --git a/dom/media/doctor/DecoderDoctorDiagnostics.cpp b/dom/media/doctor/DecoderDoctorDiagnostics.cpp new file mode 100644 index 0000000000..164c614547 --- /dev/null +++ b/dom/media/doctor/DecoderDoctorDiagnostics.cpp @@ -0,0 +1,1319 @@ +/* -*- 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 <string.h> + +#include "VideoUtils.h" +#include "mozilla/Logging.h" +#include "mozilla/Preferences.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/dom/Document.h" +#include "nsContentUtils.h" +#include "nsGkAtoms.h" +#include "nsIObserverService.h" +#include "nsIScriptError.h" +#include "nsITimer.h" +#include "nsPluginHost.h" +#include "nsPrintfCString.h" + +#if defined(MOZ_FFMPEG) +# include "FFmpegRuntimeLinker.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 { + +// 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 nsINamed { + public: + static already_AddRefed<DecoderDoctorDocumentWatcher> RetrieveOrCreate( + dom::Document* aDocument); + + NS_DECL_ISUPPORTS + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + void AddDiagnostics(DecoderDoctorDiagnostics&& aDiagnostics, + const char* aCallSite); + + private: + explicit DecoderDoctorDocumentWatcher(dom::Document* 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, nsAtom* aPropertyName, + void* aPropertyValue, void* aData); + + static const uint32_t sAnalysisPeriod_ms = 1000; + void EnsureTimerIsStarted(); + + void SynthesizeAnalysis(); + // This is used for testing and will generate an analysis based on the value + // set in `media.decoder-doctor.testing.fake-error`. + void SynthesizeFakeAnalysis(); + bool ShouldSynthesizeFakeAnalysis() const; + + // Raw pointer to a Document. + // 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. + dom::Document* mDocument; + + struct Diagnostics { + Diagnostics(DecoderDoctorDiagnostics&& aDiagnostics, const char* aCallSite, + mozilla::TimeStamp aTimeStamp) + : mDecoderDoctorDiagnostics(std::move(aDiagnostics)), + mCallSite(aCallSite), + mTimeStamp(aTimeStamp) {} + Diagnostics(const Diagnostics&) = delete; + Diagnostics(Diagnostics&& aOther) + : mDecoderDoctorDiagnostics( + std::move(aOther.mDecoderDoctorDiagnostics)), + mCallSite(std::move(aOther.mCallSite)), + mTimeStamp(aOther.mTimeStamp) {} + + const DecoderDoctorDiagnostics mDecoderDoctorDiagnostics; + const nsCString mCallSite; + const mozilla::TimeStamp mTimeStamp; + }; + 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, nsINamed) + +// static +already_AddRefed<DecoderDoctorDocumentWatcher> +DecoderDoctorDocumentWatcher::RetrieveOrCreate(dom::Document* 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( + dom::Document* 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 call our DestroyPropertyCallback. + mDocument->RemoveProperty(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, nsAtom* aPropertyName, void* aPropertyValue, void*) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPropertyName == nsGkAtoms::decoderDoctor); + DecoderDoctorDocumentWatcher* watcher = + static_cast<DecoderDoctorDocumentWatcher*>(aPropertyValue); + MOZ_ASSERT(watcher); +#ifdef DEBUG + auto* document = static_cast<dom::Document*>(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) { + NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, sAnalysisPeriod_ms, + nsITimer::TYPE_ONE_SHOT); + } +} + +enum class ReportParam : uint8_t { + // Marks the end of the parameter list. + // Keep this zero! (For implicit zero-inits when used in definitions below.) + None = 0, + + Formats, + DecodeIssue, + DocURL, + ResourceURL +}; + +struct NotificationAndReportStringId { + // Notification type, handled by DecoderDoctorChild.sys.mjs and + // DecoderDoctorParent.sys.mjs. + dom::DecoderDoctorNotificationType mNotificationType; + // Console message id. Key in dom/locales/.../chrome/dom/dom.properties. + const char* mReportStringId; + static const int maxReportParams = 4; + ReportParam mReportParams[maxReportParams]; +}; + +// Note: ReportStringIds are limited to alphanumeric only. +static const NotificationAndReportStringId sMediaWidevineNoWMF = { + dom::DecoderDoctorNotificationType::Platform_decoder_not_found, + "MediaWidevineNoWMF", + {ReportParam::None}}; +static const NotificationAndReportStringId sMediaWMFNeeded = { + dom::DecoderDoctorNotificationType::Platform_decoder_not_found, + "MediaWMFNeeded", + {ReportParam::Formats}}; +static const NotificationAndReportStringId sMediaFFMpegNotFound = { + dom::DecoderDoctorNotificationType::Platform_decoder_not_found, + "MediaPlatformDecoderNotFound", + {ReportParam::Formats}}; +static const NotificationAndReportStringId sMediaCannotPlayNoDecoders = { + dom::DecoderDoctorNotificationType::Cannot_play, + "MediaCannotPlayNoDecoders", + {ReportParam::Formats}}; +static const NotificationAndReportStringId sMediaNoDecoders = { + dom::DecoderDoctorNotificationType::Can_play_but_some_missing_decoders, + "MediaNoDecoders", + {ReportParam::Formats}}; +static const NotificationAndReportStringId sCannotInitializePulseAudio = { + dom::DecoderDoctorNotificationType::Cannot_initialize_pulseaudio, + "MediaCannotInitializePulseAudio", + {ReportParam::None}}; +static const NotificationAndReportStringId sUnsupportedLibavcodec = { + dom::DecoderDoctorNotificationType::Unsupported_libavcodec, + "MediaUnsupportedLibavcodec", + {ReportParam::None}}; +static const NotificationAndReportStringId sMediaDecodeError = { + dom::DecoderDoctorNotificationType::Decode_error, + "MediaDecodeError", + {ReportParam::ResourceURL, ReportParam::DecodeIssue}}; +static const NotificationAndReportStringId sMediaDecodeWarning = { + dom::DecoderDoctorNotificationType::Decode_warning, + "MediaDecodeWarning", + {ReportParam::ResourceURL, ReportParam::DecodeIssue}}; + +static const NotificationAndReportStringId* const + sAllNotificationsAndReportStringIds[] = { + &sMediaWidevineNoWMF, &sMediaWMFNeeded, + &sMediaFFMpegNotFound, &sMediaCannotPlayNoDecoders, + &sMediaNoDecoders, &sCannotInitializePulseAudio, + &sUnsupportedLibavcodec, &sMediaDecodeError, + &sMediaDecodeWarning}; + +// Create a webcompat-friendly description of a MediaResult. +static nsString MediaResultDescription(const MediaResult& aResult, + bool aIsError) { + nsCString name; + GetErrorName(aResult.Code(), name); + return NS_ConvertUTF8toUTF16(nsPrintfCString( + "%s Code: %s (0x%08" PRIx32 ")%s%s", aIsError ? "Error" : "Warning", + name.get(), static_cast<uint32_t>(aResult.Code()), + aResult.Message().IsEmpty() ? "" : "\nDetails: ", + aResult.Message().get())); +} + +static bool IsNotificationAllowedOnPlatform( + const NotificationAndReportStringId& aNotification) { + // Allow all notifications during testing. + if (StaticPrefs::media_decoder_doctor_testing()) { + return true; + } + // These notifications are platform independent. + if (aNotification.mNotificationType == + dom::DecoderDoctorNotificationType::Cannot_play || + aNotification.mNotificationType == + dom::DecoderDoctorNotificationType:: + Can_play_but_some_missing_decoders || + aNotification.mNotificationType == + dom::DecoderDoctorNotificationType::Decode_error || + aNotification.mNotificationType == + dom::DecoderDoctorNotificationType::Decode_warning) { + return true; + } +#if defined(XP_WIN) + if (aNotification.mNotificationType == + dom::DecoderDoctorNotificationType::Platform_decoder_not_found) { + return strcmp(sMediaWMFNeeded.mReportStringId, + aNotification.mReportStringId) == 0 || + strcmp(sMediaWidevineNoWMF.mReportStringId, + aNotification.mReportStringId) == 0; + } +#endif +#if defined(MOZ_FFMPEG) + if (aNotification.mNotificationType == + dom::DecoderDoctorNotificationType::Platform_decoder_not_found) { + return strcmp(sMediaFFMpegNotFound.mReportStringId, + aNotification.mReportStringId) == 0; + } + if (aNotification.mNotificationType == + dom::DecoderDoctorNotificationType::Unsupported_libavcodec) { + return strcmp(sUnsupportedLibavcodec.mReportStringId, + aNotification.mReportStringId) == 0; + } +#endif +#ifdef MOZ_PULSEAUDIO + if (aNotification.mNotificationType == + dom::DecoderDoctorNotificationType::Cannot_initialize_pulseaudio) { + return strcmp(sCannotInitializePulseAudio.mReportStringId, + aNotification.mReportStringId) == 0; + } +#endif + return false; +} + +static void DispatchNotification( + nsISupports* aSubject, const NotificationAndReportStringId& aNotification, + bool aIsSolved, const nsAString& aFormats, const nsAString& aDecodeIssue, + const nsACString& aDocURL, const nsAString& aResourceURL) { + 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); + } + if (!aDecodeIssue.IsEmpty()) { + data.mDecodeIssue.Construct(aDecodeIssue); + } + if (!aDocURL.IsEmpty()) { + data.mDocURL.Construct(NS_ConvertUTF8toUTF16(aDocURL)); + } + if (!aResourceURL.IsEmpty()) { + data.mResourceURL.Construct(aResourceURL); + } + 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(dom::Document* aDocument, + const char* aConsoleStringId, + const nsTArray<nsString>& aParams) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aDocument); + + DD_DEBUG( + "DecoderDoctorDiagnostics.cpp:ReportToConsole(doc=%p) ReportToConsole" + " - aMsg='%s' params={%s%s%s%s}", + aDocument, aConsoleStringId, + aParams.IsEmpty() ? "<no params>" + : NS_ConvertUTF16toUTF8(aParams[0]).get(), + (aParams.Length() < 1 || aParams[0].IsEmpty()) ? "" : ", ", + (aParams.Length() < 1 || aParams[0].IsEmpty()) + ? "" + : NS_ConvertUTF16toUTF8(aParams[0]).get(), + aParams.Length() < 2 ? "" : ", ..."); + if (StaticPrefs::media_decoder_doctor_testing()) { + Unused << nsContentUtils::DispatchTrustedEvent( + aDocument, ToSupports(aDocument), u"mozreportmediaerror"_ns, + CanBubble::eNo, Cancelable::eNo); + } + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Media"_ns, + aDocument, nsContentUtils::eDOM_PROPERTIES, + aConsoleStringId, aParams); +} + +static bool AllowNotification( + const NotificationAndReportStringId& aNotification) { + // "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. + nsAutoCString filter; + Preferences::GetCString("media.decoder-doctor.notifications-allowed", filter); + return filter.EqualsLiteral("*") || + StringListContains(filter, aNotification.mReportStringId); +} + +static bool AllowDecodeIssue(const MediaResult& aDecodeIssue, + bool aDecodeIssueIsError) { + if (aDecodeIssue == NS_OK) { + // 'NS_OK' means we are not actually reporting a decode issue, so we + // allow the report. + return true; + } + + // "media.decoder-doctor.decode-{errors,warnings}-allowed" controls which + // decode issues may be dispatched to the front-end. It either contains: + // - '*' -> Allow everything. + // - Comma-separater list of ids -> Allow if the issue name is one of them. + // - Nothing (missing or empty) -> Disable everything. + nsAutoCString filter; + Preferences::GetCString(aDecodeIssueIsError + ? "media.decoder-doctor.decode-errors-allowed" + : "media.decoder-doctor.decode-warnings-allowed", + filter); + if (filter.EqualsLiteral("*")) { + return true; + } + + nsCString decodeIssueName; + GetErrorName(aDecodeIssue.Code(), static_cast<nsACString&>(decodeIssueName)); + return StringListContains(filter, decodeIssueName); +} + +static void ReportAnalysis(dom::Document* aDocument, + const NotificationAndReportStringId& aNotification, + bool aIsSolved, const nsAString& aFormats = u""_ns, + const MediaResult& aDecodeIssue = NS_OK, + bool aDecodeIssueIsError = true, + const nsACString& aDocURL = ""_ns, + const nsAString& aResourceURL = u""_ns) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!aDocument) { + return; + } + + // Some errors should only appear on the specific platform. Eg. WMF related + // error only happens on Windows. + if (!IsNotificationAllowedOnPlatform(aNotification)) { + DD_WARN("Platform doesn't support '%s'!", aNotification.mReportStringId); + return; + } + + nsString decodeIssueDescription; + if (aDecodeIssue != NS_OK) { + decodeIssueDescription.Assign( + MediaResultDescription(aDecodeIssue, aDecodeIssueIsError)); + } + + // Report non-solved issues to console. + if (!aIsSolved) { + // Build parameter array needed by console message. + AutoTArray<nsString, NotificationAndReportStringId::maxReportParams> params; + for (int i = 0; i < NotificationAndReportStringId::maxReportParams; ++i) { + if (aNotification.mReportParams[i] == ReportParam::None) { + break; + } + switch (aNotification.mReportParams[i]) { + case ReportParam::Formats: + params.AppendElement(aFormats); + break; + case ReportParam::DecodeIssue: + params.AppendElement(decodeIssueDescription); + break; + case ReportParam::DocURL: + params.AppendElement(NS_ConvertUTF8toUTF16(aDocURL)); + break; + case ReportParam::ResourceURL: + params.AppendElement(aResourceURL); + break; + default: + MOZ_ASSERT_UNREACHABLE("Bad notification parameter choice"); + break; + } + } + ReportToConsole(aDocument, aNotification.mReportStringId, params); + } + + const bool allowNotification = AllowNotification(aNotification); + const bool allowDecodeIssue = + AllowDecodeIssue(aDecodeIssue, aDecodeIssueIsError); + DD_INFO( + "ReportAnalysis for %s (decodeResult=%s) [AllowNotification=%d, " + "AllowDecodeIssue=%d]", + aNotification.mReportStringId, aDecodeIssue.ErrorName().get(), + allowNotification, allowDecodeIssue); + if (allowNotification && allowDecodeIssue) { + DispatchNotification(aDocument->GetInnerWindow(), aNotification, aIsSolved, + aFormats, decodeIssueDescription, aDocURL, + aResourceURL); + } +} + +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 += u", "_ns; + } + aList += CleanItemForFormatsList(aItem); +} + +static bool FormatsListContains(const nsAString& aList, + const nsAString& aItem) { + return StringListContains(aList, CleanItemForFormatsList(aItem)); +} + +static const char* GetLinkStatusLibraryName() { +#if defined(MOZ_FFMPEG) + return FFmpegRuntimeLinker::LinkStatusLibraryName(); +#else + return "no library (ffmpeg disabled during build)"; +#endif +} + +static const char* GetLinkStatusString() { +#if defined(MOZ_FFMPEG) + return FFmpegRuntimeLinker::LinkStatusString(); +#else + return "no link (ffmpeg disabled during build)"; +#endif +} + +void DecoderDoctorDocumentWatcher::SynthesizeFakeAnalysis() { + MOZ_ASSERT(NS_IsMainThread()); + + nsAutoCString errorType; + Preferences::GetCString("media.decoder-doctor.testing.fake-error", errorType); + MOZ_ASSERT(!errorType.IsEmpty()); + for (const auto& id : sAllNotificationsAndReportStringIds) { + if (strcmp(id->mReportStringId, errorType.get()) == 0) { + if (id->mNotificationType == + dom::DecoderDoctorNotificationType::Decode_error) { + ReportAnalysis(mDocument, *id, false /* isSolved */, u"*"_ns, + NS_ERROR_DOM_MEDIA_DECODE_ERR, true /* IsDecodeError */); + } else { + ReportAnalysis(mDocument, *id, false /* isSolved */, u"*"_ns, NS_OK, + false /* IsDecodeError */); + } + return; + } + } +} + +void DecoderDoctorDocumentWatcher::SynthesizeAnalysis() { + MOZ_ASSERT(NS_IsMainThread()); + + nsAutoString playableFormats; + nsAutoString unplayableFormats; + // Subsets of unplayableFormats that require a specific platform decoder: + nsAutoString formatsRequiringWMF; + nsAutoString formatsRequiringFFMpeg; + nsAutoString formatsLibAVCodecUnsupported; + nsAutoString supportedKeySystems; + nsAutoString unsupportedKeySystems; + DecoderDoctorDiagnostics::KeySystemIssue lastKeySystemIssue = + DecoderDoctorDiagnostics::eUnset; + // Only deal with one decode error per document (the first one found). + const MediaResult* firstDecodeError = nullptr; + const nsString* firstDecodeErrorMediaSrc = nullptr; + // Only deal with one decode warning per document (the first one found). + const MediaResult* firstDecodeWarning = nullptr; + const nsString* firstDecodeWarningMediaSrc = nullptr; + + 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 (diag.mDecoderDoctorDiagnostics.DidWMFFailToLoad()) { + AppendToFormatsList(formatsRequiringWMF, + diag.mDecoderDoctorDiagnostics.Format()); + } else if (diag.mDecoderDoctorDiagnostics.DidFFmpegNotFound()) { + AppendToFormatsList(formatsRequiringFFMpeg, + diag.mDecoderDoctorDiagnostics.Format()); + } else if (diag.mDecoderDoctorDiagnostics.IsLibAVCodecUnsupported()) { + AppendToFormatsList(formatsLibAVCodecUnsupported, + diag.mDecoderDoctorDiagnostics.Format()); + } + } + 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; + case DecoderDoctorDiagnostics::eDecodeError: + if (!firstDecodeError) { + firstDecodeError = &diag.mDecoderDoctorDiagnostics.DecodeIssue(); + firstDecodeErrorMediaSrc = + &diag.mDecoderDoctorDiagnostics.DecodeIssueMediaSrc(); + } + break; + case DecoderDoctorDiagnostics::eDecodeWarning: + if (!firstDecodeWarning) { + firstDecodeWarning = &diag.mDecoderDoctorDiagnostics.DecodeIssue(); + firstDecodeWarningMediaSrc = + &diag.mDecoderDoctorDiagnostics.DecodeIssueMediaSrc(); + } + break; + default: + MOZ_ASSERT_UNREACHABLE("Unhandled DecoderDoctorDiagnostics type"); + 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"; + nsAutoString formatsWithIssues; + Preferences::GetString(formatsPref.Data(), formatsWithIssues); + 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: + DD_INFO( + "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - " + "unsupported key systems: %s, Widevine without WMF", + this, mDocument, + NS_ConvertUTF16toUTF8(unsupportedKeySystems).get()); + ReportAnalysis(mDocument, sMediaWidevineNoWMF, false, + unsupportedKeySystems); + return; + 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 (!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; + } + if (!formatsRequiringFFMpeg.IsEmpty()) { + MOZ_DIAGNOSTIC_ASSERT(formatsLibAVCodecUnsupported.IsEmpty()); + DD_INFO( + "DecoderDoctorDocumentWatcher[%p, " + "doc=%p]::SynthesizeAnalysis() - unplayable formats: %s -> " + "Cannot play media because ffmpeg was not found (Reason: %s)", + this, mDocument, + NS_ConvertUTF16toUTF8(formatsRequiringFFMpeg).get(), + GetLinkStatusString()); + ReportAnalysis(mDocument, sMediaFFMpegNotFound, false, + formatsRequiringFFMpeg); + return; + } + if (!formatsLibAVCodecUnsupported.IsEmpty()) { + MOZ_DIAGNOSTIC_ASSERT(formatsRequiringFFMpeg.IsEmpty()); + DD_INFO( + "DecoderDoctorDocumentWatcher[%p, " + "doc=%p]::SynthesizeAnalysis() - unplayable formats: %s -> " + "Cannot play media because of unsupported %s (Reason: %s)", + this, mDocument, + NS_ConvertUTF16toUTF8(formatsLibAVCodecUnsupported).get(), + GetLinkStatusLibraryName(), GetLinkStatusString()); + ReportAnalysis(mDocument, sUnsupportedLibavcodec, false, + formatsLibAVCodecUnsupported); + return; + } + 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; + } + + if (firstDecodeError) { + DD_INFO( + "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - " + "Decode error: %s", + this, mDocument, firstDecodeError->Description().get()); + ReportAnalysis(mDocument, sMediaDecodeError, false, u""_ns, + *firstDecodeError, + true, // aDecodeIssueIsError=true + mDocument->GetDocumentURI()->GetSpecOrDefault(), + *firstDecodeErrorMediaSrc); + return; + } + + if (firstDecodeWarning) { + DD_INFO( + "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - " + "Decode warning: %s", + this, mDocument, firstDecodeWarning->Description().get()); + ReportAnalysis(mDocument, sMediaDecodeWarning, false, u""_ns, + *firstDecodeWarning, + false, // aDecodeIssueIsError=false + mDocument->GetDocumentURI()->GetSpecOrDefault(), + *firstDecodeWarningMediaSrc); + 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; + } + + const mozilla::TimeStamp now = mozilla::TimeStamp::Now(); + + constexpr size_t MaxDiagnostics = 128; + constexpr double MaxSeconds = 10.0; + while ( + mDiagnosticsSequence.Length() > MaxDiagnostics || + (!mDiagnosticsSequence.IsEmpty() && + (now - mDiagnosticsSequence[0].mTimeStamp).ToSeconds() > MaxSeconds)) { + // Too many, or too old. + mDiagnosticsSequence.RemoveElementAt(0); + if (mDiagnosticsHandled != 0) { + // Make sure Notify picks up the new element added below. + --mDiagnosticsHandled; + } + } + + DD_DEBUG( + "DecoderDoctorDocumentWatcher[%p, " + "doc=%p]::AddDiagnostics(DecoderDoctorDiagnostics{%s}, call site '%s')", + this, mDocument, aDiagnostics.GetDescription().Data(), aCallSite); + mDiagnosticsSequence.AppendElement( + Diagnostics(std::move(aDiagnostics), aCallSite, now)); + EnsureTimerIsStarted(); +} + +bool DecoderDoctorDocumentWatcher::ShouldSynthesizeFakeAnalysis() const { + if (!StaticPrefs::media_decoder_doctor_testing()) { + return false; + } + nsAutoCString errorType; + Preferences::GetCString("media.decoder-doctor.testing.fake-error", errorType); + return !errorType.IsEmpty(); +} + +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(); + + if (ShouldSynthesizeFakeAnalysis()) { + SynthesizeFakeAnalysis(); + } else { + 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; +} + +NS_IMETHODIMP +DecoderDoctorDocumentWatcher::GetName(nsACString& aName) { + aName.AssignLiteral("DecoderDoctorDocumentWatcher_timer"); + return NS_OK; +} + +void DecoderDoctorDiagnostics::StoreFormatDiagnostics(dom::Document* 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(aFormat.Length() > 2048)) { + DD_WARN( + "DecoderDoctorDiagnostics[%p]::StoreFormatDiagnostics(Document* " + "aDocument=%p, format= TOO LONG! '%s', can-play=%d, call site '%s')", + aDocument, this, NS_ConvertUTF16toUTF8(aFormat).get(), aCanPlay, + aCallSite); + return; + } + + if (NS_WARN_IF(!aDocument)) { + DD_WARN( + "DecoderDoctorDiagnostics[%p]::StoreFormatDiagnostics(Document* " + "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(Document* " + "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(Document* " + "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; + if (aCanPlay) { + mFlags += Flags::CanPlay; + } else { + mFlags -= Flags::CanPlay; + } + + // StoreDiagnostics should only be called once, after all data is available, + // so it is safe to std::move() from this object. + watcher->AddDiagnostics(std::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( + dom::Document* 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(aKeySystem.Length() > 2048)) { + DD_WARN( + "DecoderDoctorDiagnostics[%p]::StoreMediaKeySystemAccess(Document* " + "aDocument=%p, keysystem= TOO LONG! '%s', supported=%d, call site " + "'%s')", + aDocument, this, NS_ConvertUTF16toUTF8(aKeySystem).get(), aIsSupported, + aCallSite); + return; + } + + if (NS_WARN_IF(!aDocument)) { + DD_WARN( + "DecoderDoctorDiagnostics[%p]::StoreMediaKeySystemAccess(Document* " + "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(Document* " + "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(Document* " + "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 std::move() from this object. + watcher->AddDiagnostics(std::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(dom::Document* 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(Document* " + "aDocument=nullptr, aEvent=%s, call site '%s')", + this, GetDescription().get(), aCallSite); + return; + } + + // Don't keep events for later processing, just handle them now. + 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, u"*"_ns); + } else if (aEvent.mResult == NS_OK) { + DD_INFO( + "DecoderDoctorDocumentWatcher[%p, doc=%p]::AddDiagnostics() - now " + "able to initialize PulseAudio", + this, aDocument); + ReportAnalysis(aDocument, sCannotInitializePulseAudio, true, u"*"_ns); + } + break; + } +} + +void DecoderDoctorDiagnostics::StoreDecodeError(dom::Document* aDocument, + const MediaResult& aError, + const nsString& aMediaSrc, + const char* aCallSite) { + MOZ_ASSERT(NS_IsMainThread()); + // Make sure we only store once. + MOZ_ASSERT(mDiagnosticsType == eUnsaved); + mDiagnosticsType = eDecodeError; + + if (NS_WARN_IF(aError.Message().Length() > 2048)) { + DD_WARN( + "DecoderDoctorDiagnostics[%p]::StoreDecodeError(Document* " + "aDocument=%p, aError= TOO LONG! '%s', aMediaSrc=<provided>, call site " + "'%s')", + aDocument, this, aError.Description().get(), aCallSite); + return; + } + + if (NS_WARN_IF(aMediaSrc.Length() > 2048)) { + DD_WARN( + "DecoderDoctorDiagnostics[%p]::StoreDecodeError(Document* " + "aDocument=%p, aError=%s, aMediaSrc= TOO LONG! <provided>, call site " + "'%s')", + aDocument, this, aError.Description().get(), aCallSite); + return; + } + + if (NS_WARN_IF(!aDocument)) { + DD_WARN( + "DecoderDoctorDiagnostics[%p]::StoreDecodeError(" + "Document* aDocument=nullptr, aError=%s," + " aMediaSrc=<provided>, call site '%s')", + this, aError.Description().get(), aCallSite); + return; + } + + RefPtr<DecoderDoctorDocumentWatcher> watcher = + DecoderDoctorDocumentWatcher::RetrieveOrCreate(aDocument); + + if (NS_WARN_IF(!watcher)) { + DD_WARN( + "DecoderDoctorDiagnostics[%p]::StoreDecodeError(" + "Document* aDocument=%p, aError='%s', aMediaSrc=<provided>," + " call site '%s') - Could not create document watcher", + this, aDocument, aError.Description().get(), aCallSite); + return; + } + + mDecodeIssue = aError; + mDecodeIssueMediaSrc = aMediaSrc; + + // StoreDecodeError should only be called once, after all data is + // available, so it is safe to std::move() from this object. + watcher->AddDiagnostics(std::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 == eDecodeError); +} + +void DecoderDoctorDiagnostics::StoreDecodeWarning(dom::Document* aDocument, + const MediaResult& aWarning, + const nsString& aMediaSrc, + const char* aCallSite) { + MOZ_ASSERT(NS_IsMainThread()); + // Make sure we only store once. + MOZ_ASSERT(mDiagnosticsType == eUnsaved); + mDiagnosticsType = eDecodeWarning; + + if (NS_WARN_IF(!aDocument)) { + DD_WARN( + "DecoderDoctorDiagnostics[%p]::StoreDecodeWarning(" + "Document* aDocument=nullptr, aWarning=%s," + " aMediaSrc=<provided>, call site '%s')", + this, aWarning.Description().get(), aCallSite); + return; + } + + RefPtr<DecoderDoctorDocumentWatcher> watcher = + DecoderDoctorDocumentWatcher::RetrieveOrCreate(aDocument); + + if (NS_WARN_IF(!watcher)) { + DD_WARN( + "DecoderDoctorDiagnostics[%p]::StoreDecodeWarning(" + "Document* aDocument=%p, aWarning='%s', aMediaSrc=<provided>," + " call site '%s') - Could not create document watcher", + this, aDocument, aWarning.Description().get(), aCallSite); + return; + } + + mDecodeIssue = aWarning; + mDecodeIssueMediaSrc = aMediaSrc; + + // StoreDecodeWarning should only be called once, after all data is + // available, so it is safe to std::move() from this object. + watcher->AddDiagnostics(std::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 == eDecodeWarning); +} + +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 += mFlags.contains(Flags::CanPlay) ? "', can play" : "', cannot play"; + if (mFlags.contains(Flags::VideoNotSupported)) { + s += ", but video format not supported"; + } + if (mFlags.contains(Flags::AudioNotSupported)) { + s += ", but audio format not supported"; + } + if (mFlags.contains(Flags::WMFFailedToLoad)) { + s += ", Windows platform decoder failed to load"; + } + if (mFlags.contains(Flags::FFmpegNotFound)) { + s += ", Linux platform decoder not found"; + } + if (mFlags.contains(Flags::GMPPDMFailedToStartup)) { + 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=%" PRIu32, + EventDomainString(mEvent.mDomain), + static_cast<uint32_t>(mEvent.mResult)); + break; + case eDecodeError: + s = "decode error: "; + s += mDecodeIssue.Description(); + s += ", src='"; + s += mDecodeIssueMediaSrc.IsEmpty() ? "<none>" : "<provided>"; + s += "'"; + break; + case eDecodeWarning: + s = "decode warning: "; + s += mDecodeIssue.Description(); + s += ", src='"; + s += mDecodeIssueMediaSrc.IsEmpty() ? "<none>" : "<provided>"; + s += "'"; + break; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected DiagnosticsType"); + s = "?"; + break; + } + return s; +} + +static const char* ToDecoderDoctorReportTypeStr( + const dom::DecoderDoctorReportType& aType) { + switch (aType) { + case dom::DecoderDoctorReportType::Mediawidevinenowmf: + return sMediaWidevineNoWMF.mReportStringId; + case dom::DecoderDoctorReportType::Mediawmfneeded: + return sMediaWMFNeeded.mReportStringId; + case dom::DecoderDoctorReportType::Mediaplatformdecodernotfound: + return sMediaFFMpegNotFound.mReportStringId; + case dom::DecoderDoctorReportType::Mediacannotplaynodecoders: + return sMediaCannotPlayNoDecoders.mReportStringId; + case dom::DecoderDoctorReportType::Medianodecoders: + return sMediaNoDecoders.mReportStringId; + case dom::DecoderDoctorReportType::Mediacannotinitializepulseaudio: + return sCannotInitializePulseAudio.mReportStringId; + case dom::DecoderDoctorReportType::Mediaunsupportedlibavcodec: + return sUnsupportedLibavcodec.mReportStringId; + case dom::DecoderDoctorReportType::Mediadecodeerror: + return sMediaDecodeError.mReportStringId; + case dom::DecoderDoctorReportType::Mediadecodewarning: + return sMediaDecodeWarning.mReportStringId; + default: + DD_DEBUG("Invalid report type to str"); + return "invalid-report-type"; + } +} + +void DecoderDoctorDiagnostics::SetDecoderDoctorReportType( + const dom::DecoderDoctorReportType& aType) { + DD_INFO("Set report type %s", ToDecoderDoctorReportTypeStr(aType)); + switch (aType) { + case dom::DecoderDoctorReportType::Mediawmfneeded: + SetWMFFailedToLoad(); + return; + case dom::DecoderDoctorReportType::Mediaplatformdecodernotfound: + SetFFmpegNotFound(); + return; + case dom::DecoderDoctorReportType::Mediaunsupportedlibavcodec: + SetLibAVCodecUnsupported(); + return; + case dom::DecoderDoctorReportType::Mediacannotplaynodecoders: + case dom::DecoderDoctorReportType::Medianodecoders: + // Do nothing, because these type are related with can-play, which would + // be handled in `StoreFormatDiagnostics()` when sending `false` in the + // parameter for the canplay. + return; + default: + DD_DEBUG("Not supported type"); + return; + } +} + +} // namespace mozilla |