You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
461 lines
16 KiB
461 lines
16 KiB
/**
|
|
* @file vtt-segment-loader.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(_x3, _x4, _x5) { var _again = true; _function: while (_again) { var object = _x3, property = _x4, receiver = _x5; _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 { _x3 = parent; _x4 = property; _x5 = 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 _segmentLoader = require('./segment-loader');
|
|
|
|
var _segmentLoader2 = _interopRequireDefault(_segmentLoader);
|
|
|
|
var _videoJs = require('video.js');
|
|
|
|
var _videoJs2 = _interopRequireDefault(_videoJs);
|
|
|
|
var _globalWindow = require('global/window');
|
|
|
|
var _globalWindow2 = _interopRequireDefault(_globalWindow);
|
|
|
|
var _videojsContribMediaSourcesEs5RemoveCuesFromTrackJs = require('videojs-contrib-media-sources/es5/remove-cues-from-track.js');
|
|
|
|
var _videojsContribMediaSourcesEs5RemoveCuesFromTrackJs2 = _interopRequireDefault(_videojsContribMediaSourcesEs5RemoveCuesFromTrackJs);
|
|
|
|
var _binUtils = require('./bin-utils');
|
|
|
|
var VTT_LINE_TERMINATORS = new Uint8Array('\n\n'.split('').map(function (char) {
|
|
return char.charCodeAt(0);
|
|
}));
|
|
|
|
var uintToString = function uintToString(uintArray) {
|
|
return String.fromCharCode.apply(null, uintArray);
|
|
};
|
|
|
|
/**
|
|
* An object that manages segment loading and appending.
|
|
*
|
|
* @class VTTSegmentLoader
|
|
* @param {Object} options required and optional options
|
|
* @extends videojs.EventTarget
|
|
*/
|
|
|
|
var VTTSegmentLoader = (function (_SegmentLoader) {
|
|
_inherits(VTTSegmentLoader, _SegmentLoader);
|
|
|
|
function VTTSegmentLoader(settings) {
|
|
var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
|
|
|
|
_classCallCheck(this, VTTSegmentLoader);
|
|
|
|
_get(Object.getPrototypeOf(VTTSegmentLoader.prototype), 'constructor', this).call(this, settings, options);
|
|
|
|
// SegmentLoader requires a MediaSource be specified or it will throw an error;
|
|
// however, VTTSegmentLoader has no need of a media source, so delete the reference
|
|
this.mediaSource_ = null;
|
|
|
|
this.subtitlesTrack_ = null;
|
|
}
|
|
|
|
/**
|
|
* Indicates which time ranges are buffered
|
|
*
|
|
* @return {TimeRange}
|
|
* TimeRange object representing the current buffered ranges
|
|
*/
|
|
|
|
_createClass(VTTSegmentLoader, [{
|
|
key: 'buffered_',
|
|
value: function buffered_() {
|
|
if (!this.subtitlesTrack_ || !this.subtitlesTrack_.cues.length) {
|
|
return _videoJs2['default'].createTimeRanges();
|
|
}
|
|
|
|
var cues = this.subtitlesTrack_.cues;
|
|
var start = cues[0].startTime;
|
|
var end = cues[cues.length - 1].startTime;
|
|
|
|
return _videoJs2['default'].createTimeRanges([[start, end]]);
|
|
}
|
|
|
|
/**
|
|
* Gets and sets init segment for the provided map
|
|
*
|
|
* @param {Object} map
|
|
* The map object representing the init segment to get or set
|
|
* @param {Boolean=} set
|
|
* If true, the init segment for the provided map should be saved
|
|
* @return {Object}
|
|
* map object for desired init segment
|
|
*/
|
|
}, {
|
|
key: 'initSegment',
|
|
value: function initSegment(map) {
|
|
var set = arguments.length <= 1 || arguments[1] === undefined ? false : arguments[1];
|
|
|
|
if (!map) {
|
|
return null;
|
|
}
|
|
|
|
var id = (0, _binUtils.initSegmentId)(map);
|
|
var storedMap = this.initSegments_[id];
|
|
|
|
if (set && !storedMap && map.bytes) {
|
|
// append WebVTT line terminators to the media initialization segment if it exists
|
|
// to follow the WebVTT spec (https://w3c.github.io/webvtt/#file-structure) that
|
|
// requires two or more WebVTT line terminators between the WebVTT header and the
|
|
// rest of the file
|
|
var combinedByteLength = VTT_LINE_TERMINATORS.byteLength + map.bytes.byteLength;
|
|
var combinedSegment = new Uint8Array(combinedByteLength);
|
|
|
|
combinedSegment.set(map.bytes);
|
|
combinedSegment.set(VTT_LINE_TERMINATORS, map.bytes.byteLength);
|
|
|
|
this.initSegments_[id] = storedMap = {
|
|
resolvedUri: map.resolvedUri,
|
|
byterange: map.byterange,
|
|
bytes: combinedSegment
|
|
};
|
|
}
|
|
|
|
return storedMap || map;
|
|
}
|
|
|
|
/**
|
|
* Returns true if all configuration required for loading is present, otherwise false.
|
|
*
|
|
* @return {Boolean} True if the all configuration is ready for loading
|
|
* @private
|
|
*/
|
|
}, {
|
|
key: 'couldBeginLoading_',
|
|
value: function couldBeginLoading_() {
|
|
return this.playlist_ && this.subtitlesTrack_ && !this.paused();
|
|
}
|
|
|
|
/**
|
|
* Once all the starting parameters have been specified, begin
|
|
* operation. This method should only be invoked from the INIT
|
|
* state.
|
|
*
|
|
* @private
|
|
*/
|
|
}, {
|
|
key: 'init_',
|
|
value: function init_() {
|
|
this.state = 'READY';
|
|
this.resetEverything();
|
|
return this.monitorBuffer_();
|
|
}
|
|
|
|
/**
|
|
* Set a subtitle track on the segment loader to add subtitles to
|
|
*
|
|
* @param {TextTrack=} track
|
|
* The text track to add loaded subtitles to
|
|
* @return {TextTrack}
|
|
* Returns the subtitles track
|
|
*/
|
|
}, {
|
|
key: 'track',
|
|
value: function track(_track) {
|
|
if (typeof _track === 'undefined') {
|
|
return this.subtitlesTrack_;
|
|
}
|
|
|
|
this.subtitlesTrack_ = _track;
|
|
|
|
// if we were unpaused but waiting for a sourceUpdater, start
|
|
// buffering now
|
|
if (this.state === 'INIT' && this.couldBeginLoading_()) {
|
|
this.init_();
|
|
}
|
|
|
|
return this.subtitlesTrack_;
|
|
}
|
|
|
|
/**
|
|
* Remove any data in the source buffer between start and end times
|
|
* @param {Number} start - the start time of the region to remove from the buffer
|
|
* @param {Number} end - the end time of the region to remove from the buffer
|
|
*/
|
|
}, {
|
|
key: 'remove',
|
|
value: function remove(start, end) {
|
|
(0, _videojsContribMediaSourcesEs5RemoveCuesFromTrackJs2['default'])(start, end, this.subtitlesTrack_);
|
|
}
|
|
|
|
/**
|
|
* fill the buffer with segements unless the sourceBuffers are
|
|
* currently updating
|
|
*
|
|
* Note: this function should only ever be called by monitorBuffer_
|
|
* and never directly
|
|
*
|
|
* @private
|
|
*/
|
|
}, {
|
|
key: 'fillBuffer_',
|
|
value: function fillBuffer_() {
|
|
var _this = this;
|
|
|
|
if (!this.syncPoint_) {
|
|
this.syncPoint_ = this.syncController_.getSyncPoint(this.playlist_, this.duration_(), this.currentTimeline_, this.currentTime_());
|
|
}
|
|
|
|
// see if we need to begin loading immediately
|
|
var segmentInfo = this.checkBuffer_(this.buffered_(), this.playlist_, this.mediaIndex, this.hasPlayed_(), this.currentTime_(), this.syncPoint_);
|
|
|
|
segmentInfo = this.skipEmptySegments_(segmentInfo);
|
|
|
|
if (!segmentInfo) {
|
|
return;
|
|
}
|
|
|
|
if (this.syncController_.timestampOffsetForTimeline(segmentInfo.timeline) === null) {
|
|
// We don't have the timestamp offset that we need to sync subtitles.
|
|
// Rerun on a timestamp offset or user interaction.
|
|
var checkTimestampOffset = function checkTimestampOffset() {
|
|
_this.state = 'READY';
|
|
if (!_this.paused()) {
|
|
// if not paused, queue a buffer check as soon as possible
|
|
_this.monitorBuffer_();
|
|
}
|
|
};
|
|
|
|
this.syncController_.one('timestampoffset', checkTimestampOffset);
|
|
this.state = 'WAITING_ON_TIMELINE';
|
|
return;
|
|
}
|
|
|
|
this.loadSegment_(segmentInfo);
|
|
}
|
|
|
|
/**
|
|
* Prevents the segment loader from requesting segments we know contain no subtitles
|
|
* by walking forward until we find the next segment that we don't know whether it is
|
|
* empty or not.
|
|
*
|
|
* @param {Object} segmentInfo
|
|
* a segment info object that describes the current segment
|
|
* @return {Object}
|
|
* a segment info object that describes the current segment
|
|
*/
|
|
}, {
|
|
key: 'skipEmptySegments_',
|
|
value: function skipEmptySegments_(segmentInfo) {
|
|
while (segmentInfo && segmentInfo.segment.empty) {
|
|
segmentInfo = this.generateSegmentInfo_(segmentInfo.playlist, segmentInfo.mediaIndex + 1, segmentInfo.startOfSegment + segmentInfo.duration, segmentInfo.isSyncRequest);
|
|
}
|
|
return segmentInfo;
|
|
}
|
|
|
|
/**
|
|
* append a decrypted segement to the SourceBuffer through a SourceUpdater
|
|
*
|
|
* @private
|
|
*/
|
|
}, {
|
|
key: 'handleSegment_',
|
|
value: function handleSegment_() {
|
|
var _this2 = this;
|
|
|
|
if (!this.pendingSegment_ || !this.subtitlesTrack_) {
|
|
this.state = 'READY';
|
|
return;
|
|
}
|
|
|
|
this.state = 'APPENDING';
|
|
|
|
var segmentInfo = this.pendingSegment_;
|
|
var segment = segmentInfo.segment;
|
|
|
|
// Make sure that vttjs has loaded, otherwise, wait till it finished loading
|
|
if (typeof _globalWindow2['default'].WebVTT !== 'function' && this.subtitlesTrack_ && this.subtitlesTrack_.tech_) {
|
|
var _ret = (function () {
|
|
|
|
var loadHandler = function loadHandler() {
|
|
_this2.handleSegment_();
|
|
};
|
|
|
|
_this2.state = 'WAITING_ON_VTTJS';
|
|
_this2.subtitlesTrack_.tech_.one('vttjsloaded', loadHandler);
|
|
_this2.subtitlesTrack_.tech_.one('vttjserror', function () {
|
|
_this2.subtitlesTrack_.tech_.off('vttjsloaded', loadHandler);
|
|
_this2.error({
|
|
message: 'Error loading vtt.js'
|
|
});
|
|
_this2.state = 'READY';
|
|
_this2.pause();
|
|
_this2.trigger('error');
|
|
});
|
|
|
|
return {
|
|
v: undefined
|
|
};
|
|
})();
|
|
|
|
if (typeof _ret === 'object') return _ret.v;
|
|
}
|
|
|
|
segment.requested = true;
|
|
|
|
try {
|
|
this.parseVTTCues_(segmentInfo);
|
|
} catch (e) {
|
|
this.error({
|
|
message: e.message
|
|
});
|
|
this.state = 'READY';
|
|
this.pause();
|
|
return this.trigger('error');
|
|
}
|
|
|
|
this.updateTimeMapping_(segmentInfo, this.syncController_.timelines[segmentInfo.timeline], this.playlist_);
|
|
|
|
if (segmentInfo.isSyncRequest) {
|
|
this.trigger('syncinfoupdate');
|
|
this.pendingSegment_ = null;
|
|
this.state = 'READY';
|
|
return;
|
|
}
|
|
|
|
segmentInfo.byteLength = segmentInfo.bytes.byteLength;
|
|
|
|
this.mediaSecondsLoaded += segment.duration;
|
|
|
|
if (segmentInfo.cues.length) {
|
|
// remove any overlapping cues to prevent doubling
|
|
this.remove(segmentInfo.cues[0].endTime, segmentInfo.cues[segmentInfo.cues.length - 1].endTime);
|
|
}
|
|
|
|
segmentInfo.cues.forEach(function (cue) {
|
|
_this2.subtitlesTrack_.addCue(cue);
|
|
});
|
|
|
|
this.handleUpdateEnd_();
|
|
}
|
|
|
|
/**
|
|
* Uses the WebVTT parser to parse the segment response
|
|
*
|
|
* @param {Object} segmentInfo
|
|
* a segment info object that describes the current segment
|
|
* @private
|
|
*/
|
|
}, {
|
|
key: 'parseVTTCues_',
|
|
value: function parseVTTCues_(segmentInfo) {
|
|
var decoder = undefined;
|
|
var decodeBytesToString = false;
|
|
|
|
if (typeof _globalWindow2['default'].TextDecoder === 'function') {
|
|
decoder = new _globalWindow2['default'].TextDecoder('utf8');
|
|
} else {
|
|
decoder = _globalWindow2['default'].WebVTT.StringDecoder();
|
|
decodeBytesToString = true;
|
|
}
|
|
|
|
var parser = new _globalWindow2['default'].WebVTT.Parser(_globalWindow2['default'], _globalWindow2['default'].vttjs, decoder);
|
|
|
|
segmentInfo.cues = [];
|
|
segmentInfo.timestampmap = { MPEGTS: 0, LOCAL: 0 };
|
|
|
|
parser.oncue = segmentInfo.cues.push.bind(segmentInfo.cues);
|
|
parser.ontimestampmap = function (map) {
|
|
return segmentInfo.timestampmap = map;
|
|
};
|
|
parser.onparsingerror = function (error) {
|
|
_videoJs2['default'].log.warn('Error encountered when parsing cues: ' + error.message);
|
|
};
|
|
|
|
if (segmentInfo.segment.map) {
|
|
var mapData = segmentInfo.segment.map.bytes;
|
|
|
|
if (decodeBytesToString) {
|
|
mapData = uintToString(mapData);
|
|
}
|
|
|
|
parser.parse(mapData);
|
|
}
|
|
|
|
var segmentData = segmentInfo.bytes;
|
|
|
|
if (decodeBytesToString) {
|
|
segmentData = uintToString(segmentData);
|
|
}
|
|
|
|
parser.parse(segmentData);
|
|
parser.flush();
|
|
}
|
|
|
|
/**
|
|
* Updates the start and end times of any cues parsed by the WebVTT parser using
|
|
* the information parsed from the X-TIMESTAMP-MAP header and a TS to media time mapping
|
|
* from the SyncController
|
|
*
|
|
* @param {Object} segmentInfo
|
|
* a segment info object that describes the current segment
|
|
* @param {Object} mappingObj
|
|
* object containing a mapping from TS to media time
|
|
* @param {Object} playlist
|
|
* the playlist object containing the segment
|
|
* @private
|
|
*/
|
|
}, {
|
|
key: 'updateTimeMapping_',
|
|
value: function updateTimeMapping_(segmentInfo, mappingObj, playlist) {
|
|
var segment = segmentInfo.segment;
|
|
|
|
if (!mappingObj) {
|
|
// If the sync controller does not have a mapping of TS to Media Time for the
|
|
// timeline, then we don't have enough information to update the cue
|
|
// start/end times
|
|
return;
|
|
}
|
|
|
|
if (!segmentInfo.cues.length) {
|
|
// If there are no cues, we also do not have enough information to figure out
|
|
// segment timing. Mark that the segment contains no cues so we don't re-request
|
|
// an empty segment.
|
|
segment.empty = true;
|
|
return;
|
|
}
|
|
|
|
var timestampmap = segmentInfo.timestampmap;
|
|
var diff = timestampmap.MPEGTS / 90000 - timestampmap.LOCAL + mappingObj.mapping;
|
|
|
|
segmentInfo.cues.forEach(function (cue) {
|
|
// First convert cue time to TS time using the timestamp-map provided within the vtt
|
|
cue.startTime += diff;
|
|
cue.endTime += diff;
|
|
});
|
|
|
|
if (!playlist.syncInfo) {
|
|
var firstStart = segmentInfo.cues[0].startTime;
|
|
var lastStart = segmentInfo.cues[segmentInfo.cues.length - 1].startTime;
|
|
|
|
playlist.syncInfo = {
|
|
mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex,
|
|
time: Math.min(firstStart, lastStart - segment.duration)
|
|
};
|
|
}
|
|
}
|
|
}]);
|
|
|
|
return VTTSegmentLoader;
|
|
})(_segmentLoader2['default']);
|
|
|
|
exports['default'] = VTTSegmentLoader;
|
|
module.exports = exports['default']; |