b22f4c3f by Steve Heffernan

Reorganized videojs-hls.js to make it a bit more readable and set it up for further simplification

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