Merge pull request #251 from videojs/hlse-perf
Optimize decryption
Showing
4 changed files
with
158 additions
and
27 deletions
This diff is collapsed.
Click to expand it.
... | @@ -637,6 +637,8 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { | ... | @@ -637,6 +637,8 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { |
637 | responseType: 'arraybuffer', | 637 | responseType: 'arraybuffer', |
638 | withCredentials: settings.withCredentials | 638 | withCredentials: settings.withCredentials |
639 | }, function(error, url) { | 639 | }, function(error, url) { |
640 | var segmentInfo; | ||
641 | |||
640 | // the segment request is no longer outstanding | 642 | // the segment request is no longer outstanding |
641 | tech.segmentXhr_ = null; | 643 | tech.segmentXhr_ = null; |
642 | 644 | ||
... | @@ -669,12 +671,26 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { | ... | @@ -669,12 +671,26 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { |
669 | // if the segment is the start of a timestamp discontinuity, | 671 | // if the segment is the start of a timestamp discontinuity, |
670 | // we have to wait until the sourcebuffer is empty before | 672 | // we have to wait until the sourcebuffer is empty before |
671 | // aborting the source buffer processing | 673 | // aborting the source buffer processing |
672 | tech.segmentBuffer_.push({ | 674 | segmentInfo = { |
675 | // the segment's mediaIndex at the time it was received | ||
673 | mediaIndex: tech.mediaIndex, | 676 | mediaIndex: tech.mediaIndex, |
677 | // the segment's playlist | ||
674 | playlist: tech.playlists.media(), | 678 | playlist: tech.playlists.media(), |
679 | // optionally, a time offset to seek to within the segment | ||
675 | offset: offset, | 680 | offset: offset, |
676 | bytes: new Uint8Array(this.response) | 681 | // unencrypted bytes of the segment |
677 | }); | 682 | bytes: null, |
683 | // when a key is defined for this segment, the encrypted bytes | ||
684 | encryptedBytes: null, | ||
685 | // optionally, the decrypter that is unencrypting the segment | ||
686 | decrypter: null | ||
687 | }; | ||
688 | if (segmentInfo.playlist.segments[segmentInfo.mediaIndex].key) { | ||
689 | segmentInfo.encryptedBytes = new Uint8Array(this.response); | ||
690 | } else { | ||
691 | segmentInfo.bytes = new Uint8Array(this.response); | ||
692 | } | ||
693 | tech.segmentBuffer_.push(segmentInfo); | ||
678 | player.trigger('progress'); | 694 | player.trigger('progress'); |
679 | tech.drainBuffer(); | 695 | tech.drainBuffer(); |
680 | 696 | ||
... | @@ -689,12 +705,15 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { | ... | @@ -689,12 +705,15 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { |
689 | videojs.Hls.prototype.drainBuffer = function(event) { | 705 | videojs.Hls.prototype.drainBuffer = function(event) { |
690 | var | 706 | var |
691 | i = 0, | 707 | i = 0, |
708 | segmentInfo, | ||
692 | mediaIndex, | 709 | mediaIndex, |
693 | playlist, | 710 | playlist, |
694 | offset, | 711 | offset, |
695 | tags, | 712 | tags, |
696 | bytes, | 713 | bytes, |
697 | segment, | 714 | segment, |
715 | decrypter, | ||
716 | segIv, | ||
698 | 717 | ||
699 | ptsTime, | 718 | ptsTime, |
700 | segmentOffset, | 719 | segmentOffset, |
... | @@ -704,28 +723,45 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -704,28 +723,45 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
704 | return; | 723 | return; |
705 | } | 724 | } |
706 | 725 | ||
707 | mediaIndex = segmentBuffer[0].mediaIndex; | 726 | segmentInfo = segmentBuffer[0]; |
708 | playlist = segmentBuffer[0].playlist; | 727 | |
709 | offset = segmentBuffer[0].offset; | 728 | mediaIndex = segmentInfo.mediaIndex; |
710 | bytes = segmentBuffer[0].bytes; | 729 | playlist = segmentInfo.playlist; |
730 | offset = segmentInfo.offset; | ||
731 | bytes = segmentInfo.bytes; | ||
711 | segment = playlist.segments[mediaIndex]; | 732 | segment = playlist.segments[mediaIndex]; |
712 | 733 | ||
713 | if (segment.key) { | 734 | if (segment.key && !bytes) { |
735 | |||
714 | // this is an encrypted segment | 736 | // this is an encrypted segment |
715 | // if the key download failed, we want to skip this segment | 737 | // if the key download failed, we want to skip this segment |
716 | // but if the key hasn't downloaded yet, we want to try again later | 738 | // but if the key hasn't downloaded yet, we want to try again later |
717 | if (keyFailed(segment.key)) { | 739 | if (keyFailed(segment.key)) { |
718 | return segmentBuffer.shift(); | 740 | return segmentBuffer.shift(); |
719 | } else if (!segment.key.bytes) { | 741 | } else if (!segment.key.bytes) { |
742 | |||
720 | // trigger a key request if one is not already in-flight | 743 | // trigger a key request if one is not already in-flight |
721 | return this.fetchKeys_(); | 744 | return this.fetchKeys_(); |
745 | |||
746 | } else if (segmentInfo.decrypter) { | ||
747 | |||
748 | // decryption is in progress, try again later | ||
749 | return; | ||
750 | |||
722 | } else { | 751 | } else { |
723 | // if the media sequence is greater than 2^32, the IV will be incorrect | 752 | // if the media sequence is greater than 2^32, the IV will be incorrect |
724 | // assuming 10s segments, that would be about 1300 years | 753 | // assuming 10s segments, that would be about 1300 years |
725 | var segIv = segment.key.iv || new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]); | 754 | segIv = segment.key.iv || new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]); |
726 | bytes = videojs.Hls.decrypt(bytes, | 755 | |
727 | segment.key.bytes, | 756 | // create a decrypter to incrementally decrypt the segment |
728 | segIv); | 757 | decrypter = new videojs.Hls.Decrypter(segmentInfo.encryptedBytes, |
758 | segment.key.bytes, | ||
759 | segIv, | ||
760 | function(err, bytes) { | ||
761 | segmentInfo.bytes = bytes; | ||
762 | }); | ||
763 | segmentInfo.decrypter = decrypter; | ||
764 | return; | ||
729 | } | 765 | } |
730 | } | 766 | } |
731 | 767 | ... | ... |
1 | (function(window, videojs, undefined) { | 1 | (function(window, videojs, unpad, undefined) { |
2 | 'use strict'; | 2 | 'use strict'; |
3 | /* | 3 | /* |
4 | ======== A Handy Little QUnit Reference ======== | 4 | ======== A Handy Little QUnit Reference ======== |
... | @@ -46,7 +46,7 @@ test('decrypts a single AES-128 with PKCS7 block', function() { | ... | @@ -46,7 +46,7 @@ test('decrypts a single AES-128 with PKCS7 block', function() { |
46 | 0x82, 0xa8, 0xf0, 0x67]); | 46 | 0x82, 0xa8, 0xf0, 0x67]); |
47 | 47 | ||
48 | deepEqual('howdy folks', | 48 | deepEqual('howdy folks', |
49 | stringFromBytes(videojs.Hls.decrypt(encrypted, key, initVector)), | 49 | stringFromBytes(unpad(videojs.Hls.decrypt(encrypted, key, initVector))), |
50 | 'decrypted with a byte array key'); | 50 | 'decrypted with a byte array key'); |
51 | }); | 51 | }); |
52 | 52 | ||
... | @@ -68,8 +68,100 @@ test('decrypts multiple AES-128 blocks with CBC', function() { | ... | @@ -68,8 +68,100 @@ test('decrypts multiple AES-128 blocks with CBC', function() { |
68 | ]); | 68 | ]); |
69 | 69 | ||
70 | deepEqual('0123456789abcdef01234', | 70 | deepEqual('0123456789abcdef01234', |
71 | stringFromBytes(videojs.Hls.decrypt(encrypted, key, initVector)), | 71 | stringFromBytes(unpad(videojs.Hls.decrypt(encrypted, key, initVector))), |
72 | 'decrypted multiple blocks'); | 72 | 'decrypted multiple blocks'); |
73 | }); | 73 | }); |
74 | 74 | ||
75 | })(window, window.videojs); | 75 | var clock; |
76 | |||
77 | module('Incremental Processing', { | ||
78 | setup: function() { | ||
79 | clock = sinon.useFakeTimers(); | ||
80 | }, | ||
81 | teardown: function() { | ||
82 | clock.restore(); | ||
83 | } | ||
84 | }); | ||
85 | |||
86 | test('executes a callback after a timeout', function() { | ||
87 | var asyncStream = new videojs.Hls.AsyncStream(), | ||
88 | calls = ''; | ||
89 | asyncStream.push(function() { | ||
90 | calls += 'a'; | ||
91 | }); | ||
92 | |||
93 | clock.tick(asyncStream.delay); | ||
94 | equal(calls, 'a', 'invoked the callback once'); | ||
95 | clock.tick(asyncStream.delay); | ||
96 | equal(calls, 'a', 'only invoked the callback once'); | ||
97 | }); | ||
98 | |||
99 | test('executes callback in series', function() { | ||
100 | var asyncStream = new videojs.Hls.AsyncStream(), | ||
101 | calls = ''; | ||
102 | asyncStream.push(function() { | ||
103 | calls += 'a'; | ||
104 | }); | ||
105 | asyncStream.push(function() { | ||
106 | calls += 'b'; | ||
107 | }); | ||
108 | |||
109 | clock.tick(asyncStream.delay); | ||
110 | equal(calls, 'a', 'invoked the first callback'); | ||
111 | clock.tick(asyncStream.delay); | ||
112 | equal(calls, 'ab', 'invoked the second'); | ||
113 | }); | ||
114 | |||
115 | var decrypter; | ||
116 | |||
117 | module('Incremental Decryption', { | ||
118 | setup: function() { | ||
119 | clock = sinon.useFakeTimers(); | ||
120 | }, | ||
121 | teardown: function() { | ||
122 | clock.restore(); | ||
123 | } | ||
124 | }); | ||
125 | |||
126 | test('asynchronously decrypts a 4-word block', function() { | ||
127 | var | ||
128 | key = new Uint32Array([0, 0, 0, 0]), | ||
129 | initVector = key, | ||
130 | // the string "howdy folks" encrypted | ||
131 | encrypted = new Uint8Array([ | ||
132 | 0xce, 0x90, 0x97, 0xd0, | ||
133 | 0x08, 0x46, 0x4d, 0x18, | ||
134 | 0x4f, 0xae, 0x01, 0x1c, | ||
135 | 0x82, 0xa8, 0xf0, 0x67]), | ||
136 | decrypted; | ||
137 | |||
138 | decrypter = new videojs.Hls.Decrypter(encrypted, key, initVector, function(error, result) { | ||
139 | decrypted = result; | ||
140 | }); | ||
141 | ok(!decrypted, 'asynchronously decrypts'); | ||
142 | |||
143 | clock.tick(decrypter.asyncStream_.delay * 2); | ||
144 | |||
145 | ok(decrypted, 'completed decryption'); | ||
146 | deepEqual('howdy folks', | ||
147 | stringFromBytes(decrypted), | ||
148 | 'decrypts and unpads the result'); | ||
149 | }); | ||
150 | |||
151 | test('breaks up input greater than the step value', function() { | ||
152 | var encrypted = new Int32Array(videojs.Hls.Decrypter.STEP + 4), | ||
153 | done = false, | ||
154 | decrypter = new videojs.Hls.Decrypter(encrypted, | ||
155 | new Uint32Array(4), | ||
156 | new Uint32Array(4), | ||
157 | function() { | ||
158 | done = true; | ||
159 | }); | ||
160 | clock.tick(decrypter.asyncStream_.delay * 2); | ||
161 | ok(!done, 'not finished after two ticks'); | ||
162 | |||
163 | clock.tick(decrypter.asyncStream_.delay); | ||
164 | ok(done, 'finished after the last chunk is decrypted'); | ||
165 | }); | ||
166 | |||
167 | })(window, window.videojs, window.pkcs7.unpad); | ... | ... |
... | @@ -152,10 +152,8 @@ module('HLS', { | ... | @@ -152,10 +152,8 @@ module('HLS', { |
152 | 152 | ||
153 | oldNativeHlsSupport = videojs.Hls.supportsNativeHls; | 153 | oldNativeHlsSupport = videojs.Hls.supportsNativeHls; |
154 | 154 | ||
155 | oldDecrypt = videojs.Hls.decrypt; | 155 | oldDecrypt = videojs.Hls.Decrypter; |
156 | videojs.Hls.decrypt = function() { | 156 | videojs.Hls.Decrypter = function() {}; |
157 | return new Uint8Array([0]); | ||
158 | }; | ||
159 | 157 | ||
160 | // fake XHRs | 158 | // fake XHRs |
161 | xhr = sinon.useFakeXMLHttpRequest(); | 159 | xhr = sinon.useFakeXMLHttpRequest(); |
... | @@ -173,7 +171,7 @@ module('HLS', { | ... | @@ -173,7 +171,7 @@ module('HLS', { |
173 | videojs.MediaSource.open = oldMediaSourceOpen; | 171 | videojs.MediaSource.open = oldMediaSourceOpen; |
174 | videojs.Hls.SegmentParser = oldSegmentParser; | 172 | videojs.Hls.SegmentParser = oldSegmentParser; |
175 | videojs.Hls.supportsNativeHls = oldNativeHlsSupport; | 173 | videojs.Hls.supportsNativeHls = oldNativeHlsSupport; |
176 | videojs.Hls.decrypt = oldDecrypt; | 174 | videojs.Hls.Decrypter = oldDecrypt; |
177 | videojs.SourceBuffer = oldSourceBuffer; | 175 | videojs.SourceBuffer = oldSourceBuffer; |
178 | window.setTimeout = oldSetTimeout; | 176 | window.setTimeout = oldSetTimeout; |
179 | window.clearTimeout = oldClearTimeout; | 177 | window.clearTimeout = oldClearTimeout; |
... | @@ -1904,6 +1902,10 @@ test('a new key XHR is created when a the segment is received', function() { | ... | @@ -1904,6 +1902,10 @@ test('a new key XHR is created when a the segment is received', function() { |
1904 | '#EXT-X-ENDLIST\n'); | 1902 | '#EXT-X-ENDLIST\n'); |
1905 | standardXHRResponse(requests.shift()); // segment 1 | 1903 | standardXHRResponse(requests.shift()); // segment 1 |
1906 | standardXHRResponse(requests.shift()); // key 1 | 1904 | standardXHRResponse(requests.shift()); // key 1 |
1905 | // "finish" decrypting segment 1 | ||
1906 | player.hls.segmentBuffer_[0].bytes = new Uint8Array(16); | ||
1907 | player.hls.checkBuffer_(); | ||
1908 | |||
1907 | standardXHRResponse(requests.shift()); // segment 2 | 1909 | standardXHRResponse(requests.shift()); // segment 2 |
1908 | 1910 | ||
1909 | strictEqual(requests.length, 1, 'a key XHR is created'); | 1911 | strictEqual(requests.length, 1, 'a key XHR is created'); |
... | @@ -2009,6 +2011,9 @@ test('skip segments if key requests fail more than once', function() { | ... | @@ -2009,6 +2011,9 @@ test('skip segments if key requests fail more than once', function() { |
2009 | // key for second segment | 2011 | // key for second segment |
2010 | requests[0].response = new Uint32Array([0,0,0,0]).buffer; | 2012 | requests[0].response = new Uint32Array([0,0,0,0]).buffer; |
2011 | requests.shift().respond(200, null, ''); | 2013 | requests.shift().respond(200, null, ''); |
2014 | // "finish" decryption | ||
2015 | player.hls.segmentBuffer_[0].bytes = new Uint8Array(16); | ||
2016 | player.hls.checkBuffer_(); | ||
2012 | 2017 | ||
2013 | equal(bytes.length, 2, 'bytes from the second ts segment should be added'); | 2018 | equal(bytes.length, 2, 'bytes from the second ts segment should be added'); |
2014 | equal(bytes[1], 1, 'the bytes from the second segment are added and not the first'); | 2019 | equal(bytes[1], 1, 'the bytes from the second segment are added and not the first'); |
... | @@ -2033,9 +2038,8 @@ test('the key is supplied to the decrypter in the correct format', function() { | ... | @@ -2033,9 +2038,8 @@ test('the key is supplied to the decrypter in the correct format', function() { |
2033 | 'http://media.example.com/fileSequence52-B.ts\n'); | 2038 | 'http://media.example.com/fileSequence52-B.ts\n'); |
2034 | 2039 | ||
2035 | 2040 | ||
2036 | videojs.Hls.decrypt = function(bytes, key) { | 2041 | videojs.Hls.Decrypter = function(encrypted, key) { |
2037 | keys.push(key); | 2042 | keys.push(key); |
2038 | return new Uint8Array([0]); | ||
2039 | }; | 2043 | }; |
2040 | 2044 | ||
2041 | standardXHRResponse(requests.shift()); // segment | 2045 | standardXHRResponse(requests.shift()); // segment |
... | @@ -2043,7 +2047,7 @@ test('the key is supplied to the decrypter in the correct format', function() { | ... | @@ -2043,7 +2047,7 @@ test('the key is supplied to the decrypter in the correct format', function() { |
2043 | requests[0].respond(200, null, ''); | 2047 | requests[0].respond(200, null, ''); |
2044 | requests.shift(); // key | 2048 | requests.shift(); // key |
2045 | 2049 | ||
2046 | equal(keys.length, 1, 'only one call to decrypt was made'); | 2050 | equal(keys.length, 1, 'only one Decrypter was constructed'); |
2047 | deepEqual(keys[0], | 2051 | deepEqual(keys[0], |
2048 | new Uint32Array([0, 0x01000000, 0x02000000, 0x03000000]), | 2052 | new Uint32Array([0, 0x01000000, 0x02000000, 0x03000000]), |
2049 | 'passed the specified segment key'); | 2053 | 'passed the specified segment key'); |
... | @@ -2068,9 +2072,8 @@ test('supplies the media sequence of current segment as the IV by default, if no | ... | @@ -2068,9 +2072,8 @@ test('supplies the media sequence of current segment as the IV by default, if no |
2068 | 'http://media.example.com/fileSequence52-B.ts\n'); | 2072 | 'http://media.example.com/fileSequence52-B.ts\n'); |
2069 | 2073 | ||
2070 | 2074 | ||
2071 | videojs.Hls.decrypt = function(bytes, key, iv) { | 2075 | videojs.Hls.Decrypter = function(encrypted, key, iv) { |
2072 | ivs.push(iv); | 2076 | ivs.push(iv); |
2073 | return new Uint8Array([0]); | ||
2074 | }; | 2077 | }; |
2075 | 2078 | ||
2076 | requests[0].response = new Uint32Array([0,0,0,0]).buffer; | 2079 | requests[0].response = new Uint32Array([0,0,0,0]).buffer; |
... | @@ -2078,7 +2081,7 @@ test('supplies the media sequence of current segment as the IV by default, if no | ... | @@ -2078,7 +2081,7 @@ test('supplies the media sequence of current segment as the IV by default, if no |
2078 | requests.shift(); | 2081 | requests.shift(); |
2079 | standardXHRResponse(requests.pop()); | 2082 | standardXHRResponse(requests.pop()); |
2080 | 2083 | ||
2081 | equal(ivs.length, 1, 'only one call to decrypt was made'); | 2084 | equal(ivs.length, 1, 'only one Decrypter was constructed'); |
2082 | deepEqual(ivs[0], | 2085 | deepEqual(ivs[0], |
2083 | new Uint32Array([0, 0, 0, 5]), | 2086 | new Uint32Array([0, 0, 0, 5]), |
2084 | 'the IV for the segment is the media sequence'); | 2087 | 'the IV for the segment is the media sequence'); | ... | ... |
-
Please register or sign in to post a comment