1 // Copyright 2013 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.
5 var keySystem = QueryString.keySystem;
6 var mediaFile = QueryString.mediaFile;
7 var mediaType = QueryString.mediaType || 'video/webm; codecs="vorbis, vp8"';
8 var useMSE = QueryString.useMSE == 1;
9 var usePrefixedEME = QueryString.usePrefixedEME == 1;
10 var forceInvalidResponse = QueryString.forceInvalidResponse == 1;
11 var sessionToLoad = QueryString.sessionToLoad;
12 var licenseServerURL = QueryString.licenseServerURL;
13 // Maximum license request attempts that the media element can make to get a
14 // valid license response from the license server.
15 // This is used to avoid server boot up delays since there is no direct way
16 // to know if it is ready crbug.com/339289.
17 var MAX_LICENSE_REQUEST_ATTEMPTS = 3;
18 // Delay in ms between retries to get a license from license server.
19 var LICENSE_REQUEST_RETRY_DELAY_MS = 3000;
21 // Default key used to encrypt many media files used in browser tests.
22 var KEY = new Uint8Array([0xeb, 0xdd, 0x62, 0xf1, 0x68, 0x14, 0xd2, 0x7b,
23 0x68, 0xef, 0x12, 0x2a, 0xfc, 0xe4, 0xae, 0x3c]);
24 // KEY_ID constant used as init data while encrypting test media files.
25 var KEY_ID = getInitDataFromKeyId("0123456789012345");
26 // Heart beat message header.
27 var HEART_BEAT_HEADER = 'HEARTBEAT';
28 var FILE_IO_TEST_RESULT_HEADER = 'FILEIOTESTRESULT';
29 var PREFIXED_API_LOAD_SESSION_HEADER = "LOAD_SESSION|";
30 var EXTERNAL_CLEAR_KEY_KEY_SYSTEM = "org.chromium.externalclearkey";
31 var EXTERNAL_CLEAR_KEY_FILE_IO_TEST_KEY_SYSTEM =
32 "org.chromium.externalclearkey.fileiotest";
33 // Note that his URL has been normalized from the one in clear_key_cdm.cc.
34 var EXTERNAL_CLEAR_KEY_HEARTBEAT_URL =
35 'http://test.externalclearkey.chromium.org/';
37 function stringToUint8Array(str) {
38 var result = new Uint8Array(str.length);
39 for(var i = 0; i < str.length; i++) {
40 result[i] = str.charCodeAt(i);
45 function hasPrefix(msg, prefix) {
46 if (msg.length < prefix.length)
48 for (var i = 0; i < prefix.length; ++i) {
49 if (String.fromCharCode(msg[i]) != prefix[i])
55 // JWK routines copied from third_party/WebKit/LayoutTests/media/
56 // encrypted-media/encrypted-media-utils.js
58 // Encodes data (Uint8Array) into base64 string without trailing '='.
59 // TODO(jrummell): Update once the EME spec is updated to say base64url
61 function base64Encode(data) {
62 var result = btoa(String.fromCharCode.apply(null, data));
63 return result.replace(/=+$/g, '');
66 // Creates a JWK from raw key ID and key.
67 function createJWK(keyId, key) {
68 var jwk = "{\"kty\":\"oct\",\"kid\":\"";
69 jwk += base64Encode(keyId);
71 jwk += base64Encode(key);
76 // Creates a JWK Set from an array of JWK(s).
77 function createJWKSet() {
78 var jwkSet = "{\"keys\":[";
79 for (var i = 0; i < arguments.length; i++) {
82 jwkSet += arguments[i];
88 function isHeartbeatMessage(msg) {
89 return hasPrefix(msg, HEART_BEAT_HEADER);
92 function isFileIOTestMessage(msg) {
93 return hasPrefix(msg, FILE_IO_TEST_RESULT_HEADER);
96 function loadEncryptedMediaFromURL(video) {
97 return loadEncryptedMedia(video, mediaFile, keySystem, KEY, useMSE,
101 function loadEncryptedMedia(video, mediaFile, keySystem, key, useMSE,
102 usePrefixedEME, appendSourceCallbackFn) {
103 var keyRequested = false;
104 var sourceOpened = false;
107 // Add properties to enable verification that events occurred.
108 video.receivedKeyAdded = false;
109 video.receivedHeartbeat = false;
110 video.isHeartbeatExpected = keySystem === EXTERNAL_CLEAR_KEY_KEY_SYSTEM;
111 video.receivedKeyMessage = false;
113 if (!(video && mediaFile && keySystem && key)) {
114 failTest('Missing parameters in loadEncryptedMedia().');
118 // Shared by prefixed and unprefixed EME.
119 function onNeedKey(e) {
123 console.log('onNeedKey', e);
125 var initData = sessionToLoad ? stringToUint8Array(
126 PREFIXED_API_LOAD_SESSION_HEADER + sessionToLoad) : e.initData;
128 if (usePrefixedEME) {
129 video.webkitGenerateKeyRequest(keySystem, initData);
131 mediaKeySession = mediaKeys.createSession(e.contentType, initData);
132 mediaKeySession.addEventListener('message', onMessage);
133 mediaKeySession.addEventListener('error', function() {
134 setResultInTitle("KeyError");
136 mediaKeySession.addEventListener('ready', onReady);
140 console.log(error.message);
141 setResultInTitle(error.name);
145 // Prefixed EME callbacks.
146 function onKeyAdded(e) {
147 e.target.receivedKeyAdded = true;
150 function onKeyMessage(message) {
151 video.receivedKeyMessage = true;
153 if (!message.keySystem || message.keySystem != keySystem) {
154 failTest('Message with unexpected keySystem: ' + message.keySystem);
158 if (!message.sessionId) {
159 failTest('Message without a sessionId: ' + message.sessionId);
163 processMessage(message, message.keySystem, message.defaultURL);
166 // Unprefixed EME callbacks.
167 function onReady(e) {
170 function onMessage(message) {
171 processMessage(message, keySystem, message.destinationURL);
174 // Shared by prefixed and unprefixed EME.
175 function processMessage(message, keySystem, url) {
176 video.receivedKeyMessage = true;
178 if (!message.message) {
179 failTest('Message without a message content: ' + message.message);
183 if (isHeartbeatMessage(message.message)) {
184 console.log('processMessage - heartbeat', message);
185 video.receivedHeartbeat = true;
186 verifyHeartbeatMessage(keySystem, url);
190 if (isFileIOTestMessage(message.message)) {
191 var success = getFileIOTestResult(keySystem, message);
192 console.log('processMessage - CDM file IO test: ' +
193 (success ? 'Success' : 'Fail'));
195 setResultInTitle("FILEIOTESTSUCCESS");
197 setResultInTitle("FAILED");
201 // For FileIOTest key system, no need to start playback.
202 if (keySystem == EXTERNAL_CLEAR_KEY_FILE_IO_TEST_KEY_SYSTEM)
205 // No tested key system returns defaultURL in for key request messages.
207 failTest('Message unexpectedly has URL: ' + url);
211 // When loading a session, no need to call update()/webkitAddKey().
215 console.log('processMessage - key request', message);
216 if (forceInvalidResponse) {
217 console.log('processMessage - Forcing an invalid response.');
218 var invalidData = new Uint8Array([0xAA]);
219 if (usePrefixedEME) {
220 video.webkitAddKey(keySystem, invalidData, invalidData);
222 mediaKeySession.update(invalidData);
226 // Check if should send request to locally running license server.
227 if (licenseServerURL) {
228 requestLicense(message);
231 console.log('processMessage - Respond with test key.');
232 var initData = message.message;
233 if (mediaType.indexOf('mp4') != -1)
234 initData = KEY_ID; // Temporary hack for Clear Key in v0.1.
235 if (usePrefixedEME) {
236 video.webkitAddKey(keySystem, key, initData);
238 var jwkSet = stringToUint8Array(createJWKSet(createJWK(initData, key)));
239 mediaKeySession.update(jwkSet);
243 function verifyHeartbeatMessage(keySystem, url) {
244 String.prototype.startsWith = function(prefix) {
245 return this.indexOf(prefix) === 0;
248 function isExternalClearKey(keySystem) {
249 return keySystem == EXTERNAL_CLEAR_KEY_KEY_SYSTEM ||
250 keySystem.startsWith(EXTERNAL_CLEAR_KEY_KEY_SYSTEM + '.');
253 // Only External Clear Key sends a HEARTBEAT message.
254 if (!isExternalClearKey(keySystem)) {
255 failTest('Unexpected heartbeat from ' + keySystem);
259 if (url != EXTERNAL_CLEAR_KEY_HEARTBEAT_URL) {
260 failTest('Heartbeat message with unexpected URL: ' + url);
265 function getFileIOTestResult(keySystem, e) {
266 // Only External Clear Key sends a FILEIOTESTRESULT message.
267 if (keySystem != EXTERNAL_CLEAR_KEY_FILE_IO_TEST_KEY_SYSTEM) {
268 failTest('Unexpected CDM file IO test result from ' + keySystem);
272 // The test result is either '0' or '1' appended to the header.
273 if (e.message.length != FILE_IO_TEST_RESULT_HEADER.length + 1)
276 var result_index = FILE_IO_TEST_RESULT_HEADER.length;
277 return String.fromCharCode(e.message[result_index]) == 1;
280 if (usePrefixedEME) {
281 video.addEventListener('webkitneedkey', onNeedKey);
282 video.addEventListener('webkitkeymessage', onKeyMessage);
283 video.addEventListener('webkitkeyerror', function() {
284 setResultInTitle("KeyError");
286 video.addEventListener('webkitkeyadded', onKeyAdded);
288 video.addEventListener('needkey', onNeedKey);
290 installTitleEventHandler(video, 'error');
293 var mediaSource = loadMediaSource(mediaFile, mediaType,
294 appendSourceCallbackFn);
295 video.src = window.URL.createObjectURL(mediaSource);
297 video.src = mediaFile;
299 if (!usePrefixedEME) {
301 mediaKeys = new MediaKeys(keySystem);
302 video.setMediaKeys(mediaKeys);
305 console.log(error.message, error);
306 setResultInTitle(error.name);
311 function getInitDataFromKeyId(keyID) {
312 var init_key_id = new Uint8Array(keyID.length);
313 for(var i = 0; i < keyID.length; i++) {
314 init_key_id[i] = keyID.charCodeAt(i);
319 function requestLicense(message) {
320 // Store license request attempts per target <video>.
321 if (message.target.licenseRequestAttempts == undefined)
322 message.target.licenseRequestAttempts = 0;
324 if (message.target.licenseRequestAttempts == MAX_LICENSE_REQUEST_ATTEMPTS) {
325 failTest('Exceeded maximum license request attempts.');
328 message.target.licenseRequestAttempts++;
329 console.log('Requesting license from license server ' + licenseServerURL);
330 if (!URLExists(licenseServerURL)) {
331 console.log('License server is not available, retrying in ' +
332 LICENSE_REQUEST_RETRY_DELAY_MS + ' ms.');
333 setTimeout(requestLicense, LICENSE_REQUEST_RETRY_DELAY_MS, message);
337 requestLicenseTry(message);
340 function requestLicenseTry(message) {
341 var xmlhttp = new XMLHttpRequest();
342 xmlhttp.responseType = 'arraybuffer';
343 xmlhttp.open("POST", licenseServerURL, true);
345 xmlhttp.onload = function(e) {
346 if (this.status == 200) {
347 var response = new Uint8Array(this.response);
348 console.log('Adding license response', response);
349 if (usePrefixedEME) {
350 message.target.webkitAddKey(keySystem, response, new Uint8Array(1),
353 message.target.update(response);
355 // Reset license request count so that renewal requests can be sent later.
356 message.target.licenseRequestAttempts = 0;
358 console.log('Bad response: ' + this.response);
359 console.log('License response bad status = ' + this.status);
360 console.log('Retrying license request if possible.');
361 setTimeout(requestLicense, LICENSE_REQUEST_RETRY_DELAY_MS, message);
364 console.log('license request message', message.message);
365 xmlhttp.send(message.message);
368 function URLExists(url) {
371 var http = new XMLHttpRequest();
372 http.open('HEAD', url, false);
375 return http.status != 404;