duration should work when video or audio is missing
If audio or video data is missing for an HLS stream, duration calculations should just use the info that is available. Fixes #341.
Showing
5 changed files
with
133 additions
and
10 deletions
... | @@ -5,7 +5,23 @@ | ... | @@ -5,7 +5,23 @@ |
5 | 'use strict'; | 5 | 'use strict'; |
6 | 6 | ||
7 | var DEFAULT_TARGET_DURATION = 10; | 7 | var DEFAULT_TARGET_DURATION = 10; |
8 | var accumulateDuration, ascendingNumeric, duration, intervalDuration, rangeDuration, seekable; | 8 | var accumulateDuration, ascendingNumeric, duration, intervalDuration, optionalMin, optionalMax, rangeDuration, seekable; |
9 | |||
10 | // Math.min that will return the alternative input if one of its | ||
11 | // parameters in undefined | ||
12 | optionalMin = function(left, right) { | ||
13 | left = isFinite(left) ? left : Infinity; | ||
14 | right = isFinite(right) ? right : Infinity; | ||
15 | return Math.min(left, right); | ||
16 | }; | ||
17 | |||
18 | // Math.max that will return the alternative input if one of its | ||
19 | // parameters in undefined | ||
20 | optionalMax = function(left, right) { | ||
21 | left = isFinite(left) ? left: -Infinity; | ||
22 | right = isFinite(right) ? right: -Infinity; | ||
23 | return Math.max(left, right); | ||
24 | }; | ||
9 | 25 | ||
10 | // Array.sort comparator to sort numbers in ascending order | 26 | // Array.sort comparator to sort numbers in ascending order |
11 | ascendingNumeric = function(left, right) { | 27 | ascendingNumeric = function(left, right) { |
... | @@ -91,7 +107,8 @@ | ... | @@ -91,7 +107,8 @@ |
91 | // available PTS information | 107 | // available PTS information |
92 | for (left = range.start; left < range.end; left++) { | 108 | for (left = range.start; left < range.end; left++) { |
93 | segment = playlist.segments[left]; | 109 | segment = playlist.segments[left]; |
94 | if (segment.minVideoPts !== undefined) { | 110 | if (segment.minVideoPts !== undefined || |
111 | segment.minAudioPts !== undefined) { | ||
95 | break; | 112 | break; |
96 | } | 113 | } |
97 | result += segment.duration || targetDuration; | 114 | result += segment.duration || targetDuration; |
... | @@ -100,10 +117,12 @@ | ... | @@ -100,10 +117,12 @@ |
100 | // see if there's enough information to include the trailing time | 117 | // see if there's enough information to include the trailing time |
101 | if (includeTrailingTime) { | 118 | if (includeTrailingTime) { |
102 | segment = playlist.segments[range.end]; | 119 | segment = playlist.segments[range.end]; |
103 | if (segment && segment.minVideoPts !== undefined) { | 120 | if (segment && |
121 | (segment.minVideoPts !== undefined || | ||
122 | segment.minAudioPts !== undefined)) { | ||
104 | result += 0.001 * | 123 | result += 0.001 * |
105 | (Math.min(segment.minVideoPts, segment.minAudioPts) - | 124 | (optionalMin(segment.minVideoPts, segment.minAudioPts) - |
106 | Math.min(playlist.segments[left].minVideoPts, | 125 | optionalMin(playlist.segments[left].minVideoPts, |
107 | playlist.segments[left].minAudioPts)); | 126 | playlist.segments[left].minAudioPts)); |
108 | return result; | 127 | return result; |
109 | } | 128 | } |
... | @@ -112,7 +131,8 @@ | ... | @@ -112,7 +131,8 @@ |
112 | // do the same thing while finding the latest segment | 131 | // do the same thing while finding the latest segment |
113 | for (right = range.end - 1; right >= left; right--) { | 132 | for (right = range.end - 1; right >= left; right--) { |
114 | segment = playlist.segments[right]; | 133 | segment = playlist.segments[right]; |
115 | if (segment.maxVideoPts !== undefined) { | 134 | if (segment.maxVideoPts !== undefined || |
135 | segment.maxAudioPts !== undefined) { | ||
116 | break; | 136 | break; |
117 | } | 137 | } |
118 | result += segment.duration || targetDuration; | 138 | result += segment.duration || targetDuration; |
... | @@ -121,9 +141,9 @@ | ... | @@ -121,9 +141,9 @@ |
121 | // add in the PTS interval in seconds between them | 141 | // add in the PTS interval in seconds between them |
122 | if (right >= left) { | 142 | if (right >= left) { |
123 | result += 0.001 * | 143 | result += 0.001 * |
124 | (Math.max(playlist.segments[right].maxVideoPts, | 144 | (optionalMax(playlist.segments[right].maxVideoPts, |
125 | playlist.segments[right].maxAudioPts) - | 145 | playlist.segments[right].maxAudioPts) - |
126 | Math.min(playlist.segments[left].minVideoPts, | 146 | optionalMin(playlist.segments[left].minVideoPts, |
127 | playlist.segments[left].minAudioPts)); | 147 | playlist.segments[left].minAudioPts)); |
128 | } | 148 | } |
129 | 149 | ||
... | @@ -158,7 +178,7 @@ | ... | @@ -158,7 +178,7 @@ |
158 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; | 178 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; |
159 | 179 | ||
160 | // estimate expired segment duration using the target duration | 180 | // estimate expired segment duration using the target duration |
161 | expiredSegmentCount = Math.max(playlist.mediaSequence - startSequence, 0); | 181 | expiredSegmentCount = optionalMax(playlist.mediaSequence - startSequence, 0); |
162 | result += expiredSegmentCount * targetDuration; | 182 | result += expiredSegmentCount * targetDuration; |
163 | 183 | ||
164 | // accumulate the segment durations into the result | 184 | // accumulate the segment durations into the result |
... | @@ -257,7 +277,7 @@ | ... | @@ -257,7 +277,7 @@ |
257 | // from the result. | 277 | // from the result. |
258 | for (i = playlist.segments.length - 1; i >= 0 && liveBuffer > 0; i--) { | 278 | for (i = playlist.segments.length - 1; i >= 0 && liveBuffer > 0; i--) { |
259 | segment = playlist.segments[i]; | 279 | segment = playlist.segments[i]; |
260 | pending = Math.min(duration(playlist, | 280 | pending = optionalMin(duration(playlist, |
261 | playlist.mediaSequence + i, | 281 | playlist.mediaSequence + i, |
262 | playlist.mediaSequence + i + 1), | 282 | playlist.mediaSequence + i + 1), |
263 | liveBuffer); | 283 | liveBuffer); | ... | ... |
... | @@ -899,11 +899,15 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -899,11 +899,15 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
899 | if (this.segmentParser_.tagsAvailable()) { | 899 | if (this.segmentParser_.tagsAvailable()) { |
900 | // record PTS information for the segment so we can calculate | 900 | // record PTS information for the segment so we can calculate |
901 | // accurate durations and seek reliably | 901 | // accurate durations and seek reliably |
902 | if (this.segmentParser_.stats.h264Tags()) { | ||
902 | segment.minVideoPts = this.segmentParser_.stats.minVideoPts(); | 903 | segment.minVideoPts = this.segmentParser_.stats.minVideoPts(); |
903 | segment.maxVideoPts = this.segmentParser_.stats.maxVideoPts(); | 904 | segment.maxVideoPts = this.segmentParser_.stats.maxVideoPts(); |
905 | } | ||
906 | if (this.segmentParser_.stats.aacTags()) { | ||
904 | segment.minAudioPts = this.segmentParser_.stats.minAudioPts(); | 907 | segment.minAudioPts = this.segmentParser_.stats.minAudioPts(); |
905 | segment.maxAudioPts = this.segmentParser_.stats.maxAudioPts(); | 908 | segment.maxAudioPts = this.segmentParser_.stats.maxAudioPts(); |
906 | } | 909 | } |
910 | } | ||
907 | 911 | ||
908 | while (this.segmentParser_.tagsAvailable()) { | 912 | while (this.segmentParser_.tagsAvailable()) { |
909 | tags.push(this.segmentParser_.getNextTag()); | 913 | tags.push(this.segmentParser_.getNextTag()); | ... | ... |
... | @@ -259,6 +259,30 @@ | ... | @@ -259,6 +259,30 @@ |
259 | equal(duration, 30.1, 'used the PTS-based interval'); | 259 | equal(duration, 30.1, 'used the PTS-based interval'); |
260 | }); | 260 | }); |
261 | 261 | ||
262 | test('works for media without audio', function() { | ||
263 | equal(Playlist.duration({ | ||
264 | mediaSequence: 0, | ||
265 | endList: true, | ||
266 | segments: [{ | ||
267 | minVideoPts: 0, | ||
268 | maxVideoPts: 9 * 1000, | ||
269 | uri: 'no-audio.ts' | ||
270 | }] | ||
271 | }), 9, 'used video PTS values'); | ||
272 | }); | ||
273 | |||
274 | test('works for media without video', function() { | ||
275 | equal(Playlist.duration({ | ||
276 | mediaSequence: 0, | ||
277 | endList: true, | ||
278 | segments: [{ | ||
279 | minAudioPts: 0, | ||
280 | maxAudioPts: 9 * 1000, | ||
281 | uri: 'no-video.ts' | ||
282 | }] | ||
283 | }), 9, 'used video PTS values'); | ||
284 | }); | ||
285 | |||
262 | test('uses the largest continuous available PTS ranges', function() { | 286 | test('uses the largest continuous available PTS ranges', function() { |
263 | var playlist = { | 287 | var playlist = { |
264 | mediaSequence: 0, | 288 | mediaSequence: 0, | ... | ... |
... | @@ -284,6 +284,17 @@ | ... | @@ -284,6 +284,17 @@ |
284 | equal(packets.length, 1, 'parsed non-payload metadata packet'); | 284 | equal(packets.length, 1, 'parsed non-payload metadata packet'); |
285 | }); | 285 | }); |
286 | 286 | ||
287 | test('returns undefined for PTS stats when a track is missing', function() { | ||
288 | parser.parseSegmentBinaryData(new Uint8Array(makePacket({ | ||
289 | programs: { | ||
290 | 0x01: [0x01] | ||
291 | } | ||
292 | }))); | ||
293 | |||
294 | strictEqual(parser.stats.h264Tags(), 0, 'no video tags yet'); | ||
295 | strictEqual(parser.stats.aacTags(), 0, 'no audio tags yet'); | ||
296 | }); | ||
297 | |||
287 | test('parses the first bipbop segment', function() { | 298 | test('parses the first bipbop segment', function() { |
288 | parser.parseSegmentBinaryData(window.bcSegment); | 299 | parser.parseSegmentBinaryData(window.bcSegment); |
289 | 300 | ... | ... |
... | @@ -121,12 +121,18 @@ var | ... | @@ -121,12 +121,18 @@ var |
121 | ]); | 121 | ]); |
122 | 122 | ||
123 | this.stats = { | 123 | this.stats = { |
124 | h264Tags: function() { | ||
125 | return tags.length; | ||
126 | }, | ||
124 | minVideoPts: function() { | 127 | minVideoPts: function() { |
125 | return tags[0].pts; | 128 | return tags[0].pts; |
126 | }, | 129 | }, |
127 | maxVideoPts: function() { | 130 | maxVideoPts: function() { |
128 | return tags[tags.length - 1].pts; | 131 | return tags[tags.length - 1].pts; |
129 | }, | 132 | }, |
133 | aacTags: function() { | ||
134 | return tags.length; | ||
135 | }, | ||
130 | minAudioPts: function() { | 136 | minAudioPts: function() { |
131 | return tags[0].pts; | 137 | return tags[0].pts; |
132 | }, | 138 | }, |
... | @@ -1044,6 +1050,64 @@ test('records the min and max PTS values for a segment', function() { | ... | @@ -1044,6 +1050,64 @@ test('records the min and max PTS values for a segment', function() { |
1044 | equal(player.hls.playlists.media().segments[0].maxAudioPts, 10, 'recorded max audio pts'); | 1050 | equal(player.hls.playlists.media().segments[0].maxAudioPts, 10, 'recorded max audio pts'); |
1045 | }); | 1051 | }); |
1046 | 1052 | ||
1053 | test('records PTS values for video-only segments', function() { | ||
1054 | var tags = []; | ||
1055 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1056 | player.src({ | ||
1057 | src: 'manifest/media.m3u8', | ||
1058 | type: 'application/vnd.apple.mpegurl' | ||
1059 | }); | ||
1060 | openMediaSource(player); | ||
1061 | standardXHRResponse(requests.pop()); // media.m3u8 | ||
1062 | |||
1063 | player.hls.segmentParser_.stats.aacTags = function() { | ||
1064 | return 0; | ||
1065 | }; | ||
1066 | player.hls.segmentParser_.stats.minAudioPts = function() { | ||
1067 | throw new Error('No audio tags'); | ||
1068 | }; | ||
1069 | player.hls.segmentParser_.stats.maxAudioPts = function() { | ||
1070 | throw new Error('No audio tags'); | ||
1071 | }; | ||
1072 | tags.push({ pts: 0, bytes: new Uint8Array(1) }); | ||
1073 | tags.push({ pts: 10, bytes: new Uint8Array(1) }); | ||
1074 | standardXHRResponse(requests.pop()); // segment 0 | ||
1075 | |||
1076 | equal(player.hls.playlists.media().segments[0].minVideoPts, 0, 'recorded min video pts'); | ||
1077 | equal(player.hls.playlists.media().segments[0].maxVideoPts, 10, 'recorded max video pts'); | ||
1078 | strictEqual(player.hls.playlists.media().segments[0].minAudioPts, undefined, 'min audio pts is undefined'); | ||
1079 | strictEqual(player.hls.playlists.media().segments[0].maxAudioPts, undefined, 'max audio pts is undefined'); | ||
1080 | }); | ||
1081 | |||
1082 | test('records PTS values for audio-only segments', function() { | ||
1083 | var tags = []; | ||
1084 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1085 | player.src({ | ||
1086 | src: 'manifest/media.m3u8', | ||
1087 | type: 'application/vnd.apple.mpegurl' | ||
1088 | }); | ||
1089 | openMediaSource(player); | ||
1090 | standardXHRResponse(requests.pop()); // media.m3u8 | ||
1091 | |||
1092 | player.hls.segmentParser_.stats.h264Tags = function() { | ||
1093 | return 0; | ||
1094 | }; | ||
1095 | player.hls.segmentParser_.stats.minVideoPts = function() { | ||
1096 | throw new Error('No video tags'); | ||
1097 | }; | ||
1098 | player.hls.segmentParser_.stats.maxVideoPts = function() { | ||
1099 | throw new Error('No video tags'); | ||
1100 | }; | ||
1101 | tags.push({ pts: 0, bytes: new Uint8Array(1) }); | ||
1102 | tags.push({ pts: 10, bytes: new Uint8Array(1) }); | ||
1103 | standardXHRResponse(requests.pop()); // segment 0 | ||
1104 | |||
1105 | equal(player.hls.playlists.media().segments[0].minAudioPts, 0, 'recorded min audio pts'); | ||
1106 | equal(player.hls.playlists.media().segments[0].maxAudioPts, 10, 'recorded max audio pts'); | ||
1107 | strictEqual(player.hls.playlists.media().segments[0].minVideoPts, undefined, 'min video pts is undefined'); | ||
1108 | strictEqual(player.hls.playlists.media().segments[0].maxVideoPts, undefined, 'max video pts is undefined'); | ||
1109 | }); | ||
1110 | |||
1047 | test('waits to download new segments until the media playlist is stable', function() { | 1111 | test('waits to download new segments until the media playlist is stable', function() { |
1048 | var media; | 1112 | var media; |
1049 | player.src({ | 1113 | player.src({ | ... | ... |
-
Please register or sign in to post a comment