UNPKG

12.8 kBJavaScriptView Raw
1// TODO handle fmp4 case where the timing info is accurate and doesn't involve transmux
2
3/**
4 * @file time.js
5 */
6
7import Playlist from '../playlist';
8
9// Add 25% to the segment duration to account for small discrepencies in segment timing.
10// 25% was arbitrarily chosen, and may need to be refined over time.
11const SEGMENT_END_FUDGE_PERCENT = 0.25;
12
13/**
14 * Converts a player time (any time that can be gotten/set from player.currentTime(),
15 * e.g., any time within player.seekable().start(0) to player.seekable().end(0)) to a
16 * program time (any time referencing the real world (e.g., EXT-X-PROGRAM-DATE-TIME)).
17 *
18 * The containing segment is required as the EXT-X-PROGRAM-DATE-TIME serves as an "anchor
19 * point" (a point where we have a mapping from program time to player time, with player
20 * time being the post transmux start of the segment).
21 *
22 * For more details, see [this doc](../../docs/program-time-from-player-time.md).
23 *
24 * @param {number} playerTime the player time
25 * @param {Object} segment the segment which contains the player time
26 * @return {Date} program time
27 */
28export const playerTimeToProgramTime = (playerTime, segment) => {
29 if (!segment.dateTimeObject) {
30 // Can't convert without an "anchor point" for the program time (i.e., a time that can
31 // be used to map the start of a segment with a real world time).
32 return null;
33 }
34
35 const transmuxerPrependedSeconds = segment.videoTimingInfo.transmuxerPrependedSeconds;
36 const transmuxedStart = segment.videoTimingInfo.transmuxedPresentationStart;
37
38 // get the start of the content from before old content is prepended
39 const startOfSegment = transmuxedStart + transmuxerPrependedSeconds;
40 const offsetFromSegmentStart = playerTime - startOfSegment;
41
42 return new Date(segment.dateTimeObject.getTime() + offsetFromSegmentStart * 1000);
43};
44
45export const originalSegmentVideoDuration = (videoTimingInfo) => {
46 return videoTimingInfo.transmuxedPresentationEnd -
47 videoTimingInfo.transmuxedPresentationStart -
48 videoTimingInfo.transmuxerPrependedSeconds;
49};
50
51/**
52 * Finds a segment that contains the time requested given as an ISO-8601 string. The
53 * returned segment might be an estimate or an accurate match.
54 *
55 * @param {string} programTime The ISO-8601 programTime to find a match for
56 * @param {Object} playlist A playlist object to search within
57 */
58export const findSegmentForProgramTime = (programTime, playlist) => {
59 // Assumptions:
60 // - verifyProgramDateTimeTags has already been run
61 // - live streams have been started
62
63 let dateTimeObject;
64
65 try {
66 dateTimeObject = new Date(programTime);
67 } catch (e) {
68 return null;
69 }
70
71 if (!playlist || !playlist.segments || playlist.segments.length === 0) {
72 return null;
73 }
74
75 let segment = playlist.segments[0];
76
77 if (dateTimeObject < segment.dateTimeObject) {
78 // Requested time is before stream start.
79 return null;
80 }
81
82 for (let i = 0; i < playlist.segments.length - 1; i++) {
83 segment = playlist.segments[i];
84
85 const nextSegmentStart = playlist.segments[i + 1].dateTimeObject;
86
87 if (dateTimeObject < nextSegmentStart) {
88 break;
89 }
90 }
91
92 const lastSegment = playlist.segments[playlist.segments.length - 1];
93 const lastSegmentStart = lastSegment.dateTimeObject;
94 const lastSegmentDuration = lastSegment.videoTimingInfo ?
95 originalSegmentVideoDuration(lastSegment.videoTimingInfo) :
96 lastSegment.duration + lastSegment.duration * SEGMENT_END_FUDGE_PERCENT;
97 const lastSegmentEnd =
98 new Date(lastSegmentStart.getTime() + lastSegmentDuration * 1000);
99
100 if (dateTimeObject > lastSegmentEnd) {
101 // Beyond the end of the stream, or our best guess of the end of the stream.
102 return null;
103 }
104
105 if (dateTimeObject > lastSegmentStart) {
106 segment = lastSegment;
107 }
108
109 return {
110 segment,
111 estimatedStart: segment.videoTimingInfo ?
112 segment.videoTimingInfo.transmuxedPresentationStart :
113 Playlist.duration(
114 playlist,
115 playlist.mediaSequence + playlist.segments.indexOf(segment)
116 ),
117 // Although, given that all segments have accurate date time objects, the segment
118 // selected should be accurate, unless the video has been transmuxed at some point
119 // (determined by the presence of the videoTimingInfo object), the segment's "player
120 // time" (the start time in the player) can't be considered accurate.
121 type: segment.videoTimingInfo ? 'accurate' : 'estimate'
122 };
123};
124
125/**
126 * Finds a segment that contains the given player time(in seconds).
127 *
128 * @param {number} time The player time to find a match for
129 * @param {Object} playlist A playlist object to search within
130 */
131export const findSegmentForPlayerTime = (time, playlist) => {
132 // Assumptions:
133 // - there will always be a segment.duration
134 // - we can start from zero
135 // - segments are in time order
136
137 if (!playlist || !playlist.segments || playlist.segments.length === 0) {
138 return null;
139 }
140
141 let segmentEnd = 0;
142 let segment;
143
144 for (let i = 0; i < playlist.segments.length; i++) {
145 segment = playlist.segments[i];
146
147 // videoTimingInfo is set after the segment is downloaded and transmuxed, and
148 // should contain the most accurate values we have for the segment's player times.
149 //
150 // Use the accurate transmuxedPresentationEnd value if it is available, otherwise fall
151 // back to an estimate based on the manifest derived (inaccurate) segment.duration, to
152 // calculate an end value.
153 segmentEnd = segment.videoTimingInfo ?
154 segment.videoTimingInfo.transmuxedPresentationEnd : segmentEnd + segment.duration;
155
156 if (time <= segmentEnd) {
157 break;
158 }
159 }
160
161 const lastSegment = playlist.segments[playlist.segments.length - 1];
162
163 if (lastSegment.videoTimingInfo &&
164 lastSegment.videoTimingInfo.transmuxedPresentationEnd < time) {
165 // The time requested is beyond the stream end.
166 return null;
167 }
168
169 if (time > segmentEnd) {
170 // The time is within or beyond the last segment.
171 //
172 // Check to see if the time is beyond a reasonable guess of the end of the stream.
173 if (time > segmentEnd + (lastSegment.duration * SEGMENT_END_FUDGE_PERCENT)) {
174 // Technically, because the duration value is only an estimate, the time may still
175 // exist in the last segment, however, there isn't enough information to make even
176 // a reasonable estimate.
177 return null;
178 }
179
180 segment = lastSegment;
181 }
182
183 return {
184 segment,
185 estimatedStart: segment.videoTimingInfo ?
186 segment.videoTimingInfo.transmuxedPresentationStart : segmentEnd - segment.duration,
187 // Because videoTimingInfo is only set after transmux, it is the only way to get
188 // accurate timing values.
189 type: segment.videoTimingInfo ? 'accurate' : 'estimate'
190 };
191};
192
193/**
194 * Gives the offset of the comparisonTimestamp from the programTime timestamp in seconds.
195 * If the offset returned is positive, the programTime occurs after the
196 * comparisonTimestamp.
197 * If the offset is negative, the programTime occurs before the comparisonTimestamp.
198 *
199 * @param {string} comparisonTimeStamp An ISO-8601 timestamp to compare against
200 * @param {string} programTime The programTime as an ISO-8601 string
201 * @return {number} offset
202 */
203export const getOffsetFromTimestamp = (comparisonTimeStamp, programTime) => {
204 let segmentDateTime;
205 let programDateTime;
206
207 try {
208 segmentDateTime = new Date(comparisonTimeStamp);
209 programDateTime = new Date(programTime);
210 } catch (e) {
211 // TODO handle error
212 }
213
214 const segmentTimeEpoch = segmentDateTime.getTime();
215 const programTimeEpoch = programDateTime.getTime();
216
217 return (programTimeEpoch - segmentTimeEpoch) / 1000;
218};
219
220/**
221 * Checks that all segments in this playlist have programDateTime tags.
222 *
223 * @param {Object} playlist A playlist object
224 */
225export const verifyProgramDateTimeTags = (playlist) => {
226 if (!playlist.segments || playlist.segments.length === 0) {
227 return false;
228 }
229
230 for (let i = 0; i < playlist.segments.length; i++) {
231 const segment = playlist.segments[i];
232
233 if (!segment.dateTimeObject) {
234 return false;
235 }
236 }
237
238 return true;
239};
240
241/**
242 * Returns the programTime of the media given a playlist and a playerTime.
243 * The playlist must have programDateTime tags for a programDateTime tag to be returned.
244 * If the segments containing the time requested have not been buffered yet, an estimate
245 * may be returned to the callback.
246 *
247 * @param {Object} args
248 * @param {Object} args.playlist A playlist object to search within
249 * @param {number} time A playerTime in seconds
250 * @param {Function} callback(err, programTime)
251 * @return {string} err.message A detailed error message
252 * @return {Object} programTime
253 * @return {number} programTime.mediaSeconds The streamTime in seconds
254 * @return {string} programTime.programDateTime The programTime as an ISO-8601 String
255 */
256export const getProgramTime = ({
257 playlist,
258 time = undefined,
259 callback
260}) => {
261
262 if (!callback) {
263 throw new Error('getProgramTime: callback must be provided');
264 }
265
266 if (!playlist || time === undefined) {
267 return callback({
268 message: 'getProgramTime: playlist and time must be provided'
269 });
270 }
271
272 const matchedSegment = findSegmentForPlayerTime(time, playlist);
273
274 if (!matchedSegment) {
275 return callback({
276 message: 'valid programTime was not found'
277 });
278 }
279
280 if (matchedSegment.type === 'estimate') {
281 return callback({
282 message:
283 'Accurate programTime could not be determined.' +
284 ' Please seek to e.seekTime and try again',
285 seekTime: matchedSegment.estimatedStart
286 });
287 }
288
289 const programTimeObject = {
290 mediaSeconds: time
291 };
292 const programTime = playerTimeToProgramTime(time, matchedSegment.segment);
293
294 if (programTime) {
295 programTimeObject.programDateTime = programTime.toISOString();
296 }
297
298 return callback(null, programTimeObject);
299};
300
301/**
302 * Seeks in the player to a time that matches the given programTime ISO-8601 string.
303 *
304 * @param {Object} args
305 * @param {string} args.programTime A programTime to seek to as an ISO-8601 String
306 * @param {Object} args.playlist A playlist to look within
307 * @param {number} args.retryCount The number of times to try for an accurate seek. Default is 2.
308 * @param {Function} args.seekTo A method to perform a seek
309 * @param {boolean} args.pauseAfterSeek Whether to end in a paused state after seeking. Default is true.
310 * @param {Object} args.tech The tech to seek on
311 * @param {Function} args.callback(err, newTime) A callback to return the new time to
312 * @return {string} err.message A detailed error message
313 * @return {number} newTime The exact time that was seeked to in seconds
314 */
315export const seekToProgramTime = ({
316 programTime,
317 playlist,
318 retryCount = 2,
319 seekTo,
320 pauseAfterSeek = true,
321 tech,
322 callback
323}) => {
324
325 if (!callback) {
326 throw new Error('seekToProgramTime: callback must be provided');
327 }
328
329 if (typeof programTime === 'undefined' || !playlist || !seekTo) {
330 return callback({
331 message: 'seekToProgramTime: programTime, seekTo and playlist must be provided'
332 });
333 }
334
335 if (!playlist.endList && !tech.hasStarted_) {
336 return callback({
337 message: 'player must be playing a live stream to start buffering'
338 });
339 }
340
341 if (!verifyProgramDateTimeTags(playlist)) {
342 return callback({
343 message: 'programDateTime tags must be provided in the manifest ' + playlist.resolvedUri
344 });
345 }
346
347 const matchedSegment = findSegmentForProgramTime(programTime, playlist);
348
349 // no match
350 if (!matchedSegment) {
351 return callback({
352 message: `${programTime} was not found in the stream`
353 });
354 }
355
356 const segment = matchedSegment.segment;
357 const mediaOffset = getOffsetFromTimestamp(
358 segment.dateTimeObject,
359 programTime
360 );
361
362 if (matchedSegment.type === 'estimate') {
363 // we've run out of retries
364 if (retryCount === 0) {
365 return callback({
366 message: `${programTime} is not buffered yet. Try again`
367 });
368 }
369
370 seekTo(matchedSegment.estimatedStart + mediaOffset);
371
372 tech.one('seeked', () => {
373 seekToProgramTime({
374 programTime,
375 playlist,
376 retryCount: retryCount - 1,
377 seekTo,
378 pauseAfterSeek,
379 tech,
380 callback
381 });
382 });
383
384 return;
385 }
386
387 // Since the segment.start value is determined from the buffered end or ending time
388 // of the prior segment, the seekToTime doesn't need to account for any transmuxer
389 // modifications.
390 const seekToTime = segment.start + mediaOffset;
391 const seekedCallback = () => {
392 return callback(null, tech.currentTime());
393 };
394
395 // listen for seeked event
396 tech.one('seeked', seekedCallback);
397 // pause before seeking as video.js will restore this state
398 if (pauseAfterSeek) {
399 tech.pause();
400 }
401 seekTo(seekToTime);
402};