UNPKG

22 kBJavaScriptView Raw
1/**
2 * @file sync-controller.js
3 */
4
5'use strict';
6
7Object.defineProperty(exports, '__esModule', {
8 value: true
9});
10
11var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
12
13var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };
14
15function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
16
17function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
18
19function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
20
21var _muxJsLibMp4Probe = require('mux.js/lib/mp4/probe');
22
23var _muxJsLibMp4Probe2 = _interopRequireDefault(_muxJsLibMp4Probe);
24
25var _muxJsLibToolsTsInspectorJs = require('mux.js/lib/tools/ts-inspector.js');
26
27var _playlist = require('./playlist');
28
29var _videoJs = require('video.js');
30
31var _videoJs2 = _interopRequireDefault(_videoJs);
32
33var syncPointStrategies = [
34// Stategy "VOD": Handle the VOD-case where the sync-point is *always*
35// the equivalence display-time 0 === segment-index 0
36{
37 name: 'VOD',
38 run: function run(syncController, playlist, duration, currentTimeline, currentTime) {
39 if (duration !== Infinity) {
40 var syncPoint = {
41 time: 0,
42 segmentIndex: 0
43 };
44
45 return syncPoint;
46 }
47 return null;
48 }
49},
50// Stategy "ProgramDateTime": We have a program-date-time tag in this playlist
51{
52 name: 'ProgramDateTime',
53 run: function run(syncController, playlist, duration, currentTimeline, currentTime) {
54 if (syncController.datetimeToDisplayTime && playlist.dateTimeObject) {
55 var playlistTime = playlist.dateTimeObject.getTime() / 1000;
56 var playlistStart = playlistTime + syncController.datetimeToDisplayTime;
57 var syncPoint = {
58 time: playlistStart,
59 segmentIndex: 0
60 };
61
62 return syncPoint;
63 }
64 return null;
65 }
66},
67// Stategy "Segment": We have a known time mapping for a timeline and a
68// segment in the current timeline with timing data
69{
70 name: 'Segment',
71 run: function run(syncController, playlist, duration, currentTimeline, currentTime) {
72 var segments = playlist.segments || [];
73 var syncPoint = null;
74 var lastDistance = null;
75
76 currentTime = currentTime || 0;
77
78 for (var i = 0; i < segments.length; i++) {
79 var segment = segments[i];
80
81 if (segment.timeline === currentTimeline && typeof segment.start !== 'undefined') {
82 var distance = Math.abs(currentTime - segment.start);
83
84 // Once the distance begins to increase, we have passed
85 // currentTime and can stop looking for better candidates
86 if (lastDistance !== null && lastDistance < distance) {
87 break;
88 }
89
90 if (!syncPoint || lastDistance === null || lastDistance >= distance) {
91 lastDistance = distance;
92 syncPoint = {
93 time: segment.start,
94 segmentIndex: i
95 };
96 }
97 }
98 }
99 return syncPoint;
100 }
101},
102// Stategy "Discontinuity": We have a discontinuity with a known
103// display-time
104{
105 name: 'Discontinuity',
106 run: function run(syncController, playlist, duration, currentTimeline, currentTime) {
107 var syncPoint = null;
108
109 currentTime = currentTime || 0;
110
111 if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) {
112 var lastDistance = null;
113
114 for (var i = 0; i < playlist.discontinuityStarts.length; i++) {
115 var segmentIndex = playlist.discontinuityStarts[i];
116 var discontinuity = playlist.discontinuitySequence + i + 1;
117 var discontinuitySync = syncController.discontinuities[discontinuity];
118
119 if (discontinuitySync) {
120 var distance = Math.abs(currentTime - discontinuitySync.time);
121
122 // Once the distance begins to increase, we have passed
123 // currentTime and can stop looking for better candidates
124 if (lastDistance !== null && lastDistance < distance) {
125 break;
126 }
127
128 if (!syncPoint || lastDistance === null || lastDistance >= distance) {
129 lastDistance = distance;
130 syncPoint = {
131 time: discontinuitySync.time,
132 segmentIndex: segmentIndex
133 };
134 }
135 }
136 }
137 }
138 return syncPoint;
139 }
140},
141// Stategy "Playlist": We have a playlist with a known mapping of
142// segment index to display time
143{
144 name: 'Playlist',
145 run: function run(syncController, playlist, duration, currentTimeline, currentTime) {
146 if (playlist.syncInfo) {
147 var syncPoint = {
148 time: playlist.syncInfo.time,
149 segmentIndex: playlist.syncInfo.mediaSequence - playlist.mediaSequence
150 };
151
152 return syncPoint;
153 }
154 return null;
155 }
156}];
157
158exports.syncPointStrategies = syncPointStrategies;
159
160var SyncController = (function (_videojs$EventTarget) {
161 _inherits(SyncController, _videojs$EventTarget);
162
163 function SyncController() {
164 var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
165
166 _classCallCheck(this, SyncController);
167
168 _get(Object.getPrototypeOf(SyncController.prototype), 'constructor', this).call(this);
169 // Segment Loader state variables...
170 // ...for synching across variants
171 this.inspectCache_ = undefined;
172
173 // ...for synching across variants
174 this.timelines = [];
175 this.discontinuities = [];
176 this.datetimeToDisplayTime = null;
177
178 if (options.debug) {
179 this.logger_ = _videoJs2['default'].log.bind(_videoJs2['default'], 'sync-controller ->');
180 }
181 }
182
183 /**
184 * Find a sync-point for the playlist specified
185 *
186 * A sync-point is defined as a known mapping from display-time to
187 * a segment-index in the current playlist.
188 *
189 * @param {Playlist} playlist
190 * The playlist that needs a sync-point
191 * @param {Number} duration
192 * Duration of the MediaSource (Infinite if playing a live source)
193 * @param {Number} currentTimeline
194 * The last timeline from which a segment was loaded
195 * @returns {Object}
196 * A sync-point object
197 */
198
199 _createClass(SyncController, [{
200 key: 'getSyncPoint',
201 value: function getSyncPoint(playlist, duration, currentTimeline, currentTime) {
202 var syncPoints = this.runStrategies_(playlist, duration, currentTimeline, currentTime);
203
204 if (!syncPoints.length) {
205 // Signal that we need to attempt to get a sync-point manually
206 // by fetching a segment in the playlist and constructing
207 // a sync-point from that information
208 return null;
209 }
210
211 // Now find the sync-point that is closest to the currentTime because
212 // that should result in the most accurate guess about which segment
213 // to fetch
214 return this.selectSyncPoint_(syncPoints, { key: 'time', value: currentTime });
215 }
216
217 /**
218 * Calculate the amount of time that has expired off the playlist during playback
219 *
220 * @param {Playlist} playlist
221 * Playlist object to calculate expired from
222 * @param {Number} duration
223 * Duration of the MediaSource (Infinity if playling a live source)
224 * @returns {Number|null}
225 * The amount of time that has expired off the playlist during playback. Null
226 * if no sync-points for the playlist can be found.
227 */
228 }, {
229 key: 'getExpiredTime',
230 value: function getExpiredTime(playlist, duration) {
231 if (!playlist || !playlist.segments) {
232 return null;
233 }
234
235 var syncPoints = this.runStrategies_(playlist, duration, playlist.discontinuitySequence, 0);
236
237 // Without sync-points, there is not enough information to determine the expired time
238 if (!syncPoints.length) {
239 return null;
240 }
241
242 var syncPoint = this.selectSyncPoint_(syncPoints, {
243 key: 'segmentIndex',
244 value: 0
245 });
246
247 // If the sync-point is beyond the start of the playlist, we want to subtract the
248 // duration from index 0 to syncPoint.segmentIndex instead of adding.
249 if (syncPoint.segmentIndex > 0) {
250 syncPoint.time *= -1;
251 }
252
253 return Math.abs(syncPoint.time + (0, _playlist.sumDurations)(playlist, syncPoint.segmentIndex, 0));
254 }
255
256 /**
257 * Runs each sync-point strategy and returns a list of sync-points returned by the
258 * strategies
259 *
260 * @private
261 * @param {Playlist} playlist
262 * The playlist that needs a sync-point
263 * @param {Number} duration
264 * Duration of the MediaSource (Infinity if playing a live source)
265 * @param {Number} currentTimeline
266 * The last timeline from which a segment was loaded
267 * @returns {Array}
268 * A list of sync-point objects
269 */
270 }, {
271 key: 'runStrategies_',
272 value: function runStrategies_(playlist, duration, currentTimeline, currentTime) {
273 var syncPoints = [];
274
275 // Try to find a sync-point in by utilizing various strategies...
276 for (var i = 0; i < syncPointStrategies.length; i++) {
277 var strategy = syncPointStrategies[i];
278 var syncPoint = strategy.run(this, playlist, duration, currentTimeline, currentTime);
279
280 if (syncPoint) {
281 syncPoint.strategy = strategy.name;
282 syncPoints.push({
283 strategy: strategy.name,
284 syncPoint: syncPoint
285 });
286 this.logger_('syncPoint found via <' + strategy.name + '>:', syncPoint);
287 }
288 }
289
290 return syncPoints;
291 }
292
293 /**
294 * Selects the sync-point nearest the specified target
295 *
296 * @private
297 * @param {Array} syncPoints
298 * List of sync-points to select from
299 * @param {Object} target
300 * Object specifying the property and value we are targeting
301 * @param {String} target.key
302 * Specifies the property to target. Must be either 'time' or 'segmentIndex'
303 * @param {Number} target.value
304 * The value to target for the specified key.
305 * @returns {Object}
306 * The sync-point nearest the target
307 */
308 }, {
309 key: 'selectSyncPoint_',
310 value: function selectSyncPoint_(syncPoints, target) {
311 var bestSyncPoint = syncPoints[0].syncPoint;
312 var bestDistance = Math.abs(syncPoints[0].syncPoint[target.key] - target.value);
313 var bestStrategy = syncPoints[0].strategy;
314
315 for (var i = 1; i < syncPoints.length; i++) {
316 var newDistance = Math.abs(syncPoints[i].syncPoint[target.key] - target.value);
317
318 if (newDistance < bestDistance) {
319 bestDistance = newDistance;
320 bestSyncPoint = syncPoints[i].syncPoint;
321 bestStrategy = syncPoints[i].strategy;
322 }
323 }
324
325 this.logger_('syncPoint with strategy <' + bestStrategy + '> chosen: ', bestSyncPoint);
326 return bestSyncPoint;
327 }
328
329 /**
330 * Save any meta-data present on the segments when segments leave
331 * the live window to the playlist to allow for synchronization at the
332 * playlist level later.
333 *
334 * @param {Playlist} oldPlaylist - The previous active playlist
335 * @param {Playlist} newPlaylist - The updated and most current playlist
336 */
337 }, {
338 key: 'saveExpiredSegmentInfo',
339 value: function saveExpiredSegmentInfo(oldPlaylist, newPlaylist) {
340 var mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence;
341
342 // When a segment expires from the playlist and it has a start time
343 // save that information as a possible sync-point reference in future
344 for (var i = mediaSequenceDiff - 1; i >= 0; i--) {
345 var lastRemovedSegment = oldPlaylist.segments[i];
346
347 if (lastRemovedSegment && typeof lastRemovedSegment.start !== 'undefined') {
348 newPlaylist.syncInfo = {
349 mediaSequence: oldPlaylist.mediaSequence + i,
350 time: lastRemovedSegment.start
351 };
352 this.logger_('playlist sync:', newPlaylist.syncInfo);
353 this.trigger('syncinfoupdate');
354 break;
355 }
356 }
357 }
358
359 /**
360 * Save the mapping from playlist's ProgramDateTime to display. This should
361 * only ever happen once at the start of playback.
362 *
363 * @param {Playlist} playlist - The currently active playlist
364 */
365 }, {
366 key: 'setDateTimeMapping',
367 value: function setDateTimeMapping(playlist) {
368 if (!this.datetimeToDisplayTime && playlist.dateTimeObject) {
369 var playlistTimestamp = playlist.dateTimeObject.getTime() / 1000;
370
371 this.datetimeToDisplayTime = -playlistTimestamp;
372 }
373 }
374
375 /**
376 * Reset the state of the inspection cache when we do a rendition
377 * switch
378 */
379 }, {
380 key: 'reset',
381 value: function reset() {
382 this.inspectCache_ = undefined;
383 }
384
385 /**
386 * Probe or inspect a fmp4 or an mpeg2-ts segment to determine the start
387 * and end of the segment in it's internal "media time". Used to generate
388 * mappings from that internal "media time" to the display time that is
389 * shown on the player.
390 *
391 * @param {SegmentInfo} segmentInfo - The current active request information
392 */
393 }, {
394 key: 'probeSegmentInfo',
395 value: function probeSegmentInfo(segmentInfo) {
396 var segment = segmentInfo.segment;
397 var playlist = segmentInfo.playlist;
398 var timingInfo = undefined;
399
400 if (segment.map) {
401 timingInfo = this.probeMp4Segment_(segmentInfo);
402 } else {
403 timingInfo = this.probeTsSegment_(segmentInfo);
404 }
405
406 if (timingInfo) {
407 if (this.calculateSegmentTimeMapping_(segmentInfo, timingInfo)) {
408 this.saveDiscontinuitySyncInfo_(segmentInfo);
409
410 // If the playlist does not have sync information yet, record that information
411 // now with segment timing information
412 if (!playlist.syncInfo) {
413 playlist.syncInfo = {
414 mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex,
415 time: segment.start
416 };
417 }
418 }
419 }
420
421 return timingInfo;
422 }
423
424 /**
425 * Probe an fmp4 or an mpeg2-ts segment to determine the start of the segment
426 * in it's internal "media time".
427 *
428 * @private
429 * @param {SegmentInfo} segmentInfo - The current active request information
430 * @return {object} The start and end time of the current segment in "media time"
431 */
432 }, {
433 key: 'probeMp4Segment_',
434 value: function probeMp4Segment_(segmentInfo) {
435 var segment = segmentInfo.segment;
436 var timescales = _muxJsLibMp4Probe2['default'].timescale(segment.map.bytes);
437 var startTime = _muxJsLibMp4Probe2['default'].startTime(timescales, segmentInfo.bytes);
438
439 if (segmentInfo.timestampOffset !== null) {
440 segmentInfo.timestampOffset -= startTime;
441 }
442
443 return {
444 start: startTime,
445 end: startTime + segment.duration
446 };
447 }
448
449 /**
450 * Probe an mpeg2-ts segment to determine the start and end of the segment
451 * in it's internal "media time".
452 *
453 * @private
454 * @param {SegmentInfo} segmentInfo - The current active request information
455 * @return {object} The start and end time of the current segment in "media time"
456 */
457 }, {
458 key: 'probeTsSegment_',
459 value: function probeTsSegment_(segmentInfo) {
460 var timeInfo = (0, _muxJsLibToolsTsInspectorJs.inspect)(segmentInfo.bytes, this.inspectCache_);
461 var segmentStartTime = undefined;
462 var segmentEndTime = undefined;
463
464 if (!timeInfo) {
465 return null;
466 }
467
468 if (timeInfo.video && timeInfo.video.length === 2) {
469 this.inspectCache_ = timeInfo.video[1].dts;
470 segmentStartTime = timeInfo.video[0].dtsTime;
471 segmentEndTime = timeInfo.video[1].dtsTime;
472 } else if (timeInfo.audio && timeInfo.audio.length === 2) {
473 this.inspectCache_ = timeInfo.audio[1].dts;
474 segmentStartTime = timeInfo.audio[0].dtsTime;
475 segmentEndTime = timeInfo.audio[1].dtsTime;
476 }
477
478 return {
479 start: segmentStartTime,
480 end: segmentEndTime,
481 containsVideo: timeInfo.video && timeInfo.video.length === 2,
482 containsAudio: timeInfo.audio && timeInfo.audio.length === 2
483 };
484 }
485 }, {
486 key: 'timestampOffsetForTimeline',
487 value: function timestampOffsetForTimeline(timeline) {
488 if (typeof this.timelines[timeline] === 'undefined') {
489 return null;
490 }
491 return this.timelines[timeline].time;
492 }
493 }, {
494 key: 'mappingForTimeline',
495 value: function mappingForTimeline(timeline) {
496 if (typeof this.timelines[timeline] === 'undefined') {
497 return null;
498 }
499 return this.timelines[timeline].mapping;
500 }
501
502 /**
503 * Use the "media time" for a segment to generate a mapping to "display time" and
504 * save that display time to the segment.
505 *
506 * @private
507 * @param {SegmentInfo} segmentInfo
508 * The current active request information
509 * @param {object} timingInfo
510 * The start and end time of the current segment in "media time"
511 * @returns {Boolean}
512 * Returns false if segment time mapping could not be calculated
513 */
514 }, {
515 key: 'calculateSegmentTimeMapping_',
516 value: function calculateSegmentTimeMapping_(segmentInfo, timingInfo) {
517 var segment = segmentInfo.segment;
518 var mappingObj = this.timelines[segmentInfo.timeline];
519
520 if (segmentInfo.timestampOffset !== null) {
521 this.logger_('tsO:', segmentInfo.timestampOffset);
522
523 mappingObj = {
524 time: segmentInfo.startOfSegment,
525 mapping: segmentInfo.startOfSegment - timingInfo.start
526 };
527 this.timelines[segmentInfo.timeline] = mappingObj;
528 this.trigger('timestampoffset');
529
530 segment.start = segmentInfo.startOfSegment;
531 segment.end = timingInfo.end + mappingObj.mapping;
532 } else if (mappingObj) {
533 segment.start = timingInfo.start + mappingObj.mapping;
534 segment.end = timingInfo.end + mappingObj.mapping;
535 } else {
536 return false;
537 }
538
539 return true;
540 }
541
542 /**
543 * Each time we have discontinuity in the playlist, attempt to calculate the location
544 * in display of the start of the discontinuity and save that. We also save an accuracy
545 * value so that we save values with the most accuracy (closest to 0.)
546 *
547 * @private
548 * @param {SegmentInfo} segmentInfo - The current active request information
549 */
550 }, {
551 key: 'saveDiscontinuitySyncInfo_',
552 value: function saveDiscontinuitySyncInfo_(segmentInfo) {
553 var playlist = segmentInfo.playlist;
554 var segment = segmentInfo.segment;
555
556 // If the current segment is a discontinuity then we know exactly where
557 // the start of the range and it's accuracy is 0 (greater accuracy values
558 // mean more approximation)
559 if (segment.discontinuity) {
560 this.discontinuities[segment.timeline] = {
561 time: segment.start,
562 accuracy: 0
563 };
564 } else if (playlist.discontinuityStarts.length) {
565 // Search for future discontinuities that we can provide better timing
566 // information for and save that information for sync purposes
567 for (var i = 0; i < playlist.discontinuityStarts.length; i++) {
568 var segmentIndex = playlist.discontinuityStarts[i];
569 var discontinuity = playlist.discontinuitySequence + i + 1;
570 var mediaIndexDiff = segmentIndex - segmentInfo.mediaIndex;
571 var accuracy = Math.abs(mediaIndexDiff);
572
573 if (!this.discontinuities[discontinuity] || this.discontinuities[discontinuity].accuracy > accuracy) {
574 var time = undefined;
575
576 if (mediaIndexDiff < 0) {
577 time = segment.start - (0, _playlist.sumDurations)(playlist, segmentInfo.mediaIndex, segmentIndex);
578 } else {
579 time = segment.end + (0, _playlist.sumDurations)(playlist, segmentInfo.mediaIndex + 1, segmentIndex);
580 }
581
582 this.discontinuities[discontinuity] = {
583 time: time,
584 accuracy: accuracy
585 };
586 }
587 }
588 }
589 }
590
591 /**
592 * A debugging logger noop that is set to console.log only if debugging
593 * is enabled globally
594 *
595 * @private
596 */
597 }, {
598 key: 'logger_',
599 value: function logger_() {}
600 }]);
601
602 return SyncController;
603})(_videoJs2['default'].EventTarget);
604
605exports['default'] = SyncController;
\No newline at end of file