vtt-segment-loader.js 16 KB

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