7c3935d5 by David LaPalomento

Yield control to the browser between appending FLV tags

Pushing an entire segment worth of FLV tags into the source buffer at once caused noticeable delays with high-bitrate segments. Instead, wrap each call to appendBuffer in a setTimeout of zero so that the browser has a chance to render frames while the segment is being transferred to the SWF. Make sure that appends-in-progress are cleared if a seek is initiated.
1 parent 03abd4f0
...@@ -22,7 +22,9 @@ module.exports = function(grunt) { ...@@ -22,7 +22,9 @@ module.exports = function(grunt) {
22 stripBanners: true 22 stripBanners: true
23 }, 23 },
24 dist: { 24 dist: {
25 src: ['src/video-js-hls.js', 25 nonull: true,
26 src: ['src/videojs-hls.js',
27 'src/async-queue.js',
26 'src/flv-tag.js', 28 'src/flv-tag.js',
27 'src/exp-golomb.js', 29 'src/exp-golomb.js',
28 'src/h264-stream.js', 30 'src/h264-stream.js',
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
14 14
15 <!-- HLS plugin --> 15 <!-- HLS plugin -->
16 <script src="src/videojs-hls.js"></script> 16 <script src="src/videojs-hls.js"></script>
17 <script src="src/async-queue.js"></script>
17 18
18 <!-- segment handling --> 19 <!-- segment handling -->
19 <script src="src/flv-tag.js"></script> 20 <script src="src/flv-tag.js"></script>
......
1 (function(window, videojs, undefined) {
2 'use strict';
3 /**
4 * A queue object that manages tasks that should be processed
5 * serially but asynchronously. Loosely adapted from
6 * https://github.com/caolan/async#queue.
7 * @param worker {function} the callback to invoke with each value
8 * pushed onto the queue
9 * @return {object} an object with an array of `tasks` that remain to
10 * be processed and function `push` to add new tasks
11 */
12 videojs.hls.queue = function(worker) {
13 var
14 q = {
15 tasks: [],
16 running: false,
17 push: function(task) {
18 q.tasks.push(task);
19 if (!q.running) {
20 window.setTimeout(process, 0);
21 q.running = true;
22 }
23 },
24 },
25 process = function() {
26 var task;
27 if (q.tasks.length) {
28 task = q.tasks.shift();
29 worker.call(this, task);
30 window.setTimeout(process, 0);
31 } else {
32 q.running = false;
33 }
34 };
35 return q;
36 };
37 })(window, window.videojs);
...@@ -187,6 +187,15 @@ var ...@@ -187,6 +187,15 @@ var
187 mediaSource = new videojs.MediaSource(), 187 mediaSource = new videojs.MediaSource(),
188 segmentParser = new videojs.hls.SegmentParser(), 188 segmentParser = new videojs.hls.SegmentParser(),
189 player = this, 189 player = this,
190
191 // async queue of Uint8Arrays to be appended to the SourceBuffer
192 tags = videojs.hls.queue(function(tag) {
193 player.hls.sourceBuffer.appendBuffer(tag, player);
194
195 if (player.hls.mediaIndex === player.hls.media.segments.length) {
196 mediaSource.endOfStream();
197 }
198 }),
190 srcUrl, 199 srcUrl,
191 200
192 segmentXhr, 201 segmentXhr,
...@@ -268,9 +277,14 @@ var ...@@ -268,9 +277,14 @@ var
268 player.on('seeking', function() { 277 player.on('seeking', function() {
269 var currentTime = player.currentTime(); 278 var currentTime = player.currentTime();
270 player.hls.mediaIndex = getMediaIndexByTime(player.hls.media, currentTime); 279 player.hls.mediaIndex = getMediaIndexByTime(player.hls.media, currentTime);
280
281 // cancel outstanding requests and buffer appends
271 if (segmentXhr) { 282 if (segmentXhr) {
272 segmentXhr.abort(); 283 segmentXhr.abort();
273 } 284 }
285 tags.tasks = [];
286
287 // begin filling the buffer at the new position
274 fillBuffer(currentTime * 1000); 288 fillBuffer(currentTime * 1000);
275 }); 289 });
276 290
...@@ -535,16 +549,14 @@ var ...@@ -535,16 +549,14 @@ var
535 } 549 }
536 550
537 while (segmentParser.tagsAvailable()) { 551 while (segmentParser.tagsAvailable()) {
538 player.hls.sourceBuffer.appendBuffer(segmentParser.getNextTag().bytes, player); 552 // queue up the bytes to be appended to the SourceBuffer
553 // the queue gives control back to the browser between tags
554 // so that large segments don't cause a "hiccup" in playback
555 tags.push(segmentParser.getNextTag().bytes);
539 } 556 }
540 557
541 player.hls.mediaIndex++; 558 player.hls.mediaIndex++;
542 559
543 if (player.hls.mediaIndex === player.hls.media.segments.length) {
544 mediaSource.endOfStream();
545 return;
546 }
547
548 // figure out what stream the next segment should be downloaded from 560 // figure out what stream the next segment should be downloaded from
549 // with the updated bandwidth information 561 // with the updated bandwidth information
550 playlist = player.hls.selectPlaylist(); 562 playlist = player.hls.selectPlaylist();
......
1 (function(window, queue, undefined) {
2 var
3 oldSetTimeout,
4 callbacks;
5 module('async queue', {
6 setup: function() {
7 oldSetTimeout = window.setTimeout;
8 callbacks = [];
9 window.setTimeout = function(callback) {
10 callbacks.push(callback);
11 };
12 },
13 teardown: function() {
14 window.setTimeout = oldSetTimeout;
15 }
16 });
17
18 test('runs tasks asynchronously', function() {
19 var
20 run = false,
21 q = queue(function() {
22 run = true;
23 });
24 q.push(1);
25
26 ok(!run, 'tasks are not run immediately');
27
28 callbacks[0]();
29 ok(run, 'tasks are run asynchronously');
30 });
31
32 test('runs one task at a time', function() {
33 var q = queue(function() {});
34 q.push(1);
35 q.push(2);
36 q.push(3);
37 q.push(4);
38 q.push(5);
39
40 strictEqual(q.tasks.length, 5, 'all tasks are queued');
41 strictEqual(1, callbacks.length, 'one callback is registered');
42 });
43
44 test('tasks are scheduled until the queue is empty', function() {
45 var q = queue(function() {});
46 q.push(1);
47 q.push(2);
48
49 callbacks.shift()();
50 strictEqual(1, callbacks.length, 'the next task is scheduled');
51 });
52
53 test('can be emptied at any time', function() {
54 var
55 runs = 0,
56 q = queue(function() {
57 runs++;
58 });
59 q.push(1);
60 q.push(2);
61
62 callbacks.shift()();
63 strictEqual(1, runs, 'task one is run');
64
65 q.tasks = [];
66 callbacks.shift()();
67 strictEqual(1, runs, 'the remaining tasks are cancelled');
68 });
69 })(window, window.videojs.hls.queue);
...@@ -31,6 +31,9 @@ ...@@ -31,6 +31,9 @@
31 <script src="tsSegment-bc.js"></script> 31 <script src="tsSegment-bc.js"></script>
32 <script src="../src/bin-utils.js"></script> 32 <script src="../src/bin-utils.js"></script>
33 33
34 <!-- async queue -->
35 <script src="../src/async-queue.js"></script>
36
34 <!-- Test cases --> 37 <!-- Test cases -->
35 <script> 38 <script>
36 module('environment'); 39 module('environment');
...@@ -44,6 +47,7 @@ ...@@ -44,6 +47,7 @@
44 <script src="exp-golomb_test.js"></script> 47 <script src="exp-golomb_test.js"></script>
45 <script src="flv-tag_test.js"></script> 48 <script src="flv-tag_test.js"></script>
46 <script src="m3u8_test.js"></script> 49 <script src="m3u8_test.js"></script>
50 <script src="async-queue_test.js"></script>
47 </head> 51 </head>
48 <body> 52 <body>
49 <div id="qunit"></div> 53 <div id="qunit"></div>
......
...@@ -25,9 +25,32 @@ var ...@@ -25,9 +25,32 @@ var
25 oldFlashSupported, 25 oldFlashSupported,
26 oldXhr, 26 oldXhr,
27 oldSegmentParser, 27 oldSegmentParser,
28 oldSetTimeout,
28 oldSourceBuffer, 29 oldSourceBuffer,
29 oldSupportsNativeHls, 30 oldSupportsNativeHls,
30 xhrUrls; 31 xhrUrls,
32
33 mockSegmentParser = function(tags) {
34 if (tags === undefined) {
35 tags = [];
36 }
37 return function() {
38 this.getFlvHeader = function() {
39 return 'flv';
40 };
41 this.parseSegmentBinaryData = function() {};
42 this.flushTags = function() {};
43 this.tagsAvailable = function() {
44 return tags.length;
45 };
46 this.getTags = function() {
47 return tags;
48 };
49 this.getNextTag = function() {
50 return tags.shift();
51 };
52 };
53 };
31 54
32 module('HLS', { 55 module('HLS', {
33 setup: function() { 56 setup: function() {
...@@ -59,10 +82,11 @@ module('HLS', { ...@@ -59,10 +82,11 @@ module('HLS', {
59 return videojs.createTimeRange(0, 0); 82 return videojs.createTimeRange(0, 0);
60 }; 83 };
61 84
62 // store the SegmentParser so it can be easily mocked 85 // store functionality that some tests need to mock
63 oldSegmentParser = videojs.hls.SegmentParser; 86 oldSegmentParser = videojs.hls.SegmentParser;
87 oldSetTimeout = window.setTimeout;
64 88
65 // make XHR synchronous 89 // make XHRs synchronous
66 oldXhr = window.XMLHttpRequest; 90 oldXhr = window.XMLHttpRequest;
67 window.XMLHttpRequest = function() { 91 window.XMLHttpRequest = function() {
68 this.open = function(method, url) { 92 this.open = function(method, url) {
...@@ -89,6 +113,7 @@ module('HLS', { ...@@ -89,6 +113,7 @@ module('HLS', {
89 videojs.hls.supportsNativeHls = oldSupportsNativeHls; 113 videojs.hls.supportsNativeHls = oldSupportsNativeHls;
90 videojs.hls.SegmentParser = oldSegmentParser; 114 videojs.hls.SegmentParser = oldSegmentParser;
91 videojs.SourceBuffer = oldSourceBuffer; 115 videojs.SourceBuffer = oldSourceBuffer;
116 window.setTimeout = oldSetTimeout;
92 window.XMLHttpRequest = oldXhr; 117 window.XMLHttpRequest = oldXhr;
93 } 118 }
94 }); 119 });
...@@ -665,53 +690,88 @@ test('flushes the parser after each segment', function() { ...@@ -665,53 +690,88 @@ test('flushes the parser after each segment', function() {
665 test('drops tags before the target timestamp when seeking', function() { 690 test('drops tags before the target timestamp when seeking', function() {
666 var 691 var
667 i = 10, 692 i = 10,
668 segment = [], 693 callbacks = [],
669 tags = [], 694 tags = [],
670 bytes = []; 695 bytes = [];
671 696
672 // mock out the parser and source buffer 697 // mock out the parser and source buffer
673 videojs.hls.SegmentParser = function() { 698 videojs.hls.SegmentParser = mockSegmentParser(tags);
674 this.getFlvHeader = function() {
675 return [];
676 };
677 this.parseSegmentBinaryData = function() {};
678 this.flushTags = function() {};
679 this.tagsAvailable = function() {
680 return tags.length;
681 };
682 this.getTags = function() {
683 return tags;
684 };
685 this.getNextTag = function() {
686 return tags.shift();
687 };
688 };
689 window.videojs.SourceBuffer = function() { 699 window.videojs.SourceBuffer = function() {
690 this.appendBuffer = function(chunk) { 700 this.appendBuffer = function(chunk) {
691 bytes.push(chunk); 701 bytes.push(chunk);
692 }; 702 };
693 }; 703 };
704 // capture timeouts
705 window.setTimeout = function(callback) {
706 callbacks.push(callback);
707 };
708
709 // push a tag into the buffer
710 tags.push({ pts: 0, bytes: 0 });
694 711
695 player.hls('manifest/media.m3u8'); 712 player.hls('manifest/media.m3u8');
696 videojs.mediaSources[player.currentSrc()].trigger({ 713 videojs.mediaSources[player.currentSrc()].trigger({
697 type: 'sourceopen' 714 type: 'sourceopen'
698 }); 715 });
716 while (callbacks.length) {
717 callbacks.shift()();
718 }
699 719
700 // build up some mock FLV tags 720 // mock out a new segment of FLV tags
721 bytes = [];
701 while (i--) { 722 while (i--) {
702 segment.unshift({ 723 tags.unshift({
703 pts: i * 1000, 724 pts: i * 1000,
704 bytes: i 725 bytes: i
705 }); 726 });
706 } 727 }
707 tags = segment.slice();
708 bytes = [];
709 player.currentTime = function() { 728 player.currentTime = function() {
710 return 7; 729 return 7;
711 }; 730 };
712 player.trigger('seeking'); 731 player.trigger('seeking');
713 732
714 deepEqual([7,8,9], bytes, 'three tags are appended'); 733 while (callbacks.length) {
734 callbacks.shift()();
735 }
736
737 deepEqual(bytes, [7,8,9], 'three tags are appended');
738 });
739
740 test('clears pending buffer updates when seeking', function() {
741 var
742 bytes = [],
743 callbacks = [],
744 tags = [{ pts: 0, bytes: 0 }];
745 // mock out the parser and source buffer
746 videojs.hls.SegmentParser = mockSegmentParser(tags);
747 window.videojs.SourceBuffer = function() {
748 this.appendBuffer = function(chunk) {
749 bytes.push(chunk);
750 };
751 };
752 // capture timeouts
753 window.setTimeout = function(callback) {
754 callbacks.push(callback);
755 };
756
757 // queue up a tag to be pushed into the buffer (but don't push it yet!)
758 player.hls('manifest/media.m3u8');
759 videojs.mediaSources[player.currentSrc()].trigger({
760 type: 'sourceopen'
761 });
762
763 // seek to 7s
764 tags.push({ pts: 7000, bytes: 7 });
765 player.currentTime = function() {
766 return 7;
767 };
768 player.trigger('seeking');
769
770 while (callbacks.length) {
771 callbacks.shift()();
772 }
773
774 deepEqual(bytes, ['flv', 7], 'tags queued to be appended should be cancelled');
715 }); 775 });
716 776
717 test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() { 777 test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() {
......