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,273 +9,222 @@ ...@@ -10,273 +9,222 @@
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 19 var
21 * @param right {object} a media playlist object 20 source = options.source,
22 * @return {number} Greater than zero if the bandwidth attribute of 21 settings = player.options();
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 22
41 /** 23 player.hls = this;
42 * A comparator function to sort two playlist object by resolution (width). 24 delete options.source;
43 * @param left {object} a media playlist object 25 options.swf = settings.flash.swf;
44 * @param right {object} a media playlist object 26 videojs.Flash.call(this, player, options, ready);
45 * @return {number} Greater than zero if the resolution.width attribute of 27 options.source = source;
46 * left is greater than the corresponding attribute of right. Less 28 this.bytesReceived = 0;
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 29
53 if (left.attributes && left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) { 30 // TODO: After video.js#1347 is pulled in move these to the prototype
54 leftWidth = left.attributes.RESOLUTION.width; 31 this.currentTime = function() {
32 if (this.lastSeekedTime_) {
33 return this.lastSeekedTime_;
34 }
35 // currentTime is zero while the tech is initializing
36 if (!this.el() || !this.el().vjs_getProperty) {
37 return 0;
38 }
39 return this.el().vjs_getProperty('currentTime');
40 };
41 this.setCurrentTime = function(currentTime) {
42 if (!(this.playlists && this.playlists.media())) {
43 // return immediately if the metadata is not ready yet
44 return 0;
55 } 45 }
56 46
57 leftWidth = leftWidth || window.Number.MAX_VALUE; 47 // save the seek target so currentTime can report it correctly
48 // while the seek is pending
49 this.lastSeekedTime_ = currentTime;
58 50
59 if (right.attributes && right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) { 51 // determine the requested segment
60 rightWidth = right.attributes.RESOLUTION.width; 52 this.mediaIndex = videojs.Hls.getMediaIndexByTime(this.playlists.media(), currentTime);
61 }
62 53
63 rightWidth = rightWidth || window.Number.MAX_VALUE; 54 // abort any segments still being decoded
55 this.sourceBuffer.abort();
64 56
65 // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions 57 // cancel outstanding requests and buffer appends
66 // have the same media dimensions/ resolution 58 if (this.segmentXhr_) {
67 if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) { 59 this.segmentXhr_.abort();
68 return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH;
69 } else {
70 return leftWidth - rightWidth;
71 } 60 }
72 },
73
74 xhr,
75 61
76 /** 62 // clear out any buffered segments
77 * TODO - Document this great feature. 63 this.segmentBuffer_ = [];
78 *
79 * @param playlist
80 * @param time
81 * @returns int
82 */
83 getMediaIndexByTime = function(playlist, time) {
84 var index, counter, timeRanges, currentSegmentRange;
85 64
86 timeRanges = []; 65 // begin filling the buffer at the new position
87 for (index = 0; index < playlist.segments.length; index++) { 66 this.fillBuffer(currentTime * 1000);
88 currentSegmentRange = {}; 67 };
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 68
94 for (counter = 0; counter < timeRanges.length; counter++) { 69 videojs.Hls.prototype.src.call(this, options.source && options.source.src);
95 if (time >= timeRanges[counter].start && time < timeRanges[counter].end) {
96 return counter;
97 }
98 } 70 }
71 });
99 72
100 return -1; 73 // Add HLS to the standard tech order
74 videojs.options.techOrder.unshift('hls');
101 75
102 }, 76 // the desired length of video to maintain in the buffer, in seconds
77 videojs.Hls.GOAL_BUFFER_LENGTH = 30;
103 78
104 /** 79 videojs.Hls.prototype.src = function(src) {
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 80 var
120 i, 81 self = this,
121 originalSegment; 82 mediaSource,
122 83 source;
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 84
132 // try to sync based on URI 85 if (src) {
133 i = update.segments.length; 86 this.src_ = src;
134 originalSegment = original.segments[mediaIndex - 1];
135 while (i--) {
136 if (originalSegment.uri === update.segments[i].uri) {
137 return i + 1;
138 }
139 }
140 87
141 // sync on media sequence 88 mediaSource = new videojs.MediaSource();
142 return (original.mediaSequence + mediaIndex) - update.mediaSequence; 89 source = {
143 }, 90 src: videojs.URL.createObjectURL(mediaSource),
91 type: "video/flv"
92 };
93 this.mediaSource = mediaSource;
144 94
145 /** 95 this.segmentBuffer_ = [];
146 * Calculate the duration of a playlist from a given start index to a given 96 this.segmentParser_ = new videojs.Hls.SegmentParser();
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 97
160 startIndex = startIndex || 0; 98 // load the MediaSource into the player
161 endIndex = endIndex !== undefined ? endIndex : (playlist.segments || []).length; 99 this.mediaSource.addEventListener('sourceopen', videojs.bind(this, this.handleSourceOpen));
162 i = endIndex - 1;
163 100
164 for (; i >= startIndex; i--) { 101 this.player().ready(function() {
165 segment = playlist.segments[i]; 102 // do nothing if the tech has been disposed already
166 dur += segment.duration || playlist.targetDuration || 0; 103 // this can occur if someone sets the src in player.ready(), for instance
104 if (!self.el()) {
105 return;
167 } 106 }
168 107 self.el().vjs_src(source.src);
169 return dur; 108 });
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 } 109 }
110 };
181 111
182 // if present, use the duration specified in the playlist 112 videojs.Hls.prototype.handleSourceOpen = function() {
183 if (playlist.totalDuration) { 113 // construct the video data buffer and set the appropriate MIME type
184 return playlist.totalDuration; 114 var
185 } 115 player = this.player(),
116 settings = player.options().hls || {},
117 sourceBuffer = this.mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"'),
118 oldMediaPlaylist;
186 119
187 // duration should be Infinity for live playlists 120 this.sourceBuffer = sourceBuffer;
188 if (!playlist.endList) { 121 sourceBuffer.appendBuffer(this.segmentParser_.getFlvHeader());
189 return window.Infinity;
190 }
191 122
192 return duration(playlist); 123 this.mediaIndex = 0;
193 }, 124 this.playlists = new videojs.Hls.PlaylistLoader(this.src_, settings.withCredentials);
194 125
195 resolveUrl, 126 this.playlists.on('loadedmetadata', videojs.bind(this, function() {
127 oldMediaPlaylist = this.playlists.media();
196 128
197 initSource = function(player, mediaSource, srcUrl) { 129 // periodically check if new data needs to be downloaded or
198 var 130 // buffered data should be appended to the source buffer
199 segmentParser = new videojs.Hls.SegmentParser(), 131 this.fillBuffer();
200 settings = videojs.util.mergeOptions({}, player.options().hls), 132 player.on('timeupdate', videojs.bind(this, this.fillBuffer));
201 segmentBuffer = [], 133 player.on('timeupdate', videojs.bind(this, this.drainBuffer));
134 player.on('waiting', videojs.bind(this, this.drainBuffer));
202 135
203 lastSeekedTime, 136 player.trigger('loadedmetadata');
204 segmentXhr, 137 }));
205 fillBuffer,
206 drainBuffer,
207 updateDuration;
208 138
139 this.playlists.on('error', videojs.bind(this, function() {
140 player.error(this.playlists.error);
141 }));
209 142
210 player.hls.currentTime = function() { 143 this.playlists.on('loadedplaylist', videojs.bind(this, function() {
211 if (lastSeekedTime) { 144 var updatedPlaylist = this.playlists.media();
212 return lastSeekedTime;
213 }
214 // currentTime is zero while the tech is initializing
215 if (!this.el() || !this.el().vjs_getProperty) {
216 return 0;
217 }
218 return this.el().vjs_getProperty('currentTime');
219 };
220 145
221 player.hls.setCurrentTime = function(currentTime) { 146 if (!updatedPlaylist) {
222 if (!(this.playlists && this.playlists.media())) { 147 // do nothing before an initial media playlist has been activated
223 // return immediately if the metadata is not ready yet 148 return;
224 return 0;
225 } 149 }
226 150
227 // save the seek target so currentTime can report it correctly 151 this.updateDuration(this.playlists.media());
228 // while the seek is pending 152 this.mediaIndex = videojs.Hls.translateMediaIndex(this.mediaIndex, oldMediaPlaylist, updatedPlaylist);
229 lastSeekedTime = currentTime; 153 oldMediaPlaylist = updatedPlaylist;
230 154 }));
231 // determine the requested segment
232 this.mediaIndex =
233 getMediaIndexByTime(this.playlists.media(), currentTime);
234 155
235 // abort any segments still being decoded 156 this.playlists.on('mediachange', function() {
236 this.sourceBuffer.abort(); 157 player.trigger('mediachange');
158 });
159 };
237 160
238 // cancel outstanding requests and buffer appends 161 /**
239 if (segmentXhr) { 162 * Reset the mediaIndex if play() is called after the video has
240 segmentXhr.abort(); 163 * ended.
164 */
165 videojs.Hls.prototype.play = function() {
166 if (this.ended()) {
167 this.mediaIndex = 0;
241 } 168 }
242 169
243 // clear out any buffered segments 170 // delegate back to the Flash implementation
244 segmentBuffer = []; 171 return videojs.Flash.prototype.play.apply(this, arguments);
172 };
245 173
246 // begin filling the buffer at the new position 174 videojs.Hls.prototype.duration = function() {
247 fillBuffer(currentTime * 1000); 175 var playlists = this.playlists;
248 }; 176 if (playlists) {
177 return videojs.Hls.getPlaylistTotalDuration(playlists.media());
178 }
179 return 0;
180 };
249 181
250 /** 182 /**
251 * Update the player duration 183 * Update the player duration
252 */ 184 */
253 updateDuration = function(playlist) { 185 videojs.Hls.prototype.updateDuration = function(playlist) {
254 var oldDuration = player.duration(), 186 var player = this.player(),
255 newDuration = totalDuration(playlist); 187 oldDuration = player.duration(),
188 newDuration = videojs.Hls.getPlaylistTotalDuration(playlist);
256 189
257 // if the duration has changed, invalidate the cached value 190 // if the duration has changed, invalidate the cached value
258 if (oldDuration !== newDuration) { 191 if (oldDuration !== newDuration) {
259 player.trigger('durationchange'); 192 player.trigger('durationchange');
260 } 193 }
261 }; 194 };
262 195
263 /** 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 /**
264 * Chooses the appropriate media playlist based on the current 211 * Chooses the appropriate media playlist based on the current
265 * bandwidth estimate and the player size. 212 * bandwidth estimate and the player size.
266 * @return the highest bitrate playlist less than the currently detected 213 * @return the highest bitrate playlist less than the currently detected
267 * bandwidth, accounting for some amount of bandwidth variance 214 * bandwidth, accounting for some amount of bandwidth variance
268 */ 215 */
269 player.hls.selectPlaylist = function () { 216 videojs.Hls.prototype.selectPlaylist = function () {
270 var 217 var
218 player = this.player(),
271 effectiveBitrate, 219 effectiveBitrate,
272 sortedPlaylists = player.hls.playlists.master.playlists.slice(), 220 sortedPlaylists = this.playlists.master.playlists.slice(),
273 bandwidthPlaylists = [], 221 bandwidthPlaylists = [],
274 i = sortedPlaylists.length, 222 i = sortedPlaylists.length,
275 variant, 223 variant,
276 bandwidthBestVariant, 224 bandwidthBestVariant,
277 resolutionBestVariant; 225 resolutionBestVariant;
278 226
279 sortedPlaylists.sort(playlistBandwidth); 227 sortedPlaylists.sort(videojs.Hls.comparePlaylistBandwidth);
280 228
281 // filter out any variant that has greater effective bitrate 229 // filter out any variant that has greater effective bitrate
282 // than the current estimated bandwidth 230 // than the current estimated bandwidth
...@@ -304,7 +252,7 @@ var ...@@ -304,7 +252,7 @@ var
304 i = bandwidthPlaylists.length; 252 i = bandwidthPlaylists.length;
305 253
306 // sort variants by resolution 254 // sort variants by resolution
307 bandwidthPlaylists.sort(playlistResolution); 255 bandwidthPlaylists.sort(videojs.Hls.comparePlaylistResolution);
308 256
309 // iterate through the bandwidth-filtered playlists and find 257 // iterate through the bandwidth-filtered playlists and find
310 // best rendition by player dimension 258 // best rendition by player dimension
...@@ -331,30 +279,19 @@ var ...@@ -331,30 +279,19 @@ var
331 279
332 // fallback chain of variants 280 // fallback chain of variants
333 return resolutionBestVariant || bandwidthBestVariant || sortedPlaylists[0]; 281 return resolutionBestVariant || bandwidthBestVariant || sortedPlaylists[0];
334 }; 282 };
335
336 /**
337 * Abort all outstanding work and cleanup.
338 */
339 player.hls.dispose = function() {
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 };
349 283
350 /** 284 /**
351 * Determines whether there is enough video data currently in the buffer 285 * Determines whether there is enough video data currently in the buffer
352 * and downloads a new segment if the buffered time is less than the goal. 286 * 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 287 * @param offset (optional) {number} the offset into the downloaded segment
354 * to seek to, in milliseconds 288 * to seek to, in milliseconds
355 */ 289 */
356 fillBuffer = function(offset) { 290 videojs.Hls.prototype.fillBuffer = function(offset) {
357 var 291 var
292 self = this,
293 player = this.player(),
294 settings = player.options().hls || {},
358 buffered = player.buffered(), 295 buffered = player.buffered(),
359 bufferedTime = 0, 296 bufferedTime = 0,
360 segment, 297 segment,
...@@ -362,18 +299,18 @@ var ...@@ -362,18 +299,18 @@ var
362 startTime; 299 startTime;
363 300
364 // if there is a request already in flight, do nothing 301 // if there is a request already in flight, do nothing
365 if (segmentXhr) { 302 if (this.segmentXhr_) {
366 return; 303 return;
367 } 304 }
368 305
369 // if no segments are available, do nothing 306 // if no segments are available, do nothing
370 if (player.hls.playlists.state === "HAVE_NOTHING" || 307 if (this.playlists.state === "HAVE_NOTHING" ||
371 !player.hls.playlists.media().segments) { 308 !this.playlists.media().segments) {
372 return; 309 return;
373 } 310 }
374 311
375 // if the video has finished downloading, stop trying to buffer 312 // if the video has finished downloading, stop trying to buffer
376 segment = player.hls.playlists.media().segments[player.hls.mediaIndex]; 313 segment = this.playlists.media().segments[this.mediaIndex];
377 if (!segment) { 314 if (!segment) {
378 return; 315 return;
379 } 316 }
...@@ -391,17 +328,17 @@ var ...@@ -391,17 +328,17 @@ var
391 } 328 }
392 329
393 // resolve the segment URL relative to the playlist 330 // resolve the segment URL relative to the playlist
394 if (player.hls.playlists.media().uri === srcUrl) { 331 if (this.playlists.media().uri === this.src_) {
395 segmentUri = resolveUrl(srcUrl, segment.uri); 332 segmentUri = resolveUrl(this.src_, segment.uri);
396 } else { 333 } else {
397 segmentUri = resolveUrl(resolveUrl(srcUrl, player.hls.playlists.media().uri || ''), 334 segmentUri = resolveUrl(resolveUrl(this.src_, this.playlists.media().uri || ''),
398 segment.uri); 335 segment.uri);
399 } 336 }
400 337
401 startTime = +new Date(); 338 startTime = +new Date();
402 339
403 // request the next segment 340 // request the next segment
404 segmentXhr = xhr({ 341 this.segmentXhr_ = videojs.Hls.xhr({
405 url: segmentUri, 342 url: segmentUri,
406 responseType: 'arraybuffer', 343 responseType: 'arraybuffer',
407 withCredentials: settings.withCredentials 344 withCredentials: settings.withCredentials
...@@ -409,23 +346,23 @@ var ...@@ -409,23 +346,23 @@ var
409 var tags; 346 var tags;
410 347
411 // the segment request is no longer outstanding 348 // the segment request is no longer outstanding
412 segmentXhr = null; 349 self.segmentXhr_ = null;
413 350
414 if (error) { 351 if (error) {
415 // if a segment request times out, we may have better luck with another playlist 352 // if a segment request times out, we may have better luck with another playlist
416 if (error === 'timeout') { 353 if (error === 'timeout') {
417 player.hls.bandwidth = 1; 354 self.bandwidth = 1;
418 return player.hls.playlists.media(player.hls.selectPlaylist()); 355 return self.playlists.media(self.selectPlaylist());
419 } 356 }
420 // otherwise, try jumping ahead to the next segment 357 // otherwise, try jumping ahead to the next segment
421 player.hls.error = { 358 self.error = {
422 status: this.status, 359 status: this.status,
423 message: 'HLS segment request error at URL: ' + url, 360 message: 'HLS segment request error at URL: ' + url,
424 code: (this.status >= 500) ? 4 : 2 361 code: (this.status >= 500) ? 4 : 2
425 }; 362 };
426 363
427 // try moving on to the next segment 364 // try moving on to the next segment
428 player.hls.mediaIndex++; 365 self.mediaIndex++;
429 return; 366 return;
430 } 367 }
431 368
...@@ -435,39 +372,39 @@ var ...@@ -435,39 +372,39 @@ var
435 } 372 }
436 373
437 // calculate the download bandwidth 374 // calculate the download bandwidth
438 player.hls.segmentXhrTime = (+new Date()) - startTime; 375 self.segmentXhrTime = (+new Date()) - startTime;
439 player.hls.bandwidth = (this.response.byteLength / player.hls.segmentXhrTime) * 8 * 1000; 376 self.bandwidth = (this.response.byteLength / player.hls.segmentXhrTime) * 8 * 1000;
440 player.hls.bytesReceived += this.response.byteLength; 377 self.bytesReceived += this.response.byteLength;
441 378
442 // transmux the segment data from MP2T to FLV 379 // transmux the segment data from MP2T to FLV
443 segmentParser.parseSegmentBinaryData(new Uint8Array(this.response)); 380 self.segmentParser_.parseSegmentBinaryData(new Uint8Array(this.response));
444 segmentParser.flushTags(); 381 self.segmentParser_.flushTags();
445 382
446 // package up all the work to append the segment 383 // package up all the work to append the segment
447 // if the segment is the start of a timestamp discontinuity, 384 // if the segment is the start of a timestamp discontinuity,
448 // we have to wait until the sourcebuffer is empty before 385 // we have to wait until the sourcebuffer is empty before
449 // aborting the source buffer processing 386 // aborting the source buffer processing
450 tags = []; 387 tags = [];
451 while (segmentParser.tagsAvailable()) { 388 while (self.segmentParser_.tagsAvailable()) {
452 tags.push(segmentParser.getNextTag()); 389 tags.push(self.segmentParser_.getNextTag());
453 } 390 }
454 segmentBuffer.push({ 391 self.segmentBuffer_.push({
455 mediaIndex: player.hls.mediaIndex, 392 mediaIndex: self.mediaIndex,
456 playlist: player.hls.playlists.media(), 393 playlist: self.playlists.media(),
457 offset: offset, 394 offset: offset,
458 tags: tags 395 tags: tags
459 }); 396 });
460 drainBuffer(); 397 self.drainBuffer();
461 398
462 player.hls.mediaIndex++; 399 self.mediaIndex++;
463 400
464 // figure out what stream the next segment should be downloaded from 401 // figure out what stream the next segment should be downloaded from
465 // with the updated bandwidth information 402 // with the updated bandwidth information
466 player.hls.playlists.media(player.hls.selectPlaylist()); 403 self.playlists.media(self.selectPlaylist());
467 }); 404 });
468 }; 405 };
469 406
470 drainBuffer = function(event) { 407 videojs.Hls.prototype.drainBuffer = function(event) {
471 var 408 var
472 i = 0, 409 i = 0,
473 mediaIndex, 410 mediaIndex,
...@@ -477,7 +414,8 @@ var ...@@ -477,7 +414,8 @@ var
477 segment, 414 segment,
478 415
479 ptsTime, 416 ptsTime,
480 segmentOffset; 417 segmentOffset,
418 segmentBuffer = this.segmentBuffer_;
481 419
482 if (!segmentBuffer.length) { 420 if (!segmentBuffer.length) {
483 return; 421 return;
...@@ -490,7 +428,7 @@ var ...@@ -490,7 +428,7 @@ var
490 segment = playlist.segments[mediaIndex]; 428 segment = playlist.segments[mediaIndex];
491 429
492 event = event || {}; 430 event = event || {};
493 segmentOffset = duration(playlist, 0, mediaIndex) * 1000; 431 segmentOffset = videojs.Hls.getPlaylistDuration(playlist, 0, mediaIndex) * 1000;
494 432
495 // abort() clears any data queued in the source buffer so wait 433 // abort() clears any data queued in the source buffer so wait
496 // until it empties before calling it when a discontinuity is 434 // until it empties before calling it when a discontinuity is
...@@ -499,9 +437,9 @@ var ...@@ -499,9 +437,9 @@ var
499 if (event.type !== 'waiting') { 437 if (event.type !== 'waiting') {
500 return; 438 return;
501 } 439 }
502 player.hls.sourceBuffer.abort(); 440 this.sourceBuffer.abort();
503 // tell the SWF where playback is continuing in the stitched timeline 441 // tell the SWF where playback is continuing in the stitched timeline
504 player.hls.el().vjs_setProperty('currentTime', segmentOffset * 0.001); 442 this.el().vjs_setProperty('currentTime', segmentOffset * 0.001);
505 } 443 }
506 444
507 // if we're refilling the buffer after a seek, scan through the muxed 445 // if we're refilling the buffer after a seek, scan through the muxed
...@@ -515,11 +453,11 @@ var ...@@ -515,11 +453,11 @@ var
515 } 453 }
516 454
517 // tell the SWF where we will be seeking to 455 // tell the SWF where we will be seeking to
518 player.hls.el().vjs_setProperty('currentTime', (tags[i].pts - tags[0].pts + segmentOffset) * 0.001); 456 this.el().vjs_setProperty('currentTime', (tags[i].pts - tags[0].pts + segmentOffset) * 0.001);
519 457
520 tags = tags.slice(i); 458 tags = tags.slice(i);
521 459
522 lastSeekedTime = null; 460 this.lastSeekedTime_ = null;
523 } 461 }
524 462
525 for (i = 0; i < tags.length; i++) { 463 for (i = 0; i < tags.length; i++) {
...@@ -527,83 +465,18 @@ var ...@@ -527,83 +465,18 @@ var
527 // the queue gives control back to the browser between tags 465 // the queue gives control back to the browser between tags
528 // so that large segments don't cause a "hiccup" in playback 466 // so that large segments don't cause a "hiccup" in playback
529 467
530 player.hls.sourceBuffer.appendBuffer(tags[i].bytes, player); 468 this.sourceBuffer.appendBuffer(tags[i].bytes, this.player());
531 } 469 }
532 470
533 // we're done processing this segment 471 // we're done processing this segment
534 segmentBuffer.shift(); 472 segmentBuffer.shift();
535 473
536 if (mediaIndex === playlist.segments.length) { 474 // transition the sourcebuffer to the ended state if we've hit the end of
537 mediaSource.endOfStream(); 475 // the playlist
538 } 476 if (mediaIndex + 1 === playlist.segments.length) {
539 }; 477 this.mediaSource.endOfStream();
540
541 // load the MediaSource into the player
542 mediaSource.addEventListener('sourceopen', function() {
543 // construct the video data buffer and set the appropriate MIME type
544 var
545 sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"'),
546 oldMediaPlaylist;
547
548 player.hls.sourceBuffer = sourceBuffer;
549 sourceBuffer.appendBuffer(segmentParser.getFlvHeader());
550
551 player.hls.mediaIndex = 0;
552 player.hls.playlists =
553 new videojs.Hls.PlaylistLoader(srcUrl, settings.withCredentials);
554 player.hls.playlists.on('loadedmetadata', function() {
555 oldMediaPlaylist = player.hls.playlists.media();
556
557 // periodically check if new data needs to be downloaded or
558 // buffered data should be appended to the source buffer
559 fillBuffer();
560 player.on('timeupdate', fillBuffer);
561 player.on('timeupdate', drainBuffer);
562 player.on('waiting', drainBuffer);
563
564 player.trigger('loadedmetadata');
565 });
566 player.hls.playlists.on('error', function() {
567 player.error(player.hls.playlists.error);
568 });
569 player.hls.playlists.on('loadedplaylist', function() {
570 var updatedPlaylist = player.hls.playlists.media();
571
572 if (!updatedPlaylist) {
573 // do nothing before an initial media playlist has been activated
574 return;
575 } 478 }
576 479 };
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 });
587 };
588
589 var mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i;
590
591 videojs.Hls = videojs.Flash.extend({
592 init: function(player, options, ready) {
593 var
594 source = options.source,
595 settings = player.options();
596
597 player.hls = this;
598 delete options.source;
599 options.swf = settings.flash.swf;
600 videojs.Flash.call(this, player, options, ready);
601 options.source = source;
602 this.bytesReceived = 0;
603
604 videojs.Hls.prototype.src.call(this, options.source && options.source.src);
605 }
606 });
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 }
535
536 return dur;
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 }
695 548
696 if (typeof callback !== 'function') { 549 // if present, use the duration specified in the playlist
697 callback = function() {}; 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;
580
581 // no segments have been loaded from the original playlist
582 if (mediaIndex === 0) {
583 return 0;
584 }
585 if (!(update && update.segments)) {
586 // let the media index be zero when there are no segments defined
587 return 0;
588 }
708 589
709 if (options.responseType) { 590 // try to sync based on URI
710 request.responseType = options.responseType; 591 i = update.segments.length;
592 originalSegment = original.segments[mediaIndex - 1];
593 while (i--) {
594 if (originalSegment.uri === update.segments[i].uri) {
595 return i + 1;
711 } 596 }
712 if (options.withCredentials) {
713 request.withCredentials = true;
714 } 597 }
715 if (options.timeout) { 598
716 if (request.timeout === 0) { 599 // sync on media sequence
717 request.timeout = options.timeout; 600 return (original.mediaSequence + mediaIndex) - update.mediaSequence;
718 request.ontimeout = function() { 601 };
719 request.timedout = true; 602
720 }; 603 /**
721 } else { 604 * TODO - Document this great feature.
722 // polyfill XHR2 by aborting after the timeout 605 *
723 abortTimeout = window.setTimeout(function() { 606 * @param playlist
724 if (request.readyState !== 4) { 607 * @param time
725 request.timedout = true; 608 * @returns int
726 request.abort(); 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);
727 } 619 }
728 }, options.timeout); 620
621 for (counter = 0; counter < timeRanges.length; counter++) {
622 if (time >= timeRanges[counter].start && time < timeRanges[counter].end) {
623 return counter;
729 } 624 }
730 } 625 }
731 626
732 request.onreadystatechange = function() { 627 return -1;
733 // wait until the request completes 628 };
734 if (this.readyState !== 4) { 629
735 return; 630 /**
631 * A comparator function to sort two playlist object by bandwidth.
632 * @param left {object} a media playlist object
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;
736 } 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 };
737 652
738 // clear outstanding timeouts 653 /**
739 window.clearTimeout(abortTimeout); 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;
740 664
741 // request timeout 665 if (left.attributes && left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) {
742 if (request.timedout) { 666 leftWidth = left.attributes.RESOLUTION.width;
743 return callback.call(this, 'timeout', url);
744 } 667 }
745 668
746 // request aborted or errored 669 leftWidth = leftWidth || window.Number.MAX_VALUE;
747 if (this.status >= 400 || this.status === 0) { 670
748 return callback.call(this, true, url); 671 if (right.attributes && right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) {
672 rightWidth = right.attributes.RESOLUTION.width;
749 } 673 }
750 674
751 return callback.call(this, false, url); 675 rightWidth = rightWidth || window.Number.MAX_VALUE;
752 }; 676
753 request.send(null); 677 // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions
754 return request; 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);
......