Merge pull request #117 from heff/reorg-w-114
Reorg and endOfStream fix
Showing
8 changed files
with
740 additions
and
759 deletions
... | @@ -16,6 +16,7 @@ | ... | @@ -16,6 +16,7 @@ |
16 | <script src="src/videojs-hls.js"></script> | 16 | <script src="src/videojs-hls.js"></script> |
17 | 17 | ||
18 | <!-- segment handling --> | 18 | <!-- segment handling --> |
19 | <script src="src/xhr.js"></script> | ||
19 | <script src="src/flv-tag.js"></script> | 20 | <script src="src/flv-tag.js"></script> |
20 | <script src="src/exp-golomb.js"></script> | 21 | <script src="src/exp-golomb.js"></script> |
21 | <script src="src/h264-stream.js"></script> | 22 | <script src="src/h264-stream.js"></script> | ... | ... |
1 | /* | 1 | /* |
2 | * video-js-hls | 2 | * videojs-hls |
3 | * | 3 | * |
4 | * | 4 | * Copyright (c) 2014 Brightcove |
5 | * Copyright (c) 2013 Brightcove | ||
6 | * All rights reserved. | 5 | * All rights reserved. |
7 | */ | 6 | */ |
8 | 7 | ||
... | @@ -10,206 +9,28 @@ | ... | @@ -10,206 +9,28 @@ |
10 | 'use strict'; | 9 | 'use strict'; |
11 | 10 | ||
12 | var | 11 | var |
13 | |||
14 | // a fudge factor to apply to advertised playlist bitrates to account for | 12 | // a fudge factor to apply to advertised playlist bitrates to account for |
15 | // temporary flucations in client bandwidth | 13 | // temporary flucations in client bandwidth |
16 | bandwidthVariance = 1.1, | 14 | bandwidthVariance = 1.1, |
15 | resolveUrl; | ||
17 | 16 | ||
18 | /** | 17 | videojs.Hls = videojs.Flash.extend({ |
19 | * A comparator function to sort two playlist object by bandwidth. | 18 | init: function(player, options, ready) { |
20 | * @param left {object} a media playlist object | ||
21 | * @param right {object} a media playlist object | ||
22 | * @return {number} Greater than zero if the bandwidth attribute of | ||
23 | * left is greater than the corresponding attribute of right. Less | ||
24 | * than zero if the bandwidth of right is greater than left and | ||
25 | * exactly zero if the two are equal. | ||
26 | */ | ||
27 | playlistBandwidth = function(left, right) { | ||
28 | var leftBandwidth, rightBandwidth; | ||
29 | if (left.attributes && left.attributes.BANDWIDTH) { | ||
30 | leftBandwidth = left.attributes.BANDWIDTH; | ||
31 | } | ||
32 | leftBandwidth = leftBandwidth || window.Number.MAX_VALUE; | ||
33 | if (right.attributes && right.attributes.BANDWIDTH) { | ||
34 | rightBandwidth = right.attributes.BANDWIDTH; | ||
35 | } | ||
36 | rightBandwidth = rightBandwidth || window.Number.MAX_VALUE; | ||
37 | |||
38 | return leftBandwidth - rightBandwidth; | ||
39 | }, | ||
40 | |||
41 | /** | ||
42 | * A comparator function to sort two playlist object by resolution (width). | ||
43 | * @param left {object} a media playlist object | ||
44 | * @param right {object} a media playlist object | ||
45 | * @return {number} Greater than zero if the resolution.width attribute of | ||
46 | * left is greater than the corresponding attribute of right. Less | ||
47 | * than zero if the resolution.width of right is greater than left and | ||
48 | * exactly zero if the two are equal. | ||
49 | */ | ||
50 | playlistResolution = function(left, right) { | ||
51 | var leftWidth, rightWidth; | ||
52 | |||
53 | if (left.attributes && left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) { | ||
54 | leftWidth = left.attributes.RESOLUTION.width; | ||
55 | } | ||
56 | |||
57 | leftWidth = leftWidth || window.Number.MAX_VALUE; | ||
58 | |||
59 | if (right.attributes && right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) { | ||
60 | rightWidth = right.attributes.RESOLUTION.width; | ||
61 | } | ||
62 | |||
63 | rightWidth = rightWidth || window.Number.MAX_VALUE; | ||
64 | |||
65 | // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions | ||
66 | // have the same media dimensions/ resolution | ||
67 | if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) { | ||
68 | return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH; | ||
69 | } else { | ||
70 | return leftWidth - rightWidth; | ||
71 | } | ||
72 | }, | ||
73 | |||
74 | xhr, | ||
75 | |||
76 | /** | ||
77 | * TODO - Document this great feature. | ||
78 | * | ||
79 | * @param playlist | ||
80 | * @param time | ||
81 | * @returns int | ||
82 | */ | ||
83 | getMediaIndexByTime = function(playlist, time) { | ||
84 | var index, counter, timeRanges, currentSegmentRange; | ||
85 | |||
86 | timeRanges = []; | ||
87 | for (index = 0; index < playlist.segments.length; index++) { | ||
88 | currentSegmentRange = {}; | ||
89 | currentSegmentRange.start = (index === 0) ? 0 : timeRanges[index - 1].end; | ||
90 | currentSegmentRange.end = currentSegmentRange.start + playlist.segments[index].duration; | ||
91 | timeRanges.push(currentSegmentRange); | ||
92 | } | ||
93 | |||
94 | for (counter = 0; counter < timeRanges.length; counter++) { | ||
95 | if (time >= timeRanges[counter].start && time < timeRanges[counter].end) { | ||
96 | return counter; | ||
97 | } | ||
98 | } | ||
99 | |||
100 | return -1; | ||
101 | |||
102 | }, | ||
103 | |||
104 | /** | ||
105 | * Determine the media index in one playlist that corresponds to a | ||
106 | * specified media index in another. This function can be used to | ||
107 | * calculate a new segment position when a playlist is reloaded or a | ||
108 | * variant playlist is becoming active. | ||
109 | * @param mediaIndex {number} the index into the original playlist | ||
110 | * to translate | ||
111 | * @param original {object} the playlist to translate the media | ||
112 | * index from | ||
113 | * @param update {object} the playlist to translate the media index | ||
114 | * to | ||
115 | * @param {number} the corresponding media index in the updated | ||
116 | * playlist | ||
117 | */ | ||
118 | translateMediaIndex = function(mediaIndex, original, update) { | ||
119 | var | ||
120 | i, | ||
121 | originalSegment; | ||
122 | |||
123 | // no segments have been loaded from the original playlist | ||
124 | if (mediaIndex === 0) { | ||
125 | return 0; | ||
126 | } | ||
127 | if (!(update && update.segments)) { | ||
128 | // let the media index be zero when there are no segments defined | ||
129 | return 0; | ||
130 | } | ||
131 | |||
132 | // try to sync based on URI | ||
133 | i = update.segments.length; | ||
134 | originalSegment = original.segments[mediaIndex - 1]; | ||
135 | while (i--) { | ||
136 | if (originalSegment.uri === update.segments[i].uri) { | ||
137 | return i + 1; | ||
138 | } | ||
139 | } | ||
140 | |||
141 | // sync on media sequence | ||
142 | return (original.mediaSequence + mediaIndex) - update.mediaSequence; | ||
143 | }, | ||
144 | |||
145 | /** | ||
146 | * Calculate the duration of a playlist from a given start index to a given | ||
147 | * end index. | ||
148 | * @param playlist {object} a media playlist object | ||
149 | * @param startIndex {number} an inclusive lower boundary for the playlist. | ||
150 | * Defaults to 0. | ||
151 | * @param endIndex {number} an exclusive upper boundary for the playlist. | ||
152 | * Defaults to playlist length. | ||
153 | * @return {number} the duration between the start index and end index. | ||
154 | */ | ||
155 | duration = function(playlist, startIndex, endIndex) { | ||
156 | var dur = 0, | ||
157 | segment, | ||
158 | i; | ||
159 | |||
160 | startIndex = startIndex || 0; | ||
161 | endIndex = endIndex !== undefined ? endIndex : (playlist.segments || []).length; | ||
162 | i = endIndex - 1; | ||
163 | |||
164 | for (; i >= startIndex; i--) { | ||
165 | segment = playlist.segments[i]; | ||
166 | dur += segment.duration || playlist.targetDuration || 0; | ||
167 | } | ||
168 | |||
169 | return dur; | ||
170 | }, | ||
171 | |||
172 | /** | ||
173 | * Calculate the total duration for a playlist based on segment metadata. | ||
174 | * @param playlist {object} a media playlist object | ||
175 | * @return {number} the currently known duration, in seconds | ||
176 | */ | ||
177 | totalDuration = function(playlist) { | ||
178 | if (!playlist) { | ||
179 | return 0; | ||
180 | } | ||
181 | |||
182 | // if present, use the duration specified in the playlist | ||
183 | if (playlist.totalDuration) { | ||
184 | return playlist.totalDuration; | ||
185 | } | ||
186 | |||
187 | // duration should be Infinity for live playlists | ||
188 | if (!playlist.endList) { | ||
189 | return window.Infinity; | ||
190 | } | ||
191 | |||
192 | return duration(playlist); | ||
193 | }, | ||
194 | |||
195 | resolveUrl, | ||
196 | |||
197 | initSource = function(player, mediaSource, srcUrl) { | ||
198 | var | 19 | var |
199 | segmentParser = new videojs.Hls.SegmentParser(), | 20 | source = options.source, |
200 | settings = videojs.util.mergeOptions({}, player.options().hls), | 21 | settings = player.options(); |
201 | segmentBuffer = [], | ||
202 | |||
203 | lastSeekedTime, | ||
204 | segmentXhr, | ||
205 | fillBuffer, | ||
206 | drainBuffer, | ||
207 | updateDuration; | ||
208 | 22 | ||
23 | player.hls = this; | ||
24 | delete options.source; | ||
25 | options.swf = settings.flash.swf; | ||
26 | videojs.Flash.call(this, player, options, ready); | ||
27 | options.source = source; | ||
28 | this.bytesReceived = 0; | ||
209 | 29 | ||
210 | player.hls.currentTime = function() { | 30 | // TODO: After video.js#1347 is pulled in move these to the prototype |
211 | if (lastSeekedTime) { | 31 | this.currentTime = function() { |
212 | return lastSeekedTime; | 32 | if (this.lastSeekedTime_) { |
33 | return this.lastSeekedTime_; | ||
213 | } | 34 | } |
214 | // currentTime is zero while the tech is initializing | 35 | // currentTime is zero while the tech is initializing |
215 | if (!this.el() || !this.el().vjs_getProperty) { | 36 | if (!this.el() || !this.el().vjs_getProperty) { |
... | @@ -217,8 +38,7 @@ var | ... | @@ -217,8 +38,7 @@ var |
217 | } | 38 | } |
218 | return this.el().vjs_getProperty('currentTime'); | 39 | return this.el().vjs_getProperty('currentTime'); |
219 | }; | 40 | }; |
220 | 41 | this.setCurrentTime = function(currentTime) { | |
221 | player.hls.setCurrentTime = function(currentTime) { | ||
222 | if (!(this.playlists && this.playlists.media())) { | 42 | if (!(this.playlists && this.playlists.media())) { |
223 | // return immediately if the metadata is not ready yet | 43 | // return immediately if the metadata is not ready yet |
224 | return 0; | 44 | return 0; |
... | @@ -226,384 +46,437 @@ var | ... | @@ -226,384 +46,437 @@ var |
226 | 46 | ||
227 | // save the seek target so currentTime can report it correctly | 47 | // save the seek target so currentTime can report it correctly |
228 | // while the seek is pending | 48 | // while the seek is pending |
229 | lastSeekedTime = currentTime; | 49 | this.lastSeekedTime_ = currentTime; |
230 | 50 | ||
231 | // determine the requested segment | 51 | // determine the requested segment |
232 | this.mediaIndex = | 52 | this.mediaIndex = videojs.Hls.getMediaIndexByTime(this.playlists.media(), currentTime); |
233 | getMediaIndexByTime(this.playlists.media(), currentTime); | ||
234 | 53 | ||
235 | // abort any segments still being decoded | 54 | // abort any segments still being decoded |
236 | this.sourceBuffer.abort(); | 55 | this.sourceBuffer.abort(); |
237 | 56 | ||
238 | // cancel outstanding requests and buffer appends | 57 | // cancel outstanding requests and buffer appends |
239 | if (segmentXhr) { | 58 | if (this.segmentXhr_) { |
240 | segmentXhr.abort(); | 59 | this.segmentXhr_.abort(); |
241 | } | 60 | } |
242 | 61 | ||
243 | // clear out any buffered segments | 62 | // clear out any buffered segments |
244 | segmentBuffer = []; | 63 | this.segmentBuffer_ = []; |
245 | 64 | ||
246 | // begin filling the buffer at the new position | 65 | // begin filling the buffer at the new position |
247 | fillBuffer(currentTime * 1000); | 66 | this.fillBuffer(currentTime * 1000); |
248 | }; | 67 | }; |
249 | 68 | ||
250 | /** | 69 | videojs.Hls.prototype.src.call(this, options.source && options.source.src); |
251 | * Update the player duration | 70 | } |
252 | */ | 71 | }); |
253 | updateDuration = function(playlist) { | ||
254 | var oldDuration = player.duration(), | ||
255 | newDuration = totalDuration(playlist); | ||
256 | 72 | ||
257 | // if the duration has changed, invalidate the cached value | 73 | // Add HLS to the standard tech order |
258 | if (oldDuration !== newDuration) { | 74 | videojs.options.techOrder.unshift('hls'); |
259 | player.trigger('durationchange'); | ||
260 | } | ||
261 | }; | ||
262 | 75 | ||
263 | /** | 76 | // the desired length of video to maintain in the buffer, in seconds |
264 | * Chooses the appropriate media playlist based on the current | 77 | videojs.Hls.GOAL_BUFFER_LENGTH = 30; |
265 | * bandwidth estimate and the player size. | ||
266 | * @return the highest bitrate playlist less than the currently detected | ||
267 | * bandwidth, accounting for some amount of bandwidth variance | ||
268 | */ | ||
269 | player.hls.selectPlaylist = function () { | ||
270 | var | ||
271 | effectiveBitrate, | ||
272 | sortedPlaylists = player.hls.playlists.master.playlists.slice(), | ||
273 | bandwidthPlaylists = [], | ||
274 | i = sortedPlaylists.length, | ||
275 | variant, | ||
276 | bandwidthBestVariant, | ||
277 | resolutionBestVariant; | ||
278 | |||
279 | sortedPlaylists.sort(playlistBandwidth); | ||
280 | |||
281 | // filter out any variant that has greater effective bitrate | ||
282 | // than the current estimated bandwidth | ||
283 | while (i--) { | ||
284 | variant = sortedPlaylists[i]; | ||
285 | |||
286 | // ignore playlists without bandwidth information | ||
287 | if (!variant.attributes || !variant.attributes.BANDWIDTH) { | ||
288 | continue; | ||
289 | } | ||
290 | |||
291 | effectiveBitrate = variant.attributes.BANDWIDTH * bandwidthVariance; | ||
292 | |||
293 | if (effectiveBitrate < player.hls.bandwidth) { | ||
294 | bandwidthPlaylists.push(variant); | ||
295 | |||
296 | // since the playlists are sorted in ascending order by | ||
297 | // bandwidth, the first viable variant is the best | ||
298 | if (!bandwidthBestVariant) { | ||
299 | bandwidthBestVariant = variant; | ||
300 | } | ||
301 | } | ||
302 | } | ||
303 | 78 | ||
304 | i = bandwidthPlaylists.length; | 79 | videojs.Hls.prototype.src = function(src) { |
305 | 80 | var | |
306 | // sort variants by resolution | 81 | self = this, |
307 | bandwidthPlaylists.sort(playlistResolution); | 82 | mediaSource, |
308 | 83 | source; | |
309 | // iterate through the bandwidth-filtered playlists and find | ||
310 | // best rendition by player dimension | ||
311 | while (i--) { | ||
312 | variant = bandwidthPlaylists[i]; | ||
313 | |||
314 | // ignore playlists without resolution information | ||
315 | if (!variant.attributes || | ||
316 | !variant.attributes.RESOLUTION || | ||
317 | !variant.attributes.RESOLUTION.width || | ||
318 | !variant.attributes.RESOLUTION.height) { | ||
319 | continue; | ||
320 | } | ||
321 | |||
322 | // since the playlists are sorted, the first variant that has | ||
323 | // dimensions less than or equal to the player size is the | ||
324 | // best | ||
325 | if (variant.attributes.RESOLUTION.width <= player.width() && | ||
326 | variant.attributes.RESOLUTION.height <= player.height()) { | ||
327 | resolutionBestVariant = variant; | ||
328 | break; | ||
329 | } | ||
330 | } | ||
331 | 84 | ||
332 | // fallback chain of variants | 85 | if (src) { |
333 | return resolutionBestVariant || bandwidthBestVariant || sortedPlaylists[0]; | 86 | this.src_ = src; |
334 | }; | ||
335 | 87 | ||
336 | /** | 88 | mediaSource = new videojs.MediaSource(); |
337 | * Abort all outstanding work and cleanup. | 89 | source = { |
338 | */ | 90 | src: videojs.URL.createObjectURL(mediaSource), |
339 | player.hls.dispose = function() { | 91 | type: "video/flv" |
340 | if (segmentXhr) { | ||
341 | segmentXhr.onreadystatechange = null; | ||
342 | segmentXhr.abort(); | ||
343 | } | ||
344 | if (this.playlists) { | ||
345 | this.playlists.dispose(); | ||
346 | } | ||
347 | videojs.Flash.prototype.dispose.call(this); | ||
348 | }; | 92 | }; |
93 | this.mediaSource = mediaSource; | ||
94 | |||
95 | this.segmentBuffer_ = []; | ||
96 | this.segmentParser_ = new videojs.Hls.SegmentParser(); | ||
349 | 97 | ||
350 | /** | 98 | // load the MediaSource into the player |
351 | * Determines whether there is enough video data currently in the buffer | 99 | this.mediaSource.addEventListener('sourceopen', videojs.bind(this, this.handleSourceOpen)); |
352 | * and downloads a new segment if the buffered time is less than the goal. | ||
353 | * @param offset (optional) {number} the offset into the downloaded segment | ||
354 | * to seek to, in milliseconds | ||
355 | */ | ||
356 | fillBuffer = function(offset) { | ||
357 | var | ||
358 | buffered = player.buffered(), | ||
359 | bufferedTime = 0, | ||
360 | segment, | ||
361 | segmentUri, | ||
362 | startTime; | ||
363 | |||
364 | // if there is a request already in flight, do nothing | ||
365 | if (segmentXhr) { | ||
366 | return; | ||
367 | } | ||
368 | 100 | ||
369 | // if no segments are available, do nothing | 101 | this.player().ready(function() { |
370 | if (player.hls.playlists.state === "HAVE_NOTHING" || | 102 | // do nothing if the tech has been disposed already |
371 | !player.hls.playlists.media().segments) { | 103 | // this can occur if someone sets the src in player.ready(), for instance |
104 | if (!self.el()) { | ||
372 | return; | 105 | return; |
373 | } | 106 | } |
107 | self.el().vjs_src(source.src); | ||
108 | }); | ||
109 | } | ||
110 | }; | ||
374 | 111 | ||
375 | // if the video has finished downloading, stop trying to buffer | 112 | videojs.Hls.prototype.handleSourceOpen = function() { |
376 | segment = player.hls.playlists.media().segments[player.hls.mediaIndex]; | 113 | // construct the video data buffer and set the appropriate MIME type |
377 | if (!segment) { | 114 | var |
378 | return; | 115 | player = this.player(), |
379 | } | 116 | settings = player.options().hls || {}, |
117 | sourceBuffer = this.mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"'), | ||
118 | oldMediaPlaylist; | ||
380 | 119 | ||
381 | if (buffered) { | 120 | this.sourceBuffer = sourceBuffer; |
382 | // assuming a single, contiguous buffer region | 121 | sourceBuffer.appendBuffer(this.segmentParser_.getFlvHeader()); |
383 | bufferedTime = player.buffered().end(0) - player.currentTime(); | ||
384 | } | ||
385 | 122 | ||
386 | // if there is plenty of content in the buffer and we're not | 123 | this.mediaIndex = 0; |
387 | // seeking, relax for awhile | 124 | this.playlists = new videojs.Hls.PlaylistLoader(this.src_, settings.withCredentials); |
388 | if (typeof offset !== 'number' && | ||
389 | bufferedTime >= videojs.Hls.GOAL_BUFFER_LENGTH) { | ||
390 | return; | ||
391 | } | ||
392 | 125 | ||
393 | // resolve the segment URL relative to the playlist | 126 | this.playlists.on('loadedmetadata', videojs.bind(this, function() { |
394 | if (player.hls.playlists.media().uri === srcUrl) { | 127 | oldMediaPlaylist = this.playlists.media(); |
395 | segmentUri = resolveUrl(srcUrl, segment.uri); | ||
396 | } else { | ||
397 | segmentUri = resolveUrl(resolveUrl(srcUrl, player.hls.playlists.media().uri || ''), | ||
398 | segment.uri); | ||
399 | } | ||
400 | 128 | ||
401 | startTime = +new Date(); | 129 | // periodically check if new data needs to be downloaded or |
402 | 130 | // buffered data should be appended to the source buffer | |
403 | // request the next segment | 131 | this.fillBuffer(); |
404 | segmentXhr = xhr({ | 132 | player.on('timeupdate', videojs.bind(this, this.fillBuffer)); |
405 | url: segmentUri, | 133 | player.on('timeupdate', videojs.bind(this, this.drainBuffer)); |
406 | responseType: 'arraybuffer', | 134 | player.on('waiting', videojs.bind(this, this.drainBuffer)); |
407 | withCredentials: settings.withCredentials | ||
408 | }, function(error, url) { | ||
409 | var tags; | ||
410 | |||
411 | // the segment request is no longer outstanding | ||
412 | segmentXhr = null; | ||
413 | |||
414 | if (error) { | ||
415 | // if a segment request times out, we may have better luck with another playlist | ||
416 | if (error === 'timeout') { | ||
417 | player.hls.bandwidth = 1; | ||
418 | return player.hls.playlists.media(player.hls.selectPlaylist()); | ||
419 | } | ||
420 | // otherwise, try jumping ahead to the next segment | ||
421 | player.hls.error = { | ||
422 | status: this.status, | ||
423 | message: 'HLS segment request error at URL: ' + url, | ||
424 | code: (this.status >= 500) ? 4 : 2 | ||
425 | }; | ||
426 | |||
427 | // try moving on to the next segment | ||
428 | player.hls.mediaIndex++; | ||
429 | return; | ||
430 | } | ||
431 | |||
432 | // stop processing if the request was aborted | ||
433 | if (!this.response) { | ||
434 | return; | ||
435 | } | ||
436 | |||
437 | // calculate the download bandwidth | ||
438 | player.hls.segmentXhrTime = (+new Date()) - startTime; | ||
439 | player.hls.bandwidth = (this.response.byteLength / player.hls.segmentXhrTime) * 8 * 1000; | ||
440 | player.hls.bytesReceived += this.response.byteLength; | ||
441 | |||
442 | // transmux the segment data from MP2T to FLV | ||
443 | segmentParser.parseSegmentBinaryData(new Uint8Array(this.response)); | ||
444 | segmentParser.flushTags(); | ||
445 | |||
446 | // package up all the work to append the segment | ||
447 | // if the segment is the start of a timestamp discontinuity, | ||
448 | // we have to wait until the sourcebuffer is empty before | ||
449 | // aborting the source buffer processing | ||
450 | tags = []; | ||
451 | while (segmentParser.tagsAvailable()) { | ||
452 | tags.push(segmentParser.getNextTag()); | ||
453 | } | ||
454 | segmentBuffer.push({ | ||
455 | mediaIndex: player.hls.mediaIndex, | ||
456 | playlist: player.hls.playlists.media(), | ||
457 | offset: offset, | ||
458 | tags: tags | ||
459 | }); | ||
460 | drainBuffer(); | ||
461 | |||
462 | player.hls.mediaIndex++; | ||
463 | |||
464 | // figure out what stream the next segment should be downloaded from | ||
465 | // with the updated bandwidth information | ||
466 | player.hls.playlists.media(player.hls.selectPlaylist()); | ||
467 | }); | ||
468 | }; | ||
469 | 135 | ||
470 | drainBuffer = function(event) { | 136 | player.trigger('loadedmetadata'); |
471 | var | 137 | })); |
472 | i = 0, | ||
473 | mediaIndex, | ||
474 | playlist, | ||
475 | offset, | ||
476 | tags, | ||
477 | segment, | ||
478 | 138 | ||
479 | ptsTime, | 139 | this.playlists.on('error', videojs.bind(this, function() { |
480 | segmentOffset; | 140 | player.error(this.playlists.error); |
141 | })); | ||
481 | 142 | ||
482 | if (!segmentBuffer.length) { | 143 | this.playlists.on('loadedplaylist', videojs.bind(this, function() { |
483 | return; | 144 | var updatedPlaylist = this.playlists.media(); |
484 | } | ||
485 | 145 | ||
486 | mediaIndex = segmentBuffer[0].mediaIndex; | 146 | if (!updatedPlaylist) { |
487 | playlist = segmentBuffer[0].playlist; | 147 | // do nothing before an initial media playlist has been activated |
488 | offset = segmentBuffer[0].offset; | 148 | return; |
489 | tags = segmentBuffer[0].tags; | 149 | } |
490 | segment = playlist.segments[mediaIndex]; | ||
491 | |||
492 | event = event || {}; | ||
493 | segmentOffset = duration(playlist, 0, mediaIndex) * 1000; | ||
494 | |||
495 | // abort() clears any data queued in the source buffer so wait | ||
496 | // until it empties before calling it when a discontinuity is | ||
497 | // next in the buffer | ||
498 | if (segment.discontinuity) { | ||
499 | if (event.type !== 'waiting') { | ||
500 | return; | ||
501 | } | ||
502 | player.hls.sourceBuffer.abort(); | ||
503 | // tell the SWF where playback is continuing in the stitched timeline | ||
504 | player.hls.el().vjs_setProperty('currentTime', segmentOffset * 0.001); | ||
505 | } | ||
506 | 150 | ||
507 | // if we're refilling the buffer after a seek, scan through the muxed | 151 | this.updateDuration(this.playlists.media()); |
508 | // FLV tags until we find the one that is closest to the desired | 152 | this.mediaIndex = videojs.Hls.translateMediaIndex(this.mediaIndex, oldMediaPlaylist, updatedPlaylist); |
509 | // playback time | 153 | oldMediaPlaylist = updatedPlaylist; |
510 | if (typeof offset === 'number') { | 154 | })); |
511 | ptsTime = offset - segmentOffset + tags[0].pts; | ||
512 | 155 | ||
513 | while (tags[i].pts < ptsTime) { | 156 | this.playlists.on('mediachange', function() { |
514 | i++; | 157 | player.trigger('mediachange'); |
515 | } | 158 | }); |
159 | }; | ||
516 | 160 | ||
517 | // tell the SWF where we will be seeking to | 161 | /** |
518 | player.hls.el().vjs_setProperty('currentTime', (tags[i].pts - tags[0].pts + segmentOffset) * 0.001); | 162 | * Reset the mediaIndex if play() is called after the video has |
163 | * ended. | ||
164 | */ | ||
165 | videojs.Hls.prototype.play = function() { | ||
166 | if (this.ended()) { | ||
167 | this.mediaIndex = 0; | ||
168 | } | ||
519 | 169 | ||
520 | tags = tags.slice(i); | 170 | // delegate back to the Flash implementation |
171 | return videojs.Flash.prototype.play.apply(this, arguments); | ||
172 | }; | ||
521 | 173 | ||
522 | lastSeekedTime = null; | 174 | videojs.Hls.prototype.duration = function() { |
523 | } | 175 | var playlists = this.playlists; |
176 | if (playlists) { | ||
177 | return videojs.Hls.getPlaylistTotalDuration(playlists.media()); | ||
178 | } | ||
179 | return 0; | ||
180 | }; | ||
181 | |||
182 | /** | ||
183 | * Update the player duration | ||
184 | */ | ||
185 | videojs.Hls.prototype.updateDuration = function(playlist) { | ||
186 | var player = this.player(), | ||
187 | oldDuration = player.duration(), | ||
188 | newDuration = videojs.Hls.getPlaylistTotalDuration(playlist); | ||
189 | |||
190 | // if the duration has changed, invalidate the cached value | ||
191 | if (oldDuration !== newDuration) { | ||
192 | player.trigger('durationchange'); | ||
193 | } | ||
194 | }; | ||
195 | |||
196 | /** | ||
197 | * Abort all outstanding work and cleanup. | ||
198 | */ | ||
199 | videojs.Hls.prototype.dispose = function() { | ||
200 | if (this.segmentXhr_) { | ||
201 | this.segmentXhr_.onreadystatechange = null; | ||
202 | this.segmentXhr_.abort(); | ||
203 | } | ||
204 | if (this.playlists) { | ||
205 | this.playlists.dispose(); | ||
206 | } | ||
207 | videojs.Flash.prototype.dispose.call(this); | ||
208 | }; | ||
209 | |||
210 | /** | ||
211 | * Chooses the appropriate media playlist based on the current | ||
212 | * bandwidth estimate and the player size. | ||
213 | * @return the highest bitrate playlist less than the currently detected | ||
214 | * bandwidth, accounting for some amount of bandwidth variance | ||
215 | */ | ||
216 | videojs.Hls.prototype.selectPlaylist = function () { | ||
217 | var | ||
218 | player = this.player(), | ||
219 | effectiveBitrate, | ||
220 | sortedPlaylists = this.playlists.master.playlists.slice(), | ||
221 | bandwidthPlaylists = [], | ||
222 | i = sortedPlaylists.length, | ||
223 | variant, | ||
224 | bandwidthBestVariant, | ||
225 | resolutionBestVariant; | ||
226 | |||
227 | sortedPlaylists.sort(videojs.Hls.comparePlaylistBandwidth); | ||
228 | |||
229 | // filter out any variant that has greater effective bitrate | ||
230 | // than the current estimated bandwidth | ||
231 | while (i--) { | ||
232 | variant = sortedPlaylists[i]; | ||
233 | |||
234 | // ignore playlists without bandwidth information | ||
235 | if (!variant.attributes || !variant.attributes.BANDWIDTH) { | ||
236 | continue; | ||
237 | } | ||
524 | 238 | ||
525 | for (i = 0; i < tags.length; i++) { | 239 | effectiveBitrate = variant.attributes.BANDWIDTH * bandwidthVariance; |
526 | // queue up the bytes to be appended to the SourceBuffer | ||
527 | // the queue gives control back to the browser between tags | ||
528 | // so that large segments don't cause a "hiccup" in playback | ||
529 | 240 | ||
530 | player.hls.sourceBuffer.appendBuffer(tags[i].bytes, player); | 241 | if (effectiveBitrate < player.hls.bandwidth) { |
242 | bandwidthPlaylists.push(variant); | ||
243 | |||
244 | // since the playlists are sorted in ascending order by | ||
245 | // bandwidth, the first viable variant is the best | ||
246 | if (!bandwidthBestVariant) { | ||
247 | bandwidthBestVariant = variant; | ||
531 | } | 248 | } |
249 | } | ||
250 | } | ||
251 | |||
252 | i = bandwidthPlaylists.length; | ||
253 | |||
254 | // sort variants by resolution | ||
255 | bandwidthPlaylists.sort(videojs.Hls.comparePlaylistResolution); | ||
532 | 256 | ||
533 | // we're done processing this segment | 257 | // iterate through the bandwidth-filtered playlists and find |
534 | segmentBuffer.shift(); | 258 | // best rendition by player dimension |
259 | while (i--) { | ||
260 | variant = bandwidthPlaylists[i]; | ||
535 | 261 | ||
536 | if (mediaIndex === playlist.segments.length) { | 262 | // ignore playlists without resolution information |
537 | mediaSource.endOfStream(); | 263 | if (!variant.attributes || |
264 | !variant.attributes.RESOLUTION || | ||
265 | !variant.attributes.RESOLUTION.width || | ||
266 | !variant.attributes.RESOLUTION.height) { | ||
267 | continue; | ||
268 | } | ||
269 | |||
270 | // since the playlists are sorted, the first variant that has | ||
271 | // dimensions less than or equal to the player size is the | ||
272 | // best | ||
273 | if (variant.attributes.RESOLUTION.width <= player.width() && | ||
274 | variant.attributes.RESOLUTION.height <= player.height()) { | ||
275 | resolutionBestVariant = variant; | ||
276 | break; | ||
277 | } | ||
278 | } | ||
279 | |||
280 | // fallback chain of variants | ||
281 | return resolutionBestVariant || bandwidthBestVariant || sortedPlaylists[0]; | ||
282 | }; | ||
283 | |||
284 | /** | ||
285 | * Determines whether there is enough video data currently in the buffer | ||
286 | * and downloads a new segment if the buffered time is less than the goal. | ||
287 | * @param offset (optional) {number} the offset into the downloaded segment | ||
288 | * to seek to, in milliseconds | ||
289 | */ | ||
290 | videojs.Hls.prototype.fillBuffer = function(offset) { | ||
291 | var | ||
292 | self = this, | ||
293 | player = this.player(), | ||
294 | settings = player.options().hls || {}, | ||
295 | buffered = player.buffered(), | ||
296 | bufferedTime = 0, | ||
297 | segment, | ||
298 | segmentUri, | ||
299 | startTime; | ||
300 | |||
301 | // if there is a request already in flight, do nothing | ||
302 | if (this.segmentXhr_) { | ||
303 | return; | ||
304 | } | ||
305 | |||
306 | // if no segments are available, do nothing | ||
307 | if (this.playlists.state === "HAVE_NOTHING" || | ||
308 | !this.playlists.media().segments) { | ||
309 | return; | ||
310 | } | ||
311 | |||
312 | // if the video has finished downloading, stop trying to buffer | ||
313 | segment = this.playlists.media().segments[this.mediaIndex]; | ||
314 | if (!segment) { | ||
315 | return; | ||
316 | } | ||
317 | |||
318 | if (buffered) { | ||
319 | // assuming a single, contiguous buffer region | ||
320 | bufferedTime = player.buffered().end(0) - player.currentTime(); | ||
321 | } | ||
322 | |||
323 | // if there is plenty of content in the buffer and we're not | ||
324 | // seeking, relax for awhile | ||
325 | if (typeof offset !== 'number' && | ||
326 | bufferedTime >= videojs.Hls.GOAL_BUFFER_LENGTH) { | ||
327 | return; | ||
328 | } | ||
329 | |||
330 | // resolve the segment URL relative to the playlist | ||
331 | if (this.playlists.media().uri === this.src_) { | ||
332 | segmentUri = resolveUrl(this.src_, segment.uri); | ||
333 | } else { | ||
334 | segmentUri = resolveUrl(resolveUrl(this.src_, this.playlists.media().uri || ''), | ||
335 | segment.uri); | ||
336 | } | ||
337 | |||
338 | startTime = +new Date(); | ||
339 | |||
340 | // request the next segment | ||
341 | this.segmentXhr_ = videojs.Hls.xhr({ | ||
342 | url: segmentUri, | ||
343 | responseType: 'arraybuffer', | ||
344 | withCredentials: settings.withCredentials | ||
345 | }, function(error, url) { | ||
346 | var tags; | ||
347 | |||
348 | // the segment request is no longer outstanding | ||
349 | self.segmentXhr_ = null; | ||
350 | |||
351 | if (error) { | ||
352 | // if a segment request times out, we may have better luck with another playlist | ||
353 | if (error === 'timeout') { | ||
354 | self.bandwidth = 1; | ||
355 | return self.playlists.media(self.selectPlaylist()); | ||
538 | } | 356 | } |
539 | }; | 357 | // otherwise, try jumping ahead to the next segment |
358 | self.error = { | ||
359 | status: this.status, | ||
360 | message: 'HLS segment request error at URL: ' + url, | ||
361 | code: (this.status >= 500) ? 4 : 2 | ||
362 | }; | ||
540 | 363 | ||
541 | // load the MediaSource into the player | 364 | // try moving on to the next segment |
542 | mediaSource.addEventListener('sourceopen', function() { | 365 | self.mediaIndex++; |
543 | // construct the video data buffer and set the appropriate MIME type | 366 | return; |
544 | var | 367 | } |
545 | sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"'), | 368 | |
546 | oldMediaPlaylist; | 369 | // stop processing if the request was aborted |
547 | 370 | if (!this.response) { | |
548 | player.hls.sourceBuffer = sourceBuffer; | 371 | return; |
549 | sourceBuffer.appendBuffer(segmentParser.getFlvHeader()); | 372 | } |
550 | 373 | ||
551 | player.hls.mediaIndex = 0; | 374 | // calculate the download bandwidth |
552 | player.hls.playlists = | 375 | self.segmentXhrTime = (+new Date()) - startTime; |
553 | new videojs.Hls.PlaylistLoader(srcUrl, settings.withCredentials); | 376 | self.bandwidth = (this.response.byteLength / player.hls.segmentXhrTime) * 8 * 1000; |
554 | player.hls.playlists.on('loadedmetadata', function() { | 377 | self.bytesReceived += this.response.byteLength; |
555 | oldMediaPlaylist = player.hls.playlists.media(); | 378 | |
556 | 379 | // transmux the segment data from MP2T to FLV | |
557 | // periodically check if new data needs to be downloaded or | 380 | self.segmentParser_.parseSegmentBinaryData(new Uint8Array(this.response)); |
558 | // buffered data should be appended to the source buffer | 381 | self.segmentParser_.flushTags(); |
559 | fillBuffer(); | 382 | |
560 | player.on('timeupdate', fillBuffer); | 383 | // package up all the work to append the segment |
561 | player.on('timeupdate', drainBuffer); | 384 | // if the segment is the start of a timestamp discontinuity, |
562 | player.on('waiting', drainBuffer); | 385 | // we have to wait until the sourcebuffer is empty before |
563 | 386 | // aborting the source buffer processing | |
564 | player.trigger('loadedmetadata'); | 387 | tags = []; |
565 | }); | 388 | while (self.segmentParser_.tagsAvailable()) { |
566 | player.hls.playlists.on('error', function() { | 389 | tags.push(self.segmentParser_.getNextTag()); |
567 | player.error(player.hls.playlists.error); | 390 | } |
568 | }); | 391 | self.segmentBuffer_.push({ |
569 | player.hls.playlists.on('loadedplaylist', function() { | 392 | mediaIndex: self.mediaIndex, |
570 | var updatedPlaylist = player.hls.playlists.media(); | 393 | playlist: self.playlists.media(), |
571 | 394 | offset: offset, | |
572 | if (!updatedPlaylist) { | 395 | tags: tags |
573 | // do nothing before an initial media playlist has been activated | ||
574 | return; | ||
575 | } | ||
576 | |||
577 | updateDuration(player.hls.playlists.media()); | ||
578 | player.hls.mediaIndex = translateMediaIndex(player.hls.mediaIndex, | ||
579 | oldMediaPlaylist, | ||
580 | updatedPlaylist); | ||
581 | oldMediaPlaylist = updatedPlaylist; | ||
582 | }); | ||
583 | player.hls.playlists.on('mediachange', function() { | ||
584 | player.trigger('mediachange'); | ||
585 | }); | ||
586 | }); | 396 | }); |
587 | }; | 397 | self.drainBuffer(); |
588 | 398 | ||
589 | var mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i; | 399 | self.mediaIndex++; |
590 | 400 | ||
591 | videojs.Hls = videojs.Flash.extend({ | 401 | // figure out what stream the next segment should be downloaded from |
592 | init: function(player, options, ready) { | 402 | // with the updated bandwidth information |
593 | var | 403 | self.playlists.media(self.selectPlaylist()); |
594 | source = options.source, | 404 | }); |
595 | settings = player.options(); | 405 | }; |
596 | 406 | ||
597 | player.hls = this; | 407 | videojs.Hls.prototype.drainBuffer = function(event) { |
598 | delete options.source; | 408 | var |
599 | options.swf = settings.flash.swf; | 409 | i = 0, |
600 | videojs.Flash.call(this, player, options, ready); | 410 | mediaIndex, |
601 | options.source = source; | 411 | playlist, |
602 | this.bytesReceived = 0; | 412 | offset, |
413 | tags, | ||
414 | segment, | ||
415 | |||
416 | ptsTime, | ||
417 | segmentOffset, | ||
418 | segmentBuffer = this.segmentBuffer_; | ||
419 | |||
420 | if (!segmentBuffer.length) { | ||
421 | return; | ||
422 | } | ||
603 | 423 | ||
604 | videojs.Hls.prototype.src.call(this, options.source && options.source.src); | 424 | mediaIndex = segmentBuffer[0].mediaIndex; |
425 | playlist = segmentBuffer[0].playlist; | ||
426 | offset = segmentBuffer[0].offset; | ||
427 | tags = segmentBuffer[0].tags; | ||
428 | segment = playlist.segments[mediaIndex]; | ||
429 | |||
430 | event = event || {}; | ||
431 | segmentOffset = videojs.Hls.getPlaylistDuration(playlist, 0, mediaIndex) * 1000; | ||
432 | |||
433 | // abort() clears any data queued in the source buffer so wait | ||
434 | // until it empties before calling it when a discontinuity is | ||
435 | // next in the buffer | ||
436 | if (segment.discontinuity) { | ||
437 | if (event.type !== 'waiting') { | ||
438 | return; | ||
439 | } | ||
440 | this.sourceBuffer.abort(); | ||
441 | // tell the SWF where playback is continuing in the stitched timeline | ||
442 | this.el().vjs_setProperty('currentTime', segmentOffset * 0.001); | ||
605 | } | 443 | } |
606 | }); | 444 | |
445 | // if we're refilling the buffer after a seek, scan through the muxed | ||
446 | // FLV tags until we find the one that is closest to the desired | ||
447 | // playback time | ||
448 | if (typeof offset === 'number') { | ||
449 | ptsTime = offset - segmentOffset + tags[0].pts; | ||
450 | |||
451 | while (tags[i].pts < ptsTime) { | ||
452 | i++; | ||
453 | } | ||
454 | |||
455 | // tell the SWF where we will be seeking to | ||
456 | this.el().vjs_setProperty('currentTime', (tags[i].pts - tags[0].pts + segmentOffset) * 0.001); | ||
457 | |||
458 | tags = tags.slice(i); | ||
459 | |||
460 | this.lastSeekedTime_ = null; | ||
461 | } | ||
462 | |||
463 | for (i = 0; i < tags.length; i++) { | ||
464 | // queue up the bytes to be appended to the SourceBuffer | ||
465 | // the queue gives control back to the browser between tags | ||
466 | // so that large segments don't cause a "hiccup" in playback | ||
467 | |||
468 | this.sourceBuffer.appendBuffer(tags[i].bytes, this.player()); | ||
469 | } | ||
470 | |||
471 | // we're done processing this segment | ||
472 | segmentBuffer.shift(); | ||
473 | |||
474 | // transition the sourcebuffer to the ended state if we've hit the end of | ||
475 | // the playlist | ||
476 | if (mediaIndex + 1 === playlist.segments.length) { | ||
477 | this.mediaSource.endOfStream(); | ||
478 | } | ||
479 | }; | ||
607 | 480 | ||
608 | /** | 481 | /** |
609 | * Whether the browser has built-in HLS support. | 482 | * Whether the browser has built-in HLS support. |
... | @@ -625,43 +498,6 @@ videojs.Hls.supportsNativeHls = (function() { | ... | @@ -625,43 +498,6 @@ videojs.Hls.supportsNativeHls = (function() { |
625 | (/probably|maybe/).test(vndMpeg); | 498 | (/probably|maybe/).test(vndMpeg); |
626 | })(); | 499 | })(); |
627 | 500 | ||
628 | // the desired length of video to maintain in the buffer, in seconds | ||
629 | videojs.Hls.GOAL_BUFFER_LENGTH = 30; | ||
630 | |||
631 | videojs.Hls.prototype.src = function(src) { | ||
632 | var | ||
633 | player = this.player(), | ||
634 | self = this, | ||
635 | mediaSource, | ||
636 | source; | ||
637 | |||
638 | if (src) { | ||
639 | mediaSource = new videojs.MediaSource(); | ||
640 | source = { | ||
641 | src: videojs.URL.createObjectURL(mediaSource), | ||
642 | type: "video/flv" | ||
643 | }; | ||
644 | this.mediaSource = mediaSource; | ||
645 | initSource(player, mediaSource, src); | ||
646 | this.player().ready(function() { | ||
647 | // do nothing if the tech has been disposed already | ||
648 | // this can occur if someone sets the src in player.ready(), for instance | ||
649 | if (!self.el()) { | ||
650 | return; | ||
651 | } | ||
652 | self.el().vjs_src(source.src); | ||
653 | }); | ||
654 | } | ||
655 | }; | ||
656 | |||
657 | videojs.Hls.prototype.duration = function() { | ||
658 | var playlists = this.playlists; | ||
659 | if (playlists) { | ||
660 | return totalDuration(playlists.media()); | ||
661 | } | ||
662 | return 0; | ||
663 | }; | ||
664 | |||
665 | videojs.Hls.isSupported = function() { | 501 | videojs.Hls.isSupported = function() { |
666 | return !videojs.Hls.supportsNativeHls && | 502 | return !videojs.Hls.supportsNativeHls && |
667 | videojs.Flash.isSupported() && | 503 | videojs.Flash.isSupported() && |
... | @@ -669,89 +505,182 @@ videojs.Hls.isSupported = function() { | ... | @@ -669,89 +505,182 @@ videojs.Hls.isSupported = function() { |
669 | }; | 505 | }; |
670 | 506 | ||
671 | videojs.Hls.canPlaySource = function(srcObj) { | 507 | videojs.Hls.canPlaySource = function(srcObj) { |
508 | var mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i; | ||
672 | return mpegurlRE.test(srcObj.type); | 509 | return mpegurlRE.test(srcObj.type); |
673 | }; | 510 | }; |
674 | 511 | ||
675 | /** | 512 | /** |
676 | * Creates and sends an XMLHttpRequest. | 513 | * Calculate the duration of a playlist from a given start index to a given |
677 | * @param options {string | object} if this argument is a string, it | 514 | * end index. |
678 | * is intrepreted as a URL and a simple GET request is | 515 | * @param playlist {object} a media playlist object |
679 | * inititated. If it is an object, it should contain a `url` | 516 | * @param startIndex {number} an inclusive lower boundary for the playlist. |
680 | * property that indicates the URL to request and optionally a | 517 | * Defaults to 0. |
681 | * `method` which is the type of HTTP request to send. | 518 | * @param endIndex {number} an exclusive upper boundary for the playlist. |
682 | * @param callback (optional) {function} a function to call when the | 519 | * Defaults to playlist length. |
683 | * request completes. If the request was not successful, the first | 520 | * @return {number} the duration between the start index and end index. |
684 | * argument will be falsey. | ||
685 | * @return {object} the XMLHttpRequest that was initiated. | ||
686 | */ | 521 | */ |
687 | xhr = videojs.Hls.xhr = function(url, callback) { | 522 | videojs.Hls.getPlaylistDuration = function(playlist, startIndex, endIndex) { |
688 | var | 523 | var dur = 0, |
689 | options = { | 524 | segment, |
690 | method: 'GET', | 525 | i; |
691 | timeout: 45 * 1000 | 526 | |
692 | }, | 527 | startIndex = startIndex || 0; |
693 | request, | 528 | endIndex = endIndex !== undefined ? endIndex : (playlist.segments || []).length; |
694 | abortTimeout; | 529 | i = endIndex - 1; |
530 | |||
531 | for (; i >= startIndex; i--) { | ||
532 | segment = playlist.segments[i]; | ||
533 | dur += segment.duration || playlist.targetDuration || 0; | ||
534 | } | ||
695 | 535 | ||
696 | if (typeof callback !== 'function') { | 536 | return dur; |
697 | callback = function() {}; | 537 | }; |
538 | |||
539 | /** | ||
540 | * Calculate the total duration for a playlist based on segment metadata. | ||
541 | * @param playlist {object} a media playlist object | ||
542 | * @return {number} the currently known duration, in seconds | ||
543 | */ | ||
544 | videojs.Hls.getPlaylistTotalDuration = function(playlist) { | ||
545 | if (!playlist) { | ||
546 | return 0; | ||
547 | } | ||
548 | |||
549 | // if present, use the duration specified in the playlist | ||
550 | if (playlist.totalDuration) { | ||
551 | return playlist.totalDuration; | ||
698 | } | 552 | } |
699 | 553 | ||
700 | if (typeof url === 'object') { | 554 | // duration should be Infinity for live playlists |
701 | options = videojs.util.mergeOptions(options, url); | 555 | if (!playlist.endList) { |
702 | url = options.url; | 556 | return window.Infinity; |
703 | } | 557 | } |
704 | 558 | ||
705 | request = new window.XMLHttpRequest(); | 559 | return videojs.Hls.getPlaylistDuration(playlist); |
706 | request.open(options.method, url); | 560 | }; |
707 | request.url = url; | 561 | |
562 | /** | ||
563 | * Determine the media index in one playlist that corresponds to a | ||
564 | * specified media index in another. This function can be used to | ||
565 | * calculate a new segment position when a playlist is reloaded or a | ||
566 | * variant playlist is becoming active. | ||
567 | * @param mediaIndex {number} the index into the original playlist | ||
568 | * to translate | ||
569 | * @param original {object} the playlist to translate the media | ||
570 | * index from | ||
571 | * @param update {object} the playlist to translate the media index | ||
572 | * to | ||
573 | * @param {number} the corresponding media index in the updated | ||
574 | * playlist | ||
575 | */ | ||
576 | videojs.Hls.translateMediaIndex = function(mediaIndex, original, update) { | ||
577 | var | ||
578 | i, | ||
579 | originalSegment; | ||
708 | 580 | ||
709 | if (options.responseType) { | 581 | // no segments have been loaded from the original playlist |
710 | request.responseType = options.responseType; | 582 | if (mediaIndex === 0) { |
583 | return 0; | ||
711 | } | 584 | } |
712 | if (options.withCredentials) { | 585 | if (!(update && update.segments)) { |
713 | request.withCredentials = true; | 586 | // let the media index be zero when there are no segments defined |
587 | return 0; | ||
714 | } | 588 | } |
715 | if (options.timeout) { | 589 | |
716 | if (request.timeout === 0) { | 590 | // try to sync based on URI |
717 | request.timeout = options.timeout; | 591 | i = update.segments.length; |
718 | request.ontimeout = function() { | 592 | originalSegment = original.segments[mediaIndex - 1]; |
719 | request.timedout = true; | 593 | while (i--) { |
720 | }; | 594 | if (originalSegment.uri === update.segments[i].uri) { |
721 | } else { | 595 | return i + 1; |
722 | // polyfill XHR2 by aborting after the timeout | ||
723 | abortTimeout = window.setTimeout(function() { | ||
724 | if (request.readyState !== 4) { | ||
725 | request.timedout = true; | ||
726 | request.abort(); | ||
727 | } | ||
728 | }, options.timeout); | ||
729 | } | 596 | } |
730 | } | 597 | } |
731 | 598 | ||
732 | request.onreadystatechange = function() { | 599 | // sync on media sequence |
733 | // wait until the request completes | 600 | return (original.mediaSequence + mediaIndex) - update.mediaSequence; |
734 | if (this.readyState !== 4) { | 601 | }; |
735 | return; | ||
736 | } | ||
737 | 602 | ||
738 | // clear outstanding timeouts | 603 | /** |
739 | window.clearTimeout(abortTimeout); | 604 | * TODO - Document this great feature. |
605 | * | ||
606 | * @param playlist | ||
607 | * @param time | ||
608 | * @returns int | ||
609 | */ | ||
610 | videojs.Hls.getMediaIndexByTime = function(playlist, time) { | ||
611 | var index, counter, timeRanges, currentSegmentRange; | ||
612 | |||
613 | timeRanges = []; | ||
614 | for (index = 0; index < playlist.segments.length; index++) { | ||
615 | currentSegmentRange = {}; | ||
616 | currentSegmentRange.start = (index === 0) ? 0 : timeRanges[index - 1].end; | ||
617 | currentSegmentRange.end = currentSegmentRange.start + playlist.segments[index].duration; | ||
618 | timeRanges.push(currentSegmentRange); | ||
619 | } | ||
740 | 620 | ||
741 | // request timeout | 621 | for (counter = 0; counter < timeRanges.length; counter++) { |
742 | if (request.timedout) { | 622 | if (time >= timeRanges[counter].start && time < timeRanges[counter].end) { |
743 | return callback.call(this, 'timeout', url); | 623 | return counter; |
744 | } | 624 | } |
625 | } | ||
745 | 626 | ||
746 | // request aborted or errored | 627 | return -1; |
747 | if (this.status >= 400 || this.status === 0) { | 628 | }; |
748 | return callback.call(this, true, url); | ||
749 | } | ||
750 | 629 | ||
751 | return callback.call(this, false, url); | 630 | /** |
752 | }; | 631 | * A comparator function to sort two playlist object by bandwidth. |
753 | request.send(null); | 632 | * @param left {object} a media playlist object |
754 | return request; | 633 | * @param right {object} a media playlist object |
634 | * @return {number} Greater than zero if the bandwidth attribute of | ||
635 | * left is greater than the corresponding attribute of right. Less | ||
636 | * than zero if the bandwidth of right is greater than left and | ||
637 | * exactly zero if the two are equal. | ||
638 | */ | ||
639 | videojs.Hls.comparePlaylistBandwidth = function(left, right) { | ||
640 | var leftBandwidth, rightBandwidth; | ||
641 | if (left.attributes && left.attributes.BANDWIDTH) { | ||
642 | leftBandwidth = left.attributes.BANDWIDTH; | ||
643 | } | ||
644 | leftBandwidth = leftBandwidth || window.Number.MAX_VALUE; | ||
645 | if (right.attributes && right.attributes.BANDWIDTH) { | ||
646 | rightBandwidth = right.attributes.BANDWIDTH; | ||
647 | } | ||
648 | rightBandwidth = rightBandwidth || window.Number.MAX_VALUE; | ||
649 | |||
650 | return leftBandwidth - rightBandwidth; | ||
651 | }; | ||
652 | |||
653 | /** | ||
654 | * A comparator function to sort two playlist object by resolution (width). | ||
655 | * @param left {object} a media playlist object | ||
656 | * @param right {object} a media playlist object | ||
657 | * @return {number} Greater than zero if the resolution.width attribute of | ||
658 | * left is greater than the corresponding attribute of right. Less | ||
659 | * than zero if the resolution.width of right is greater than left and | ||
660 | * exactly zero if the two are equal. | ||
661 | */ | ||
662 | videojs.Hls.comparePlaylistResolution = function(left, right) { | ||
663 | var leftWidth, rightWidth; | ||
664 | |||
665 | if (left.attributes && left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) { | ||
666 | leftWidth = left.attributes.RESOLUTION.width; | ||
667 | } | ||
668 | |||
669 | leftWidth = leftWidth || window.Number.MAX_VALUE; | ||
670 | |||
671 | if (right.attributes && right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) { | ||
672 | rightWidth = right.attributes.RESOLUTION.width; | ||
673 | } | ||
674 | |||
675 | rightWidth = rightWidth || window.Number.MAX_VALUE; | ||
676 | |||
677 | // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions | ||
678 | // have the same media dimensions/ resolution | ||
679 | if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) { | ||
680 | return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH; | ||
681 | } else { | ||
682 | return leftWidth - rightWidth; | ||
683 | } | ||
755 | }; | 684 | }; |
756 | 685 | ||
757 | /** | 686 | /** |
... | @@ -793,7 +722,4 @@ resolveUrl = videojs.Hls.resolveUrl = function(basePath, path) { | ... | @@ -793,7 +722,4 @@ resolveUrl = videojs.Hls.resolveUrl = function(basePath, path) { |
793 | return result; | 722 | return result; |
794 | }; | 723 | }; |
795 | 724 | ||
796 | // Add HLS to the standard tech order | ||
797 | videojs.options.techOrder.unshift('hls'); | ||
798 | |||
799 | })(window, window.videojs, document); | 725 | })(window, window.videojs, document); | ... | ... |
src/xhr.js
0 → 100644
1 | (function(videojs){ | ||
2 | /** | ||
3 | * Creates and sends an XMLHttpRequest. | ||
4 | * TODO - expose video.js core's XHR and use that instead | ||
5 | * | ||
6 | * @param options {string | object} if this argument is a string, it | ||
7 | * is intrepreted as a URL and a simple GET request is | ||
8 | * inititated. If it is an object, it should contain a `url` | ||
9 | * property that indicates the URL to request and optionally a | ||
10 | * `method` which is the type of HTTP request to send. | ||
11 | * @param callback (optional) {function} a function to call when the | ||
12 | * request completes. If the request was not successful, the first | ||
13 | * argument will be falsey. | ||
14 | * @return {object} the XMLHttpRequest that was initiated. | ||
15 | */ | ||
16 | videojs.Hls.xhr = function(url, callback) { | ||
17 | var | ||
18 | options = { | ||
19 | method: 'GET', | ||
20 | timeout: 45 * 1000 | ||
21 | }, | ||
22 | request, | ||
23 | abortTimeout; | ||
24 | |||
25 | if (typeof callback !== 'function') { | ||
26 | callback = function() {}; | ||
27 | } | ||
28 | |||
29 | if (typeof url === 'object') { | ||
30 | options = videojs.util.mergeOptions(options, url); | ||
31 | url = options.url; | ||
32 | } | ||
33 | |||
34 | request = new window.XMLHttpRequest(); | ||
35 | request.open(options.method, url); | ||
36 | request.url = url; | ||
37 | |||
38 | if (options.responseType) { | ||
39 | request.responseType = options.responseType; | ||
40 | } | ||
41 | if (options.withCredentials) { | ||
42 | request.withCredentials = true; | ||
43 | } | ||
44 | if (options.timeout) { | ||
45 | if (request.timeout === 0) { | ||
46 | request.timeout = options.timeout; | ||
47 | request.ontimeout = function() { | ||
48 | request.timedout = true; | ||
49 | }; | ||
50 | } else { | ||
51 | // polyfill XHR2 by aborting after the timeout | ||
52 | abortTimeout = window.setTimeout(function() { | ||
53 | if (request.readyState !== 4) { | ||
54 | request.timedout = true; | ||
55 | request.abort(); | ||
56 | } | ||
57 | }, options.timeout); | ||
58 | } | ||
59 | } | ||
60 | |||
61 | request.onreadystatechange = function() { | ||
62 | // wait until the request completes | ||
63 | if (this.readyState !== 4) { | ||
64 | return; | ||
65 | } | ||
66 | |||
67 | // clear outstanding timeouts | ||
68 | window.clearTimeout(abortTimeout); | ||
69 | |||
70 | // request timeout | ||
71 | if (request.timedout) { | ||
72 | return callback.call(this, 'timeout', url); | ||
73 | } | ||
74 | |||
75 | // request aborted or errored | ||
76 | if (this.status >= 400 || this.status === 0) { | ||
77 | return callback.call(this, true, url); | ||
78 | } | ||
79 | |||
80 | return callback.call(this, false, url); | ||
81 | }; | ||
82 | request.send(null); | ||
83 | return request; | ||
84 | }; | ||
85 | |||
86 | })(window.videojs); | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
... | @@ -79,6 +79,7 @@ module.exports = function(config) { | ... | @@ -79,6 +79,7 @@ module.exports = function(config) { |
79 | '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js', | 79 | '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js', |
80 | '../test/karma-qunit-shim.js', | 80 | '../test/karma-qunit-shim.js', |
81 | '../src/videojs-hls.js', | 81 | '../src/videojs-hls.js', |
82 | '../src/xhr.js', | ||
82 | '../src/flv-tag.js', | 83 | '../src/flv-tag.js', |
83 | '../src/exp-golomb.js', | 84 | '../src/exp-golomb.js', |
84 | '../src/h264-stream.js', | 85 | '../src/h264-stream.js', | ... | ... |
... | @@ -43,6 +43,7 @@ module.exports = function(config) { | ... | @@ -43,6 +43,7 @@ module.exports = function(config) { |
43 | '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js', | 43 | '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js', |
44 | '../test/karma-qunit-shim.js', | 44 | '../test/karma-qunit-shim.js', |
45 | '../src/videojs-hls.js', | 45 | '../src/videojs-hls.js', |
46 | '../src/xhr.js', | ||
46 | '../src/flv-tag.js', | 47 | '../src/flv-tag.js', |
47 | '../src/exp-golomb.js', | 48 | '../src/exp-golomb.js', |
48 | '../src/h264-stream.js', | 49 | '../src/h264-stream.js', | ... | ... |
... | @@ -123,6 +123,7 @@ | ... | @@ -123,6 +123,7 @@ |
123 | <script src="../../node_modules/video.js/dist/video-js/video.js"></script> | 123 | <script src="../../node_modules/video.js/dist/video-js/video.js"></script> |
124 | <script src="../../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script> | 124 | <script src="../../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script> |
125 | <script src="../../src/videojs-hls.js"></script> | 125 | <script src="../../src/videojs-hls.js"></script> |
126 | <script src="../../src/xhr.js"></script> | ||
126 | <script src="../../src/stream.js"></script> | 127 | <script src="../../src/stream.js"></script> |
127 | <script src="../../src/m3u8/m3u8-parser.js"></script> | 128 | <script src="../../src/m3u8/m3u8-parser.js"></script> |
128 | <script src="../../src/playlist-loader.js"></script> | 129 | <script src="../../src/playlist-loader.js"></script> | ... | ... |
... | @@ -20,6 +20,7 @@ | ... | @@ -20,6 +20,7 @@ |
20 | 20 | ||
21 | <!-- HLS plugin --> | 21 | <!-- HLS plugin --> |
22 | <script src="../src/videojs-hls.js"></script> | 22 | <script src="../src/videojs-hls.js"></script> |
23 | <script src="../src/xhr.js"></script> | ||
23 | <script src="../src/flv-tag.js"></script> | 24 | <script src="../src/flv-tag.js"></script> |
24 | <script src="../src/exp-golomb.js"></script> | 25 | <script src="../src/exp-golomb.js"></script> |
25 | <script src="../src/h264-stream.js"></script> | 26 | <script src="../src/h264-stream.js"></script> | ... | ... |
... | @@ -51,10 +51,18 @@ var | ... | @@ -51,10 +51,18 @@ var |
51 | tech.vjs_getProperty = function() {}; | 51 | tech.vjs_getProperty = function() {}; |
52 | tech.vjs_setProperty = function() {}; | 52 | tech.vjs_setProperty = function() {}; |
53 | tech.vjs_src = function() {}; | 53 | tech.vjs_src = function() {}; |
54 | tech.vjs_play = function() {}; | ||
54 | videojs.Flash.onReady(tech.id); | 55 | videojs.Flash.onReady(tech.id); |
55 | 56 | ||
56 | return player; | 57 | return player; |
57 | }, | 58 | }, |
59 | openMediaSource = function(player) { | ||
60 | player.hls.mediaSource.trigger({ | ||
61 | type: 'sourceopen' | ||
62 | }); | ||
63 | // endOfStream triggers an exception if flash isn't available | ||
64 | player.hls.mediaSource.endOfStream = function() {}; | ||
65 | }, | ||
58 | standardXHRResponse = function(request) { | 66 | standardXHRResponse = function(request) { |
59 | if (!request.url) { | 67 | if (!request.url) { |
60 | return; | 68 | return; |
... | @@ -160,9 +168,7 @@ test('starts playing if autoplay is specified', function() { | ... | @@ -160,9 +168,7 @@ test('starts playing if autoplay is specified', function() { |
160 | src: 'manifest/playlist.m3u8', | 168 | src: 'manifest/playlist.m3u8', |
161 | type: 'application/vnd.apple.mpegurl' | 169 | type: 'application/vnd.apple.mpegurl' |
162 | }); | 170 | }); |
163 | player.hls.mediaSource.trigger({ | 171 | openMediaSource(player); |
164 | type: 'sourceopen' | ||
165 | }); | ||
166 | 172 | ||
167 | standardXHRResponse(requests[0]); | 173 | standardXHRResponse(requests[0]); |
168 | strictEqual(1, plays, 'play was called'); | 174 | strictEqual(1, plays, 'play was called'); |
... | @@ -179,9 +185,7 @@ test('creates a PlaylistLoader on init', function() { | ... | @@ -179,9 +185,7 @@ test('creates a PlaylistLoader on init', function() { |
179 | src:'manifest/playlist.m3u8', | 185 | src:'manifest/playlist.m3u8', |
180 | type: 'application/vnd.apple.mpegurl' | 186 | type: 'application/vnd.apple.mpegurl' |
181 | }); | 187 | }); |
182 | player.hls.mediaSource.trigger({ | 188 | openMediaSource(player); |
183 | type: 'sourceopen' | ||
184 | }); | ||
185 | standardXHRResponse(requests[0]); | 189 | standardXHRResponse(requests[0]); |
186 | ok(loadedmetadata, 'loadedmetadata fires'); | 190 | ok(loadedmetadata, 'loadedmetadata fires'); |
187 | ok(player.hls.playlists.master, 'set the master playlist'); | 191 | ok(player.hls.playlists.master, 'set the master playlist'); |
... | @@ -204,9 +208,7 @@ test('sets the duration if one is available on the playlist', function() { | ... | @@ -204,9 +208,7 @@ test('sets the duration if one is available on the playlist', function() { |
204 | src: 'manifest/media.m3u8', | 208 | src: 'manifest/media.m3u8', |
205 | type: 'application/vnd.apple.mpegurl' | 209 | type: 'application/vnd.apple.mpegurl' |
206 | }); | 210 | }); |
207 | player.hls.mediaSource.trigger({ | 211 | openMediaSource(player); |
208 | type: 'sourceopen' | ||
209 | }); | ||
210 | 212 | ||
211 | standardXHRResponse(requests[0]); | 213 | standardXHRResponse(requests[0]); |
212 | strictEqual(calls, 1, 'duration is set'); | 214 | strictEqual(calls, 1, 'duration is set'); |
... | @@ -226,9 +228,7 @@ test('calculates the duration if needed', function() { | ... | @@ -226,9 +228,7 @@ test('calculates the duration if needed', function() { |
226 | src: 'http://example.com/manifest/missingExtinf.m3u8', | 228 | src: 'http://example.com/manifest/missingExtinf.m3u8', |
227 | type: 'application/vnd.apple.mpegurl' | 229 | type: 'application/vnd.apple.mpegurl' |
228 | }); | 230 | }); |
229 | player.hls.mediaSource.trigger({ | 231 | openMediaSource(player); |
230 | type: 'sourceopen' | ||
231 | }); | ||
232 | 232 | ||
233 | standardXHRResponse(requests[0]); | 233 | standardXHRResponse(requests[0]); |
234 | strictEqual(durations.length, 1, 'duration is set'); | 234 | strictEqual(durations.length, 1, 'duration is set'); |
... | @@ -245,9 +245,7 @@ test('starts downloading a segment on loadedmetadata', function() { | ... | @@ -245,9 +245,7 @@ test('starts downloading a segment on loadedmetadata', function() { |
245 | player.buffered = function() { | 245 | player.buffered = function() { |
246 | return videojs.createTimeRange(0, 0); | 246 | return videojs.createTimeRange(0, 0); |
247 | }; | 247 | }; |
248 | player.hls.mediaSource.trigger({ | 248 | openMediaSource(player); |
249 | type: 'sourceopen' | ||
250 | }); | ||
251 | 249 | ||
252 | standardXHRResponse(requests[0]); | 250 | standardXHRResponse(requests[0]); |
253 | standardXHRResponse(requests[1]); | 251 | standardXHRResponse(requests[1]); |
... | @@ -263,9 +261,7 @@ test('recognizes absolute URIs and requests them unmodified', function() { | ... | @@ -263,9 +261,7 @@ test('recognizes absolute URIs and requests them unmodified', function() { |
263 | src: 'manifest/absoluteUris.m3u8', | 261 | src: 'manifest/absoluteUris.m3u8', |
264 | type: 'application/vnd.apple.mpegurl' | 262 | type: 'application/vnd.apple.mpegurl' |
265 | }); | 263 | }); |
266 | player.hls.mediaSource.trigger({ | 264 | openMediaSource(player); |
267 | type: 'sourceopen' | ||
268 | }); | ||
269 | 265 | ||
270 | standardXHRResponse(requests[0]); | 266 | standardXHRResponse(requests[0]); |
271 | standardXHRResponse(requests[1]); | 267 | standardXHRResponse(requests[1]); |
... | @@ -279,9 +275,7 @@ test('recognizes domain-relative URLs', function() { | ... | @@ -279,9 +275,7 @@ test('recognizes domain-relative URLs', function() { |
279 | src: 'manifest/domainUris.m3u8', | 275 | src: 'manifest/domainUris.m3u8', |
280 | type: 'application/vnd.apple.mpegurl' | 276 | type: 'application/vnd.apple.mpegurl' |
281 | }); | 277 | }); |
282 | player.hls.mediaSource.trigger({ | 278 | openMediaSource(player); |
283 | type: 'sourceopen' | ||
284 | }); | ||
285 | 279 | ||
286 | standardXHRResponse(requests[0]); | 280 | standardXHRResponse(requests[0]); |
287 | standardXHRResponse(requests[1]); | 281 | standardXHRResponse(requests[1]); |
... | @@ -297,9 +291,7 @@ test('re-initializes the tech for each source', function() { | ... | @@ -297,9 +291,7 @@ test('re-initializes the tech for each source', function() { |
297 | src: 'manifest/master.m3u8', | 291 | src: 'manifest/master.m3u8', |
298 | type: 'application/vnd.apple.mpegurl' | 292 | type: 'application/vnd.apple.mpegurl' |
299 | }); | 293 | }); |
300 | player.hls.mediaSource.trigger({ | 294 | openMediaSource(player); |
301 | type: 'sourceopen' | ||
302 | }); | ||
303 | firstPlaylists = player.hls.playlists; | 295 | firstPlaylists = player.hls.playlists; |
304 | firstMSE = player.hls.mediaSource; | 296 | firstMSE = player.hls.mediaSource; |
305 | 297 | ||
... | @@ -307,9 +299,7 @@ test('re-initializes the tech for each source', function() { | ... | @@ -307,9 +299,7 @@ test('re-initializes the tech for each source', function() { |
307 | src: 'manifest/master.m3u8', | 299 | src: 'manifest/master.m3u8', |
308 | type: 'application/vnd.apple.mpegurl' | 300 | type: 'application/vnd.apple.mpegurl' |
309 | }); | 301 | }); |
310 | player.hls.mediaSource.trigger({ | 302 | openMediaSource(player); |
311 | type: 'sourceopen' | ||
312 | }); | ||
313 | secondPlaylists = player.hls.playlists; | 303 | secondPlaylists = player.hls.playlists; |
314 | secondMSE = player.hls.mediaSource; | 304 | secondMSE = player.hls.mediaSource; |
315 | 305 | ||
... | @@ -326,9 +316,7 @@ test('triggers an error when a master playlist request errors', function() { | ... | @@ -326,9 +316,7 @@ test('triggers an error when a master playlist request errors', function() { |
326 | src: 'manifest/master.m3u8', | 316 | src: 'manifest/master.m3u8', |
327 | type: 'application/vnd.apple.mpegurl' | 317 | type: 'application/vnd.apple.mpegurl' |
328 | }); | 318 | }); |
329 | player.hls.mediaSource.trigger({ | 319 | openMediaSource(player); |
330 | type: 'sourceopen' | ||
331 | }); | ||
332 | requests.pop().respond(500); | 320 | requests.pop().respond(500); |
333 | 321 | ||
334 | ok(player.error(), 'an error is triggered'); | 322 | ok(player.error(), 'an error is triggered'); |
... | @@ -341,9 +329,7 @@ test('downloads media playlists after loading the master', function() { | ... | @@ -341,9 +329,7 @@ test('downloads media playlists after loading the master', function() { |
341 | src: 'manifest/master.m3u8', | 329 | src: 'manifest/master.m3u8', |
342 | type: 'application/vnd.apple.mpegurl' | 330 | type: 'application/vnd.apple.mpegurl' |
343 | }); | 331 | }); |
344 | player.hls.mediaSource.trigger({ | 332 | openMediaSource(player); |
345 | type: 'sourceopen' | ||
346 | }); | ||
347 | 333 | ||
348 | standardXHRResponse(requests[0]); | 334 | standardXHRResponse(requests[0]); |
349 | standardXHRResponse(requests[1]); | 335 | standardXHRResponse(requests[1]); |
... | @@ -367,9 +353,7 @@ test('timeupdates do not check to fill the buffer until a media playlist is read | ... | @@ -367,9 +353,7 @@ test('timeupdates do not check to fill the buffer until a media playlist is read |
367 | src: 'manifest/media.m3u8', | 353 | src: 'manifest/media.m3u8', |
368 | type: 'application/vnd.apple.mpegurl' | 354 | type: 'application/vnd.apple.mpegurl' |
369 | }); | 355 | }); |
370 | player.hls.mediaSource.trigger({ | 356 | openMediaSource(player); |
371 | type: 'sourceopen' | ||
372 | }); | ||
373 | player.trigger('timeupdate'); | 357 | player.trigger('timeupdate'); |
374 | 358 | ||
375 | strictEqual(1, requests.length, 'one request was made'); | 359 | strictEqual(1, requests.length, 'one request was made'); |
... | @@ -381,9 +365,7 @@ test('calculates the bandwidth after downloading a segment', function() { | ... | @@ -381,9 +365,7 @@ test('calculates the bandwidth after downloading a segment', function() { |
381 | src: 'manifest/media.m3u8', | 365 | src: 'manifest/media.m3u8', |
382 | type: 'application/vnd.apple.mpegurl' | 366 | type: 'application/vnd.apple.mpegurl' |
383 | }); | 367 | }); |
384 | player.hls.mediaSource.trigger({ | 368 | openMediaSource(player); |
385 | type: 'sourceopen' | ||
386 | }); | ||
387 | 369 | ||
388 | standardXHRResponse(requests[0]); | 370 | standardXHRResponse(requests[0]); |
389 | standardXHRResponse(requests[1]); | 371 | standardXHRResponse(requests[1]); |
... | @@ -405,9 +387,7 @@ test('selects a playlist after segment downloads', function() { | ... | @@ -405,9 +387,7 @@ test('selects a playlist after segment downloads', function() { |
405 | calls++; | 387 | calls++; |
406 | return player.hls.playlists.master.playlists[0]; | 388 | return player.hls.playlists.master.playlists[0]; |
407 | }; | 389 | }; |
408 | player.hls.mediaSource.trigger({ | 390 | openMediaSource(player); |
409 | type: 'sourceopen' | ||
410 | }); | ||
411 | 391 | ||
412 | standardXHRResponse(requests[0]); | 392 | standardXHRResponse(requests[0]); |
413 | standardXHRResponse(requests[1]); | 393 | standardXHRResponse(requests[1]); |
... | @@ -433,9 +413,7 @@ test('moves to the next segment if there is a network error', function() { | ... | @@ -433,9 +413,7 @@ test('moves to the next segment if there is a network error', function() { |
433 | src: 'manifest/master.m3u8', | 413 | src: 'manifest/master.m3u8', |
434 | type: 'application/vnd.apple.mpegurl' | 414 | type: 'application/vnd.apple.mpegurl' |
435 | }); | 415 | }); |
436 | player.hls.mediaSource.trigger({ | 416 | openMediaSource(player); |
437 | type: 'sourceopen' | ||
438 | }); | ||
439 | 417 | ||
440 | standardXHRResponse(requests[0]); | 418 | standardXHRResponse(requests[0]); |
441 | standardXHRResponse(requests[1]); | 419 | standardXHRResponse(requests[1]); |
... | @@ -468,9 +446,7 @@ test('updates the duration after switching playlists', function() { | ... | @@ -468,9 +446,7 @@ test('updates the duration after switching playlists', function() { |
468 | calls++; | 446 | calls++; |
469 | } | 447 | } |
470 | }; | 448 | }; |
471 | player.hls.mediaSource.trigger({ | 449 | openMediaSource(player); |
472 | type: 'sourceopen' | ||
473 | }); | ||
474 | 450 | ||
475 | standardXHRResponse(requests[0]); | 451 | standardXHRResponse(requests[0]); |
476 | standardXHRResponse(requests[1]); | 452 | standardXHRResponse(requests[1]); |
... | @@ -490,9 +466,7 @@ test('downloads additional playlists if required', function() { | ... | @@ -490,9 +466,7 @@ test('downloads additional playlists if required', function() { |
490 | src: 'manifest/master.m3u8', | 466 | src: 'manifest/master.m3u8', |
491 | type: 'application/vnd.apple.mpegurl' | 467 | type: 'application/vnd.apple.mpegurl' |
492 | }); | 468 | }); |
493 | player.hls.mediaSource.trigger({ | 469 | openMediaSource(player); |
494 | type: 'sourceopen' | ||
495 | }); | ||
496 | 470 | ||
497 | standardXHRResponse(requests[0]); | 471 | standardXHRResponse(requests[0]); |
498 | standardXHRResponse(requests[1]); | 472 | standardXHRResponse(requests[1]); |
... | @@ -531,9 +505,7 @@ test('selects a playlist below the current bandwidth', function() { | ... | @@ -531,9 +505,7 @@ test('selects a playlist below the current bandwidth', function() { |
531 | src: 'manifest/master.m3u8', | 505 | src: 'manifest/master.m3u8', |
532 | type: 'application/vnd.apple.mpegurl' | 506 | type: 'application/vnd.apple.mpegurl' |
533 | }); | 507 | }); |
534 | player.hls.mediaSource.trigger({ | 508 | openMediaSource(player); |
535 | type: 'sourceopen' | ||
536 | }); | ||
537 | 509 | ||
538 | standardXHRResponse(requests[0]); | 510 | standardXHRResponse(requests[0]); |
539 | 511 | ||
... | @@ -556,9 +528,7 @@ test('raises the minimum bitrate for a stream proportionially', function() { | ... | @@ -556,9 +528,7 @@ test('raises the minimum bitrate for a stream proportionially', function() { |
556 | src: 'manifest/master.m3u8', | 528 | src: 'manifest/master.m3u8', |
557 | type: 'application/vnd.apple.mpegurl' | 529 | type: 'application/vnd.apple.mpegurl' |
558 | }); | 530 | }); |
559 | player.hls.mediaSource.trigger({ | 531 | openMediaSource(player); |
560 | type: 'sourceopen' | ||
561 | }); | ||
562 | 532 | ||
563 | standardXHRResponse(requests[0]); | 533 | standardXHRResponse(requests[0]); |
564 | 534 | ||
... | @@ -581,9 +551,7 @@ test('uses the lowest bitrate if no other is suitable', function() { | ... | @@ -581,9 +551,7 @@ test('uses the lowest bitrate if no other is suitable', function() { |
581 | src: 'manifest/master.m3u8', | 551 | src: 'manifest/master.m3u8', |
582 | type: 'application/vnd.apple.mpegurl' | 552 | type: 'application/vnd.apple.mpegurl' |
583 | }); | 553 | }); |
584 | player.hls.mediaSource.trigger({ | 554 | openMediaSource(player); |
585 | type: 'sourceopen' | ||
586 | }); | ||
587 | 555 | ||
588 | standardXHRResponse(requests[0]); | 556 | standardXHRResponse(requests[0]); |
589 | 557 | ||
... | @@ -605,9 +573,7 @@ test('selects the correct rendition by player dimensions', function() { | ... | @@ -605,9 +573,7 @@ test('selects the correct rendition by player dimensions', function() { |
605 | type: 'application/vnd.apple.mpegurl' | 573 | type: 'application/vnd.apple.mpegurl' |
606 | }); | 574 | }); |
607 | 575 | ||
608 | player.hls.mediaSource.trigger({ | 576 | openMediaSource(player); |
609 | type: 'sourceopen' | ||
610 | }); | ||
611 | 577 | ||
612 | standardXHRResponse(requests[0]); | 578 | standardXHRResponse(requests[0]); |
613 | 579 | ||
... | @@ -644,9 +610,7 @@ test('does not download the next segment if the buffer is full', function() { | ... | @@ -644,9 +610,7 @@ test('does not download the next segment if the buffer is full', function() { |
644 | player.buffered = function() { | 610 | player.buffered = function() { |
645 | return videojs.createTimeRange(0, currentTime + videojs.Hls.GOAL_BUFFER_LENGTH); | 611 | return videojs.createTimeRange(0, currentTime + videojs.Hls.GOAL_BUFFER_LENGTH); |
646 | }; | 612 | }; |
647 | player.hls.mediaSource.trigger({ | 613 | openMediaSource(player); |
648 | type: 'sourceopen' | ||
649 | }); | ||
650 | 614 | ||
651 | standardXHRResponse(requests[0]); | 615 | standardXHRResponse(requests[0]); |
652 | 616 | ||
... | @@ -660,9 +624,7 @@ test('downloads the next segment if the buffer is getting low', function() { | ... | @@ -660,9 +624,7 @@ test('downloads the next segment if the buffer is getting low', function() { |
660 | src: 'manifest/media.m3u8', | 624 | src: 'manifest/media.m3u8', |
661 | type: 'application/vnd.apple.mpegurl' | 625 | type: 'application/vnd.apple.mpegurl' |
662 | }); | 626 | }); |
663 | player.hls.mediaSource.trigger({ | 627 | openMediaSource(player); |
664 | type: 'sourceopen' | ||
665 | }); | ||
666 | 628 | ||
667 | standardXHRResponse(requests[0]); | 629 | standardXHRResponse(requests[0]); |
668 | standardXHRResponse(requests[1]); | 630 | standardXHRResponse(requests[1]); |
... | @@ -691,9 +653,7 @@ test('stops downloading segments at the end of the playlist', function() { | ... | @@ -691,9 +653,7 @@ test('stops downloading segments at the end of the playlist', function() { |
691 | src: 'manifest/media.m3u8', | 653 | src: 'manifest/media.m3u8', |
692 | type: 'application/vnd.apple.mpegurl' | 654 | type: 'application/vnd.apple.mpegurl' |
693 | }); | 655 | }); |
694 | player.hls.mediaSource.trigger({ | 656 | openMediaSource(player); |
695 | type: 'sourceopen' | ||
696 | }); | ||
697 | standardXHRResponse(requests[0]); | 657 | standardXHRResponse(requests[0]); |
698 | requests = []; | 658 | requests = []; |
699 | player.hls.mediaIndex = 4; | 659 | player.hls.mediaIndex = 4; |
... | @@ -707,9 +667,7 @@ test('only makes one segment request at a time', function() { | ... | @@ -707,9 +667,7 @@ test('only makes one segment request at a time', function() { |
707 | src: 'manifest/media.m3u8', | 667 | src: 'manifest/media.m3u8', |
708 | type: 'application/vnd.apple.mpegurl' | 668 | type: 'application/vnd.apple.mpegurl' |
709 | }); | 669 | }); |
710 | player.hls.mediaSource.trigger({ | 670 | openMediaSource(player); |
711 | type: 'sourceopen' | ||
712 | }); | ||
713 | standardXHRResponse(requests.pop()); | 671 | standardXHRResponse(requests.pop()); |
714 | player.trigger('timeupdate'); | 672 | player.trigger('timeupdate'); |
715 | 673 | ||
... | @@ -723,9 +681,7 @@ test('cancels outstanding XHRs when seeking', function() { | ... | @@ -723,9 +681,7 @@ test('cancels outstanding XHRs when seeking', function() { |
723 | src: 'manifest/media.m3u8', | 681 | src: 'manifest/media.m3u8', |
724 | type: 'application/vnd.apple.mpegurl' | 682 | type: 'application/vnd.apple.mpegurl' |
725 | }); | 683 | }); |
726 | player.hls.mediaSource.trigger({ | 684 | openMediaSource(player); |
727 | type: 'sourceopen' | ||
728 | }); | ||
729 | standardXHRResponse(requests[0]); | 685 | standardXHRResponse(requests[0]); |
730 | player.hls.media = { | 686 | player.hls.media = { |
731 | segments: [{ | 687 | segments: [{ |
... | @@ -764,9 +720,7 @@ test('flushes the parser after each segment', function() { | ... | @@ -764,9 +720,7 @@ test('flushes the parser after each segment', function() { |
764 | src: 'manifest/media.m3u8', | 720 | src: 'manifest/media.m3u8', |
765 | type: 'application/vnd.apple.mpegurl' | 721 | type: 'application/vnd.apple.mpegurl' |
766 | }); | 722 | }); |
767 | player.hls.mediaSource.trigger({ | 723 | openMediaSource(player); |
768 | type: 'sourceopen' | ||
769 | }); | ||
770 | 724 | ||
771 | standardXHRResponse(requests[0]); | 725 | standardXHRResponse(requests[0]); |
772 | standardXHRResponse(requests[1]); | 726 | standardXHRResponse(requests[1]); |
... | @@ -794,9 +748,7 @@ test('drops tags before the target timestamp when seeking', function() { | ... | @@ -794,9 +748,7 @@ test('drops tags before the target timestamp when seeking', function() { |
794 | src: 'manifest/media.m3u8', | 748 | src: 'manifest/media.m3u8', |
795 | type: 'application/vnd.apple.mpegurl' | 749 | type: 'application/vnd.apple.mpegurl' |
796 | }); | 750 | }); |
797 | player.hls.mediaSource.trigger({ | 751 | openMediaSource(player); |
798 | type: 'sourceopen' | ||
799 | }); | ||
800 | standardXHRResponse(requests[0]); | 752 | standardXHRResponse(requests[0]); |
801 | standardXHRResponse(requests[1]); | 753 | standardXHRResponse(requests[1]); |
802 | 754 | ||
... | @@ -836,9 +788,7 @@ test('calls abort() on the SourceBuffer before seeking', function() { | ... | @@ -836,9 +788,7 @@ test('calls abort() on the SourceBuffer before seeking', function() { |
836 | src: 'manifest/media.m3u8', | 788 | src: 'manifest/media.m3u8', |
837 | type: 'application/vnd.apple.mpegurl' | 789 | type: 'application/vnd.apple.mpegurl' |
838 | }); | 790 | }); |
839 | player.hls.mediaSource.trigger({ | 791 | openMediaSource(player); |
840 | type: 'sourceopen' | ||
841 | }); | ||
842 | 792 | ||
843 | standardXHRResponse(requests[0]); | 793 | standardXHRResponse(requests[0]); |
844 | standardXHRResponse(requests[1]); | 794 | standardXHRResponse(requests[1]); |
... | @@ -863,9 +813,7 @@ test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() { | ... | @@ -863,9 +813,7 @@ test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() { |
863 | src: 'manifest/media.m3u8', | 813 | src: 'manifest/media.m3u8', |
864 | type: 'application/vnd.apple.mpegurl' | 814 | type: 'application/vnd.apple.mpegurl' |
865 | }); | 815 | }); |
866 | player.hls.mediaSource.trigger({ | 816 | openMediaSource(player); |
867 | type: 'sourceopen' | ||
868 | }); | ||
869 | requests.pop().respond(404); | 817 | requests.pop().respond(404); |
870 | 818 | ||
871 | equal(errorTriggered, | 819 | equal(errorTriggered, |
... | @@ -883,9 +831,7 @@ test('segment 404 should trigger MEDIA_ERR_NETWORK', function () { | ... | @@ -883,9 +831,7 @@ test('segment 404 should trigger MEDIA_ERR_NETWORK', function () { |
883 | type: 'application/vnd.apple.mpegurl' | 831 | type: 'application/vnd.apple.mpegurl' |
884 | }); | 832 | }); |
885 | 833 | ||
886 | player.hls.mediaSource.trigger({ | 834 | openMediaSource(player); |
887 | type: 'sourceopen' | ||
888 | }); | ||
889 | 835 | ||
890 | standardXHRResponse(requests[0]); | 836 | standardXHRResponse(requests[0]); |
891 | requests[1].respond(404); | 837 | requests[1].respond(404); |
... | @@ -899,9 +845,7 @@ test('segment 500 should trigger MEDIA_ERR_ABORTED', function () { | ... | @@ -899,9 +845,7 @@ test('segment 500 should trigger MEDIA_ERR_ABORTED', function () { |
899 | type: 'application/vnd.apple.mpegurl' | 845 | type: 'application/vnd.apple.mpegurl' |
900 | }); | 846 | }); |
901 | 847 | ||
902 | player.hls.mediaSource.trigger({ | 848 | openMediaSource(player); |
903 | type: 'sourceopen' | ||
904 | }); | ||
905 | 849 | ||
906 | standardXHRResponse(requests[0]); | 850 | standardXHRResponse(requests[0]); |
907 | requests[1].respond(500); | 851 | requests[1].respond(500); |
... | @@ -914,9 +858,7 @@ test('duration is Infinity for live playlists', function() { | ... | @@ -914,9 +858,7 @@ test('duration is Infinity for live playlists', function() { |
914 | src: 'http://example.com/manifest/missingEndlist.m3u8', | 858 | src: 'http://example.com/manifest/missingEndlist.m3u8', |
915 | type: 'application/vnd.apple.mpegurl' | 859 | type: 'application/vnd.apple.mpegurl' |
916 | }); | 860 | }); |
917 | player.hls.mediaSource.trigger({ | 861 | openMediaSource(player); |
918 | type: 'sourceopen' | ||
919 | }); | ||
920 | 862 | ||
921 | standardXHRResponse(requests[0]); | 863 | standardXHRResponse(requests[0]); |
922 | 864 | ||
... | @@ -929,9 +871,7 @@ test('updates the media index when a playlist reloads', function() { | ... | @@ -929,9 +871,7 @@ test('updates the media index when a playlist reloads', function() { |
929 | src: 'http://example.com/live-updating.m3u8', | 871 | src: 'http://example.com/live-updating.m3u8', |
930 | type: 'application/vnd.apple.mpegurl' | 872 | type: 'application/vnd.apple.mpegurl' |
931 | }); | 873 | }); |
932 | player.hls.mediaSource.trigger({ | 874 | openMediaSource(player); |
933 | type: 'sourceopen' | ||
934 | }); | ||
935 | 875 | ||
936 | requests[0].respond(200, null, | 876 | requests[0].respond(200, null, |
937 | '#EXTM3U\n' + | 877 | '#EXTM3U\n' + |
... | @@ -971,9 +911,7 @@ test('mediaIndex is zero before the first segment loads', function() { | ... | @@ -971,9 +911,7 @@ test('mediaIndex is zero before the first segment loads', function() { |
971 | src: 'http://example.com/first-seg-load.m3u8', | 911 | src: 'http://example.com/first-seg-load.m3u8', |
972 | type: 'application/vnd.apple.mpegurl' | 912 | type: 'application/vnd.apple.mpegurl' |
973 | }); | 913 | }); |
974 | player.hls.mediaSource.trigger({ | 914 | openMediaSource(player); |
975 | type: 'sourceopen' | ||
976 | }); | ||
977 | 915 | ||
978 | strictEqual(player.hls.mediaIndex, 0, 'mediaIndex is zero'); | 916 | strictEqual(player.hls.mediaIndex, 0, 'mediaIndex is zero'); |
979 | }); | 917 | }); |
... | @@ -983,9 +921,7 @@ test('reloads out-of-date live playlists when switching variants', function() { | ... | @@ -983,9 +921,7 @@ test('reloads out-of-date live playlists when switching variants', function() { |
983 | src: 'http://example.com/master.m3u8', | 921 | src: 'http://example.com/master.m3u8', |
984 | type: 'application/vnd.apple.mpegurl' | 922 | type: 'application/vnd.apple.mpegurl' |
985 | }); | 923 | }); |
986 | player.hls.mediaSource.trigger({ | 924 | openMediaSource(player); |
987 | type: 'sourceopen' | ||
988 | }); | ||
989 | 925 | ||
990 | player.hls.master = { | 926 | player.hls.master = { |
991 | playlists: [{ | 927 | playlists: [{ |
... | @@ -1026,9 +962,7 @@ test('if withCredentials option is used, withCredentials is set on the XHR objec | ... | @@ -1026,9 +962,7 @@ test('if withCredentials option is used, withCredentials is set on the XHR objec |
1026 | src: 'http://example.com/media.m3u8', | 962 | src: 'http://example.com/media.m3u8', |
1027 | type: 'application/vnd.apple.mpegurl' | 963 | type: 'application/vnd.apple.mpegurl' |
1028 | }); | 964 | }); |
1029 | player.hls.mediaSource.trigger({ | 965 | openMediaSource(player); |
1030 | type: 'sourceopen' | ||
1031 | }); | ||
1032 | ok(requests[0].withCredentials, "with credentials should be set to true if that option is passed in"); | 966 | ok(requests[0].withCredentials, "with credentials should be set to true if that option is passed in"); |
1033 | }); | 967 | }); |
1034 | 968 | ||
... | @@ -1038,9 +972,7 @@ test('does not break if the playlist has no segments', function() { | ... | @@ -1038,9 +972,7 @@ test('does not break if the playlist has no segments', function() { |
1038 | type: 'application/vnd.apple.mpegurl' | 972 | type: 'application/vnd.apple.mpegurl' |
1039 | }); | 973 | }); |
1040 | try { | 974 | try { |
1041 | player.hls.mediaSource.trigger({ | 975 | openMediaSource(player); |
1042 | type: 'sourceopen' | ||
1043 | }); | ||
1044 | requests[0].respond(200, null, | 976 | requests[0].respond(200, null, |
1045 | '#EXTM3U\n' + | 977 | '#EXTM3U\n' + |
1046 | '#EXT-X-PLAYLIST-TYPE:VOD\n' + | 978 | '#EXT-X-PLAYLIST-TYPE:VOD\n' + |
... | @@ -1060,9 +992,7 @@ test('waits until the buffer is empty before appending bytes at a discontinuity' | ... | @@ -1060,9 +992,7 @@ test('waits until the buffer is empty before appending bytes at a discontinuity' |
1060 | src: 'disc.m3u8', | 992 | src: 'disc.m3u8', |
1061 | type: 'application/vnd.apple.mpegurl' | 993 | type: 'application/vnd.apple.mpegurl' |
1062 | }); | 994 | }); |
1063 | player.hls.mediaSource.trigger({ | 995 | openMediaSource(player); |
1064 | type: 'sourceopen' | ||
1065 | }); | ||
1066 | player.currentTime = function() { return currentTime; }; | 996 | player.currentTime = function() { return currentTime; }; |
1067 | player.buffered = function() { | 997 | player.buffered = function() { |
1068 | return videojs.createTimeRange(0, bufferEnd); | 998 | return videojs.createTimeRange(0, bufferEnd); |
... | @@ -1109,9 +1039,7 @@ test('clears the segment buffer on seek', function() { | ... | @@ -1109,9 +1039,7 @@ test('clears the segment buffer on seek', function() { |
1109 | src: 'disc.m3u8', | 1039 | src: 'disc.m3u8', |
1110 | type: 'application/vnd.apple.mpegurl' | 1040 | type: 'application/vnd.apple.mpegurl' |
1111 | }); | 1041 | }); |
1112 | player.hls.mediaSource.trigger({ | 1042 | openMediaSource(player); |
1113 | type: 'sourceopen' | ||
1114 | }); | ||
1115 | oldCurrentTime = player.currentTime; | 1043 | oldCurrentTime = player.currentTime; |
1116 | player.currentTime = function(time) { | 1044 | player.currentTime = function(time) { |
1117 | if (time !== undefined) { | 1045 | if (time !== undefined) { |
... | @@ -1158,9 +1086,7 @@ test('resets the switching algorithm if a request times out', function() { | ... | @@ -1158,9 +1086,7 @@ test('resets the switching algorithm if a request times out', function() { |
1158 | src: 'master.m3u8', | 1086 | src: 'master.m3u8', |
1159 | type: 'application/vnd.apple.mpegurl' | 1087 | type: 'application/vnd.apple.mpegurl' |
1160 | }); | 1088 | }); |
1161 | player.hls.mediaSource.trigger({ | 1089 | openMediaSource(player); |
1162 | type: 'sourceopen' | ||
1163 | }); | ||
1164 | standardXHRResponse(requests.shift()); // master | 1090 | standardXHRResponse(requests.shift()); // master |
1165 | standardXHRResponse(requests.shift()); // media.m3u8 | 1091 | standardXHRResponse(requests.shift()); // media.m3u8 |
1166 | // simulate a segment timeout | 1092 | // simulate a segment timeout |
... | @@ -1181,9 +1107,7 @@ test('disposes the playlist loader', function() { | ... | @@ -1181,9 +1107,7 @@ test('disposes the playlist loader', function() { |
1181 | src: 'manifest/master.m3u8', | 1107 | src: 'manifest/master.m3u8', |
1182 | type: 'application/vnd.apple.mpegurl' | 1108 | type: 'application/vnd.apple.mpegurl' |
1183 | }); | 1109 | }); |
1184 | player.hls.mediaSource.trigger({ | 1110 | openMediaSource(player); |
1185 | type: 'sourceopen' | ||
1186 | }); | ||
1187 | loaderDispose = player.hls.playlists.dispose; | 1111 | loaderDispose = player.hls.playlists.dispose; |
1188 | player.hls.playlists.dispose = function() { | 1112 | player.hls.playlists.dispose = function() { |
1189 | disposes++; | 1113 | disposes++; |
... | @@ -1232,9 +1156,7 @@ test('tracks the bytes downloaded', function() { | ... | @@ -1232,9 +1156,7 @@ test('tracks the bytes downloaded', function() { |
1232 | src: 'http://example.com/media.m3u8', | 1156 | src: 'http://example.com/media.m3u8', |
1233 | type: 'application/vnd.apple.mpegurl' | 1157 | type: 'application/vnd.apple.mpegurl' |
1234 | }); | 1158 | }); |
1235 | player.hls.mediaSource.trigger({ | 1159 | openMediaSource(player); |
1236 | type: 'sourceopen' | ||
1237 | }); | ||
1238 | 1160 | ||
1239 | strictEqual(player.hls.bytesReceived, 0, 'no bytes received'); | 1161 | strictEqual(player.hls.bytesReceived, 0, 'no bytes received'); |
1240 | 1162 | ||
... | @@ -1270,9 +1192,7 @@ test('re-emits mediachange events', function() { | ... | @@ -1270,9 +1192,7 @@ test('re-emits mediachange events', function() { |
1270 | src: 'http://example.com/media.m3u8', | 1192 | src: 'http://example.com/media.m3u8', |
1271 | type: 'application/vnd.apple.mpegurl' | 1193 | type: 'application/vnd.apple.mpegurl' |
1272 | }); | 1194 | }); |
1273 | player.hls.mediaSource.trigger({ | 1195 | openMediaSource(player); |
1274 | type: 'sourceopen' | ||
1275 | }); | ||
1276 | 1196 | ||
1277 | player.hls.playlists.trigger('mediachange'); | 1197 | player.hls.playlists.trigger('mediachange'); |
1278 | strictEqual(mediaChanges, 1, 'fired mediachange'); | 1198 | strictEqual(mediaChanges, 1, 'fired mediachange'); |
... | @@ -1302,4 +1222,48 @@ test('can be disposed before finishing initialization', function() { | ... | @@ -1302,4 +1222,48 @@ test('can be disposed before finishing initialization', function() { |
1302 | } | 1222 | } |
1303 | }); | 1223 | }); |
1304 | 1224 | ||
1225 | test('calls ended() on the media source at the end of a playlist', function() { | ||
1226 | var endOfStreams = 0; | ||
1227 | player.src({ | ||
1228 | src: 'http://example.com/media.m3u8', | ||
1229 | type: 'application/vnd.apple.mpegurl' | ||
1230 | }); | ||
1231 | openMediaSource(player); | ||
1232 | player.hls.mediaSource.endOfStream = function() { | ||
1233 | endOfStreams++; | ||
1234 | }; | ||
1235 | // playlist response | ||
1236 | requests.shift().respond(200, null, | ||
1237 | '#EXTM3U\n' + | ||
1238 | '#EXTINF:10,\n' + | ||
1239 | '0.ts\n' + | ||
1240 | '#EXT-X-ENDLIST\n'); | ||
1241 | // segment response | ||
1242 | requests[0].response = new ArrayBuffer(17); | ||
1243 | requests.shift().respond(200, null, ''); | ||
1244 | |||
1245 | strictEqual(endOfStreams, 1, 'ended media source'); | ||
1246 | }); | ||
1247 | |||
1248 | test('calling play() at the end of a video resets the media index', function() { | ||
1249 | player.src({ | ||
1250 | src: 'http://example.com/media.m3u8', | ||
1251 | type: 'application/vnd.apple.mpegurl' | ||
1252 | }); | ||
1253 | openMediaSource(player); | ||
1254 | requests.shift().respond(200, null, | ||
1255 | '#EXTM3U\n' + | ||
1256 | '#EXTINF:10,\n' + | ||
1257 | '0.ts\n' + | ||
1258 | '#EXT-X-ENDLIST\n'); | ||
1259 | standardXHRResponse(requests.shift()); | ||
1260 | |||
1261 | strictEqual(player.hls.mediaIndex, 1, 'index is 1 after the first segment'); | ||
1262 | player.hls.ended = function() { | ||
1263 | return true; | ||
1264 | }; | ||
1265 | player.play(); | ||
1266 | strictEqual(player.hls.mediaIndex, 0, 'index is 1 after the first segment'); | ||
1267 | }); | ||
1268 | |||
1305 | })(window, window.videojs); | 1269 | })(window, window.videojs); | ... | ... |
-
Please register or sign in to post a comment