improved video duration calculation. closes #321
Showing
11 changed files
with
275 additions
and
170 deletions
... | @@ -2,7 +2,7 @@ CHANGELOG | ... | @@ -2,7 +2,7 @@ CHANGELOG |
2 | ========= | 2 | ========= |
3 | 3 | ||
4 | ## HEAD (Unreleased) | 4 | ## HEAD (Unreleased) |
5 | _(none)_ | 5 | * @dmlap improved video duration calculation. ([view](https://github.com/videojs/videojs-contrib-hls/pull/321)) |
6 | 6 | ||
7 | -------------------- | 7 | -------------------- |
8 | 8 | ... | ... |
... | @@ -363,29 +363,4 @@ hls.FlvTag.frameTime = function(tag) { | ... | @@ -363,29 +363,4 @@ hls.FlvTag.frameTime = function(tag) { |
363 | return pts; | 363 | return pts; |
364 | }; | 364 | }; |
365 | 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 | ||
381 | first = tags[0], | ||
382 | last = tags[tags.length - 1], | ||
383 | frameDuration; | ||
384 | |||
385 | // use the interval between the last two tags or assume 24 fps | ||
386 | frameDuration = last.pts - tags[tags.length - 2].pts || (1/24); | ||
387 | |||
388 | return (last.pts - first.pts) + frameDuration; | ||
389 | }; | ||
390 | |||
391 | })(this); | 366 | })(this); | ... | ... |
... | @@ -18,6 +18,7 @@ | ... | @@ -18,6 +18,7 @@ |
18 | resolveUrl = videojs.Hls.resolveUrl, | 18 | resolveUrl = videojs.Hls.resolveUrl, |
19 | xhr = videojs.Hls.xhr, | 19 | xhr = videojs.Hls.xhr, |
20 | Playlist = videojs.Hls.Playlist, | 20 | Playlist = videojs.Hls.Playlist, |
21 | mergeOptions = videojs.util.mergeOptions, | ||
21 | 22 | ||
22 | /** | 23 | /** |
23 | * Returns a new master playlist that is the result of merging an | 24 | * Returns a new master playlist that is the result of merging an |
... | @@ -33,7 +34,7 @@ | ... | @@ -33,7 +34,7 @@ |
33 | updateMaster = function(master, media) { | 34 | updateMaster = function(master, media) { |
34 | var | 35 | var |
35 | changed = false, | 36 | changed = false, |
36 | result = videojs.util.mergeOptions(master, {}), | 37 | result = mergeOptions(master, {}), |
37 | i, | 38 | i, |
38 | playlist; | 39 | playlist; |
39 | 40 | ||
... | @@ -50,14 +51,47 @@ | ... | @@ -50,14 +51,47 @@ |
50 | continue; | 51 | continue; |
51 | } | 52 | } |
52 | 53 | ||
53 | result.playlists[i] = videojs.util.mergeOptions(playlist, media); | 54 | result.playlists[i] = mergeOptions(playlist, media); |
54 | result.playlists[media.uri] = result.playlists[i]; | 55 | result.playlists[media.uri] = result.playlists[i]; |
56 | |||
57 | // if the update could overlap existing segment information, | ||
58 | // merge the two lists | ||
59 | if (playlist.segments) { | ||
60 | result.playlists[i].segments = updateSegments(playlist.segments, | ||
61 | media.segments, | ||
62 | media.mediaSequence - playlist.mediaSequence); | ||
63 | } | ||
55 | changed = true; | 64 | changed = true; |
56 | } | 65 | } |
57 | } | 66 | } |
58 | return changed ? result : null; | 67 | return changed ? result : null; |
59 | }, | 68 | }, |
60 | 69 | ||
70 | /** | ||
71 | * Returns a new array of segments that is the result of merging | ||
72 | * properties from an older list of segments onto an updated | ||
73 | * list. No properties on the updated playlist will be overridden. | ||
74 | * @param original {array} the outdated list of segments | ||
75 | * @param update {array} the updated list of segments | ||
76 | * @param offset {number} (optional) the index of the first update | ||
77 | * segment in the original segment list. For non-live playlists, | ||
78 | * this should always be zero and does not need to be | ||
79 | * specified. For live playlists, it should be the difference | ||
80 | * between the media sequence numbers in the original and updated | ||
81 | * playlists. | ||
82 | * @return a list of merged segment objects | ||
83 | */ | ||
84 | updateSegments = function(original, update, offset) { | ||
85 | var result = update.slice(), length, i; | ||
86 | offset = offset || 0; | ||
87 | length = Math.min(original.length, update.length + offset); | ||
88 | |||
89 | for (i = offset; i < length; i++) { | ||
90 | result[i - offset] = mergeOptions(original[i], result[i - offset]); | ||
91 | } | ||
92 | return result; | ||
93 | }, | ||
94 | |||
61 | PlaylistLoader = function(srcUrl, withCredentials) { | 95 | PlaylistLoader = function(srcUrl, withCredentials) { |
62 | var | 96 | var |
63 | loader = this, | 97 | loader = this, | ... | ... |
... | @@ -21,7 +21,7 @@ | ... | @@ -21,7 +21,7 @@ |
21 | * index. | 21 | * index. |
22 | */ | 22 | */ |
23 | segmentsDuration = function(playlist, startSequence, endSequence) { | 23 | segmentsDuration = function(playlist, startSequence, endSequence) { |
24 | var targetDuration, i, segment, expiredSegmentCount, result = 0; | 24 | var targetDuration, i, j, segment, endSegment, expiredSegmentCount, result = 0; |
25 | 25 | ||
26 | startSequence = startSequence || 0; | 26 | startSequence = startSequence || 0; |
27 | i = startSequence; | 27 | i = startSequence; |
... | @@ -36,9 +36,27 @@ | ... | @@ -36,9 +36,27 @@ |
36 | // accumulate the segment durations into the result | 36 | // accumulate the segment durations into the result |
37 | for (; i < endSequence; i++) { | 37 | for (; i < endSequence; i++) { |
38 | segment = playlist.segments[i - playlist.mediaSequence]; | 38 | segment = playlist.segments[i - playlist.mediaSequence]; |
39 | result += segment.preciseDuration || | 39 | |
40 | segment.duration || | 40 | // when PTS values aren't available, use information from the playlist |
41 | targetDuration; | 41 | if (segment.minVideoPts === undefined) { |
42 | result += segment.duration || | ||
43 | targetDuration; | ||
44 | continue; | ||
45 | } | ||
46 | |||
47 | // find the last segment with PTS info and use that to calculate | ||
48 | // the interval duration | ||
49 | for(j = i; j < endSequence - 1; j++) { | ||
50 | endSegment = playlist.segments[j - playlist.mediaSequence + 1]; | ||
51 | if (endSegment.maxVideoPts === undefined || | ||
52 | endSegment.discontinuity) { | ||
53 | break; | ||
54 | } | ||
55 | } | ||
56 | endSegment = playlist.segments[j - playlist.mediaSequence]; | ||
57 | result += (Math.max(endSegment.maxVideoPts, endSegment.maxAudioPts) - | ||
58 | Math.min(segment.minVideoPts, segment.minAudioPts)) * 0.001; | ||
59 | i = j; | ||
42 | } | 60 | } |
43 | 61 | ||
44 | return result; | 62 | return result; | ... | ... |
... | @@ -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,15 +870,17 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -870,15 +870,17 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
870 | 870 | ||
871 | tags = []; | 871 | tags = []; |
872 | 872 | ||
873 | while (this.segmentParser_.tagsAvailable()) { | 873 | if (this.segmentParser_.tagsAvailable()) { |
874 | tags.push(this.segmentParser_.getNextTag()); | 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(); | ||
875 | } | 880 | } |
876 | 881 | ||
877 | if (tags.length > 0) { | 882 | while (this.segmentParser_.tagsAvailable()) { |
878 | // Use the presentation timestamp of the ts segment to calculate its | 883 | tags.push(this.segmentParser_.getNextTag()); |
879 | // exact duration, since this may differ by fractions of a second | ||
880 | // from what is reported in the playlist | ||
881 | segment.preciseDuration = videojs.Hls.FlvTag.durationFromTags(tags) * 0.001; | ||
882 | } | 884 | } |
883 | 885 | ||
884 | this.updateDuration(this.playlists.media()); | 886 | this.updateDuration(this.playlists.media()); | ... | ... |
... | @@ -57,32 +57,4 @@ test('writeBytes grows the internal byte array dynamically', function() { | ... | @@ -57,32 +57,4 @@ 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 | |||
88 | })(this); | 60 | })(this); | ... | ... |
... | @@ -331,6 +331,35 @@ | ... | @@ -331,6 +331,35 @@ |
331 | 'requested the media playlist'); | 331 | 'requested the media playlist'); |
332 | }); | 332 | }); |
333 | 333 | ||
334 | test('preserves segment metadata across playlist refreshes', function() { | ||
335 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'), segment; | ||
336 | requests.pop().respond(200, null, | ||
337 | '#EXTM3U\n' + | ||
338 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
339 | '#EXTINF:10,\n' + | ||
340 | '0.ts\n' + | ||
341 | '#EXTINF:10,\n' + | ||
342 | '1.ts\n' + | ||
343 | '#EXTINF:10,\n' + | ||
344 | '2.ts\n'); | ||
345 | // add PTS info to 1.ts | ||
346 | segment = loader.media().segments[1]; | ||
347 | segment.minVideoPts = 14; | ||
348 | segment.maxAudioPts = 27; | ||
349 | segment.preciseDuration = 10.045; | ||
350 | |||
351 | clock.tick(10 * 1000); // trigger a refresh | ||
352 | requests.pop().respond(200, null, | ||
353 | '#EXTM3U\n' + | ||
354 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
355 | '#EXTINF:10,\n' + | ||
356 | '1.ts\n' + | ||
357 | '#EXTINF:10,\n' + | ||
358 | '2.ts\n'); | ||
359 | |||
360 | deepEqual(loader.media().segments[0], segment, 'preserved segment attributes'); | ||
361 | }); | ||
362 | |||
334 | test('clears the update timeout when switching quality', function() { | 363 | test('clears the update timeout when switching quality', function() { |
335 | var loader = new videojs.Hls.PlaylistLoader('live-master.m3u8'), refreshes = 0; | 364 | var loader = new videojs.Hls.PlaylistLoader('live-master.m3u8'), refreshes = 0; |
336 | // track the number of playlist refreshes triggered | 365 | // track the number of playlist refreshes triggered | ... | ... |
... | @@ -38,6 +38,98 @@ | ... | @@ -38,6 +38,98 @@ |
38 | equal(duration, 14 * 10, 'duration includes dropped segments'); | 38 | equal(duration, 14 * 10, 'duration includes dropped segments'); |
39 | }); | 39 | }); |
40 | 40 | ||
41 | test('interval duration uses PTS values when available', function() { | ||
42 | var duration = Playlist.duration({ | ||
43 | mediaSequence: 0, | ||
44 | endList: true, | ||
45 | segments: [{ | ||
46 | minVideoPts: 1, | ||
47 | minAudioPts: 2, | ||
48 | uri: '0.ts' | ||
49 | }, { | ||
50 | duration: 10, | ||
51 | maxVideoPts: 2 * 10 * 1000 + 1, | ||
52 | maxAudioPts: 2 * 10 * 1000 + 2, | ||
53 | uri: '1.ts' | ||
54 | }, { | ||
55 | duration: 10, | ||
56 | maxVideoPts: 3 * 10 * 1000 + 1, | ||
57 | maxAudioPts: 3 * 10 * 1000 + 2, | ||
58 | uri: '2.ts' | ||
59 | }, { | ||
60 | duration: 10, | ||
61 | maxVideoPts: 4 * 10 * 1000 + 1, | ||
62 | maxAudioPts: 4 * 10 * 1000 + 2, | ||
63 | uri: '3.ts' | ||
64 | }] | ||
65 | }, 0, 4); | ||
66 | |||
67 | equal(duration, ((4 * 10 * 1000 + 2) - 1) * 0.001, 'used PTS values'); | ||
68 | }); | ||
69 | |||
70 | test('interval duration works when partial PTS information is available', function() { | ||
71 | var firstInterval, secondInterval, duration = Playlist.duration({ | ||
72 | mediaSequence: 0, | ||
73 | endList: true, | ||
74 | segments: [{ | ||
75 | minVideoPts: 1, | ||
76 | minAudioPts: 2, | ||
77 | maxVideoPts: 1 * 10 * 1000 + 1, | ||
78 | |||
79 | // intentionally less duration than video | ||
80 | // the max stream duration should be used | ||
81 | maxAudioPts: 1 * 10 * 1000 + 1, | ||
82 | uri: '0.ts' | ||
83 | }, { | ||
84 | duration: 10, | ||
85 | uri: '1.ts' | ||
86 | }, { | ||
87 | duration: 10, | ||
88 | minVideoPts: 2 * 10 * 1000 + 7, | ||
89 | minAudioPts: 2 * 10 * 1000 + 10, | ||
90 | maxVideoPts: 3 * 10 * 1000 + 1, | ||
91 | maxAudioPts: 3 * 10 * 1000 + 2, | ||
92 | uri: '2.ts' | ||
93 | }, { | ||
94 | duration: 10, | ||
95 | maxVideoPts: 4 * 10 * 1000 + 1, | ||
96 | maxAudioPts: 4 * 10 * 1000 + 2, | ||
97 | uri: '3.ts' | ||
98 | }] | ||
99 | }, 0, 4); | ||
100 | |||
101 | firstInterval = (1 * 10 * 1000 + 1) - 1; | ||
102 | firstInterval *= 0.001; | ||
103 | secondInterval = (4 * 10 * 1000 + 2) - (2 * 10 * 1000 + 7); | ||
104 | secondInterval *= 0.001; | ||
105 | |||
106 | equal(duration, firstInterval + 10 + secondInterval, 'calculated with mixed intervals'); | ||
107 | }); | ||
108 | |||
109 | test('interval duration accounts for discontinuities', function() { | ||
110 | var duration = Playlist.duration({ | ||
111 | mediaSequence: 0, | ||
112 | endList: true, | ||
113 | segments: [{ | ||
114 | minVideoPts: 0, | ||
115 | minAudioPts: 0, | ||
116 | maxVideoPts: 1 * 10 * 1000, | ||
117 | maxAudioPts: 1 * 10 * 1000, | ||
118 | uri: '0.ts' | ||
119 | }, { | ||
120 | discontinuity: true, | ||
121 | minVideoPts: 2 * 10 * 1000, | ||
122 | minAudioPts: 2 * 10 * 1000, | ||
123 | maxVideoPts: 3 * 10 * 1000, | ||
124 | maxAudioPts: 3 * 10 * 1000, | ||
125 | duration: 10, | ||
126 | uri: '1.ts' | ||
127 | }] | ||
128 | }, 0, 2); | ||
129 | |||
130 | equal(duration, 10 + 10, 'handles discontinuities'); | ||
131 | }); | ||
132 | |||
41 | test('calculates seekable time ranges from the available segments', function() { | 133 | test('calculates seekable time ranges from the available segments', function() { |
42 | var playlist = { | 134 | var playlist = { |
43 | mediaSequence: 0, | 135 | mediaSequence: 0, | ... | ... |
... | @@ -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({ |
... | @@ -1140,58 +1185,9 @@ test('flushes the parser after each segment', function() { | ... | @@ -1140,58 +1185,9 @@ test('flushes the parser after each segment', function() { |
1140 | strictEqual(flushes, 1, 'tags are flushed at the end of a segment'); | 1185 | strictEqual(flushes, 1, 'tags are flushed at the end of a segment'); |
1141 | }); | 1186 | }); |
1142 | 1187 | ||
1143 | test('calculates preciseDuration for a new segment', function() { | ||
1144 | var tags = [ | ||
1145 | { pts : 200 * 1000, bytes: new Uint8Array(1) }, | ||
1146 | { pts : 300 * 1000, bytes: new Uint8Array(1) } | ||
1147 | ]; | ||
1148 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1149 | |||
1150 | player.src({ | ||
1151 | src: 'manifest/media.m3u8', | ||
1152 | type: 'application/vnd.apple.mpegurl' | ||
1153 | }); | ||
1154 | openMediaSource(player); | ||
1155 | |||
1156 | standardXHRResponse(requests[0]); | ||
1157 | strictEqual(player.duration(), 40, 'player duration is read from playlist on load'); | ||
1158 | standardXHRResponse(requests[1]); | ||
1159 | strictEqual(player.hls.playlists.media().segments[0].preciseDuration, 200, 'preciseDuration is calculated and stored'); | ||
1160 | strictEqual(player.duration(), 230, 'player duration is calculated using preciseDuration'); | ||
1161 | }); | ||
1162 | |||
1163 | test('calculates preciseDuration correctly around discontinuities', function() { | ||
1164 | var tags = []; | ||
1165 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1166 | player.src({ | ||
1167 | src: 'manifest/media.m3u8', | ||
1168 | type: 'application/vnd.apple.mpegurl' | ||
1169 | }); | ||
1170 | openMediaSource(player); | ||
1171 | requests.shift().respond(200, null, | ||
1172 | '#EXTM3U\n' + | ||
1173 | '#EXTINF:10,\n' + | ||
1174 | '0.ts\n' + | ||
1175 | '#EXT-X-DISCONTINUITY\n' + | ||
1176 | '#EXTINF:10,\n' + | ||
1177 | '1.ts\n' + | ||
1178 | '#EXT-X-ENDLIST\n'); | ||
1179 | tags.push({ pts: 10 * 1000, bytes: new Uint8Array(1) }); | ||
1180 | standardXHRResponse(requests.shift()); // segment 0 | ||
1181 | player.hls.checkBuffer_(); | ||
1182 | |||
1183 | // the PTS value of the second segment is *earlier* than the first | ||
1184 | tags.push({ pts: 0 * 1000, bytes: new Uint8Array(1) }); | ||
1185 | tags.push({ pts: 5 * 1000, bytes: new Uint8Array(1) }); | ||
1186 | standardXHRResponse(requests.shift()); // segment 1 | ||
1187 | |||
1188 | equal(player.hls.playlists.media().segments[1].preciseDuration, | ||
1189 | 5 + 5, // duration includes the time to display the second tag | ||
1190 | 'duration is independent of previous segments'); | ||
1191 | }); | ||
1192 | |||
1193 | test('exposes in-band metadata events as cues', function() { | 1188 | test('exposes in-band metadata events as cues', function() { |
1194 | var track; | 1189 | var track; |
1190 | videojs.Hls.SegmentParser = mockSegmentParser(); | ||
1195 | player.src({ | 1191 | player.src({ |
1196 | src: 'manifest/media.m3u8', | 1192 | src: 'manifest/media.m3u8', |
1197 | type: 'application/vnd.apple.mpegurl' | 1193 | type: 'application/vnd.apple.mpegurl' |
... | @@ -1199,10 +1195,6 @@ test('exposes in-band metadata events as cues', function() { | ... | @@ -1199,10 +1195,6 @@ test('exposes in-band metadata events as cues', function() { |
1199 | openMediaSource(player); | 1195 | openMediaSource(player); |
1200 | 1196 | ||
1201 | player.hls.segmentParser_.parseSegmentBinaryData = function() { | 1197 | 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 | 1198 | // trigger a metadata event |
1207 | player.hls.segmentParser_.metadataStream.trigger('data', { | 1199 | player.hls.segmentParser_.metadataStream.trigger('data', { |
1208 | pts: 2000, | 1200 | pts: 2000, |
... | @@ -1251,23 +1243,14 @@ test('exposes in-band metadata events as cues', function() { | ... | @@ -1251,23 +1243,14 @@ test('exposes in-band metadata events as cues', function() { |
1251 | 1243 | ||
1252 | test('only adds in-band cues the first time they are encountered', function() { | 1244 | test('only adds in-band cues the first time they are encountered', function() { |
1253 | var tags = [{ pts: 0, bytes: new Uint8Array(1) }], track; | 1245 | var tags = [{ pts: 0, bytes: new Uint8Array(1) }], track; |
1246 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1254 | player.src({ | 1247 | player.src({ |
1255 | src: 'manifest/media.m3u8', | 1248 | src: 'manifest/media.m3u8', |
1256 | type: 'application/vnd.apple.mpegurl' | 1249 | type: 'application/vnd.apple.mpegurl' |
1257 | }); | 1250 | }); |
1258 | openMediaSource(player); | 1251 | openMediaSource(player); |
1259 | 1252 | ||
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() { | 1253 | 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 | 1254 | // trigger a metadata event |
1272 | player.hls.segmentParser_.metadataStream.trigger('data', { | 1255 | player.hls.segmentParser_.metadataStream.trigger('data', { |
1273 | pts: 2000, | 1256 | pts: 2000, |
... | @@ -1295,23 +1278,14 @@ test('clears in-band cues ahead of current time on seek', function() { | ... | @@ -1295,23 +1278,14 @@ test('clears in-band cues ahead of current time on seek', function() { |
1295 | tags = [], | 1278 | tags = [], |
1296 | events = [], | 1279 | events = [], |
1297 | track; | 1280 | track; |
1281 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1298 | player.src({ | 1282 | player.src({ |
1299 | src: 'manifest/media.m3u8', | 1283 | src: 'manifest/media.m3u8', |
1300 | type: 'application/vnd.apple.mpegurl' | 1284 | type: 'application/vnd.apple.mpegurl' |
1301 | }); | 1285 | }); |
1302 | openMediaSource(player); | 1286 | openMediaSource(player); |
1303 | 1287 | ||
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() { | 1288 | 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 | 1289 | // trigger a metadata event |
1316 | if (events.length) { | 1290 | if (events.length) { |
1317 | player.hls.segmentParser_.metadataStream.trigger('data', events.shift()); | 1291 | player.hls.segmentParser_.metadataStream.trigger('data', events.shift()); |
... | @@ -1360,26 +1334,17 @@ test('clears in-band cues ahead of current time on seek', function() { | ... | @@ -1360,26 +1334,17 @@ test('clears in-band cues ahead of current time on seek', function() { |
1360 | 1334 | ||
1361 | test('translates ID3 PTS values to cue media timeline positions', function() { | 1335 | test('translates ID3 PTS values to cue media timeline positions', function() { |
1362 | var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track; | 1336 | var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track; |
1337 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1363 | player.src({ | 1338 | player.src({ |
1364 | src: 'manifest/media.m3u8', | 1339 | src: 'manifest/media.m3u8', |
1365 | type: 'application/vnd.apple.mpegurl' | 1340 | type: 'application/vnd.apple.mpegurl' |
1366 | }); | 1341 | }); |
1367 | openMediaSource(player); | 1342 | openMediaSource(player); |
1368 | 1343 | ||
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() { | 1344 | player.hls.segmentParser_.parseSegmentBinaryData = function() { |
1376 | // setup the timestamp offset | 1345 | // setup the timestamp offset |
1377 | this.timestampOffset = tags[0].pts; | 1346 | this.timestampOffset = tags[0].pts; |
1378 | 1347 | ||
1379 | // fake out a descriptor | ||
1380 | player.hls.segmentParser_.metadataStream.descriptor = new Uint8Array([ | ||
1381 | 1, 2, 3, 0xbb | ||
1382 | ]); | ||
1383 | // trigger a metadata event | 1348 | // trigger a metadata event |
1384 | player.hls.segmentParser_.metadataStream.trigger('data', { | 1349 | player.hls.segmentParser_.metadataStream.trigger('data', { |
1385 | pts: 5 * 1000, | 1350 | pts: 5 * 1000, |
... | @@ -1400,26 +1365,17 @@ test('translates ID3 PTS values to cue media timeline positions', function() { | ... | @@ -1400,26 +1365,17 @@ test('translates ID3 PTS values to cue media timeline positions', function() { |
1400 | 1365 | ||
1401 | test('translates ID3 PTS values across discontinuities', function() { | 1366 | test('translates ID3 PTS values across discontinuities', function() { |
1402 | var tags = [], events = [], track; | 1367 | var tags = [], events = [], track; |
1368 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1403 | player.src({ | 1369 | player.src({ |
1404 | src: 'cues-and-discontinuities.m3u8', | 1370 | src: 'cues-and-discontinuities.m3u8', |
1405 | type: 'application/vnd.apple.mpegurl' | 1371 | type: 'application/vnd.apple.mpegurl' |
1406 | }); | 1372 | }); |
1407 | openMediaSource(player); | 1373 | openMediaSource(player); |
1408 | 1374 | ||
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() { | 1375 | player.hls.segmentParser_.parseSegmentBinaryData = function() { |
1416 | if (this.timestampOffset === null) { | 1376 | if (this.timestampOffset === null) { |
1417 | this.timestampOffset = tags[0].pts; | 1377 | this.timestampOffset = tags[0].pts; |
1418 | } | 1378 | } |
1419 | // fake out a descriptor | ||
1420 | player.hls.segmentParser_.metadataStream.descriptor = new Uint8Array([ | ||
1421 | 1, 2, 3, 0xbb | ||
1422 | ]); | ||
1423 | // trigger a metadata event | 1379 | // trigger a metadata event |
1424 | if (events.length) { | 1380 | if (events.length) { |
1425 | player.hls.segmentParser_.metadataStream.trigger('data', events.shift()); | 1381 | player.hls.segmentParser_.metadataStream.trigger('data', events.shift()); |
... | @@ -1437,7 +1393,9 @@ test('translates ID3 PTS values across discontinuities', function() { | ... | @@ -1437,7 +1393,9 @@ test('translates ID3 PTS values across discontinuities', function() { |
1437 | '1.ts\n'); | 1393 | '1.ts\n'); |
1438 | 1394 | ||
1439 | // segment 0 starts at PTS 14000 and has a cue point at 15000 | 1395 | // segment 0 starts at PTS 14000 and has a cue point at 15000 |
1440 | tags.push({ pts: 14 * 1000, bytes: new Uint8Array(1) }); | 1396 | player.hls.segmentParser_.timestampOffset = 14 * 1000; |
1397 | tags.push({ pts: 14 * 1000, bytes: new Uint8Array(1) }, | ||
1398 | { pts: 24 * 1000, bytes: new Uint8Array(1) }); | ||
1441 | events.push({ | 1399 | events.push({ |
1442 | pts: 15 * 1000, | 1400 | pts: 15 * 1000, |
1443 | data: new Uint8Array([]), | 1401 | data: new Uint8Array([]), |
... | @@ -1449,14 +1407,14 @@ test('translates ID3 PTS values across discontinuities', function() { | ... | @@ -1449,14 +1407,14 @@ test('translates ID3 PTS values across discontinuities', function() { |
1449 | standardXHRResponse(requests.shift()); // segment 0 | 1407 | standardXHRResponse(requests.shift()); // segment 0 |
1450 | 1408 | ||
1451 | // segment 1 is after a discontinuity, starts at PTS 22000 | 1409 | // segment 1 is after a discontinuity, starts at PTS 22000 |
1452 | // and has a cue point at 15000 | 1410 | // and has a cue point at 23000 |
1453 | tags.push({ pts: 22 * 1000, bytes: new Uint8Array(1) }); | 1411 | tags.push({ pts: 22 * 1000, bytes: new Uint8Array(1) }); |
1454 | events.push({ | 1412 | events.push({ |
1455 | pts: 23 * 1000, | 1413 | pts: 23 * 1000, |
1456 | data: new Uint8Array([]), | 1414 | data: new Uint8Array([]), |
1457 | frames: [{ | 1415 | frames: [{ |
1458 | id: 'TXXX', | 1416 | id: 'TXXX', |
1459 | value: 'cue 0' | 1417 | value: 'cue 1' |
1460 | }] | 1418 | }] |
1461 | }); | 1419 | }); |
1462 | player.hls.checkBuffer_(); | 1420 | player.hls.checkBuffer_(); | ... | ... |
-
Please register or sign in to post a comment