123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605 |
- /**
- * @file sync-controller.js
- */
- 'use strict';
- Object.defineProperty(exports, '__esModule', {
- value: true
- });
- var _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; }; })();
- var _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); } } };
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
- function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
- function _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; }
- var _muxJsLibMp4Probe = require('mux.js/lib/mp4/probe');
- var _muxJsLibMp4Probe2 = _interopRequireDefault(_muxJsLibMp4Probe);
- var _muxJsLibToolsTsInspectorJs = require('mux.js/lib/tools/ts-inspector.js');
- var _playlist = require('./playlist');
- var _videoJs = require('video.js');
- var _videoJs2 = _interopRequireDefault(_videoJs);
- var syncPointStrategies = [
- // Stategy "VOD": Handle the VOD-case where the sync-point is *always*
- // the equivalence display-time 0 === segment-index 0
- {
- name: 'VOD',
- run: function run(syncController, playlist, duration, currentTimeline, currentTime) {
- if (duration !== Infinity) {
- var syncPoint = {
- time: 0,
- segmentIndex: 0
- };
- return syncPoint;
- }
- return null;
- }
- },
- // Stategy "ProgramDateTime": We have a program-date-time tag in this playlist
- {
- name: 'ProgramDateTime',
- run: function run(syncController, playlist, duration, currentTimeline, currentTime) {
- if (syncController.datetimeToDisplayTime && playlist.dateTimeObject) {
- var playlistTime = playlist.dateTimeObject.getTime() / 1000;
- var playlistStart = playlistTime + syncController.datetimeToDisplayTime;
- var syncPoint = {
- time: playlistStart,
- segmentIndex: 0
- };
- return syncPoint;
- }
- return null;
- }
- },
- // Stategy "Segment": We have a known time mapping for a timeline and a
- // segment in the current timeline with timing data
- {
- name: 'Segment',
- run: function run(syncController, playlist, duration, currentTimeline, currentTime) {
- var segments = playlist.segments || [];
- var syncPoint = null;
- var lastDistance = null;
- currentTime = currentTime || 0;
- for (var i = 0; i < segments.length; i++) {
- var segment = segments[i];
- if (segment.timeline === currentTimeline && typeof segment.start !== 'undefined') {
- var distance = Math.abs(currentTime - segment.start);
- // Once the distance begins to increase, we have passed
- // currentTime and can stop looking for better candidates
- if (lastDistance !== null && lastDistance < distance) {
- break;
- }
- if (!syncPoint || lastDistance === null || lastDistance >= distance) {
- lastDistance = distance;
- syncPoint = {
- time: segment.start,
- segmentIndex: i
- };
- }
- }
- }
- return syncPoint;
- }
- },
- // Stategy "Discontinuity": We have a discontinuity with a known
- // display-time
- {
- name: 'Discontinuity',
- run: function run(syncController, playlist, duration, currentTimeline, currentTime) {
- var syncPoint = null;
- currentTime = currentTime || 0;
- if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) {
- var lastDistance = null;
- for (var i = 0; i < playlist.discontinuityStarts.length; i++) {
- var segmentIndex = playlist.discontinuityStarts[i];
- var discontinuity = playlist.discontinuitySequence + i + 1;
- var discontinuitySync = syncController.discontinuities[discontinuity];
- if (discontinuitySync) {
- var distance = Math.abs(currentTime - discontinuitySync.time);
- // Once the distance begins to increase, we have passed
- // currentTime and can stop looking for better candidates
- if (lastDistance !== null && lastDistance < distance) {
- break;
- }
- if (!syncPoint || lastDistance === null || lastDistance >= distance) {
- lastDistance = distance;
- syncPoint = {
- time: discontinuitySync.time,
- segmentIndex: segmentIndex
- };
- }
- }
- }
- }
- return syncPoint;
- }
- },
- // Stategy "Playlist": We have a playlist with a known mapping of
- // segment index to display time
- {
- name: 'Playlist',
- run: function run(syncController, playlist, duration, currentTimeline, currentTime) {
- if (playlist.syncInfo) {
- var syncPoint = {
- time: playlist.syncInfo.time,
- segmentIndex: playlist.syncInfo.mediaSequence - playlist.mediaSequence
- };
- return syncPoint;
- }
- return null;
- }
- }];
- exports.syncPointStrategies = syncPointStrategies;
- var SyncController = (function (_videojs$EventTarget) {
- _inherits(SyncController, _videojs$EventTarget);
- function SyncController() {
- var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
- _classCallCheck(this, SyncController);
- _get(Object.getPrototypeOf(SyncController.prototype), 'constructor', this).call(this);
- // Segment Loader state variables...
- // ...for synching across variants
- this.inspectCache_ = undefined;
- // ...for synching across variants
- this.timelines = [];
- this.discontinuities = [];
- this.datetimeToDisplayTime = null;
- if (options.debug) {
- this.logger_ = _videoJs2['default'].log.bind(_videoJs2['default'], 'sync-controller ->');
- }
- }
- /**
- * Find a sync-point for the playlist specified
- *
- * A sync-point is defined as a known mapping from display-time to
- * a segment-index in the current playlist.
- *
- * @param {Playlist} playlist
- * The playlist that needs a sync-point
- * @param {Number} duration
- * Duration of the MediaSource (Infinite if playing a live source)
- * @param {Number} currentTimeline
- * The last timeline from which a segment was loaded
- * @returns {Object}
- * A sync-point object
- */
- _createClass(SyncController, [{
- key: 'getSyncPoint',
- value: function getSyncPoint(playlist, duration, currentTimeline, currentTime) {
- var syncPoints = this.runStrategies_(playlist, duration, currentTimeline, currentTime);
- if (!syncPoints.length) {
- // Signal that we need to attempt to get a sync-point manually
- // by fetching a segment in the playlist and constructing
- // a sync-point from that information
- return null;
- }
- // Now find the sync-point that is closest to the currentTime because
- // that should result in the most accurate guess about which segment
- // to fetch
- return this.selectSyncPoint_(syncPoints, { key: 'time', value: currentTime });
- }
- /**
- * Calculate the amount of time that has expired off the playlist during playback
- *
- * @param {Playlist} playlist
- * Playlist object to calculate expired from
- * @param {Number} duration
- * Duration of the MediaSource (Infinity if playling a live source)
- * @returns {Number|null}
- * The amount of time that has expired off the playlist during playback. Null
- * if no sync-points for the playlist can be found.
- */
- }, {
- key: 'getExpiredTime',
- value: function getExpiredTime(playlist, duration) {
- if (!playlist || !playlist.segments) {
- return null;
- }
- var syncPoints = this.runStrategies_(playlist, duration, playlist.discontinuitySequence, 0);
- // Without sync-points, there is not enough information to determine the expired time
- if (!syncPoints.length) {
- return null;
- }
- var syncPoint = this.selectSyncPoint_(syncPoints, {
- key: 'segmentIndex',
- value: 0
- });
- // If the sync-point is beyond the start of the playlist, we want to subtract the
- // duration from index 0 to syncPoint.segmentIndex instead of adding.
- if (syncPoint.segmentIndex > 0) {
- syncPoint.time *= -1;
- }
- return Math.abs(syncPoint.time + (0, _playlist.sumDurations)(playlist, syncPoint.segmentIndex, 0));
- }
- /**
- * Runs each sync-point strategy and returns a list of sync-points returned by the
- * strategies
- *
- * @private
- * @param {Playlist} playlist
- * The playlist that needs a sync-point
- * @param {Number} duration
- * Duration of the MediaSource (Infinity if playing a live source)
- * @param {Number} currentTimeline
- * The last timeline from which a segment was loaded
- * @returns {Array}
- * A list of sync-point objects
- */
- }, {
- key: 'runStrategies_',
- value: function runStrategies_(playlist, duration, currentTimeline, currentTime) {
- var syncPoints = [];
- // Try to find a sync-point in by utilizing various strategies...
- for (var i = 0; i < syncPointStrategies.length; i++) {
- var strategy = syncPointStrategies[i];
- var syncPoint = strategy.run(this, playlist, duration, currentTimeline, currentTime);
- if (syncPoint) {
- syncPoint.strategy = strategy.name;
- syncPoints.push({
- strategy: strategy.name,
- syncPoint: syncPoint
- });
- this.logger_('syncPoint found via <' + strategy.name + '>:', syncPoint);
- }
- }
- return syncPoints;
- }
- /**
- * Selects the sync-point nearest the specified target
- *
- * @private
- * @param {Array} syncPoints
- * List of sync-points to select from
- * @param {Object} target
- * Object specifying the property and value we are targeting
- * @param {String} target.key
- * Specifies the property to target. Must be either 'time' or 'segmentIndex'
- * @param {Number} target.value
- * The value to target for the specified key.
- * @returns {Object}
- * The sync-point nearest the target
- */
- }, {
- key: 'selectSyncPoint_',
- value: function selectSyncPoint_(syncPoints, target) {
- var bestSyncPoint = syncPoints[0].syncPoint;
- var bestDistance = Math.abs(syncPoints[0].syncPoint[target.key] - target.value);
- var bestStrategy = syncPoints[0].strategy;
- for (var i = 1; i < syncPoints.length; i++) {
- var newDistance = Math.abs(syncPoints[i].syncPoint[target.key] - target.value);
- if (newDistance < bestDistance) {
- bestDistance = newDistance;
- bestSyncPoint = syncPoints[i].syncPoint;
- bestStrategy = syncPoints[i].strategy;
- }
- }
- this.logger_('syncPoint with strategy <' + bestStrategy + '> chosen: ', bestSyncPoint);
- return bestSyncPoint;
- }
- /**
- * Save any meta-data present on the segments when segments leave
- * the live window to the playlist to allow for synchronization at the
- * playlist level later.
- *
- * @param {Playlist} oldPlaylist - The previous active playlist
- * @param {Playlist} newPlaylist - The updated and most current playlist
- */
- }, {
- key: 'saveExpiredSegmentInfo',
- value: function saveExpiredSegmentInfo(oldPlaylist, newPlaylist) {
- var mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence;
- // When a segment expires from the playlist and it has a start time
- // save that information as a possible sync-point reference in future
- for (var i = mediaSequenceDiff - 1; i >= 0; i--) {
- var lastRemovedSegment = oldPlaylist.segments[i];
- if (lastRemovedSegment && typeof lastRemovedSegment.start !== 'undefined') {
- newPlaylist.syncInfo = {
- mediaSequence: oldPlaylist.mediaSequence + i,
- time: lastRemovedSegment.start
- };
- this.logger_('playlist sync:', newPlaylist.syncInfo);
- this.trigger('syncinfoupdate');
- break;
- }
- }
- }
- /**
- * Save the mapping from playlist's ProgramDateTime to display. This should
- * only ever happen once at the start of playback.
- *
- * @param {Playlist} playlist - The currently active playlist
- */
- }, {
- key: 'setDateTimeMapping',
- value: function setDateTimeMapping(playlist) {
- if (!this.datetimeToDisplayTime && playlist.dateTimeObject) {
- var playlistTimestamp = playlist.dateTimeObject.getTime() / 1000;
- this.datetimeToDisplayTime = -playlistTimestamp;
- }
- }
- /**
- * Reset the state of the inspection cache when we do a rendition
- * switch
- */
- }, {
- key: 'reset',
- value: function reset() {
- this.inspectCache_ = undefined;
- }
- /**
- * Probe or inspect a fmp4 or an mpeg2-ts segment to determine the start
- * and end of the segment in it's internal "media time". Used to generate
- * mappings from that internal "media time" to the display time that is
- * shown on the player.
- *
- * @param {SegmentInfo} segmentInfo - The current active request information
- */
- }, {
- key: 'probeSegmentInfo',
- value: function probeSegmentInfo(segmentInfo) {
- var segment = segmentInfo.segment;
- var playlist = segmentInfo.playlist;
- var timingInfo = undefined;
- if (segment.map) {
- timingInfo = this.probeMp4Segment_(segmentInfo);
- } else {
- timingInfo = this.probeTsSegment_(segmentInfo);
- }
- if (timingInfo) {
- if (this.calculateSegmentTimeMapping_(segmentInfo, timingInfo)) {
- this.saveDiscontinuitySyncInfo_(segmentInfo);
- // If the playlist does not have sync information yet, record that information
- // now with segment timing information
- if (!playlist.syncInfo) {
- playlist.syncInfo = {
- mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex,
- time: segment.start
- };
- }
- }
- }
- return timingInfo;
- }
- /**
- * Probe an fmp4 or an mpeg2-ts segment to determine the start of the segment
- * in it's internal "media time".
- *
- * @private
- * @param {SegmentInfo} segmentInfo - The current active request information
- * @return {object} The start and end time of the current segment in "media time"
- */
- }, {
- key: 'probeMp4Segment_',
- value: function probeMp4Segment_(segmentInfo) {
- var segment = segmentInfo.segment;
- var timescales = _muxJsLibMp4Probe2['default'].timescale(segment.map.bytes);
- var startTime = _muxJsLibMp4Probe2['default'].startTime(timescales, segmentInfo.bytes);
- if (segmentInfo.timestampOffset !== null) {
- segmentInfo.timestampOffset -= startTime;
- }
- return {
- start: startTime,
- end: startTime + segment.duration
- };
- }
- /**
- * Probe an mpeg2-ts segment to determine the start and end of the segment
- * in it's internal "media time".
- *
- * @private
- * @param {SegmentInfo} segmentInfo - The current active request information
- * @return {object} The start and end time of the current segment in "media time"
- */
- }, {
- key: 'probeTsSegment_',
- value: function probeTsSegment_(segmentInfo) {
- var timeInfo = (0, _muxJsLibToolsTsInspectorJs.inspect)(segmentInfo.bytes, this.inspectCache_);
- var segmentStartTime = undefined;
- var segmentEndTime = undefined;
- if (!timeInfo) {
- return null;
- }
- if (timeInfo.video && timeInfo.video.length === 2) {
- this.inspectCache_ = timeInfo.video[1].dts;
- segmentStartTime = timeInfo.video[0].dtsTime;
- segmentEndTime = timeInfo.video[1].dtsTime;
- } else if (timeInfo.audio && timeInfo.audio.length === 2) {
- this.inspectCache_ = timeInfo.audio[1].dts;
- segmentStartTime = timeInfo.audio[0].dtsTime;
- segmentEndTime = timeInfo.audio[1].dtsTime;
- }
- return {
- start: segmentStartTime,
- end: segmentEndTime,
- containsVideo: timeInfo.video && timeInfo.video.length === 2,
- containsAudio: timeInfo.audio && timeInfo.audio.length === 2
- };
- }
- }, {
- key: 'timestampOffsetForTimeline',
- value: function timestampOffsetForTimeline(timeline) {
- if (typeof this.timelines[timeline] === 'undefined') {
- return null;
- }
- return this.timelines[timeline].time;
- }
- }, {
- key: 'mappingForTimeline',
- value: function mappingForTimeline(timeline) {
- if (typeof this.timelines[timeline] === 'undefined') {
- return null;
- }
- return this.timelines[timeline].mapping;
- }
- /**
- * Use the "media time" for a segment to generate a mapping to "display time" and
- * save that display time to the segment.
- *
- * @private
- * @param {SegmentInfo} segmentInfo
- * The current active request information
- * @param {object} timingInfo
- * The start and end time of the current segment in "media time"
- * @returns {Boolean}
- * Returns false if segment time mapping could not be calculated
- */
- }, {
- key: 'calculateSegmentTimeMapping_',
- value: function calculateSegmentTimeMapping_(segmentInfo, timingInfo) {
- var segment = segmentInfo.segment;
- var mappingObj = this.timelines[segmentInfo.timeline];
- if (segmentInfo.timestampOffset !== null) {
- this.logger_('tsO:', segmentInfo.timestampOffset);
- mappingObj = {
- time: segmentInfo.startOfSegment,
- mapping: segmentInfo.startOfSegment - timingInfo.start
- };
- this.timelines[segmentInfo.timeline] = mappingObj;
- this.trigger('timestampoffset');
- segment.start = segmentInfo.startOfSegment;
- segment.end = timingInfo.end + mappingObj.mapping;
- } else if (mappingObj) {
- segment.start = timingInfo.start + mappingObj.mapping;
- segment.end = timingInfo.end + mappingObj.mapping;
- } else {
- return false;
- }
- return true;
- }
- /**
- * Each time we have discontinuity in the playlist, attempt to calculate the location
- * in display of the start of the discontinuity and save that. We also save an accuracy
- * value so that we save values with the most accuracy (closest to 0.)
- *
- * @private
- * @param {SegmentInfo} segmentInfo - The current active request information
- */
- }, {
- key: 'saveDiscontinuitySyncInfo_',
- value: function saveDiscontinuitySyncInfo_(segmentInfo) {
- var playlist = segmentInfo.playlist;
- var segment = segmentInfo.segment;
- // If the current segment is a discontinuity then we know exactly where
- // the start of the range and it's accuracy is 0 (greater accuracy values
- // mean more approximation)
- if (segment.discontinuity) {
- this.discontinuities[segment.timeline] = {
- time: segment.start,
- accuracy: 0
- };
- } else if (playlist.discontinuityStarts.length) {
- // Search for future discontinuities that we can provide better timing
- // information for and save that information for sync purposes
- for (var i = 0; i < playlist.discontinuityStarts.length; i++) {
- var segmentIndex = playlist.discontinuityStarts[i];
- var discontinuity = playlist.discontinuitySequence + i + 1;
- var mediaIndexDiff = segmentIndex - segmentInfo.mediaIndex;
- var accuracy = Math.abs(mediaIndexDiff);
- if (!this.discontinuities[discontinuity] || this.discontinuities[discontinuity].accuracy > accuracy) {
- var time = undefined;
- if (mediaIndexDiff < 0) {
- time = segment.start - (0, _playlist.sumDurations)(playlist, segmentInfo.mediaIndex, segmentIndex);
- } else {
- time = segment.end + (0, _playlist.sumDurations)(playlist, segmentInfo.mediaIndex + 1, segmentIndex);
- }
- this.discontinuities[discontinuity] = {
- time: time,
- accuracy: accuracy
- };
- }
- }
- }
- }
- /**
- * A debugging logger noop that is set to console.log only if debugging
- * is enabled globally
- *
- * @private
- */
- }, {
- key: 'logger_',
- value: function logger_() {}
- }]);
- return SyncController;
- })(_videoJs2['default'].EventTarget);
- exports['default'] = SyncController;
|