/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set ts=2 sw=2 sts=2 et tw=80: */ /* 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/. */ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineLazyGetter(lazy, "gNavigatorBundle", function () { return Services.strings.createBundle( "chrome://browser/locale/browser.properties" ); }); XPCOMUtils.defineLazyPreferenceGetter( lazy, "DEBUG_LOG", "media.decoder-doctor.testing", false ); function LOG_DD(message) { if (lazy.DEBUG_LOG) { dump("[DecoderDoctorParent] " + message + "\n"); } } export class DecoderDoctorParent extends JSWindowActorParent { getLabelForNotificationBox({ type, decoderDoctorReportId }) { if (type == "platform-decoder-not-found") { if (decoderDoctorReportId == "MediaWMFNeeded") { return lazy.gNavigatorBundle.GetStringFromName( "decoder.noHWAcceleration.message" ); } // Although this name seems generic, this is actually for not being able // to find libavcodec on Linux. if (decoderDoctorReportId == "MediaPlatformDecoderNotFound") { return lazy.gNavigatorBundle.GetStringFromName( "decoder.noCodecsLinux.message" ); } } if (type == "cannot-initialize-pulseaudio") { return lazy.gNavigatorBundle.GetStringFromName( "decoder.noPulseAudio.message" ); } if (type == "unsupported-libavcodec" && AppConstants.platform == "linux") { return lazy.gNavigatorBundle.GetStringFromName( "decoder.unsupportedLibavcodec.message" ); } if (type == "decode-error") { return lazy.gNavigatorBundle.GetStringFromName( "decoder.decodeError.message" ); } if (type == "decode-warning") { return lazy.gNavigatorBundle.GetStringFromName( "decoder.decodeWarning.message" ); } return ""; } getSumoForLearnHowButton({ type, decoderDoctorReportId }) { if ( type == "platform-decoder-not-found" && decoderDoctorReportId == "MediaWMFNeeded" ) { return "fix-video-audio-problems-firefox-windows"; } if (type == "cannot-initialize-pulseaudio") { return "fix-common-audio-and-video-issues"; } return ""; } getEndpointForReportIssueButton(type) { if (type == "decode-error" || type == "decode-warning") { return Services.prefs.getStringPref( "media.decoder-doctor.new-issue-endpoint", "" ); } return ""; } receiveMessage(aMessage) { // The top level browsing context's embedding element should be a xul browser element. let browser = this.browsingContext.top.embedderElement; // The xul browser is owned by a window. let window = browser?.ownerGlobal; if (!browser || !window) { // We don't have a browser or window so bail! return; } let box = browser.getTabBrowser().getNotificationBox(browser); let notificationId = "decoder-doctor-notification"; if (box.getNotificationWithValue(notificationId)) { // We already have a notification showing, bail. return; } let parsedData; try { parsedData = JSON.parse(aMessage.data); } catch (ex) { console.error( "Malformed Decoder Doctor message with data: ", aMessage.data ); return; } // parsedData (the result of parsing the incoming 'data' json string) // contains analysis information from Decoder Doctor: // - 'type' is the type of issue, it determines which text to show in the // infobar. // - 'isSolved' is true when the notification actually indicates the // resolution of that issue, to be reported as telemetry. // - 'decoderDoctorReportId' is the Decoder Doctor issue identifier, to be // used here as key for the telemetry (counting infobar displays, // "Learn how" buttons clicks, and resolutions) and for the prefs used // to store at-issue formats. // - 'formats' contains a comma-separated list of formats (or key systems) // that suffer the issue. These are kept in a pref, which the backend // uses to later find when an issue is resolved. // - 'decodeIssue' is a description of the decode error/warning. // - 'resourceURL' is the resource with the issue. let { type, isSolved, decoderDoctorReportId, formats, decodeIssue, docURL, resourceURL, } = parsedData; type = type.toLowerCase(); // Error out early on invalid ReportId if (!/^\w+$/im.test(decoderDoctorReportId)) { return; } LOG_DD( `type=${type}, isSolved=${isSolved}, ` + `decoderDoctorReportId=${decoderDoctorReportId}, formats=${formats}, ` + `decodeIssue=${decodeIssue}, docURL=${docURL}, ` + `resourceURL=${resourceURL}` ); let title = this.getLabelForNotificationBox({ type, decoderDoctorReportId, }); if (!title) { return; } // We keep the list of formats in prefs for the sake of the decoder itself, // which reads it to determine when issues get solved for these formats. // (Writing prefs from e10s content is not allowed.) let formatsPref = formats && "media.decoder-doctor." + decoderDoctorReportId + ".formats"; let buttonClickedPref = "media.decoder-doctor." + decoderDoctorReportId + ".button-clicked"; let formatsInPref = formats && Services.prefs.getCharPref(formatsPref, ""); if (!isSolved) { if (formats) { if (!formatsInPref) { Services.prefs.setCharPref(formatsPref, formats); } else { // Split existing formats into an array of strings. let existing = formatsInPref.split(",").map(x => x.trim()); // Keep given formats that were not already recorded. let newbies = formats .split(",") .map(x => x.trim()) .filter(x => !existing.includes(x)); // And rewrite pref with the added new formats (if any). if (newbies.length) { Services.prefs.setCharPref( formatsPref, existing.concat(newbies).join(", ") ); } } } else if (!decodeIssue) { console.error( "Malformed Decoder Doctor unsolved message with no formats nor decode issue" ); return; } let buttons = []; let sumo = this.getSumoForLearnHowButton({ type, decoderDoctorReportId }); if (sumo) { LOG_DD(`sumo=${sumo}`); buttons.push({ label: lazy.gNavigatorBundle.GetStringFromName( "decoder.noCodecs.button" ), supportPage: sumo, callback() { let clickedInPref = Services.prefs.getBoolPref( buttonClickedPref, false ); if (!clickedInPref) { Services.prefs.setBoolPref(buttonClickedPref, true); } }, }); } let endpoint = this.getEndpointForReportIssueButton(type); if (endpoint) { LOG_DD(`endpoint=${endpoint}`); buttons.push({ label: lazy.gNavigatorBundle.GetStringFromName( "decoder.decodeError.button" ), accessKey: lazy.gNavigatorBundle.GetStringFromName( "decoder.decodeError.accesskey" ), callback() { let clickedInPref = Services.prefs.getBoolPref( buttonClickedPref, false ); if (!clickedInPref) { Services.prefs.setBoolPref(buttonClickedPref, true); } let params = new URLSearchParams(); params.append("url", docURL); params.append("label", "type-media"); params.append("problem_type", "video_bug"); params.append("src", "media-decode-error"); let details = { "Technical Information:": decodeIssue }; if (resourceURL) { details["Resource:"] = resourceURL; } params.append("details", JSON.stringify(details)); window.openTrustedLinkIn(endpoint + "?" + params.toString(), "tab"); }, }); } box.appendNotification( notificationId, { label: title, image: "", // This uses the info icon as specified below. priority: box.PRIORITY_INFO_LOW, }, buttons ); } else if (formatsInPref) { // Issue is solved, and prefs haven't been cleared yet, meaning it's the // first time we get this resolution -> Clear prefs and report telemetry. Services.prefs.clearUserPref(formatsPref); Services.prefs.clearUserPref(buttonClickedPref); } } }