b8baf3c8 by David LaPalomento

Don't append data until the previous update finishes

If segments are delivered very quickly, it's possible that a segment after a discontinuity will be ready before the previous segment is finished getting processed by the media source. Our current mechanism for signalling discontinuities is synchronous so this could lead to us injecting a discontinuity into the middle of the first segment instead of at the end. Instead, adopt a workflow more closely aligned to how real SourceBuffers work and don't append additional bytes until the previous append has been fully processed.
1 parent 61b0afe6
...@@ -721,10 +721,18 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -721,10 +721,18 @@ videojs.Hls.prototype.drainBuffer = function(event) {
721 segmentOffset = 0, 721 segmentOffset = 0,
722 segmentBuffer = this.segmentBuffer_; 722 segmentBuffer = this.segmentBuffer_;
723 723
724 // if the buffer is empty or the source buffer hasn't been created
725 // yet, do nothing
724 if (!segmentBuffer.length || !this.sourceBuffer) { 726 if (!segmentBuffer.length || !this.sourceBuffer) {
725 return; 727 return;
726 } 728 }
727 729
730 // we can't append more data if the source buffer is busy processing
731 // what we've already sent
732 if (this.sourceBuffer.updating) {
733 return;
734 }
735
728 segmentInfo = segmentBuffer[0]; 736 segmentInfo = segmentBuffer[0];
729 737
730 mediaIndex = segmentInfo.mediaIndex; 738 mediaIndex = segmentInfo.mediaIndex;
...@@ -825,13 +833,18 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -825,13 +833,18 @@ videojs.Hls.prototype.drainBuffer = function(event) {
825 this.el().vjs_discontinuity(); 833 this.el().vjs_discontinuity();
826 } 834 }
827 835
828 for (i = 0; i < tags.length; i++) { 836 (function() {
829 // queue up the bytes to be appended to the SourceBuffer 837 var segmentByteLength = 0, j, segment;
830 // the queue gives control back to the browser between tags 838 for (i = 0; i < tags.length; i++) {
831 // so that large segments don't cause a "hiccup" in playback 839 segmentByteLength += tags[i].bytes.byteLength;
832 840 }
833 this.sourceBuffer.appendBuffer(tags[i].bytes, this.player()); 841 segment = new Uint8Array(segmentByteLength);
834 } 842 for (i = 0, j = 0; i < tags.length; i++) {
843 segment.set(tags[i].bytes, j);
844 j += tags[i].bytes.byteLength;
845 }
846 this.sourceBuffer.appendBuffer(segment);
847 }).call(this);
835 848
836 // we're done processing this segment 849 // we're done processing this segment
837 segmentBuffer.shift(); 850 segmentBuffer.shift();
......
...@@ -953,6 +953,29 @@ test('only makes one segment request at a time', function() { ...@@ -953,6 +953,29 @@ test('only makes one segment request at a time', function() {
953 strictEqual(1, requests.length, 'only one XHR is made'); 953 strictEqual(1, requests.length, 'only one XHR is made');
954 }); 954 });
955 955
956 test('only appends one segment at a time', function() {
957 var appends = 0, tags = [{ pts: 0, bytes: new Uint8Array(1) }];
958 videojs.Hls.SegmentParser = mockSegmentParser(tags);
959 player.src({
960 src: 'manifest/media.m3u8',
961 type: 'application/vnd.apple.mpegurl'
962 });
963 openMediaSource(player);
964 standardXHRResponse(requests.pop()); // media.m3u8
965 standardXHRResponse(requests.pop()); // segment 0
966
967 player.hls.sourceBuffer.updating = true;
968 player.hls.sourceBuffer.appendBuffer = function() {
969 appends++;
970 };
971 tags.push({ pts: 0, bytes: new Uint8Array(1) });
972
973 player.hls.checkBuffer_();
974 standardXHRResponse(requests.pop()); // segment 1
975 player.hls.checkBuffer_(); // should be a no-op
976 equal(appends, 0, 'did not append while updating');
977 });
978
956 test('cancels outstanding XHRs when seeking', function() { 979 test('cancels outstanding XHRs when seeking', function() {
957 player.src({ 980 player.src({
958 src: 'manifest/media.m3u8', 981 src: 'manifest/media.m3u8',
...@@ -1064,21 +1087,8 @@ test('flushes the parser after each segment', function() { ...@@ -1064,21 +1087,8 @@ test('flushes the parser after each segment', function() {
1064 }); 1087 });
1065 1088
1066 test('calculates preciseTimestamp and preciseDuration for a new segment', function() { 1089 test('calculates preciseTimestamp and preciseDuration for a new segment', function() {
1067 // mock out the segment parser 1090 var tags = [{ pts : 200000, bytes: new Uint8Array(1) }];
1068 videojs.Hls.SegmentParser = function() { 1091 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1069 var tagsAvailable = true,
1070 tag = { pts : 200000 };
1071 this.getFlvHeader = function() {
1072 return [];
1073 };
1074 this.parseSegmentBinaryData = function() {};
1075 this.flushTags = function() {};
1076 this.tagsAvailable = function() { return tagsAvailable; };
1077 this.getNextTag = function() { tagsAvailable = false; return tag; };
1078 this.metadataStream = {
1079 on: Function.prototype
1080 };
1081 };
1082 1092
1083 player.src({ 1093 player.src({
1084 src: 'manifest/media.m3u8', 1094 src: 'manifest/media.m3u8',
...@@ -1168,7 +1178,7 @@ test('drops tags before the target timestamp when seeking', function() { ...@@ -1168,7 +1178,7 @@ test('drops tags before the target timestamp when seeking', function() {
1168 }; 1178 };
1169 1179
1170 // push a tag into the buffer 1180 // push a tag into the buffer
1171 tags.push({ pts: 0, bytes: 0 }); 1181 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1172 1182
1173 player.src({ 1183 player.src({
1174 src: 'manifest/media.m3u8', 1184 src: 'manifest/media.m3u8',
...@@ -1183,20 +1193,20 @@ test('drops tags before the target timestamp when seeking', function() { ...@@ -1183,20 +1193,20 @@ test('drops tags before the target timestamp when seeking', function() {
1183 while (i--) { 1193 while (i--) {
1184 tags.unshift({ 1194 tags.unshift({
1185 pts: i * 1000, 1195 pts: i * 1000,
1186 bytes: i 1196 bytes: new Uint8Array([i])
1187 }); 1197 });
1188 } 1198 }
1189 player.currentTime(7); 1199 player.currentTime(7);
1190 standardXHRResponse(requests[2]); 1200 standardXHRResponse(requests[2]);
1191 1201
1192 deepEqual(bytes, [7,8,9], 'three tags are appended'); 1202 deepEqual(bytes, [new Uint8Array([7,8,9])], 'three tags are appended');
1193 }); 1203 });
1194 1204
1195 test('calls abort() on the SourceBuffer before seeking', function() { 1205 test('calls abort() on the SourceBuffer before seeking', function() {
1196 var 1206 var
1197 aborts = 0, 1207 aborts = 0,
1198 bytes = [], 1208 bytes = [],
1199 tags = [{ pts: 0, bytes: 0 }]; 1209 tags = [{ pts: 0, bytes: new Uint8Array(1) }];
1200 1210
1201 1211
1202 // track calls to abort() 1212 // track calls to abort()
...@@ -1221,8 +1231,8 @@ test('calls abort() on the SourceBuffer before seeking', function() { ...@@ -1221,8 +1231,8 @@ test('calls abort() on the SourceBuffer before seeking', function() {
1221 1231
1222 // drainBuffer() uses the first PTS value to account for any timestamp discontinuities in the stream 1232 // drainBuffer() uses the first PTS value to account for any timestamp discontinuities in the stream
1223 // adding a tag with a PTS of zero looks like a stream with no discontinuities 1233 // adding a tag with a PTS of zero looks like a stream with no discontinuities
1224 tags.push({ pts: 0, bytes: 0 }); 1234 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1225 tags.push({ pts: 7000, bytes: 7 }); 1235 tags.push({ pts: 7000, bytes: new Uint8Array([7]) });
1226 // seek to 7s 1236 // seek to 7s
1227 player.currentTime(7); 1237 player.currentTime(7);
1228 standardXHRResponse(requests[2]); 1238 standardXHRResponse(requests[2]);
...@@ -1474,7 +1484,7 @@ test('calls vjs_discontinuity() before appending bytes at a discontinuity', func ...@@ -1474,7 +1484,7 @@ test('calls vjs_discontinuity() before appending bytes at a discontinuity', func
1474 player.hls.checkBuffer_(); 1484 player.hls.checkBuffer_();
1475 strictEqual(discontinuities, 0, 'no discontinuities before the segment is received'); 1485 strictEqual(discontinuities, 0, 'no discontinuities before the segment is received');
1476 1486
1477 tags.push({}); 1487 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1478 standardXHRResponse(requests.pop()); 1488 standardXHRResponse(requests.pop());
1479 strictEqual(discontinuities, 1, 'signals a discontinuity'); 1489 strictEqual(discontinuities, 1, 'signals a discontinuity');
1480 }); 1490 });
...@@ -1521,7 +1531,7 @@ test('clears the segment buffer on seek', function() { ...@@ -1521,7 +1531,7 @@ test('clears the segment buffer on seek', function() {
1521 1531
1522 // seek back to the beginning 1532 // seek back to the beginning
1523 player.currentTime(0); 1533 player.currentTime(0);
1524 tags.push({ pts: 0, bytes: 0 }); 1534 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1525 standardXHRResponse(requests.pop()); 1535 standardXHRResponse(requests.pop());
1526 strictEqual(aborts, 1, 'aborted once for the seek'); 1536 strictEqual(aborts, 1, 'aborted once for the seek');
1527 1537
...@@ -1571,7 +1581,7 @@ test('continues playing after seek to discontinuity', function() { ...@@ -1571,7 +1581,7 @@ test('continues playing after seek to discontinuity', function() {
1571 1581
1572 // seek to the discontinuity 1582 // seek to the discontinuity
1573 player.currentTime(10); 1583 player.currentTime(10);
1574 tags.push({ pts: 0, bytes: 0 }); 1584 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1575 standardXHRResponse(requests.pop()); 1585 standardXHRResponse(requests.pop());
1576 strictEqual(aborts, 1, 'aborted once for the seek'); 1586 strictEqual(aborts, 1, 'aborted once for the seek');
1577 1587
...@@ -1853,8 +1863,8 @@ test('drainBuffer will not proceed with empty source buffer', function() { ...@@ -1853,8 +1863,8 @@ test('drainBuffer will not proceed with empty source buffer', function() {
1853 }; 1863 };
1854 1864
1855 player.hls.sourceBuffer = undefined; 1865 player.hls.sourceBuffer = undefined;
1856 compareBuffer = [{mediaIndex: 0, playlist: newMedia, offset: 0, bytes: [0,0,0]}]; 1866 compareBuffer = [{mediaIndex: 0, playlist: newMedia, offset: 0, bytes: new Uint8Array(3)}];
1857 player.hls.segmentBuffer_ = [{mediaIndex: 0, playlist: newMedia, offset: 0, bytes: [0,0,0]}]; 1867 player.hls.segmentBuffer_ = [{mediaIndex: 0, playlist: newMedia, offset: 0, bytes: new Uint8Array(3)}];
1858 1868
1859 player.hls.drainBuffer(); 1869 player.hls.drainBuffer();
1860 1870
...@@ -2018,7 +2028,7 @@ test('retries key requests once upon failure', function() { ...@@ -2018,7 +2028,7 @@ test('retries key requests once upon failure', function() {
2018 2028
2019 test('skip segments if key requests fail more than once', function() { 2029 test('skip segments if key requests fail more than once', function() {
2020 var bytes = [], 2030 var bytes = [],
2021 tags = [{ pts: 0, bytes: 0 }]; 2031 tags = [{ pts: 0, bytes: new Uint8Array(1) }];
2022 2032
2023 videojs.Hls.SegmentParser = mockSegmentParser(tags); 2033 videojs.Hls.SegmentParser = mockSegmentParser(tags);
2024 window.videojs.SourceBuffer = function() { 2034 window.videojs.SourceBuffer = function() {
...@@ -2047,7 +2057,7 @@ test('skip segments if key requests fail more than once', function() { ...@@ -2047,7 +2057,7 @@ test('skip segments if key requests fail more than once', function() {
2047 requests.shift().respond(404); // fail key, again 2057 requests.shift().respond(404); // fail key, again
2048 2058
2049 tags.length = 0; 2059 tags.length = 0;
2050 tags.push({pts: 0, bytes: 1}); 2060 tags.push({pts: 0, bytes: new Uint8Array([1]) });
2051 player.hls.checkBuffer_(); 2061 player.hls.checkBuffer_();
2052 standardXHRResponse(requests.shift()); // segment 2 2062 standardXHRResponse(requests.shift()); // segment 2
2053 equal(bytes.length, 1, 'bytes from the ts segments should not be added'); 2063 equal(bytes.length, 1, 'bytes from the ts segments should not be added');
...@@ -2060,7 +2070,7 @@ test('skip segments if key requests fail more than once', function() { ...@@ -2060,7 +2070,7 @@ test('skip segments if key requests fail more than once', function() {
2060 player.hls.checkBuffer_(); 2070 player.hls.checkBuffer_();
2061 2071
2062 equal(bytes.length, 2, 'bytes from the second ts segment should be added'); 2072 equal(bytes.length, 2, 'bytes from the second ts segment should be added');
2063 equal(bytes[1], 1, 'the bytes from the second segment are added and not the first'); 2073 deepEqual(bytes[1], new Uint8Array([1]), 'the bytes from the second segment are added and not the first');
2064 }); 2074 });
2065 2075
2066 test('the key is supplied to the decrypter in the correct format', function() { 2076 test('the key is supplied to the decrypter in the correct format', function() {
...@@ -2191,7 +2201,7 @@ test('resolves relative key URLs against the playlist', function() { ...@@ -2191,7 +2201,7 @@ test('resolves relative key URLs against the playlist', function() {
2191 }); 2201 });
2192 2202
2193 test('treats invalid keys as a key request failure', function() { 2203 test('treats invalid keys as a key request failure', function() {
2194 var tags = [{ pts: 0, bytes: 0 }], bytes = []; 2204 var tags = [{ pts: 0, bytes: new Uint8Array(1) }], bytes = [];
2195 videojs.Hls.SegmentParser = mockSegmentParser(tags); 2205 videojs.Hls.SegmentParser = mockSegmentParser(tags);
2196 window.videojs.SourceBuffer = function() { 2206 window.videojs.SourceBuffer = function() {
2197 this.appendBuffer = function(chunk) { 2207 this.appendBuffer = function(chunk) {
...@@ -2231,12 +2241,12 @@ test('treats invalid keys as a key request failure', function() { ...@@ -2231,12 +2241,12 @@ test('treats invalid keys as a key request failure', function() {
2231 equal(bytes[0], 'flv', 'appended the flv header'); 2241 equal(bytes[0], 'flv', 'appended the flv header');
2232 2242
2233 tags.length = 0; 2243 tags.length = 0;
2234 tags.push({ pts: 1, bytes: 1 }); 2244 tags.push({ pts: 1, bytes: new Uint8Array([1]) });
2235 // second segment request 2245 // second segment request
2236 standardXHRResponse(requests.shift()); 2246 standardXHRResponse(requests.shift());
2237 2247
2238 equal(bytes.length, 2, 'appended bytes'); 2248 equal(bytes.length, 2, 'appended bytes');
2239 equal(1, bytes[1], 'skipped to the second segment'); 2249 deepEqual(new Uint8Array([1]), bytes[1], 'skipped to the second segment');
2240 }); 2250 });
2241 2251
2242 test('live stream should not call endOfStream', function(){ 2252 test('live stream should not call endOfStream', function(){
......