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.
Showing
7 changed files
with
217 additions
and
32 deletions
... | @@ -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> | ... | ... |
src/async-queue.js
0 → 100644
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(); | ... | ... |
test/async-queue_test.js
0 → 100644
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() { | ... | ... |
-
Please register or sign in to post a comment