56520081 by David LaPalomento

Merge pull request #267 from videojs/disc-action-insertion

Discontinuity insertion race
2 parents 61b0afe6 83c43ec9
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
44 }, 44 },
45 "dependencies": { 45 "dependencies": {
46 "pkcs7": "^0.2.2", 46 "pkcs7": "^0.2.2",
47 "videojs-contrib-media-sources": "^0.3.0", 47 "videojs-contrib-media-sources": "^1.0.0",
48 "videojs-swf": "^4.6.0" 48 "videojs-swf": "^4.6.0"
49 } 49 }
50 } 50 }
......
1 /**
2 * An object that stores the bytes of an FLV tag and methods for
3 * querying and manipulating that data.
4 * @see http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf
5 */
1 (function(window) { 6 (function(window) {
2 7
3 window.videojs = window.videojs || {}; 8 window.videojs = window.videojs || {};
...@@ -358,4 +363,29 @@ hls.FlvTag.frameTime = function(tag) { ...@@ -358,4 +363,29 @@ hls.FlvTag.frameTime = function(tag) {
358 return pts; 363 return pts;
359 }; 364 };
360 365
366 /**
367 * Calculate the media timeline duration represented by an array of
368 * tags. This function assumes the tags are already pre-sorted by
369 * presentation timestamp (PTS), in ascending order. Returns zero if
370 * there are less than two FLV tags to inspect.
371 * @param tags {array} the FlvTag objects to query
372 * @return the number of milliseconds between the display time of the
373 * first tag and the last tag.
374 */
375 hls.FlvTag.durationFromTags = function(tags) {
376 if (tags.length < 2) {
377 return 0;
378 }
379
380 var
381 first = tags[0],
382 last = tags[tags.length - 1],
383 frameDuration;
384
385 // use the interval between the last two tags or assume 24 fps
386 frameDuration = last.pts - tags[tags.length - 2].pts || (1/24);
387
388 return (last.pts - first.pts) + frameDuration;
389 };
390
361 })(this); 391 })(this);
......
...@@ -714,17 +714,24 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -714,17 +714,24 @@ videojs.Hls.prototype.drainBuffer = function(event) {
714 tags, 714 tags,
715 bytes, 715 bytes,
716 segment, 716 segment,
717 durationOffset,
718 decrypter, 717 decrypter,
719 segIv, 718 segIv,
720 ptsTime, 719 ptsTime,
721 segmentOffset = 0, 720 segmentOffset = 0,
722 segmentBuffer = this.segmentBuffer_; 721 segmentBuffer = this.segmentBuffer_;
723 722
723 // if the buffer is empty or the source buffer hasn't been created
724 // yet, do nothing
724 if (!segmentBuffer.length || !this.sourceBuffer) { 725 if (!segmentBuffer.length || !this.sourceBuffer) {
725 return; 726 return;
726 } 727 }
727 728
729 // we can't append more data if the source buffer is busy processing
730 // what we've already sent
731 if (this.sourceBuffer.updating) {
732 return;
733 }
734
728 segmentInfo = segmentBuffer[0]; 735 segmentInfo = segmentBuffer[0];
729 736
730 mediaIndex = segmentInfo.mediaIndex; 737 mediaIndex = segmentInfo.mediaIndex;
...@@ -780,23 +787,11 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -780,23 +787,11 @@ videojs.Hls.prototype.drainBuffer = function(event) {
780 tags.push(this.segmentParser_.getNextTag()); 787 tags.push(this.segmentParser_.getNextTag());
781 } 788 }
782 789
783 // This block of code uses the presentation timestamp of the ts segment to calculate its exact duration, since this 790 // Use the presentation timestamp of the ts segment to calculate its
784 // may differ by fractions of a second from what is reported. Using the exact, calculated 'preciseDuration' allows 791 // exact duration, since this may differ by fractions of a second
785 // for smoother seeking and calculation of the total playlist duration, which previously (especially in short videos) 792 // from what is reported in the playlist
786 // was reported erroneously and made the play head overrun the end of the progress bar.
787 if (tags.length > 0) { 793 if (tags.length > 0) {
788 segment.preciseTimestamp = tags[tags.length - 1].pts; 794 segment.preciseDuration = videojs.Hls.FlvTag.durationFromTags(tags) * 0.001;
789
790 if (playlist.segments[mediaIndex - 1]) {
791 if (playlist.segments[mediaIndex - 1].preciseTimestamp) {
792 durationOffset = playlist.segments[mediaIndex - 1].preciseTimestamp;
793 } else {
794 durationOffset = (playlist.targetDuration * (mediaIndex - 1) + playlist.segments[mediaIndex - 1].duration) * 1000;
795 }
796 segment.preciseDuration = (segment.preciseTimestamp - durationOffset) / 1000;
797 } else if (mediaIndex === 0) {
798 segment.preciseDuration = segment.preciseTimestamp / 1000;
799 }
800 } 795 }
801 796
802 this.updateDuration(this.playlists.media()); 797 this.updateDuration(this.playlists.media());
...@@ -825,13 +820,18 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -825,13 +820,18 @@ videojs.Hls.prototype.drainBuffer = function(event) {
825 this.el().vjs_discontinuity(); 820 this.el().vjs_discontinuity();
826 } 821 }
827 822
828 for (i = 0; i < tags.length; i++) { 823 (function() {
829 // queue up the bytes to be appended to the SourceBuffer 824 var segmentByteLength = 0, j, segment;
830 // the queue gives control back to the browser between tags 825 for (i = 0; i < tags.length; i++) {
831 // so that large segments don't cause a "hiccup" in playback 826 segmentByteLength += tags[i].bytes.byteLength;
832 827 }
833 this.sourceBuffer.appendBuffer(tags[i].bytes, this.player()); 828 segment = new Uint8Array(segmentByteLength);
834 } 829 for (i = 0, j = 0; i < tags.length; i++) {
830 segment.set(tags[i].bytes, j);
831 j += tags[i].bytes.byteLength;
832 }
833 this.sourceBuffer.appendBuffer(segment);
834 }).call(this);
835 835
836 // we're done processing this segment 836 // we're done processing this segment
837 segmentBuffer.shift(); 837 segmentBuffer.shift();
......
...@@ -57,4 +57,32 @@ test('writeBytes grows the internal byte array dynamically', function() { ...@@ -57,4 +57,32 @@ test('writeBytes grows the internal byte array dynamically', function() {
57 } 57 }
58 }); 58 });
59 59
60 })(this); 60 test('calculates the duration of a tag array from PTS values', function() {
61 var tags = [], count = 20, i;
62
63 for (i = 0; i < count; i++) {
64 tags[i] = new FlvTag(FlvTag.VIDEO_TAG);
65 tags[i].pts = i * 1000;
66 }
67
68 equal(FlvTag.durationFromTags(tags), count * 1000, 'calculated duration from PTS values');
69 });
70
71 test('durationFromTags() assumes 24fps if the last frame duration cannot be calculated', function() {
72 var tags = [
73 new FlvTag(FlvTag.VIDEO_TAG),
74 new FlvTag(FlvTag.VIDEO_TAG),
75 new FlvTag(FlvTag.VIDEO_TAG)
76 ];
77 tags[0].pts = 0;
78 tags[1].pts = tags[2].pts = 1000;
79
80 equal(FlvTag.durationFromTags(tags), 1000 + (1/24) , 'assumes 24fps video');
81 });
82
83 test('durationFromTags() returns zero if there are less than two frames', function() {
84 equal(FlvTag.durationFromTags([]), 0, 'returns zero for empty input');
85 equal(FlvTag.durationFromTags([new FlvTag(FlvTag.VIDEO_TAG)]), 0, 'returns zero for a singleton input');
86 });
87
88 })(this);
......
...@@ -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',
...@@ -1063,22 +1086,12 @@ test('flushes the parser after each segment', function() { ...@@ -1063,22 +1086,12 @@ test('flushes the parser after each segment', function() {
1063 strictEqual(flushes, 1, 'tags are flushed at the end of a segment'); 1086 strictEqual(flushes, 1, 'tags are flushed at the end of a segment');
1064 }); 1087 });
1065 1088
1066 test('calculates preciseTimestamp and preciseDuration for a new segment', function() { 1089 test('calculates preciseDuration for a new segment', function() {
1067 // mock out the segment parser 1090 var tags = [
1068 videojs.Hls.SegmentParser = function() { 1091 { pts : 200 * 1000, bytes: new Uint8Array(1) },
1069 var tagsAvailable = true, 1092 { pts : 300 * 1000, bytes: new Uint8Array(1) }
1070 tag = { pts : 200000 }; 1093 ];
1071 this.getFlvHeader = function() { 1094 videojs.Hls.SegmentParser = mockSegmentParser(tags);
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 1095
1083 player.src({ 1096 player.src({
1084 src: 'manifest/media.m3u8', 1097 src: 'manifest/media.m3u8',
...@@ -1089,11 +1102,40 @@ test('calculates preciseTimestamp and preciseDuration for a new segment', functi ...@@ -1089,11 +1102,40 @@ test('calculates preciseTimestamp and preciseDuration for a new segment', functi
1089 standardXHRResponse(requests[0]); 1102 standardXHRResponse(requests[0]);
1090 strictEqual(player.duration(), 40, 'player duration is read from playlist on load'); 1103 strictEqual(player.duration(), 40, 'player duration is read from playlist on load');
1091 standardXHRResponse(requests[1]); 1104 standardXHRResponse(requests[1]);
1092 strictEqual(player.hls.playlists.media().segments[0].preciseTimestamp, 200000, 'preciseTimestamp is calculated and stored');
1093 strictEqual(player.hls.playlists.media().segments[0].preciseDuration, 200, 'preciseDuration is calculated and stored'); 1105 strictEqual(player.hls.playlists.media().segments[0].preciseDuration, 200, 'preciseDuration is calculated and stored');
1094 strictEqual(player.duration(), 230, 'player duration is calculated using preciseDuration'); 1106 strictEqual(player.duration(), 230, 'player duration is calculated using preciseDuration');
1095 }); 1107 });
1096 1108
1109 test('calculates preciseDuration correctly around discontinuities', function() {
1110 var tags = [];
1111 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1112 player.src({
1113 src: 'manifest/media.m3u8',
1114 type: 'application/vnd.apple.mpegurl'
1115 });
1116 openMediaSource(player);
1117 requests.shift().respond(200, null,
1118 '#EXTM3U\n' +
1119 '#EXTINF:10,\n' +
1120 '0.ts\n' +
1121 '#EXT-X-DISCONTINUITY\n' +
1122 '#EXTINF:10,\n' +
1123 '1.ts\n' +
1124 '#EXT-X-ENDLIST\n');
1125 tags.push({ pts: 10 * 1000, bytes: new Uint8Array(1) });
1126 standardXHRResponse(requests.shift()); // segment 0
1127 player.hls.checkBuffer_();
1128
1129 // the PTS value of the second segment is *earlier* than the first
1130 tags.push({ pts: 0 * 1000, bytes: new Uint8Array(1) });
1131 tags.push({ pts: 5 * 1000, bytes: new Uint8Array(1) });
1132 standardXHRResponse(requests.shift()); // segment 1
1133
1134 equal(player.hls.playlists.media().segments[1].preciseDuration,
1135 5 + 5, // duration includes the time to display the second tag
1136 'duration is independent of previous segments');
1137 });
1138
1097 test('exposes in-band metadata events as cues', function() { 1139 test('exposes in-band metadata events as cues', function() {
1098 var track; 1140 var track;
1099 player.src({ 1141 player.src({
...@@ -1168,7 +1210,7 @@ test('drops tags before the target timestamp when seeking', function() { ...@@ -1168,7 +1210,7 @@ test('drops tags before the target timestamp when seeking', function() {
1168 }; 1210 };
1169 1211
1170 // push a tag into the buffer 1212 // push a tag into the buffer
1171 tags.push({ pts: 0, bytes: 0 }); 1213 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1172 1214
1173 player.src({ 1215 player.src({
1174 src: 'manifest/media.m3u8', 1216 src: 'manifest/media.m3u8',
...@@ -1183,20 +1225,20 @@ test('drops tags before the target timestamp when seeking', function() { ...@@ -1183,20 +1225,20 @@ test('drops tags before the target timestamp when seeking', function() {
1183 while (i--) { 1225 while (i--) {
1184 tags.unshift({ 1226 tags.unshift({
1185 pts: i * 1000, 1227 pts: i * 1000,
1186 bytes: i 1228 bytes: new Uint8Array([i])
1187 }); 1229 });
1188 } 1230 }
1189 player.currentTime(7); 1231 player.currentTime(7);
1190 standardXHRResponse(requests[2]); 1232 standardXHRResponse(requests[2]);
1191 1233
1192 deepEqual(bytes, [7,8,9], 'three tags are appended'); 1234 deepEqual(bytes, [new Uint8Array([7,8,9])], 'three tags are appended');
1193 }); 1235 });
1194 1236
1195 test('calls abort() on the SourceBuffer before seeking', function() { 1237 test('calls abort() on the SourceBuffer before seeking', function() {
1196 var 1238 var
1197 aborts = 0, 1239 aborts = 0,
1198 bytes = [], 1240 bytes = [],
1199 tags = [{ pts: 0, bytes: 0 }]; 1241 tags = [{ pts: 0, bytes: new Uint8Array(1) }];
1200 1242
1201 1243
1202 // track calls to abort() 1244 // track calls to abort()
...@@ -1221,8 +1263,8 @@ test('calls abort() on the SourceBuffer before seeking', function() { ...@@ -1221,8 +1263,8 @@ test('calls abort() on the SourceBuffer before seeking', function() {
1221 1263
1222 // drainBuffer() uses the first PTS value to account for any timestamp discontinuities in the stream 1264 // 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 1265 // adding a tag with a PTS of zero looks like a stream with no discontinuities
1224 tags.push({ pts: 0, bytes: 0 }); 1266 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1225 tags.push({ pts: 7000, bytes: 7 }); 1267 tags.push({ pts: 7000, bytes: new Uint8Array([7]) });
1226 // seek to 7s 1268 // seek to 7s
1227 player.currentTime(7); 1269 player.currentTime(7);
1228 standardXHRResponse(requests[2]); 1270 standardXHRResponse(requests[2]);
...@@ -1474,7 +1516,7 @@ test('calls vjs_discontinuity() before appending bytes at a discontinuity', func ...@@ -1474,7 +1516,7 @@ test('calls vjs_discontinuity() before appending bytes at a discontinuity', func
1474 player.hls.checkBuffer_(); 1516 player.hls.checkBuffer_();
1475 strictEqual(discontinuities, 0, 'no discontinuities before the segment is received'); 1517 strictEqual(discontinuities, 0, 'no discontinuities before the segment is received');
1476 1518
1477 tags.push({}); 1519 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1478 standardXHRResponse(requests.pop()); 1520 standardXHRResponse(requests.pop());
1479 strictEqual(discontinuities, 1, 'signals a discontinuity'); 1521 strictEqual(discontinuities, 1, 'signals a discontinuity');
1480 }); 1522 });
...@@ -1521,7 +1563,7 @@ test('clears the segment buffer on seek', function() { ...@@ -1521,7 +1563,7 @@ test('clears the segment buffer on seek', function() {
1521 1563
1522 // seek back to the beginning 1564 // seek back to the beginning
1523 player.currentTime(0); 1565 player.currentTime(0);
1524 tags.push({ pts: 0, bytes: 0 }); 1566 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1525 standardXHRResponse(requests.pop()); 1567 standardXHRResponse(requests.pop());
1526 strictEqual(aborts, 1, 'aborted once for the seek'); 1568 strictEqual(aborts, 1, 'aborted once for the seek');
1527 1569
...@@ -1571,7 +1613,7 @@ test('continues playing after seek to discontinuity', function() { ...@@ -1571,7 +1613,7 @@ test('continues playing after seek to discontinuity', function() {
1571 1613
1572 // seek to the discontinuity 1614 // seek to the discontinuity
1573 player.currentTime(10); 1615 player.currentTime(10);
1574 tags.push({ pts: 0, bytes: 0 }); 1616 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1575 standardXHRResponse(requests.pop()); 1617 standardXHRResponse(requests.pop());
1576 strictEqual(aborts, 1, 'aborted once for the seek'); 1618 strictEqual(aborts, 1, 'aborted once for the seek');
1577 1619
...@@ -1853,8 +1895,8 @@ test('drainBuffer will not proceed with empty source buffer', function() { ...@@ -1853,8 +1895,8 @@ test('drainBuffer will not proceed with empty source buffer', function() {
1853 }; 1895 };
1854 1896
1855 player.hls.sourceBuffer = undefined; 1897 player.hls.sourceBuffer = undefined;
1856 compareBuffer = [{mediaIndex: 0, playlist: newMedia, offset: 0, bytes: [0,0,0]}]; 1898 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]}]; 1899 player.hls.segmentBuffer_ = [{mediaIndex: 0, playlist: newMedia, offset: 0, bytes: new Uint8Array(3)}];
1858 1900
1859 player.hls.drainBuffer(); 1901 player.hls.drainBuffer();
1860 1902
...@@ -2018,7 +2060,7 @@ test('retries key requests once upon failure', function() { ...@@ -2018,7 +2060,7 @@ test('retries key requests once upon failure', function() {
2018 2060
2019 test('skip segments if key requests fail more than once', function() { 2061 test('skip segments if key requests fail more than once', function() {
2020 var bytes = [], 2062 var bytes = [],
2021 tags = [{ pts: 0, bytes: 0 }]; 2063 tags = [{ pts: 0, bytes: new Uint8Array(1) }];
2022 2064
2023 videojs.Hls.SegmentParser = mockSegmentParser(tags); 2065 videojs.Hls.SegmentParser = mockSegmentParser(tags);
2024 window.videojs.SourceBuffer = function() { 2066 window.videojs.SourceBuffer = function() {
...@@ -2047,7 +2089,7 @@ test('skip segments if key requests fail more than once', function() { ...@@ -2047,7 +2089,7 @@ test('skip segments if key requests fail more than once', function() {
2047 requests.shift().respond(404); // fail key, again 2089 requests.shift().respond(404); // fail key, again
2048 2090
2049 tags.length = 0; 2091 tags.length = 0;
2050 tags.push({pts: 0, bytes: 1}); 2092 tags.push({pts: 0, bytes: new Uint8Array([1]) });
2051 player.hls.checkBuffer_(); 2093 player.hls.checkBuffer_();
2052 standardXHRResponse(requests.shift()); // segment 2 2094 standardXHRResponse(requests.shift()); // segment 2
2053 equal(bytes.length, 1, 'bytes from the ts segments should not be added'); 2095 equal(bytes.length, 1, 'bytes from the ts segments should not be added');
...@@ -2060,7 +2102,7 @@ test('skip segments if key requests fail more than once', function() { ...@@ -2060,7 +2102,7 @@ test('skip segments if key requests fail more than once', function() {
2060 player.hls.checkBuffer_(); 2102 player.hls.checkBuffer_();
2061 2103
2062 equal(bytes.length, 2, 'bytes from the second ts segment should be added'); 2104 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'); 2105 deepEqual(bytes[1], new Uint8Array([1]), 'the bytes from the second segment are added and not the first');
2064 }); 2106 });
2065 2107
2066 test('the key is supplied to the decrypter in the correct format', function() { 2108 test('the key is supplied to the decrypter in the correct format', function() {
...@@ -2191,7 +2233,7 @@ test('resolves relative key URLs against the playlist', function() { ...@@ -2191,7 +2233,7 @@ test('resolves relative key URLs against the playlist', function() {
2191 }); 2233 });
2192 2234
2193 test('treats invalid keys as a key request failure', function() { 2235 test('treats invalid keys as a key request failure', function() {
2194 var tags = [{ pts: 0, bytes: 0 }], bytes = []; 2236 var tags = [{ pts: 0, bytes: new Uint8Array(1) }], bytes = [];
2195 videojs.Hls.SegmentParser = mockSegmentParser(tags); 2237 videojs.Hls.SegmentParser = mockSegmentParser(tags);
2196 window.videojs.SourceBuffer = function() { 2238 window.videojs.SourceBuffer = function() {
2197 this.appendBuffer = function(chunk) { 2239 this.appendBuffer = function(chunk) {
...@@ -2231,12 +2273,12 @@ test('treats invalid keys as a key request failure', function() { ...@@ -2231,12 +2273,12 @@ test('treats invalid keys as a key request failure', function() {
2231 equal(bytes[0], 'flv', 'appended the flv header'); 2273 equal(bytes[0], 'flv', 'appended the flv header');
2232 2274
2233 tags.length = 0; 2275 tags.length = 0;
2234 tags.push({ pts: 1, bytes: 1 }); 2276 tags.push({ pts: 1, bytes: new Uint8Array([1]) });
2235 // second segment request 2277 // second segment request
2236 standardXHRResponse(requests.shift()); 2278 standardXHRResponse(requests.shift());
2237 2279
2238 equal(bytes.length, 2, 'appended bytes'); 2280 equal(bytes.length, 2, 'appended bytes');
2239 equal(1, bytes[1], 'skipped to the second segment'); 2281 deepEqual(new Uint8Array([1]), bytes[1], 'skipped to the second segment');
2240 }); 2282 });
2241 2283
2242 test('live stream should not call endOfStream', function(){ 2284 test('live stream should not call endOfStream', function(){
......