454617f1 by David LaPalomento

Resolve network path segment URLs. Check currentSrc for m3u8s to load on init.

Figure out an absolute path to segment URLs that are specified like "/path/0.ts". If the currentSrc URL ends in ".m3u8" attempt to load it during init. Re-organize XHR mocking in test cases to capture multiple request URLs.
1 parent 5d6531d2
...@@ -23,18 +23,24 @@ var ...@@ -23,18 +23,24 @@ var
23 mediaSource = new videojs.MediaSource(), 23 mediaSource = new videojs.MediaSource(),
24 segmentParser = new videojs.hls.SegmentParser(), 24 segmentParser = new videojs.hls.SegmentParser(),
25 player = this, 25 player = this,
26 extname,
26 url, 27 url,
27 28
28 segmentXhr, 29 segmentXhr,
29 fillBuffer, 30 fillBuffer,
30 selectPlaylist; 31 selectPlaylist;
31 32
33 extname = (/[^#?]*(?:\/[^#?]*\.([^#?]*))/).exec(player.currentSrc());
32 if (typeof options === 'string') { 34 if (typeof options === 'string') {
33 url = options; 35 url = options;
34 } else if (options) { 36 } else if (options) {
35 url = options.url; 37 url = options.url;
38 } else if (extname && extname[1] === 'm3u8') {
39 // if the currentSrc looks like an m3u8, attempt to use it
40 url = player.currentSrc();
36 } else { 41 } else {
37 // do nothing until the plugin is initialized with a valid URL 42 // do nothing until the plugin is initialized with a valid URL
43 videojs.log('hls: no valid playlist URL specified');
38 return; 44 return;
39 } 45 }
40 46
...@@ -94,8 +100,22 @@ var ...@@ -94,8 +100,22 @@ var
94 } 100 }
95 101
96 segmentUri = segment.uri; 102 segmentUri = segment.uri;
97 if (!(/^([A-z]*:)?\/\//).test(segmentUri)) { 103 if ((/^\/[^\/]/).test(segmentUri)) {
98 // the segment URI is relative to the manifest 104 // the segment is specified with a network path,
105 // e.g. "/01.ts"
106 (function() {
107 // use an anchor to resolve the manifest URL to an absolute path
108 // this method should work back to IE6:
109 // http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
110 var resolver = document.createElement('div');
111 resolver.innerHTML = '<a href="' + url + '"></a>';
112
113 segmentUri = (/^[A-z]*:\/\/[^\/]*/).exec(resolver.firstChild.href)[0] +
114 segmentUri;
115 })();
116 } else if (!(/^([A-z]*:)?\/\//).test(segmentUri)) {
117 // the segment is specified with a relative path,
118 // e.g. "../01.ts" or "path/to/01.ts"
99 segmentUri = url.split('/').slice(0, -1).concat(segmentUri).join('/'); 119 segmentUri = url.split('/').slice(0, -1).concat(segmentUri).join('/');
100 } 120 }
101 121
...@@ -109,7 +129,7 @@ var ...@@ -109,7 +129,7 @@ var
109 player.hls.segmentXhrTime = (+new Date()) - startTime; 129 player.hls.segmentXhrTime = (+new Date()) - startTime;
110 player.hls.bandwidth = segmentXhr.response.byteLength / player.hls.segmentXhrTime; 130 player.hls.bandwidth = segmentXhr.response.byteLength / player.hls.segmentXhrTime;
111 131
112 // transmux the segment data from M2TS to FLV 132 // transmux the segment data from MP2T to FLV
113 segmentParser.parseSegmentBinaryData(new Uint8Array(segmentXhr.response)); 133 segmentParser.parseSegmentBinaryData(new Uint8Array(segmentXhr.response));
114 while (segmentParser.tagsAvailable()) { 134 while (segmentParser.tagsAvailable()) {
115 player.hls.sourceBuffer.appendBuffer(segmentParser.getNextTag().bytes, 135 player.hls.sourceBuffer.appendBuffer(segmentParser.getNextTag().bytes,
......
1 {
2 "allowCache": true,
3 "targetDuration": 10,
4 "mediaSequence": 0,
5 "playlistType": "VOD",
6 "segments": [{
7 "duration": 10,
8 "uri": "/00001.ts"
9 }, {
10 "duration": 10,
11 "uri": "/subdir/00002.ts"
12 }, {
13 "duration": 10,
14 "uri": "/00003.ts"
15 }, {
16 "duration": 10,
17 "uri": "/00004.ts"
18 }]
19 }
1 #EXTM3U
2 #EXT-X-PLAYLIST-TYPE:VOD
3 #EXT-X-TARGETDURATION:10
4 #EXTINF:10,
5 /00001.ts
6 #EXTINF:10,
7 /subdir/00002.ts
8 #EXTINF:10,
9 /00003.ts
10 #EXTINF:10,
11 /00004.ts
12 #ZEN-TOTAL-DURATION:57.9911
13 #EXT-X-ENDLIST
...@@ -26,16 +26,22 @@ var ...@@ -26,16 +26,22 @@ var
26 oldFlashSupported, 26 oldFlashSupported,
27 oldXhr, 27 oldXhr,
28 oldSourceBuffer, 28 oldSourceBuffer,
29 xhrParams; 29 xhrUrls;
30 30
31 module('HLS', { 31 module('HLS', {
32 setup: function() { 32 setup: function() {
33 // force Flash support in phantomjs 33
34 // mock out Flash feature for phantomjs
34 oldFlashSupported = videojs.Flash.isSupported; 35 oldFlashSupported = videojs.Flash.isSupported;
35 videojs.Flash.isSupported = function() { 36 videojs.Flash.isSupported = function() {
36 return true; 37 return true;
37 }; 38 };
39 oldSourceBuffer = window.videojs.SourceBuffer;
40 window.videojs.SourceBuffer = function() {
41 this.appendBuffer = function() {};
42 };
38 43
44 // create the test player
39 var video = document.createElement('video'); 45 var video = document.createElement('video');
40 document.querySelector('#qunit-fixture').appendChild(video); 46 document.querySelector('#qunit-fixture').appendChild(video);
41 player = videojs(video, { 47 player = videojs(video, {
...@@ -51,34 +57,29 @@ module('HLS', { ...@@ -51,34 +57,29 @@ module('HLS', {
51 // make XHR synchronous 57 // make XHR synchronous
52 oldXhr = window.XMLHttpRequest; 58 oldXhr = window.XMLHttpRequest;
53 window.XMLHttpRequest = function() { 59 window.XMLHttpRequest = function() {
54 this.open = function() { 60 this.open = function(method, url) {
55 xhrParams = arguments; 61 xhrUrls.push(url);
56 }; 62 };
57 this.send = function() { 63 this.send = function() {
58 // if the request URL looks like one of the test manifests, grab the 64 // if the request URL looks like one of the test manifests, grab the
59 // contents off the global object 65 // contents off the global object
60 var manifestName = (/.*\/(.*)\.m3u8/).exec(xhrParams[1]); 66 var manifestName = (/.*\/(.*)\.m3u8/).exec(xhrUrls.slice(-1)[0]);
61 if (manifestName) { 67 if (manifestName) {
62 manifestName = manifestName[1]; 68 manifestName = manifestName[1];
63 } 69 }
64 this.responseText = window.manifests[manifestName || xhrParams[1]]; 70 this.responseText = window.manifests[manifestName || xhrUrls.slice(-1)[0]];
65 this.response = new Uint8Array([1]).buffer; 71 this.response = new Uint8Array([1]).buffer;
66 72
67 this.readyState = 4; 73 this.readyState = 4;
68 this.onreadystatechange(); 74 this.onreadystatechange();
69 }; 75 };
70 }; 76 };
71 77 xhrUrls = [];
72 // mock out SourceBuffer since it won't be available in phantomjs
73 oldSourceBuffer = window.videojs.SourceBuffer;
74 window.videojs.SourceBuffer = function() {
75 this.appendBuffer = function() {};
76 };
77 }, 78 },
78 teardown: function() { 79 teardown: function() {
79 videojs.Flash.isSupported = oldFlashSupported; 80 videojs.Flash.isSupported = oldFlashSupported;
80 window.XMLHttpRequest = oldXhr;
81 window.videojs.SourceBuffer = oldSourceBuffer; 81 window.videojs.SourceBuffer = oldSourceBuffer;
82 window.XMLHttpRequest = oldXhr;
82 } 83 }
83 }); 84 });
84 85
...@@ -115,23 +116,31 @@ test('starts downloading a segment on loadedmetadata', function() { ...@@ -115,23 +116,31 @@ test('starts downloading a segment on loadedmetadata', function() {
115 type: 'sourceopen' 116 type: 'sourceopen'
116 }); 117 });
117 118
118 strictEqual(xhrParams[1], 'manifest/00001.ts', 'the first segment is requested'); 119 strictEqual(xhrUrls[1], 'manifest/00001.ts', 'the first segment is requested');
119 }); 120 });
120 121
121 test('recognizes absolute URIs and requests them unmodified', function() { 122 test('recognizes absolute URIs and requests them unmodified', function() {
122 player.hls('manifest/absoluteUris.m3u8'); 123 player.hls('manifest/absoluteUris.m3u8');
123 player.buffered = function() {
124 return videojs.createTimeRange(0, 0);
125 };
126 videojs.mediaSources[player.currentSrc()].trigger({ 124 videojs.mediaSources[player.currentSrc()].trigger({
127 type: 'sourceopen' 125 type: 'sourceopen'
128 }); 126 });
129 127
130 strictEqual(xhrParams[1], 128 strictEqual(xhrUrls[1],
131 'http://example.com/00001.ts', 129 'http://example.com/00001.ts',
132 'the first segment is requested'); 130 'the first segment is requested');
133 }); 131 });
134 132
133 test('recognizes domain-relative URLs', function() {
134 player.hls('manifest/domainUris.m3u8');
135 videojs.mediaSources[player.currentSrc()].trigger({
136 type: 'sourceopen'
137 });
138
139 strictEqual(xhrUrls[1],
140 window.location.origin + '/00001.ts',
141 'the first segment is requested');
142 });
143
135 test('re-initializes the plugin for each source', function() { 144 test('re-initializes the plugin for each source', function() {
136 var firstInit, secondInit; 145 var firstInit, secondInit;
137 player.hls('manifest/master.m3u8'); 146 player.hls('manifest/master.m3u8');
...@@ -166,10 +175,9 @@ test('does not download the next segment if the buffer is full', function() { ...@@ -166,10 +175,9 @@ test('does not download the next segment if the buffer is full', function() {
166 videojs.mediaSources[player.currentSrc()].trigger({ 175 videojs.mediaSources[player.currentSrc()].trigger({
167 type: 'sourceopen' 176 type: 'sourceopen'
168 }); 177 });
169 xhrParams = null;
170 player.trigger('timeupdate'); 178 player.trigger('timeupdate');
171 179
172 strictEqual(xhrParams, null, 'no segment request was made'); 180 strictEqual(xhrUrls.length, 1, 'no segment request was made');
173 }); 181 });
174 182
175 test('downloads the next segment if the buffer is getting low', function() { 183 test('downloads the next segment if the buffer is getting low', function() {
...@@ -177,17 +185,17 @@ test('downloads the next segment if the buffer is getting low', function() { ...@@ -177,17 +185,17 @@ test('downloads the next segment if the buffer is getting low', function() {
177 videojs.mediaSources[player.currentSrc()].trigger({ 185 videojs.mediaSources[player.currentSrc()].trigger({
178 type: 'sourceopen' 186 type: 'sourceopen'
179 }); 187 });
188 strictEqual(xhrUrls.length, 2, 'did not make a request');
180 player.currentTime = function() { 189 player.currentTime = function() {
181 return 15; 190 return 15;
182 }; 191 };
183 player.buffered = function() { 192 player.buffered = function() {
184 return videojs.createTimeRange(0, 19.999); 193 return videojs.createTimeRange(0, 19.999);
185 }; 194 };
186 xhrParams = null;
187 player.trigger('timeupdate'); 195 player.trigger('timeupdate');
188 196
189 ok(xhrParams, 'made a request'); 197 strictEqual(xhrUrls.length, 3, 'made a request');
190 strictEqual(xhrParams[1], 'manifest/00002.ts', 'made segment request'); 198 strictEqual(xhrUrls[2], 'manifest/00002.ts', 'made segment request');
191 }); 199 });
192 200
193 test('stops downloading segments at the end of the playlist', function() { 201 test('stops downloading segments at the end of the playlist', function() {
...@@ -195,11 +203,11 @@ test('stops downloading segments at the end of the playlist', function() { ...@@ -195,11 +203,11 @@ test('stops downloading segments at the end of the playlist', function() {
195 videojs.mediaSources[player.currentSrc()].trigger({ 203 videojs.mediaSources[player.currentSrc()].trigger({
196 type: 'sourceopen' 204 type: 'sourceopen'
197 }); 205 });
198 xhrParams = null; 206 xhrUrls = [];
199 player.hls.currentMediaIndex = 4; 207 player.hls.currentMediaIndex = 4;
200 player.trigger('timeupdate'); 208 player.trigger('timeupdate');
201 209
202 strictEqual(xhrParams, null, 'no request is made'); 210 strictEqual(xhrUrls.length, 0, 'no request is made');
203 }); 211 });
204 212
205 test('only makes one segment request at a time', function() { 213 test('only makes one segment request at a time', function() {
...@@ -222,6 +230,45 @@ test('only makes one segment request at a time', function() { ...@@ -222,6 +230,45 @@ test('only makes one segment request at a time', function() {
222 strictEqual(1, openedXhrs, 'only one XHR is made'); 230 strictEqual(1, openedXhrs, 'only one XHR is made');
223 }); 231 });
224 232
233 test('uses the currentSrc if no options are provided and it ends in ".m3u8"', function() {
234 var url = 'http://example.com/services/mobile/streaming/index/master.m3u8?videoId=1824650741001';
235 player.src(url);
236 player.hls();
237 videojs.mediaSources[player.currentSrc()].trigger({
238 type: 'sourceopen'
239 });
240
241 strictEqual(url, xhrUrls[0], 'currentSrc is used');
242 });
243
244 test('ignores currentSrc if it doesn\'t have the "m3u8" extension', function() {
245 player.src('basdfasdfasdfliel//.m3u9');
246 player.hls();
247 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
248 strictEqual(xhrUrls.length, 0, 'no request is made');
249
250 player.src('');
251 player.hls();
252 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
253 strictEqual(xhrUrls.length, 0, 'no request is made');
254
255 player.src('http://example.com/movie.mp4?q=why.m3u8');
256 player.hls();
257 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
258 strictEqual(xhrUrls.length, 0, 'no request is made');
259
260 player.src('http://example.m3u8/movie.mp4');
261 player.hls();
262 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
263 strictEqual(xhrUrls.length, 0, 'no request is made');
264
265 player.src('//example.com/movie.mp4#http://tricky.com/master.m3u8');
266 player.hls();
267 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
268 strictEqual(xhrUrls.length, 0, 'no request is made');
269 });
270
271
225 module('segment controller', { 272 module('segment controller', {
226 setup: function() { 273 setup: function() {
227 segmentController = new window.videojs.hls.SegmentController(); 274 segmentController = new window.videojs.hls.SegmentController();
......