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,109 +10,141 @@ ...@@ -10,109 +10,141 @@
10 10
11 videojs.hls = {}; 11 videojs.hls = {};
12 12
13 var init = function(options) { 13 var
14 var 14 // the desired length of video to maintain in the buffer, in seconds
15 mediaSource = new videojs.MediaSource(), 15 goalBufferLength = 5,
16 segmentParser = new videojs.hls.SegmentParser(), 16
17 player = this, 17 /**
18 url, 18 * Initializes the HLS plugin.
19 19 * @param options {mixed} the URL to an HLS playlist
20 fillBuffer, 20 */
21 selectPlaylist; 21 init = function(options) {
22 22 var
23 if (typeof options === 'string') { 23 mediaSource = new videojs.MediaSource(),
24 url = options; 24 segmentParser = new videojs.hls.SegmentParser(),
25 } else { 25 player = this,
26 url = options.url; 26 url,
27 } 27
28 28 fillBuffer,
29 // expose the HLS plugin state 29 selectPlaylist;
30 player.hls.readyState = function() { 30
31 if (!player.hls.manifest) { 31 if (typeof options === 'string') {
32 return 0; // HAVE_NOTHING 32 url = options;
33 } else {
34 url = options.url;
33 } 35 }
34 return 1; // HAVE_METADATA
35 };
36 36
37 // load the MediaSource into the player 37 // expose the HLS plugin state
38 mediaSource.addEventListener('sourceopen', function() { 38 player.hls.readyState = function() {
39 // construct the video data buffer and set the appropriate MIME type 39 if (!player.hls.manifest) {
40 var sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"'); 40 return 0; // HAVE_NOTHING
41 player.hls.sourceBuffer = sourceBuffer; 41 }
42 sourceBuffer.appendBuffer(segmentParser.getFlvHeader()); 42 return 1; // HAVE_METADATA
43
44 // Chooses the appropriate media playlist based on the current bandwidth
45 // estimate and the player size
46 selectPlaylist = function() {
47 player.hls.currentPlaylist = player.hls.manifest;
48 player.hls.currentMediaIndex = 0;
49 }; 43 };
50 44
51 // download a new segment if one is needed 45 // load the MediaSource into the player
52 fillBuffer = function() { 46 mediaSource.addEventListener('sourceopen', function() {
53 var 47 // construct the video data buffer and set the appropriate MIME type
54 xhr = new window.XMLHttpRequest(), 48 var sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"');
55 segment = player.hls.currentPlaylist.segments[player.hls.currentMediaIndex], 49 player.hls.sourceBuffer = sourceBuffer;
56 segmentUri = segment.uri, 50 sourceBuffer.appendBuffer(segmentParser.getFlvHeader());
57 startTime; 51
58 if (!(/^([A-z]*:)?\/\//).test(segmentUri)) { 52 // Chooses the appropriate media playlist based on the current bandwidth
59 // the segment URI is relative to the manifest 53 // estimate and the player size
60 segmentUri = url.split('/').slice(0, -1).concat(segmentUri).join('/'); 54 selectPlaylist = function() {
61 } 55 player.hls.currentPlaylist = player.hls.manifest;
62 xhr.open('GET', segmentUri); 56 player.hls.currentMediaIndex = 0;
63 xhr.responseType = 'arraybuffer'; 57 };
64 xhr.onreadystatechange = function() {
65 var elapsed;
66 if (xhr.readyState === 4) {
67 // calculate the download bandwidth
68 elapsed = ((+new Date()) - startTime) * 1000;
69 player.hls.bandwidth = xhr.response.byteLength / elapsed;
70
71 // transmux the segment data from M2TS to FLV
72 segmentParser.parseSegmentBinaryData(new Uint8Array(xhr.response));
73 while (segmentParser.tagsAvailable()) {
74 player.hls.sourceBuffer.appendBuffer(segmentParser.getNextTag().bytes,
75 player);
76 }
77 58
78 // update the segment index 59 /**
79 player.hls.currentMediaIndex++; 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 */
63 fillBuffer = function() {
64 var
65 buffered = player.buffered(),
66 bufferedTime = 0,
67 xhr = new window.XMLHttpRequest(),
68 segment = player.hls.currentPlaylist.segments[player.hls.currentMediaIndex],
69 segmentUri,
70 startTime;
71
72 // if the video has finished downloading, stop trying to buffer
73 if (!segment) {
74 return;
80 } 75 }
81 }; 76
82 startTime = +new Date(); 77 if (buffered) {
83 xhr.send(null); 78 // assuming a single, contiguous buffer region
84 }; 79 bufferedTime = player.buffered().end(0) - player.currentTime();
85 player.on('loadedmetadata', fillBuffer);
86
87 // download and process the manifest
88 (function() {
89 var xhr = new window.XMLHttpRequest();
90 xhr.open('GET', url);
91 xhr.onreadystatechange = function() {
92 var parser;
93
94 if (xhr.readyState === 4) {
95 // readystate DONE
96 parser = new videojs.m3u8.Parser();
97 parser.push(xhr.responseText);
98 player.hls.manifest = parser.manifest;
99
100 player.trigger('loadedmanifest');
101
102 if (parser.manifest.segments) {
103 selectPlaylist();
104 player.trigger('loadedmetadata');
105 }
106 } 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;
89 if (!(/^([A-z]*:)?\/\//).test(segmentUri)) {
90 // the segment URI is relative to the manifest
91 segmentUri = url.split('/').slice(0, -1).concat(segmentUri).join('/');
92 }
93
94 // request the next segment
95 xhr.open('GET', segmentUri);
96 xhr.responseType = 'arraybuffer';
97 xhr.onreadystatechange = function() {
98 if (xhr.readyState === 4) {
99 // calculate the download bandwidth
100 player.hls.segmentRequestTime = (+new Date()) - startTime;
101 player.hls.bandwidth = xhr.response.byteLength / player.hls.segmentRequestTime;
102
103 // transmux the segment data from M2TS to FLV
104 segmentParser.parseSegmentBinaryData(new Uint8Array(xhr.response));
105 while (segmentParser.tagsAvailable()) {
106 player.hls.sourceBuffer.appendBuffer(segmentParser.getNextTag().bytes,
107 player);
108 }
109
110 player.hls.currentMediaIndex++;
111 }
112 };
113 startTime = +new Date();
114 xhr.send(null);
107 }; 115 };
108 xhr.send(null); 116 player.on('loadedmetadata', fillBuffer);
109 })(); 117 player.on('timeupdate', fillBuffer);
110 }); 118
111 player.src({ 119 // download and process the manifest
112 src: videojs.URL.createObjectURL(mediaSource), 120 (function() {
113 type: "video/flv" 121 var xhr = new window.XMLHttpRequest();
114 }); 122 xhr.open('GET', url);
115 }; 123 xhr.onreadystatechange = function() {
124 var parser;
125
126 if (xhr.readyState === 4) {
127 // readystate DONE
128 parser = new videojs.m3u8.Parser();
129 parser.push(xhr.responseText);
130 player.hls.manifest = parser.manifest;
131
132 player.trigger('loadedmanifest');
133
134 if (parser.manifest.segments) {
135 selectPlaylist();
136 player.trigger('loadedmetadata');
137 }
138 }
139 };
140 xhr.send(null);
141 })();
142 });
143 player.src({
144 src: videojs.URL.createObjectURL(mediaSource),
145 type: "video/flv"
146 });
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', {
......