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