UNPKG

15.6 kBJavaScriptView Raw
1/**
2 * @file vtt-segment-loader.js
3 */
4'use strict';
5
6Object.defineProperty(exports, '__esModule', {
7 value: true
8});
9
10var _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; }; })();
11
12var _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); } } };
13
14function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
15
16function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
17
18function _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; }
19
20var _segmentLoader = require('./segment-loader');
21
22var _segmentLoader2 = _interopRequireDefault(_segmentLoader);
23
24var _videoJs = require('video.js');
25
26var _videoJs2 = _interopRequireDefault(_videoJs);
27
28var _globalWindow = require('global/window');
29
30var _globalWindow2 = _interopRequireDefault(_globalWindow);
31
32var _videojsContribMediaSourcesEs5RemoveCuesFromTrackJs = require('videojs-contrib-media-sources/es5/remove-cues-from-track.js');
33
34var _videojsContribMediaSourcesEs5RemoveCuesFromTrackJs2 = _interopRequireDefault(_videojsContribMediaSourcesEs5RemoveCuesFromTrackJs);
35
36var _binUtils = require('./bin-utils');
37
38var VTT_LINE_TERMINATORS = new Uint8Array('\n\n'.split('').map(function (char) {
39 return char.charCodeAt(0);
40}));
41
42var uintToString = function uintToString(uintArray) {
43 return String.fromCharCode.apply(null, uintArray);
44};
45
46/**
47 * An object that manages segment loading and appending.
48 *
49 * @class VTTSegmentLoader
50 * @param {Object} options required and optional options
51 * @extends videojs.EventTarget
52 */
53
54var VTTSegmentLoader = (function (_SegmentLoader) {
55 _inherits(VTTSegmentLoader, _SegmentLoader);
56
57 function VTTSegmentLoader(settings) {
58 var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
59
60 _classCallCheck(this, VTTSegmentLoader);
61
62 _get(Object.getPrototypeOf(VTTSegmentLoader.prototype), 'constructor', this).call(this, settings, options);
63
64 // SegmentLoader requires a MediaSource be specified or it will throw an error;
65 // however, VTTSegmentLoader has no need of a media source, so delete the reference
66 this.mediaSource_ = null;
67
68 this.subtitlesTrack_ = null;
69 }
70
71 /**
72 * Indicates which time ranges are buffered
73 *
74 * @return {TimeRange}
75 * TimeRange object representing the current buffered ranges
76 */
77
78 _createClass(VTTSegmentLoader, [{
79 key: 'buffered_',
80 value: function buffered_() {
81 if (!this.subtitlesTrack_ || !this.subtitlesTrack_.cues.length) {
82 return _videoJs2['default'].createTimeRanges();
83 }
84
85 var cues = this.subtitlesTrack_.cues;
86 var start = cues[0].startTime;
87 var end = cues[cues.length - 1].startTime;
88
89 return _videoJs2['default'].createTimeRanges([[start, end]]);
90 }
91
92 /**
93 * Gets and sets init segment for the provided map
94 *
95 * @param {Object} map
96 * The map object representing the init segment to get or set
97 * @param {Boolean=} set
98 * If true, the init segment for the provided map should be saved
99 * @return {Object}
100 * map object for desired init segment
101 */
102 }, {
103 key: 'initSegment',
104 value: function initSegment(map) {
105 var set = arguments.length <= 1 || arguments[1] === undefined ? false : arguments[1];
106
107 if (!map) {
108 return null;
109 }
110
111 var id = (0, _binUtils.initSegmentId)(map);
112 var storedMap = this.initSegments_[id];
113
114 if (set && !storedMap && map.bytes) {
115 // append WebVTT line terminators to the media initialization segment if it exists
116 // to follow the WebVTT spec (https://w3c.github.io/webvtt/#file-structure) that
117 // requires two or more WebVTT line terminators between the WebVTT header and the
118 // rest of the file
119 var combinedByteLength = VTT_LINE_TERMINATORS.byteLength + map.bytes.byteLength;
120 var combinedSegment = new Uint8Array(combinedByteLength);
121
122 combinedSegment.set(map.bytes);
123 combinedSegment.set(VTT_LINE_TERMINATORS, map.bytes.byteLength);
124
125 this.initSegments_[id] = storedMap = {
126 resolvedUri: map.resolvedUri,
127 byterange: map.byterange,
128 bytes: combinedSegment
129 };
130 }
131
132 return storedMap || map;
133 }
134
135 /**
136 * Returns true if all configuration required for loading is present, otherwise false.
137 *
138 * @return {Boolean} True if the all configuration is ready for loading
139 * @private
140 */
141 }, {
142 key: 'couldBeginLoading_',
143 value: function couldBeginLoading_() {
144 return this.playlist_ && this.subtitlesTrack_ && !this.paused();
145 }
146
147 /**
148 * Once all the starting parameters have been specified, begin
149 * operation. This method should only be invoked from the INIT
150 * state.
151 *
152 * @private
153 */
154 }, {
155 key: 'init_',
156 value: function init_() {
157 this.state = 'READY';
158 this.resetEverything();
159 return this.monitorBuffer_();
160 }
161
162 /**
163 * Set a subtitle track on the segment loader to add subtitles to
164 *
165 * @param {TextTrack} track
166 * The text track to add loaded subtitles to
167 */
168 }, {
169 key: 'track',
170 value: function track(_track) {
171 this.subtitlesTrack_ = _track;
172
173 // if we were unpaused but waiting for a sourceUpdater, start
174 // buffering now
175 if (this.state === 'INIT' && this.couldBeginLoading_()) {
176 this.init_();
177 }
178 }
179
180 /**
181 * Remove any data in the source buffer between start and end times
182 * @param {Number} start - the start time of the region to remove from the buffer
183 * @param {Number} end - the end time of the region to remove from the buffer
184 */
185 }, {
186 key: 'remove',
187 value: function remove(start, end) {
188 (0, _videojsContribMediaSourcesEs5RemoveCuesFromTrackJs2['default'])(start, end, this.subtitlesTrack_);
189 }
190
191 /**
192 * fill the buffer with segements unless the sourceBuffers are
193 * currently updating
194 *
195 * Note: this function should only ever be called by monitorBuffer_
196 * and never directly
197 *
198 * @private
199 */
200 }, {
201 key: 'fillBuffer_',
202 value: function fillBuffer_() {
203 var _this = this;
204
205 if (!this.syncPoint_) {
206 this.syncPoint_ = this.syncController_.getSyncPoint(this.playlist_, this.duration_(), this.currentTimeline_, this.currentTime_());
207 }
208
209 // see if we need to begin loading immediately
210 var segmentInfo = this.checkBuffer_(this.buffered_(), this.playlist_, this.mediaIndex, this.hasPlayed_(), this.currentTime_(), this.syncPoint_);
211
212 segmentInfo = this.skipEmptySegments_(segmentInfo);
213
214 if (!segmentInfo) {
215 return;
216 }
217
218 if (this.syncController_.timestampOffsetForTimeline(segmentInfo.timeline) === null) {
219 // We don't have the timestamp offset that we need to sync subtitles.
220 // Rerun on a timestamp offset or user interaction.
221 var checkTimestampOffset = function checkTimestampOffset() {
222 _this.state = 'READY';
223 if (!_this.paused()) {
224 // if not paused, queue a buffer check as soon as possible
225 _this.monitorBuffer_();
226 }
227 };
228
229 this.syncController_.one('timestampoffset', checkTimestampOffset);
230 this.state = 'WAITING_ON_TIMELINE';
231 return;
232 }
233
234 this.loadSegment_(segmentInfo);
235 }
236
237 /**
238 * Prevents the segment loader from requesting segments we know contain no subtitles
239 * by walking forward until we find the next segment that we don't know whether it is
240 * empty or not.
241 *
242 * @param {Object} segmentInfo
243 * a segment info object that describes the current segment
244 * @return {Object}
245 * a segment info object that describes the current segment
246 */
247 }, {
248 key: 'skipEmptySegments_',
249 value: function skipEmptySegments_(segmentInfo) {
250 while (segmentInfo && segmentInfo.segment.empty) {
251 segmentInfo = this.generateSegmentInfo_(segmentInfo.playlist, segmentInfo.mediaIndex + 1, segmentInfo.startOfSegment + segmentInfo.duration, segmentInfo.isSyncRequest);
252 }
253 return segmentInfo;
254 }
255
256 /**
257 * append a decrypted segement to the SourceBuffer through a SourceUpdater
258 *
259 * @private
260 */
261 }, {
262 key: 'handleSegment_',
263 value: function handleSegment_() {
264 var _this2 = this;
265
266 if (!this.pendingSegment_) {
267 this.state = 'READY';
268 return;
269 }
270
271 this.state = 'APPENDING';
272
273 var segmentInfo = this.pendingSegment_;
274 var segment = segmentInfo.segment;
275
276 // Make sure that vttjs has loaded, otherwise, wait till it finished loading
277 if (typeof _globalWindow2['default'].WebVTT !== 'function' && this.subtitlesTrack_ && this.subtitlesTrack_.tech_) {
278 var _ret = (function () {
279
280 var loadHandler = function loadHandler() {
281 _this2.handleSegment_();
282 };
283
284 _this2.state = 'WAITING_ON_VTTJS';
285 _this2.subtitlesTrack_.tech_.one('vttjsloaded', loadHandler);
286 _this2.subtitlesTrack_.tech_.one('vttjserror', function () {
287 _this2.subtitlesTrack_.tech_.off('vttjsloaded', loadHandler);
288 _this2.error({
289 message: 'Error loading vtt.js'
290 });
291 _this2.state = 'READY';
292 _this2.pause();
293 _this2.trigger('error');
294 });
295
296 return {
297 v: undefined
298 };
299 })();
300
301 if (typeof _ret === 'object') return _ret.v;
302 }
303
304 segment.requested = true;
305
306 try {
307 this.parseVTTCues_(segmentInfo);
308 } catch (e) {
309 this.error({
310 message: e.message
311 });
312 this.state = 'READY';
313 this.pause();
314 return this.trigger('error');
315 }
316
317 this.updateTimeMapping_(segmentInfo, this.syncController_.timelines[segmentInfo.timeline], this.playlist_);
318
319 if (segmentInfo.isSyncRequest) {
320 this.trigger('syncinfoupdate');
321 this.pendingSegment_ = null;
322 this.state = 'READY';
323 return;
324 }
325
326 segmentInfo.byteLength = segmentInfo.bytes.byteLength;
327
328 this.mediaSecondsLoaded += segment.duration;
329
330 segmentInfo.cues.forEach(function (cue) {
331 _this2.subtitlesTrack_.addCue(cue);
332 });
333
334 this.handleUpdateEnd_();
335 }
336
337 /**
338 * Uses the WebVTT parser to parse the segment response
339 *
340 * @param {Object} segmentInfo
341 * a segment info object that describes the current segment
342 * @private
343 */
344 }, {
345 key: 'parseVTTCues_',
346 value: function parseVTTCues_(segmentInfo) {
347 var decoder = undefined;
348 var decodeBytesToString = false;
349
350 if (typeof _globalWindow2['default'].TextDecoder === 'function') {
351 decoder = new _globalWindow2['default'].TextDecoder('utf8');
352 } else {
353 decoder = _globalWindow2['default'].WebVTT.StringDecoder();
354 decodeBytesToString = true;
355 }
356
357 var parser = new _globalWindow2['default'].WebVTT.Parser(_globalWindow2['default'], _globalWindow2['default'].vttjs, decoder);
358
359 segmentInfo.cues = [];
360 segmentInfo.timestampmap = { MPEGTS: 0, LOCAL: 0 };
361
362 parser.oncue = segmentInfo.cues.push.bind(segmentInfo.cues);
363 parser.ontimestampmap = function (map) {
364 return segmentInfo.timestampmap = map;
365 };
366 parser.onparsingerror = function (error) {
367 _videoJs2['default'].log.warn('Error encountered when parsing cues: ' + error.message);
368 };
369
370 if (segmentInfo.segment.map) {
371 var mapData = segmentInfo.segment.map.bytes;
372
373 if (decodeBytesToString) {
374 mapData = uintToString(mapData);
375 }
376
377 parser.parse(mapData);
378 }
379
380 var segmentData = segmentInfo.bytes;
381
382 if (decodeBytesToString) {
383 segmentData = uintToString(segmentData);
384 }
385
386 parser.parse(segmentData);
387 parser.flush();
388 }
389
390 /**
391 * Updates the start and end times of any cues parsed by the WebVTT parser using
392 * the information parsed from the X-TIMESTAMP-MAP header and a TS to media time mapping
393 * from the SyncController
394 *
395 * @param {Object} segmentInfo
396 * a segment info object that describes the current segment
397 * @param {Object} mappingObj
398 * object containing a mapping from TS to media time
399 * @param {Object} playlist
400 * the playlist object containing the segment
401 * @private
402 */
403 }, {
404 key: 'updateTimeMapping_',
405 value: function updateTimeMapping_(segmentInfo, mappingObj, playlist) {
406 var segment = segmentInfo.segment;
407
408 if (!mappingObj) {
409 // If the sync controller does not have a mapping of TS to Media Time for the
410 // timeline, then we don't have enough information to update the cue
411 // start/end times
412 return;
413 }
414
415 if (!segmentInfo.cues.length) {
416 // If there are no cues, we also do not have enough information to figure out
417 // segment timing. Mark that the segment contains no cues so we don't re-request
418 // an empty segment.
419 segment.empty = true;
420 return;
421 }
422
423 var timestampmap = segmentInfo.timestampmap;
424 var diff = timestampmap.MPEGTS / 90000 - timestampmap.LOCAL + mappingObj.mapping;
425
426 segmentInfo.cues.forEach(function (cue) {
427 // First convert cue time to TS time using the timestamp-map provided within the vtt
428 cue.startTime += diff;
429 cue.endTime += diff;
430 });
431
432 if (!playlist.syncInfo) {
433 var firstStart = segmentInfo.cues[0].startTime;
434 var lastStart = segmentInfo.cues[segmentInfo.cues.length - 1].startTime;
435
436 playlist.syncInfo = {
437 mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex,
438 time: Math.min(firstStart, lastStart - segment.duration)
439 };
440 }
441 }
442 }]);
443
444 return VTTSegmentLoader;
445})(_segmentLoader2['default']);
446
447exports['default'] = VTTSegmentLoader;
448module.exports = exports['default'];
\No newline at end of file