4624510d by David LaPalomento

Merge pull request #246 from videojs/live-hlse-fixes

Fix discontinuities
2 parents 021896e3 4222a1f8
......@@ -70,7 +70,7 @@
type="application/x-mpegURL">
</video>
<script>
videojs.options.flash.swf = 'node_modules/video.js/dist/video-js/video-js.swf';
videojs.options.flash.swf = 'node_modules/videojs-swf/dist/video-js.swf';
// initialize the player
var player = videojs('video');
</script>
......
......@@ -43,6 +43,7 @@
},
"dependencies": {
"pkcs7": "^0.2.2",
"videojs-contrib-media-sources": "^0.3.0"
"videojs-contrib-media-sources": "^0.3.0",
"videojs-swf": "^4.6.0"
}
}
......
......@@ -221,14 +221,13 @@ videojs.Hls.prototype.src = function(src) {
this.mediaIndex = videojs.Hls.translateMediaIndex(this.mediaIndex, oldMediaPlaylist, updatedPlaylist);
oldMediaPlaylist = updatedPlaylist;
this.fetchKeys(updatedPlaylist, this.mediaIndex);
this.fetchKeys_();
}));
this.playlists.on('mediachange', videojs.bind(this, function() {
// abort outstanding key requests and check if new keys need to be retrieved
if (keyXhr) {
this.cancelKeyXhr();
this.fetchKeys(this.playlists.media(), this.mediaIndex);
}
player.trigger('mediachange');
......@@ -330,11 +329,10 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) {
// cancel outstanding requests and buffer appends
this.cancelSegmentXhr();
// fetch new encryption keys, if necessary
// abort outstanding key requests, if necessary
if (keyXhr) {
keyXhr.aborted = true;
this.cancelKeyXhr();
this.fetchKeys(this.playlists.media(), this.mediaIndex);
}
// clear out any buffered segments
......@@ -659,6 +657,7 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
offset: offset,
bytes: new Uint8Array(this.response)
});
player.trigger('progress');
tech.drainBuffer();
tech.mediaIndex++;
......@@ -700,7 +699,8 @@ videojs.Hls.prototype.drainBuffer = function(event) {
if (keyFailed(segment.key)) {
return segmentBuffer.shift();
} else if (!segment.key.bytes) {
return;
// trigger a key request if one is not already in-flight
return this.fetchKeys_();
} else {
// if the media sequence is greater than 2^32, the IV will be incorrect
// assuming 10s segments, that would be about 1300 years
......@@ -714,23 +714,6 @@ videojs.Hls.prototype.drainBuffer = function(event) {
event = event || {};
segmentOffset = videojs.Hls.getPlaylistDuration(playlist, 0, mediaIndex) * 1000;
// abort() clears any data queued in the source buffer so wait
// until it empties before calling it when a discontinuity is
// next in the buffer
if (segment.discontinuity) {
if (event.type === 'waiting') {
this.sourceBuffer.abort();
// tell the SWF where playback is continuing in the stitched timeline
this.el().vjs_setProperty('currentTime', segmentOffset * 0.001);
} else if (event.type === 'timeupdate') {
return;
} else if (typeof offset !== 'number') {
//if the discontinuity is reached under normal conditions, ie not a seek,
//the buffer already contains data and does not need to be refilled,
return;
}
}
// transmux the segment data from MP2T to FLV
this.segmentParser_.parseSegmentBinaryData(bytes);
this.segmentParser_.flushTags();
......@@ -758,6 +741,12 @@ videojs.Hls.prototype.drainBuffer = function(event) {
this.lastSeekedTime_ = null;
}
// when we're crossing a discontinuity, inject metadata to indicate
// that the decoder should be reset appropriately
if (segment.discontinuity && tags.length) {
this.el().vjs_discontinuity();
}
for (i = 0; i < tags.length; i++) {
// queue up the bytes to be appended to the SourceBuffer
// the queue gives control back to the browser between tags
......@@ -776,11 +765,19 @@ videojs.Hls.prototype.drainBuffer = function(event) {
}
};
videojs.Hls.prototype.fetchKeys = function(playlist, index) {
var i, key, tech, player, settings, view;
/**
* Attempt to retrieve keys starting at a particular media
* segment. This method has no effect if segments are not yet
* available or a key request is already in progress.
*
* @param playlist {object} the media playlist to fetch keys for
* @param index {number} the media segment index to start from
*/
videojs.Hls.prototype.fetchKeys_ = function() {
var i, key, tech, player, settings, segment, view, receiveKey;
// if there is a pending XHR or no segments, don't do anything
if (keyXhr || !playlist.segments) {
if (keyXhr || !this.segmentBuffer_.length) {
return;
}
......@@ -788,39 +785,55 @@ videojs.Hls.prototype.fetchKeys = function(playlist, index) {
player = this.player();
settings = player.options().hls || {};
// jshint -W083
for (i = index; i < playlist.segments.length; i++) {
key = playlist.segments[i].key;
if (key && !key.bytes && !keyFailed(key)) {
/**
* Handle a key XHR response. This function needs to lookup the
*/
receiveKey = function(key) {
return function(error) {
keyXhr = null;
if (error || !this.response || this.response.byteLength !== 16) {
key.retries = key.retries || 0;
key.retries++;
if (!this.aborted) {
// try fetching again
tech.fetchKeys_();
}
return;
}
view = new DataView(this.response);
key.bytes = new Uint32Array([
view.getUint32(0),
view.getUint32(4),
view.getUint32(8),
view.getUint32(12)
]);
// check to see if this allows us to make progress buffering now
tech.checkBuffer_();
};
};
for (i = 0; i < tech.segmentBuffer_.length; i++) {
segment = tech.segmentBuffer_[i].playlist.segments[tech.segmentBuffer_[i].mediaIndex];
key = segment.key;
// continue looking if this segment is unencrypted
if (!key) {
continue;
}
// request the key if the retry limit hasn't been reached
if (!key.bytes && !keyFailed(key)) {
keyXhr = videojs.Hls.xhr({
url: this.playlistUriToUrl(key.uri),
responseType: 'arraybuffer',
withCredentials: settings.withCredentials
}, function(err, url) {
keyXhr = null;
if (err || !this.response || this.response.byteLength !== 16) {
key.retries = key.retries || 0;
key.retries++;
if (!this.aborted) {
tech.fetchKeys(playlist, i);
}
return;
}
view = new DataView(this.response);
key.bytes = new Uint32Array([
view.getUint32(0),
view.getUint32(4),
view.getUint32(8),
view.getUint32(12)
]);
tech.fetchKeys(playlist, i++, url);
});
}, receiveKey(key));
break;
}
}
// jshint +W083
};
/**
......@@ -925,9 +938,7 @@ videojs.Hls.getPlaylistTotalDuration = function(playlist) {
* playlist
*/
videojs.Hls.translateMediaIndex = function(mediaIndex, original, update) {
var i,
originalSegment,
translatedMediaIndex;
var translatedMediaIndex;
// no segments have been loaded from the original playlist
if (mediaIndex === 0) {
......@@ -939,15 +950,8 @@ videojs.Hls.translateMediaIndex = function(mediaIndex, original, update) {
return 0;
}
// try to sync based on URI
i = update.segments.length;
originalSegment = original.segments[mediaIndex - 1];
while (i--) {
if (originalSegment.uri === update.segments[i].uri) {
return i + 1;
}
}
// translate based on media sequence numbers. syncing up across
// bitrate switches should be happening here.
translatedMediaIndex = (mediaIndex + (original.mediaSequence - update.mediaSequence));
if (translatedMediaIndex >= update.segments.length || translatedMediaIndex < 0) {
......@@ -955,7 +959,6 @@ videojs.Hls.translateMediaIndex = function(mediaIndex, original, update) {
return videojs.Hls.getMediaIndexForLive_(update) + 1;
}
// sync on media sequence
return translatedMediaIndex;
};
......
......@@ -54,6 +54,7 @@ var
tech.vjs_setProperty = function() {};
tech.vjs_src = function() {};
tech.vjs_play = function() {};
tech.vjs_discontinuity = function() {};
videojs.Flash.onReady(tech.id);
return player;
......@@ -86,7 +87,7 @@ var
contentType = 'video/MP2T';
}
request.response = new Uint8Array([1]).buffer;
request.response = new Uint8Array(16).buffer;
request.respond(200,
{ 'Content-Type': contentType },
window.manifests[manifestName]);
......@@ -571,6 +572,23 @@ test('calculates the bandwidth after downloading a segment', function() {
'saves segment request time: ' + player.hls.segmentXhrTime + 's');
});
test('fires a progress event after downloading a segment', function() {
var progressCount = 0;
player.src({
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
standardXHRResponse(requests.shift());
player.on('progress', function() {
progressCount++;
});
standardXHRResponse(requests.shift());
equal(progressCount, 1, 'fired a progress event');
});
test('selects a playlist after segment downloads', function() {
var calls = 0;
player.src({
......@@ -1221,6 +1239,7 @@ test('updates the media index when a playlist reloads', function() {
// reload the updated playlist
player.hls.playlists.media = function() {
return {
mediaSequence: 1,
segments: [{
uri: '1.ts'
}, {
......@@ -1348,9 +1367,10 @@ test('does not break if the playlist has no segments', function() {
strictEqual(requests.length, 1, 'no requests for non-existent segments were queued');
});
test('waits until the buffer is empty before appending bytes at a discontinuity', function() {
var aborts = 0, setTime, currentTime, bufferEnd;
test('calls vjs_discontinuity() before appending bytes at a discontinuity', function() {
var discontinuities = 0, tags = [], currentTime, bufferEnd;
videojs.Hls.SegmentParser = mockSegmentParser(tags);
player.src({
src: 'discontinuity.m3u8',
type: 'application/vnd.apple.mpegurl'
......@@ -1360,13 +1380,8 @@ test('waits until the buffer is empty before appending bytes at a discontinuity'
player.buffered = function() {
return videojs.createTimeRange(0, bufferEnd);
};
player.hls.sourceBuffer.abort = function() {
aborts++;
};
player.hls.el().vjs_setProperty = function(name, value) {
if (name === 'currentTime') {
return setTime = value;
}
player.el().querySelector('.vjs-tech').vjs_discontinuity = function() {
discontinuities++;
};
requests.pop().respond(200, null,
......@@ -1382,15 +1397,11 @@ test('waits until the buffer is empty before appending bytes at a discontinuity'
currentTime = 6;
bufferEnd = 10;
player.hls.checkBuffer_();
strictEqual(aborts, 0, 'no aborts before the buffer empties');
strictEqual(discontinuities, 0, 'no discontinuities before the segment is received');
tags.push({});
standardXHRResponse(requests.pop());
strictEqual(aborts, 0, 'no aborts before the buffer empties');
// pretend the buffer has emptied
player.trigger('waiting');
strictEqual(aborts, 1, 'aborted before appending the new segment');
strictEqual(setTime, 10, 'updated the time after crossing the discontinuity');
strictEqual(discontinuities, 1, 'signals a discontinuity');
});
test('clears the segment buffer on seek', function() {
......@@ -1781,40 +1792,23 @@ test('drainBuffer will not proceed with empty source buffer', function() {
player.hls.playlists.media = oldMedia;
});
test('calling fetchKeys() when a new playlist is loaded will create an XHR', function() {
test('keys are requested when an encrypted segment is loaded', function() {
player.src({
src: 'https://example.com/encrypted-media.m3u8',
src: 'https://example.com/encrypted.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
var oldMedia = player.hls.playlists.media;
player.hls.playlists.media = function() {
return {
segments: [{
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=52'
},
uri: 'http://media.example.com/fileSequence52-A.ts'
}, {
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=53'
},
uri: 'http://media.example.com/fileSequence53-B.ts'
}]
};
};
player.hls.playlists.trigger('loadedplaylist');
strictEqual(requests.length, 2, 'a key XHR is created');
strictEqual(requests[1].url, player.hls.playlists.media().segments[0].key.uri, 'a key XHR is created with correct uri');
standardXHRResponse(requests.shift()); // playlist
standardXHRResponse(requests.shift()); // first segment
player.hls.playlists.media = oldMedia;
strictEqual(requests.length, 1, 'a key XHR is created');
strictEqual(requests[0].url,
player.hls.playlists.media().segments[0].key.uri,
'a key XHR is created with correct uri');
});
test('fetchKeys() resolves URLs relative to the master playlist', function() {
test('keys are resolved relative to the master playlist', function() {
player.src({
src: 'video/master-encrypted.m3u8',
type: 'application/vnd.apple.mpegurl'
......@@ -1833,12 +1827,13 @@ test('fetchKeys() resolves URLs relative to the master playlist', function() {
'http://media.example.com/fileSequence1.ts\n' +
'#EXT-X-ENDLIST\n');
equal(requests.length, 2, 'requested two URLs');
standardXHRResponse(requests.shift());
equal(requests.length, 1, 'requested the key');
ok((/video\/playlist\/keys\/key\.php$/).test(requests[0].url),
'resolves multiple relative paths');
});
test('fetchKeys() resolves URLs relative to their containing playlist', function() {
test('keys are resolved relative to their containing playlist', function() {
player.src({
src: 'video/media-encrypted.m3u8',
type: 'application/vnd.apple.mpegurl'
......@@ -1851,197 +1846,100 @@ test('fetchKeys() resolves URLs relative to their containing playlist', function
'#EXTINF:2.833,\n' +
'http://media.example.com/fileSequence1.ts\n' +
'#EXT-X-ENDLIST\n');
equal(requests.length, 2, 'requested two URLs');
standardXHRResponse(requests.shift());
equal(requests.length, 1, 'requested a key');
ok((/video\/keys\/key\.php$/).test(requests[0].url),
'resolves multiple relative paths');
});
test('a new keys XHR is created when a previous key XHR finishes', function() {
test('a new key XHR is created when a the segment is received', function() {
player.src({
src: 'https://example.com/encrypted-media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
var oldMedia = player.hls.playlists.media;
player.hls.playlists.media = function() {
return {
segments: [{
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=52'
},
uri: 'http://media.example.com/fileSequence52-A.ts'
}, {
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=53'
},
uri: 'http://media.example.com/fileSequence53-B.ts'
}]
};
};
// we're inject the media playlist, so drop the request
requests.shift();
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-TARGETDURATION:15\n' +
'#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
'#EXTINF:2.833,\n' +
'http://media.example.com/fileSequence1.ts\n' +
'#EXT-X-KEY:METHOD=AES-128,URI="keys/key2.php"\n' +
'#EXTINF:2.833,\n' +
'http://media.example.com/fileSequence2.ts\n' +
'#EXT-X-ENDLIST\n');
standardXHRResponse(requests.shift()); // segment 1
standardXHRResponse(requests.shift()); // key 1
standardXHRResponse(requests.shift()); // segment 2
player.hls.playlists.trigger('loadedplaylist');
// key response
requests[0].response = new Uint32Array([0, 0, 0, 0]).buffer;
requests.shift().respond(200, null, '');
strictEqual(requests.length, 1, 'a key XHR is created');
strictEqual(requests[0].url, player.hls.playlists.media().segments[1].key.uri, 'a key XHR is created with the correct uri');
player.hls.playlists.media = oldMedia;
});
test('calling fetchKeys() when a seek happens will create an XHR', function() {
player.src({
src: 'https://example.com/encrypted-media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
var oldMedia = player.hls.playlists.media;
player.hls.playlists.media = function() {
return {
segments: [{
duration: 10,
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=52'
},
uri: 'http://media.example.com/fileSequence52-A.ts'
}, {
duration: 10,
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=53'
},
uri: 'http://media.example.com/fileSequence53-B.ts'
}]
};
};
player.hls.fetchKeys(player.hls.playlists.media(), 0);
player.currentTime(11);
ok(requests[1].aborted, 'the key XHR should be aborted');
equal(requests.length, 3, 'we should get a new key XHR');
equal(requests[2].url, player.hls.playlists.media().segments[1].key.uri, 'urls should match');
player.hls.playlists.media = oldMedia;
strictEqual(requests[0].url,
'https://example.com/' +
player.hls.playlists.media().segments[1].key.uri,
'a key XHR is created with the correct uri');
});
test('calling fetchKeys() when a key XHR is in progress will *not* create an XHR', function() {
test('seeking should abort an outstanding key request and create a new one', function() {
player.src({
src: 'https://example.com/encrypted-media.m3u8',
src: 'https://example.com/encrypted.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
var oldMedia = player.hls.playlists.media;
player.hls.playlists.media = function() {
return {
segments: [{
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=52'
},
uri: 'http://media.example.com/fileSequence52-A.ts'
}, {
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=53'
},
uri: 'http://media.example.com/fileSequence53-B.ts'
}]
};
};
strictEqual(requests.length, 1, 'no key XHR created for the player');
player.hls.playlists.trigger('loadedplaylist');
player.hls.fetchKeys(player.hls.playlists.media(), 0);
strictEqual(requests.length, 2, 'only the original XHR is available');
player.hls.playlists.media = oldMedia;
});
test('calling fetchKeys() when all keys are fetched, will *not* create an XHR', function() {
player.src({
src: 'https://example.com/encrypted-media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
var oldMedia = player.hls.playlists.media;
player.hls.playlists.media = function() {
return {
segments: [{
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=52',
bytes: new Uint8Array([1])
},
uri: 'http://media.example.com/fileSequence52-A.ts'
}, {
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=53',
bytes: new Uint8Array([1])
},
uri: 'http://media.example.com/fileSequence53-B.ts'
}]
};
};
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-TARGETDURATION:15\n' +
'#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
'#EXTINF:9,\n' +
'http://media.example.com/fileSequence1.ts\n' +
'#EXT-X-KEY:METHOD=AES-128,URI="keys/key2.php"\n' +
'#EXTINF:9,\n' +
'http://media.example.com/fileSequence2.ts\n' +
'#EXT-X-ENDLIST\n');
standardXHRResponse(requests.shift()); // segment 1
player.hls.fetchKeys(player.hls.playlists.media(), 0);
strictEqual(requests.length, 1, 'no XHR for keys created since they were all downloaded');
player.currentTime(11);
ok(requests[0].aborted, 'the key XHR should be aborted');
requests.shift(); // aborted key 1
player.hls.playlists.media = oldMedia;
equal(requests.length, 1, 'requested the new segment');
standardXHRResponse(requests.shift()); // segment 2
equal(requests.length, 1, 'requested the new key');
equal(requests[0].url,
'https://example.com/' +
player.hls.playlists.media().segments[1].key.uri,
'urls should match');
});
test('retries key requests once upon failure', function() {
player.src({
src: 'https://example.com/encrypted-media.m3u8',
src: 'https://example.com/encrypted.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
var oldMedia = player.hls.playlists.media;
player.hls.playlists.media = function() {
return {
segments: [{
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=52'
},
uri: 'http://media.example.com/fileSequence52-A.ts'
}, {
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=53'
},
uri: 'http://media.example.com/fileSequence53-B.ts'
}]
};
};
player.hls.fetchKeys(player.hls.playlists.media(), 0);
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' +
'#EXTINF:2.833,\n' +
'http://media.example.com/fileSequence52-A.ts\n' +
'#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=53"\n' +
'#EXTINF:15.0,\n' +
'http://media.example.com/fileSequence53-A.ts\n');
standardXHRResponse(requests.shift()); // segment
requests[0].respond(404);
equal(requests.length, 2, 'create a new XHR for the same key');
equal(requests[1].url, requests[0].url, 'should be the same key');
requests[1].respond(404);
equal(requests.length, 3, 'create a new XHR for the same key');
equal(requests[2].url, requests[1].url, 'should be the same key');
requests[2].respond(404);
equal(requests.length, 4, 'create a new XHR for the same key');
notEqual(requests[3].url, requests[2].url, 'should be the same key');
equal(requests[3].url, player.hls.playlists.media().segments[1].key.uri);
player.hls.playlists.media = oldMedia;
equal(requests.length, 2, 'gives up after one retry');
});
test('skip segments if key requests fail more than once', function() {
var bytes = [],
tags = [{ pats: 0, bytes: 0 }];
tags = [{ pts: 0, bytes: 0 }];
videojs.Hls.SegmentParser = mockSegmentParser(tags);
window.videojs.SourceBuffer = function() {
......@@ -2057,7 +1955,7 @@ test('skip segments if key requests fail more than once', function() {
});
openMediaSource(player);
requests.pop().respond(200, null,
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' +
'#EXTINF:2.833,\n' +
......@@ -2065,32 +1963,19 @@ test('skip segments if key requests fail more than once', function() {
'#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=53"\n' +
'#EXTINF:15.0,\n' +
'http://media.example.com/fileSequence53-A.ts\n');
standardXHRResponse(requests.shift()); // segment 1
requests.shift().respond(404); // fail key
requests.shift().respond(404); // fail key, again
player.hls.playlists.trigger('loadedplaylist');
tags.length = 0;
tags.push({pts: 0, bytes: 1});
player.hls.checkBuffer_();
// respond to ts segment
standardXHRResponse(requests.pop());
// fail key
requests.pop().respond(404);
// fail key, again
requests.pop().respond(404);
standardXHRResponse(requests.shift()); // segment 2
equal(bytes.length, 1, 'bytes from the ts segments should not be added');
// key for second segment
requests[0].response = new Uint32Array([0,0,0,0]).buffer;
requests[0].respond(200, null, '');
requests.shift();
equal(bytes.length, 1, 'bytes from the ts segments should not be added');
player.hls.checkBuffer_();
tags.length = 0;
tags.push({pts: 0, bytes: 1});
// second segment
standardXHRResponse(requests.pop());
requests.shift().respond(200, null, '');
equal(bytes.length, 2, 'bytes from the second ts segment should be added');
equal(bytes[1], 1, 'the bytes from the second segment are added and not the first');
......@@ -2120,10 +2005,10 @@ test('the key is supplied to the decrypter in the correct format', function() {
return new Uint8Array([0]);
};
standardXHRResponse(requests.shift()); // segment
requests[0].response = new Uint32Array([0,1,2,3]).buffer;
requests[0].respond(200, null, '');
requests.shift();
standardXHRResponse(requests.pop());
requests.shift(); // key
equal(keys.length, 1, 'only one call to decrypt was made');
deepEqual(keys[0],
......@@ -2188,23 +2073,26 @@ test('switching playlists with an outstanding key request does not stall playbac
player.hls.playlists.media = function() {
return player.hls.playlists.master.playlists[0];
};
// don't respond to the initial key request
requests.shift();
// first segment of the original media playlist
standardXHRResponse(requests.shift());
// don't respond to the initial key request
requests.shift();
// "switch" media
player.hls.playlists.trigger('mediachange');
player.trigger('timeupdate');
player.hls.checkBuffer_();
ok(requests.length, 'made a request');
equal(requests[0].url,
'http://media.example.com/fileSequence52-B.ts',
'requested the segment');
equal(requests[1].url,
'https://priv.example.com/key.php?r=52',
'requested the segment and key');
'requested the key');
});
test('resovles relative key URLs against the playlist', function() {
test('resolves relative key URLs against the playlist', function() {
player.src({
src: 'https://example.com/media.m3u8',
type: 'application/vnd.apple.mpegurl'
......@@ -2217,6 +2105,8 @@ test('resovles relative key URLs against the playlist', function() {
'#EXT-X-KEY:METHOD=AES-128,URI="key.php?r=52"\n' +
'#EXTINF:2.833,\n' +
'http://media.example.com/fileSequence52-A.ts\n');
standardXHRResponse(requests.shift()); // segment
equal(requests[0].url, 'https://example.com/key.php?r=52', 'resolves the key URL');
});
......@@ -2243,11 +2133,11 @@ test('treats invalid keys as a key request failure', function() {
'#EXT-X-KEY:METHOD=NONE\n' +
'#EXTINF:15.0,\n' +
'http://media.example.com/fileSequence52-B.ts\n');
// segment request
standardXHRResponse(requests.shift());
// keys should be 16 bytes long
requests[0].response = new Uint8Array(1).buffer;
requests.shift().respond(200, null, '');
// segment request
standardXHRResponse(requests.shift());
equal(requests[0].url, 'https://priv.example.com/key.php?r=52', 'retries the key');
......