4b0e7317 by David LaPalomento

Merge pull request #178 from videojs/pre-segment-switch

Pre segment switch
2 parents de317697 014577a9
...@@ -29,12 +29,13 @@ ...@@ -29,12 +29,13 @@
29 "karma-firefox-launcher": "~0.1.3", 29 "karma-firefox-launcher": "~0.1.3",
30 "karma-ie-launcher": "~0.1.1", 30 "karma-ie-launcher": "~0.1.1",
31 "karma-opera-launcher": "~0.1.0", 31 "karma-opera-launcher": "~0.1.0",
32 "karma-phantomjs-launcher": "~0.1.1", 32 "karma-phantomjs-launcher": "^0.1.4",
33 "karma-qunit": "~0.1.1", 33 "karma-qunit": "~0.1.1",
34 "karma-safari-launcher": "~0.1.1", 34 "karma-safari-launcher": "~0.1.1",
35 "karma-sauce-launcher": "~0.1.8", 35 "karma-sauce-launcher": "~0.1.8",
36 "qunitjs": "^1.15.0",
36 "sinon": "1.10.2", 37 "sinon": "1.10.2",
37 "video.js": "^4.7.2" 38 "video.js": "^4.9.0"
38 }, 39 },
39 "dependencies": { 40 "dependencies": {
40 "pkcs7": "^0.2.2", 41 "pkcs7": "^0.2.2",
......
...@@ -58,6 +58,8 @@ ...@@ -58,6 +58,8 @@
58 haveMetadata = function(error, xhr, url) { 58 haveMetadata = function(error, xhr, url) {
59 var parser, refreshDelay, update; 59 var parser, refreshDelay, update;
60 60
61 loader.setBandwidth(request || xhr);
62
61 // any in-flight request is now finished 63 // any in-flight request is now finished
62 request = null; 64 request = null;
63 65
...@@ -200,6 +202,10 @@ ...@@ -200,6 +202,10 @@
200 }); 202 });
201 }; 203 };
202 204
205 loader.setBandwidth = function(xhr) {
206 loader.bandwidth = xhr.bandwidth;
207 };
208
203 // live playlist staleness timeout 209 // live playlist staleness timeout
204 loader.on('mediaupdatetimeout', function() { 210 loader.on('mediaupdatetimeout', function() {
205 if (loader.state !== 'HAVE_METADATA') { 211 if (loader.state !== 'HAVE_METADATA') {
......
...@@ -101,19 +101,58 @@ videojs.Hls.prototype.handleSourceOpen = function() { ...@@ -101,19 +101,58 @@ videojs.Hls.prototype.handleSourceOpen = function() {
101 sourceBuffer.appendBuffer(this.segmentParser_.getFlvHeader()); 101 sourceBuffer.appendBuffer(this.segmentParser_.getFlvHeader());
102 102
103 this.mediaIndex = 0; 103 this.mediaIndex = 0;
104
105 if (this.playlists) {
106 this.playlists.dispose();
107 }
108
104 this.playlists = new videojs.Hls.PlaylistLoader(this.src_, settings.withCredentials); 109 this.playlists = new videojs.Hls.PlaylistLoader(this.src_, settings.withCredentials);
105 110
106 this.playlists.on('loadedmetadata', videojs.bind(this, function() { 111 this.playlists.on('loadedmetadata', videojs.bind(this, function() {
112 var selectedPlaylist, loaderHandler, newBitrate, segmentDuration,
113 segmentDlTime, setupEvents, threshold;
114
115 setupEvents = function() {
116 this.fillBuffer();
117
118 // periodically check if new data needs to be downloaded or
119 // buffered data should be appended to the source buffer
120 player.on('timeupdate', videojs.bind(this, this.fillBuffer));
121 player.on('timeupdate', videojs.bind(this, this.drainBuffer));
122 player.on('waiting', videojs.bind(this, this.drainBuffer));
123
124 player.trigger('loadedmetadata');
125 };
126
107 oldMediaPlaylist = this.playlists.media(); 127 oldMediaPlaylist = this.playlists.media();
128 this.bandwidth = this.playlists.bandwidth;
129 selectedPlaylist = this.selectPlaylist();
130 newBitrate = selectedPlaylist.attributes &&
131 selectedPlaylist.attributes.BANDWIDTH;
132 segmentDuration = oldMediaPlaylist.segments &&
133 oldMediaPlaylist.segments[this.mediaIndex].duration ||
134 oldMediaPlaylist.targetDuration;
135
136 segmentDlTime = (segmentDuration * newBitrate) / this.bandwidth;
137
138 if (!segmentDlTime) {
139 segmentDlTime = Infinity;
140 }
108 141
109 // periodically check if new data needs to be downloaded or 142 // this threshold is to account for having a high latency on the manifest
110 // buffered data should be appended to the source buffer 143 // request which is a somewhat small file.
111 this.fillBuffer(); 144 threshold = 10;
112 player.on('timeupdate', videojs.bind(this, this.fillBuffer));
113 player.on('timeupdate', videojs.bind(this, this.drainBuffer));
114 player.on('waiting', videojs.bind(this, this.drainBuffer));
115 145
116 player.trigger('loadedmetadata'); 146 if (segmentDlTime <= threshold) {
147 this.playlists.media(selectedPlaylist);
148 loaderHandler = videojs.bind(this, function() {
149 setupEvents.call(this);
150 this.playlists.off('loadedplaylist', loaderHandler);
151 });
152 this.playlists.on('loadedplaylist', loaderHandler);
153 } else {
154 setupEvents.call(this);
155 }
117 })); 156 }));
118 157
119 this.playlists.on('error', videojs.bind(this, function() { 158 this.playlists.on('error', videojs.bind(this, function() {
...@@ -409,12 +448,27 @@ videojs.Hls.prototype.fillBuffer = function(offset) { ...@@ -409,12 +448,27 @@ videojs.Hls.prototype.fillBuffer = function(offset) {
409 this.loadSegment(segmentUri, offset); 448 this.loadSegment(segmentUri, offset);
410 }; 449 };
411 450
451 /*
452 * Sets `bandwidth`, `segmentXhrTime`, and appends to the `bytesReceived.
453 * Expects an object with:
454 * * `roundTripTime` - the round trip time for the request we're setting the time for
455 * * `bandwidth` - the bandwidth we want to set
456 * * `bytesReceived` - amount of bytes downloaded
457 * `bandwidth` is the only required property.
458 */
459 videojs.Hls.prototype.setBandwidth = function(xhr) {
460 var tech = this;
461 // calculate the download bandwidth
462 tech.segmentXhrTime = xhr.roundTripTime;
463 tech.bandwidth = xhr.bandwidth;
464 tech.bytesReceived += xhr.bytesReceived || 0;
465 };
466
412 videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { 467 videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
413 var 468 var
414 tech = this, 469 tech = this,
415 player = this.player(), 470 player = this.player(),
416 settings = player.options().hls || {}, 471 settings = player.options().hls || {};
417 startTime = +new Date();
418 472
419 // request the next segment 473 // request the next segment
420 this.segmentXhr_ = videojs.Hls.xhr({ 474 this.segmentXhr_ = videojs.Hls.xhr({
...@@ -448,10 +502,7 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { ...@@ -448,10 +502,7 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
448 return; 502 return;
449 } 503 }
450 504
451 // calculate the download bandwidth 505 tech.setBandwidth(this);
452 tech.segmentXhrTime = (+new Date()) - startTime;
453 tech.bandwidth = (this.response.byteLength / tech.segmentXhrTime) * 8 * 1000;
454 tech.bytesReceived += this.response.byteLength;
455 506
456 // package up all the work to append the segment 507 // package up all the work to append the segment
457 // if the segment is the start of a timestamp discontinuity, 508 // if the segment is the start of a timestamp discontinuity,
......
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
34 request = new window.XMLHttpRequest(); 34 request = new window.XMLHttpRequest();
35 request.open(options.method, url); 35 request.open(options.method, url);
36 request.url = url; 36 request.url = url;
37 request.requestTime = new Date().getTime();
37 38
38 if (options.responseType) { 39 if (options.responseType) {
39 request.responseType = options.responseType; 40 request.responseType = options.responseType;
...@@ -69,6 +70,13 @@ ...@@ -69,6 +70,13 @@
69 return callback.call(this, true, url); 70 return callback.call(this, true, url);
70 } 71 }
71 72
73 if (this.response) {
74 this.responseTime = new Date().getTime();
75 this.roundTripTime = this.responseTime - this.requestTime;
76 this.bytesReceived = this.response.byteLength || this.response.length;
77 this.bandwidth = Math.floor((this.bytesReceived / this.roundTripTime) * 8 * 1000);
78 }
79
72 return callback.call(this, false, url); 80 return callback.call(this, false, url);
73 }; 81 };
74 request.send(null); 82 request.send(null);
......
...@@ -5,21 +5,21 @@ ...@@ -5,21 +5,21 @@
5 "segments": [ 5 "segments": [
6 { 6 {
7 "duration": 10, 7 "duration": 10,
8 "uri": "00001.ts" 8 "uri": "media-00001.ts"
9 }, 9 },
10 { 10 {
11 "duration": 10, 11 "duration": 10,
12 "uri": "00002.ts" 12 "uri": "media-00002.ts"
13 }, 13 },
14 { 14 {
15 "duration": 10, 15 "duration": 10,
16 "uri": "00003.ts" 16 "uri": "media-00003.ts"
17 }, 17 },
18 { 18 {
19 "duration": 10, 19 "duration": 10,
20 "uri": "00004.ts" 20 "uri": "media-00004.ts"
21 } 21 }
22 ], 22 ],
23 "targetDuration": 10, 23 "targetDuration": 10,
24 "endList": true 24 "endList": true
25 }
...\ No newline at end of file ...\ No newline at end of file
25 }
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
2 #EXT-X-PLAYLIST-TYPE:VOD 2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-TARGETDURATION:10 3 #EXT-X-TARGETDURATION:10
4 #EXTINF:10, 4 #EXTINF:10,
5 00001.ts 5 media-00001.ts
6 #EXTINF:10, 6 #EXTINF:10,
7 00002.ts 7 media-00002.ts
8 #EXTINF:10, 8 #EXTINF:10,
9 00003.ts 9 media-00003.ts
10 #EXTINF:10, 10 #EXTINF:10,
11 00004.ts 11 media-00004.ts
12 #ZEN-TOTAL-DURATION:57.9911 12 #ZEN-TOTAL-DURATION:57.9911
13 #EXT-X-ENDLIST 13 #EXT-X-ENDLIST
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
2 #EXT-X-PLAYLIST-TYPE:VOD 2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-TARGETDURATION:10 3 #EXT-X-TARGETDURATION:10
4 #EXTINF:10, 4 #EXTINF:10,
5 00001.ts 5 media1-00001.ts
6 #EXTINF:10, 6 #EXTINF:10,
7 00002.ts 7 media1-00002.ts
8 #EXTINF:10, 8 #EXTINF:10,
9 00003.ts 9 media1-00003.ts
10 #EXTINF:10, 10 #EXTINF:10,
11 00004.ts 11 media1-00004.ts
12 #ZEN-TOTAL-DURATION:57.9911 12 #ZEN-TOTAL-DURATION:57.9911
13 #EXT-X-ENDLIST 13 #EXT-X-ENDLIST
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
2 #EXT-X-PLAYLIST-TYPE:VOD 2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-TARGETDURATION:10 3 #EXT-X-TARGETDURATION:10
4 #EXTINF:10, 4 #EXTINF:10,
5 00001.ts 5 media3-00001.ts
6 #EXTINF:10, 6 #EXTINF:10,
7 00002.ts 7 media3-00002.ts
8 #EXTINF:10, 8 #EXTINF:10,
9 00003.ts 9 media3-00003.ts
10 #EXTINF:10, 10 #EXTINF:10,
11 00004.ts 11 media3-00004.ts
12 #ZEN-TOTAL-DURATION:57.9911 12 #ZEN-TOTAL-DURATION:57.9911
13 #EXT-X-ENDLIST 13 #EXT-X-ENDLIST
......
...@@ -260,7 +260,7 @@ test('starts downloading a segment on loadedmetadata', function() { ...@@ -260,7 +260,7 @@ test('starts downloading a segment on loadedmetadata', function() {
260 strictEqual(requests[1].url, 260 strictEqual(requests[1].url,
261 window.location.origin + 261 window.location.origin +
262 window.location.pathname.split('/').slice(0, -1).join('/') + 262 window.location.pathname.split('/').slice(0, -1).join('/') +
263 '/manifest/00001.ts', 263 '/manifest/media-00001.ts',
264 'the first segment is requested'); 264 'the first segment is requested');
265 }); 265 });
266 266
...@@ -349,8 +349,41 @@ test('downloads media playlists after loading the master', function() { ...@@ -349,8 +349,41 @@ test('downloads media playlists after loading the master', function() {
349 openMediaSource(player); 349 openMediaSource(player);
350 350
351 standardXHRResponse(requests[0]); 351 standardXHRResponse(requests[0]);
352
353 // set bandwidth to a high number, so, we don't switch;
354 player.hls.bandwidth = 500000;
355 standardXHRResponse(requests[1]);
356 standardXHRResponse(requests[2]);
357
358 strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested');
359 strictEqual(requests[1].url,
360 window.location.origin +
361 window.location.pathname.split('/').slice(0, -1).join('/') +
362 '/manifest/media.m3u8',
363 'media playlist requested');
364 strictEqual(requests[2].url,
365 window.location.origin +
366 window.location.pathname.split('/').slice(0, -1).join('/') +
367 '/manifest/media-00001.ts',
368 'first segment requested');
369 });
370
371 test('downloads a second media playlist before playback, if bandwidth is high', function() {
372 player.src({
373 src: 'manifest/master.m3u8',
374 type: 'application/vnd.apple.mpegurl'
375 });
376 openMediaSource(player);
377
378 standardXHRResponse(requests[0]);
379
380 player.hls.playlists.setBandwidth = function() {
381 player.hls.playlists.bandwidth = 100000;
382 };
383
352 standardXHRResponse(requests[1]); 384 standardXHRResponse(requests[1]);
353 standardXHRResponse(requests[2]); 385 standardXHRResponse(requests[2]);
386 standardXHRResponse(requests[3]);
354 387
355 strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested'); 388 strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested');
356 strictEqual(requests[1].url, 389 strictEqual(requests[1].url,
...@@ -361,7 +394,12 @@ test('downloads media playlists after loading the master', function() { ...@@ -361,7 +394,12 @@ test('downloads media playlists after loading the master', function() {
361 strictEqual(requests[2].url, 394 strictEqual(requests[2].url,
362 window.location.origin + 395 window.location.origin +
363 window.location.pathname.split('/').slice(0, -1).join('/') + 396 window.location.pathname.split('/').slice(0, -1).join('/') +
364 '/manifest/00001.ts', 397 '/manifest/media1.m3u8',
398 'media playlist requested');
399 strictEqual(requests[3].url,
400 window.location.origin +
401 window.location.pathname.split('/').slice(0, -1).join('/') +
402 '/manifest/media1-00001.ts',
365 'first segment requested'); 403 'first segment requested');
366 }); 404 });
367 405
...@@ -385,6 +423,10 @@ test('calculates the bandwidth after downloading a segment', function() { ...@@ -385,6 +423,10 @@ test('calculates the bandwidth after downloading a segment', function() {
385 openMediaSource(player); 423 openMediaSource(player);
386 424
387 standardXHRResponse(requests[0]); 425 standardXHRResponse(requests[0]);
426
427 // set the request time to be a bit earlier so our bandwidth calculations are not NaN
428 requests[1].requestTime = (new Date())-100;
429
388 standardXHRResponse(requests[1]); 430 standardXHRResponse(requests[1]);
389 431
390 ok(player.hls.bandwidth, 'bandwidth is calculated'); 432 ok(player.hls.bandwidth, 'bandwidth is calculated');
...@@ -407,10 +449,12 @@ test('selects a playlist after segment downloads', function() { ...@@ -407,10 +449,12 @@ test('selects a playlist after segment downloads', function() {
407 openMediaSource(player); 449 openMediaSource(player);
408 450
409 standardXHRResponse(requests[0]); 451 standardXHRResponse(requests[0]);
452
453 player.hls.bandwidth = 3000000;
410 standardXHRResponse(requests[1]); 454 standardXHRResponse(requests[1]);
411 standardXHRResponse(requests[2]); 455 standardXHRResponse(requests[2]);
412 456
413 strictEqual(calls, 1, 'selects after the initial segment'); 457 strictEqual(calls, 2, 'selects after the initial segment');
414 player.currentTime = function() { 458 player.currentTime = function() {
415 return 1; 459 return 1;
416 }; 460 };
...@@ -420,7 +464,8 @@ test('selects a playlist after segment downloads', function() { ...@@ -420,7 +464,8 @@ test('selects a playlist after segment downloads', function() {
420 player.trigger('timeupdate'); 464 player.trigger('timeupdate');
421 465
422 standardXHRResponse(requests[3]); 466 standardXHRResponse(requests[3]);
423 strictEqual(calls, 2, 'selects after additional segments'); 467
468 strictEqual(calls, 3, 'selects after additional segments');
424 }); 469 });
425 470
426 test('moves to the next segment if there is a network error', function() { 471 test('moves to the next segment if there is a network error', function() {
...@@ -433,6 +478,8 @@ test('moves to the next segment if there is a network error', function() { ...@@ -433,6 +478,8 @@ test('moves to the next segment if there is a network error', function() {
433 openMediaSource(player); 478 openMediaSource(player);
434 479
435 standardXHRResponse(requests[0]); 480 standardXHRResponse(requests[0]);
481
482 player.hls.bandwidth = 3000000;
436 standardXHRResponse(requests[1]); 483 standardXHRResponse(requests[1]);
437 484
438 mediaIndex = player.hls.mediaIndex; 485 mediaIndex = player.hls.mediaIndex;
...@@ -486,6 +533,8 @@ test('downloads additional playlists if required', function() { ...@@ -486,6 +533,8 @@ test('downloads additional playlists if required', function() {
486 openMediaSource(player); 533 openMediaSource(player);
487 534
488 standardXHRResponse(requests[0]); 535 standardXHRResponse(requests[0]);
536
537 player.hls.bandwidth = 3000000;
489 standardXHRResponse(requests[1]); 538 standardXHRResponse(requests[1]);
490 // before an m3u8 is downloaded, no segments are available 539 // before an m3u8 is downloaded, no segments are available
491 player.hls.selectPlaylist = function() { 540 player.hls.selectPlaylist = function() {
...@@ -661,7 +710,7 @@ test('downloads the next segment if the buffer is getting low', function() { ...@@ -661,7 +710,7 @@ test('downloads the next segment if the buffer is getting low', function() {
661 strictEqual(requests[2].url, 710 strictEqual(requests[2].url,
662 window.location.origin + 711 window.location.origin +
663 window.location.pathname.split('/').slice(0, -1).join('/') + 712 window.location.pathname.split('/').slice(0, -1).join('/') +
664 '/manifest/00002.ts', 713 '/manifest/media-00002.ts',
665 'made segment request'); 714 'made segment request');
666 }); 715 });
667 716
...@@ -1161,6 +1210,8 @@ test('resets the switching algorithm if a request times out', function() { ...@@ -1161,6 +1210,8 @@ test('resets the switching algorithm if a request times out', function() {
1161 }); 1210 });
1162 openMediaSource(player); 1211 openMediaSource(player);
1163 standardXHRResponse(requests.shift()); // master 1212 standardXHRResponse(requests.shift()); // master
1213
1214 player.hls.bandwidth = 3000000;
1164 standardXHRResponse(requests.shift()); // media.m3u8 1215 standardXHRResponse(requests.shift()); // media.m3u8
1165 // simulate a segment timeout 1216 // simulate a segment timeout
1166 requests[0].timedout = true; 1217 requests[0].timedout = true;
...@@ -1207,7 +1258,10 @@ test('remove event handlers on dispose', function() { ...@@ -1207,7 +1258,10 @@ test('remove event handlers on dispose', function() {
1207 oldOn.call(player, type, handler); 1258 oldOn.call(player, type, handler);
1208 }; 1259 };
1209 player.off = function(type, handler) { 1260 player.off = function(type, handler) {
1210 offhandlers++; 1261 // ignore the top-level videojs removals that aren't relevant to HLS
1262 if (type && type !== 'dispose') {
1263 offhandlers++;
1264 }
1211 oldOff.call(player, type, handler); 1265 oldOff.call(player, type, handler);
1212 }; 1266 };
1213 player.src({ 1267 player.src({
...@@ -1215,7 +1269,9 @@ test('remove event handlers on dispose', function() { ...@@ -1215,7 +1269,9 @@ test('remove event handlers on dispose', function() {
1215 type: 'application/vnd.apple.mpegurl' 1269 type: 'application/vnd.apple.mpegurl'
1216 }); 1270 });
1217 openMediaSource(player); 1271 openMediaSource(player);
1218 player.hls.playlists.trigger('loadedmetadata'); 1272
1273 standardXHRResponse(requests[0]);
1274 standardXHRResponse(requests[1]);
1219 1275
1220 player.dispose(); 1276 player.dispose();
1221 1277
......