/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */ var gEMEHandler = { get uiEnabled() { #ifdef MOZ_EME let emeUIEnabled = Services.prefs.getBoolPref("browser.eme.ui.enabled"); // Force-disable on WinXP: if (navigator.platform.toLowerCase().startsWith("win")) { emeUIEnabled = emeUIEnabled && parseFloat(Services.sysinfo.get("version")) >= 6; } return emeUIEnabled; #else return false; #endif }, ensureEMEEnabled: function(browser, keySystem) { Services.prefs.setBoolPref("media.eme.enabled", true); if (keySystem) { if (keySystem.startsWith("com.adobe") && Services.prefs.getPrefType("media.gmp-eme-adobe.enabled") && !Services.prefs.getBoolPref("media.gmp-eme-adobe.enabled")) { Services.prefs.setBoolPref("media.gmp-eme-adobe.enabled", true); } else if (keySystem == "com.widevine.alpha" && Services.prefs.getPrefType("media.gmp-widevinecdm.enabled") && !Services.prefs.getBoolPref("media.gmp-widevinecdm.enabled")) { Services.prefs.setBoolPref("media.gmp-widevinecdm.enabled", true); } } browser.reload(); }, isKeySystemVisible: function(keySystem) { if (!keySystem) { return false; } if (keySystem.startsWith("com.adobe") && Services.prefs.getPrefType("media.gmp-eme-adobe.visible")) { return Services.prefs.getBoolPref("media.gmp-eme-adobe.visible"); } if (keySystem == "com.widevine.alpha" && Services.prefs.getPrefType("media.gmp-widevinecdm.visible")) { return Services.prefs.getBoolPref("media.gmp-widevinecdm.visible"); } return true; }, getEMEDisabledFragment: function(msgId) { let mainMessage = gNavigatorBundle.getString("emeNotifications.drmContentDisabled.message"); let [prefix, suffix] = mainMessage.split(/%(?:\$\d)?S/).map(s => document.createTextNode(s)); let text = gNavigatorBundle.getString("emeNotifications.drmContentDisabled.learnMoreLabel"); let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL"); let link = document.createElement("label"); link.className = "text-link"; link.setAttribute("href", baseURL + "drm-content"); link.textContent = text; let fragment = document.createDocumentFragment(); [prefix, link, suffix].forEach(n => fragment.appendChild(n)); return fragment; }, getMessageWithBrandName: function(notificationId) { let msgId = "emeNotifications." + notificationId + ".message"; return gNavigatorBundle.getFormattedString(msgId, [this._brandShortName]); }, receiveMessage: function({target: browser, data: data}) { let parsedData; try { parsedData = JSON.parse(data); } catch (ex) { Cu.reportError("Malformed EME video message with data: " + data); return; } let {status: status, keySystem: keySystem} = parsedData; // Don't need to show if disabled or keysystem not visible. if (!this.uiEnabled || !this.isKeySystemVisible(keySystem)) { return; } let notificationId; let buttonCallback; // Notification message can be either a string or a DOM fragment. let notificationMessage; switch (status) { case "available": case "cdm-created": // Only show the chain icon for proprietary CDMs. Clearkey is not one. if (keySystem != "org.w3.clearkey") { this.showPopupNotificationForSuccess(browser, keySystem); } // ... and bail! return; case "api-disabled": case "cdm-disabled": notificationId = "drmContentDisabled"; buttonCallback = gEMEHandler.ensureEMEEnabled.bind(gEMEHandler, browser, keySystem) notificationMessage = this.getEMEDisabledFragment(); break; case "cdm-insufficient-version": notificationId = "drmContentCDMInsufficientVersion"; notificationMessage = this.getMessageWithBrandName(notificationId); break; case "cdm-not-installed": notificationId = "drmContentCDMInstalling"; notificationMessage = this.getMessageWithBrandName(notificationId); break; case "cdm-not-supported": // Not to pop up user-level notification because they cannot do anything // about it. return; default: Cu.reportError(new Error("Unknown message ('" + status + "') dealing with EME key request: " + data)); return; } // Now actually create the notification let box = gBrowser.getNotificationBox(browser); if (box.getNotificationWithValue(notificationId)) { return; } let buttons = []; if (buttonCallback) { let msgPrefix = "emeNotifications." + notificationId + "."; let btnLabelId = msgPrefix + "button.label"; let btnAccessKeyId = msgPrefix + "button.accesskey"; buttons.push({ label: gNavigatorBundle.getString(btnLabelId), accessKey: gNavigatorBundle.getString(btnAccessKeyId), callback: buttonCallback }); } let iconURL = "chrome://browser/skin/drm-icon.svg#chains-black"; box.appendNotification(notificationMessage, notificationId, iconURL, box.PRIORITY_WARNING_MEDIUM, buttons); }, showPopupNotificationForSuccess: function(browser, keySystem) { // We're playing EME content! Remove any "we can't play because..." messages. var box = gBrowser.getNotificationBox(browser); ["drmContentDisabled", "drmContentCDMInstalling" ].forEach(function (value) { var notification = box.getNotificationWithValue(value); if (notification) box.removeNotification(notification); }); // Don't bother creating it if it's already there: if (PopupNotifications.getNotification("drmContentPlaying", browser)) { return; } let msgPrefix = "emeNotifications.drmContentPlaying."; let msgId = msgPrefix + "message2"; let btnLabelId = msgPrefix + "button.label"; let btnAccessKeyId = msgPrefix + "button.accesskey"; let message = gNavigatorBundle.getFormattedString(msgId, [this._brandShortName]); let anchorId = "eme-notification-icon"; let firstPlayPref = "browser.eme.ui.firstContentShown"; if (!Services.prefs.getPrefType(firstPlayPref) || !Services.prefs.getBoolPref(firstPlayPref)) { document.getElementById(anchorId).setAttribute("firstplay", "true"); Services.prefs.setBoolPref(firstPlayPref, true); } else { document.getElementById(anchorId).removeAttribute("firstplay"); } let mainAction = { label: gNavigatorBundle.getString(btnLabelId), accessKey: gNavigatorBundle.getString(btnAccessKeyId), callback: function() { openPreferences("paneContent"); }, dismiss: true }; let options = { dismissed: true, eventCallback: aTopic => aTopic == "swapping", learnMoreURL: Services.urlFormatter.formatURLPref("app.support.baseURL") + "drm-content", }; PopupNotifications.show(browser, "drmContentPlaying", message, anchorId, mainAction, null, options); }, QueryInterface: XPCOMUtils.generateQI([Ci.nsIMessageListener]) }; XPCOMUtils.defineLazyGetter(gEMEHandler, "_brandShortName", function() { return document.getElementById("bundle_brand").getString("brandShortName"); }); const TELEMETRY_DDSTAT_SHOWN = 0; const TELEMETRY_DDSTAT_SHOWN_FIRST = 1; const TELEMETRY_DDSTAT_CLICKED = 2; const TELEMETRY_DDSTAT_CLICKED_FIRST = 3; const TELEMETRY_DDSTAT_SOLVED = 4; let gDecoderDoctorHandler = { getLabelForNotificationBox(type) { if (type == "adobe-cdm-not-found" && AppConstants.platform == "win") { return gNavigatorBundle.getString("decoder.noCodecs.message"); } if (type == "adobe-cdm-not-activated" && AppConstants.platform == "win") { return gNavigatorBundle.getString("decoder.noCodecs.message"); } if (type == "platform-decoder-not-found") { if (AppConstants.platform == "win") { return gNavigatorBundle.getString("decoder.noHWAcceleration.message"); } if (AppConstants.platform == "linux") { return gNavigatorBundle.getString("decoder.noCodecsLinux.message"); } } if (type == "cannot-initialize-pulseaudio") { return gNavigatorBundle.getString("decoder.noPulseAudio.message"); } if (type == "unsupported-libavcodec" && AppConstants.platform == "linux") { return gNavigatorBundle.getString("decoder.unsupportedLibavcodec.message"); } return ""; }, getSumoForLearnHowButton(type) { if (AppConstants.platform == "win") { return "fix-video-audio-problems-firefox-windows"; } if (type == "cannot-initialize-pulseaudio") { return "fix-common-audio-and-video-issues"; } return ""; }, receiveMessage({target: browser, data: data}) { let box = gBrowser.getNotificationBox(browser); let notificationId = "decoder-doctor-notification"; if (box.getNotificationWithValue(notificationId)) { return; } let parsedData; try { parsedData = JSON.parse(data); } catch (ex) { Cu.reportError("Malformed Decoder Doctor message with data: " + 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. // - '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. // - 'isSolved' is true when the notification actually indicates the // resolution of that issue, to be reported as telemetry. let {type, isSolved, decoderDoctorReportId, formats} = parsedData; type = type.toLowerCase(); // Error out early on invalid ReportId if (!(/^\w+$/mi).test(decoderDoctorReportId)) { return } let title = gDecoderDoctorHandler.getLabelForNotificationBox(type); 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 now allowed.) let formatsPref = "media.decoder-doctor." + decoderDoctorReportId + ".formats"; let buttonClickedPref = "media.decoder-doctor." + decoderDoctorReportId + ".button-clicked"; let histogram = Services.telemetry.getKeyedHistogramById("DECODER_DOCTOR_INFOBAR_STATS"); let formatsInPref = Services.prefs.getPrefType(formatsPref) && Services.prefs.getCharPref(formatsPref); if (!isSolved) { if (!formats) { Cu.reportError("Malformed Decoder Doctor unsolved message with no formats"); return; } if (!formatsInPref) { Services.prefs.setCharPref(formatsPref, formats); histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_SHOWN_FIRST); } else { // Split existing formats into an array of strings. let existing = formatsInPref.split(",").map(String.trim); // Keep given formats that were not already recorded. let newbies = formats.split(",").map(String.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(", ")); } } histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_SHOWN); let buttons = []; let sumo = gDecoderDoctorHandler.getSumoForLearnHowButton(type); if (sumo) { buttons.push({ label: gNavigatorBundle.getString("decoder.noCodecs.button"), accessKey: gNavigatorBundle.getString("decoder.noCodecs.accesskey"), callback() { let clickedInPref = Services.prefs.getPrefType(buttonClickedPref) && Services.prefs.getBoolPref(buttonClickedPref); if (!clickedInPref) { Services.prefs.setBoolPref(buttonClickedPref, true); histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_CLICKED_FIRST); } histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_CLICKED); let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL"); openUILinkIn(baseURL + sumo, "tab"); } }); } box.appendNotification( title, notificationId, "", // This uses the info icon as specified below. 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); histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_SOLVED); } }, } window.getGroupMessageManager("browsers").addMessageListener("DecoderDoctor:Notification", gDecoderDoctorHandler); window.getGroupMessageManager("browsers").addMessageListener("EMEVideo:ContentMediaKeysRequest", gEMEHandler); window.addEventListener("unload", function() { window.getGroupMessageManager("browsers").removeMessageListener("EMEVideo:ContentMediaKeysRequest", gEMEHandler); window.getGroupMessageManager("browsers").removeMessageListener("DecoderDoctor:Notification", gDecoderDoctorHandler); }, false);