53322e5d by jrivera

Merge branch 'master' into development

2 parents b5e60aba 8718c2e2
...@@ -16,3 +16,10 @@ notifications: ...@@ -16,3 +16,10 @@ notifications:
16 before_script: 16 before_script:
17 - export DISPLAY=:99.0 17 - export DISPLAY=:99.0
18 - sh -e /etc/init.d/xvfb start 18 - sh -e /etc/init.d/xvfb start
19 env:
20 global:
21 - secure: dM7svnHPPu5IiUMeFWW5zg+iuWNpwt6SSDi3MmVvhSclNMRLesQoRB+7Qq5J/LiKhmjpv1/GlNVV0CTsHMRhZNwQ3fo38eEuTXv99aAflEITXwSEh/VntKViHbGFubn06EnVkJoH6MX3zJ6kbiwc2QdSQbywKzS6l6quUEpWpd0=
22 - secure: AnduYGXka5ft1x7V3SuVYqvlKLvJGhUaRNFdy4UDJr3ZVuwpQjE4TMDG8REmJIJvXfHbh4qY4N1cFSGnXkZ4bH21Xk0v9DLhsxbarKz+X2BvPgXs+Af9EQ6vLEy/5S1vMLxfT5+y+Ec5bVNGOsdUZby8Y21CRzSg6ADN9kwPGlE=
23 addons:
24 sauce_connect: true
25 firefox: latest
......
1 { 1 {
2 "name": "videojs-contrib-hls", 2 "name": "videojs-contrib-hls",
3 "version": "1.3.5", 3 "version": "1.3.9",
4 "description": "Play back HLS with video.js, even where it's not natively supported", 4 "description": "Play back HLS with video.js, even where it's not natively supported",
5 "main": "es5/videojs-contrib-hls.js", 5 "main": "es5/videojs-contrib-hls.js",
6 "engines": { 6 "engines": {
......
...@@ -3,6 +3,16 @@ ...@@ -3,6 +3,16 @@
3 */ 3 */
4 import {createTimeRange} from 'video.js'; 4 import {createTimeRange} from 'video.js';
5 5
6 let Playlist = {
7 /**
8 * The number of segments that are unsafe to start playback at in
9 * a live stream. Changing this value can cause playback stalls.
10 * See HTTP Live Streaming, "Playing the Media Playlist File"
11 * https://tools.ietf.org/html/draft-pantos-http-live-streaming-18#section-6.3.3
12 */
13 UNSAFE_LIVE_SEGMENTS: 3
14 };
15
6 const backwardDuration = function(playlist, endSequence) { 16 const backwardDuration = function(playlist, endSequence) {
7 let result = 0; 17 let result = 0;
8 let i = endSequence - playlist.mediaSequence; 18 let i = endSequence - playlist.mediaSequence;
...@@ -187,12 +197,12 @@ export const seekable = function(playlist) { ...@@ -187,12 +197,12 @@ export const seekable = function(playlist) {
187 start = intervalDuration(playlist, playlist.mediaSequence); 197 start = intervalDuration(playlist, playlist.mediaSequence);
188 end = intervalDuration(playlist, 198 end = intervalDuration(playlist,
189 playlist.mediaSequence + 199 playlist.mediaSequence +
190 Math.max(0, playlist.segments.length - 3)); 200 Math.max(0, playlist.segments.length - Playlist.UNSAFE_LIVE_SEGMENTS));
191 return createTimeRange(start, end); 201 return createTimeRange(start, end);
192 }; 202 };
193 203
204 Playlist.duration = duration;
205 Playlist.seekable = seekable;
206
194 // exports 207 // exports
195 export default { 208 export default Playlist;
196 duration,
197 seekable
198 };
......
...@@ -197,6 +197,88 @@ const keyFailed = function(key) { ...@@ -197,6 +197,88 @@ const keyFailed = function(key) {
197 return key.retries && key.retries >= 2; 197 return key.retries && key.retries >= 2;
198 }; 198 };
199 199
200 /**
201 * Returns the CSS value for the specified property on an element
202 * using `getComputedStyle`. Firefox has a long-standing issue where
203 * getComputedStyle() may return null when running in an iframe with
204 * `display: none`.
205 * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397
206 */
207 const safeGetComputedStyle = function(el, property) {
208 let result;
209
210 if (!el) {
211 return '';
212 }
213
214 result = getComputedStyle(el);
215 if (!result) {
216 return '';
217 }
218
219 return result[property];
220 };
221
222 /**
223 * Updates segment with information about its end-point in time and, optionally,
224 * the segment duration if we have enough information to determine a segment duration
225 * accurately.
226 * @param playlist {object} a media playlist object
227 * @param segmentIndex {number} the index of segment we last appended
228 * @param segmentEnd {number} the known of the segment referenced by segmentIndex
229 */
230 const updateSegmentMetadata = function(playlist, segmentIndex, segmentEnd) {
231 if (!playlist) {
232 return;
233 }
234
235 let segment = playlist.segments[segmentIndex];
236 let previousSegment = playlist.segments[segmentIndex - 1];
237
238 if (segmentEnd && segment) {
239 segment.end = segmentEnd;
240
241 // fix up segment durations based on segment end data
242 if (!previousSegment) {
243 // first segment is always has a start time of 0 making its duration
244 // equal to the segment end
245 segment.duration = segment.end;
246 } else if (previousSegment.end) {
247 segment.duration = segment.end - previousSegment.end;
248 }
249 }
250 };
251
252 /**
253 * Determines if we should call endOfStream on the media source based on the state
254 * of the buffer or if appened segment was the final segment in the playlist.
255 * @param playlist {object} a media playlist object
256 * @param mediaSource {object} the MediaSource object
257 * @param segmentIndex {number} the index of segment we last appended
258 * @param currentBuffered {object} the buffered region that currentTime resides in
259 * @return {boolean} whether the calling function should call endOfStream on the MediaSource
260 */
261 const detectEndOfStream = function(playlist, mediaSource, segmentIndex, currentBuffered) {
262 if (!playlist) {
263 return false;
264 }
265
266 let segments = playlist.segments;
267
268 // determine a few boolean values to help make the branch below easier
269 // to read
270 let appendedLastSegment = (segmentIndex === segments.length - 1);
271 let bufferedToEnd = (currentBuffered.length &&
272 segments[segments.length - 1].end <= currentBuffered.end(0));
273
274 // if we've buffered to the end of the video, we need to call endOfStream
275 // so that MediaSources can trigger the `ended` event when it runs out of
276 // buffered data instead of waiting for me
277 return playlist.endList &&
278 mediaSource.readyState === 'open' &&
279 (appendedLastSegment || bufferedToEnd);
280 };
281
200 const parseCodecs = function(codecs) { 282 const parseCodecs = function(codecs) {
201 let result = { 283 let result = {
202 codecCount: 0, 284 codecCount: 0,
...@@ -592,10 +674,15 @@ export default class HlsHandler extends Component { ...@@ -592,10 +674,15 @@ export default class HlsHandler extends Component {
592 duration() { 674 duration() {
593 let playlists = this.playlists; 675 let playlists = this.playlists;
594 676
595 if (playlists) { 677 if (!playlists) {
596 return Hls.Playlist.duration(playlists.media()); 678 return 0;
679 }
680
681 if (this.mediaSource) {
682 return this.mediaSource.duration;
597 } 683 }
598 return 0; 684
685 return Hls.Playlist.duration(playlists.media());
599 } 686 }
600 687
601 seekable() { 688 seekable() {
...@@ -635,6 +722,7 @@ export default class HlsHandler extends Component { ...@@ -635,6 +722,7 @@ export default class HlsHandler extends Component {
635 updateDuration(playlist) { 722 updateDuration(playlist) {
636 let oldDuration = this.mediaSource.duration; 723 let oldDuration = this.mediaSource.duration;
637 let newDuration = Hls.Playlist.duration(playlist); 724 let newDuration = Hls.Playlist.duration(playlist);
725 let buffered = this.tech_.buffered();
638 let setDuration = () => { 726 let setDuration = () => {
639 this.mediaSource.duration = newDuration; 727 this.mediaSource.duration = newDuration;
640 this.tech_.trigger('durationchange'); 728 this.tech_.trigger('durationchange');
...@@ -642,6 +730,10 @@ export default class HlsHandler extends Component { ...@@ -642,6 +730,10 @@ export default class HlsHandler extends Component {
642 this.mediaSource.removeEventListener('sourceopen', setDuration); 730 this.mediaSource.removeEventListener('sourceopen', setDuration);
643 }; 731 };
644 732
733 if (buffered.length > 0) {
734 newDuration = Math.max(newDuration, buffered.end(buffered.length - 1));
735 }
736
645 // if the duration has changed, invalidate the cached value 737 // if the duration has changed, invalidate the cached value
646 if (oldDuration !== newDuration) { 738 if (oldDuration !== newDuration) {
647 // update the duration 739 // update the duration
...@@ -767,8 +859,8 @@ export default class HlsHandler extends Component { ...@@ -767,8 +859,8 @@ export default class HlsHandler extends Component {
767 // (this could be the lowest bitrate rendition as we go through all of them above) 859 // (this could be the lowest bitrate rendition as we go through all of them above)
768 variant = null; 860 variant = null;
769 861
770 width = parseInt(getComputedStyle(this.tech_.el()).width, 10); 862 width = parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10);
771 height = parseInt(getComputedStyle(this.tech_.el()).height, 10); 863 height = parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10);
772 864
773 // iterate through the bandwidth-filtered playlists and find 865 // iterate through the bandwidth-filtered playlists and find
774 // best rendition by player dimension 866 // best rendition by player dimension
...@@ -1094,6 +1186,7 @@ export default class HlsHandler extends Component { ...@@ -1094,6 +1186,7 @@ export default class HlsHandler extends Component {
1094 let segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex]; 1186 let segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
1095 let removeToTime = 0; 1187 let removeToTime = 0;
1096 let seekable = this.seekable(); 1188 let seekable = this.seekable();
1189 let currentTime = this.tech_.currentTime();
1097 1190
1098 // Chrome has a hard limit of 150mb of 1191 // Chrome has a hard limit of 150mb of
1099 // buffer and a very conservative "garbage collector" 1192 // buffer and a very conservative "garbage collector"
...@@ -1103,10 +1196,10 @@ export default class HlsHandler extends Component { ...@@ -1103,10 +1196,10 @@ export default class HlsHandler extends Component {
1103 if (this.sourceBuffer && !this.sourceBuffer.updating) { 1196 if (this.sourceBuffer && !this.sourceBuffer.updating) {
1104 // If we have a seekable range use that as the limit for what can be removed safely 1197 // If we have a seekable range use that as the limit for what can be removed safely
1105 // otherwise remove anything older than 1 minute before the current play head 1198 // otherwise remove anything older than 1 minute before the current play head
1106 if (seekable.length && seekable.start(0) > 0) { 1199 if (seekable.length && seekable.start(0) > 0 && seekable.start(0) < currentTime) {
1107 removeToTime = seekable.start(0); 1200 removeToTime = seekable.start(0);
1108 } else { 1201 } else {
1109 removeToTime = this.tech_.currentTime() - 60; 1202 removeToTime = currentTime - 60;
1110 } 1203 }
1111 1204
1112 if (removeToTime > 0) { 1205 if (removeToTime > 0) {
...@@ -1260,37 +1353,43 @@ export default class HlsHandler extends Component { ...@@ -1260,37 +1353,43 @@ export default class HlsHandler extends Component {
1260 1353
1261 updateEndHandler_() { 1354 updateEndHandler_() {
1262 let segmentInfo = this.pendingSegment_; 1355 let segmentInfo = this.pendingSegment_;
1263 let segment;
1264 let segments;
1265 let playlist; 1356 let playlist;
1266 let currentMediaIndex; 1357 let currentMediaIndex;
1267 let currentBuffered; 1358 let currentBuffered;
1268 let seekable; 1359 let seekable;
1269 let timelineUpdate; 1360 let timelineUpdate;
1270 1361 let isEndOfStream;
1271 this.pendingSegment_ = null;
1272 1362
1273 // stop here if the update errored or was aborted 1363 // stop here if the update errored or was aborted
1274 if (!segmentInfo) { 1364 if (!segmentInfo) {
1365 this.pendingSegment_ = null;
1366 return;
1367 }
1368
1369 // In Firefox, the updateend event is triggered for both removing from the buffer and
1370 // adding to the buffer. To prevent this code from executing on removals, we wait for
1371 // segmentInfo to have a filled in buffered value before we continue processing.
1372 if (!segmentInfo.buffered) {
1275 return; 1373 return;
1276 } 1374 }
1277 1375
1278 playlist = this.playlists.media(); 1376 this.pendingSegment_ = null;
1279 segments = playlist.segments; 1377
1378 playlist = segmentInfo.playlist;
1280 currentMediaIndex = segmentInfo.mediaIndex + 1379 currentMediaIndex = segmentInfo.mediaIndex +
1281 (segmentInfo.mediaSequence - playlist.mediaSequence); 1380 (segmentInfo.mediaSequence - playlist.mediaSequence);
1282 currentBuffered = this.findBufferedRange_(); 1381 currentBuffered = this.findBufferedRange_();
1382 isEndOfStream = detectEndOfStream(playlist, this.mediaSource, currentMediaIndex, currentBuffered);
1283 1383
1284 // if we switched renditions don't try to add segment timeline 1384 // if we switched renditions don't try to add segment timeline
1285 // information to the playlist 1385 // information to the playlist
1286 if (segmentInfo.playlist.uri !== this.playlists.media().uri) { 1386 if (segmentInfo.playlist.uri !== this.playlists.media().uri) {
1387 if (isEndOfStream) {
1388 return this.mediaSource.endOfStream();
1389 }
1287 return this.fillBuffer(); 1390 return this.fillBuffer();
1288 } 1391 }
1289 1392
1290 // annotate the segment with any start and end time information
1291 // added by the media processing
1292 segment = playlist.segments[currentMediaIndex];
1293
1294 // when seeking to the beginning of the seekable range, it's 1393 // when seeking to the beginning of the seekable range, it's
1295 // possible that imprecise timing information may cause the seek to 1394 // possible that imprecise timing information may cause the seek to
1296 // end up earlier than the start of the range 1395 // end up earlier than the start of the range
...@@ -1313,17 +1412,13 @@ export default class HlsHandler extends Component { ...@@ -1313,17 +1412,13 @@ export default class HlsHandler extends Component {
1313 timelineUpdate = Hls.findSoleUncommonTimeRangesEnd_(segmentInfo.buffered, 1412 timelineUpdate = Hls.findSoleUncommonTimeRangesEnd_(segmentInfo.buffered,
1314 this.tech_.buffered()); 1413 this.tech_.buffered());
1315 1414
1316 if (timelineUpdate && segment) { 1415 // Update segment meta-data (duration and end-point) based on timeline
1317 segment.end = timelineUpdate; 1416 updateSegmentMetadata(playlist, currentMediaIndex, timelineUpdate);
1318 }
1319 1417
1320 // if we've buffered to the end of the video, let the MediaSource know 1418 // If we decide to signal the end of stream, then we can return instead
1321 if (this.playlists.media().endList && 1419 // of trying to fetch more segments
1322 currentBuffered.length && 1420 if (isEndOfStream) {
1323 segments[segments.length - 1].end <= currentBuffered.end(0) && 1421 return this.mediaSource.endOfStream();
1324 this.mediaSource.readyState === 'open') {
1325 this.mediaSource.endOfStream();
1326 return;
1327 } 1422 }
1328 1423
1329 if (timelineUpdate !== null || 1424 if (timelineUpdate !== null ||
......
...@@ -436,7 +436,7 @@ QUnit.test('duration is set when the source opens after the playlist is loaded', ...@@ -436,7 +436,7 @@ QUnit.test('duration is set when the source opens after the playlist is loaded',
436 'set the duration'); 436 'set the duration');
437 }); 437 });
438 438
439 QUnit.test('calls `remove` on sourceBuffer to when loading a live segment', function() { 439 QUnit.test('calls `remove` based on seekable when loading a live segment', function() {
440 let removes = []; 440 let removes = [];
441 let seekable = videojs.createTimeRanges([[60, 120]]); 441 let seekable = videojs.createTimeRanges([[60, 120]]);
442 442
...@@ -487,7 +487,59 @@ QUnit.test('calls `remove` on sourceBuffer to when loading a live segment', func ...@@ -487,7 +487,59 @@ QUnit.test('calls `remove` on sourceBuffer to when loading a live segment', func
487 'remove called with the right range'); 487 'remove called with the right range');
488 }); 488 });
489 489
490 QUnit.test('calls `remove` on sourceBuffer to when loading a vod segment', function() { 490 QUnit.test('calls `remove` based on currentTime when loading a live segment ' +
491 'if seekable start is after currentTime', function() {
492 let removes = [];
493 let seekable = videojs.createTimeRanges([[0, 80]]);
494
495 this.player.src({
496 src: 'liveStart30sBefore.m3u8',
497 type: 'application/vnd.apple.mpegurl'
498 });
499 openMediaSource(this.player, this.clock);
500 this.player.tech_.hls.seekable = function() {
501 return seekable;
502 };
503
504 openMediaSource(this.player, this.clock);
505 this.player.tech_.hls.mediaSource.addSourceBuffer = function() {
506 return new (videojs.extend(videojs.EventTarget, {
507 constructor() {},
508 abort() {},
509 buffered: videojs.createTimeRange(),
510 appendBuffer() {},
511 remove(start, end) {
512 removes.push([start, end]);
513 }
514 }))();
515 };
516 this.player.tech_.hls.bandwidth = 20e10;
517 this.player.tech_.triggerReady();
518 standardXHRResponse(this.requests[0]);
519 this.player.tech_.hls.playlists.trigger('loadedmetadata');
520 this.player.tech_.trigger('canplay');
521
522 this.player.tech_.paused = function() {
523 return false;
524 };
525
526 this.player.tech_.readyState = function() {
527 return 1;
528 };
529
530 this.player.tech_.trigger('play');
531 this.clock.tick(1);
532 // Change seekable so that it starts *after* the currentTime which was set
533 // based on the previous seekable range (the end of 80)
534 seekable = videojs.createTimeRanges([[100, 120]]);
535 standardXHRResponse(this.requests[1]);
536
537 QUnit.strictEqual(this.requests[0].url, 'liveStart30sBefore.m3u8', 'master playlist requested');
538 QUnit.equal(removes.length, 1, 'remove called');
539 QUnit.deepEqual(removes[0], [0, 80 - 60], 'remove called with the right range');
540 });
541
542 QUnit.test('calls `remove` based on currentTime when loading a vod segment', function() {
491 let removes = []; 543 let removes = [];
492 544
493 this.player.src({ 545 this.player.src({
...@@ -2268,6 +2320,81 @@ QUnit.test('tracks segment end times as they are buffered', function() { ...@@ -2268,6 +2320,81 @@ QUnit.test('tracks segment end times as they are buffered', function() {
2268 QUnit.equal(this.player.tech_.hls.mediaSource.duration, 10 + 9.5, 'updated duration'); 2320 QUnit.equal(this.player.tech_.hls.mediaSource.duration, 10 + 9.5, 'updated duration');
2269 }); 2321 });
2270 2322
2323 QUnit.test('updates first segment duration as it is buffered', function() {
2324 let bufferEnd = 0;
2325
2326 this.player.src({
2327 src: 'media.m3u8',
2328 type: 'application/x-mpegURL'
2329 });
2330 openMediaSource(this.player, this.clock);
2331
2332 // as new segments are downloaded, the buffer end is updated
2333 this.player.tech_.buffered = function() {
2334 return videojs.createTimeRange(0, bufferEnd);
2335 };
2336 this.requests.shift().respond(200, null,
2337 '#EXTM3U\n' +
2338 '#EXTINF:10,\n' +
2339 '0.ts\n' +
2340 '#EXTINF:10,\n' +
2341 '1.ts\n' +
2342 '#EXT-X-ENDLIST\n');
2343
2344 // 0.ts is shorter than advertised
2345 standardXHRResponse(this.requests.shift());
2346 QUnit.equal(this.player.tech_.hls.mediaSource.duration, 20, 'original duration is from the m3u8');
2347 QUnit.equal(this.player.tech_.hls.playlists.media().segments[0].duration, 10,
2348 'segment duration initially based on playlist');
2349
2350 bufferEnd = 9.5;
2351 this.player.tech_.hls.sourceBuffer.trigger('update');
2352 this.player.tech_.hls.sourceBuffer.trigger('updateend');
2353 QUnit.equal(this.player.tech_.hls.playlists.media().segments[0].duration, 9.5,
2354 'updated segment duration');
2355 });
2356
2357 QUnit.test('updates segment durations as they are buffered', function() {
2358 let bufferEnd = 0;
2359
2360 this.player.src({
2361 src: 'media.m3u8',
2362 type: 'application/x-mpegURL'
2363 });
2364 openMediaSource(this.player, this.clock);
2365
2366 // as new segments are downloaded, the buffer end is updated
2367 this.player.tech_.buffered = function() {
2368 return videojs.createTimeRange(0, bufferEnd);
2369 };
2370 this.requests.shift().respond(200, null,
2371 '#EXTM3U\n' +
2372 '#EXTINF:10,\n' +
2373 '0.ts\n' +
2374 '#EXTINF:10,\n' +
2375 '1.ts\n' +
2376 '#EXT-X-ENDLIST\n');
2377
2378 // 0.ts is shorter than advertised
2379 standardXHRResponse(this.requests.shift());
2380 QUnit.equal(this.player.tech_.hls.mediaSource.duration, 20, 'original duration is from the m3u8');
2381
2382 QUnit.equal(this.player.tech_.hls.playlists.media().segments[1].duration, 10,
2383 'segment duration initially based on playlist');
2384
2385 bufferEnd = 9.5;
2386 this.player.tech_.hls.sourceBuffer.trigger('update');
2387 this.player.tech_.hls.sourceBuffer.trigger('updateend');
2388 this.clock.tick(1);
2389 standardXHRResponse(this.requests.shift());
2390 bufferEnd = 19;
2391 this.player.tech_.hls.sourceBuffer.trigger('update');
2392 this.player.tech_.hls.sourceBuffer.trigger('updateend');
2393
2394 QUnit.equal(this.player.tech_.hls.playlists.media().segments[1].duration, 9.5,
2395 'updated segment duration');
2396 });
2397
2271 QUnit.skip('seeking does not fail when targeted between segments', function() { 2398 QUnit.skip('seeking does not fail when targeted between segments', function() {
2272 let currentTime; 2399 let currentTime;
2273 let segmentUrl; 2400 let segmentUrl;
...@@ -2576,7 +2703,7 @@ QUnit.test('can be disposed before finishing initialization', function() { ...@@ -2576,7 +2703,7 @@ QUnit.test('can be disposed before finishing initialization', function() {
2576 } 2703 }
2577 }); 2704 });
2578 2705
2579 QUnit.test('calls ended() on the media source at the end of a playlist', function() { 2706 QUnit.test('calls endOfStream on the media source after appending the last segment', function() {
2580 let endOfStreams = 0; 2707 let endOfStreams = 0;
2581 let buffered = [[]]; 2708 let buffered = [[]];
2582 2709
...@@ -2591,11 +2718,15 @@ QUnit.test('calls ended() on the media source at the end of a playlist', functio ...@@ -2591,11 +2718,15 @@ QUnit.test('calls ended() on the media source at the end of a playlist', functio
2591 this.player.tech_.hls.mediaSource.endOfStream = function() { 2718 this.player.tech_.hls.mediaSource.endOfStream = function() {
2592 endOfStreams++; 2719 endOfStreams++;
2593 }; 2720 };
2721 this.player.currentTime(20);
2722 this.clock.tick(1);
2594 // playlist response 2723 // playlist response
2595 this.requests.shift().respond(200, null, 2724 this.requests.shift().respond(200, null,
2596 '#EXTM3U\n' + 2725 '#EXTM3U\n' +
2597 '#EXTINF:10,\n' + 2726 '#EXTINF:10,\n' +
2598 '0.ts\n' + 2727 '0.ts\n' +
2728 '#EXTINF:10,\n' +
2729 '1.ts\n' +
2599 '#EXT-X-ENDLIST\n'); 2730 '#EXT-X-ENDLIST\n');
2600 // segment response 2731 // segment response
2601 this.requests[0].response = new ArrayBuffer(17); 2732 this.requests[0].response = new ArrayBuffer(17);
...@@ -2604,7 +2735,52 @@ QUnit.test('calls ended() on the media source at the end of a playlist', functio ...@@ -2604,7 +2735,52 @@ QUnit.test('calls ended() on the media source at the end of a playlist', functio
2604 2735
2605 buffered = [[0, 10]]; 2736 buffered = [[0, 10]];
2606 this.player.tech_.hls.sourceBuffer.trigger('updateend'); 2737 this.player.tech_.hls.sourceBuffer.trigger('updateend');
2607 QUnit.strictEqual(endOfStreams, 1, 'ended media source'); 2738 QUnit.strictEqual(endOfStreams, 1, 'called endOfStream on the media source');
2739 });
2740
2741 QUnit.test('calls endOfStream on the media source when the current buffer ends at duration', function() {
2742 let endOfStreams = 0;
2743 let buffered = [[]];
2744
2745 this.player.src({
2746 src: 'http://example.com/media.m3u8',
2747 type: 'application/vnd.apple.mpegurl'
2748 });
2749 openMediaSource(this.player, this.clock);
2750 this.player.tech_.buffered = function() {
2751 return videojs.createTimeRanges(buffered);
2752 };
2753 this.player.tech_.hls.mediaSource.endOfStream = function() {
2754 endOfStreams++;
2755 };
2756 this.player.currentTime(19);
2757 this.clock.tick(1);
2758 // playlist response
2759 this.requests.shift().respond(200, null,
2760 '#EXTM3U\n' +
2761 '#EXTINF:10,\n' +
2762 '0.ts\n' +
2763 '#EXTINF:10,\n' +
2764 '1.ts\n' +
2765 '#EXT-X-ENDLIST\n');
2766 // segment response
2767 this.requests[0].response = new ArrayBuffer(17);
2768 this.requests.shift().respond(200, null, '');
2769 QUnit.strictEqual(endOfStreams, 0, 'waits for the buffer update to finish');
2770
2771 buffered = [[10, 20]];
2772 this.player.tech_.hls.sourceBuffer.trigger('updateend');
2773
2774 this.player.currentTime(5);
2775 this.clock.tick(1);
2776 // segment response
2777 this.requests[0].response = new ArrayBuffer(17);
2778 this.requests.shift().respond(200, null, '');
2779
2780 buffered = [[0, 20]];
2781 this.player.tech_.hls.sourceBuffer.trigger('updateend');
2782
2783 QUnit.strictEqual(endOfStreams, 2, 'called endOfStream on the media source twice');
2608 }); 2784 });
2609 2785
2610 QUnit.test('calling play() at the end of a video replays', function() { 2786 QUnit.test('calling play() at the end of a video replays', function() {
...@@ -3086,6 +3262,68 @@ QUnit.test('does not download segments if preload option set to none', function( ...@@ -3086,6 +3262,68 @@ QUnit.test('does not download segments if preload option set to none', function(
3086 QUnit.equal(this.requests.length, 0, 'did not download any segments'); 3262 QUnit.equal(this.requests.length, 0, 'did not download any segments');
3087 }); 3263 });
3088 3264
3265 QUnit.test('does not process update end until buffered value has been set', function() {
3266 let drainBufferCallCount = 0;
3267 let origDrainBuffer;
3268
3269 this.player.src({
3270 src: 'master.m3u8',
3271 type: 'application/vnd.apple.mpegurl'
3272 });
3273
3274 openMediaSource(this.player, this.clock);
3275 origDrainBuffer = this.player.tech_.hls.drainBuffer;
3276 this.player.tech_.hls.drainBuffer = function() {
3277 drainBufferCallCount++;
3278 };
3279
3280 // master
3281 standardXHRResponse(this.requests.shift());
3282 // media
3283 standardXHRResponse(this.requests.shift());
3284
3285 QUnit.equal(drainBufferCallCount, 0, 'drainBuffer not called yet');
3286
3287 // segment
3288 standardXHRResponse(this.requests.shift());
3289
3290 QUnit.ok(this.player.tech_.hls.pendingSegment_, 'pending segment exists');
3291 QUnit.equal(drainBufferCallCount, 1, 'drainBuffer called');
3292
3293 this.player.tech_.hls.sourceBuffer.trigger('updateend');
3294 QUnit.ok(this.player.tech_.hls.pendingSegment_, 'pending segment exists');
3295
3296 this.player.tech_.hls.drainBuffer = origDrainBuffer;
3297 this.player.tech_.hls.drainBuffer();
3298 QUnit.ok(this.player.tech_.hls.pendingSegment_, 'pending segment exists');
3299
3300 this.player.tech_.hls.sourceBuffer.trigger('updateend');
3301 QUnit.ok(!this.player.tech_.hls.pendingSegment_, 'pending segment cleared out');
3302 });
3303
3304 // workaround https://bugzilla.mozilla.org/show_bug.cgi?id=548397
3305 QUnit.test('selectPlaylist does not fail if getComputedStyle returns null', function() {
3306 let oldGetComputedStyle = window.getComputedStyle;
3307
3308 window.getComputedStyle = function() {
3309 return null;
3310 };
3311
3312 this.player.src({
3313 src: 'master.m3u8',
3314 type: 'application/vnd.apple.mpegurl'
3315 });
3316 openMediaSource(this.player, this.clock);
3317 // master
3318 standardXHRResponse(this.requests.shift());
3319 // media
3320 standardXHRResponse(this.requests.shift());
3321
3322 this.player.tech_.hls.selectPlaylist();
3323 QUnit.ok(true, 'should not throw');
3324 window.getComputedStyle = oldGetComputedStyle;
3325 });
3326
3089 QUnit.module('Buffer Inspection'); 3327 QUnit.module('Buffer Inspection');
3090 QUnit.test('detects time range end-point changed by updates', function() { 3328 QUnit.test('detects time range end-point changed by updates', function() {
3091 let edge; 3329 let edge;
......