619fa74f by David LaPalomento

Fix discontinuities

Never match media indices on playlist reload based on segment URI because some streams may re-use the same segment in multiple positions. Change fetchKeys() so that it operates on buffered segments instead of directly modifying a version of the playlist. Before that change, live playlists with low segment durations could stall because the key would be applied to the previous version of the live playlist and segments would get blocked up forever in the queue waiting for their key to arrive. Use a much less destructive mechanism for playing across discontinuities. vjs_discontinuity() on the SWF allows us to signal a timestamp discontinuity without flushing the playback buffer. That means we don't have to wait until the buffer is empty when a discontinuity is encountered and feeding data to the SWF doesn't have to block either. Update tests to reflect new key-segment request ordering.
1 parent 021896e3
......@@ -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');
......