Basic support for master playlists
If a master playlist has been downloaded, immediately fetch the default variant playlist and start buffering it. This matches HLS network activity in Safari on OS X which also seems to lazily load the non-default variant streams. Consolidate relative URL resolution and use a solution involving the `base` element to take advantage of browser logic for URL composition. Update test cases to expect absolute URLs for XHRs after the initial manifest request.
Showing
8 changed files
with
279 additions
and
137 deletions
... | @@ -412,7 +412,7 @@ | ... | @@ -412,7 +412,7 @@ |
412 | this.manifest.totalDuration = calculatedDuration; | 412 | this.manifest.totalDuration = calculatedDuration; |
413 | this.trigger('info', { | 413 | this.trigger('info', { |
414 | message: 'updating total duration to use a calculated value' | 414 | message: 'updating total duration to use a calculated value' |
415 | }) | 415 | }); |
416 | } | 416 | } |
417 | } | 417 | } |
418 | })[entry.tagType] || noop).call(self); | 418 | })[entry.tagType] || noop).call(self); | ... | ... |
... | @@ -6,7 +6,7 @@ | ... | @@ -6,7 +6,7 @@ |
6 | * All rights reserved. | 6 | * All rights reserved. |
7 | */ | 7 | */ |
8 | 8 | ||
9 | (function(window, videojs, undefined) { | 9 | (function(window, videojs, document, undefined) { |
10 | 10 | ||
11 | videojs.hls = {}; | 11 | videojs.hls = {}; |
12 | 12 | ||
... | @@ -15,6 +15,45 @@ var | ... | @@ -15,6 +15,45 @@ var |
15 | goalBufferLength = 5, | 15 | goalBufferLength = 5, |
16 | 16 | ||
17 | /** | 17 | /** |
18 | * Constructs a new URI by interpreting a path relative to another | ||
19 | * URI. | ||
20 | * @param basePath {string} a relative or absolute URI | ||
21 | * @param path {string} a path part to combine with the base | ||
22 | * @return {string} a URI that is equivalent to composing `base` | ||
23 | * with `path` | ||
24 | * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue | ||
25 | */ | ||
26 | resolveUrl = function(basePath, path) { | ||
27 | // use the base element to get the browser to handle URI resolution | ||
28 | var | ||
29 | oldBase = document.querySelector('base'), | ||
30 | docHead = document.querySelector('head'), | ||
31 | a = document.createElement('a'), | ||
32 | base = oldBase, | ||
33 | oldHref, | ||
34 | result; | ||
35 | |||
36 | // prep the document | ||
37 | if (oldBase) { | ||
38 | oldHref = oldBase.href; | ||
39 | } else { | ||
40 | base = docHead.appendChild(document.createElement('base')); | ||
41 | } | ||
42 | |||
43 | base.href = basePath; | ||
44 | a.href = path; | ||
45 | result = a.href; | ||
46 | |||
47 | // clean up | ||
48 | if (oldBase) { | ||
49 | oldBase.href = oldHref; | ||
50 | } else { | ||
51 | docHead.removeChild(base); | ||
52 | } | ||
53 | return result; | ||
54 | }, | ||
55 | |||
56 | /** | ||
18 | * Initializes the HLS plugin. | 57 | * Initializes the HLS plugin. |
19 | * @param options {mixed} the URL to an HLS playlist | 58 | * @param options {mixed} the URL to an HLS playlist |
20 | */ | 59 | */ |
... | @@ -24,21 +63,21 @@ var | ... | @@ -24,21 +63,21 @@ var |
24 | segmentParser = new videojs.hls.SegmentParser(), | 63 | segmentParser = new videojs.hls.SegmentParser(), |
25 | player = this, | 64 | player = this, |
26 | extname, | 65 | extname, |
27 | url, | 66 | srcUrl, |
28 | 67 | ||
29 | segmentXhr, | 68 | segmentXhr, |
30 | fillBuffer, | ||
31 | onDurationUpdate, | 69 | onDurationUpdate, |
32 | selectPlaylist; | 70 | downloadPlaylist, |
71 | fillBuffer; | ||
33 | 72 | ||
34 | extname = (/[^#?]*(?:\/[^#?]*\.([^#?]*))/).exec(player.currentSrc()); | 73 | extname = (/[^#?]*(?:\/[^#?]*\.([^#?]*))/).exec(player.currentSrc()); |
35 | if (typeof options === 'string') { | 74 | if (typeof options === 'string') { |
36 | url = options; | 75 | srcUrl = options; |
37 | } else if (options) { | 76 | } else if (options) { |
38 | url = options.url; | 77 | srcUrl = options.url; |
39 | } else if (extname && extname[1] === 'm3u8') { | 78 | } else if (extname && extname[1] === 'm3u8') { |
40 | // if the currentSrc looks like an m3u8, attempt to use it | 79 | // if the currentSrc looks like an m3u8, attempt to use it |
41 | url = player.currentSrc(); | 80 | srcUrl = player.currentSrc(); |
42 | } else { | 81 | } else { |
43 | // do nothing until the plugin is initialized with a valid URL | 82 | // do nothing until the plugin is initialized with a valid URL |
44 | videojs.log('hls: no valid playlist URL specified'); | 83 | videojs.log('hls: no valid playlist URL specified'); |
... | @@ -47,137 +86,155 @@ var | ... | @@ -47,137 +86,155 @@ var |
47 | 86 | ||
48 | // expose the HLS plugin state | 87 | // expose the HLS plugin state |
49 | player.hls.readyState = function() { | 88 | player.hls.readyState = function() { |
50 | if (!player.hls.manifest) { | 89 | if (!player.hls.media) { |
51 | return 0; // HAVE_NOTHING | 90 | return 0; // HAVE_NOTHING |
52 | } | 91 | } |
53 | return 1; // HAVE_METADATA | 92 | return 1; // HAVE_METADATA |
54 | }; | 93 | }; |
55 | 94 | ||
56 | // load the MediaSource into the player | ||
57 | mediaSource.addEventListener('sourceopen', function() { | ||
58 | // construct the video data buffer and set the appropriate MIME type | ||
59 | var sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"'); | ||
60 | player.hls.sourceBuffer = sourceBuffer; | ||
61 | sourceBuffer.appendBuffer(segmentParser.getFlvHeader()); | ||
62 | 95 | ||
63 | // Chooses the appropriate media playlist based on the current bandwidth | 96 | /** |
64 | // estimate and the player size | 97 | * Chooses the appropriate media playlist based on the current |
65 | selectPlaylist = function() { | 98 | * bandwidth estimate and the player size. |
66 | player.hls.currentPlaylist = player.hls.manifest; | 99 | */ |
67 | player.hls.currentMediaIndex = 0; | 100 | player.hls.selectPlaylist = function() { |
68 | }; | 101 | player.hls.media = player.hls.master.playlists[0]; |
102 | player.hls.mediaIndex = 0; | ||
103 | }; | ||
69 | 104 | ||
70 | onDurationUpdate = function(value) { | 105 | onDurationUpdate = function(value) { |
71 | player.duration(value); | 106 | player.duration(value); |
72 | }; | 107 | }; |
73 | 108 | ||
74 | /** | 109 | /** |
75 | * Determines whether there is enough video data currently in the buffer | 110 | * Download an M3U8 and update the current manifest object. If the provided |
76 | * and downloads a new segment if the buffered time is less than the goal. | 111 | * URL is a master playlist, the default variant will be downloaded and |
77 | */ | 112 | * parsed as well. Triggers `loadedmanifest` once for each playlist that is |
78 | fillBuffer = function() { | 113 | * downloaded and `loadedmetadata` after at least one media playlist has |
79 | var | 114 | * been parsed. Whether multiple playlists were downloaded or not, after |
80 | buffered = player.buffered(), | 115 | * `loadedmetadata` fires a parsed or inferred master playlist object will |
81 | bufferedTime = 0, | 116 | * be available as `player.hls.master`. |
82 | segment = player.hls.currentPlaylist.segments[player.hls.currentMediaIndex], | 117 | * |
83 | segmentUri, | 118 | * @param url {string} a URL to the M3U8 file to process |
84 | startTime; | 119 | */ |
85 | 120 | downloadPlaylist = function(url) { | |
86 | // if there is a request already in flight, do nothing | 121 | var xhr = new window.XMLHttpRequest(); |
87 | if (segmentXhr) { | 122 | xhr.open('GET', url); |
88 | return; | 123 | xhr.onreadystatechange = function() { |
89 | } | 124 | var i, parser, playlist, playlistUri; |
125 | |||
126 | if (xhr.readyState === 4) { | ||
127 | // readystate DONE | ||
128 | parser = new videojs.m3u8.Parser(); | ||
129 | parser.on('durationUpdate', onDurationUpdate); | ||
130 | parser.push(xhr.responseText); | ||
131 | |||
132 | // master playlists | ||
133 | if (parser.manifest.playlists) { | ||
134 | player.hls.master = parser.manifest; | ||
135 | downloadPlaylist(resolveUrl(url, parser.manifest.playlists[0].uri)); | ||
136 | player.trigger('loadedmanifest'); | ||
137 | return; | ||
138 | } | ||
90 | 139 | ||
91 | // if the video has finished downloading, stop trying to buffer | 140 | // media playlists |
92 | if (!segment) { | 141 | if (player.hls.master) { |
93 | return; | 142 | // merge this playlist into the master |
94 | } | 143 | i = player.hls.master.playlists.length; |
144 | while (i--) { | ||
145 | playlist = player.hls.master.playlists[i]; | ||
146 | playlistUri = resolveUrl(srcUrl, playlist.uri); | ||
147 | if (playlistUri === url) { | ||
148 | player.hls.master.playlists[i] = | ||
149 | videojs.util.mergeOptions(playlist, parser.manifest); | ||
150 | } | ||
151 | } | ||
152 | } else { | ||
153 | // infer a master playlist if none was previously requested | ||
154 | player.hls.master = { | ||
155 | playlists: [parser.manifest] | ||
156 | }; | ||
157 | } | ||
95 | 158 | ||
96 | if (buffered) { | 159 | player.hls.selectPlaylist(); |
97 | // assuming a single, contiguous buffer region | 160 | player.trigger('loadedmanifest'); |
98 | bufferedTime = player.buffered().end(0) - player.currentTime(); | 161 | player.trigger('loadedmetadata'); |
99 | } | 162 | } |
163 | }; | ||
164 | xhr.send(null); | ||
165 | }; | ||
100 | 166 | ||
101 | // if there is plenty of content in the buffer, relax for awhile | 167 | /** |
102 | if (bufferedTime >= goalBufferLength) { | 168 | * Determines whether there is enough video data currently in the buffer |
103 | return; | 169 | * and downloads a new segment if the buffered time is less than the goal. |
104 | } | 170 | */ |
171 | fillBuffer = function() { | ||
172 | var | ||
173 | buffered = player.buffered(), | ||
174 | bufferedTime = 0, | ||
175 | segment = player.hls.media.segments[player.hls.mediaIndex], | ||
176 | segmentUri, | ||
177 | startTime; | ||
178 | |||
179 | // if there is a request already in flight, do nothing | ||
180 | if (segmentXhr) { | ||
181 | return; | ||
182 | } | ||
105 | 183 | ||
106 | segmentUri = segment.uri; | 184 | // if the video has finished downloading, stop trying to buffer |
107 | if ((/^\/[^\/]/).test(segmentUri)) { | 185 | if (!segment) { |
108 | // the segment is specified with a network path, | 186 | return; |
109 | // e.g. "/01.ts" | 187 | } |
110 | (function() { | ||
111 | // use an anchor to resolve the manifest URL to an absolute path | ||
112 | // this method should work back to IE6: | ||
113 | // http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue | ||
114 | var resolver = document.createElement('div'); | ||
115 | resolver.innerHTML = '<a href="' + url + '"></a>'; | ||
116 | |||
117 | segmentUri = (/^[A-z]*:\/\/[^\/]*/).exec(resolver.firstChild.href)[0] + | ||
118 | segmentUri; | ||
119 | })(); | ||
120 | } else if (!(/^([A-z]*:)?\/\//).test(segmentUri)) { | ||
121 | // the segment is specified with a relative path, | ||
122 | // e.g. "../01.ts" or "path/to/01.ts" | ||
123 | segmentUri = url.split('/').slice(0, -1).concat(segmentUri).join('/'); | ||
124 | } | ||
125 | 188 | ||
126 | // request the next segment | 189 | if (buffered) { |
127 | segmentXhr = new window.XMLHttpRequest(); | 190 | // assuming a single, contiguous buffer region |
128 | segmentXhr.open('GET', segmentUri); | 191 | bufferedTime = player.buffered().end(0) - player.currentTime(); |
129 | segmentXhr.responseType = 'arraybuffer'; | 192 | } |
130 | segmentXhr.onreadystatechange = function() { | 193 | |
131 | if (segmentXhr.readyState === 4) { | 194 | // if there is plenty of content in the buffer, relax for awhile |
132 | // calculate the download bandwidth | 195 | if (bufferedTime >= goalBufferLength) { |
133 | player.hls.segmentXhrTime = (+new Date()) - startTime; | 196 | return; |
134 | player.hls.bandwidth = segmentXhr.response.byteLength / player.hls.segmentXhrTime; | 197 | } |
135 | |||
136 | // transmux the segment data from MP2T to FLV | ||
137 | segmentParser.parseSegmentBinaryData(new Uint8Array(segmentXhr.response)); | ||
138 | while (segmentParser.tagsAvailable()) { | ||
139 | player.hls.sourceBuffer.appendBuffer(segmentParser.getNextTag().bytes, | ||
140 | player); | ||
141 | } | ||
142 | 198 | ||
143 | segmentXhr = null; | 199 | segmentUri = resolveUrl(resolveUrl(srcUrl, player.hls.media.uri || ''), |
144 | player.hls.currentMediaIndex++; | 200 | segment.uri); |
201 | |||
202 | // request the next segment | ||
203 | segmentXhr = new window.XMLHttpRequest(); | ||
204 | segmentXhr.open('GET', segmentUri); | ||
205 | segmentXhr.responseType = 'arraybuffer'; | ||
206 | segmentXhr.onreadystatechange = function() { | ||
207 | if (segmentXhr.readyState === 4) { | ||
208 | // calculate the download bandwidth | ||
209 | player.hls.segmentXhrTime = (+new Date()) - startTime; | ||
210 | player.hls.bandwidth = segmentXhr.response.byteLength / player.hls.segmentXhrTime; | ||
211 | |||
212 | // transmux the segment data from MP2T to FLV | ||
213 | segmentParser.parseSegmentBinaryData(new Uint8Array(segmentXhr.response)); | ||
214 | while (segmentParser.tagsAvailable()) { | ||
215 | player.hls.sourceBuffer.appendBuffer(segmentParser.getNextTag().bytes, | ||
216 | player); | ||
145 | } | 217 | } |
146 | }; | 218 | |
147 | startTime = +new Date(); | 219 | segmentXhr = null; |
148 | segmentXhr.send(null); | 220 | player.hls.mediaIndex++; |
221 | } | ||
149 | }; | 222 | }; |
223 | startTime = +new Date(); | ||
224 | segmentXhr.send(null); | ||
225 | }; | ||
226 | |||
227 | // load the MediaSource into the player | ||
228 | mediaSource.addEventListener('sourceopen', function() { | ||
229 | // construct the video data buffer and set the appropriate MIME type | ||
230 | var sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"'); | ||
231 | player.hls.sourceBuffer = sourceBuffer; | ||
232 | sourceBuffer.appendBuffer(segmentParser.getFlvHeader()); | ||
233 | |||
150 | player.on('loadedmetadata', fillBuffer); | 234 | player.on('loadedmetadata', fillBuffer); |
151 | player.on('timeupdate', fillBuffer); | 235 | player.on('timeupdate', fillBuffer); |
152 | |||
153 | // download and process the manifest | ||
154 | (function() { | ||
155 | var xhr = new window.XMLHttpRequest(); | ||
156 | xhr.open('GET', url); | ||
157 | xhr.onreadystatechange = function() { | ||
158 | var parser; | ||
159 | |||
160 | if (xhr.readyState === 4) { | ||
161 | // readystate DONE | ||
162 | parser = new videojs.m3u8.Parser(); | ||
163 | parser.on('durationUpdate', onDurationUpdate); | ||
164 | parser.push(xhr.responseText); | ||
165 | player.hls.manifest = parser.manifest; | ||
166 | |||
167 | if(parser.manifest.totalDuration) { | ||
168 | player.duration(parser.manifest.totalDuration); | ||
169 | } | ||
170 | |||
171 | player.trigger('loadedmanifest'); | ||
172 | 236 | ||
173 | if (parser.manifest.segments) { | 237 | downloadPlaylist(srcUrl); |
174 | selectPlaylist(); | ||
175 | player.trigger('loadedmetadata'); | ||
176 | } | ||
177 | } | ||
178 | }; | ||
179 | xhr.send(null); | ||
180 | })(); | ||
181 | }); | 238 | }); |
182 | player.src({ | 239 | player.src({ |
183 | src: videojs.URL.createObjectURL(mediaSource), | 240 | src: videojs.URL.createObjectURL(mediaSource), |
... | @@ -195,4 +252,4 @@ videojs.plugin('hls', function() { | ... | @@ -195,4 +252,4 @@ videojs.plugin('hls', function() { |
195 | initialize().apply(this, arguments); | 252 | initialize().apply(this, arguments); |
196 | }); | 253 | }); |
197 | 254 | ||
198 | })(window, window.videojs); | 255 | })(window, window.videojs, document); | ... | ... |
1 | #EXTM3U | 1 | #EXTM3U |
2 | #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=200000 | 2 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000 |
3 | prog_index.m3u8 | 3 | prog_index.m3u8 |
4 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=40000 | ||
5 | prog_index1.m3u8 | ||
6 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000 | ||
7 | prog_index2.m3u8 | ||
8 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1928000 | ||
9 | prog_index3.m3u8 | ... | ... |
test/manifest/brightcove.json
0 → 100644
1 | { | ||
2 | "allowCache": true, | ||
3 | "playlists": [{ | ||
4 | "attributes": { | ||
5 | "PROGRAM-ID": 1, | ||
6 | "BANDWIDTH": 240000, | ||
7 | "RESOLUTION": { | ||
8 | "width": 396, | ||
9 | "height": 224 | ||
10 | } | ||
11 | }, | ||
12 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001" | ||
13 | }, { | ||
14 | "attributes": { | ||
15 | "PROGRAM-ID": 1, | ||
16 | "BANDWIDTH": 40000 | ||
17 | }, | ||
18 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001" | ||
19 | }, { | ||
20 | "attributes": { | ||
21 | "PROGRAM-ID": 1, | ||
22 | "BANDWIDTH": 440000, | ||
23 | "RESOLUTION": { | ||
24 | "width": 396, | ||
25 | "height": 224 | ||
26 | } | ||
27 | }, | ||
28 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001" | ||
29 | }, { | ||
30 | "attributes": { | ||
31 | "PROGRAM-ID": 1, | ||
32 | "BANDWIDTH": 1928000, | ||
33 | "RESOLUTION": { | ||
34 | "width": 960, | ||
35 | "height": 540 | ||
36 | } | ||
37 | }, | ||
38 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" | ||
39 | }] | ||
40 | } |
test/manifest/brightcove.m3u8
0 → 100644
1 | #EXTM3U | ||
2 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000,RESOLUTION=396x224 | ||
3 | http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001 | ||
4 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=40000 | ||
5 | http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001 | ||
6 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000,RESOLUTION=396x224 | ||
7 | http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001 | ||
8 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1928000,RESOLUTION=960x540 | ||
9 | http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001 |
... | @@ -10,13 +10,13 @@ | ... | @@ -10,13 +10,13 @@ |
10 | "height": 224 | 10 | "height": 224 |
11 | } | 11 | } |
12 | }, | 12 | }, |
13 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001" | 13 | "uri": "media.m3u8" |
14 | }, { | 14 | }, { |
15 | "attributes": { | 15 | "attributes": { |
16 | "PROGRAM-ID": 1, | 16 | "PROGRAM-ID": 1, |
17 | "BANDWIDTH": 40000 | 17 | "BANDWIDTH": 40000 |
18 | }, | 18 | }, |
19 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001" | 19 | "uri": "media.m3u8" |
20 | }, { | 20 | }, { |
21 | "attributes": { | 21 | "attributes": { |
22 | "PROGRAM-ID": 1, | 22 | "PROGRAM-ID": 1, |
... | @@ -26,7 +26,7 @@ | ... | @@ -26,7 +26,7 @@ |
26 | "height": 224 | 26 | "height": 224 |
27 | } | 27 | } |
28 | }, | 28 | }, |
29 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001" | 29 | "uri": "media.m3u8" |
30 | }, { | 30 | }, { |
31 | "attributes": { | 31 | "attributes": { |
32 | "PROGRAM-ID": 1, | 32 | "PROGRAM-ID": 1, |
... | @@ -36,6 +36,6 @@ | ... | @@ -36,6 +36,6 @@ |
36 | "height": 540 | 36 | "height": 540 |
37 | } | 37 | } |
38 | }, | 38 | }, |
39 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" | 39 | "uri": "media.m3u8" |
40 | }] | 40 | }] |
41 | } | 41 | } | ... | ... |
1 | # A simple master playlist with multiple variant streams | ||
1 | #EXTM3U | 2 | #EXTM3U |
2 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000,RESOLUTION=396x224 | 3 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000,RESOLUTION=396x224 |
3 | http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001 | 4 | media.m3u8 |
4 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=40000 | 5 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=40000 |
5 | http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001 | 6 | media.m3u8 |
6 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000,RESOLUTION=396x224 | 7 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000,RESOLUTION=396x224 |
7 | http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001 | 8 | media.m3u8 |
8 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1928000,RESOLUTION=960x540 | 9 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1928000,RESOLUTION=960x540 |
9 | http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001 | 10 | media.m3u8 | ... | ... |
... | @@ -63,7 +63,7 @@ module('HLS', { | ... | @@ -63,7 +63,7 @@ module('HLS', { |
63 | this.send = function() { | 63 | this.send = function() { |
64 | // 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 |
65 | // contents off the global object | 65 | // contents off the global object |
66 | var manifestName = (/.*\/(.*)\.m3u8/).exec(xhrUrls.slice(-1)[0]); | 66 | var manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(xhrUrls.slice(-1)[0]); |
67 | if (manifestName) { | 67 | if (manifestName) { |
68 | manifestName = manifestName[1]; | 68 | manifestName = manifestName[1]; |
69 | } | 69 | } |
... | @@ -99,10 +99,11 @@ test('loads the specified manifest URL on init', function() { | ... | @@ -99,10 +99,11 @@ test('loads the specified manifest URL on init', function() { |
99 | }); | 99 | }); |
100 | ok(loadedmanifest, 'loadedmanifest fires'); | 100 | ok(loadedmanifest, 'loadedmanifest fires'); |
101 | ok(loadedmetadata, 'loadedmetadata fires'); | 101 | ok(loadedmetadata, 'loadedmetadata fires'); |
102 | ok(player.hls.manifest, 'the manifest is available'); | 102 | ok(player.hls.master, 'a master is inferred'); |
103 | ok(player.hls.manifest.segments, 'the segment entries are parsed'); | 103 | ok(player.hls.media, 'the manifest is available'); |
104 | strictEqual(player.hls.manifest, | 104 | ok(player.hls.media.segments, 'the segment entries are parsed'); |
105 | player.hls.currentPlaylist, | 105 | strictEqual(player.hls.master.playlists[0], |
106 | player.hls.media, | ||
106 | 'the playlist is selected'); | 107 | 'the playlist is selected'); |
107 | strictEqual(player.hls.readyState(), 1, 'the readyState is HAVE_METADATA'); | 108 | strictEqual(player.hls.readyState(), 1, 'the readyState is HAVE_METADATA'); |
108 | }); | 109 | }); |
... | @@ -116,7 +117,11 @@ test('starts downloading a segment on loadedmetadata', function() { | ... | @@ -116,7 +117,11 @@ test('starts downloading a segment on loadedmetadata', function() { |
116 | type: 'sourceopen' | 117 | type: 'sourceopen' |
117 | }); | 118 | }); |
118 | 119 | ||
119 | strictEqual(xhrUrls[1], 'manifest/00001.ts', 'the first segment is requested'); | 120 | strictEqual(xhrUrls[1], |
121 | window.location.origin + | ||
122 | window.location.pathname.split('/').slice(0, -1).join('/') + | ||
123 | '/manifest/00001.ts', | ||
124 | 'the first segment is requested'); | ||
120 | }); | 125 | }); |
121 | 126 | ||
122 | test('recognizes absolute URIs and requests them unmodified', function() { | 127 | test('recognizes absolute URIs and requests them unmodified', function() { |
... | @@ -151,6 +156,26 @@ test('re-initializes the plugin for each source', function() { | ... | @@ -151,6 +156,26 @@ test('re-initializes the plugin for each source', function() { |
151 | notStrictEqual(firstInit, secondInit, 'the plugin object is replaced'); | 156 | notStrictEqual(firstInit, secondInit, 'the plugin object is replaced'); |
152 | }); | 157 | }); |
153 | 158 | ||
159 | test('downloads media playlists after loading the master', function() { | ||
160 | player.hls('manifest/master.m3u8'); | ||
161 | videojs.mediaSources[player.currentSrc()].trigger({ | ||
162 | type: 'sourceopen' | ||
163 | }); | ||
164 | |||
165 | strictEqual(xhrUrls.length, 3, 'three requests were made'); | ||
166 | strictEqual(xhrUrls[0], 'manifest/master.m3u8', 'master playlist requested'); | ||
167 | strictEqual(xhrUrls[1], | ||
168 | window.location.origin + | ||
169 | window.location.pathname.split('/').slice(0, -1).join('/') + | ||
170 | '/manifest/media.m3u8', | ||
171 | 'media playlist requested'); | ||
172 | strictEqual(xhrUrls[2], | ||
173 | window.location.origin + | ||
174 | window.location.pathname.split('/').slice(0, -1).join('/') + | ||
175 | '/manifest/00001.ts', | ||
176 | 'first segment requested'); | ||
177 | }); | ||
178 | |||
154 | test('calculates the bandwidth after downloading a segment', function() { | 179 | test('calculates the bandwidth after downloading a segment', function() { |
155 | player.hls('manifest/media.m3u8'); | 180 | player.hls('manifest/media.m3u8'); |
156 | videojs.mediaSources[player.currentSrc()].trigger({ | 181 | videojs.mediaSources[player.currentSrc()].trigger({ |
... | @@ -195,7 +220,11 @@ test('downloads the next segment if the buffer is getting low', function() { | ... | @@ -195,7 +220,11 @@ test('downloads the next segment if the buffer is getting low', function() { |
195 | player.trigger('timeupdate'); | 220 | player.trigger('timeupdate'); |
196 | 221 | ||
197 | strictEqual(xhrUrls.length, 3, 'made a request'); | 222 | strictEqual(xhrUrls.length, 3, 'made a request'); |
198 | strictEqual(xhrUrls[2], 'manifest/00002.ts', 'made segment request'); | 223 | strictEqual(xhrUrls[2], |
224 | window.location.origin + | ||
225 | window.location.pathname.split('/').slice(0, -1).join('/') + | ||
226 | '/manifest/00002.ts', | ||
227 | 'made segment request'); | ||
199 | }); | 228 | }); |
200 | 229 | ||
201 | test('stops downloading segments at the end of the playlist', function() { | 230 | test('stops downloading segments at the end of the playlist', function() { |
... | @@ -204,7 +233,7 @@ test('stops downloading segments at the end of the playlist', function() { | ... | @@ -204,7 +233,7 @@ test('stops downloading segments at the end of the playlist', function() { |
204 | type: 'sourceopen' | 233 | type: 'sourceopen' |
205 | }); | 234 | }); |
206 | xhrUrls = []; | 235 | xhrUrls = []; |
207 | player.hls.currentMediaIndex = 4; | 236 | player.hls.mediaIndex = 4; |
208 | player.trigger('timeupdate'); | 237 | player.trigger('timeupdate'); |
209 | 238 | ||
210 | strictEqual(xhrUrls.length, 0, 'no request is made'); | 239 | strictEqual(xhrUrls.length, 0, 'no request is made'); | ... | ... |
-
Please register or sign in to post a comment