Bug 1111153 - show error notifications for broken EME content, r=florian
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Fri, 20 Feb 2015 10:38:18 +0100
changeset 229942 80bd1ae9dd0af4a6e12f2a4cdf448179de1ae2e1
parent 229941 ee9a8d36a96ab43388b2f38b2327a94ea88e0aff
child 229977 5f1009731a977b83d2b177099c6ae3b12085ec7a
child 230048 9cb1667fd7f0c02fa4c4dfcaa83940d7272a63ce
push id28303
push usercbook@mozilla.com
push dateFri, 20 Feb 2015 14:07:30 +0000
treeherdermozilla-central@80bd1ae9dd0a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersflorian
bugs1111153
milestone38.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1111153 - show error notifications for broken EME content, r=florian
browser/base/content/browser-eme.js
browser/base/content/browser.xul
browser/locales/en-US/chrome/browser/browser.dtd
browser/locales/en-US/chrome/browser/browser.properties
browser/modules/ContentObservers.jsm
browser/themes/shared/drm-icon.svg
toolkit/content/widgets/notification.xml
--- a/browser/base/content/browser-eme.js
+++ b/browser/base/content/browser-eme.js
@@ -1,36 +1,191 @@
 # -*- 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/.
 
-function gEMEListener(msg /*{target: browser, data: data} */) {
-  let browser = msg.target;
-  let notificationId = "drmContentPlaying";
-  // Don't need to show if disabled, nor reshow if it's already there
-  if (!Services.prefs.getBoolPref("browser.eme.ui.enabled") ||
-      PopupNotifications.getNotification(notificationId, browser)) {
-    return;
-  }
+let gEMEHandler = {
+  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 == "org.w3.clearkey" &&
+                 Services.prefs.getPrefType("media.eme.clearkey.enabled") &&
+                 !Services.prefs.getBoolPref("media.eme.clearkey.enabled")) {
+        Services.prefs.setBoolPref("media.eme.clearkey.enabled", true);
+      }
+    }
+    browser.reload();
+  },
+  getLearnMoreLink: function(msgId) {
+    let text = gNavigatorBundle.getString("emeNotifications." + msgId + ".learnMoreLabel");
+    let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+    return "<label class='text-link' href='" + baseURL + "drm-content'>" +
+           text + "</label>";
+  },
+  getDRMLabel: function(keySystem) {
+    if (keySystem.startsWith("com.adobe")) {
+      return "Adobe Primetime";
+    }
+    if (keySystem == "org.w3.clearkey") {
+      return "ClearKey";
+    }
+    return gNavigatorBundle.getString("emeNotifications.unknownDRMSoftware");
+  },
+  onDontAskAgain: function(menuPopupItem) {
+    let button = menuPopupItem.parentNode.anchorNode;
+    let bar = button.parentNode;
+    Services.prefs.setBoolPref("browser.eme.ui." + bar.value + ".disabled", true);
+    bar.close();
+  },
+  onNotNow: function(menuPopupItem) {
+    let button = menuPopupItem.parentNode.anchorNode;
+    button.parentNode.close();
+  },
+  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
+    if (!Services.prefs.getBoolPref("browser.eme.ui.enabled")) {
+      return;
+    }
+
+    let notificationId;
+    let buttonCallback;
+    let params = [];
+    switch (status) {
+      case "available":
+      case "cdm-created":
+        this.showPopupNotificationForSuccess(browser, keySystem);
+        // ... and bail!
+        return;
+
+      case "api-disabled":
+      case "cdm-disabled":
+        notificationId = "drmContentDisabled";
+        buttonCallback = gEMEHandler.ensureEMEEnabled.bind(gEMEHandler, browser, keySystem)
+        params = [this.getLearnMoreLink(notificationId)];
+        break;
+
+      case "cdm-not-supported":
+        notificationId = "drmContentCDMNotSupported";
+        params = [this._brandShortName, this.getLearnMoreLink(notificationId)];
+        break;
+
+      case "cdm-insufficient-version":
+        notificationId = "drmContentCDMInsufficientVersion";
+        params = [this._brandShortName];
+        break;
+
+      case "cdm-not-installed":
+        notificationId = "drmContentCDMInstalling";
+        params = [this._brandShortName];
+        break;
 
-  let msgId = "emeNotifications.drmContentPlaying.message";
-  let brandName = document.getElementById("bundle_brand").getString("brandShortName");
-  let message = gNavigatorBundle.getFormattedString(msgId, [msg.data.drmProvider, brandName]);
-  let anchorId = "eme-notification-icon";
+      case "error":
+        // Fall through and do the same for unknown messages:
+      default:
+        let typeOfIssue = status == "error" ? "error" : "message ('" + status + "')";
+        Cu.reportError("Unknown " + typeOfIssue + " dealing with EME key request: " + data);
+        return;
+    }
+
+    this.showNotificationBar(browser, notificationId, params, buttonCallback);
+  },
+  showNotificationBar: function(browser, notificationId, labelParams, callback) {
+    let box = gBrowser.getNotificationBox(browser);
+    if (box.getNotificationWithValue(notificationId)) {
+      return;
+    }
+
+    // If the user turned these off, bail out:
+    try {
+      if (Services.prefs.getBoolPref("browser.eme.ui." + notificationId + ".disabled")) {
+        return;
+      }
+    } catch (ex) { /* Don't care if the pref doesn't exist */ }
+
+    let msgPrefix = "emeNotifications." + notificationId + ".";
+    let msgId = msgPrefix + "message";
+
+    let message = labelParams.length ?
+                  gNavigatorBundle.getFormattedString(msgId, labelParams) :
+                  gNavigatorBundle.getString(msgId);
+
+    let buttons = [];
+    if (callback) {
+      let btnLabelId = msgPrefix + "button.label";
+      let btnAccessKeyId = msgPrefix + "button.accesskey";
+      buttons.push({
+        label: gNavigatorBundle.getString(btnLabelId),
+        accesskey: gNavigatorBundle.getString(btnAccessKeyId),
+        callback: callback
+      });
 
-  let mainAction = {
-    label: gNavigatorBundle.getString("emeNotifications.drmContentPlaying.button.label"),
-    accessKey: gNavigatorBundle.getString("emeNotifications.drmContentPlaying.button.accesskey"),
-    callback: function() { openPreferences("paneContent"); },
-    dismiss: true
-  };
-  let options = {
-    dismissed: true,
-    eventCallback: aTopic => aTopic == "swapping",
-  };
-  PopupNotifications.show(browser, notificationId, message, anchorId, mainAction, null, options);
+      let optionsId = "emeNotifications.optionsButton";
+      buttons.push({
+        label: gNavigatorBundle.getString(optionsId + ".label"),
+        accesskey: gNavigatorBundle.getString(optionsId + ".accesskey"),
+        popup: "emeNotificationsPopup"
+      });
+    }
+
+    let iconURL = "chrome://browser/skin/drm-icon.svg#chains-black";
+
+    // Do a little dance to get rich content into the notification:
+    let fragment = document.createDocumentFragment();
+    let descriptionContainer = document.createElement("description");
+    descriptionContainer.innerHTML = message;
+    while (descriptionContainer.childNodes.length) {
+      fragment.appendChild(descriptionContainer.childNodes[0]);
+    }
+
+    box.appendNotification(fragment, notificationId, iconURL, box.PRIORITY_WARNING_MEDIUM,
+                           buttons);
+  },
+  showPopupNotificationForSuccess: function(browser, keySystem) {
+    // Don't bother creating it if it's already there:
+    if (PopupNotifications.getNotification("drmContentPlaying", browser)) {
+      return;
+    }
+
+    let msgPrefix = "emeNotifications.drmContentPlaying.";
+    let msgId = msgPrefix + "message";
+    let btnLabelId = msgPrefix + "button.label";
+    let btnAccessKeyId = msgPrefix + "button.accesskey";
+
+    let drmProvider = this.getDRMLabel(keySystem);
+    let message = gNavigatorBundle.getFormattedString(msgId, [drmProvider, this._brandShortName]);
+    let anchorId = "eme-notification-icon";
+
+    let mainAction = {
+      label: gNavigatorBundle.getString(btnLabelId),
+      accessKey: gNavigatorBundle.getString(btnAccessKeyId),
+      callback: function() { openPreferences("paneContent"); },
+      dismiss: true
+    };
+    let options = {
+      dismissed: true,
+      eventCallback: aTopic => aTopic == "swapping",
+    };
+    PopupNotifications.show(browser, "drmContentPlaying", message, anchorId, mainAction, null, options);
+  },
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMessageListener])
 };
 
-window.messageManager.addMessageListener("EMEVideo:MetadataLoaded", gEMEListener);
+XPCOMUtils.defineLazyGetter(gEMEHandler, "_brandShortName", function() {
+  return document.getElementById("bundle_brand").getString("brandShortName");
+});
+
+window.messageManager.addMessageListener("EMEVideo:ContentMediaKeysRequest", gEMEHandler);
 window.addEventListener("unload", function() {
-  window.messageManager.removeMessageListener("EMEVideo:MetadataLoaded", gEMEListener);
+  window.messageManager.removeMessageListener("EMEVideo:ContentMediaKeysRequest", gEMEHandler);
 }, false);
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -562,16 +562,27 @@
       </vbox>
       <vbox id="bookmarked-notification-dropmarker-anchor">
         <image id="bookmarked-notification-dropmarker-icon"/>
       </vbox>
     </hbox>
 
     <tooltip id="dynamic-shortcut-tooltip"
              onpopupshowing="UpdateDynamicShortcutTooltipText(this);"/>
+
+    <menupopup id="emeNotificationsPopup">
+      <menuitem id="emeNotificationsNotNow"
+                label="&emeNotificationsNotNow.label;"
+                acceskey="&emeNotificationsNotNow.accesskey;"
+                oncommand="gEMEHandler.onNotNow(this);"/>
+      <menuitem id="emeNotificationsDontAskAgain"
+                label="&emeNotificationsDontAskAgain.label;"
+                acceskey="&emeNotificationsDontAskAgain.accesskey;"
+                oncommand="gEMEHandler.onDontAskAgain(this);"/>
+    </menupopup>
   </popupset>
 
 #ifdef CAN_DRAW_IN_TITLEBAR
 <vbox id="titlebar">
   <hbox id="titlebar-content">
     <spacer id="titlebar-spacer" flex="1"/>
     <hbox id="titlebar-buttonbox-container">
 #ifdef XP_WIN
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -835,8 +835,13 @@ just addresses the organization to follo
 <!ENTITY processHang.debugScript.accessKey        "D">
 <!ENTITY processHang.terminatePlugin.label        "Kill Plugin">
 <!ENTITY processHang.terminatePlugin.accessKey    "P">
 <!ENTITY processHang.terminateProcess.label       "Kill Web Process">
 <!ENTITY processHang.terminateProcess.accessKey   "K">
 
 <!ENTITY emeLearnMoreContextMenu.label            "Learn more about DRM…">
 <!ENTITY emeLearnMoreContextMenu.accesskey        "D">
+
+<!ENTITY emeNotificationsNotNow.label             "Not now">
+<!ENTITY emeNotificationsNotNow.accesskey         "N">
+<!ENTITY emeNotificationsDontAskAgain.label       "Don't ask me again">
+<!ENTITY emeNotificationsDontAskAgain.accesskey   "D">
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -591,16 +591,39 @@ getUserMedia.sharingMenuMicrophoneWindow
 # origin for the sharing menu if no readable origin could be deduced from the URL.
 getUserMedia.sharingMenuUnknownHost = Unknown origin
 
 # LOCALIZATION NOTE(emeNotifications.drmContentPlaying.message): %1$S is the vendor name of the DRM that's in use, %2$S is brandShortName.
 emeNotifications.drmContentPlaying.message = Some audio or video on this site uses %1$S DRM software, which may limit what %2$S can let you do with it.
 emeNotifications.drmContentPlaying.button.label = Configure…
 emeNotifications.drmContentPlaying.button.accesskey = C
 
+# LOCALIZATION NOTE(emeNotifications.drmContentDisabled.message): NB: inserted via innerHTML, so please don't use <, > or & in this string. %S will be the 'learn more' link
+emeNotifications.drmContentDisabled.message = You must enable DRM to play some audio or video on this page. %S
+emeNotifications.drmContentDisabled.button.label = Enable DRM
+emeNotifications.drmContentDisabled.button.accesskey = E
+# LOCALIZATION NOTE(emeNotifications.drmContentDisabled.learnMoreLabel): NB: inserted via innerHTML, so please don't use <, > or & in this string.
+emeNotifications.drmContentDisabled.learnMoreLabel = Learn More
+
+# LOCALIZATION NOTE(emeNotifications.drmContentCDMNotSupported.message): NB: inserted via innerHTML, so please don't use <, > or & in this string. %1$S is brandShortName, %2$S will be the 'learn more' link
+emeNotifications.drmContentCDMNotSupported.message = The audio or video on this page requires DRM software that %1$S does not support. %2$S
+# LOCALIZATION NOTE(emeNotifications.drmContentCDMNotSupported.learnMoreLabel): NB: inserted via innerHTML, so please don't use <, > or & in this string.
+emeNotifications.drmContentCDMNotSupported.learnMoreLabel = Learn More
+
+# LOCALIZATION NOTE(emeNotifications.drmContentCDMInsufficientVersion.message): NB: inserted via innerHTML, so please don't use <, > or & in this string. %S is brandShortName
+emeNotifications.drmContentCDMInsufficientVersion.message = %S is installing updates needed to play the audio or video on this page. Please try again later.
+
+# LOCALIZATION NOTE(emeNotifications.drmContentCDMInstalling.message): NB: inserted via innerHTML, so please don't use <, > or & in this string. %S is brandShortName
+emeNotifications.drmContentCDMInstalling.message = %S is installing components needed to play the audio or video on this page. Please try again later.
+
+emeNotifications.optionsButton.label = Options
+emeNotifications.optionsButton.accesskey = O
+
+emeNotifications.unknownDRMSoftware = Unknown
+
 # LOCALIZATION NOTE - %S is brandShortName
 slowStartup.message = %S seems slow… to… start.
 slowStartup.helpButton.label = Learn How to Speed It Up
 slowStartup.helpButton.accesskey = L
 slowStartup.disableNotificationButton.label = Don't Tell Me Again
 slowStartup.disableNotificationButton.accesskey = A
 
 
--- a/browser/modules/ContentObservers.jsm
+++ b/browser/modules/ContentObservers.jsm
@@ -15,33 +15,29 @@
 
 this.EXPORTED_SYMBOLS = [];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 
 let gEMEUIObserver = function(subject, topic, data) {
-  let win = subject.ownerDocument.defaultView.top;
+  let win = subject.top;
   let mm = getMessageManagerForWindow(win);
   if (mm) {
-    mm.sendAsyncMessage("EMEVideo:MetadataLoaded", {
-      // bug 1129370 covers making this the actual DRM provider inferred from
-      // either |subject| or |data| here.
-      drmProvider: "Adobe"
-    });
+    mm.sendAsyncMessage("EMEVideo:ContentMediaKeysRequest", data);
   }
 };
 
 function getMessageManagerForWindow(aContentWindow) {
   let ir = aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIDocShell)
                          .sameTypeRootTreeItem
                          .QueryInterface(Ci.nsIInterfaceRequestor);
   try {
     // If e10s is disabled, this throws NS_NOINTERFACE for closed tabs.
     return ir.getInterface(Ci.nsIContentFrameMessageManager);
   } catch(e if e.result == Cr.NS_NOINTERFACE) {
     return null;
   }
 }
 
-Services.obs.addObserver(gEMEUIObserver, "media-eme-metadataloaded", false);
+Services.obs.addObserver(gEMEUIObserver, "mediakeys-request", false);
--- a/browser/themes/shared/drm-icon.svg
+++ b/browser/themes/shared/drm-icon.svg
@@ -3,16 +3,19 @@
      viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
   <style>
     #chains > use > path {
       fill: url(#baseGradient);
     }
     #chains-pressed > use > path {
       fill: url(#pressedGradient);
     }
+    #chains-black > use > path {
+      fill: black;
+    }
 
     g:not(:target) {
       display: none;
     }
   </style>
   <defs>
     <linearGradient id="baseGradient" gradientUnits="userSpaceOnUse" x1="8" x2="8" y1="16" y2="0">
       <stop offset="0" style="stop-color:#808080"/>
@@ -39,9 +42,14 @@
     <use xlink:href="#path2"/>
     <use xlink:href="#path3"/>
   </g>
   <g id="chains-pressed">
     <use xlink:href="#path1"/>
     <use xlink:href="#path2"/>
     <use xlink:href="#path3"/>
   </g>
+  <g id="chains-black">
+    <use xlink:href="#path1"/>
+    <use xlink:href="#path2"/>
+    <use xlink:href="#path3"/>
+  </g>
 </svg>
--- a/toolkit/content/widgets/notification.xml
+++ b/toolkit/content/widgets/notification.xml
@@ -96,17 +96,21 @@
             for (var n = notifications.length - 1; n >= 0; n--) {
               if (notifications[n].priority < aPriority)
                 break;
               insertPos = notifications[n];
             }
 
             const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
             var newitem = document.createElementNS(XULNS, "notification");
-            newitem.setAttribute("label", aLabel);
+            // Can't use instanceof in case this was created from a different document:
+            let labelIsDocFragment = aLabel && typeof aLabel == "object" && aLabel.nodeType &&
+                                     aLabel.nodeType == aLabel.DOCUMENT_FRAGMENT_NODE;
+            if (!labelIsDocFragment)
+              newitem.setAttribute("label", aLabel);
             newitem.setAttribute("value", aValue);
             if (aImage)
               newitem.setAttribute("image", aImage);
             newitem.eventCallback = aEventCallback;
 
             if (aButtons) {
               // The notification-button-default class is added to the button
               // with isDefault set to true. If there is no such button, it is
@@ -143,16 +147,23 @@
 
             if (!insertPos) {
               newitem.style.position = "fixed";
               newitem.style.top = "100%";
               newitem.style.marginTop = "-15px";
               newitem.style.opacity = "0";
             }
             this.insertBefore(newitem, insertPos);
+            // Can only insert the document fragment after the item has been created because
+            // otherwise the XBL structure isn't there yet:
+            if (labelIsDocFragment) {
+              document.getAnonymousElementByAttribute(newitem, "anonid", "messageText")
+                .appendChild(aLabel);
+            }
+
             if (!insertPos)
               this._showNotification(newitem, true);
 
             // Fire event for accessibility APIs
             var event = document.createEvent("Events");
             event.initEvent("AlertActive", true, true);
             newitem.dispatchEvent(event);