00b5acf1 by David LaPalomento

Calculate preciseDuration correctly around discontinuities

Don't use previous segment timestamps to calculate the precise duration of a segment so that we don't have to worry about timestamp discontinuities. Update to contrib-media-sources 1.0.
1 parent b8baf3c8
...@@ -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,26 @@ hls.FlvTag.frameTime = function(tag) { ...@@ -358,4 +363,26 @@ 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 first = tags[0], last = tags[tags.length - 1], frameDuration;
381
382 // use the interval between the last two tags or assume 24 fps
383 frameDuration = last.pts - tags[tags.length - 2].pts || (1/24);
384
385 return (last.pts - first.pts) + frameDuration;
386 };
387
361 })(this); 388 })(this);
......
...@@ -714,7 +714,6 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -714,7 +714,6 @@ 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,
...@@ -788,23 +787,11 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -788,23 +787,11 @@ videojs.Hls.prototype.drainBuffer = function(event) {
788 tags.push(this.segmentParser_.getNextTag()); 787 tags.push(this.segmentParser_.getNextTag());
789 } 788 }
790 789
791 // 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
792 // 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
793 // for smoother seeking and calculation of the total playlist duration, which previously (especially in short videos) 792 // from what is reported in the playlist
794 // was reported erroneously and made the play head overrun the end of the progress bar.
795 if (tags.length > 0) { 793 if (tags.length > 0) {
796 segment.preciseTimestamp = tags[tags.length - 1].pts; 794 segment.preciseDuration = videojs.Hls.FlvTag.durationFromTags(tags) * 0.001;
797
798 if (playlist.segments[mediaIndex - 1]) {
799 if (playlist.segments[mediaIndex - 1].preciseTimestamp) {
800 durationOffset = playlist.segments[mediaIndex - 1].preciseTimestamp;
801 } else {
802 durationOffset = (playlist.targetDuration * (mediaIndex - 1) + playlist.segments[mediaIndex - 1].duration) * 1000;
803 }
804 segment.preciseDuration = (segment.preciseTimestamp - durationOffset) / 1000;
805 } else if (mediaIndex === 0) {
806 segment.preciseDuration = segment.preciseTimestamp / 1000;
807 }
808 } 795 }
809 796
810 this.updateDuration(this.playlists.media()); 797 this.updateDuration(this.playlists.media());
......
...@@ -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);
......
...@@ -1086,8 +1086,11 @@ test('flushes the parser after each segment', function() { ...@@ -1086,8 +1086,11 @@ test('flushes the parser after each segment', function() {
1086 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');
1087 }); 1087 });
1088 1088
1089 test('calculates preciseTimestamp and preciseDuration for a new segment', function() { 1089 test('calculates preciseDuration for a new segment', function() {
1090 var tags = [{ pts : 200000, bytes: new Uint8Array(1) }]; 1090 var tags = [
1091 { pts : 200 * 1000, bytes: new Uint8Array(1) },
1092 { pts : 300 * 1000, bytes: new Uint8Array(1) }
1093 ];
1091 videojs.Hls.SegmentParser = mockSegmentParser(tags); 1094 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1092 1095
1093 player.src({ 1096 player.src({
...@@ -1099,11 +1102,40 @@ test('calculates preciseTimestamp and preciseDuration for a new segment', functi ...@@ -1099,11 +1102,40 @@ test('calculates preciseTimestamp and preciseDuration for a new segment', functi
1099 standardXHRResponse(requests[0]); 1102 standardXHRResponse(requests[0]);
1100 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');
1101 standardXHRResponse(requests[1]); 1104 standardXHRResponse(requests[1]);
1102 strictEqual(player.hls.playlists.media().segments[0].preciseTimestamp, 200000, 'preciseTimestamp is calculated and stored');
1103 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');
1104 strictEqual(player.duration(), 230, 'player duration is calculated using preciseDuration'); 1106 strictEqual(player.duration(), 230, 'player duration is calculated using preciseDuration');
1105 }); 1107 });
1106 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
1107 test('exposes in-band metadata events as cues', function() { 1139 test('exposes in-band metadata events as cues', function() {
1108 var track; 1140 var track;
1109 player.src({ 1141 player.src({
......