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.
Showing
4 changed files
with
93 additions
and
52 deletions
... | @@ -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_(); | ... | ... |
-
Please register or sign in to post a comment