Fix seeks between segments. Improve duration calculation. Closes #339
Showing
49 changed files
with
621 additions
and
192 deletions
... | @@ -2,7 +2,7 @@ CHANGELOG | ... | @@ -2,7 +2,7 @@ CHANGELOG |
2 | ========= | 2 | ========= |
3 | 3 | ||
4 | ## HEAD (Unreleased) | 4 | ## HEAD (Unreleased) |
5 | _(none)_ | 5 | * Fix seeks between segments. Improve duration calculation. ([view](https://github.com/videojs/videojs-contrib-hls/pull/339)) |
6 | 6 | ||
7 | -------------------- | 7 | -------------------- |
8 | 8 | ... | ... |
... | @@ -375,7 +375,8 @@ | ... | @@ -375,7 +375,8 @@ |
375 | 375 | ||
376 | // the manifest is empty until the parse stream begins delivering data | 376 | // the manifest is empty until the parse stream begins delivering data |
377 | this.manifest = { | 377 | this.manifest = { |
378 | allowCache: true | 378 | allowCache: true, |
379 | discontinuityStarts: [] | ||
379 | }; | 380 | }; |
380 | 381 | ||
381 | // update the manifest with the m3u8 entry from the parse stream | 382 | // update the manifest with the m3u8 entry from the parse stream |
... | @@ -513,6 +514,7 @@ | ... | @@ -513,6 +514,7 @@ |
513 | }, | 514 | }, |
514 | 'discontinuity': function() { | 515 | 'discontinuity': function() { |
515 | currentUri.discontinuity = true; | 516 | currentUri.discontinuity = true; |
517 | this.manifest.discontinuityStarts.push(uris.length); | ||
516 | }, | 518 | }, |
517 | 'targetduration': function() { | 519 | 'targetduration': function() { |
518 | if (!isFinite(entry.duration) || entry.duration < 0) { | 520 | if (!isFinite(entry.duration) || entry.duration < 0) { | ... | ... |
... | @@ -431,11 +431,12 @@ | ... | @@ -431,11 +431,12 @@ |
431 | for (i = 0; i < this.media_.segments.length; i++) { | 431 | for (i = 0; i < this.media_.segments.length; i++) { |
432 | time -= Playlist.duration(this.media_, | 432 | time -= Playlist.duration(this.media_, |
433 | this.media_.mediaSequence + i, | 433 | this.media_.mediaSequence + i, |
434 | this.media_.mediaSequence + i + 1); | 434 | this.media_.mediaSequence + i + 1, |
435 | false); | ||
435 | 436 | ||
436 | // HLS version 3 and lower round segment durations to the | 437 | // HLS version 3 and lower round segment durations to the |
437 | // nearest decimal integer. When the correct media index is | 438 | // nearest decimal integer. When the correct media index is |
438 | // ambiguous, prefer the lower one. | 439 | // ambiguous, prefer the higher one. |
439 | if (time <= 0) { | 440 | if (time <= 0) { |
440 | return i; | 441 | return i; |
441 | } | 442 | } | ... | ... |
... | @@ -5,7 +5,130 @@ | ... | @@ -5,7 +5,130 @@ |
5 | 'use strict'; | 5 | 'use strict'; |
6 | 6 | ||
7 | var DEFAULT_TARGET_DURATION = 10; | 7 | var DEFAULT_TARGET_DURATION = 10; |
8 | var duration, seekable, segmentsDuration; | 8 | var accumulateDuration, ascendingNumeric, duration, intervalDuration, rangeDuration, seekable; |
9 | |||
10 | // Array.sort comparator to sort numbers in ascending order | ||
11 | ascendingNumeric = function(left, right) { | ||
12 | return left - right; | ||
13 | }; | ||
14 | |||
15 | /** | ||
16 | * Returns the media duration for the segments between a start and | ||
17 | * exclusive end index. The start and end parameters are interpreted | ||
18 | * as indices into the currently available segments. This method | ||
19 | * does not calculate durations for segments that have expired. | ||
20 | * @param playlist {object} a media playlist object | ||
21 | * @param start {number} an inclusive lower boundary for the | ||
22 | * segments to examine. | ||
23 | * @param end {number} an exclusive upper boundary for the segments | ||
24 | * to examine. | ||
25 | * @param includeTrailingTime {boolean} if false, the interval between | ||
26 | * the final segment and the subsequent segment will not be included | ||
27 | * in the result | ||
28 | * @return {number} the duration between the start index and end | ||
29 | * index in seconds. | ||
30 | */ | ||
31 | accumulateDuration = function(playlist, start, end, includeTrailingTime) { | ||
32 | var | ||
33 | ranges = [], | ||
34 | rangeEnds = (playlist.discontinuityStarts || []).concat(end), | ||
35 | result = 0, | ||
36 | i; | ||
37 | |||
38 | // short circuit if start and end don't specify a non-empty range | ||
39 | // of segments | ||
40 | if (start >= end) { | ||
41 | return 0; | ||
42 | } | ||
43 | |||
44 | // create a range object for each discontinuity sequence | ||
45 | rangeEnds.sort(ascendingNumeric); | ||
46 | for (i = 0; i < rangeEnds.length; i++) { | ||
47 | if (rangeEnds[i] > start) { | ||
48 | ranges.push({ start: start, end: rangeEnds[i] }); | ||
49 | i++; | ||
50 | break; | ||
51 | } | ||
52 | } | ||
53 | for (; i < rangeEnds.length; i++) { | ||
54 | // ignore times ranges later than end | ||
55 | if (rangeEnds[i] >= end) { | ||
56 | ranges.push({ start: rangeEnds[i - 1], end: end }); | ||
57 | break; | ||
58 | } | ||
59 | ranges.push({ start: ranges[ranges.length - 1].end, end: rangeEnds[i] }); | ||
60 | } | ||
61 | |||
62 | // add up the durations for each of the ranges | ||
63 | for (i = 0; i < ranges.length; i++) { | ||
64 | result += rangeDuration(playlist, | ||
65 | ranges[i], | ||
66 | i === ranges.length - 1 && includeTrailingTime); | ||
67 | } | ||
68 | |||
69 | return result; | ||
70 | }; | ||
71 | |||
72 | /** | ||
73 | * Returns the duration of the specified range of segments. The | ||
74 | * range *must not* cross a discontinuity. | ||
75 | * @param playlist {object} a media playlist object | ||
76 | * @param range {object} an object that specifies a starting and | ||
77 | * ending index into the available segments. | ||
78 | * @param includeTrailingTime {boolean} if false, the interval between | ||
79 | * the final segment and the subsequent segment will not be included | ||
80 | * in the result | ||
81 | * @return {number} the duration of the range in seconds. | ||
82 | */ | ||
83 | rangeDuration = function(playlist, range, includeTrailingTime) { | ||
84 | var | ||
85 | result = 0, | ||
86 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION, | ||
87 | segment, | ||
88 | left, right; | ||
89 | |||
90 | // accumulate while searching for the earliest segment with | ||
91 | // available PTS information | ||
92 | for (left = range.start; left < range.end; left++) { | ||
93 | segment = playlist.segments[left]; | ||
94 | if (segment.minVideoPts !== undefined) { | ||
95 | break; | ||
96 | } | ||
97 | result += segment.duration || targetDuration; | ||
98 | } | ||
99 | |||
100 | // see if there's enough information to include the trailing time | ||
101 | if (includeTrailingTime) { | ||
102 | segment = playlist.segments[range.end]; | ||
103 | if (segment && segment.minVideoPts !== undefined) { | ||
104 | result += 0.001 * | ||
105 | (Math.min(segment.minVideoPts, segment.minAudioPts) - | ||
106 | Math.min(playlist.segments[left].minVideoPts, | ||
107 | playlist.segments[left].minAudioPts)); | ||
108 | return result; | ||
109 | } | ||
110 | } | ||
111 | |||
112 | // do the same thing while finding the latest segment | ||
113 | for (right = range.end - 1; right >= left; right--) { | ||
114 | segment = playlist.segments[right]; | ||
115 | if (segment.maxVideoPts !== undefined) { | ||
116 | break; | ||
117 | } | ||
118 | result += segment.duration || targetDuration; | ||
119 | } | ||
120 | |||
121 | // add in the PTS interval in seconds between them | ||
122 | if (right >= left) { | ||
123 | result += 0.001 * | ||
124 | (Math.max(playlist.segments[right].maxVideoPts, | ||
125 | playlist.segments[right].maxAudioPts) - | ||
126 | Math.min(playlist.segments[left].minVideoPts, | ||
127 | playlist.segments[left].minAudioPts)); | ||
128 | } | ||
129 | |||
130 | return result; | ||
131 | }; | ||
9 | 132 | ||
10 | /** | 133 | /** |
11 | * Calculate the media duration from the segments associated with a | 134 | * Calculate the media duration from the segments associated with a |
... | @@ -17,47 +140,28 @@ | ... | @@ -17,47 +140,28 @@ |
17 | * boundary for the playlist. Defaults to 0. | 140 | * boundary for the playlist. Defaults to 0. |
18 | * @param endSequence {number} (optional) an exclusive upper boundary | 141 | * @param endSequence {number} (optional) an exclusive upper boundary |
19 | * for the playlist. Defaults to playlist length. | 142 | * for the playlist. Defaults to playlist length. |
143 | * @param includeTrailingTime {boolean} if false, the interval between | ||
144 | * the final segment and the subsequent segment will not be included | ||
145 | * in the result | ||
20 | * @return {number} the duration between the start index and end | 146 | * @return {number} the duration between the start index and end |
21 | * index. | 147 | * index. |
22 | */ | 148 | */ |
23 | segmentsDuration = function(playlist, startSequence, endSequence) { | 149 | intervalDuration = function(playlist, startSequence, endSequence, includeTrailingTime) { |
24 | var targetDuration, i, j, segment, endSegment, expiredSegmentCount, result = 0; | 150 | var result = 0, targetDuration, expiredSegmentCount; |
25 | 151 | ||
26 | startSequence = startSequence || 0; | 152 | startSequence = startSequence || 0; |
27 | i = startSequence; | ||
28 | endSequence = endSequence !== undefined ? endSequence : (playlist.segments || []).length; | 153 | endSequence = endSequence !== undefined ? endSequence : (playlist.segments || []).length; |
29 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; | 154 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; |
30 | 155 | ||
31 | // estimate expired segment duration using the target duration | 156 | // estimate expired segment duration using the target duration |
32 | expiredSegmentCount = Math.max(playlist.mediaSequence - startSequence, 0); | 157 | expiredSegmentCount = Math.max(playlist.mediaSequence - startSequence, 0); |
33 | result += expiredSegmentCount * targetDuration; | 158 | result += expiredSegmentCount * targetDuration; |
34 | i += expiredSegmentCount; | ||
35 | 159 | ||
36 | // accumulate the segment durations into the result | 160 | // accumulate the segment durations into the result |
37 | for (; i < endSequence; i++) { | 161 | result += accumulateDuration(playlist, |
38 | segment = playlist.segments[i - playlist.mediaSequence]; | 162 | startSequence + expiredSegmentCount - playlist.mediaSequence, |
39 | 163 | endSequence - playlist.mediaSequence, | |
40 | // when PTS values aren't available, use information from the playlist | 164 | includeTrailingTime); |
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; | ||
60 | } | ||
61 | 165 | ||
62 | return result; | 166 | return result; |
63 | }; | 167 | }; |
... | @@ -72,14 +176,21 @@ | ... | @@ -72,14 +176,21 @@ |
72 | * boundary for the playlist. Defaults to 0. | 176 | * boundary for the playlist. Defaults to 0. |
73 | * @param endSequence {number} (optional) an exclusive upper boundary | 177 | * @param endSequence {number} (optional) an exclusive upper boundary |
74 | * for the playlist. Defaults to playlist length. | 178 | * for the playlist. Defaults to playlist length. |
179 | * @param includeTrailingTime {boolean} (optional) if false, the interval between | ||
180 | * the final segment and the subsequent segment will not be included | ||
181 | * in the result | ||
75 | * @return {number} the duration between the start index and end | 182 | * @return {number} the duration between the start index and end |
76 | * index. | 183 | * index. |
77 | */ | 184 | */ |
78 | duration = function(playlist, startSequence, endSequence) { | 185 | duration = function(playlist, startSequence, endSequence, includeTrailingTime) { |
79 | if (!playlist) { | 186 | if (!playlist) { |
80 | return 0; | 187 | return 0; |
81 | } | 188 | } |
82 | 189 | ||
190 | if (includeTrailingTime === undefined) { | ||
191 | includeTrailingTime = true; | ||
192 | } | ||
193 | |||
83 | // if a slice of the total duration is not requested, use | 194 | // if a slice of the total duration is not requested, use |
84 | // playlist-level duration indicators when they're present | 195 | // playlist-level duration indicators when they're present |
85 | if (startSequence === undefined && endSequence === undefined) { | 196 | if (startSequence === undefined && endSequence === undefined) { |
... | @@ -95,9 +206,10 @@ | ... | @@ -95,9 +206,10 @@ |
95 | } | 206 | } |
96 | 207 | ||
97 | // calculate the total duration based on the segment durations | 208 | // calculate the total duration based on the segment durations |
98 | return segmentsDuration(playlist, | 209 | return intervalDuration(playlist, |
99 | startSequence, | 210 | startSequence, |
100 | endSequence); | 211 | endSequence, |
212 | includeTrailingTime); | ||
101 | }; | 213 | }; |
102 | 214 | ||
103 | /** | 215 | /** |
... | @@ -119,8 +231,8 @@ | ... | @@ -119,8 +231,8 @@ |
119 | return videojs.createTimeRange(0, duration(playlist)); | 231 | return videojs.createTimeRange(0, duration(playlist)); |
120 | } | 232 | } |
121 | 233 | ||
122 | start = segmentsDuration(playlist, 0, playlist.mediaSequence); | 234 | start = intervalDuration(playlist, 0, playlist.mediaSequence); |
123 | end = start + segmentsDuration(playlist, | 235 | end = start + intervalDuration(playlist, |
124 | playlist.mediaSequence, | 236 | playlist.mediaSequence, |
125 | playlist.mediaSequence + playlist.segments.length); | 237 | playlist.mediaSequence + playlist.segments.length); |
126 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; | 238 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; | ... | ... |
... | @@ -31,15 +31,6 @@ | ... | @@ -31,15 +31,6 @@ |
31 | // allow in-band metadata to be observed | 31 | // allow in-band metadata to be observed |
32 | self.metadataStream = new MetadataStream(); | 32 | self.metadataStream = new MetadataStream(); |
33 | 33 | ||
34 | this.mediaTimelineOffset = null; | ||
35 | |||
36 | // The first timestamp value encountered during parsing. This | ||
37 | // value can be used to determine the relative timing between | ||
38 | // frames and the start of the current timestamp sequence. It | ||
39 | // should be reset to null before parsing a segment with | ||
40 | // discontinuous timestamp values from previous segments. | ||
41 | self.timestampOffset = null; | ||
42 | |||
43 | // For information on the FLV format, see | 34 | // For information on the FLV format, see |
44 | // http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf. | 35 | // http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf. |
45 | // Technically, this function returns the header and a metadata FLV tag | 36 | // Technically, this function returns the header and a metadata FLV tag |
... | @@ -360,13 +351,6 @@ | ... | @@ -360,13 +351,6 @@ |
360 | // Skip past "optional" portion of PTS header | 351 | // Skip past "optional" portion of PTS header |
361 | offset += pesHeaderLength; | 352 | offset += pesHeaderLength; |
362 | 353 | ||
363 | // keep track of the earliest encounted PTS value so | ||
364 | // external parties can align timestamps across | ||
365 | // discontinuities | ||
366 | if (self.timestampOffset === null) { | ||
367 | self.timestampOffset = pts; | ||
368 | } | ||
369 | |||
370 | if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) { | 354 | if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) { |
371 | h264Stream.setNextTimeStamp(pts, | 355 | h264Stream.setNextTimeStamp(pts, |
372 | dts, | 356 | dts, | ... | ... |
... | @@ -260,7 +260,7 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { | ... | @@ -260,7 +260,7 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { |
260 | // add a metadata cue whenever a metadata event is triggered during | 260 | // add a metadata cue whenever a metadata event is triggered during |
261 | // segment parsing | 261 | // segment parsing |
262 | metadataStream.on('data', function(metadata) { | 262 | metadataStream.on('data', function(metadata) { |
263 | var i, cue, frame, time, media, segmentOffset, hexDigit; | 263 | var i, hexDigit; |
264 | 264 | ||
265 | // create the metadata track if this is the first ID3 tag we've | 265 | // create the metadata track if this is the first ID3 tag we've |
266 | // seen | 266 | // seen |
... | @@ -276,19 +276,11 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { | ... | @@ -276,19 +276,11 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { |
276 | } | 276 | } |
277 | } | 277 | } |
278 | 278 | ||
279 | // calculate the start time for the segment that is currently being parsed | 279 | // store this event for processing once the muxing has finished |
280 | media = tech.playlists.media(); | 280 | tech.segmentBuffer_[0].pendingMetadata.push({ |
281 | segmentOffset = tech.playlists.expiredPreDiscontinuity_ + tech.playlists.expiredPostDiscontinuity_; | 281 | textTrack: textTrack, |
282 | segmentOffset += videojs.Hls.Playlist.duration(media, media.mediaSequence, media.mediaSequence + tech.mediaIndex); | 282 | metadata: metadata |
283 | 283 | }); | |
284 | // create cue points for all the ID3 frames in this metadata event | ||
285 | for (i = 0; i < metadata.frames.length; i++) { | ||
286 | frame = metadata.frames[i]; | ||
287 | time = tech.segmentParser_.mediaTimelineOffset + ((metadata.pts - tech.segmentParser_.timestampOffset) * 0.001); | ||
288 | cue = new window.VTTCue(time, time, frame.value || frame.url || ''); | ||
289 | cue.frame = frame; | ||
290 | textTrack.addCue(cue); | ||
291 | } | ||
292 | }); | 284 | }); |
293 | 285 | ||
294 | // when seeking, clear out all cues ahead of the earliest position | 286 | // when seeking, clear out all cues ahead of the earliest position |
... | @@ -312,6 +304,30 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { | ... | @@ -312,6 +304,30 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { |
312 | }); | 304 | }); |
313 | }; | 305 | }; |
314 | 306 | ||
307 | videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) { | ||
308 | var i, cue, frame, metadata, minPts, segment, segmentOffset, textTrack, time; | ||
309 | segmentOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist, | ||
310 | segmentInfo.playlist.mediaSequence, | ||
311 | segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex); | ||
312 | segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex]; | ||
313 | minPts = Math.min(segment.minVideoPts, segment.minAudioPts); | ||
314 | |||
315 | while (segmentInfo.pendingMetadata.length) { | ||
316 | metadata = segmentInfo.pendingMetadata[0].metadata; | ||
317 | textTrack = segmentInfo.pendingMetadata[0].textTrack; | ||
318 | |||
319 | // create cue points for all the ID3 frames in this metadata event | ||
320 | for (i = 0; i < metadata.frames.length; i++) { | ||
321 | frame = metadata.frames[i]; | ||
322 | time = segmentOffset + ((metadata.pts - minPts) * 0.001); | ||
323 | cue = new window.VTTCue(time, time, frame.value || frame.url || ''); | ||
324 | cue.frame = frame; | ||
325 | textTrack.addCue(cue); | ||
326 | } | ||
327 | segmentInfo.pendingMetadata.shift(); | ||
328 | } | ||
329 | }; | ||
330 | |||
315 | /** | 331 | /** |
316 | * Reset the mediaIndex if play() is called after the video has | 332 | * Reset the mediaIndex if play() is called after the video has |
317 | * ended. | 333 | * ended. |
... | @@ -780,7 +796,10 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { | ... | @@ -780,7 +796,10 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { |
780 | // when a key is defined for this segment, the encrypted bytes | 796 | // when a key is defined for this segment, the encrypted bytes |
781 | encryptedBytes: null, | 797 | encryptedBytes: null, |
782 | // optionally, the decrypter that is unencrypting the segment | 798 | // optionally, the decrypter that is unencrypting the segment |
783 | decrypter: null | 799 | decrypter: null, |
800 | // metadata events discovered during muxing that need to be | ||
801 | // translated into cue points | ||
802 | pendingMetadata: [] | ||
784 | }; | 803 | }; |
785 | if (segmentInfo.playlist.segments[segmentInfo.mediaIndex].key) { | 804 | if (segmentInfo.playlist.segments[segmentInfo.mediaIndex].key) { |
786 | segmentInfo.encryptedBytes = new Uint8Array(this.response); | 805 | segmentInfo.encryptedBytes = new Uint8Array(this.response); |
... | @@ -870,20 +889,6 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -870,20 +889,6 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
870 | } | 889 | } |
871 | 890 | ||
872 | event = event || {}; | 891 | event = event || {}; |
873 | segmentOffset = this.playlists.expiredPreDiscontinuity_; | ||
874 | segmentOffset += this.playlists.expiredPostDiscontinuity_; | ||
875 | segmentOffset += videojs.Hls.Playlist.duration(playlist, playlist.mediaSequence, playlist.mediaSequence + mediaIndex); | ||
876 | segmentOffset *= 1000; | ||
877 | |||
878 | // if this segment starts is the start of a new discontinuity | ||
879 | // sequence, the segment parser's timestamp offset must be | ||
880 | // re-calculated | ||
881 | if (segment.discontinuity) { | ||
882 | this.segmentParser_.mediaTimelineOffset = segmentOffset * 0.001; | ||
883 | this.segmentParser_.timestampOffset = null; | ||
884 | } else if (this.segmentParser_.mediaTimelineOffset === null) { | ||
885 | this.segmentParser_.mediaTimelineOffset = segmentOffset * 0.001; | ||
886 | } | ||
887 | 892 | ||
888 | // transmux the segment data from MP2T to FLV | 893 | // transmux the segment data from MP2T to FLV |
889 | this.segmentParser_.parseSegmentBinaryData(bytes); | 894 | this.segmentParser_.parseSegmentBinaryData(bytes); |
... | @@ -904,20 +909,27 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -904,20 +909,27 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
904 | tags.push(this.segmentParser_.getNextTag()); | 909 | tags.push(this.segmentParser_.getNextTag()); |
905 | } | 910 | } |
906 | 911 | ||
912 | this.addCuesForMetadata_(segmentInfo); | ||
907 | this.updateDuration(this.playlists.media()); | 913 | this.updateDuration(this.playlists.media()); |
908 | 914 | ||
909 | // if we're refilling the buffer after a seek, scan through the muxed | 915 | // if we're refilling the buffer after a seek, scan through the muxed |
910 | // FLV tags until we find the one that is closest to the desired | 916 | // FLV tags until we find the one that is closest to the desired |
911 | // playback time | 917 | // playback time |
912 | if (typeof offset === 'number') { | 918 | if (typeof offset === 'number') { |
913 | ptsTime = offset - segmentOffset + tags[0].pts; | 919 | // determine the offset within this segment we're seeking to |
914 | 920 | segmentOffset = this.playlists.expiredPostDiscontinuity_ + this.playlists.expiredPreDiscontinuity_; | |
915 | while (tags[i].pts < ptsTime) { | 921 | segmentOffset += videojs.Hls.Playlist.duration(playlist, |
922 | playlist.mediaSequence, | ||
923 | playlist.mediaSequence + mediaIndex); | ||
924 | segmentOffset = offset - (segmentOffset * 1000); | ||
925 | ptsTime = segmentOffset + tags[0].pts; | ||
926 | |||
927 | while (tags[i + 1] && tags[i].pts < ptsTime) { | ||
916 | i++; | 928 | i++; |
917 | } | 929 | } |
918 | 930 | ||
919 | // tell the SWF where we will be seeking to | 931 | // tell the SWF the media position of the first tag we'll be delivering |
920 | this.el().vjs_setProperty('currentTime', (tags[i].pts - tags[0].pts + segmentOffset) * 0.001); | 932 | this.el().vjs_setProperty('currentTime', ((tags[i].pts - ptsTime + offset) * 0.001)); |
921 | 933 | ||
922 | tags = tags.slice(i); | 934 | tags = tags.slice(i); |
923 | 935 | ||
... | @@ -1139,29 +1151,6 @@ videojs.Hls.getMediaIndexByTime = function() { | ... | @@ -1139,29 +1151,6 @@ videojs.Hls.getMediaIndexByTime = function() { |
1139 | }; | 1151 | }; |
1140 | 1152 | ||
1141 | /** | 1153 | /** |
1142 | * Determine the current time in seconds in one playlist by a media index. This | ||
1143 | * function iterates through the segments of a playlist up to the specified index | ||
1144 | * and then returns the time up to that point. | ||
1145 | * | ||
1146 | * @param playlist {object} The playlist of the segments being searched. | ||
1147 | * @param mediaIndex {number} The index of the target segment in the playlist. | ||
1148 | * @returns {number} The current time to that point, or 0 if none appropriate. | ||
1149 | */ | ||
1150 | videojs.Hls.prototype.getCurrentTimeByMediaIndex_ = function(playlist, mediaIndex) { | ||
1151 | var index, time = 0; | ||
1152 | |||
1153 | if (!playlist.segments || mediaIndex === 0) { | ||
1154 | return 0; | ||
1155 | } | ||
1156 | |||
1157 | for (index = 0; index < mediaIndex; index++) { | ||
1158 | time += playlist.segments[index].duration; | ||
1159 | } | ||
1160 | |||
1161 | return time; | ||
1162 | }; | ||
1163 | |||
1164 | /** | ||
1165 | * A comparator function to sort two playlist object by bandwidth. | 1154 | * A comparator function to sort two playlist object by bandwidth. |
1166 | * @param left {object} a media playlist object | 1155 | * @param left {object} a media playlist object |
1167 | * @param right {object} a media playlist object | 1156 | * @param right {object} a media playlist object | ... | ... |
... | @@ -22,5 +22,6 @@ | ... | @@ -22,5 +22,6 @@ |
22 | ], | 22 | ], |
23 | "targetDuration": 10, | 23 | "targetDuration": 10, |
24 | "endList": true, | 24 | "endList": true, |
25 | "discontinuitySequence": 0 | 25 | "discontinuitySequence": 0, |
26 | "discontinuityStarts": [] | ||
26 | } | 27 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -142,5 +142,6 @@ | ... | @@ -142,5 +142,6 @@ |
142 | ], | 142 | ], |
143 | "targetDuration": 10, | 143 | "targetDuration": 10, |
144 | "endList": true, | 144 | "endList": true, |
145 | "discontinuitySequence": 0 | 145 | "discontinuitySequence": 0, |
146 | "discontinuityStarts": [] | ||
146 | } | 147 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -14,5 +14,6 @@ | ... | @@ -14,5 +14,6 @@ |
14 | ], | 14 | ], |
15 | "targetDuration": 10, | 15 | "targetDuration": 10, |
16 | "endList": true, | 16 | "endList": true, |
17 | "discontinuitySequence": 0 | 17 | "discontinuitySequence": 0, |
18 | "discontinuityStarts": [] | ||
18 | } | 19 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -41,5 +41,6 @@ | ... | @@ -41,5 +41,6 @@ |
41 | }, | 41 | }, |
42 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" | 42 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" |
43 | } | 43 | } |
44 | ] | 44 | ], |
45 | "discontinuityStarts": [] | ||
45 | } | 46 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -138,5 +138,6 @@ | ... | @@ -138,5 +138,6 @@ |
138 | ], | 138 | ], |
139 | "targetDuration": 10, | 139 | "targetDuration": 10, |
140 | "endList": true, | 140 | "endList": true, |
141 | "discontinuitySequence": 0 | 141 | "discontinuitySequence": 0, |
142 | "discontinuityStarts": [] | ||
142 | } | 143 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -14,5 +14,6 @@ | ... | @@ -14,5 +14,6 @@ |
14 | ], | 14 | ], |
15 | "targetDuration": 10, | 15 | "targetDuration": 10, |
16 | "endList": true, | 16 | "endList": true, |
17 | "discontinuitySequence": 0 | 17 | "discontinuitySequence": 0, |
18 | "discontinuityStarts": [] | ||
18 | } | 19 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -22,5 +22,6 @@ | ... | @@ -22,5 +22,6 @@ |
22 | ], | 22 | ], |
23 | "targetDuration": 10, | 23 | "targetDuration": 10, |
24 | "endList": true, | 24 | "endList": true, |
25 | "discontinuitySequence": 0 | 25 | "discontinuitySequence": 0, |
26 | "discontinuityStarts": [] | ||
26 | } | 27 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -14,5 +14,6 @@ | ... | @@ -14,5 +14,6 @@ |
14 | ], | 14 | ], |
15 | "targetDuration": 10, | 15 | "targetDuration": 10, |
16 | "endList": true, | 16 | "endList": true, |
17 | "discontinuitySequence": 0 | 17 | "discontinuitySequence": 0, |
18 | "discontinuityStarts": [] | ||
18 | } | 19 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -22,5 +22,6 @@ | ... | @@ -22,5 +22,6 @@ |
22 | ], | 22 | ], |
23 | "targetDuration": 8, | 23 | "targetDuration": 8, |
24 | "endList": true, | 24 | "endList": true, |
25 | "discontinuitySequence": 0 | 25 | "discontinuitySequence": 0, |
26 | "discontinuityStarts": [] | ||
26 | } | 27 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -29,5 +29,6 @@ | ... | @@ -29,5 +29,6 @@ |
29 | ], | 29 | ], |
30 | "targetDuration": 10, | 30 | "targetDuration": 10, |
31 | "endList": true, | 31 | "endList": true, |
32 | "discontinuitySequence": 0 | 32 | "discontinuitySequence": 0, |
33 | "discontinuityStarts": [] | ||
33 | } | 34 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -41,5 +41,6 @@ | ... | @@ -41,5 +41,6 @@ |
41 | }, | 41 | }, |
42 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" | 42 | "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" |
43 | } | 43 | } |
44 | ] | 44 | ], |
45 | "discontinuityStarts": [] | ||
45 | } | 46 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -2,6 +2,7 @@ | ... | @@ -2,6 +2,7 @@ |
2 | "allowCache": true, | 2 | "allowCache": true, |
3 | "mediaSequence": 7794, | 3 | "mediaSequence": 7794, |
4 | "discontinuitySequence": 0, | 4 | "discontinuitySequence": 0, |
5 | "discontinuityStarts": [], | ||
5 | "segments": [ | 6 | "segments": [ |
6 | { | 7 | { |
7 | "duration": 2.833, | 8 | "duration": 2.833, | ... | ... |
... | @@ -30,5 +30,6 @@ | ... | @@ -30,5 +30,6 @@ |
30 | ], | 30 | ], |
31 | "targetDuration": 10, | 31 | "targetDuration": 10, |
32 | "endList": true, | 32 | "endList": true, |
33 | "discontinuitySequence": 0 | 33 | "discontinuitySequence": 0, |
34 | "discontinuityStarts": [] | ||
34 | } | 35 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -9,5 +9,6 @@ | ... | @@ -9,5 +9,6 @@ |
9 | ], | 9 | ], |
10 | "targetDuration": 8, | 10 | "targetDuration": 8, |
11 | "endList": true, | 11 | "endList": true, |
12 | "discontinuitySequence": 0 | 12 | "discontinuitySequence": 0, |
13 | "discontinuityStarts": [] | ||
13 | } | 14 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -142,5 +142,6 @@ | ... | @@ -142,5 +142,6 @@ |
142 | ], | 142 | ], |
143 | "targetDuration": 10, | 143 | "targetDuration": 10, |
144 | "endList": true, | 144 | "endList": true, |
145 | "discontinuitySequence": 0 | 145 | "discontinuitySequence": 0, |
146 | "discontinuityStarts": [] | ||
146 | } | 147 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -14,5 +14,6 @@ | ... | @@ -14,5 +14,6 @@ |
14 | ], | 14 | ], |
15 | "targetDuration": 10, | 15 | "targetDuration": 10, |
16 | "endList": true, | 16 | "endList": true, |
17 | "discontinuitySequence": 0 | 17 | "discontinuitySequence": 0, |
18 | "discontinuityStarts": [] | ||
18 | } | 19 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -22,5 +22,6 @@ | ... | @@ -22,5 +22,6 @@ |
22 | ], | 22 | ], |
23 | "targetDuration": 8, | 23 | "targetDuration": 8, |
24 | "endList": true, | 24 | "endList": true, |
25 | "discontinuitySequence": 0 | 25 | "discontinuitySequence": 0, |
26 | "discontinuityStarts": [] | ||
26 | } | 27 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -29,5 +29,6 @@ | ... | @@ -29,5 +29,6 @@ |
29 | ], | 29 | ], |
30 | "targetDuration": 10, | 30 | "targetDuration": 10, |
31 | "endList": true, | 31 | "endList": true, |
32 | "discontinuitySequence": 0 | 32 | "discontinuitySequence": 0, |
33 | "discontinuityStarts": [] | ||
33 | } | 34 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -25,5 +25,6 @@ | ... | @@ -25,5 +25,6 @@ |
25 | ], | 25 | ], |
26 | "targetDuration": 10, | 26 | "targetDuration": 10, |
27 | "endList": true, | 27 | "endList": true, |
28 | "discontinuitySequence": 0 | 28 | "discontinuitySequence": 0, |
29 | "discontinuityStarts": [] | ||
29 | } | 30 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -9,5 +9,6 @@ | ... | @@ -9,5 +9,6 @@ |
9 | ], | 9 | ], |
10 | "targetDuration": 10, | 10 | "targetDuration": 10, |
11 | "endList": true, | 11 | "endList": true, |
12 | "discontinuitySequence": 0 | 12 | "discontinuitySequence": 0, |
13 | "discontinuityStarts": [] | ||
13 | } | 14 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -22,5 +22,6 @@ | ... | @@ -22,5 +22,6 @@ |
22 | ], | 22 | ], |
23 | "targetDuration": 10, | 23 | "targetDuration": 10, |
24 | "endList": true, | 24 | "endList": true, |
25 | "discontinuitySequence": 0 | 25 | "discontinuitySequence": 0, |
26 | "discontinuityStarts": [] | ||
26 | } | 27 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -22,5 +22,6 @@ | ... | @@ -22,5 +22,6 @@ |
22 | ], | 22 | ], |
23 | "targetDuration": 8, | 23 | "targetDuration": 8, |
24 | "endList": true, | 24 | "endList": true, |
25 | "discontinuitySequence": 0 | 25 | "discontinuitySequence": 0, |
26 | "discontinuityStarts": [] | ||
26 | } | 27 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -18,5 +18,6 @@ | ... | @@ -18,5 +18,6 @@ |
18 | ], | 18 | ], |
19 | "targetDuration": 10, | 19 | "targetDuration": 10, |
20 | "endList": true, | 20 | "endList": true, |
21 | "discontinuitySequence": 0 | 21 | "discontinuitySequence": 0, |
22 | "discontinuityStarts": [] | ||
22 | } | 23 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -22,5 +22,6 @@ | ... | @@ -22,5 +22,6 @@ |
22 | ], | 22 | ], |
23 | "targetDuration": 8, | 23 | "targetDuration": 8, |
24 | "endList": true, | 24 | "endList": true, |
25 | "discontinuitySequence": 0 | 25 | "discontinuitySequence": 0, |
26 | "discontinuityStarts": [] | ||
26 | } | 27 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -22,5 +22,6 @@ | ... | @@ -22,5 +22,6 @@ |
22 | ], | 22 | ], |
23 | "targetDuration": 8, | 23 | "targetDuration": 8, |
24 | "endList": true, | 24 | "endList": true, |
25 | "discontinuitySequence": 0 | 25 | "discontinuitySequence": 0, |
26 | "discontinuityStarts": [] | ||
26 | } | 27 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -22,5 +22,6 @@ | ... | @@ -22,5 +22,6 @@ |
22 | ], | 22 | ], |
23 | "targetDuration": 8, | 23 | "targetDuration": 8, |
24 | "endList": true, | 24 | "endList": true, |
25 | "discontinuitySequence": 0 | 25 | "discontinuitySequence": 0, |
26 | "discontinuityStarts": [] | ||
26 | } | 27 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -142,5 +142,6 @@ | ... | @@ -142,5 +142,6 @@ |
142 | ], | 142 | ], |
143 | "targetDuration": 10, | 143 | "targetDuration": 10, |
144 | "endList": true, | 144 | "endList": true, |
145 | "discontinuitySequence": 0 | 145 | "discontinuitySequence": 0, |
146 | "discontinuityStarts": [] | ||
146 | } | 147 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -10,5 +10,6 @@ | ... | @@ -10,5 +10,6 @@ |
10 | ], | 10 | ], |
11 | "targetDuration": 8, | 11 | "targetDuration": 8, |
12 | "endList": true, | 12 | "endList": true, |
13 | "discontinuitySequence": 0 | 13 | "discontinuitySequence": 0, |
14 | "discontinuityStarts": [] | ||
14 | } | 15 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -22,5 +22,6 @@ | ... | @@ -22,5 +22,6 @@ |
22 | ], | 22 | ], |
23 | "targetDuration": 8, | 23 | "targetDuration": 8, |
24 | "endList": true, | 24 | "endList": true, |
25 | "discontinuitySequence": 0 | 25 | "discontinuitySequence": 0, |
26 | "discontinuityStarts": [] | ||
26 | } | 27 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -10,5 +10,6 @@ | ... | @@ -10,5 +10,6 @@ |
10 | ], | 10 | ], |
11 | "targetDuration": 10, | 11 | "targetDuration": 10, |
12 | "endList": true, | 12 | "endList": true, |
13 | "discontinuitySequence": 0 | 13 | "discontinuitySequence": 0, |
14 | "discontinuityStarts": [] | ||
14 | } | 15 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -22,5 +22,6 @@ | ... | @@ -22,5 +22,6 @@ |
22 | ], | 22 | ], |
23 | "targetDuration": 10, | 23 | "targetDuration": 10, |
24 | "endList": true, | 24 | "endList": true, |
25 | "discontinuitySequence": 0 | 25 | "discontinuitySequence": 0, |
26 | "discontinuityStarts": [] | ||
26 | } | 27 | } |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -3,7 +3,7 @@ | ... | @@ -3,7 +3,7 @@ |
3 | 'use strict'; | 3 | 'use strict'; |
4 | var Playlist = videojs.Hls.Playlist; | 4 | var Playlist = videojs.Hls.Playlist; |
5 | 5 | ||
6 | module('Playlist Utilities'); | 6 | module('Playlist Duration'); |
7 | 7 | ||
8 | test('total duration for live playlists is Infinity', function() { | 8 | test('total duration for live playlists is Infinity', function() { |
9 | var duration = Playlist.duration({ | 9 | var duration = Playlist.duration({ |
... | @@ -16,7 +16,9 @@ | ... | @@ -16,7 +16,9 @@ |
16 | equal(duration, Infinity, 'duration is infinity'); | 16 | equal(duration, Infinity, 'duration is infinity'); |
17 | }); | 17 | }); |
18 | 18 | ||
19 | test('interval duration accounts for media sequences', function() { | 19 | module('Playlist Interval Duration'); |
20 | |||
21 | test('accounts for media sequences', function() { | ||
20 | var duration = Playlist.duration({ | 22 | var duration = Playlist.duration({ |
21 | mediaSequence: 10, | 23 | mediaSequence: 10, |
22 | endList: true, | 24 | endList: true, |
... | @@ -38,7 +40,7 @@ | ... | @@ -38,7 +40,7 @@ |
38 | equal(duration, 14 * 10, 'duration includes dropped segments'); | 40 | equal(duration, 14 * 10, 'duration includes dropped segments'); |
39 | }); | 41 | }); |
40 | 42 | ||
41 | test('interval duration uses PTS values when available', function() { | 43 | test('uses PTS values when available', function() { |
42 | var duration = Playlist.duration({ | 44 | var duration = Playlist.duration({ |
43 | mediaSequence: 0, | 45 | mediaSequence: 0, |
44 | endList: true, | 46 | endList: true, |
... | @@ -67,52 +69,244 @@ | ... | @@ -67,52 +69,244 @@ |
67 | equal(duration, ((4 * 10 * 1000 + 2) - 1) * 0.001, 'used PTS values'); | 69 | equal(duration, ((4 * 10 * 1000 + 2) - 1) * 0.001, 'used PTS values'); |
68 | }); | 70 | }); |
69 | 71 | ||
70 | test('interval duration works when partial PTS information is available', function() { | 72 | test('works when partial PTS information is available', function() { |
71 | var firstInterval, secondInterval, duration = Playlist.duration({ | 73 | var duration = Playlist.duration({ |
72 | mediaSequence: 0, | 74 | mediaSequence: 0, |
73 | endList: true, | 75 | endList: true, |
74 | segments: [{ | 76 | segments: [{ |
75 | minVideoPts: 1, | 77 | minVideoPts: 1, |
76 | minAudioPts: 2, | 78 | minAudioPts: 2, |
77 | maxVideoPts: 1 * 10 * 1000 + 1, | 79 | maxVideoPts: 10 * 1000 + 1, |
78 | 80 | ||
79 | // intentionally less duration than video | 81 | // intentionally less duration than video |
80 | // the max stream duration should be used | 82 | // the max stream duration should be used |
81 | maxAudioPts: 1 * 10 * 1000 + 1, | 83 | maxAudioPts: 10 * 1000 + 1, |
84 | uri: '0.ts' | ||
85 | }, { | ||
86 | duration: 9, | ||
87 | uri: '1.ts' | ||
88 | }, { | ||
89 | duration: 10, | ||
90 | uri: '2.ts' | ||
91 | }, { | ||
92 | duration: 10, | ||
93 | minVideoPts: 30 * 1000 + 7, | ||
94 | minAudioPts: 30 * 1000 + 10, | ||
95 | maxVideoPts: 40 * 1000 + 1, | ||
96 | maxAudioPts: 40 * 1000 + 2, | ||
97 | uri: '3.ts' | ||
98 | }, { | ||
99 | duration: 10, | ||
100 | maxVideoPts: 50 * 1000 + 1, | ||
101 | maxAudioPts: 50 * 1000 + 2, | ||
102 | uri: '4.ts' | ||
103 | }] | ||
104 | }, 0, 5); | ||
105 | |||
106 | equal(duration, | ||
107 | ((50 * 1000 + 2) - 1) * 0.001, | ||
108 | 'calculated with mixed intervals'); | ||
109 | }); | ||
110 | |||
111 | test('ignores segments before the start', function() { | ||
112 | var duration = Playlist.duration({ | ||
113 | mediaSequence: 0, | ||
114 | segments: [{ | ||
115 | duration: 10, | ||
82 | uri: '0.ts' | 116 | uri: '0.ts' |
83 | }, { | 117 | }, { |
84 | duration: 10, | 118 | duration: 10, |
85 | uri: '1.ts' | 119 | uri: '1.ts' |
86 | }, { | 120 | }, { |
87 | duration: 10, | 121 | duration: 10, |
88 | minVideoPts: 2 * 10 * 1000 + 7, | 122 | uri: '2.ts' |
89 | minAudioPts: 2 * 10 * 1000 + 10, | 123 | }] |
90 | maxVideoPts: 3 * 10 * 1000 + 1, | 124 | }, 1, 3); |
91 | maxAudioPts: 3 * 10 * 1000 + 2, | 125 | |
126 | equal(duration, 10 + 10, 'ignored the first segment'); | ||
127 | }); | ||
128 | |||
129 | test('ignores discontinuity sequences earlier than the start', function() { | ||
130 | var duration = Playlist.duration({ | ||
131 | mediaSequence: 0, | ||
132 | discontinuityStarts: [1, 3], | ||
133 | segments: [{ | ||
134 | minVideoPts: 0, | ||
135 | minAudioPts: 0, | ||
136 | maxVideoPts: 10 * 1000, | ||
137 | maxAudioPts: 10 * 1000, | ||
138 | uri: '0.ts' | ||
139 | }, { | ||
140 | discontinuity: true, | ||
141 | duration: 9, | ||
142 | uri: '1.ts' | ||
143 | }, { | ||
144 | duration: 10, | ||
92 | uri: '2.ts' | 145 | uri: '2.ts' |
93 | }, { | 146 | }, { |
147 | discontinuity: true, | ||
94 | duration: 10, | 148 | duration: 10, |
95 | maxVideoPts: 4 * 10 * 1000 + 1, | ||
96 | maxAudioPts: 4 * 10 * 1000 + 2, | ||
97 | uri: '3.ts' | 149 | uri: '3.ts' |
98 | }] | 150 | }] |
99 | }, 0, 4); | 151 | }, 2, 4); |
152 | |||
153 | equal(duration, 10 + 10, 'excluded the earlier segments'); | ||
154 | }); | ||
100 | 155 | ||
101 | firstInterval = (1 * 10 * 1000 + 1) - 1; | 156 | test('ignores discontinuity sequences later than the end', function() { |
102 | firstInterval *= 0.001; | 157 | var duration = Playlist.duration({ |
103 | secondInterval = (4 * 10 * 1000 + 2) - (2 * 10 * 1000 + 7); | 158 | mediaSequence: 0, |
104 | secondInterval *= 0.001; | 159 | discontinuityStarts: [1, 3], |
160 | segments: [{ | ||
161 | minVideoPts: 0, | ||
162 | minAudioPts: 0, | ||
163 | maxVideoPts: 10 * 1000, | ||
164 | maxAudioPts: 10 * 1000, | ||
165 | uri: '0.ts' | ||
166 | }, { | ||
167 | discontinuity: true, | ||
168 | duration: 9, | ||
169 | uri: '1.ts' | ||
170 | }, { | ||
171 | duration: 10, | ||
172 | uri: '2.ts' | ||
173 | }, { | ||
174 | discontinuity: true, | ||
175 | duration: 10, | ||
176 | uri: '3.ts' | ||
177 | }] | ||
178 | }, 0, 2); | ||
105 | 179 | ||
106 | equal(duration, firstInterval + 10 + secondInterval, 'calculated with mixed intervals'); | 180 | equal(duration, 19, 'excluded the later segments'); |
107 | }); | 181 | }); |
108 | 182 | ||
109 | test('interval duration accounts for discontinuities', function() { | 183 | test('handles trailing segments without PTS information', function() { |
110 | var duration = Playlist.duration({ | 184 | var duration = Playlist.duration({ |
111 | mediaSequence: 0, | 185 | mediaSequence: 0, |
112 | endList: true, | 186 | endList: true, |
113 | segments: [{ | 187 | segments: [{ |
114 | minVideoPts: 0, | 188 | minVideoPts: 0, |
115 | minAudioPts: 0, | 189 | minAudioPts: 0, |
190 | maxVideoPts: 10 * 1000, | ||
191 | maxAudioPts: 10 * 1000, | ||
192 | uri: '0.ts' | ||
193 | }, { | ||
194 | duration: 9, | ||
195 | uri: '1.ts' | ||
196 | }, { | ||
197 | duration: 10, | ||
198 | uri: '2.ts' | ||
199 | }, { | ||
200 | minVideoPts: 29.5 * 1000, | ||
201 | minAudioPts: 29.5 * 1000, | ||
202 | maxVideoPts: 39.5 * 1000, | ||
203 | maxAudioPts: 39.5 * 1000, | ||
204 | uri: '3.ts' | ||
205 | }] | ||
206 | }, 0, 3); | ||
207 | |||
208 | equal(duration, 29.5, 'calculated duration'); | ||
209 | }); | ||
210 | |||
211 | test('uses PTS intervals when the start and end segment have them', function() { | ||
212 | var playlist, duration; | ||
213 | playlist = { | ||
214 | mediaSequence: 0, | ||
215 | segments: [{ | ||
216 | minVideoPts: 0, | ||
217 | minAudioPts: 0, | ||
218 | maxVideoPts: 10 * 1000, | ||
219 | maxAudioPts: 10 * 1000, | ||
220 | uri: '0.ts' | ||
221 | }, { | ||
222 | duration: 9, | ||
223 | uri: '1.ts' | ||
224 | },{ | ||
225 | minVideoPts: 20 * 1000 + 100, | ||
226 | minAudioPts: 20 * 1000 + 100, | ||
227 | maxVideoPts: 30 * 1000 + 100, | ||
228 | maxAudioPts: 30 * 1000 + 100, | ||
229 | duration: 10, | ||
230 | uri: '2.ts' | ||
231 | }] | ||
232 | }; | ||
233 | duration = Playlist.duration(playlist, 0, 2); | ||
234 | |||
235 | equal(duration, 20.1, 'used the PTS-based interval'); | ||
236 | |||
237 | duration = Playlist.duration(playlist, 0, 3); | ||
238 | equal(duration, 30.1, 'used the PTS-based interval'); | ||
239 | }); | ||
240 | |||
241 | test('uses the largest continuous available PTS ranges', function() { | ||
242 | var playlist = { | ||
243 | mediaSequence: 0, | ||
244 | segments: [{ | ||
245 | minVideoPts: 0, | ||
246 | minAudioPts: 0, | ||
247 | maxVideoPts: 10 * 1000, | ||
248 | maxAudioPts: 10 * 1000, | ||
249 | uri: '0.ts' | ||
250 | }, { | ||
251 | duration: 10, | ||
252 | uri: '1.ts' | ||
253 | }, { | ||
254 | // starts 0.5s earlier than the previous segment indicates | ||
255 | minVideoPts: 19.5 * 1000, | ||
256 | minAudioPts: 19.5 * 1000, | ||
257 | maxVideoPts: 29.5 * 1000, | ||
258 | maxAudioPts: 29.5 * 1000, | ||
259 | uri: '2.ts' | ||
260 | }, { | ||
261 | duration: 10, | ||
262 | uri: '3.ts' | ||
263 | }, { | ||
264 | // ... but by the last segment, there is actual 0.5s more | ||
265 | // content than duration indicates | ||
266 | minVideoPts: 40.5 * 1000, | ||
267 | minAudioPts: 40.5 * 1000, | ||
268 | maxVideoPts: 50.5 * 1000, | ||
269 | maxAudioPts: 50.5 * 1000, | ||
270 | uri: '4.ts' | ||
271 | }] | ||
272 | }; | ||
273 | |||
274 | equal(Playlist.duration(playlist, 0, 5), | ||
275 | 50.5, | ||
276 | 'calculated across the larger PTS interval'); | ||
277 | }); | ||
278 | |||
279 | test('counts the time between segments as part of the earlier segment\'s duration', function() { | ||
280 | var duration = Playlist.duration({ | ||
281 | mediaSequence: 0, | ||
282 | endList: true, | ||
283 | segments: [{ | ||
284 | minVideoPts: 0, | ||
285 | minAudioPts: 0, | ||
286 | maxVideoPts: 1 * 10 * 1000, | ||
287 | maxAudioPts: 1 * 10 * 1000, | ||
288 | uri: '0.ts' | ||
289 | }, { | ||
290 | minVideoPts: 1 * 10 * 1000 + 100, | ||
291 | minAudioPts: 1 * 10 * 1000 + 100, | ||
292 | maxVideoPts: 2 * 10 * 1000 + 100, | ||
293 | maxAudioPts: 2 * 10 * 1000 + 100, | ||
294 | duration: 10, | ||
295 | uri: '1.ts' | ||
296 | }] | ||
297 | }, 0, 1); | ||
298 | |||
299 | equal(duration, (1 * 10 * 1000 + 100) * 0.001, 'included the segment gap'); | ||
300 | }); | ||
301 | |||
302 | test('accounts for discontinuities', function() { | ||
303 | var duration = Playlist.duration({ | ||
304 | mediaSequence: 0, | ||
305 | endList: true, | ||
306 | discontinuityStarts: [1], | ||
307 | segments: [{ | ||
308 | minVideoPts: 0, | ||
309 | minAudioPts: 0, | ||
116 | maxVideoPts: 1 * 10 * 1000, | 310 | maxVideoPts: 1 * 10 * 1000, |
117 | maxAudioPts: 1 * 10 * 1000, | 311 | maxAudioPts: 1 * 10 * 1000, |
118 | uri: '0.ts' | 312 | uri: '0.ts' |
... | @@ -130,6 +324,76 @@ | ... | @@ -130,6 +324,76 @@ |
130 | equal(duration, 10 + 10, 'handles discontinuities'); | 324 | equal(duration, 10 + 10, 'handles discontinuities'); |
131 | }); | 325 | }); |
132 | 326 | ||
327 | test('does not count ending segment gaps across a discontinuity', function() { | ||
328 | var duration = Playlist.duration({ | ||
329 | mediaSequence: 0, | ||
330 | discontinuityStarts: [1], | ||
331 | endList: true, | ||
332 | segments: [{ | ||
333 | minVideoPts: 0, | ||
334 | minAudioPts: 0, | ||
335 | maxVideoPts: 1 * 10 * 1000, | ||
336 | maxAudioPts: 1 * 10 * 1000, | ||
337 | uri: '0.ts' | ||
338 | }, { | ||
339 | discontinuity: true, | ||
340 | minVideoPts: 1 * 10 * 1000 + 100, | ||
341 | minAudioPts: 1 * 10 * 1000 + 100, | ||
342 | maxVideoPts: 2 * 10 * 1000 + 100, | ||
343 | maxAudioPts: 2 * 10 * 1000 + 100, | ||
344 | duration: 10, | ||
345 | uri: '1.ts' | ||
346 | }] | ||
347 | }, 0, 1); | ||
348 | |||
349 | equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap'); | ||
350 | }); | ||
351 | |||
352 | test('trailing duration on the final segment can be excluded', function() { | ||
353 | var duration = Playlist.duration({ | ||
354 | mediaSequence: 0, | ||
355 | endList: true, | ||
356 | segments: [{ | ||
357 | minVideoPts: 0, | ||
358 | minAudioPts: 0, | ||
359 | maxVideoPts: 1 * 10 * 1000, | ||
360 | maxAudioPts: 1 * 10 * 1000, | ||
361 | uri: '0.ts' | ||
362 | }, { | ||
363 | minVideoPts: 1 * 10 * 1000 + 100, | ||
364 | minAudioPts: 1 * 10 * 1000 + 100, | ||
365 | maxVideoPts: 2 * 10 * 1000 + 100, | ||
366 | maxAudioPts: 2 * 10 * 1000 + 100, | ||
367 | duration: 10, | ||
368 | uri: '1.ts' | ||
369 | }] | ||
370 | }, 0, 1, false); | ||
371 | |||
372 | equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap'); | ||
373 | }); | ||
374 | |||
375 | test('a non-positive length interval has zero duration', function() { | ||
376 | var playlist = { | ||
377 | mediaSequence: 0, | ||
378 | discontinuityStarts: [1], | ||
379 | segments: [{ | ||
380 | duration: 10, | ||
381 | uri: '0.ts' | ||
382 | }, { | ||
383 | discontinuity: true, | ||
384 | duration: 10, | ||
385 | uri: '1.ts' | ||
386 | }] | ||
387 | }; | ||
388 | |||
389 | equal(Playlist.duration(playlist, 0, 0), 0, 'zero-length duration is zero'); | ||
390 | equal(Playlist.duration(playlist, 0, 0, false), 0, 'zero-length duration is zero'); | ||
391 | equal(Playlist.duration(playlist, 0, -1), 0, 'negative length duration is zero'); | ||
392 | equal(Playlist.duration(playlist, 2, 1, false), 0, 'negative length duration is zero'); | ||
393 | }); | ||
394 | |||
395 | module('Playlist Seekable'); | ||
396 | |||
133 | test('calculates seekable time ranges from the available segments', function() { | 397 | test('calculates seekable time ranges from the available segments', function() { |
134 | var playlist = { | 398 | var playlist = { |
135 | mediaSequence: 0, | 399 | mediaSequence: 0, | ... | ... |
... | @@ -97,15 +97,13 @@ var | ... | @@ -97,15 +97,13 @@ var |
97 | var MockSegmentParser; | 97 | var MockSegmentParser; |
98 | 98 | ||
99 | if (tags === undefined) { | 99 | if (tags === undefined) { |
100 | tags = []; | 100 | tags = [{ pts: 0, bytes: new Uint8Array(1) }]; |
101 | } | 101 | } |
102 | MockSegmentParser = function() { | 102 | MockSegmentParser = function() { |
103 | this.getFlvHeader = function() { | 103 | this.getFlvHeader = function() { |
104 | return 'flv'; | 104 | return 'flv'; |
105 | }; | 105 | }; |
106 | this.parseSegmentBinaryData = function() {}; | 106 | this.parseSegmentBinaryData = function() {}; |
107 | this.timestampOffset = 0; | ||
108 | this.mediaTimelineOffset = 0; | ||
109 | this.flushTags = function() {}; | 107 | this.flushTags = function() {}; |
110 | this.tagsAvailable = function() { | 108 | this.tagsAvailable = function() { |
111 | return tags.length; | 109 | return tags.length; |
... | @@ -1287,30 +1285,32 @@ test('clears in-band cues ahead of current time on seek', function() { | ... | @@ -1287,30 +1285,32 @@ test('clears in-band cues ahead of current time on seek', function() { |
1287 | 1285 | ||
1288 | player.hls.segmentParser_.parseSegmentBinaryData = function() { | 1286 | player.hls.segmentParser_.parseSegmentBinaryData = function() { |
1289 | // trigger a metadata event | 1287 | // trigger a metadata event |
1290 | if (events.length) { | 1288 | while (events.length) { |
1291 | player.hls.segmentParser_.metadataStream.trigger('data', events.shift()); | 1289 | player.hls.segmentParser_.metadataStream.trigger('data', events.shift()); |
1292 | } | 1290 | } |
1293 | }; | 1291 | }; |
1294 | standardXHRResponse(requests.shift()); // media | 1292 | standardXHRResponse(requests.shift()); // media |
1295 | tags.push({ pts: 10 * 1000, bytes: new Uint8Array(1) }); | 1293 | tags.push({ pts: 0, bytes: new Uint8Array(1) }, |
1294 | { pts: 10 * 1000, bytes: new Uint8Array(1) }); | ||
1296 | events.push({ | 1295 | events.push({ |
1297 | pts: 20 * 1000, | 1296 | pts: 9.9 * 1000, |
1298 | data: new Uint8Array([]), | 1297 | data: new Uint8Array([]), |
1299 | frames: [{ | 1298 | frames: [{ |
1300 | id: 'TXXX', | 1299 | id: 'TXXX', |
1301 | value: 'cue 3' | 1300 | value: 'cue 1' |
1302 | }] | 1301 | }] |
1303 | }); | 1302 | }); |
1304 | events.push({ | 1303 | events.push({ |
1305 | pts: 9.9 * 1000, | 1304 | pts: 20 * 1000, |
1306 | data: new Uint8Array([]), | 1305 | data: new Uint8Array([]), |
1307 | frames: [{ | 1306 | frames: [{ |
1308 | id: 'TXXX', | 1307 | id: 'TXXX', |
1309 | value: 'cue 1' | 1308 | value: 'cue 3' |
1310 | }] | 1309 | }] |
1311 | }); | 1310 | }); |
1312 | standardXHRResponse(requests.shift()); // segment 0 | 1311 | standardXHRResponse(requests.shift()); // segment 0 |
1313 | tags.push({ pts: 20 * 1000, bytes: new Uint8Array(1) }); | 1312 | tags.push({ pts: 10 * 1000 + 1, bytes: new Uint8Array(1) }, |
1313 | { pts: 20 * 1000, bytes: new Uint8Array(1) }); | ||
1314 | events.push({ | 1314 | events.push({ |
1315 | pts: 19.9 * 1000, | 1315 | pts: 19.9 * 1000, |
1316 | data: new Uint8Array([]), | 1316 | data: new Uint8Array([]), |
... | @@ -1323,12 +1323,12 @@ test('clears in-band cues ahead of current time on seek', function() { | ... | @@ -1323,12 +1323,12 @@ test('clears in-band cues ahead of current time on seek', function() { |
1323 | standardXHRResponse(requests.shift()); // segment 1 | 1323 | standardXHRResponse(requests.shift()); // segment 1 |
1324 | 1324 | ||
1325 | track = player.textTracks()[0]; | 1325 | track = player.textTracks()[0]; |
1326 | equal(track.cues.length, 2, 'added the cues'); | 1326 | equal(track.cues.length, 3, 'added the cues'); |
1327 | 1327 | ||
1328 | // seek into segment 1 | 1328 | // seek into segment 1 |
1329 | player.currentTime(11); | 1329 | player.currentTime(11); |
1330 | player.trigger('seeking'); | 1330 | player.trigger('seeking'); |
1331 | equal(track.cues.length, 1, 'removed a cue'); | 1331 | equal(track.cues.length, 1, 'removed later cues'); |
1332 | equal(track.cues[0].startTime, 9.9, 'retained the earlier cue'); | 1332 | equal(track.cues[0].startTime, 9.9, 'retained the earlier cue'); |
1333 | }); | 1333 | }); |
1334 | 1334 | ||
... | @@ -1342,9 +1342,6 @@ test('translates ID3 PTS values to cue media timeline positions', function() { | ... | @@ -1342,9 +1342,6 @@ test('translates ID3 PTS values to cue media timeline positions', function() { |
1342 | openMediaSource(player); | 1342 | openMediaSource(player); |
1343 | 1343 | ||
1344 | player.hls.segmentParser_.parseSegmentBinaryData = function() { | 1344 | player.hls.segmentParser_.parseSegmentBinaryData = function() { |
1345 | // setup the timestamp offset | ||
1346 | this.timestampOffset = tags[0].pts; | ||
1347 | |||
1348 | // trigger a metadata event | 1345 | // trigger a metadata event |
1349 | player.hls.segmentParser_.metadataStream.trigger('data', { | 1346 | player.hls.segmentParser_.metadataStream.trigger('data', { |
1350 | pts: 5 * 1000, | 1347 | pts: 5 * 1000, |
... | @@ -1373,9 +1370,6 @@ test('translates ID3 PTS values across discontinuities', function() { | ... | @@ -1373,9 +1370,6 @@ test('translates ID3 PTS values across discontinuities', function() { |
1373 | openMediaSource(player); | 1370 | openMediaSource(player); |
1374 | 1371 | ||
1375 | player.hls.segmentParser_.parseSegmentBinaryData = function() { | 1372 | player.hls.segmentParser_.parseSegmentBinaryData = function() { |
1376 | if (this.timestampOffset === null) { | ||
1377 | this.timestampOffset = tags[0].pts; | ||
1378 | } | ||
1379 | // trigger a metadata event | 1373 | // trigger a metadata event |
1380 | if (events.length) { | 1374 | if (events.length) { |
1381 | player.hls.segmentParser_.metadataStream.trigger('data', events.shift()); | 1375 | player.hls.segmentParser_.metadataStream.trigger('data', events.shift()); |
... | @@ -1393,7 +1387,6 @@ test('translates ID3 PTS values across discontinuities', function() { | ... | @@ -1393,7 +1387,6 @@ test('translates ID3 PTS values across discontinuities', function() { |
1393 | '1.ts\n'); | 1387 | '1.ts\n'); |
1394 | 1388 | ||
1395 | // segment 0 starts at PTS 14000 and has a cue point at 15000 | 1389 | // segment 0 starts at PTS 14000 and has a cue point at 15000 |
1396 | player.hls.segmentParser_.timestampOffset = 14 * 1000; | ||
1397 | tags.push({ pts: 14 * 1000, bytes: new Uint8Array(1) }, | 1390 | tags.push({ pts: 14 * 1000, bytes: new Uint8Array(1) }, |
1398 | { pts: 24 * 1000, bytes: new Uint8Array(1) }); | 1391 | { pts: 24 * 1000, bytes: new Uint8Array(1) }); |
1399 | events.push({ | 1392 | events.push({ |
... | @@ -2010,6 +2003,48 @@ test('continues playing after seek to discontinuity', function() { | ... | @@ -2010,6 +2003,48 @@ test('continues playing after seek to discontinuity', function() { |
2010 | strictEqual(aborts, 1, 'cleared the segment buffer on a seek'); | 2003 | strictEqual(aborts, 1, 'cleared the segment buffer on a seek'); |
2011 | }); | 2004 | }); |
2012 | 2005 | ||
2006 | test('seeking does not fail when targeted between segments', function() { | ||
2007 | var tags = [], currentTime, segmentUrl; | ||
2008 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
2009 | player.src({ | ||
2010 | src: 'media.m3u8', | ||
2011 | type: 'application/vnd.apple.mpegurl' | ||
2012 | }); | ||
2013 | openMediaSource(player); | ||
2014 | |||
2015 | // mock out the currentTime callbacks | ||
2016 | player.hls.el().vjs_setProperty = function(property, value) { | ||
2017 | if (property === 'currentTime') { | ||
2018 | currentTime = value; | ||
2019 | } | ||
2020 | }; | ||
2021 | player.hls.el().vjs_getProperty = function(property) { | ||
2022 | if (property === 'currentTime') { | ||
2023 | return currentTime; | ||
2024 | } | ||
2025 | }; | ||
2026 | |||
2027 | standardXHRResponse(requests.shift()); // media | ||
2028 | tags.push({ pts: 100, bytes: new Uint8Array(1) }, | ||
2029 | { pts: 9 * 1000 + 100, bytes: new Uint8Array(1) }); | ||
2030 | standardXHRResponse(requests.shift()); // segment 0 | ||
2031 | player.hls.checkBuffer_(); | ||
2032 | tags.push({ pts: 9.5 * 1000 + 100, bytes: new Uint8Array(1) }, | ||
2033 | { pts: 20 * 1000 + 100, bytes: new Uint8Array(1) }); | ||
2034 | segmentUrl = requests[0].url; | ||
2035 | standardXHRResponse(requests.shift()); // segment 1 | ||
2036 | |||
2037 | // seek to a time that is greater than the last tag in segment 0 but | ||
2038 | // less than the first in segment 1 | ||
2039 | player.currentTime(9.4); | ||
2040 | equal(requests[0].url, segmentUrl, 'requested the later segment'); | ||
2041 | |||
2042 | tags.push({ pts: 9.5 * 1000 + 100, bytes: new Uint8Array(1) }, | ||
2043 | { pts: 20 * 1000 + 100, bytes: new Uint8Array(1) }); | ||
2044 | standardXHRResponse(requests.shift()); // segment 1 | ||
2045 | equal(player.currentTime(), 9.5, 'seeked to the later time'); | ||
2046 | }); | ||
2047 | |||
2013 | test('resets the switching algorithm if a request times out', function() { | 2048 | test('resets the switching algorithm if a request times out', function() { |
2014 | player.src({ | 2049 | player.src({ |
2015 | src: 'master.m3u8', | 2050 | src: 'master.m3u8', |
... | @@ -2666,12 +2701,13 @@ test('treats invalid keys as a key request failure', function() { | ... | @@ -2666,12 +2701,13 @@ test('treats invalid keys as a key request failure', function() { |
2666 | equal(bytes[0], 'flv', 'appended the flv header'); | 2701 | equal(bytes[0], 'flv', 'appended the flv header'); |
2667 | 2702 | ||
2668 | tags.length = 0; | 2703 | tags.length = 0; |
2669 | tags.push({ pts: 1, bytes: new Uint8Array([1]) }); | 2704 | tags.push({ pts: 2833, bytes: new Uint8Array([1]) }, |
2705 | { pts: 4833, bytes: new Uint8Array([2]) }); | ||
2670 | // second segment request | 2706 | // second segment request |
2671 | standardXHRResponse(requests.shift()); | 2707 | standardXHRResponse(requests.shift()); |
2672 | 2708 | ||
2673 | equal(bytes.length, 2, 'appended bytes'); | 2709 | equal(bytes.length, 2, 'appended bytes'); |
2674 | deepEqual(new Uint8Array([1]), bytes[1], 'skipped to the second segment'); | 2710 | deepEqual(bytes[1], new Uint8Array([1, 2]), 'skipped to the second segment'); |
2675 | }); | 2711 | }); |
2676 | 2712 | ||
2677 | test('live stream should not call endOfStream', function(){ | 2713 | test('live stream should not call endOfStream', function(){ | ... | ... |
-
Please register or sign in to post a comment