EncryptedMediaParent.sys.mjs (9467B)
1 /* vim: set ts=2 sw=2 sts=2 et tw=80: */ 2 /* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 const lazy = {}; 7 8 ChromeUtils.defineLazyGetter(lazy, "gBrandBundle", function () { 9 return Services.strings.createBundle( 10 "chrome://branding/locale/brand.properties" 11 ); 12 }); 13 14 ChromeUtils.defineLazyGetter(lazy, "gNavigatorBundle", function () { 15 return Services.strings.createBundle( 16 "chrome://browser/locale/browser.properties" 17 ); 18 }); 19 20 ChromeUtils.defineLazyGetter(lazy, "gFluentStrings", function () { 21 return new Localization(["branding/brand.ftl", "browser/browser.ftl"], true); 22 }); 23 24 export class EncryptedMediaParent extends JSWindowActorParent { 25 isUiEnabled() { 26 return Services.prefs.getBoolPref("browser.eme.ui.enabled"); 27 } 28 29 ensureEMEEnabled(aBrowser, aKeySystem) { 30 Services.prefs.setBoolPref("media.eme.enabled", true); 31 if ( 32 aKeySystem && 33 aKeySystem == "com.widevine.alpha" && 34 Services.prefs.getPrefType("media.gmp-widevinecdm.enabled") && 35 !Services.prefs.getBoolPref("media.gmp-widevinecdm.enabled") 36 ) { 37 Services.prefs.setBoolPref("media.gmp-widevinecdm.enabled", true); 38 } 39 aBrowser.reload(); 40 } 41 42 isKeySystemVisible(aKeySystem) { 43 if (!aKeySystem) { 44 return false; 45 } 46 if ( 47 aKeySystem == "com.widevine.alpha" && 48 Services.prefs.getPrefType("media.gmp-widevinecdm.visible") 49 ) { 50 return Services.prefs.getBoolPref("media.gmp-widevinecdm.visible"); 51 } 52 return true; 53 } 54 55 getMessageWithBrandName(aNotificationId) { 56 let msgId = "emeNotifications." + aNotificationId + ".message"; 57 return lazy.gNavigatorBundle.formatStringFromName(msgId, [ 58 lazy.gBrandBundle.GetStringFromName("brandShortName"), 59 ]); 60 } 61 62 async receiveMessage(aMessage) { 63 if (!this.handledMessages) { 64 this.handledMessages = new Set(); 65 } 66 // The top level browsing context's embedding element should be a xul browser element. 67 let browser = this.browsingContext.top.embedderElement; 68 69 if (!browser) { 70 // We don't have a browser so bail! 71 return; 72 } 73 74 let parsedData; 75 try { 76 parsedData = JSON.parse(aMessage.data); 77 } catch (ex) { 78 console.error("Malformed EME video message with data: ", aMessage.data); 79 return; 80 } 81 let { status, keySystem } = parsedData; 82 if (this.handledMessages.has(status)) { 83 return; 84 } 85 86 // First, see if we need to do updates. We don't need to do anything for 87 // hidden keysystems: 88 if (!this.isKeySystemVisible(keySystem)) { 89 return; 90 } 91 if (status == "cdm-not-installed") { 92 Services.obs.notifyObservers(browser, "EMEVideo:CDMMissing"); 93 } 94 95 // Don't need to show UI if disabled. 96 if (!this.isUiEnabled()) { 97 return; 98 } 99 100 let notificationId; 101 let buttonCallback; 102 let supportPage; 103 // Notification message can be either a string or a DOM fragment. 104 let notificationMessage; 105 switch (status) { 106 case "available": 107 case "cdm-created": 108 // Only show the chain icon for proprietary CDMs. Clearkey is not one. 109 if (keySystem != "org.w3.clearkey") { 110 this.showPopupNotificationForSuccess(browser, keySystem); 111 } 112 this.reportEMEDecryptionProbe(); 113 // ... and bail! 114 return; 115 116 case "api-disabled": 117 case "cdm-disabled": 118 this.handledMessages.add(status); 119 notificationId = "drmContentDisabled"; 120 buttonCallback = () => { 121 this.ensureEMEEnabled(browser, keySystem); 122 }; 123 notificationMessage = lazy.gNavigatorBundle.GetStringFromName( 124 "emeNotifications.drmContentDisabled.message2" 125 ); 126 supportPage = "drm-content"; 127 break; 128 129 case "cdm-not-installed": 130 this.handledMessages.add(status); 131 notificationId = "drmContentCDMInstalling"; 132 notificationMessage = this.getMessageWithBrandName(notificationId); 133 break; 134 135 case "cdm-not-supported": 136 // Not to pop up user-level notification because they cannot do anything 137 // about it. 138 return; 139 default: 140 console.error( 141 new Error( 142 "Unknown message ('" + 143 status + 144 "') dealing with EME key request: " + 145 aMessage.data 146 ) 147 ); 148 return; 149 } 150 151 // Now actually create the notification 152 153 let notificationBox = browser.getTabBrowser().getNotificationBox(browser); 154 if (notificationBox.getNotificationWithValue(notificationId)) { 155 this.handledMessages.delete(status); 156 return; 157 } 158 159 let buttons = []; 160 if (supportPage) { 161 buttons.push({ supportPage }); 162 } 163 if (buttonCallback) { 164 let msgPrefix = "emeNotifications." + notificationId + "."; 165 let manageLabelId = msgPrefix + "button.label"; 166 let manageAccessKeyId = msgPrefix + "button.accesskey"; 167 buttons.push({ 168 label: lazy.gNavigatorBundle.GetStringFromName(manageLabelId), 169 accessKey: lazy.gNavigatorBundle.GetStringFromName(manageAccessKeyId), 170 callback: buttonCallback, 171 }); 172 } 173 174 let iconURL = "chrome://browser/skin/drm-icon.svg"; 175 await notificationBox.appendNotification( 176 notificationId, 177 { 178 label: notificationMessage, 179 image: iconURL, 180 priority: notificationBox.PRIORITY_INFO_HIGH, 181 }, 182 buttons 183 ); 184 this.handledMessages.delete(status); 185 } 186 187 async showPopupNotificationForSuccess(aBrowser) { 188 // We're playing EME content! Remove any "we can't play because..." messages. 189 let notificationBox = aBrowser.getTabBrowser().getNotificationBox(aBrowser); 190 ["drmContentDisabled", "drmContentCDMInstalling"].forEach(function (value) { 191 let notification = notificationBox.getNotificationWithValue(value); 192 if (notification) { 193 notificationBox.removeNotification(notification); 194 } 195 }); 196 197 // Don't bother creating it if it's already there: 198 if ( 199 aBrowser.ownerGlobal.PopupNotifications.getNotification( 200 "drmContentPlaying", 201 aBrowser 202 ) 203 ) { 204 return; 205 } 206 207 let msgId = "eme-notifications-drm-content-playing"; 208 let manageLabelId = "eme-notifications-drm-content-playing-manage"; 209 let manageAccessKeyId = 210 "eme-notifications-drm-content-playing-manage-accesskey"; 211 let dismissLabelId = "eme-notifications-drm-content-playing-dismiss"; 212 let dismissAccessKeyId = 213 "eme-notifications-drm-content-playing-dismiss-accesskey"; 214 215 let [ 216 message, 217 manageLabel, 218 manageAccessKey, 219 dismissLabel, 220 dismissAccessKey, 221 ] = await lazy.gFluentStrings.formatValues([ 222 msgId, 223 manageLabelId, 224 manageAccessKeyId, 225 dismissLabelId, 226 dismissAccessKeyId, 227 ]); 228 229 let anchorId = "eme-notification-icon"; 230 let firstPlayPref = "browser.eme.ui.firstContentShown"; 231 let document = aBrowser.ownerDocument; 232 if ( 233 !Services.prefs.getPrefType(firstPlayPref) || 234 !Services.prefs.getBoolPref(firstPlayPref) 235 ) { 236 document.getElementById(anchorId).setAttribute("firstplay", "true"); 237 Services.prefs.setBoolPref(firstPlayPref, true); 238 } else { 239 document.getElementById(anchorId).removeAttribute("firstplay"); 240 } 241 242 let mainAction = { 243 label: manageLabel, 244 accessKey: manageAccessKey, 245 callback() { 246 aBrowser.ownerGlobal.openPreferences("general-drm"); 247 }, 248 dismiss: true, 249 }; 250 251 let secondaryActions = [ 252 { 253 label: dismissLabel, 254 accessKey: dismissAccessKey, 255 callback: () => {}, 256 dismiss: true, 257 }, 258 ]; 259 260 let options = { 261 dismissed: true, 262 eventCallback: aTopic => aTopic == "swapping", 263 learnMoreURL: 264 Services.urlFormatter.formatURLPref("app.support.baseURL") + 265 "drm-content", 266 hideClose: true, 267 }; 268 aBrowser.ownerGlobal.PopupNotifications.show( 269 aBrowser, 270 "drmContentPlaying", 271 message, 272 anchorId, 273 mainAction, 274 secondaryActions, 275 options 276 ); 277 } 278 279 async reportEMEDecryptionProbe() { 280 let hasHardwareDecryption = false; 281 let hasSoftwareClearlead = false; 282 let hasHardwareClearlead = false; 283 let hasWMF = false; 284 285 // Get CDM capabilities from the GMP process. 286 let infos = []; 287 let cdmInfo = await ChromeUtils.getGMPContentDecryptionModuleInformation(); 288 infos.push(...cdmInfo); 289 290 // Get CDM capabilities from the MFCDM process, if exists. 291 if (ChromeUtils.getWMFContentDecryptionModuleInformation !== undefined) { 292 hasWMF = true; 293 cdmInfo = await ChromeUtils.getWMFContentDecryptionModuleInformation(); 294 infos.push(...cdmInfo); 295 } 296 297 for (let info of infos) { 298 if (info.isHardwareDecryption) { 299 hasHardwareDecryption = true; 300 } 301 if (info.clearlead) { 302 if (info.isHardwareDecryption) { 303 hasHardwareClearlead = true; 304 } else { 305 hasSoftwareClearlead = true; 306 } 307 } 308 } 309 Glean.mediadrm.decryption.has_hardware_decryption.set( 310 hasHardwareDecryption 311 ); 312 Glean.mediadrm.decryption.has_hardware_clearlead.set(hasHardwareClearlead); 313 Glean.mediadrm.decryption.has_software_clearlead.set(hasSoftwareClearlead); 314 Glean.mediadrm.decryption.has_wmf.set(hasWMF); 315 } 316 }