bd468d14 by David LaPalomento

Merge pull request #11 from videojs/feature/async-appends

Yield control to the browser between appending FLV tags
2 parents 03abd4f0 ec27dbff
...@@ -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 callbacks.shift()();
53 strictEqual(1, callbacks.length, 'nothing is scheduled on an empty queue');
54 });
55
56 test('can be emptied at any time', function() {
57 var
58 runs = 0,
59 q = queue(function() {
60 runs++;
61 });
62 q.push(1);
63 q.push(2);
64
65 callbacks.shift()();
66 strictEqual(1, runs, 'task one is run');
67
68 q.tasks = [];
69 callbacks.shift()();
70 strictEqual(1, runs, 'the remaining tasks are cancelled');
71 });
72 })(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() {
......