Bug 1142379 - Encrypt audio and video tracks using separate encryption settings. r=edwin

This commit is contained in:
Gerald Squelart 2015-03-22 12:41:00 -04:00
parent 4c1646cd0f
commit ed55cb93ff
17 changed files with 279 additions and 83 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This XML file describes the encryption applied to |bipbop-cenc*|. To
generate the bipbop-cenc1 files, run the following commands:
# Encrypt bipbop-no-edts.mp4 with the keys specified in this file,
# and output to |bipbop-cenc1-{video,audio}.mp4|
MP4Box -crypt bipbop-frag-cenc-video.xml -rem 2 -out bipbop-cenc1-video.mp4 bipbop-no-edts.mp4
MP4Box -crypt bipbop-frag-cenc-audio.xml -rem 1 -out bipbop-cenc1-audio.mp4 bipbop-no-edts.mp4
# Fragment |bipbop-cenc1-*.mp4| into 500ms segments:
MP4Box -dash 500 -rap -segment-name bipbop-cenc1-video -subsegs-per-sidx 5 bipbop-cenc1-video.mp4
MP4Box -dash 500 -rap -segment-name bipbop-cenc1-audio -subsegs-per-sidx 5 bipbop-cenc1-audio.mp4
# The above command will generate a set of fragments in |bipbop-cenc1-{video,audio}*.m4s
# and |bipbop-cenc1-{video,audio}init.mp4| containing just the init segment.
# To cut down the duration, we throw out all but the first 3 audio & 2 video segments:
rm bipbop-cenc1-audio{[^123],[123][^.]}.m4s
rm bipbop-cenc1-video{[^12],[12][^.]}.m4s
# MP4Box will also have generated some *.mpd files we don't need:
rm bipbop-cenc1-*.mpd
# Delete intermediate encrypted files:
rm bipbop-cenc1-{audio,video}.mp4
-->
<GPACDRM type="CENC AES-CTR">
<DRMInfo type="pssh" version="1">
<!--
SystemID specified in
https://dvcs.w3.org/hg/html-media/raw-file/tip/encrypted-media/cenc-format.html
-->
<BS ID128="1077efecc0b24d02ace33c1e52e2fb4b" />
<!-- Number of KeyIDs = 1 -->
<BS bits="32" value="1" />
<!-- KeyID -->
<BS ID128="0x7e571d047e571d047e571d047e571d04" />
</DRMInfo>
<CrypTrack trackID="2" isEncrypted="1" IV_size="16" saiSavedBox="senc"
first_IV="0x00000000000000000000000000000000">
<key KID="0x7e571d047e571d047e571d047e571d04"
value="0x7e5744447e5744447e5744447e574444" />
</CrypTrack>
</GPACDRM>

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This XML file describes the encryption applied to |bipbop-cenc*|. To
generate the bipbop-cenc1 files, run the following commands:
# Encrypt bipbop-no-edts.mp4 with the keys specified in this file,
# and output to |bipbop-cenc1-{video,audio}.mp4|
MP4Box -crypt bipbop-frag-cenc-video.xml -rem 2 -out bipbop-cenc1-video.mp4 bipbop-no-edts.mp4
MP4Box -crypt bipbop-frag-cenc-audio.xml -rem 1 -out bipbop-cenc1-audio.mp4 bipbop-no-edts.mp4
# Fragment |bipbop-cenc1-*.mp4| into 500ms segments:
MP4Box -dash 500 -rap -segment-name bipbop-cenc1-video -subsegs-per-sidx 5 bipbop-cenc1-video.mp4
MP4Box -dash 500 -rap -segment-name bipbop-cenc1-audio -subsegs-per-sidx 5 bipbop-cenc1-audio.mp4
# The above command will generate a set of fragments in |bipbop-cenc1-{video,audio}*.m4s
# and |bipbop-cenc1-{video,audio}init.mp4| containing just the init segment.
# To cut down the duration, we throw out all but the first 3 audio & 2 video segments:
rm bipbop-cenc1-audio{[^123],[123][^.]}.m4s
rm bipbop-cenc1-video{[^12],[12][^.]}.m4s
# MP4Box will also have generated some *.mpd files we don't need:
rm bipbop-cenc1-*.mpd
# Delete intermediate encrypted files:
rm bipbop-cenc1-{audio,video}.mp4
-->
<GPACDRM type="CENC AES-CTR">
<DRMInfo type="pssh" version="1">
<!--
SystemID specified in
https://dvcs.w3.org/hg/html-media/raw-file/tip/encrypted-media/cenc-format.html
-->
<BS ID128="1077efecc0b24d02ace33c1e52e2fb4b" />
<!-- Number of KeyIDs = 1 -->
<BS bits="32" value="1" />
<!-- KeyID -->
<BS ID128="0x7e571d037e571d037e571d037e571d03" />
</DRMInfo>
<CrypTrack trackID="1" isEncrypted="1" IV_size="16" saiSavedBox="senc"
first_IV="0x00000000000000000000000000000000">
<key KID="0x7e571d037e571d037e571d037e571d03"
value="0x7e5733337e5733337e5733337e573333" />
</CrypTrack>
</GPACDRM>

View File

@ -34,6 +34,14 @@ function StringToArrayBuffer(str)
return arr;
}
function StringToHex(str){
var res = "";
for (var i = 0; i < str.length; ++i) {
res += ("0" + str.charCodeAt(i).toString(16)).slice(-2);
}
return res;
}
function Base64ToHex(str)
{
var bin = window.atob(str.replace(/-/g, "+").replace(/_/g, "/"));
@ -89,11 +97,8 @@ function TimeRangesToString(trs)
function SourceBufferToString(sb)
{
return ("SourceBuffer{"
+ "AppendMode=" + (sb.AppendMode || "-")
+ ", updating=" + (sb.updating ? "true" : "false")
+ ", buffered=" + TimeRangesToString(sb.buffered)
+ ", audioTracks=" + (sb.audioTracks ? sb.audioTracks.length : "-")
+ ", videoTracks=" + (sb.videoTracks ? sb.videoTracks.length : "-")
+ (sb.updating ? "updating, " : "")
+ "buffered=" + TimeRangesToString(sb.buffered)
+ "}");
}
@ -307,8 +312,7 @@ function PlayMultiTrack(test, elem, token)
}
// Returns a promise that is resolved when the media element is ready to have
// its play() function called; when it's loaded MSE fragments, or once the load
// has started for non-MSE video.
// its play() function called; when it's loaded MSE fragments.
function LoadTest(test, elem, token)
{
if (test.fragments) {
@ -349,25 +353,51 @@ function SetupEME(test, token, params)
? params.onSetKeysFail
: bail(token + " Failed to set MediaKeys on <video> element");
var firstEncrypted = true;
v.addEventListener("encrypted", function(ev) {
if (!firstEncrypted) {
// TODO: Better way to handle 'encrypted'?
// Maybe wait for metadataloaded and all expected 'encrypted's?
Log(token, "got encrypted event again, initDataType=" + ev.initDataType);
return;
}
firstEncrypted = false;
Log(token, "got encrypted event, initDataType=" + ev.initDataType);
var options = [
// null: No session management in progress, just go ahead and update the session.
// [...]: Session management in progress, add [initDataType, initData] to
// this queue to get it processed when possible.
var initDataQueue = [];
function processInitDataQueue()
{
initDataType: ev.initDataType,
videoType: test.type,
audioType: test.type,
if (initDataQueue === null) { return; }
if (initDataQueue.length === 0) { initDataQueue = null; return; }
var ev = initDataQueue.shift();
var sessionType = (params && params.sessionType) ? params.sessionType : "temporary";
Log(token, "createSession(" + sessionType + ") for (" + ev.initDataType + ", " + StringToHex(ArrayBufferToString(ev.initData)) + ")");
var session = v.mediaKeys.createSession(sessionType);
if (params && params.onsessioncreated) {
params.onsessioncreated(session);
}
];
return new Promise(function (resolve, reject) {
session.addEventListener("message", UpdateSessionFunc(test, token, sessionType, resolve, reject));
Log(token, "session.generateRequest(" + ev.initDataType + ", " + StringToHex(ArrayBufferToString(ev.initData)) + ")");
session.generateRequest(ev.initDataType, ev.initData).catch(function(reason) {
// Reject the promise if generateRequest() failed. Otherwise it will
// be resolve in UpdateSessionFunc().
bail(token + ": session.generateRequest(" + ev.initDataType + ", " + StringToHex(ArrayBufferToString(ev.initData)) + ") failed")(reason);
reject();
});
})
.then(function(aSession) {
Log(token, "session.generateRequest(" + ev.initDataType + ", " + StringToHex(ArrayBufferToString(ev.initData)) + ") succeeded");
if (params && params.onsessionupdated) {
params.onsessionupdated(aSession);
}
processInitDataQueue();
});
}
// All 'initDataType's should be the same.
// null indicates no 'encrypted' event received yet.
var initDataType = null;
v.addEventListener("encrypted", function(ev) {
if (initDataType === null) {
Log(token, "got first encrypted(" + ev.initDataType + ", " + StringToHex(ArrayBufferToString(ev.initData)) + "), setup session");
initDataType = ev.initDataType;
initDataQueue.push(ev);
function chain(promise, onReject) {
return promise.then(function(value) {
@ -378,6 +408,13 @@ function SetupEME(test, token, params)
})
}
var options = [
{
initDataType: ev.initDataType,
videoType: test.type,
audioType: test.type,
}
];
var p = navigator.requestMediaKeySystemAccess(KEYSYSTEM_TYPE, options);
var r = bail(token + " Failed to request key system access.");
chain(p, r)
@ -396,29 +433,23 @@ function SetupEME(test, token, params)
.then(function() {
Log(token, "set MediaKeys on <video> element ok");
var sessionType = (params && params.sessionType) ? params.sessionType : "temporary";
var session = v.mediaKeys.createSession(sessionType);
if (params && params.onsessioncreated) {
params.onsessioncreated(session);
}
return new Promise(function (resolve, reject) {
session.addEventListener("message", UpdateSessionFunc(test, token, sessionType, resolve, reject));
session.generateRequest(ev.initDataType, ev.initData).catch(function(reason) {
// Reject the promise if generateRequest() failed. Otherwise it will
// be resolve in UpdateSessionFunc().
bail(token + ": session.generateRequest failed")(reason);
reject();
});
});
processInitDataQueue();
})
.then(function(session) {
Log(token, ": session.generateRequest succeeded");
if (params && params.onsessionupdated) {
params.onsessionupdated(session);
} else {
if (ev.initDataType !== initDataType) {
return bail(token + ": encrypted(" + ev.initDataType + ", " +
StringToHex(ArrayBufferToString(ev.initData)) + ")")
("expected " + initDataType);
}
if (initDataQueue !== null) {
Log(token, "got encrypted(" + ev.initDataType + ", " + StringToHex(ArrayBufferToString(ev.initData)) + ") event, queue it for later session update");
initDataQueue.push(ev);
} else {
Log(token, "got encrypted(" + ev.initDataType + ", " + StringToHex(ArrayBufferToString(ev.initData)) + ") event, update session now");
initDataQueue = [ev];
processInitDataQueue();
}
}
});
});
return v;
}

View File

@ -647,7 +647,7 @@ var gMetadataTests = [
// Test files for Encrypted Media Extensions
var gEMETests = [
{
name:"bipbop-cenc-videoinit.mp4",
name:"video-only with 2 keys",
type:"video/mp4; codecs=\"avc1.64000d,mp4a.40.2\"",
fragments:[ "bipbop-cenc-videoinit.mp4",
"bipbop-cenc-video1.m4s",
@ -659,10 +659,11 @@ var gEMETests = [
"7e571d047e571d047e571d047e571d04" : "7e5744447e5744447e5744447e574444",
},
sessionType:"temporary",
sessions:1,
duration:1.60,
},
{
name:"bipbop-cenc-videoinit.mp4",
name:"video-only with 2 keys, CORS",
type:"video/mp4; codecs=\"avc1.64000d,mp4a.40.2\"",
fragments:[ "bipbop-cenc-videoinit.mp4",
"bipbop-cenc-video1.m4s",
@ -674,11 +675,12 @@ var gEMETests = [
"7e571d047e571d047e571d047e571d04" : "7e5744447e5744447e5744447e574444",
},
sessionType:"temporary",
sessions:1,
crossOrigin:true,
duration:1.60,
},
{
name:"bipbop-cenc-videoinit.mp4",
name:"audio&video tracks, both with all keys",
type:"video/mp4; codecs=\"avc1.64000d,mp4a.40.2\"",
tracks: [
{
@ -705,10 +707,11 @@ var gEMETests = [
"7e571d047e571d047e571d047e571d04" : "7e5744447e5744447e5744447e574444",
},
sessionType:"temporary",
sessions:2,
duration:1.60,
},
{
name:"bipbop-cenc-videoinit.mp4",
name:"audio&video tracks, both with all keys, CORS",
type:"video/mp4; codecs=\"avc1.64000d,mp4a.40.2\"",
tracks: [
{
@ -735,9 +738,41 @@ var gEMETests = [
"7e571d047e571d047e571d047e571d04" : "7e5744447e5744447e5744447e574444",
},
sessionType:"temporary",
sessions:2,
crossOrigin:true,
duration:1.60,
},
{
name:"audio&video tracks, each with its key",
type:"video/mp4; codecs=\"avc1.64000d,mp4a.40.2\"",
tracks: [
{
name:"audio",
type:"audio/mp4; codecs=\"mp4a.40.2\"",
fragments:[ "bipbop-cenc1-audioinit.mp4",
"bipbop-cenc1-audio1.m4s",
"bipbop-cenc1-audio2.m4s",
"bipbop-cenc1-audio3.m4s",
],
},
{
name:"video",
type:"video/mp4; codecs=\"avc1.64000d\"",
fragments:[ "bipbop-cenc1-videoinit.mp4",
"bipbop-cenc1-video1.m4s",
"bipbop-cenc1-video2.m4s",
],
},
],
keys: {
// "keyid" : "key"
"7e571d037e571d037e571d037e571d03" : "7e5733337e5733337e5733337e573333",
"7e571d047e571d047e571d047e571d04" : "7e5744447e5744447e5744447e574444",
},
sessionType:"temporary",
sessions:2,
duration:1.60,
},
];
var gEMENonMSEFailTests = [

View File

@ -52,6 +52,13 @@ support-files =
bipbop-cenc-video1.m4s
bipbop-cenc-video2.m4s
bipbop-cenc-videoinit.mp4
bipbop-cenc1-audio1.m4s
bipbop-cenc1-audio2.m4s
bipbop-cenc1-audio3.m4s
bipbop-cenc1-audioinit.mp4
bipbop-cenc1-video1.m4s
bipbop-cenc1-video2.m4s
bipbop-cenc1-videoinit.mp4
bogus.duh
bogus.ogv
bogus.ogv^headers^

View File

@ -38,7 +38,10 @@ function startTest(test, token)
manager.finished(token);
});
LoadTest(test, v, token);
manager.started(token + "_load");
LoadTest(test, v, token)
.then(function() { manager.finished(token + "_load"); })
.catch(function() { manager.finished(token + "_load"); });
}
function beginTest() {

View File

@ -52,7 +52,10 @@ function startTest(test, token)
manager.finished(token);
});
LoadTest(test, v, token);
manager.started(token + "_load");
LoadTest(test, v, token)
.then(function() { manager.finished(token + "_load"); })
.catch(function() { manager.finished(token + "_load"); });
}
function beginTest() {

View File

@ -74,7 +74,6 @@ function startTest(test, token)
onsessionupdated: function(session) {
Log(token, "Session created");
var sessionId;
initialSession = session;
// Once the session has loaded and has all its keys usable, close
// all sessions without calling remove() on them.
@ -147,11 +146,14 @@ function startTest(test, token)
}
);
LoadTest(test, v, token);
manager.started(token + "_load");
LoadTest(test, v, token)
.then(function() { manager.finished(token + "_load"); })
.catch(function() { manager.finished(token + "_load"); });
}
function beginTest() {
manager.runTests(gEMETests, startTest);
manager.runTests(gEMETests.filter(t => t.sessions === 1), startTest);
}
var prefs = [

View File

@ -74,16 +74,22 @@ function startTest(test, token)
ok(Math.abs(test.duration - v.currentTime) < 0.1,
TimeStamp(token) + " Current time should be same as duration");
// Verify all sessions had all keys went sent the to the CDM usable, and thus
// Verify all sessions had all keys went sent to the CDM usable, and thus
// that we received keystatuseschange event(s).
is(sessions.length, 1, TimeStamp(token) + " should have 1 session");
is(sessions.length, test.sessions, TimeStamp(token) + " should have " + test.sessions + " session" + (test.sessions===1?"":"s"));
var keyIdsReceived = [];
for (var keyid in test.keys) { keyIdsReceived[keyid] = false; }
for (var i = 0; i < sessions.length; i++) {
var session = sessions[i];
ok(session.gotKeysChanged, TimeStamp(token) + " should have received at least one keychange event");
for (var kid in session.keyIdsReceived) {
ok(session.keyIdsReceived[kid], TimeStamp(token) + " key with id " + kid + " was usable as expected");
Log(token, "session " + i + " key " + kid + " = " + (session.keyIdsReceived[kid] ? "true" : "false"));
if (session.keyIdsReceived[kid]) { keyIdsReceived[kid] = true; }
}
}
for (var kid in keyIdsReceived) {
ok(keyIdsReceived[kid], TimeStamp(token) + " key with id " + kid + " was usable as expected");
}
manager.finished(token);
});

View File

@ -32,7 +32,10 @@ function startTest(test, token)
ok(false, TimeStamp(case1token) + " should never reach loadeddata, as setMediaKeys should fail");
});
manager.started(case1token);
LoadTest(test, v1, case1token);
manager.started(case1token + "_load");
LoadTest(test, v1, case1token)
.then(function() { manager.finished(case1token + "_load" ); })
.catch(function() { manager.finished(case1token + "_load" ); });
// Case 2. creating a MediaElementSource on a media element with a MediaKeys should fail.
@ -51,7 +54,10 @@ function startTest(test, token)
manager.finished(case2token);
});
manager.started(case2token);
LoadTest(test, v2, case2token);
manager.started(case2token + "_load");
LoadTest(test, v2, case2token)
.then(function() { manager.finished(case2token + "_load" ); })
.catch(function() { manager.finished(case2token + "_load" ); });
// Case 3. capturing a media element with mozCaptureStream that has a MediaKeys should fail.
@ -69,7 +75,10 @@ function startTest(test, token)
manager.finished(case3token);
});
manager.started(case3token);
LoadTest(test, v3, case3token);
manager.started(case3token + "_load");
LoadTest(test, v3, case3token)
.then(function() { manager.finished(case3token + "_load" ); })
.catch(function() { manager.finished(case3token + "_load" ); });
}
function beginTest() {