cc20895d by David LaPalomento

Record min and max PTS values for segments

Segment duration can have different interpretations in different contexts. To ensure we have all the information we need to seek accurately, record PTS values for the audio and video streams within a segment. For #314.
1 parent 675d9423
...@@ -479,8 +479,20 @@ ...@@ -479,8 +479,20 @@
479 h264Tags: function() { 479 h264Tags: function() {
480 return h264Stream.tags.length; 480 return h264Stream.tags.length;
481 }, 481 },
482 minVideoPts: function() {
483 return h264Stream.tags[0].pts;
484 },
485 maxVideoPts: function() {
486 return h264Stream.tags[h264Stream.tags.length - 1].pts;
487 },
482 aacTags: function() { 488 aacTags: function() {
483 return aacStream.tags.length; 489 return aacStream.tags.length;
490 },
491 minAudioPts: function() {
492 return aacStream.tags[0].pts;
493 },
494 maxAudioPts: function() {
495 return aacStream.tags[aacStream.tags.length - 1].pts;
484 } 496 }
485 }; 497 };
486 }; 498 };
......
...@@ -870,6 +870,15 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -870,6 +870,15 @@ videojs.Hls.prototype.drainBuffer = function(event) {
870 870
871 tags = []; 871 tags = [];
872 872
873 if (this.segmentParser_.tagsAvailable()) {
874 // record PTS information for the segment so we can calculate
875 // accurate durations and seek reliably
876 segment.minVideoPts = this.segmentParser_.stats.minVideoPts();
877 segment.maxVideoPts = this.segmentParser_.stats.maxVideoPts();
878 segment.minAudioPts = this.segmentParser_.stats.minAudioPts();
879 segment.maxAudioPts = this.segmentParser_.stats.maxAudioPts();
880 }
881
873 while (this.segmentParser_.tagsAvailable()) { 882 while (this.segmentParser_.tagsAvailable()) {
874 tags.push(this.segmentParser_.getNextTag()); 883 tags.push(this.segmentParser_.getNextTag());
875 } 884 }
......
...@@ -422,10 +422,19 @@ ...@@ -422,10 +422,19 @@
422 byte, 422 byte,
423 tag, 423 tag,
424 type, 424 type,
425 minVideoPts,
426 maxVideoPts,
427 minAudioPts,
428 maxAudioPts,
425 currentPts = 0, 429 currentPts = 0,
426 lastTime = 0; 430 lastTime = 0;
427 parser.parseSegmentBinaryData(window.bcSegment); 431 parser.parseSegmentBinaryData(window.bcSegment);
428 432
433 minVideoPts = parser.stats.minVideoPts();
434 maxVideoPts = parser.stats.maxVideoPts();
435 minAudioPts = parser.stats.minAudioPts();
436 maxAudioPts = parser.stats.maxAudioPts();
437
429 while (parser.tagsAvailable()) { 438 while (parser.tagsAvailable()) {
430 tag = parser.getNextTag(); 439 tag = parser.getNextTag();
431 type = tag.bytes[0]; 440 type = tag.bytes[0];
...@@ -435,11 +444,15 @@ ...@@ -435,11 +444,15 @@
435 444
436 // generic flv headers 445 // generic flv headers
437 switch (type) { 446 switch (type) {
438 case 8: ok(true, 'the type is audio'); 447 case 8: ok(true, 'the type is audio');
448 ok(minAudioPts <= currentPts, 'not less than minimum audio PTS');
449 ok(maxAudioPts >= currentPts, 'not greater than max audio PTS');
439 break; 450 break;
440 case 9: ok(true, 'the type is video'); 451 case 9: ok(true, 'the type is video');
452 ok(minVideoPts <= currentPts, 'not less than minimum video PTS');
453 ok(maxVideoPts >= currentPts, 'not greater than max video PTS');
441 break; 454 break;
442 case 18: ok(true, 'the type is script'); 455 case 18: ok(true, 'the type is script');
443 break; 456 break;
444 default: ok(false, 'the type (' + type + ') is unrecognized'); 457 default: ok(false, 'the type (' + type + ') is unrecognized');
445 } 458 }
......
...@@ -94,14 +94,18 @@ var ...@@ -94,14 +94,18 @@ var
94 }, 94 },
95 95
96 mockSegmentParser = function(tags) { 96 mockSegmentParser = function(tags) {
97 var MockSegmentParser;
98
97 if (tags === undefined) { 99 if (tags === undefined) {
98 tags = []; 100 tags = [];
99 } 101 }
100 return function() { 102 MockSegmentParser = function() {
101 this.getFlvHeader = function() { 103 this.getFlvHeader = function() {
102 return 'flv'; 104 return 'flv';
103 }; 105 };
104 this.parseSegmentBinaryData = function() {}; 106 this.parseSegmentBinaryData = function() {};
107 this.timestampOffset = 0;
108 this.mediaTimelineOffset = 0;
105 this.flushTags = function() {}; 109 this.flushTags = function() {};
106 this.tagsAvailable = function() { 110 this.tagsAvailable = function() {
107 return tags.length; 111 return tags.length;
...@@ -112,10 +116,31 @@ var ...@@ -112,10 +116,31 @@ var
112 this.getNextTag = function() { 116 this.getNextTag = function() {
113 return tags.shift(); 117 return tags.shift();
114 }; 118 };
115 this.metadataStream = { 119 this.metadataStream = new videojs.Hls.Stream();
116 on: Function.prototype 120 this.metadataStream.init();
121 this.metadataStream.descriptor = new Uint8Array([
122 1, 2, 3, 0xbb
123 ]);
124
125 this.stats = {
126 minVideoPts: function() {
127 return tags[0].pts;
128 },
129 maxVideoPts: function() {
130 return tags[tags.length - 1].pts;
131 },
132 minAudioPts: function() {
133 return tags[0].pts;
134 },
135 maxAudioPts: function() {
136 return tags[tags.length - 1].pts;
137 },
117 }; 138 };
118 }; 139 };
140
141 MockSegmentParser.STREAM_TYPES = videojs.Hls.SegmentParser.STREAM_TYPES;
142
143 return MockSegmentParser;
119 }, 144 },
120 145
121 // return an absolute version of a page-relative URL 146 // return an absolute version of a page-relative URL
...@@ -1001,6 +1026,26 @@ test('only appends one segment at a time', function() { ...@@ -1001,6 +1026,26 @@ test('only appends one segment at a time', function() {
1001 equal(appends, 0, 'did not append while updating'); 1026 equal(appends, 0, 'did not append while updating');
1002 }); 1027 });
1003 1028
1029 test('records the min and max PTS values for a segment', function() {
1030 var tags = [];
1031 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1032 player.src({
1033 src: 'manifest/media.m3u8',
1034 type: 'application/vnd.apple.mpegurl'
1035 });
1036 openMediaSource(player);
1037 standardXHRResponse(requests.pop()); // media.m3u8
1038
1039 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1040 tags.push({ pts: 10, bytes: new Uint8Array(1) });
1041 standardXHRResponse(requests.pop()); // segment 0
1042
1043 equal(player.hls.playlists.media().segments[0].minVideoPts, 0, 'recorded min video pts');
1044 equal(player.hls.playlists.media().segments[0].maxVideoPts, 10, 'recorded max video pts');
1045 equal(player.hls.playlists.media().segments[0].minAudioPts, 0, 'recorded min audio pts');
1046 equal(player.hls.playlists.media().segments[0].maxAudioPts, 10, 'recorded max audio pts');
1047 });
1048
1004 test('waits to download new segments until the media playlist is stable', function() { 1049 test('waits to download new segments until the media playlist is stable', function() {
1005 var media; 1050 var media;
1006 player.src({ 1051 player.src({
...@@ -1192,6 +1237,7 @@ test('calculates preciseDuration correctly around discontinuities', function() { ...@@ -1192,6 +1237,7 @@ test('calculates preciseDuration correctly around discontinuities', function() {
1192 1237
1193 test('exposes in-band metadata events as cues', function() { 1238 test('exposes in-band metadata events as cues', function() {
1194 var track; 1239 var track;
1240 videojs.Hls.SegmentParser = mockSegmentParser();
1195 player.src({ 1241 player.src({
1196 src: 'manifest/media.m3u8', 1242 src: 'manifest/media.m3u8',
1197 type: 'application/vnd.apple.mpegurl' 1243 type: 'application/vnd.apple.mpegurl'
...@@ -1199,10 +1245,6 @@ test('exposes in-band metadata events as cues', function() { ...@@ -1199,10 +1245,6 @@ test('exposes in-band metadata events as cues', function() {
1199 openMediaSource(player); 1245 openMediaSource(player);
1200 1246
1201 player.hls.segmentParser_.parseSegmentBinaryData = function() { 1247 player.hls.segmentParser_.parseSegmentBinaryData = function() {
1202 // fake out a descriptor
1203 player.hls.segmentParser_.metadataStream.descriptor = new Uint8Array([
1204 1, 2, 3, 0xbb
1205 ]);
1206 // trigger a metadata event 1248 // trigger a metadata event
1207 player.hls.segmentParser_.metadataStream.trigger('data', { 1249 player.hls.segmentParser_.metadataStream.trigger('data', {
1208 pts: 2000, 1250 pts: 2000,
...@@ -1251,23 +1293,14 @@ test('exposes in-band metadata events as cues', function() { ...@@ -1251,23 +1293,14 @@ test('exposes in-band metadata events as cues', function() {
1251 1293
1252 test('only adds in-band cues the first time they are encountered', function() { 1294 test('only adds in-band cues the first time they are encountered', function() {
1253 var tags = [{ pts: 0, bytes: new Uint8Array(1) }], track; 1295 var tags = [{ pts: 0, bytes: new Uint8Array(1) }], track;
1296 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1254 player.src({ 1297 player.src({
1255 src: 'manifest/media.m3u8', 1298 src: 'manifest/media.m3u8',
1256 type: 'application/vnd.apple.mpegurl' 1299 type: 'application/vnd.apple.mpegurl'
1257 }); 1300 });
1258 openMediaSource(player); 1301 openMediaSource(player);
1259 1302
1260 player.hls.segmentParser_.getNextTag = function() {
1261 return tags.shift();
1262 };
1263 player.hls.segmentParser_.tagsAvailable = function() {
1264 return tags.length;
1265 };
1266 player.hls.segmentParser_.parseSegmentBinaryData = function() { 1303 player.hls.segmentParser_.parseSegmentBinaryData = function() {
1267 // fake out a descriptor
1268 player.hls.segmentParser_.metadataStream.descriptor = new Uint8Array([
1269 1, 2, 3, 0xbb
1270 ]);
1271 // trigger a metadata event 1304 // trigger a metadata event
1272 player.hls.segmentParser_.metadataStream.trigger('data', { 1305 player.hls.segmentParser_.metadataStream.trigger('data', {
1273 pts: 2000, 1306 pts: 2000,
...@@ -1295,23 +1328,14 @@ test('clears in-band cues ahead of current time on seek', function() { ...@@ -1295,23 +1328,14 @@ test('clears in-band cues ahead of current time on seek', function() {
1295 tags = [], 1328 tags = [],
1296 events = [], 1329 events = [],
1297 track; 1330 track;
1331 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1298 player.src({ 1332 player.src({
1299 src: 'manifest/media.m3u8', 1333 src: 'manifest/media.m3u8',
1300 type: 'application/vnd.apple.mpegurl' 1334 type: 'application/vnd.apple.mpegurl'
1301 }); 1335 });
1302 openMediaSource(player); 1336 openMediaSource(player);
1303 1337
1304 player.hls.segmentParser_.getNextTag = function() {
1305 return tags.shift();
1306 };
1307 player.hls.segmentParser_.tagsAvailable = function() {
1308 return tags.length;
1309 };
1310 player.hls.segmentParser_.parseSegmentBinaryData = function() { 1338 player.hls.segmentParser_.parseSegmentBinaryData = function() {
1311 // fake out a descriptor
1312 player.hls.segmentParser_.metadataStream.descriptor = new Uint8Array([
1313 1, 2, 3, 0xbb
1314 ]);
1315 // trigger a metadata event 1339 // trigger a metadata event
1316 if (events.length) { 1340 if (events.length) {
1317 player.hls.segmentParser_.metadataStream.trigger('data', events.shift()); 1341 player.hls.segmentParser_.metadataStream.trigger('data', events.shift());
...@@ -1360,26 +1384,17 @@ test('clears in-band cues ahead of current time on seek', function() { ...@@ -1360,26 +1384,17 @@ test('clears in-band cues ahead of current time on seek', function() {
1360 1384
1361 test('translates ID3 PTS values to cue media timeline positions', function() { 1385 test('translates ID3 PTS values to cue media timeline positions', function() {
1362 var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track; 1386 var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track;
1387 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1363 player.src({ 1388 player.src({
1364 src: 'manifest/media.m3u8', 1389 src: 'manifest/media.m3u8',
1365 type: 'application/vnd.apple.mpegurl' 1390 type: 'application/vnd.apple.mpegurl'
1366 }); 1391 });
1367 openMediaSource(player); 1392 openMediaSource(player);
1368 1393
1369 player.hls.segmentParser_.getNextTag = function() {
1370 return tags.shift();
1371 };
1372 player.hls.segmentParser_.tagsAvailable = function() {
1373 return tags.length;
1374 };
1375 player.hls.segmentParser_.parseSegmentBinaryData = function() { 1394 player.hls.segmentParser_.parseSegmentBinaryData = function() {
1376 // setup the timestamp offset 1395 // setup the timestamp offset
1377 this.timestampOffset = tags[0].pts; 1396 this.timestampOffset = tags[0].pts;
1378 1397
1379 // fake out a descriptor
1380 player.hls.segmentParser_.metadataStream.descriptor = new Uint8Array([
1381 1, 2, 3, 0xbb
1382 ]);
1383 // trigger a metadata event 1398 // trigger a metadata event
1384 player.hls.segmentParser_.metadataStream.trigger('data', { 1399 player.hls.segmentParser_.metadataStream.trigger('data', {
1385 pts: 5 * 1000, 1400 pts: 5 * 1000,
...@@ -1400,26 +1415,17 @@ test('translates ID3 PTS values to cue media timeline positions', function() { ...@@ -1400,26 +1415,17 @@ test('translates ID3 PTS values to cue media timeline positions', function() {
1400 1415
1401 test('translates ID3 PTS values across discontinuities', function() { 1416 test('translates ID3 PTS values across discontinuities', function() {
1402 var tags = [], events = [], track; 1417 var tags = [], events = [], track;
1418 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1403 player.src({ 1419 player.src({
1404 src: 'cues-and-discontinuities.m3u8', 1420 src: 'cues-and-discontinuities.m3u8',
1405 type: 'application/vnd.apple.mpegurl' 1421 type: 'application/vnd.apple.mpegurl'
1406 }); 1422 });
1407 openMediaSource(player); 1423 openMediaSource(player);
1408 1424
1409 player.hls.segmentParser_.getNextTag = function() {
1410 return tags.shift();
1411 };
1412 player.hls.segmentParser_.tagsAvailable = function() {
1413 return tags.length;
1414 };
1415 player.hls.segmentParser_.parseSegmentBinaryData = function() { 1425 player.hls.segmentParser_.parseSegmentBinaryData = function() {
1416 if (this.timestampOffset === null) { 1426 if (this.timestampOffset === null) {
1417 this.timestampOffset = tags[0].pts; 1427 this.timestampOffset = tags[0].pts;
1418 } 1428 }
1419 // fake out a descriptor
1420 player.hls.segmentParser_.metadataStream.descriptor = new Uint8Array([
1421 1, 2, 3, 0xbb
1422 ]);
1423 // trigger a metadata event 1429 // trigger a metadata event
1424 if (events.length) { 1430 if (events.length) {
1425 player.hls.segmentParser_.metadataStream.trigger('data', events.shift()); 1431 player.hls.segmentParser_.metadataStream.trigger('data', events.shift());
...@@ -1437,6 +1443,7 @@ test('translates ID3 PTS values across discontinuities', function() { ...@@ -1437,6 +1443,7 @@ test('translates ID3 PTS values across discontinuities', function() {
1437 '1.ts\n'); 1443 '1.ts\n');
1438 1444
1439 // segment 0 starts at PTS 14000 and has a cue point at 15000 1445 // segment 0 starts at PTS 14000 and has a cue point at 15000
1446 player.hls.segmentParser_.timestampOffset = 14 * 1000;
1440 tags.push({ pts: 14 * 1000, bytes: new Uint8Array(1) }); 1447 tags.push({ pts: 14 * 1000, bytes: new Uint8Array(1) });
1441 events.push({ 1448 events.push({
1442 pts: 15 * 1000, 1449 pts: 15 * 1000,
...@@ -1449,14 +1456,14 @@ test('translates ID3 PTS values across discontinuities', function() { ...@@ -1449,14 +1456,14 @@ test('translates ID3 PTS values across discontinuities', function() {
1449 standardXHRResponse(requests.shift()); // segment 0 1456 standardXHRResponse(requests.shift()); // segment 0
1450 1457
1451 // segment 1 is after a discontinuity, starts at PTS 22000 1458 // segment 1 is after a discontinuity, starts at PTS 22000
1452 // and has a cue point at 15000 1459 // and has a cue point at 23000
1453 tags.push({ pts: 22 * 1000, bytes: new Uint8Array(1) }); 1460 tags.push({ pts: 22 * 1000, bytes: new Uint8Array(1) });
1454 events.push({ 1461 events.push({
1455 pts: 23 * 1000, 1462 pts: 23 * 1000,
1456 data: new Uint8Array([]), 1463 data: new Uint8Array([]),
1457 frames: [{ 1464 frames: [{
1458 id: 'TXXX', 1465 id: 'TXXX',
1459 value: 'cue 0' 1466 value: 'cue 1'
1460 }] 1467 }]
1461 }); 1468 });
1462 player.hls.checkBuffer_(); 1469 player.hls.checkBuffer_();
......