sync-controller.js 21 KB

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