1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
media / test / data / eme_player_js / player_utils.js [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// The PlayerUtils provides utility functions to binding common media events
// to specific player functions. It also provides functions to load media source
// base on test configurations.
var PlayerUtils = new function() {
}
// Prepares a video element for playback by setting default event handlers
// and source attribute.
PlayerUtils.registerDefaultEventListeners = function(player) {
Utils.timeLog('Registering video event handlers.');
// Map from event name to event listener function name. It is common for
// event listeners to be named onEventName.
var eventListenerMap = {
'encrypted': 'onEncrypted',
};
for (eventName in eventListenerMap) {
var eventListenerFunction = player[eventListenerMap[eventName]];
if (eventListenerFunction) {
player.video.addEventListener(eventName, function(e) {
player[eventListenerMap[e.type]](e);
});
}
}
player.video.addEventListener('error', function(error) {
// This most likely happens on pipeline failures (e.g. when the CDM
// crashes). Don't report a failure if the test is checking that sessions
// are closed on a crash.
Utils.timeLog('onHTMLElementError', error.target.error.message);
if (player.testConfig.keySystem == CRASH_TEST_KEYSYSTEM) {
// On failure the session should have been closed, so verify.
player.session.closed.then(
function(result) {
Utils.setResultInTitle(EME_SESSION_CLOSED_AND_ERROR);
},
function(error) { Utils.failTest(error); });
} else {
Utils.failTest(error);
}
});
};
// Register the necessary event handlers needed when playing encrypted content.
// Returns a promise that resolves to the player.
PlayerUtils.registerEMEEventListeners = function(player) {
player.video.addEventListener('encrypted', function(message) {
function addMediaKeySessionListeners(mediaKeySession) {
mediaKeySession.addEventListener('message', function(message) {
Utils.timeLog('MediaKeyMessageEvent: ' + message.messageType);
player.video.receivedKeyMessage = true;
if (message.messageType == 'license-request' ||
message.messageType == 'license-renewal' ||
message.messageType == 'license-release' ||
message.messageType == 'individualization-request') {
player.video.receivedMessageTypes.add(message.messageType);
} else {
Utils.failTest('Unexpected message type:' + message.messageType,
EME_MESSAGE_UNEXPECTED_TYPE);
}
player.onMessage(message);
});
mediaKeySession.addEventListener('keystatuseschange', function(e) {
const result = [];
for (let item of mediaKeySession.keyStatuses) {
result.push(`{kid:${
Utils.base64urlEncode(
Utils.convertToUint8Array(item[0]))},status:${item[1]}}`);
}
Utils.timeLog('KeyStatusesChange: ' + result.join(','));
if (player.testConfig.playCount == 0) {
Utils.setResultInTitle('ENDED');
}
});
}
// Calls getStatusForPolicy() and returns a resolved promise if the result
// matches the |expectedResult|, whose value can be:
// - a valid key status, e.g. "usable", in which case getStatusForPolicy()
// must be resolved with |expectedResult|.
// - "rejected", in which case getStatusForPolicy() must be rejected.
// - "resolved", in which case getStatusForPolicy() can be resolved by any
// value.
async function getStatusForHdcpPolicy(
mediaKeys, hdcpVersion, expectedResult) {
try {
var keyStatus =
await mediaKeys.getStatusForPolicy({minHdcpVersion: hdcpVersion});
if (expectedResult == 'resolved' ||
(expectedResult != 'rejected' && keyStatus == expectedResult)) {
return true;
}
return Promise.reject(
'For HDCP version "' + hdcpVersion + '", keyStatus "' + keyStatus +
'" does not match "' + expectedResult + '"');
} catch (e) {
if (expectedResult == 'rejected') {
return true;
}
return Promise.reject('Promise rejected unexpectedly: ' + e);
}
}
// Tests HDCP policy check. Returns a resolved promise if all tests pass.
function testGetStatusForHdcpPolicy(mediaKeys) {
const keySystem = this.testConfig.keySystem;
Utils.timeLog('Key system: ' + keySystem);
if (keySystem == EXTERNAL_CLEARKEY) {
// ClearKeyCdm pretends the device is HDCP 2.0 compliant. See
// ClearKeyCdm::GetStatusForPolicy() for details.
return Promise.all([
getStatusForHdcpPolicy(mediaKeys, '', 'usable'),
getStatusForHdcpPolicy(mediaKeys, '1.0', 'usable'),
getStatusForHdcpPolicy(mediaKeys, '2.3', 'output-restricted'),
]);
}
if (keySystem == CLEARKEY) {
// AesDecryptor does not support getStatusForPolicy() so the promise
// is always rejected.
return Promise.all([
getStatusForHdcpPolicy(mediaKeys, '', 'rejected'),
getStatusForHdcpPolicy(mediaKeys, '1.0', 'rejected'),
]);
}
if (keySystem == WIDEVINE_KEYSYSTEM) {
// Widevine CDM supports getStatusForPolicy() so the promise is always
// resolved. However the key status depends on the device's HDCP level
// so we cannot enforce it.
return Promise.all([
getStatusForHdcpPolicy(mediaKeys, '', 'usable'),
getStatusForHdcpPolicy(mediaKeys, '1.0', 'resolved'),
]);
}
return Promise.reject('Unsupported key system');
}
try {
if (player.testConfig.sessionToLoad) {
// Create a session to load using a new MediaKeys.
// TODO(jrummell): Add a test that covers remove().
player.access.createMediaKeys()
.then(function(mediaKeys) {
// As the tests run with a different origin every time, there is
// no way currently to create a session in one test and then load
// it in a subsequent test (https://crbug.com/715349).
// So if |sessionToLoad| is 'PersistentLicense', create a session
// that can be loaded and use that session to load. Otherwise
// use the name provided (which allows for testing load() on a
// session which doesn't exist).
if (player.testConfig.sessionToLoad == 'PersistentLicense') {
return Utils.createSessionToLoad(
mediaKeys, player.testConfig.sessionToLoad);
} else {
return player.testConfig.sessionToLoad;
}
})
.then(function(sessionId) {
Utils.timeLog('Loading session: ' + sessionId);
player.session =
message.target.mediaKeys.createSession('persistent-license');
addMediaKeySessionListeners(player.session);
return player.session.load(sessionId);
})
.then(
function(result) {
if (!result)
Utils.failTest('Session not found.', EME_SESSION_NOT_FOUND);
},
function(error) {
Utils.failTest(error, EME_LOAD_FAILED);
});
} else if (player.testConfig.policyCheck) {
// TODO(xhwang): We should be able to move all policy check code to a
// new test js file once we figure out an easy way to separate the
// requrestMediaKeySystemAccess() logic from the rest of this file.
Utils.timeLog('Policy check test.');
player.access.createMediaKeys().then(function (mediaKeys) {
// Call getStatusForPolicy() before creating any MediaKeySessions.
return testGetStatusForHdcpPolicy(mediaKeys);
}).then(function (result) {
Utils.timeLog('Policy check test passed.');
Utils.setResultInTitle(UNIT_TEST_SUCCESS);
}).catch(function (error) {
Utils.timeLog('Policy check test failed.');
Utils.failTest(error, UNIT_TEST_FAILURE);
});
} else {
Utils.timeLog(
'Creating new media key session for initDataType: ' +
message.initDataType + ', initData: ' +
Utils.getHexString(new Uint8Array(message.initData)));
player.session = message.target.mediaKeys.createSession();
addMediaKeySessionListeners(player.session);
player.session.generateRequest(message.initDataType, message.initData)
.catch(function(error) {
// Ignore the error if a crash is expected. This ensures that
// the decoder actually detects and reports the error.
if (this.testConfig.keySystem != CRASH_TEST_KEYSYSTEM) {
Utils.failTest(error, EME_GENERATEREQUEST_FAILED);
}
});
}
} catch (e) {
Utils.failTest(e);
}
});
this.registerDefaultEventListeners(player);
player.video.receivedKeyMessage = false;
player.video.receivedMessageTypes = new Set();
Utils.timeLog('Setting video media keys: ' + player.testConfig.keySystem);
var config = {
audioCapabilities: [],
videoCapabilities: [],
persistentState: 'optional',
sessionTypes: ['temporary'],
};
// requestMediaKeySystemAccess() requires at least one of 'audioCapabilities'
// or 'videoCapabilities' to be specified. It also requires only codecs
// specific to the capability, so unlike MSE cannot have both audio and
// video codecs in the contentType.
if (player.testConfig.mediaType) {
if (player.testConfig.mediaType.substring(0, 5) == 'video') {
config.videoCapabilities = [{contentType: player.testConfig.mediaType}];
} else if (player.testConfig.mediaType.substring(0, 5) == 'audio') {
config.audioCapabilities = [{contentType: player.testConfig.mediaType}];
}
// Handle special cases where both audio and video are needed.
if (player.testConfig.mediaType == 'video/webm; codecs="vorbis, vp8"') {
config.audioCapabilities = [{contentType: 'audio/webm; codecs="vorbis"'}];
config.videoCapabilities = [{contentType: 'video/webm; codecs="vp8"'}];
} else if (
player.testConfig.mediaType == 'video/webm; codecs="opus, vp9"') {
config.audioCapabilities = [{contentType: 'audio/webm; codecs="opus"'}];
config.videoCapabilities = [{contentType: 'video/webm; codecs="vp9"'}];
} else if (
player.testConfig.mediaType ==
'video/mp4; codecs="mp4a.40.2, avc1.64001E"') {
config.audioCapabilities =
[{contentType: 'audio/mp4; codecs="mp4a.40.2"'}];
config.videoCapabilities =
[{contentType: 'video/mp4; codecs="avc1.64001E"'}];
}
} else {
// Some tests (e.g. mse_different_containers.html) specify audio and
// video codecs separately.
if (player.testConfig.videoFormat) {
config.videoCapabilities = [{contentType: player.testConfig.videoFormat}];
}
if (player.testConfig.audioFormat) {
config.audioCapabilities = [{contentType: player.testConfig.audioFormat}];
}
}
// The File IO test requires persistent state support.
if (player.testConfig.keySystem == FILE_IO_TEST_KEYSYSTEM) {
config.persistentState = 'required';
} else if (
player.testConfig.sessionToLoad ||
player.testConfig.keySystem == STORAGE_ID_TEST_KEYSYSTEM) {
config.persistentState = 'required';
config.sessionTypes = ['temporary', 'persistent-license'];
}
return navigator
.requestMediaKeySystemAccess(player.testConfig.keySystem, [config])
.then(function(access) {
player.access = access;
return access.createMediaKeys();
})
.then(function(mediaKeys) {
return player.video.setMediaKeys(mediaKeys);
})
.then(function(result) {
return player;
})
.catch(function(error) {
Utils.failTest(error, NOTSUPPORTEDERROR);
});
};
PlayerUtils.setVideoSource = function(player) {
if (player.testConfig.useMSE) {
Utils.timeLog('Loading media using MSE.');
var mediaSource =
MediaSourceUtils.loadMediaSourceFromTestConfig(player.testConfig);
player.video.src = window.URL.createObjectURL(mediaSource);
} else {
Utils.timeLog('Loading media using src.');
player.video.src = player.testConfig.mediaFile;
}
Utils.timeLog('video.src has been set to ' + player.video.src);
};
// Initialize the player to play encrypted content. Returns a promise that
// resolves to the player.
PlayerUtils.initEMEPlayer = function(player) {
return player.registerEventListeners().then(function(result) {
PlayerUtils.setVideoSource(player);
Utils.timeLog('initEMEPlayer() done');
return player;
});
};
// Return the appropriate player based on test configuration.
PlayerUtils.createPlayer = function(video, testConfig) {
function getPlayerType(keySystem) {
switch (keySystem) {
case WIDEVINE_KEYSYSTEM:
return WidevinePlayer;
case CLEARKEY:
case EXTERNAL_CLEARKEY:
case MEDIAFOUNDATION_CLEARKEY:
case MESSAGE_TYPE_TEST_KEYSYSTEM:
case CRASH_TEST_KEYSYSTEM:
return ClearKeyPlayer;
case FILE_IO_TEST_KEYSYSTEM:
case OUTPUT_PROTECTION_TEST_KEYSYSTEM:
case PLATFORM_VERIFICATION_TEST_KEYSYSTEM:
case VERIFY_HOST_FILES_TEST_KEYSYSTEM:
case STORAGE_ID_TEST_KEYSYSTEM:
return UnitTestPlayer;
default:
Utils.timeLog(keySystem + ' is not a known key system');
return ClearKeyPlayer;
}
}
var Player = getPlayerType(testConfig.keySystem);
return new Player(video, testConfig);
};
PlayerUtils.removeSession = async function(player) {
// Once remove() is called, another 'keystatuseschange' and 'message' events
// will happen.
const waitForKeyStatusChangePromise =
Utils.waitForEvent(player.session, 'keystatuseschange');
const waitForMessagePromise = Utils.waitForEvent(
player.session, 'message', function(e, resolve, reject) {
Utils.timeLog(e.messageType);
resolve();
});
Utils.timeLog('Calling remove()');
const removePromise = player.session.remove();
return Promise.all(
[removePromise, waitForKeyStatusChangePromise, waitForMessagePromise]);
}