Adds --disable-npapi as an additional build option. The default is still to build in NPAPI compatibility, but with this flag, it's finally possible to build without pulling in GTK2 on systems that don't have the package. This essentially ifdefs everything associated with NPAPI, following various Mozilla removal bugs. Ref: BZ 1682030, BZ 1251202, BZ 1217665, BZ 1687239
877 lines
33 KiB
C++
877 lines
33 KiB
C++
/* -*- 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<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()
|
|
{
|
|
#ifdef MOZ_ENABLE_NPAPI
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
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=<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";
|
|
}
|
|
#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
|