Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / chrome / test / data / media / encrypted_media_utils.js
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.
4
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;
20
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/';
36
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);
41     }
42     return result;
43 }
44
45 function hasPrefix(msg, prefix) {
46   if (msg.length < prefix.length)
47     return false;
48   for (var i = 0; i < prefix.length; ++i) {
49     if (String.fromCharCode(msg[i]) != prefix[i])
50       return false;
51   }
52   return true;
53 }
54
55 // JWK routines copied from third_party/WebKit/LayoutTests/media/
56 //   encrypted-media/encrypted-media-utils.js
57 //
58 // Encodes data (Uint8Array) into base64 string without trailing '='.
59 // TODO(jrummell): Update once the EME spec is updated to say base64url
60 // encoding.
61 function base64Encode(data) {
62   var result = btoa(String.fromCharCode.apply(null, data));
63   return result.replace(/=+$/g, '');
64 }
65
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);
70   jwk += "\",\"k\":\"";
71   jwk += base64Encode(key);
72   jwk += "\"}";
73   return jwk;
74 }
75
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++) {
80     if (i != 0)
81       jwkSet += ",";
82     jwkSet += arguments[i];
83   }
84   jwkSet += "]}";
85   return jwkSet;
86 }
87
88 function isHeartbeatMessage(msg) {
89   return hasPrefix(msg, HEART_BEAT_HEADER);
90 }
91
92 function isFileIOTestMessage(msg) {
93   return hasPrefix(msg, FILE_IO_TEST_RESULT_HEADER);
94 }
95
96 function loadEncryptedMediaFromURL(video) {
97   return loadEncryptedMedia(video, mediaFile, keySystem, KEY, useMSE,
98                             usePrefixedEME);
99 }
100
101 function loadEncryptedMedia(video, mediaFile, keySystem, key, useMSE,
102                             usePrefixedEME, appendSourceCallbackFn) {
103   var keyRequested = false;
104   var sourceOpened = false;
105   var mediaKeys;
106   var mediaKeySession;
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;
112
113   if (!(video && mediaFile && keySystem && key)) {
114     failTest('Missing parameters in loadEncryptedMedia().');
115     return;
116   }
117
118   // Shared by prefixed and unprefixed EME.
119   function onNeedKey(e) {
120     if (keyRequested)
121       return;
122     keyRequested = true;
123     console.log('onNeedKey', e);
124
125     var initData = sessionToLoad ? stringToUint8Array(
126         PREFIXED_API_LOAD_SESSION_HEADER + sessionToLoad) : e.initData;
127     try {
128       if (usePrefixedEME) {
129         video.webkitGenerateKeyRequest(keySystem, initData);
130       } else {
131         mediaKeySession = mediaKeys.createSession(e.contentType, initData);
132         mediaKeySession.addEventListener('message', onMessage);
133         mediaKeySession.addEventListener('error', function() {
134             setResultInTitle("KeyError");
135         });
136         mediaKeySession.addEventListener('ready', onReady);
137       }
138     }
139     catch(error) {
140       console.log(error.message);
141       setResultInTitle(error.name);
142     }
143   }
144
145   // Prefixed EME callbacks.
146   function onKeyAdded(e) {
147     e.target.receivedKeyAdded = true;
148   }
149
150   function onKeyMessage(message) {
151     video.receivedKeyMessage = true;
152
153     if (!message.keySystem || message.keySystem != keySystem) {
154       failTest('Message with unexpected keySystem: ' + message.keySystem);
155       return;
156     }
157
158     if (!message.sessionId) {
159       failTest('Message without a sessionId: ' + message.sessionId);
160       return;
161     }
162
163     processMessage(message, message.keySystem, message.defaultURL);
164   }
165
166   // Unprefixed EME callbacks.
167   function onReady(e) {
168   }
169
170   function onMessage(message) {
171     processMessage(message, keySystem, message.destinationURL);
172   }
173
174   // Shared by prefixed and unprefixed EME.
175   function processMessage(message, keySystem, url) {
176     video.receivedKeyMessage = true;
177
178     if (!message.message) {
179       failTest('Message without a message content: ' + message.message);
180       return;
181     }
182
183     if (isHeartbeatMessage(message.message)) {
184       console.log('processMessage - heartbeat', message);
185       video.receivedHeartbeat = true;
186       verifyHeartbeatMessage(keySystem, url);
187       return;
188     }
189
190     if (isFileIOTestMessage(message.message)) {
191       var success = getFileIOTestResult(keySystem, message);
192       console.log('processMessage - CDM file IO test: ' +
193                   (success ? 'Success' : 'Fail'));
194       if (success)
195         setResultInTitle("FILEIOTESTSUCCESS");
196       else
197         setResultInTitle("FAILED");
198       return;
199     }
200
201     // For FileIOTest key system, no need to start playback.
202     if (keySystem == EXTERNAL_CLEAR_KEY_FILE_IO_TEST_KEY_SYSTEM)
203       return;
204
205     // No tested key system returns defaultURL in for key request messages.
206     if (url) {
207       failTest('Message unexpectedly has URL: ' + url);
208       return;
209     }
210
211     // When loading a session, no need to call update()/webkitAddKey().
212     if (sessionToLoad)
213       return;
214
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);
221       } else {
222         mediaKeySession.update(invalidData);
223       }
224       return;
225     }
226     // Check if should send request to locally running license server.
227     if (licenseServerURL) {
228       requestLicense(message);
229       return;
230     }
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);
237     } else {
238       var jwkSet = stringToUint8Array(createJWKSet(createJWK(initData, key)));
239       mediaKeySession.update(jwkSet);
240     }
241   }
242
243   function verifyHeartbeatMessage(keySystem, url) {
244     String.prototype.startsWith = function(prefix) {
245       return this.indexOf(prefix) === 0;
246     }
247
248     function isExternalClearKey(keySystem) {
249       return keySystem == EXTERNAL_CLEAR_KEY_KEY_SYSTEM ||
250              keySystem.startsWith(EXTERNAL_CLEAR_KEY_KEY_SYSTEM + '.');
251     }
252
253     // Only External Clear Key sends a HEARTBEAT message.
254     if (!isExternalClearKey(keySystem)) {
255       failTest('Unexpected heartbeat from ' + keySystem);
256       return;
257     }
258
259     if (url != EXTERNAL_CLEAR_KEY_HEARTBEAT_URL) {
260       failTest('Heartbeat message with unexpected URL: ' + url);
261       return;
262     }
263   }
264
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);
269       return false;
270     }
271
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)
274       return false;
275
276     var result_index = FILE_IO_TEST_RESULT_HEADER.length;
277     return String.fromCharCode(e.message[result_index]) == 1;
278   }
279
280   if (usePrefixedEME) {
281     video.addEventListener('webkitneedkey', onNeedKey);
282     video.addEventListener('webkitkeymessage', onKeyMessage);
283     video.addEventListener('webkitkeyerror', function() {
284         setResultInTitle("KeyError");
285     });
286     video.addEventListener('webkitkeyadded', onKeyAdded);
287   } else {
288     video.addEventListener('needkey', onNeedKey);
289   }
290   installTitleEventHandler(video, 'error');
291
292   if (useMSE) {
293     var mediaSource = loadMediaSource(mediaFile, mediaType,
294                                       appendSourceCallbackFn);
295     video.src = window.URL.createObjectURL(mediaSource);
296   } else {
297     video.src = mediaFile;
298   }
299   if (!usePrefixedEME) {
300     try {
301       mediaKeys = new MediaKeys(keySystem);
302       video.setMediaKeys(mediaKeys);
303     }
304     catch(error) {
305       console.log(error.message, error);
306       setResultInTitle(error.name);
307     }
308   }
309 }
310
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);
315   }
316   return init_key_id;
317 }
318
319 function requestLicense(message) {
320   // Store license request attempts per target <video>.
321   if (message.target.licenseRequestAttempts == undefined)
322     message.target.licenseRequestAttempts = 0;
323
324   if (message.target.licenseRequestAttempts ==  MAX_LICENSE_REQUEST_ATTEMPTS) {
325     failTest('Exceeded maximum license request attempts.');
326     return;
327   }
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);
334     return;
335   }
336
337   requestLicenseTry(message);
338 }
339
340 function requestLicenseTry(message) {
341   var xmlhttp = new XMLHttpRequest();
342   xmlhttp.responseType = 'arraybuffer';
343   xmlhttp.open("POST", licenseServerURL, true);
344
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),
351                                     message.sessionId);
352       } else {
353         message.target.update(response);
354       }
355       // Reset license request count so that renewal requests can be sent later.
356       message.target.licenseRequestAttempts = 0;
357     } else {
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);
362     }
363   }
364   console.log('license request message', message.message);
365   xmlhttp.send(message.message);
366 }
367
368 function URLExists(url) {
369   if (!url)
370     return false;
371   var http = new XMLHttpRequest();
372   http.open('HEAD', url, false);
373   try {
374     http.send();
375     return http.status != 404;
376   } catch (e) {
377     return false;
378   }
379 }