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