Bug 1111153 - show error notifications for broken EME content (includes fix for bug 1139022), r=florian
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Fri, 20 Feb 2015 10:38:18 +0100
changeset 250249 0631cc897937
parent 250248 55823773c733
child 250250 529b83aa2c7b
push id4526
push usergijskruitbosch@gmail.com
push date2015-03-05 00:08 +0000
treeherdermozilla-beta@0e44d113855f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersflorian
bugs1111153, 1139022
milestone37.0
Bug 1111153 - show error notifications for broken EME content (includes fix for bug 1139022), r=florian
browser/base/content/baseMenuOverlay.xul
browser/base/content/browser-eme.js
browser/base/content/browser-eme.properties
browser/base/content/browser.xul
browser/base/jar.mn
browser/modules/ContentObservers.jsm
browser/themes/shared/drm-icon.svg
toolkit/content/widgets/notification.xml
--- a/browser/base/content/baseMenuOverlay.xul
+++ b/browser/base/content/baseMenuOverlay.xul
@@ -107,10 +107,11 @@
              key="&hideOtherAppsCmdMac.commandkey;"
              modifiers="accel,alt"/>
 #endif
     </keyset>
 
     <stringbundleset id="stringbundleset">
         <stringbundle id="bundle_browser" src="chrome://browser/locale/browser.properties"/>
         <stringbundle id="bundle_browser_region" src="chrome://browser-region/locale/region.properties"/>
+        <stringbundle id="bundle_eme" src="chrome://browser/content/browser-eme.properties"/>
     </stringbundleset>
 </overlay>
--- a/browser/base/content/browser-eme.js
+++ b/browser/base/content/browser-eme.js
@@ -1,36 +1,195 @@
 # -*- 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;
-  }
+XPCOMUtils.defineLazyGetter(this, "gEMEBundle", function() {
+  return document.getElementById("bundle_eme");
+});
+
+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 = gEMEBundle.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 gEMEBundle.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;
 
-  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 "cdm-not-installed":
+        notificationId = "drmContentCDMInstalling";
+        params = [this._brandShortName];
+        break;
+
+      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 ?
+                  gEMEBundle.getFormattedString(msgId, labelParams) :
+                  gEMEBundle.getString(msgId);
+
+    let buttons = [];
+    if (callback) {
+      let btnLabelId = msgPrefix + "button.label";
+      let btnAccessKeyId = msgPrefix + "button.accesskey";
+      buttons.push({
+        label: gEMEBundle.getString(btnLabelId),
+        accessKey: gEMEBundle.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: gEMEBundle.getString(optionsId + ".label"),
+        accessKey: gEMEBundle.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 = gEMEBundle.getFormattedString(msgId, [drmProvider, this._brandShortName]);
+    let anchorId = "eme-notification-icon";
+
+    let mainAction = {
+      label: gEMEBundle.getString(btnLabelId),
+      accessKey: gEMEBundle.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);
new file mode 100644
--- /dev/null
+++ b/browser/base/content/browser-eme.properties
@@ -0,0 +1,28 @@
+# 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/.
+
+# This is a temporary file and not meant for localization; later versions
+# of Firefox include these strings in browser.properties
+
+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
+
+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
+emeNotifications.drmContentDisabled.learnMoreLabel = Learn More
+
+emeNotifications.drmContentCDMNotSupported.message = The audio or video on this page requires DRM software that %1$S does not support. %2$S
+emeNotifications.drmContentCDMNotSupported.learnMoreLabel = Learn More
+
+emeNotifications.drmContentCDMInsufficientVersion.message = %S is installing updates needed to play the audio or video on this page. Please try again later.
+
+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
+
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -572,16 +572,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="Not now"
+                acceskey="N"
+                oncommand="gEMEHandler.onNotNow(this);"/>
+      <menuitem id="emeNotificationsDontAskAgain"
+                label="Don't ask me again"
+                acceskey="D"
+                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/base/jar.mn
+++ b/browser/base/jar.mn
@@ -69,16 +69,17 @@ browser.jar:
         content/browser/aboutRobots-widget-left.png   (content/aboutRobots-widget-left.png)
         content/browser/aboutSocialError.xhtml        (content/aboutSocialError.xhtml)
         content/browser/aboutProviderDirectory.xhtml  (content/aboutProviderDirectory.xhtml)
         content/browser/aboutTabCrashed.js            (content/aboutTabCrashed.js)
         content/browser/aboutTabCrashed.xhtml         (content/aboutTabCrashed.xhtml)
 *       content/browser/browser.css                   (content/browser.css)
 *       content/browser/browser.js                    (content/browser.js)
 *       content/browser/browser.xul                   (content/browser.xul)
+*       content/browser/browser-eme.properties        (content/browser-eme.properties)
 *       content/browser/browser-tabPreviews.xml       (content/browser-tabPreviews.xml)
 *       content/browser/chatWindow.xul                (content/chatWindow.xul)
         content/browser/content.js                    (content/content.js)
         content/browser/defaultthemes/1.footer.jpg    (content/defaultthemes/1.footer.jpg)
         content/browser/defaultthemes/1.header.jpg    (content/defaultthemes/1.header.jpg)
         content/browser/defaultthemes/1.icon.jpg      (content/defaultthemes/1.icon.jpg)
         content/browser/defaultthemes/1.preview.jpg   (content/defaultthemes/1.preview.jpg)
         content/browser/defaultthemes/2.footer.jpg    (content/defaultthemes/2.footer.jpg)
--- 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);