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.
Showing
4 changed files
with
127 additions
and
28 deletions
... | @@ -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, | ... | ... |
test/manifest/domainUris.json
0 → 100644
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 | } |
test/manifest/domainUris.m3u8
0 → 100644
... | @@ -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(); | ... | ... |
-
Please register or sign in to post a comment