Source: lib/mss/mss_parser.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.mss.MssParser');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.Deprecate');
  9. goog.require('shaka.abr.Ewma');
  10. goog.require('shaka.log');
  11. goog.require('shaka.media.InitSegmentReference');
  12. goog.require('shaka.media.ManifestParser');
  13. goog.require('shaka.media.PresentationTimeline');
  14. goog.require('shaka.media.QualityObserver');
  15. goog.require('shaka.media.SegmentIndex');
  16. goog.require('shaka.media.SegmentReference');
  17. goog.require('shaka.mss.ContentProtection');
  18. goog.require('shaka.net.NetworkingEngine');
  19. goog.require('shaka.transmuxer.Mp4Generator');
  20. goog.require('shaka.util.Error');
  21. goog.require('shaka.util.LanguageUtils');
  22. goog.require('shaka.util.ManifestParserUtils');
  23. goog.require('shaka.util.MimeUtils');
  24. goog.require('shaka.util.OperationManager');
  25. goog.require('shaka.util.PlayerConfiguration');
  26. goog.require('shaka.util.Timer');
  27. goog.require('shaka.util.TXml');
  28. goog.require('shaka.util.XmlUtils');
  29. /**
  30. * Creates a new MSS parser.
  31. *
  32. * @implements {shaka.extern.ManifestParser}
  33. * @export
  34. */
  35. shaka.mss.MssParser = class {
  36. /** Creates a new MSS parser. */
  37. constructor() {
  38. /** @private {?shaka.extern.ManifestConfiguration} */
  39. this.config_ = null;
  40. /** @private {?shaka.extern.ManifestParser.PlayerInterface} */
  41. this.playerInterface_ = null;
  42. /** @private {!Array<string>} */
  43. this.manifestUris_ = [];
  44. /** @private {?shaka.extern.Manifest} */
  45. this.manifest_ = null;
  46. /** @private {number} */
  47. this.globalId_ = 1;
  48. /**
  49. * The update period in seconds, or 0 for no updates.
  50. * @private {number}
  51. */
  52. this.updatePeriod_ = 0;
  53. /** @private {?shaka.media.PresentationTimeline} */
  54. this.presentationTimeline_ = null;
  55. /**
  56. * An ewma that tracks how long updates take.
  57. * This is to mitigate issues caused by slow parsing on embedded devices.
  58. * @private {!shaka.abr.Ewma}
  59. */
  60. this.averageUpdateDuration_ = new shaka.abr.Ewma(5);
  61. /** @private {shaka.util.Timer} */
  62. this.updateTimer_ = new shaka.util.Timer(() => {
  63. this.onUpdate_();
  64. });
  65. /** @private {!shaka.util.OperationManager} */
  66. this.operationManager_ = new shaka.util.OperationManager();
  67. /**
  68. * @private {!Map<number, !BufferSource>}
  69. */
  70. this.initSegmentDataByStreamId_ = new Map();
  71. /** @private {function():boolean} */
  72. this.isPreloadFn_ = () => false;
  73. }
  74. /**
  75. * @param {shaka.extern.ManifestConfiguration} config
  76. * @param {(function():boolean)=} isPreloadFn
  77. * @override
  78. * @exportInterface
  79. */
  80. configure(config, isPreloadFn) {
  81. goog.asserts.assert(config.mss != null,
  82. 'MssManifestConfiguration should not be null!');
  83. this.config_ = config;
  84. if (isPreloadFn) {
  85. this.isPreloadFn_ = isPreloadFn;
  86. }
  87. }
  88. /**
  89. * @override
  90. * @exportInterface
  91. */
  92. async start(uri, playerInterface) {
  93. goog.asserts.assert(this.config_, 'Must call configure() before start()!');
  94. this.manifestUris_ = [uri];
  95. this.playerInterface_ = playerInterface;
  96. await this.requestManifest_();
  97. // Make sure that the parser has not been destroyed.
  98. if (!this.playerInterface_) {
  99. throw new shaka.util.Error(
  100. shaka.util.Error.Severity.CRITICAL,
  101. shaka.util.Error.Category.PLAYER,
  102. shaka.util.Error.Code.OPERATION_ABORTED);
  103. }
  104. this.setUpdateTimer_();
  105. goog.asserts.assert(this.manifest_, 'Manifest should be non-null!');
  106. return this.manifest_;
  107. }
  108. /**
  109. * Called when the update timer ticks.
  110. *
  111. * @return {!Promise}
  112. * @private
  113. */
  114. async onUpdate_() {
  115. goog.asserts.assert(this.updatePeriod_ >= 0,
  116. 'There should be an update period');
  117. shaka.log.info('Updating manifest...');
  118. try {
  119. await this.requestManifest_();
  120. } catch (error) {
  121. goog.asserts.assert(error instanceof shaka.util.Error,
  122. 'Should only receive a Shaka error');
  123. // Try updating again, but ensure we haven't been destroyed.
  124. if (this.playerInterface_) {
  125. // We will retry updating, so override the severity of the error.
  126. error.severity = shaka.util.Error.Severity.RECOVERABLE;
  127. this.playerInterface_.onError(error);
  128. }
  129. }
  130. // Detect a call to stop()
  131. if (!this.playerInterface_) {
  132. return;
  133. }
  134. this.setUpdateTimer_();
  135. }
  136. /**
  137. * Sets the update timer. Does nothing if the manifest is not live.
  138. *
  139. * @private
  140. */
  141. setUpdateTimer_() {
  142. if (this.updatePeriod_ <= 0) {
  143. return;
  144. }
  145. const finalDelay = Math.max(
  146. shaka.mss.MssParser.MIN_UPDATE_PERIOD_,
  147. this.updatePeriod_,
  148. this.averageUpdateDuration_.getEstimate());
  149. // We do not run the timer as repeating because part of update is async and
  150. // we need schedule the update after it finished.
  151. this.updateTimer_.tickAfter(/* seconds= */ finalDelay);
  152. }
  153. /**
  154. * @override
  155. * @exportInterface
  156. */
  157. stop() {
  158. this.playerInterface_ = null;
  159. this.config_ = null;
  160. this.manifestUris_ = [];
  161. this.manifest_ = null;
  162. if (this.updateTimer_ != null) {
  163. this.updateTimer_.stop();
  164. this.updateTimer_ = null;
  165. }
  166. this.initSegmentDataByStreamId_.clear();
  167. return this.operationManager_.destroy();
  168. }
  169. /**
  170. * @override
  171. * @exportInterface
  172. */
  173. async update() {
  174. try {
  175. await this.requestManifest_();
  176. } catch (error) {
  177. if (!this.playerInterface_ || !error) {
  178. return;
  179. }
  180. goog.asserts.assert(error instanceof shaka.util.Error, 'Bad error type');
  181. this.playerInterface_.onError(error);
  182. }
  183. }
  184. /**
  185. * @override
  186. * @exportInterface
  187. */
  188. onExpirationUpdated(sessionId, expiration) {
  189. // No-op
  190. }
  191. /**
  192. * @override
  193. * @exportInterface
  194. */
  195. onInitialVariantChosen(variant) {
  196. // No-op
  197. }
  198. /**
  199. * @override
  200. * @exportInterface
  201. */
  202. banLocation(uri) {
  203. // No-op
  204. }
  205. /** @override */
  206. setMediaElement(mediaElement) {
  207. // No-op
  208. }
  209. /**
  210. * Makes a network request for the manifest and parses the resulting data.
  211. *
  212. * @private
  213. */
  214. async requestManifest_() {
  215. const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
  216. const type = shaka.net.NetworkingEngine.AdvancedRequestType.MSS;
  217. const request = shaka.net.NetworkingEngine.makeRequest(
  218. this.manifestUris_, this.config_.retryParameters);
  219. const networkingEngine = this.playerInterface_.networkingEngine;
  220. const startTime = Date.now();
  221. const operation = networkingEngine.request(requestType, request, {
  222. type,
  223. isPreload: this.isPreloadFn_(),
  224. });
  225. this.operationManager_.manage(operation);
  226. const response = await operation.promise;
  227. // Detect calls to stop().
  228. if (!this.playerInterface_) {
  229. return;
  230. }
  231. // For redirections add the response uri to the first entry in the
  232. // Manifest Uris array.
  233. if (response.uri && !this.manifestUris_.includes(response.uri)) {
  234. this.manifestUris_.unshift(response.uri);
  235. }
  236. // This may throw, but it will result in a failed promise.
  237. this.parseManifest_(response.data, response.uri);
  238. // Keep track of how long the longest manifest update took.
  239. const endTime = Date.now();
  240. const updateDuration = (endTime - startTime) / 1000.0;
  241. this.averageUpdateDuration_.sample(1, updateDuration);
  242. }
  243. /**
  244. * Parses the manifest XML. This also handles updates and will update the
  245. * stored manifest.
  246. *
  247. * @param {BufferSource} data
  248. * @param {string} finalManifestUri The final manifest URI, which may
  249. * differ from this.manifestUri_ if there has been a redirect.
  250. * @return {!Promise}
  251. * @private
  252. */
  253. parseManifest_(data, finalManifestUri) {
  254. let manifestData = data;
  255. const manifestPreprocessor = this.config_.mss.manifestPreprocessor;
  256. const defaultManifestPreprocessor =
  257. shaka.util.PlayerConfiguration.defaultManifestPreprocessor;
  258. if (manifestPreprocessor != defaultManifestPreprocessor) {
  259. shaka.Deprecate.deprecateFeature(5,
  260. 'manifest.mss.manifestPreprocessor configuration',
  261. 'Please Use manifest.mss.manifestPreprocessorTXml instead.');
  262. const mssElement =
  263. shaka.util.XmlUtils.parseXml(manifestData, 'SmoothStreamingMedia');
  264. if (!mssElement) {
  265. throw new shaka.util.Error(
  266. shaka.util.Error.Severity.CRITICAL,
  267. shaka.util.Error.Category.MANIFEST,
  268. shaka.util.Error.Code.MSS_INVALID_XML,
  269. finalManifestUri);
  270. }
  271. manifestPreprocessor(mssElement);
  272. manifestData = shaka.util.XmlUtils.toArrayBuffer(mssElement);
  273. }
  274. const mss = shaka.util.TXml.parseXml(manifestData, 'SmoothStreamingMedia');
  275. if (!mss) {
  276. throw new shaka.util.Error(
  277. shaka.util.Error.Severity.CRITICAL,
  278. shaka.util.Error.Category.MANIFEST,
  279. shaka.util.Error.Code.MSS_INVALID_XML,
  280. finalManifestUri);
  281. }
  282. const manifestPreprocessorTXml = this.config_.mss.manifestPreprocessorTXml;
  283. const defaultManifestPreprocessorTXml =
  284. shaka.util.PlayerConfiguration.defaultManifestPreprocessorTXml;
  285. if (manifestPreprocessorTXml != defaultManifestPreprocessorTXml) {
  286. manifestPreprocessorTXml(mss);
  287. }
  288. this.processManifest_(mss, finalManifestUri);
  289. return Promise.resolve();
  290. }
  291. /**
  292. * Takes a formatted MSS and converts it into a manifest.
  293. *
  294. * @param {!shaka.extern.xml.Node} mss
  295. * @param {string} finalManifestUri The final manifest URI, which may
  296. * differ from this.manifestUri_ if there has been a redirect.
  297. * @private
  298. */
  299. processManifest_(mss, finalManifestUri) {
  300. const TXml = shaka.util.TXml;
  301. if (!this.presentationTimeline_) {
  302. this.presentationTimeline_ = new shaka.media.PresentationTimeline(
  303. /* presentationStartTime= */ null, /* delay= */ 0);
  304. }
  305. const isLive = TXml.parseAttr(mss, 'IsLive',
  306. TXml.parseBoolean, /* defaultValue= */ false);
  307. if (isLive) {
  308. throw new shaka.util.Error(
  309. shaka.util.Error.Severity.CRITICAL,
  310. shaka.util.Error.Category.MANIFEST,
  311. shaka.util.Error.Code.MSS_LIVE_CONTENT_NOT_SUPPORTED);
  312. }
  313. this.presentationTimeline_.setStatic(!isLive);
  314. const timescale = TXml.parseAttr(mss, 'TimeScale',
  315. TXml.parseNonNegativeInt, shaka.mss.MssParser.DEFAULT_TIME_SCALE_);
  316. goog.asserts.assert(timescale && timescale >= 0,
  317. 'Timescale must be defined!');
  318. let dvrWindowLength = TXml.parseAttr(mss, 'DVRWindowLength',
  319. TXml.parseNonNegativeInt);
  320. // If the DVRWindowLength field is omitted for a live presentation or set
  321. // to 0, the DVR window is effectively infinite
  322. if (isLive && (dvrWindowLength === 0 || isNaN(dvrWindowLength))) {
  323. dvrWindowLength = Infinity;
  324. }
  325. // Start-over
  326. const canSeek = TXml.parseAttr(mss, 'CanSeek',
  327. TXml.parseBoolean, /* defaultValue= */ false);
  328. if (dvrWindowLength === 0 && canSeek) {
  329. dvrWindowLength = Infinity;
  330. }
  331. let segmentAvailabilityDuration = null;
  332. if (dvrWindowLength && dvrWindowLength > 0) {
  333. segmentAvailabilityDuration = dvrWindowLength / timescale;
  334. }
  335. // If it's live, we check for an override.
  336. if (isLive && !isNaN(this.config_.availabilityWindowOverride)) {
  337. segmentAvailabilityDuration = this.config_.availabilityWindowOverride;
  338. }
  339. // If it's null, that means segments are always available. This is always
  340. // the case for VOD, and sometimes the case for live.
  341. if (segmentAvailabilityDuration == null) {
  342. segmentAvailabilityDuration = Infinity;
  343. }
  344. this.presentationTimeline_.setSegmentAvailabilityDuration(
  345. segmentAvailabilityDuration);
  346. // Duration in timescale units.
  347. const duration = TXml.parseAttr(mss, 'Duration',
  348. TXml.parseNonNegativeInt, Infinity);
  349. goog.asserts.assert(duration && duration >= 0,
  350. 'Duration must be defined!');
  351. if (!isLive) {
  352. this.presentationTimeline_.setDuration(duration / timescale);
  353. }
  354. /** @type {!shaka.mss.MssParser.Context} */
  355. const context = {
  356. variants: [],
  357. textStreams: [],
  358. timescale: timescale,
  359. duration: duration / timescale,
  360. };
  361. this.parseStreamIndexes_(mss, context);
  362. // These steps are not done on manifest update.
  363. if (!this.manifest_) {
  364. this.manifest_ = {
  365. presentationTimeline: this.presentationTimeline_,
  366. variants: context.variants,
  367. textStreams: context.textStreams,
  368. imageStreams: [],
  369. offlineSessionIds: [],
  370. sequenceMode: this.config_.mss.sequenceMode,
  371. ignoreManifestTimestampsInSegmentsMode: false,
  372. type: shaka.media.ManifestParser.MSS,
  373. serviceDescription: null,
  374. nextUrl: null,
  375. periodCount: 1,
  376. gapCount: 0,
  377. isLowLatency: false,
  378. startTime: null,
  379. };
  380. // This is the first point where we have a meaningful presentation start
  381. // time, and we need to tell PresentationTimeline that so that it can
  382. // maintain consistency from here on.
  383. this.presentationTimeline_.lockStartTime();
  384. } else {
  385. // Just update the variants and text streams.
  386. this.manifest_.variants = context.variants;
  387. this.manifest_.textStreams = context.textStreams;
  388. // Re-filter the manifest. This will check any configured restrictions on
  389. // new variants, and will pass any new init data to DrmEngine to ensure
  390. // that key rotation works correctly.
  391. this.playerInterface_.filter(this.manifest_);
  392. }
  393. }
  394. /**
  395. * @param {!shaka.extern.xml.Node} mss
  396. * @param {!shaka.mss.MssParser.Context} context
  397. * @private
  398. */
  399. parseStreamIndexes_(mss, context) {
  400. const ContentProtection = shaka.mss.ContentProtection;
  401. const TXml = shaka.util.TXml;
  402. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  403. const protectionElements = TXml.findChildren(mss, 'Protection');
  404. const drmInfos = ContentProtection.parseFromProtection(
  405. protectionElements, this.config_.mss.keySystemsBySystemId);
  406. const audioStreams = [];
  407. const videoStreams = [];
  408. const textStreams = [];
  409. const streamIndexes = TXml.findChildren(mss, 'StreamIndex');
  410. for (const streamIndex of streamIndexes) {
  411. const qualityLevels = TXml.findChildren(streamIndex, 'QualityLevel');
  412. const timeline = this.createTimeline_(
  413. streamIndex, context.timescale, context.duration);
  414. // For each QualityLevel node, create a stream element
  415. for (const qualityLevel of qualityLevels) {
  416. const stream = this.createStream_(
  417. streamIndex, qualityLevel, timeline, drmInfos, context);
  418. if (!stream) {
  419. // Skip unsupported stream
  420. continue;
  421. }
  422. if (stream.type == ContentType.AUDIO &&
  423. !this.config_.disableAudio) {
  424. audioStreams.push(stream);
  425. } else if (stream.type == ContentType.VIDEO &&
  426. !this.config_.disableVideo) {
  427. videoStreams.push(stream);
  428. } else if (stream.type == ContentType.TEXT &&
  429. !this.config_.disableText) {
  430. textStreams.push(stream);
  431. }
  432. }
  433. }
  434. const variants = [];
  435. for (const audio of (audioStreams.length > 0 ? audioStreams : [null])) {
  436. for (const video of (videoStreams.length > 0 ? videoStreams : [null])) {
  437. variants.push(this.createVariant_(audio, video));
  438. }
  439. }
  440. context.variants = variants;
  441. context.textStreams = textStreams;
  442. }
  443. /**
  444. * @param {!shaka.extern.xml.Node} streamIndex
  445. * @param {!shaka.extern.xml.Node} qualityLevel
  446. * @param {!Array<shaka.mss.MssParser.TimeRange>} timeline
  447. * @param {!Array<shaka.extern.DrmInfo>} drmInfos
  448. * @param {!shaka.mss.MssParser.Context} context
  449. * @return {?shaka.extern.Stream}
  450. * @private
  451. */
  452. createStream_(streamIndex, qualityLevel, timeline, drmInfos, context) {
  453. const TXml = shaka.util.TXml;
  454. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  455. const MssParser = shaka.mss.MssParser;
  456. const type = streamIndex.attributes['Type'];
  457. const isValidType = type === 'audio' || type === 'video' ||
  458. type === 'text';
  459. if (!isValidType) {
  460. shaka.log.alwaysWarn('Ignoring unrecognized type:', type);
  461. return null;
  462. }
  463. const lang = streamIndex.attributes['Language'];
  464. const id = this.globalId_++;
  465. const bandwidth = TXml.parseAttr(
  466. qualityLevel, 'Bitrate', TXml.parsePositiveInt);
  467. const width = TXml.parseAttr(
  468. qualityLevel, 'MaxWidth', TXml.parsePositiveInt);
  469. const height = TXml.parseAttr(
  470. qualityLevel, 'MaxHeight', TXml.parsePositiveInt);
  471. const channelsCount = TXml.parseAttr(
  472. qualityLevel, 'Channels', TXml.parsePositiveInt);
  473. const audioSamplingRate = TXml.parseAttr(
  474. qualityLevel, 'SamplingRate', TXml.parsePositiveInt);
  475. let duration = context.duration;
  476. if (timeline.length) {
  477. const start = timeline[0].start;
  478. const end = timeline[timeline.length - 1].end;
  479. duration = end - start;
  480. }
  481. const presentationDuration = this.presentationTimeline_.getDuration();
  482. this.presentationTimeline_.setDuration(
  483. Math.min(duration, presentationDuration));
  484. /** @type {!shaka.extern.Stream} */
  485. const stream = {
  486. id: id,
  487. originalId: streamIndex.attributes['Name'] || String(id),
  488. groupId: null,
  489. createSegmentIndex: () => Promise.resolve(),
  490. closeSegmentIndex: () => Promise.resolve(),
  491. segmentIndex: null,
  492. mimeType: '',
  493. codecs: '',
  494. frameRate: undefined,
  495. pixelAspectRatio: undefined,
  496. bandwidth: bandwidth || 0,
  497. width: width || undefined,
  498. height: height || undefined,
  499. kind: '',
  500. encrypted: drmInfos.length > 0,
  501. drmInfos: drmInfos,
  502. keyIds: new Set(),
  503. language: shaka.util.LanguageUtils.normalize(lang || 'und'),
  504. originalLanguage: lang,
  505. label: '',
  506. type: '',
  507. primary: false,
  508. trickModeVideo: null,
  509. dependencyStream: null,
  510. emsgSchemeIdUris: [],
  511. roles: [],
  512. forced: false,
  513. channelsCount: channelsCount,
  514. audioSamplingRate: audioSamplingRate,
  515. spatialAudio: false,
  516. closedCaptions: null,
  517. hdr: undefined,
  518. colorGamut: undefined,
  519. videoLayout: undefined,
  520. tilesLayout: undefined,
  521. matchedStreams: [],
  522. mssPrivateData: {
  523. duration: duration,
  524. timescale: context.timescale,
  525. codecPrivateData: null,
  526. },
  527. accessibilityPurpose: null,
  528. external: false,
  529. fastSwitching: false,
  530. fullMimeTypes: new Set(),
  531. isAudioMuxedInVideo: false,
  532. baseOriginalId: null,
  533. };
  534. // This is specifically for text tracks.
  535. const subType = streamIndex.attributes['Subtype'];
  536. if (subType) {
  537. const role = MssParser.ROLE_MAPPING_.get(subType);
  538. if (role) {
  539. stream.roles.push(role);
  540. }
  541. if (role === 'main') {
  542. stream.primary = true;
  543. }
  544. }
  545. let fourCCValue = qualityLevel.attributes['FourCC'];
  546. // If FourCC not defined at QualityLevel level,
  547. // then get it from StreamIndex level
  548. if (fourCCValue === null || fourCCValue === '') {
  549. fourCCValue = streamIndex.attributes['FourCC'];
  550. }
  551. // If still not defined (optional for audio stream,
  552. // see https://msdn.microsoft.com/en-us/library/ff728116%28v=vs.95%29.aspx),
  553. // then we consider the stream is an audio AAC stream
  554. if (!fourCCValue) {
  555. if (type === 'audio') {
  556. fourCCValue = 'AAC';
  557. } else if (type === 'video') {
  558. shaka.log.alwaysWarn('FourCC is not defined whereas it is required ' +
  559. 'for a QualityLevel element for a StreamIndex of type "video"');
  560. return null;
  561. }
  562. }
  563. // Check if codec is supported
  564. if (!MssParser.SUPPORTED_CODECS_.includes(fourCCValue.toUpperCase())) {
  565. shaka.log.alwaysWarn('Codec not supported:', fourCCValue);
  566. return null;
  567. }
  568. const codecPrivateData = this.getCodecPrivateData_(
  569. qualityLevel, type, fourCCValue, stream);
  570. stream.mssPrivateData.codecPrivateData = codecPrivateData;
  571. switch (type) {
  572. case 'audio':
  573. if (!codecPrivateData) {
  574. shaka.log.alwaysWarn('Quality unsupported without CodecPrivateData',
  575. type);
  576. return null;
  577. }
  578. stream.type = ContentType.AUDIO;
  579. // This mimetype is fake to allow the transmuxing.
  580. stream.mimeType = 'mss/audio/mp4';
  581. stream.codecs = this.getAACCodec_(
  582. qualityLevel, fourCCValue, codecPrivateData);
  583. break;
  584. case 'video':
  585. if (!codecPrivateData) {
  586. shaka.log.alwaysWarn('Quality unsupported without CodecPrivateData',
  587. type);
  588. return null;
  589. }
  590. stream.type = ContentType.VIDEO;
  591. // This mimetype is fake to allow the transmuxing.
  592. stream.mimeType = 'mss/video/mp4';
  593. stream.codecs = this.getH264Codec_(
  594. qualityLevel, codecPrivateData);
  595. break;
  596. case 'text':
  597. stream.type = ContentType.TEXT;
  598. stream.mimeType = 'application/mp4';
  599. if (fourCCValue === 'TTML' || fourCCValue === 'DFXP') {
  600. stream.codecs = 'stpp';
  601. }
  602. break;
  603. }
  604. stream.fullMimeTypes.add(shaka.util.MimeUtils.getFullType(
  605. stream.mimeType, stream.codecs));
  606. // Lazy-Load the segment index to avoid create all init segment at the
  607. // same time
  608. stream.createSegmentIndex = () => {
  609. if (stream.segmentIndex) {
  610. return Promise.resolve();
  611. }
  612. let initSegmentData;
  613. if (this.initSegmentDataByStreamId_.has(stream.id)) {
  614. initSegmentData = this.initSegmentDataByStreamId_.get(stream.id);
  615. } else {
  616. let videoNalus = [];
  617. if (stream.type == ContentType.VIDEO) {
  618. const codecPrivateData = stream.mssPrivateData.codecPrivateData;
  619. videoNalus = codecPrivateData.split('00000001').slice(1);
  620. }
  621. /** @type {shaka.transmuxer.Mp4Generator.StreamInfo} */
  622. const streamInfo = {
  623. id: stream.id,
  624. type: stream.type,
  625. codecs: stream.codecs,
  626. encrypted: stream.encrypted,
  627. timescale: stream.mssPrivateData.timescale,
  628. duration: stream.mssPrivateData.duration,
  629. videoNalus: videoNalus,
  630. audioConfig: new Uint8Array([]),
  631. videoConfig: new Uint8Array([]),
  632. hSpacing: 0,
  633. vSpacing: 0,
  634. data: null, // Data is not necessary for init segment.
  635. stream: stream,
  636. };
  637. const mp4Generator = new shaka.transmuxer.Mp4Generator([streamInfo]);
  638. initSegmentData = mp4Generator.initSegment();
  639. this.initSegmentDataByStreamId_.set(stream.id, initSegmentData);
  640. }
  641. const qualityInfo =
  642. shaka.media.QualityObserver.createQualityInfo(stream);
  643. const initSegmentRef = new shaka.media.InitSegmentReference(
  644. () => [],
  645. /* startByte= */ 0,
  646. /* endByte= */ null,
  647. qualityInfo,
  648. stream.mssPrivateData.timescale,
  649. initSegmentData,
  650. /* aesKey= */ null,
  651. stream.encrypted);
  652. const segments = this.createSegments_(initSegmentRef,
  653. stream, streamIndex, timeline);
  654. stream.segmentIndex = new shaka.media.SegmentIndex(segments);
  655. return Promise.resolve();
  656. };
  657. stream.closeSegmentIndex = () => {
  658. // If we have a segment index, release it.
  659. if (stream.segmentIndex) {
  660. stream.segmentIndex.release();
  661. stream.segmentIndex = null;
  662. }
  663. };
  664. return stream;
  665. }
  666. /**
  667. * @param {!shaka.extern.xml.Node} qualityLevel
  668. * @param {string} type
  669. * @param {string} fourCCValue
  670. * @param {!shaka.extern.Stream} stream
  671. * @return {?string}
  672. * @private
  673. */
  674. getCodecPrivateData_(qualityLevel, type, fourCCValue, stream) {
  675. const codecPrivateData = qualityLevel.attributes['CodecPrivateData'];
  676. if (codecPrivateData) {
  677. return codecPrivateData;
  678. }
  679. if (type !== 'audio') {
  680. return null;
  681. }
  682. // For the audio we can reconstruct the CodecPrivateData
  683. // By default stereo
  684. const channels = stream.channelsCount || 2;
  685. // By default 44,1kHz.
  686. const samplingRate = stream.audioSamplingRate || 44100;
  687. const samplingFrequencyIndex = {
  688. 96000: 0x0,
  689. 88200: 0x1,
  690. 64000: 0x2,
  691. 48000: 0x3,
  692. 44100: 0x4,
  693. 32000: 0x5,
  694. 24000: 0x6,
  695. 22050: 0x7,
  696. 16000: 0x8,
  697. 12000: 0x9,
  698. 11025: 0xA,
  699. 8000: 0xB,
  700. 7350: 0xC,
  701. };
  702. const indexFreq = samplingFrequencyIndex[samplingRate];
  703. if (fourCCValue === 'AACH') {
  704. // High Efficiency AAC Profile
  705. const objectType = 0x05;
  706. // 4 bytes :
  707. // XXXXX XXXX XXXX XXXX
  708. // 'ObjectType' 'Freq Index' 'Channels value' 'Extension Sampling Freq'
  709. // XXXXX XXX XXXXXXX
  710. // 'ObjectType' 'GAS' 'alignment = 0'
  711. const data = new Uint8Array(4);
  712. // In HE AAC Extension Sampling frequence
  713. // equals to SamplingRate * 2
  714. const extensionSamplingFrequencyIndex =
  715. samplingFrequencyIndex[samplingRate * 2];
  716. // Freq Index is present for 3 bits in the first byte, last bit is in
  717. // the second
  718. data[0] = (objectType << 3) | (indexFreq >> 1);
  719. data[1] = (indexFreq << 7) | (channels << 3) |
  720. (extensionSamplingFrequencyIndex >> 1);
  721. // Origin object type equals to 2 => AAC Main Low Complexity
  722. data[2] = (extensionSamplingFrequencyIndex << 7) | (0x02 << 2);
  723. // Alignment bits
  724. data[3] = 0x0;
  725. // Put the 4 bytes in an 16 bits array
  726. const arr16 = new Uint16Array(2);
  727. arr16[0] = (data[0] << 8) + data[1];
  728. arr16[1] = (data[2] << 8) + data[3];
  729. // Convert decimal to hex value
  730. return arr16[0].toString(16) + arr16[1].toString(16);
  731. } else {
  732. // AAC Main Low Complexity
  733. const objectType = 0x02;
  734. // 2 bytes:
  735. // XXXXX XXXX XXXX XXX
  736. // 'ObjectType' 'Freq Index' 'Channels value' 'GAS = 000'
  737. const data = new Uint8Array(2);
  738. // Freq Index is present for 3 bits in the first byte, last bit is in
  739. // the second
  740. data[0] = (objectType << 3) | (indexFreq >> 1);
  741. data[1] = (indexFreq << 7) | (channels << 3);
  742. // Put the 2 bytes in an 16 bits array
  743. const arr16 = new Uint16Array(1);
  744. arr16[0] = (data[0] << 8) + data[1];
  745. // Convert decimal to hex value
  746. return arr16[0].toString(16);
  747. }
  748. }
  749. /**
  750. * @param {!shaka.extern.xml.Node} qualityLevel
  751. * @param {string} fourCCValue
  752. * @param {?string} codecPrivateData
  753. * @return {string}
  754. * @private
  755. */
  756. getAACCodec_(qualityLevel, fourCCValue, codecPrivateData) {
  757. let objectType = 0;
  758. // Chrome problem, in implicit AAC HE definition, so when AACH is detected
  759. // in FourCC set objectType to 5 => strange, it should be 2
  760. if (fourCCValue === 'AACH') {
  761. objectType = 0x05;
  762. }
  763. if (!codecPrivateData) {
  764. // AAC Main Low Complexity => object Type = 2
  765. objectType = 0x02;
  766. if (fourCCValue === 'AACH') {
  767. // High Efficiency AAC Profile = object Type = 5 SBR
  768. objectType = 0x05;
  769. }
  770. } else if (objectType === 0) {
  771. objectType = (parseInt(codecPrivateData.substr(0, 2), 16) & 0xF8) >> 3;
  772. }
  773. return 'mp4a.40.' + objectType;
  774. }
  775. /**
  776. * @param {!shaka.extern.xml.Node} qualityLevel
  777. * @param {?string} codecPrivateData
  778. * @return {string}
  779. * @private
  780. */
  781. getH264Codec_(qualityLevel, codecPrivateData) {
  782. // Extract from the CodecPrivateData field the hexadecimal representation
  783. // of the following three bytes in the sequence parameter set NAL unit.
  784. // => Find the SPS nal header
  785. const nalHeader = /00000001[0-9]7/.exec(codecPrivateData);
  786. if (!nalHeader.length) {
  787. return '';
  788. }
  789. if (!codecPrivateData) {
  790. return '';
  791. }
  792. // => Find the 6 characters after the SPS nalHeader (if it exists)
  793. const avcOti = codecPrivateData.substr(
  794. codecPrivateData.indexOf(nalHeader[0]) + 10, 6);
  795. return 'avc1.' + avcOti;
  796. }
  797. /**
  798. * @param {!shaka.media.InitSegmentReference} initSegmentRef
  799. * @param {!shaka.extern.Stream} stream
  800. * @param {!shaka.extern.xml.Node} streamIndex
  801. * @param {!Array<shaka.mss.MssParser.TimeRange>} timeline
  802. * @return {!Array<!shaka.media.SegmentReference>}
  803. * @private
  804. */
  805. createSegments_(initSegmentRef, stream, streamIndex, timeline) {
  806. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  807. const url = streamIndex.attributes['Url'];
  808. goog.asserts.assert(url, 'Missing URL for segments');
  809. const mediaUrl = url.replace('{bitrate}', String(stream.bandwidth));
  810. const segments = [];
  811. for (const time of timeline) {
  812. const getUris = () => {
  813. return ManifestParserUtils.resolveUris(this.manifestUris_,
  814. [mediaUrl.replace('{start time}', String(time.unscaledStart))]);
  815. };
  816. segments.push(new shaka.media.SegmentReference(
  817. time.start,
  818. time.end,
  819. getUris,
  820. /* startByte= */ 0,
  821. /* endByte= */ null,
  822. initSegmentRef,
  823. /* timestampOffset= */ 0,
  824. /* appendWindowStart= */ 0,
  825. /* appendWindowEnd= */ stream.mssPrivateData.duration));
  826. }
  827. return segments;
  828. }
  829. /**
  830. * Expands a streamIndex into an array-based timeline. The results are in
  831. * seconds.
  832. *
  833. * @param {!shaka.extern.xml.Node} streamIndex
  834. * @param {number} timescale
  835. * @param {number} duration The duration in seconds.
  836. * @return {!Array<shaka.mss.MssParser.TimeRange>}
  837. * @private
  838. */
  839. createTimeline_(streamIndex, timescale, duration) {
  840. goog.asserts.assert(
  841. timescale > 0 && timescale < Infinity,
  842. 'timescale must be a positive, finite integer');
  843. goog.asserts.assert(
  844. duration > 0, 'duration must be a positive integer');
  845. const TXml = shaka.util.TXml;
  846. const timePoints = TXml.findChildren(streamIndex, 'c');
  847. /** @type {!Array<shaka.mss.MssParser.TimeRange>} */
  848. const timeline = [];
  849. let lastEndTime = 0;
  850. for (let i = 0; i < timePoints.length; ++i) {
  851. const timePoint = timePoints[i];
  852. const next = timePoints[i + 1];
  853. const t =
  854. TXml.parseAttr(timePoint, 't', TXml.parseNonNegativeInt);
  855. const d =
  856. TXml.parseAttr(timePoint, 'd', TXml.parseNonNegativeInt);
  857. const r = TXml.parseAttr(timePoint, 'r', TXml.parseInt);
  858. if (!d) {
  859. shaka.log.warning(
  860. '"c" element must have a duration:',
  861. 'ignoring the remaining "c" elements.', timePoint);
  862. return timeline;
  863. }
  864. let startTime = t != null ? t : lastEndTime;
  865. let repeat = r || 0;
  866. // Unlike in DASH, in MSS r does not start counting repetitions at 0 but
  867. // at 1, to maintain the code equivalent to DASH if r exists we
  868. // subtract 1.
  869. if (repeat) {
  870. repeat--;
  871. }
  872. if (repeat < 0) {
  873. if (next) {
  874. const nextStartTime =
  875. TXml.parseAttr(next, 't', TXml.parseNonNegativeInt);
  876. if (nextStartTime == null) {
  877. shaka.log.warning(
  878. 'An "c" element cannot have a negative repeat',
  879. 'if the next "c" element does not have a valid start time:',
  880. 'ignoring the remaining "c" elements.', timePoint);
  881. return timeline;
  882. } else if (startTime >= nextStartTime) {
  883. shaka.log.warning(
  884. 'An "c" element cannot have a negative repeat if its start ',
  885. 'time exceeds the next "c" element\'s start time:',
  886. 'ignoring the remaining "c" elements.', timePoint);
  887. return timeline;
  888. }
  889. repeat = Math.ceil((nextStartTime - startTime) / d) - 1;
  890. } else {
  891. if (duration == Infinity) {
  892. // The MSS spec. actually allows the last "c" element to have a
  893. // negative repeat value even when it has an infinite
  894. // duration. No one uses this feature and no one ever should,
  895. // ever.
  896. shaka.log.warning(
  897. 'The last "c" element cannot have a negative repeat',
  898. 'if the Period has an infinite duration:',
  899. 'ignoring the last "c" element.', timePoint);
  900. return timeline;
  901. } else if (startTime / timescale >= duration) {
  902. shaka.log.warning(
  903. 'The last "c" element cannot have a negative repeat',
  904. 'if its start time exceeds the duration:',
  905. 'ignoring the last "c" element.', timePoint);
  906. return timeline;
  907. }
  908. repeat = Math.ceil((duration * timescale - startTime) / d) - 1;
  909. }
  910. }
  911. for (let j = 0; j <= repeat; ++j) {
  912. const endTime = startTime + d;
  913. const item = {
  914. start: startTime / timescale,
  915. end: endTime / timescale,
  916. unscaledStart: startTime,
  917. };
  918. timeline.push(item);
  919. startTime = endTime;
  920. lastEndTime = endTime;
  921. }
  922. }
  923. return timeline;
  924. }
  925. /**
  926. * @param {?shaka.extern.Stream} audioStream
  927. * @param {?shaka.extern.Stream} videoStream
  928. * @return {!shaka.extern.Variant}
  929. * @private
  930. */
  931. createVariant_(audioStream, videoStream) {
  932. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  933. goog.asserts.assert(!audioStream ||
  934. audioStream.type == ContentType.AUDIO, 'Audio parameter mismatch!');
  935. goog.asserts.assert(!videoStream ||
  936. videoStream.type == ContentType.VIDEO, 'Video parameter mismatch!');
  937. let bandwidth = 0;
  938. if (audioStream && audioStream.bandwidth && audioStream.bandwidth > 0) {
  939. bandwidth += audioStream.bandwidth;
  940. }
  941. if (videoStream && videoStream.bandwidth && videoStream.bandwidth > 0) {
  942. bandwidth += videoStream.bandwidth;
  943. }
  944. return {
  945. id: this.globalId_++,
  946. language: audioStream ? audioStream.language : 'und',
  947. disabledUntilTime: 0,
  948. primary: (!!audioStream && audioStream.primary) ||
  949. (!!videoStream && videoStream.primary),
  950. audio: audioStream,
  951. video: videoStream,
  952. bandwidth: bandwidth,
  953. allowedByApplication: true,
  954. allowedByKeySystem: true,
  955. decodingInfos: [],
  956. };
  957. }
  958. };
  959. /**
  960. * Contains the minimum amount of time, in seconds, between manifest update
  961. * requests.
  962. *
  963. * @private
  964. * @const {number}
  965. */
  966. shaka.mss.MssParser.MIN_UPDATE_PERIOD_ = 3;
  967. /**
  968. * @private
  969. * @const {number}
  970. */
  971. shaka.mss.MssParser.DEFAULT_TIME_SCALE_ = 1e7;
  972. /**
  973. * MSS supported codecs.
  974. *
  975. * @private
  976. * @const {!Array<string>}
  977. */
  978. shaka.mss.MssParser.SUPPORTED_CODECS_ = [
  979. 'AAC',
  980. 'AACL',
  981. 'AACH',
  982. 'AACP',
  983. 'AVC1',
  984. 'H264',
  985. 'TTML',
  986. 'DFXP',
  987. ];
  988. /**
  989. * MPEG-DASH Role and accessibility mapping for text tracks according to
  990. * ETSI TS 103 285 v1.1.1 (section 7.1.2)
  991. *
  992. * @const {!Map<string, string>}
  993. * @private
  994. */
  995. shaka.mss.MssParser.ROLE_MAPPING_ = new Map()
  996. .set('CAPT', 'main')
  997. .set('SUBT', 'alternate')
  998. .set('DESC', 'main');
  999. /**
  1000. * @typedef {{
  1001. * variants: !Array<shaka.extern.Variant>,
  1002. * textStreams: !Array<shaka.extern.Stream>,
  1003. * timescale: number,
  1004. * duration: number
  1005. * }}
  1006. *
  1007. * @property {!Array<shaka.extern.Variant>} variants
  1008. * The presentation's Variants.
  1009. * @property {!Array<shaka.extern.Stream>} textStreams
  1010. * The presentation's text streams.
  1011. * @property {number} timescale
  1012. * The presentation's timescale.
  1013. * @property {number} duration
  1014. * The presentation's duration.
  1015. */
  1016. shaka.mss.MssParser.Context;
  1017. /**
  1018. * @typedef {{
  1019. * start: number,
  1020. * unscaledStart: number,
  1021. * end: number
  1022. * }}
  1023. *
  1024. * @description
  1025. * Defines a time range of a media segment. Times are in seconds.
  1026. *
  1027. * @property {number} start
  1028. * The start time of the range.
  1029. * @property {number} unscaledStart
  1030. * The start time of the range in representation timescale units.
  1031. * @property {number} end
  1032. * The end time (exclusive) of the range.
  1033. */
  1034. shaka.mss.MssParser.TimeRange;
  1035. shaka.media.ManifestParser.registerParserByMime(
  1036. 'application/vnd.ms-sstr+xml', () => new shaka.mss.MssParser());