b58a7fcb by David LaPalomento

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.
1 parent 385ed9fb
...@@ -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,9 +277,9 @@ ...@@ -257,9 +277,9 @@
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);
264 liveBuffer -= pending; 284 liveBuffer -= pending;
265 end -= pending; 285 end -= pending;
......
...@@ -899,10 +899,14 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -899,10 +899,14 @@ 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 segment.minVideoPts = this.segmentParser_.stats.minVideoPts(); 902 if (this.segmentParser_.stats.h264Tags()) {
903 segment.maxVideoPts = this.segmentParser_.stats.maxVideoPts(); 903 segment.minVideoPts = this.segmentParser_.stats.minVideoPts();
904 segment.minAudioPts = this.segmentParser_.stats.minAudioPts(); 904 segment.maxVideoPts = this.segmentParser_.stats.maxVideoPts();
905 segment.maxAudioPts = this.segmentParser_.stats.maxAudioPts(); 905 }
906 if (this.segmentParser_.stats.aacTags()) {
907 segment.minAudioPts = this.segmentParser_.stats.minAudioPts();
908 segment.maxAudioPts = this.segmentParser_.stats.maxAudioPts();
909 }
906 } 910 }
907 911
908 while (this.segmentParser_.tagsAvailable()) { 912 while (this.segmentParser_.tagsAvailable()) {
......
...@@ -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({
......