playlist.js
10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
/**
* @file playlist.js
*
* Playlist related utilities.
*/
import {createTimeRange} from 'video.js';
let Playlist = {
/**
* The number of segments that are unsafe to start playback at in
* a live stream. Changing this value can cause playback stalls.
* See HTTP Live Streaming, "Playing the Media Playlist File"
* https://tools.ietf.org/html/draft-pantos-http-live-streaming-18#section-6.3.3
*/
UNSAFE_LIVE_SEGMENTS: 3
};
/**
* walk backward until we find a duration we can use
* or return a failure
*
* @param {Playlist} playlist the playlist to walk through
* @param {Number} endSequence the mediaSequence to stop walking on
*/
const backwardDuration = function(playlist, endSequence) {
let result = 0;
let i = endSequence - playlist.mediaSequence;
// if a start time is available for segment immediately following
// the interval, use it
let segment = playlist.segments[i];
// Walk backward until we find the latest segment with timeline
// information that is earlier than endSequence
if (segment) {
if (typeof segment.start !== 'undefined') {
return { result: segment.start, precise: true };
}
if (typeof segment.end !== 'undefined') {
return {
result: segment.end - segment.duration,
precise: true
};
}
}
while (i--) {
segment = playlist.segments[i];
if (typeof segment.end !== 'undefined') {
return { result: result + segment.end, precise: true };
}
result += segment.duration;
if (typeof segment.start !== 'undefined') {
return { result: result + segment.start, precise: true };
}
}
return { result, precise: false };
};
/**
* walk forward until we find a duration we can use
* or return a failure
*
* @param {Playlist} playlist the playlist to walk through
* @param {Number} endSequence the mediaSequence to stop walking on
*/
const forwardDuration = function(playlist, endSequence) {
let result = 0;
let segment;
let i = endSequence - playlist.mediaSequence;
// Walk forward until we find the earliest segment with timeline
// information
for (; i < playlist.segments.length; i++) {
segment = playlist.segments[i];
if (typeof segment.start !== 'undefined') {
return {
result: segment.start - result,
precise: true
};
}
result += segment.duration;
if (typeof segment.end !== 'undefined') {
return {
result: segment.end - result,
precise: true
};
}
}
// indicate we didn't find a useful duration estimate
return { result: -1, precise: false };
};
/**
* Calculate the media duration from the segments associated with a
* playlist. The duration of a subinterval of the available segments
* may be calculated by specifying an end index.
*
* @param {Object} playlist a media playlist object
* @param {Number=} endSequence an exclusive upper boundary
* for the playlist. Defaults to playlist length.
* @param {Number} expired the amount of time that has dropped
* off the front of the playlist in a live scenario
* @return {Number} the duration between the first available segment
* and end index.
*/
const intervalDuration = function(playlist, endSequence, expired) {
let backward;
let forward;
if (typeof endSequence === 'undefined') {
endSequence = playlist.mediaSequence + playlist.segments.length;
}
if (endSequence < playlist.mediaSequence) {
return 0;
}
// do a backward walk to estimate the duration
backward = backwardDuration(playlist, endSequence);
if (backward.precise) {
// if we were able to base our duration estimate on timing
// information provided directly from the Media Source, return
// it
return backward.result;
}
// walk forward to see if a precise duration estimate can be made
// that way
forward = forwardDuration(playlist, endSequence);
if (forward.precise) {
// we found a segment that has been buffered and so it's
// position is known precisely
return forward.result;
}
// return the less-precise, playlist-based duration estimate
return backward.result + expired;
};
/**
* Calculates the duration of a playlist. If a start and end index
* are specified, the duration will be for the subset of the media
* timeline between those two indices. The total duration for live
* playlists is always Infinity.
*
* @param {Object} playlist a media playlist object
* @param {Number=} endSequence an exclusive upper
* boundary for the playlist. Defaults to the playlist media
* sequence number plus its length.
* @param {Number=} expired the amount of time that has
* dropped off the front of the playlist in a live scenario
* @return {Number} the duration between the start index and end
* index.
*/
export const duration = function(playlist, endSequence, expired) {
if (!playlist) {
return 0;
}
if (typeof expired !== 'number') {
expired = 0;
}
// if a slice of the total duration is not requested, use
// playlist-level duration indicators when they're present
if (typeof endSequence === 'undefined') {
// if present, use the duration specified in the playlist
if (playlist.totalDuration) {
return playlist.totalDuration;
}
// duration should be Infinity for live playlists
if (!playlist.endList) {
return window.Infinity;
}
}
// calculate the total duration based on the segment durations
return intervalDuration(playlist,
endSequence,
expired);
};
/**
* Calculates the interval of time that is currently seekable in a
* playlist. The returned time ranges are relative to the earliest
* moment in the specified playlist that is still available. A full
* seekable implementation for live streams would need to offset
* these values by the duration of content that has expired from the
* stream.
*
* @param {Object} playlist a media playlist object
* @param {Number=} expired the amount of time that has
* dropped off the front of the playlist in a live scenario
* @return {TimeRanges} the periods of time that are valid targets
* for seeking
*/
export const seekable = function(playlist, expired) {
let start;
let end;
let endSequence;
if (typeof expired !== 'number') {
expired = 0;
}
// without segments, there are no seekable ranges
if (!playlist || !playlist.segments) {
return createTimeRange();
}
// when the playlist is complete, the entire duration is seekable
if (playlist.endList) {
return createTimeRange(0, duration(playlist));
}
// live playlists should not expose three segment durations worth
// of content from the end of the playlist
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3
start = intervalDuration(playlist, playlist.mediaSequence, expired);
endSequence = Math.max(0, playlist.segments.length - Playlist.UNSAFE_LIVE_SEGMENTS);
end = intervalDuration(playlist,
playlist.mediaSequence + endSequence,
expired);
return createTimeRange(start, end);
};
/**
* Determine the index of the segment that contains a specified
* playback position in a media playlist.
*
* @param {Object} playlist the media playlist to query
* @param {Number} time The number of seconds since the earliest
* possible position to determine the containing segment for
* @param {Number=} expired the duration of content, in
* seconds, that has been removed from this playlist because it
* expired
* @return {Number} The number of the media segment that contains
* that time position.
*/
export const getMediaIndexForTime_ = function(playlist, time, expired) {
let i;
let segment;
let originalTime = time;
let numSegments = playlist.segments.length;
let lastSegment = numSegments - 1;
let startIndex;
let endIndex;
let knownStart;
let knownEnd;
if (!playlist) {
return 0;
}
// when the requested position is earlier than the current set of
// segments, return the earliest segment index
if (time < 0) {
return 0;
}
expired = expired || 0;
// find segments with known timing information that bound the
// target time
for (i = 0; i < numSegments; i++) {
segment = playlist.segments[i];
if (segment.end) {
if (segment.end > time) {
knownEnd = segment.end;
endIndex = i;
break;
} else {
knownStart = segment.end;
startIndex = i + 1;
}
}
}
// time was equal to or past the end of the last segment in the playlist
if (startIndex === numSegments) {
return numSegments;
}
// use the bounds we just found and playlist information to
// estimate the segment that contains the time we are looking for
if (typeof startIndex !== 'undefined') {
// We have a known-start point that is before our desired time so
// walk from that point forwards
time = time - knownStart;
for (i = startIndex; i < (endIndex || numSegments); i++) {
segment = playlist.segments[i];
time -= segment.duration;
if (time < 0) {
return i;
}
}
if (i >= endIndex) {
// We haven't found a segment but we did hit a known end point
// so fallback to interpolating between the segment index
// based on the known span of the timeline we are dealing with
// and the number of segments inside that span
return startIndex + Math.floor(
((originalTime - knownStart) / (knownEnd - knownStart)) *
(endIndex - startIndex));
}
// We _still_ haven't found a segment so load the last one
return lastSegment;
} else if (typeof endIndex !== 'undefined') {
// We _only_ have a known-end point that is after our desired time so
// walk from that point backwards
time = knownEnd - time;
for (i = endIndex; i >= 0; i--) {
segment = playlist.segments[i];
time -= segment.duration;
if (time < 0) {
return i;
}
}
// We haven't found a segment so load the first one if time is zero
if (time === 0) {
return 0;
}
return -1;
}
// We known nothing so walk from the front of the playlist,
// subtracting durations until we find a segment that contains
// time and return it
time = time - expired;
if (time < 0) {
return -1;
}
for (i = 0; i < numSegments; i++) {
segment = playlist.segments[i];
time -= segment.duration;
if (time < 0) {
return i;
}
}
// We are out of possible candidates so load the last one...
// The last one is the least likely to overlap a buffer and therefore
// the one most likely to tell us something about the timeline
return lastSegment;
};
Playlist.duration = duration;
Playlist.seekable = seekable;
Playlist.getMediaIndexForTime_ = getMediaIndexForTime_;
// exports
export default Playlist;