f2be83ef by David LaPalomento

Simple buffer management algorithm

On loadedmetadata and timeupdate, check the length of content from the current playhead position to the end of the buffer. If the amount of time buffered is less than a goal size of 5 seconds, download another segment of the video. For this algorithm to work, it requires an update to the video.js swf to expose the bufferLength property on the netstream directly. Added a fix so that once all segments are downloaded, the plugin does not attempt to continue downloading segments.
1 parent 52799413
...@@ -10,7 +10,15 @@ ...@@ -10,7 +10,15 @@
10 10
11 videojs.hls = {}; 11 videojs.hls = {};
12 12
13 var init = function(options) { 13 var
14 // the desired length of video to maintain in the buffer, in seconds
15 goalBufferLength = 5,
16
17 /**
18 * Initializes the HLS plugin.
19 * @param options {mixed} the URL to an HLS playlist
20 */
21 init = function(options) {
14 var 22 var
15 mediaSource = new videojs.MediaSource(), 23 mediaSource = new videojs.MediaSource(),
16 segmentParser = new videojs.hls.SegmentParser(), 24 segmentParser = new videojs.hls.SegmentParser(),
...@@ -48,25 +56,49 @@ var init = function(options) { ...@@ -48,25 +56,49 @@ var init = function(options) {
48 player.hls.currentMediaIndex = 0; 56 player.hls.currentMediaIndex = 0;
49 }; 57 };
50 58
51 // download a new segment if one is needed 59 /**
60 * Determines whether there is enough video data currently in the buffer
61 * and downloads a new segment if the buffered time is less than the goal.
62 */
52 fillBuffer = function() { 63 fillBuffer = function() {
53 var 64 var
65 buffered = player.buffered(),
66 bufferedTime = 0,
54 xhr = new window.XMLHttpRequest(), 67 xhr = new window.XMLHttpRequest(),
55 segment = player.hls.currentPlaylist.segments[player.hls.currentMediaIndex], 68 segment = player.hls.currentPlaylist.segments[player.hls.currentMediaIndex],
56 segmentUri = segment.uri, 69 segmentUri,
57 startTime; 70 startTime;
71
72 // if the video has finished downloading, stop trying to buffer
73 if (!segment) {
74 return;
75 }
76
77 if (buffered) {
78 // assuming a single, contiguous buffer region
79 bufferedTime = player.buffered().end(0) - player.currentTime();
80 }
81
82 // if there is plenty of content in the buffer, relax for awhile
83 console.log('bufferedTime:', bufferedTime);
84 if (bufferedTime >= goalBufferLength) {
85 return;
86 }
87
88 segmentUri = segment.uri;
58 if (!(/^([A-z]*:)?\/\//).test(segmentUri)) { 89 if (!(/^([A-z]*:)?\/\//).test(segmentUri)) {
59 // the segment URI is relative to the manifest 90 // the segment URI is relative to the manifest
60 segmentUri = url.split('/').slice(0, -1).concat(segmentUri).join('/'); 91 segmentUri = url.split('/').slice(0, -1).concat(segmentUri).join('/');
61 } 92 }
93
94 // request the next segment
62 xhr.open('GET', segmentUri); 95 xhr.open('GET', segmentUri);
63 xhr.responseType = 'arraybuffer'; 96 xhr.responseType = 'arraybuffer';
64 xhr.onreadystatechange = function() { 97 xhr.onreadystatechange = function() {
65 var elapsed;
66 if (xhr.readyState === 4) { 98 if (xhr.readyState === 4) {
67 // calculate the download bandwidth 99 // calculate the download bandwidth
68 elapsed = ((+new Date()) - startTime) * 1000; 100 player.hls.segmentRequestTime = (+new Date()) - startTime;
69 player.hls.bandwidth = xhr.response.byteLength / elapsed; 101 player.hls.bandwidth = xhr.response.byteLength / player.hls.segmentRequestTime;
70 102
71 // transmux the segment data from M2TS to FLV 103 // transmux the segment data from M2TS to FLV
72 segmentParser.parseSegmentBinaryData(new Uint8Array(xhr.response)); 104 segmentParser.parseSegmentBinaryData(new Uint8Array(xhr.response));
...@@ -75,7 +107,6 @@ var init = function(options) { ...@@ -75,7 +107,6 @@ var init = function(options) {
75 player); 107 player);
76 } 108 }
77 109
78 // update the segment index
79 player.hls.currentMediaIndex++; 110 player.hls.currentMediaIndex++;
80 } 111 }
81 }; 112 };
...@@ -83,6 +114,7 @@ var init = function(options) { ...@@ -83,6 +114,7 @@ var init = function(options) {
83 xhr.send(null); 114 xhr.send(null);
84 }; 115 };
85 player.on('loadedmetadata', fillBuffer); 116 player.on('loadedmetadata', fillBuffer);
117 player.on('timeupdate', fillBuffer);
86 118
87 // download and process the manifest 119 // download and process the manifest
88 (function() { 120 (function() {
...@@ -112,7 +144,7 @@ var init = function(options) { ...@@ -112,7 +144,7 @@ var init = function(options) {
112 src: videojs.URL.createObjectURL(mediaSource), 144 src: videojs.URL.createObjectURL(mediaSource),
113 type: "video/flv" 145 type: "video/flv"
114 }); 146 });
115 }; 147 };
116 148
117 videojs.plugin('hls', function() { 149 videojs.plugin('hls', function() {
118 var initialize = function() { 150 var initialize = function() {
......
...@@ -20,13 +20,27 @@ ...@@ -20,13 +20,27 @@
20 throws(block, [expected], [message]) 20 throws(block, [expected], [message])
21 */ 21 */
22 22
23 var player, oldXhr, oldSourceBuffer, xhrParams; 23 var player, oldFlashSupported, oldXhr, oldSourceBuffer, xhrParams;
24 24
25 module('HLS', { 25 module('HLS', {
26 setup: function() { 26 setup: function() {
27 var video = document.createElement('video'); 27 var video = document.createElement('video');
28 document.querySelector('#qunit-fixture').appendChild(video); 28 document.querySelector('#qunit-fixture').appendChild(video);
29 player = videojs(video); 29 player = videojs(video, {
30 flash: {
31 swf: '../node_modules/video.js/dist/video-js/video-js.swf',
32 },
33 techOrder: ['flash']
34 });
35
36 // force Flash support in phantomjs
37 oldFlashSupported = videojs.Flash.isSupported;
38 videojs.Flash.isSupported = function() {
39 return true;
40 };
41 player.buffered = function() {
42 return videojs.createTimeRange(0, 0);
43 };
30 44
31 // make XHR synchronous 45 // make XHR synchronous
32 oldXhr = window.XMLHttpRequest; 46 oldXhr = window.XMLHttpRequest;
...@@ -56,6 +70,7 @@ module('HLS', { ...@@ -56,6 +70,7 @@ module('HLS', {
56 }; 70 };
57 }, 71 },
58 teardown: function() { 72 teardown: function() {
73 videojs.Flash.isSupported = oldFlashSupported;
59 window.XMLHttpRequest = oldXhr; 74 window.XMLHttpRequest = oldXhr;
60 window.videojs.SourceBuffer = oldSourceBuffer; 75 window.videojs.SourceBuffer = oldSourceBuffer;
61 } 76 }
...@@ -87,6 +102,9 @@ test('loads the specified manifest URL on init', function() { ...@@ -87,6 +102,9 @@ test('loads the specified manifest URL on init', function() {
87 102
88 test('starts downloading a segment on loadedmetadata', function() { 103 test('starts downloading a segment on loadedmetadata', function() {
89 player.hls('manifest/media.m3u8'); 104 player.hls('manifest/media.m3u8');
105 player.buffered = function() {
106 return videojs.createTimeRange(0, 0);
107 };
90 videojs.mediaSources[player.currentSrc()].trigger({ 108 videojs.mediaSources[player.currentSrc()].trigger({
91 type: 'sourceopen' 109 type: 'sourceopen'
92 }); 110 });
...@@ -96,6 +114,9 @@ test('starts downloading a segment on loadedmetadata', function() { ...@@ -96,6 +114,9 @@ test('starts downloading a segment on loadedmetadata', function() {
96 114
97 test('recognizes absolute URIs and requests them unmodified', function() { 115 test('recognizes absolute URIs and requests them unmodified', function() {
98 player.hls('manifest/absoluteUris.m3u8'); 116 player.hls('manifest/absoluteUris.m3u8');
117 player.buffered = function() {
118 return videojs.createTimeRange(0, 0);
119 };
99 videojs.mediaSources[player.currentSrc()].trigger({ 120 videojs.mediaSources[player.currentSrc()].trigger({
100 type: 'sourceopen' 121 type: 'sourceopen'
101 }); 122 });
...@@ -113,7 +134,6 @@ test('re-initializes the plugin for each source', function() { ...@@ -113,7 +134,6 @@ test('re-initializes the plugin for each source', function() {
113 secondInit = player.hls; 134 secondInit = player.hls;
114 135
115 notStrictEqual(firstInit, secondInit, 'the plugin object is replaced'); 136 notStrictEqual(firstInit, secondInit, 'the plugin object is replaced');
116
117 }); 137 });
118 138
119 test('calculates the bandwidth after downloading a segment', function() { 139 test('calculates the bandwidth after downloading a segment', function() {
...@@ -125,6 +145,55 @@ test('calculates the bandwidth after downloading a segment', function() { ...@@ -125,6 +145,55 @@ test('calculates the bandwidth after downloading a segment', function() {
125 ok(player.hls.bandwidth, 'bandwidth is calculated'); 145 ok(player.hls.bandwidth, 'bandwidth is calculated');
126 ok(player.hls.bandwidth > 0, 146 ok(player.hls.bandwidth > 0,
127 'bandwidth is positive: ' + player.hls.bandwidth); 147 'bandwidth is positive: ' + player.hls.bandwidth);
148 ok(player.hls.segmentRequestTime >= 0,
149 'saves segment request time: ' + player.hls.segmentRequestTime + 's');
150 });
151
152 test('does not download the next segment if the buffer is full', function() {
153 player.hls('manifest/media.m3u8');
154 player.currentTime = function() {
155 return 15;
156 };
157 player.buffered = function() {
158 return videojs.createTimeRange(0, 20);
159 };
160 videojs.mediaSources[player.currentSrc()].trigger({
161 type: 'sourceopen'
162 });
163 xhrParams = null;
164 player.trigger('timeupdate');
165
166 strictEqual(xhrParams, null, 'no segment request was made');
167 });
168
169 test('downloads the next segment if the buffer is getting low', function() {
170 player.hls('manifest/media.m3u8');
171 videojs.mediaSources[player.currentSrc()].trigger({
172 type: 'sourceopen'
173 });
174 player.currentTime = function() {
175 return 15;
176 };
177 player.buffered = function() {
178 return videojs.createTimeRange(0, 19.999);
179 };
180 xhrParams = null;
181 player.trigger('timeupdate');
182
183 ok(xhrParams, 'made a request');
184 strictEqual(xhrParams[1], 'manifest/00002.ts', 'made segment request');
185 });
186
187 test('stops downloading segments at the end of the playlist', function() {
188 player.hls('manifest/media.m3u8');
189 videojs.mediaSources[player.currentSrc()].trigger({
190 type: 'sourceopen'
191 });
192 xhrParams = null;
193 player.hls.currentMediaIndex = 4;
194 player.trigger('timeupdate');
195
196 strictEqual(xhrParams, null, 'no request is made');
128 }); 197 });
129 198
130 module('segment controller', { 199 module('segment controller', {
......