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.
Showing
2 changed files
with
62 additions
and
39 deletions
... | @@ -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(){ | ... | ... |
-
Please register or sign in to post a comment