b858aa92 by Steve Heffernan

Merge pull request #117 from heff/reorg-w-114

Reorg and endOfStream fix
2 parents 74ac9838 21c692d7
...@@ -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);
......
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);
......