1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5// Utils provide logging functions and other JS functions commonly used by the 6// app and media players. 7var Utils = new function() { 8 this.titleChanged = false; 9}; 10 11// Adds options to document element. 12Utils.addOptions = function(elementID, keyValueOptions, disabledOptions) { 13 disabledOptions = disabledOptions || []; 14 var selectElement = document.getElementById(elementID); 15 var keys = Object.keys(keyValueOptions); 16 for (var i = 0; i < keys.length; i++) { 17 var key = keys[i]; 18 var option = new Option(key, keyValueOptions[key]); 19 option.title = keyValueOptions[key]; 20 if (disabledOptions.indexOf(key) >= 0) 21 option.disabled = true; 22 selectElement.options.add(option); 23 } 24}; 25 26Utils.convertToArray = function(input) { 27 if (Array.isArray(input)) 28 return input; 29 return [input]; 30}; 31 32Utils.convertToUint8Array = function(msg) { 33 if (typeof msg == 'string') { 34 var ans = new Uint8Array(msg.length); 35 for (var i = 0; i < msg.length; i++) { 36 ans[i] = msg.charCodeAt(i); 37 } 38 return ans; 39 } 40 // Assume it is an ArrayBuffer or ArrayBufferView. If it already is a 41 // Uint8Array, this will just make a copy of the view. 42 return new Uint8Array(msg); 43}; 44 45Utils.createJWKData = function(keyId, key) { 46 // JWK routines copied from third_party/WebKit/LayoutTests/media/ 47 // encrypted-media/encrypted-media-utils.js 48 // 49 // Encodes data (Uint8Array) into base64 string without trailing '='. 50 // TODO(jrummell): Update once the EME spec is updated to say base64url 51 // encoding. 52 function base64Encode(data) { 53 var result = btoa(String.fromCharCode.apply(null, data)); 54 return result.replace(/=+$/g, ''); 55 } 56 57 // Creates a JWK from raw key ID and key. 58 function createJWK(keyId, key) { 59 var jwk = '{"kty":"oct","kid":"'; 60 jwk += base64Encode(keyId); 61 jwk += '","k":"'; 62 jwk += base64Encode(key); 63 jwk += '"}'; 64 return jwk; 65 } 66 67 // Creates a JWK Set from an array of JWK(s). 68 function createJWKSet() { 69 var jwkSet = '{"keys":['; 70 for (var i = 0; i < arguments.length; i++) { 71 if (i != 0) 72 jwkSet += ','; 73 jwkSet += arguments[i]; 74 } 75 jwkSet += ']}'; 76 return jwkSet; 77 } 78 79 return Utils.convertToUint8Array(createJWKSet(createJWK(keyId, key))); 80}; 81 82Utils.extractFirstLicenseKey = function(message) { 83 // Decodes data (Uint8Array) from base64 string. 84 // TODO(jrummell): Update once the EME spec is updated to say base64url 85 // encoding. 86 function base64Decode(data) { 87 return atob(data); 88 } 89 90 function convertToString(data) { 91 return String.fromCharCode.apply(null, Utils.convertToUint8Array(data)); 92 } 93 94 try { 95 var json = JSON.parse(convertToString(message)); 96 // Decode the first element of 'kids', return it as an Uint8Array. 97 return Utils.convertToUint8Array(base64Decode(json.kids[0])); 98 } catch (error) { 99 // Not valid JSON, so return message untouched as Uint8Array. 100 return Utils.convertToUint8Array(message); 101 } 102} 103 104Utils.documentLog = function(log, success, time) { 105 if (!docLogs) 106 return; 107 time = time || Utils.getCurrentTimeString(); 108 var timeLog = '<span style="color: green">' + time + '</span>'; 109 var logColor = !success ? 'red' : 'black'; // default is true. 110 log = '<span style="color: "' + logColor + '>' + log + '</span>'; 111 docLogs.innerHTML = timeLog + ' - ' + log + '<br>' + docLogs.innerHTML; 112}; 113 114Utils.ensureOptionInList = function(listID, option) { 115 var selectElement = document.getElementById(listID); 116 for (var i = 0; i < selectElement.length; i++) { 117 if (selectElement.options[i].value == option) { 118 selectElement.value = option; 119 return; 120 } 121 } 122 // The list does not have the option, let's add it and select it. 123 var optionElement = new Option(option, option); 124 optionElement.title = option; 125 selectElement.options.add(optionElement); 126 selectElement.value = option; 127}; 128 129Utils.failTest = function(msg, newTitle) { 130 var failMessage = 'FAIL: '; 131 var title = 'FAILED'; 132 // Handle exception messages; 133 if (msg.message) { 134 title = msg.name || 'Error'; 135 failMessage += title + ' ' + msg.message; 136 } else if (msg instanceof Event) { 137 // Handle failing events. 138 failMessage = msg.target + '.' + msg.type; 139 title = msg.type; 140 } else { 141 failMessage += msg; 142 } 143 // Force newTitle if passed. 144 title = newTitle || title; 145 // Log failure. 146 Utils.documentLog(failMessage, false); 147 console.log(failMessage, msg); 148 Utils.setResultInTitle(title); 149}; 150 151Utils.getCurrentTimeString = function() { 152 var date = new Date(); 153 var hours = ('0' + date.getHours()).slice(-2); 154 var minutes = ('0' + date.getMinutes()).slice(-2); 155 var secs = ('0' + date.getSeconds()).slice(-2); 156 var milliSecs = ('00' + date.getMilliseconds()).slice(-3); 157 return hours + ':' + minutes + ':' + secs + '.' + milliSecs; 158}; 159 160Utils.getDefaultKey = function(forceInvalidResponse) { 161 if (forceInvalidResponse) { 162 Utils.timeLog('Forcing invalid key data.'); 163 return new Uint8Array([0xAA]); 164 } 165 return KEY; 166}; 167 168Utils.getHexString = function(uintArray) { 169 var hex_str = ''; 170 for (var i = 0; i < uintArray.length; i++) { 171 var hex = uintArray[i].toString(16); 172 if (hex.length == 1) 173 hex = '0' + hex; 174 hex_str += hex; 175 } 176 return hex_str; 177}; 178 179Utils.getInitDataFromMessage = function(message, mediaType, decodeJSONMessage) { 180 var initData; 181 if (mediaType.indexOf('mp4') != -1) { 182 // Temporary hack for Clear Key in v0.1. 183 // If content uses mp4, then message.message is PSSH data. Instead of 184 // parsing that data we hard code the initData. 185 initData = Utils.convertToUint8Array(KEY_ID); 186 } else if (decodeJSONMessage) { 187 initData = Utils.extractFirstLicenseKey(message.message); 188 } else { 189 initData = Utils.convertToUint8Array(message.message); 190 } 191 return initData; 192}; 193 194Utils.hasPrefix = function(msg, prefix) { 195 var message = String.fromCharCode.apply(null, msg); 196 return message.substring(0, prefix.length) == prefix; 197}; 198 199Utils.installTitleEventHandler = function(element, event) { 200 element.addEventListener(event, function(e) { 201 Utils.setResultInTitle(e.type); 202 }, false); 203}; 204 205Utils.isHeartBeatMessage = function(msg) { 206 return Utils.hasPrefix(Utils.convertToUint8Array(msg), HEART_BEAT_HEADER); 207}; 208 209Utils.resetTitleChange = function() { 210 this.titleChanged = false; 211 document.title = ''; 212}; 213 214Utils.sendRequest = function(requestType, responseType, message, serverURL, 215 onSuccessCallbackFn, forceInvalidResponse) { 216 var requestAttemptCount = 0; 217 var MAXIMUM_REQUEST_ATTEMPTS = 3; 218 var REQUEST_RETRY_DELAY_MS = 3000; 219 220 function sendRequestAttempt() { 221 requestAttemptCount++; 222 if (requestAttemptCount == MAXIMUM_REQUEST_ATTEMPTS) { 223 Utils.failTest('FAILED: Exceeded maximum license request attempts.'); 224 return; 225 } 226 var xmlhttp = new XMLHttpRequest(); 227 xmlhttp.responseType = responseType; 228 xmlhttp.open(requestType, serverURL, true); 229 230 xmlhttp.onload = function(e) { 231 if (this.status == 200) { 232 if (onSuccessCallbackFn) 233 onSuccessCallbackFn(this.response); 234 } else { 235 Utils.timeLog('Bad response status: ' + this.status); 236 Utils.timeLog('Bad response: ' + this.response); 237 Utils.timeLog('Retrying request if possible in ' + 238 REQUEST_RETRY_DELAY_MS + 'ms'); 239 setTimeout(sendRequestAttempt, REQUEST_RETRY_DELAY_MS); 240 } 241 }; 242 Utils.timeLog('Attempt (' + requestAttemptCount + 243 '): sending request to server: ' + serverURL); 244 xmlhttp.send(message); 245 } 246 247 if (forceInvalidResponse) { 248 Utils.timeLog('Not sending request - forcing an invalid response.'); 249 return onSuccessCallbackFn([0xAA]); 250 } 251 sendRequestAttempt(); 252}; 253 254Utils.setResultInTitle = function(title) { 255 // If document title is 'ENDED', then update it with new title to possibly 256 // mark a test as failure. Otherwise, keep the first title change in place. 257 if (!this.titleChanged || document.title.toUpperCase() == 'ENDED') 258 document.title = title.toUpperCase(); 259 Utils.timeLog('Set document title to: ' + title + ', updated title: ' + 260 document.title); 261 this.titleChanged = true; 262}; 263 264Utils.timeLog = function(/**/) { 265 if (arguments.length == 0) 266 return; 267 var time = Utils.getCurrentTimeString(); 268 // Log to document. 269 Utils.documentLog(arguments[0], time); 270 // Log to JS console. 271 var logString = time + ' - '; 272 for (var i = 0; i < arguments.length; i++) { 273 logString += ' ' + arguments[i]; 274 } 275 console.log(logString); 276}; 277