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.
Showing
5 changed files
with
95 additions
and
21 deletions
... | @@ -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 | 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 | |||
60 | })(this); | 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({ | ... | ... |
-
Please register or sign in to post a comment