Source: lib/hls/hls_parser.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.hls.HlsParser');
  7. goog.require('goog.Uri');
  8. goog.require('goog.asserts');
  9. goog.require('shaka.hls.ManifestTextParser');
  10. goog.require('shaka.hls.Playlist');
  11. goog.require('shaka.hls.PlaylistType');
  12. goog.require('shaka.hls.Tag');
  13. goog.require('shaka.hls.Utils');
  14. goog.require('shaka.log');
  15. goog.require('shaka.media.DrmEngine');
  16. goog.require('shaka.media.InitSegmentReference');
  17. goog.require('shaka.media.ManifestParser');
  18. goog.require('shaka.media.PresentationTimeline');
  19. goog.require('shaka.media.SegmentIndex');
  20. goog.require('shaka.media.SegmentReference');
  21. goog.require('shaka.net.DataUriPlugin');
  22. goog.require('shaka.net.NetworkingEngine');
  23. goog.require('shaka.util.ArrayUtils');
  24. goog.require('shaka.util.BufferUtils');
  25. goog.require('shaka.util.DataViewReader');
  26. goog.require('shaka.util.Error');
  27. goog.require('shaka.util.FakeEvent');
  28. goog.require('shaka.util.Functional');
  29. goog.require('shaka.util.LanguageUtils');
  30. goog.require('shaka.util.ManifestParserUtils');
  31. goog.require('shaka.util.MimeUtils');
  32. goog.require('shaka.util.Mp4Parser');
  33. goog.require('shaka.util.Mp4BoxParsers');
  34. goog.require('shaka.util.Networking');
  35. goog.require('shaka.util.OperationManager');
  36. goog.require('shaka.util.Pssh');
  37. goog.require('shaka.util.Timer');
  38. goog.requireType('shaka.hls.Segment');
  39. /**
  40. * HLS parser.
  41. *
  42. * @implements {shaka.extern.ManifestParser}
  43. * @export
  44. */
  45. shaka.hls.HlsParser = class {
  46. /**
  47. * Creates an Hls Parser object.
  48. */
  49. constructor() {
  50. /** @private {?shaka.extern.ManifestParser.PlayerInterface} */
  51. this.playerInterface_ = null;
  52. /** @private {?shaka.extern.ManifestConfiguration} */
  53. this.config_ = null;
  54. /** @private {number} */
  55. this.globalId_ = 1;
  56. /** @private {!Map.<string, string>} */
  57. this.globalVariables_ = new Map();
  58. /**
  59. * A map from group id to stream infos created from the media tags.
  60. * @private {!Map.<string, !Array.<?shaka.hls.HlsParser.StreamInfo>>}
  61. */
  62. this.groupIdToStreamInfosMap_ = new Map();
  63. /**
  64. * The values are strings of the form "<VIDEO URI> - <AUDIO URI>",
  65. * where the URIs are the verbatim media playlist URIs as they appeared in
  66. * the master playlist.
  67. *
  68. * Used to avoid duplicates that vary only in their text stream.
  69. *
  70. * @private {!Set.<string>}
  71. */
  72. this.variantUriSet_ = new Set();
  73. /**
  74. * A map from (verbatim) media playlist URI to stream infos representing the
  75. * playlists.
  76. *
  77. * On update, used to iterate through and update from media playlists.
  78. *
  79. * On initial parse, used to iterate through and determine minimum
  80. * timestamps, offsets, and to handle TS rollover.
  81. *
  82. * During parsing, used to avoid duplicates in the async methods
  83. * createStreamInfoFromMediaTag_ and createStreamInfoFromVariantTag_.
  84. *
  85. * During parsing of updates, used by getStartTime_ to determine the start
  86. * time of the first segment from existing segment references.
  87. *
  88. * @private {!Map.<string, shaka.hls.HlsParser.StreamInfo>}
  89. */
  90. this.uriToStreamInfosMap_ = new Map();
  91. /** @private {?shaka.media.PresentationTimeline} */
  92. this.presentationTimeline_ = null;
  93. /**
  94. * The master playlist URI, after redirects.
  95. *
  96. * @private {string}
  97. */
  98. this.masterPlaylistUri_ = '';
  99. /** @private {shaka.hls.ManifestTextParser} */
  100. this.manifestTextParser_ = new shaka.hls.ManifestTextParser();
  101. /**
  102. * This is the number of seconds we want to wait between finishing a
  103. * manifest update and starting the next one. This will be set when we parse
  104. * the manifest.
  105. *
  106. * @private {number}
  107. */
  108. this.updatePlaylistDelay_ = 0;
  109. /**
  110. * This timer is used to trigger the start of a manifest update. A manifest
  111. * update is async. Once the update is finished, the timer will be restarted
  112. * to trigger the next update. The timer will only be started if the content
  113. * is live content.
  114. *
  115. * @private {shaka.util.Timer}
  116. */
  117. this.updatePlaylistTimer_ = new shaka.util.Timer(() => {
  118. this.onUpdate_();
  119. });
  120. /** @private {shaka.hls.HlsParser.PresentationType_} */
  121. this.presentationType_ = shaka.hls.HlsParser.PresentationType_.VOD;
  122. /** @private {?shaka.extern.Manifest} */
  123. this.manifest_ = null;
  124. /** @private {number} */
  125. this.maxTargetDuration_ = 0;
  126. /** @private {number} */
  127. this.minTargetDuration_ = Infinity;
  128. /** Partial segments target duration.
  129. * @private {number}
  130. */
  131. this.partialTargetDuration_ = 0;
  132. /** @private {number} */
  133. this.lowLatencyPresentationDelay_ = 0;
  134. /** @private {shaka.util.OperationManager} */
  135. this.operationManager_ = new shaka.util.OperationManager();
  136. /** @private {!Array.<!Array.<!shaka.media.SegmentReference>>} */
  137. this.segmentsToNotifyByStream_ = [];
  138. /** A map from closed captions' group id, to a map of closed captions info.
  139. * {group id -> {closed captions channel id -> language}}
  140. * @private {Map.<string, Map.<string, string>>}
  141. */
  142. this.groupIdToClosedCaptionsMap_ = new Map();
  143. /** True if some of the variants in the playlist is encrypted with AES-128.
  144. * @private {boolean} */
  145. this.aesEncrypted_ = false;
  146. /** @private {Map.<string, string>} */
  147. this.groupIdToCodecsMap_ = new Map();
  148. /** @private {?number} */
  149. this.playlistStartTime_ = null;
  150. /** A cache mapping EXT-X-MAP tag info to the InitSegmentReference created
  151. * from the tag.
  152. * The key is a string combining the EXT-X-MAP tag's absolute uri, and
  153. * its BYTERANGE if available.
  154. * {!Map.<string, !shaka.media.InitSegmentReference>} */
  155. this.mapTagToInitSegmentRefMap_ = new Map();
  156. /**
  157. * A cache mapping a discontinuity sequence number of a segment with
  158. * EXT-X-DISCONTINUITY tag into its timestamp offset.
  159. * Key: the discontinuity sequence number of a segment
  160. * Value: the segment reference's timestamp offset.
  161. * {!Map.<number, number>}
  162. */
  163. this.discontinuityToTso_ = new Map();
  164. /** @private {boolean} */
  165. this.lowLatencyMode_ = false;
  166. }
  167. /**
  168. * @override
  169. * @exportInterface
  170. */
  171. configure(config) {
  172. this.config_ = config;
  173. }
  174. /**
  175. * @override
  176. * @exportInterface
  177. */
  178. async start(uri, playerInterface) {
  179. goog.asserts.assert(this.config_, 'Must call configure() before start()!');
  180. this.playerInterface_ = playerInterface;
  181. this.lowLatencyMode_ = playerInterface.isLowLatencyMode();
  182. const response = await this.requestManifest_(uri);
  183. // Record the master playlist URI after redirects.
  184. this.masterPlaylistUri_ = response.uri;
  185. goog.asserts.assert(response.data, 'Response data should be non-null!');
  186. await this.parseManifest_(response.data);
  187. // Start the update timer if we want updates.
  188. const delay = this.updatePlaylistDelay_;
  189. if (delay > 0) {
  190. this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
  191. }
  192. goog.asserts.assert(this.manifest_, 'Manifest should be non-null');
  193. return this.manifest_;
  194. }
  195. /**
  196. * @override
  197. * @exportInterface
  198. */
  199. stop() {
  200. // Make sure we don't update the manifest again. Even if the timer is not
  201. // running, this is safe to call.
  202. if (this.updatePlaylistTimer_) {
  203. this.updatePlaylistTimer_.stop();
  204. this.updatePlaylistTimer_ = null;
  205. }
  206. /** @type {!Array.<!Promise>} */
  207. const pending = [];
  208. if (this.operationManager_) {
  209. pending.push(this.operationManager_.destroy());
  210. this.operationManager_ = null;
  211. }
  212. this.playerInterface_ = null;
  213. this.config_ = null;
  214. this.variantUriSet_.clear();
  215. this.manifest_ = null;
  216. this.uriToStreamInfosMap_.clear();
  217. this.groupIdToStreamInfosMap_.clear();
  218. this.groupIdToCodecsMap_.clear();
  219. this.globalVariables_.clear();
  220. return Promise.all(pending);
  221. }
  222. /**
  223. * @override
  224. * @exportInterface
  225. */
  226. async update() {
  227. if (!this.isLive_()) {
  228. return;
  229. }
  230. /** @type {!Array.<!Promise>} */
  231. const updates = [];
  232. // Reset the start time for the new media playlist.
  233. this.playlistStartTime_ = null;
  234. const streamInfos = Array.from(this.uriToStreamInfosMap_.values());
  235. // Wait for the first stream info created, so that the start time is fetched
  236. // and can be reused.
  237. if (streamInfos.length) {
  238. await this.updateStream_(streamInfos[0]);
  239. }
  240. for (let i = 1; i < streamInfos.length; i++) {
  241. updates.push(this.updateStream_(streamInfos[i]));
  242. }
  243. await Promise.all(updates);
  244. }
  245. /**
  246. * Updates a stream.
  247. *
  248. * @param {!shaka.hls.HlsParser.StreamInfo} streamInfo
  249. * @return {!Promise}
  250. * @private
  251. */
  252. async updateStream_(streamInfo) {
  253. const PresentationType = shaka.hls.HlsParser.PresentationType_;
  254. const manifestUri = streamInfo.absoluteMediaPlaylistUri;
  255. const uriObj = new goog.Uri(manifestUri);
  256. if (this.lowLatencyMode_ && streamInfo.canSkipSegments) {
  257. // Enable delta updates. This will replace older segments with
  258. // 'EXT-X-SKIP' tag in the media playlist.
  259. uriObj.setQueryData(new goog.Uri.QueryData('_HLS_skip=YES'));
  260. }
  261. const response = await this.requestManifest_(uriObj.toString());
  262. /** @type {shaka.hls.Playlist} */
  263. const playlist = this.manifestTextParser_.parsePlaylist(
  264. response.data, response.uri);
  265. if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
  266. throw new shaka.util.Error(
  267. shaka.util.Error.Severity.CRITICAL,
  268. shaka.util.Error.Category.MANIFEST,
  269. shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY);
  270. }
  271. /** @type {!Array.<!shaka.hls.Tag>} */
  272. const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
  273. 'EXT-X-DEFINE');
  274. const mediaVariables = this.parseMediaVariables_(variablesTags);
  275. const stream = streamInfo.stream;
  276. const segments = await this.createSegments_(
  277. streamInfo.verbatimMediaPlaylistUri, playlist, stream.type,
  278. stream.mimeType, streamInfo.mediaSequenceToStartTime, mediaVariables,
  279. streamInfo.discontinuityToMediaSequence);
  280. stream.segmentIndex.mergeAndEvict(
  281. segments, this.presentationTimeline_.getSegmentAvailabilityStart());
  282. if (segments.length) {
  283. const mediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber(
  284. playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0);
  285. const playlistStartTime = streamInfo.mediaSequenceToStartTime.get(
  286. mediaSequenceNumber);
  287. stream.segmentIndex.evict(playlistStartTime);
  288. }
  289. const newestSegment = segments[segments.length - 1];
  290. goog.asserts.assert(newestSegment, 'Should have segments!');
  291. // Once the last segment has been added to the playlist,
  292. // #EXT-X-ENDLIST tag will be appended.
  293. // If that happened, treat the rest of the EVENT presentation as VOD.
  294. const endListTag =
  295. shaka.hls.Utils.getFirstTagWithName(playlist.tags, 'EXT-X-ENDLIST');
  296. if (endListTag) {
  297. // Convert the presentation to VOD and set the duration to the last
  298. // segment's end time.
  299. this.setPresentationType_(PresentationType.VOD);
  300. this.presentationTimeline_.setDuration(newestSegment.endTime);
  301. }
  302. }
  303. /**
  304. * @override
  305. * @exportInterface
  306. */
  307. onExpirationUpdated(sessionId, expiration) {
  308. // No-op
  309. }
  310. /**
  311. * Parses the manifest.
  312. *
  313. * @param {BufferSource} data
  314. * @return {!Promise}
  315. * @private
  316. */
  317. async parseManifest_(data) {
  318. const Utils = shaka.hls.Utils;
  319. goog.asserts.assert(this.masterPlaylistUri_,
  320. 'Master playlist URI must be set before calling parseManifest_!');
  321. const playlist = this.manifestTextParser_.parsePlaylist(
  322. data, this.masterPlaylistUri_);
  323. // We don't support directly providing a Media Playlist.
  324. // See the error code for details.
  325. if (playlist.type != shaka.hls.PlaylistType.MASTER) {
  326. throw new shaka.util.Error(
  327. shaka.util.Error.Severity.CRITICAL,
  328. shaka.util.Error.Category.MANIFEST,
  329. shaka.util.Error.Code.HLS_MASTER_PLAYLIST_NOT_PROVIDED);
  330. }
  331. /** @type {!Array.<!shaka.hls.Tag>} */
  332. const variablesTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-DEFINE');
  333. this.parseMasterVariables_(variablesTags);
  334. /** @type {!Array.<!shaka.hls.Tag>} */
  335. const mediaTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-MEDIA');
  336. /** @type {!Array.<!shaka.hls.Tag>} */
  337. const variantTags = Utils.filterTagsByName(
  338. playlist.tags, 'EXT-X-STREAM-INF');
  339. this.parseCodecs_(variantTags);
  340. /** @type {!Array.<!shaka.hls.Tag>} */
  341. const sesionDataTags =
  342. Utils.filterTagsByName(playlist.tags, 'EXT-X-SESSION-DATA');
  343. for (const tag of sesionDataTags) {
  344. const id = tag.getAttributeValue('DATA-ID');
  345. const uri = tag.getAttributeValue('URI');
  346. const language = tag.getAttributeValue('LANGUAGE');
  347. const value = tag.getAttributeValue('VALUE');
  348. const data = (new Map()).set('id', id);
  349. if (uri) {
  350. data.set('uri',
  351. shaka.hls.Utils.constructAbsoluteUri(this.masterPlaylistUri_, uri));
  352. }
  353. if (language) {
  354. data.set('language', language);
  355. }
  356. if (value) {
  357. data.set('value', value);
  358. }
  359. const event = new shaka.util.FakeEvent('sessiondata', data);
  360. if (this.playerInterface_) {
  361. this.playerInterface_.onEvent(event);
  362. }
  363. }
  364. // Parse audio and video media tags first, so that we can extract segment
  365. // start time from audio/video streams and reuse for text streams.
  366. await this.createStreamInfosFromMediaTags_(mediaTags);
  367. this.parseClosedCaptions_(mediaTags);
  368. const variants = await this.createVariantsForTags_(variantTags);
  369. const textStreams = await this.parseTexts_(mediaTags);
  370. // Make sure that the parser has not been destroyed.
  371. if (!this.playerInterface_) {
  372. throw new shaka.util.Error(
  373. shaka.util.Error.Severity.CRITICAL,
  374. shaka.util.Error.Category.PLAYER,
  375. shaka.util.Error.Code.OPERATION_ABORTED);
  376. }
  377. if (this.aesEncrypted_ && variants.length == 0) {
  378. // We do not support AES-128 encryption with HLS yet. Variants is null
  379. // when the playlist is encrypted with AES-128.
  380. shaka.log.info('No stream is created, because we don\'t support AES-128',
  381. 'encryption yet');
  382. throw new shaka.util.Error(
  383. shaka.util.Error.Severity.CRITICAL,
  384. shaka.util.Error.Category.MANIFEST,
  385. shaka.util.Error.Code.HLS_AES_128_ENCRYPTION_NOT_SUPPORTED);
  386. }
  387. // Find the min and max timestamp of the earliest segment in all streams.
  388. // Find the minimum duration of all streams as well.
  389. let minFirstTimestamp = Infinity;
  390. let minDuration = Infinity;
  391. for (const streamInfo of this.uriToStreamInfosMap_.values()) {
  392. minFirstTimestamp =
  393. Math.min(minFirstTimestamp, streamInfo.minTimestamp);
  394. if (streamInfo.stream.type != 'text') {
  395. minDuration = Math.min(minDuration,
  396. streamInfo.maxTimestamp - streamInfo.minTimestamp);
  397. }
  398. }
  399. // This assert is our own sanity check.
  400. goog.asserts.assert(this.presentationTimeline_ == null,
  401. 'Presentation timeline created early!');
  402. this.createPresentationTimeline_();
  403. // This assert satisfies the compiler that it is not null for the rest of
  404. // the method.
  405. goog.asserts.assert(this.presentationTimeline_,
  406. 'Presentation timeline not created!');
  407. if (this.isLive_()) {
  408. // The HLS spec (RFC 8216) states in 6.3.4:
  409. // "the client MUST wait for at least the target duration before
  410. // attempting to reload the Playlist file again".
  411. // For LL-HLS, the server must add a new partial segment to the Playlist
  412. // every part target duration.
  413. this.updatePlaylistDelay_ = this.minTargetDuration_;
  414. // The spec says nothing much about seeking in live content, but Safari's
  415. // built-in HLS implementation does not allow it. Therefore we will set
  416. // the availability window equal to the presentation delay. The player
  417. // will be able to buffer ahead three segments, but the seek window will
  418. // be zero-sized.
  419. const PresentationType = shaka.hls.HlsParser.PresentationType_;
  420. if (this.presentationType_ == PresentationType.LIVE) {
  421. // This defaults to the presentation delay, which has the effect of
  422. // making the live stream unseekable. This is consistent with Apple's
  423. // HLS implementation.
  424. let segmentAvailabilityDuration = this.presentationTimeline_.getDelay();
  425. // The app can override that with a longer duration, to allow seeking.
  426. if (!isNaN(this.config_.availabilityWindowOverride)) {
  427. segmentAvailabilityDuration = this.config_.availabilityWindowOverride;
  428. }
  429. this.presentationTimeline_.setSegmentAvailabilityDuration(
  430. segmentAvailabilityDuration);
  431. }
  432. } else {
  433. // For VOD/EVENT content, offset everything back to 0.
  434. // Use the minimum timestamp as the offset for all streams.
  435. // Use the minimum duration as the presentation duration.
  436. this.presentationTimeline_.setDuration(minDuration);
  437. // Use a negative offset to adjust towards 0.
  438. this.presentationTimeline_.offset(-minFirstTimestamp);
  439. for (const streamInfo of this.uriToStreamInfosMap_.values()) {
  440. // The segments were created with actual media times, rather than
  441. // presentation-aligned times, so offset them all now.
  442. streamInfo.stream.segmentIndex.offset(-minFirstTimestamp);
  443. // Finally, fit the segments to the playlist duration.
  444. streamInfo.stream.segmentIndex.fit(/* periodStart= */ 0, minDuration);
  445. }
  446. }
  447. // Now that the content has been fit, notify segments.
  448. this.segmentsToNotifyByStream_ = [];
  449. const streamsToNotify = [];
  450. for (const variant of variants) {
  451. for (const stream of [variant.video, variant.audio]) {
  452. if (stream) {
  453. streamsToNotify.push(stream);
  454. }
  455. }
  456. }
  457. await Promise.all(streamsToNotify.map(async (stream) => {
  458. await stream.createSegmentIndex();
  459. }));
  460. for (const stream of streamsToNotify) {
  461. this.segmentsToNotifyByStream_.push(stream.segmentIndex.references);
  462. }
  463. this.notifySegments_();
  464. // This asserts that the live edge is being calculated from segment times.
  465. // For VOD and event streams, this check should still pass.
  466. goog.asserts.assert(
  467. !this.presentationTimeline_.usingPresentationStartTime(),
  468. 'We should not be using the presentation start time in HLS!');
  469. this.manifest_ = {
  470. presentationTimeline: this.presentationTimeline_,
  471. variants,
  472. textStreams,
  473. imageStreams: [],
  474. offlineSessionIds: [],
  475. minBufferTime: 0,
  476. };
  477. this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
  478. }
  479. /**
  480. * Get the variables of each variant tag, and store in a map.
  481. * @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
  482. * @private
  483. */
  484. parseMasterVariables_(tags) {
  485. for (const variableTag of tags) {
  486. const name = variableTag.getAttributeValue('NAME');
  487. const value = variableTag.getAttributeValue('VALUE');
  488. if (name && value) {
  489. if (!this.globalVariables_.has(name)) {
  490. this.globalVariables_.set(name, value);
  491. }
  492. }
  493. }
  494. }
  495. /**
  496. * Get the variables of each variant tag, and store in a map.
  497. * @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
  498. * @return {!Map.<string, string>}
  499. * @private
  500. */
  501. parseMediaVariables_(tags) {
  502. const mediaVariables = new Map();
  503. for (const variableTag of tags) {
  504. const name = variableTag.getAttributeValue('NAME');
  505. const value = variableTag.getAttributeValue('VALUE');
  506. const mediaImport = variableTag.getAttributeValue('IMPORT');
  507. if (name && value) {
  508. mediaVariables.set(name, value);
  509. }
  510. if (mediaImport) {
  511. const globalValue = this.globalVariables_.get(mediaImport);
  512. if (globalValue) {
  513. mediaVariables.set(mediaImport, globalValue);
  514. }
  515. }
  516. }
  517. return mediaVariables;
  518. }
  519. /**
  520. * Get the codecs of each variant tag, and store in a map from
  521. * audio/video/subtitle group id to the codecs arraylist.
  522. * @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
  523. * @private
  524. */
  525. parseCodecs_(tags) {
  526. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  527. for (const variantTag of tags) {
  528. const audioGroupId = variantTag.getAttributeValue('AUDIO');
  529. const videoGroupId = variantTag.getAttributeValue('VIDEO');
  530. const subGroupId = variantTag.getAttributeValue('SUBTITLES');
  531. const allCodecs = this.getCodecsForVariantTag_(variantTag);
  532. if (subGroupId) {
  533. const textCodecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
  534. ContentType.TEXT, allCodecs);
  535. goog.asserts.assert(textCodecs != null, 'Text codecs should be valid.');
  536. this.groupIdToCodecsMap_.set(subGroupId, textCodecs);
  537. shaka.util.ArrayUtils.remove(allCodecs, textCodecs);
  538. }
  539. if (audioGroupId) {
  540. const codecs = shaka.util.ManifestParserUtils.guessCodecs(
  541. ContentType.AUDIO, allCodecs);
  542. this.groupIdToCodecsMap_.set(audioGroupId, codecs);
  543. }
  544. if (videoGroupId) {
  545. const codecs = shaka.util.ManifestParserUtils.guessCodecs(
  546. ContentType.VIDEO, allCodecs);
  547. this.groupIdToCodecsMap_.set(videoGroupId, codecs);
  548. }
  549. }
  550. }
  551. /**
  552. * Parse Subtitles and Closed Captions from 'EXT-X-MEDIA' tags.
  553. * Create text streams for Subtitles, but not Closed Captions.
  554. *
  555. * @param {!Array.<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
  556. * @return {!Promise.<!Array.<!shaka.extern.Stream>>}
  557. * @private
  558. */
  559. async parseTexts_(mediaTags) {
  560. // Create text stream for each Subtitle media tag.
  561. const subtitleTags =
  562. shaka.hls.Utils.filterTagsByType(mediaTags, 'SUBTITLES');
  563. const textStreamPromises = subtitleTags.map(async (tag) => {
  564. const disableText = this.config_.disableText;
  565. if (disableText) {
  566. return null;
  567. }
  568. try {
  569. const streamInfo = await this.createStreamInfoFromMediaTag_(tag);
  570. goog.asserts.assert(
  571. streamInfo, 'Should always have a streamInfo for text');
  572. return streamInfo.stream;
  573. } catch (e) {
  574. if (this.config_.hls.ignoreTextStreamFailures) {
  575. return null;
  576. }
  577. throw e;
  578. }
  579. });
  580. const textStreams = await Promise.all(textStreamPromises);
  581. // Set the codecs for text streams.
  582. for (const tag of subtitleTags) {
  583. const groupId = tag.getRequiredAttrValue('GROUP-ID');
  584. const codecs = this.groupIdToCodecsMap_.get(groupId);
  585. if (codecs) {
  586. const textStreamInfos = this.groupIdToStreamInfosMap_.get(groupId);
  587. if (textStreamInfos) {
  588. for (const textStreamInfo of textStreamInfos) {
  589. textStreamInfo.stream.codecs = codecs;
  590. }
  591. }
  592. }
  593. }
  594. // Do not create text streams for Closed captions.
  595. return textStreams.filter((s) => s);
  596. }
  597. /**
  598. * @param {!Array.<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
  599. * @private
  600. */
  601. async createStreamInfosFromMediaTags_(mediaTags) {
  602. // Filter out subtitles and media tags without uri.
  603. mediaTags = mediaTags.filter((tag) => {
  604. const uri = tag.getAttributeValue('URI') || '';
  605. const type = tag.getAttributeValue('TYPE');
  606. return type != 'SUBTITLES' && uri != '';
  607. });
  608. // Create stream info for each audio / video media tag.
  609. // Wait for the first stream info created, so that the start time is fetched
  610. // and can be reused.
  611. if (mediaTags.length) {
  612. await this.createStreamInfoFromMediaTag_(mediaTags[0]);
  613. }
  614. const promises = mediaTags.slice(1).map((tag) => {
  615. return this.createStreamInfoFromMediaTag_(tag);
  616. });
  617. await Promise.all(promises);
  618. }
  619. /**
  620. * @param {!Array.<!shaka.hls.Tag>} tags Variant tags from the playlist.
  621. * @return {!Promise.<!Array.<!shaka.extern.Variant>>}
  622. * @private
  623. */
  624. async createVariantsForTags_(tags) {
  625. // Create variants for each variant tag.
  626. const variantsPromises = tags.map(async (tag) => {
  627. const frameRate = tag.getAttributeValue('FRAME-RATE');
  628. const bandwidth = Number(tag.getAttributeValue('AVERAGE-BANDWIDTH')) ||
  629. Number(tag.getRequiredAttrValue('BANDWIDTH'));
  630. const resolution = tag.getAttributeValue('RESOLUTION');
  631. const [width, height] = resolution ? resolution.split('x') : [null, null];
  632. const videoRange = tag.getAttributeValue('VIDEO-RANGE');
  633. const streamInfos = await this.createStreamInfosForVariantTag_(tag,
  634. resolution, frameRate);
  635. if (streamInfos) {
  636. goog.asserts.assert(streamInfos.audio.length ||
  637. streamInfos.video.length, 'We should have created a stream!');
  638. return this.createVariants_(
  639. streamInfos.audio,
  640. streamInfos.video,
  641. bandwidth,
  642. width,
  643. height,
  644. frameRate,
  645. videoRange);
  646. }
  647. // We do not support AES-128 encryption with HLS yet. If the streamInfos
  648. // is null because of AES-128 encryption, do not create variants for that.
  649. return [];
  650. });
  651. const allVariants = await Promise.all(variantsPromises);
  652. let variants = allVariants.reduce(shaka.util.Functional.collapseArrays, []);
  653. // Filter out null variants.
  654. variants = variants.filter((variant) => variant != null);
  655. return variants;
  656. }
  657. /**
  658. * Create audio and video streamInfos from an 'EXT-X-STREAM-INF' tag and its
  659. * related media tags.
  660. *
  661. * @param {!shaka.hls.Tag} tag
  662. * @param {?string} resolution
  663. * @param {?string} frameRate
  664. * @return {!Promise.<?shaka.hls.HlsParser.StreamInfos>}
  665. * @private
  666. */
  667. async createStreamInfosForVariantTag_(tag, resolution, frameRate) {
  668. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  669. /** @type {!Array.<string>} */
  670. let allCodecs = this.getCodecsForVariantTag_(tag);
  671. const audioGroupId = tag.getAttributeValue('AUDIO');
  672. const videoGroupId = tag.getAttributeValue('VIDEO');
  673. goog.asserts.assert(audioGroupId == null || videoGroupId == null,
  674. 'Unexpected: both video and audio described by media tags!');
  675. const groupId = audioGroupId || videoGroupId;
  676. const streamInfos =
  677. (groupId && this.groupIdToStreamInfosMap_.has(groupId)) ?
  678. this.groupIdToStreamInfosMap_.get(groupId) : [];
  679. /** @type {shaka.hls.HlsParser.StreamInfos} */
  680. const res = {
  681. audio: audioGroupId ? streamInfos : [],
  682. video: videoGroupId ? streamInfos : [],
  683. };
  684. // Make an educated guess about the stream type.
  685. shaka.log.debug('Guessing stream type for', tag.toString());
  686. let type;
  687. let ignoreStream = false;
  688. // The Microsoft HLS manifest generators will make audio-only variants
  689. // that link to their URI both directly and through an audio tag.
  690. // In that case, ignore the local URI and use the version in the
  691. // AUDIO tag, so you inherit its language.
  692. // As an example, see the manifest linked in issue #860.
  693. const streamURI = tag.getRequiredAttrValue('URI');
  694. const hasSameUri = res.audio.find((audio) => {
  695. return audio && audio.verbatimMediaPlaylistUri == streamURI;
  696. });
  697. const videoCodecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
  698. ContentType.VIDEO, allCodecs);
  699. const audioCodecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
  700. ContentType.AUDIO, allCodecs);
  701. if (audioCodecs && !videoCodecs) {
  702. // There are no associated media tags, and there's only audio codec,
  703. // and no video codec, so it should be audio.
  704. type = ContentType.AUDIO;
  705. shaka.log.debug('Guessing audio-only.');
  706. } else if (!streamInfos.length && audioCodecs && videoCodecs) {
  707. // There are both audio and video codecs, so assume multiplexed content.
  708. // Note that the default used when CODECS is missing assumes multiple
  709. // (and therefore multiplexed).
  710. // Recombine the codec strings into one so that MediaSource isn't
  711. // lied to later. (That would trigger an error in Chrome.)
  712. shaka.log.debug('Guessing multiplexed audio+video.');
  713. type = ContentType.VIDEO;
  714. allCodecs = [[videoCodecs, audioCodecs].join(',')];
  715. } else if (res.audio.length && hasSameUri) {
  716. shaka.log.debug('Guessing audio-only.');
  717. type = ContentType.AUDIO;
  718. ignoreStream = true;
  719. } else if (res.video.length) {
  720. // There are associated video streams. Assume this is audio.
  721. shaka.log.debug('Guessing audio-only.');
  722. type = ContentType.AUDIO;
  723. } else {
  724. shaka.log.debug('Guessing video-only.');
  725. type = ContentType.VIDEO;
  726. }
  727. let streamInfo;
  728. if (!ignoreStream) {
  729. streamInfo =
  730. await this.createStreamInfoFromVariantTag_(tag, allCodecs, type);
  731. }
  732. if (streamInfo) {
  733. res[streamInfo.stream.type] = [streamInfo];
  734. } else if (streamInfo === null) {
  735. // Triple-equals for undefined.
  736. shaka.log.debug('streamInfo is null');
  737. return null;
  738. }
  739. this.filterLegacyCodecs_(res);
  740. return res;
  741. }
  742. /**
  743. * Get the codecs from the 'EXT-X-STREAM-INF' tag.
  744. *
  745. * @param {!shaka.hls.Tag} tag
  746. * @return {!Array.<string>} codecs
  747. * @private
  748. */
  749. getCodecsForVariantTag_(tag) {
  750. // These are the default codecs to assume if none are specified.
  751. // The video codec is H.264, with baseline profile and level 3.0.
  752. // http://blog.pearce.org.nz/2013/11/what-does-h264avc1-codecs-parameters.html
  753. // The audio codec is "low-complexity" AAC.
  754. const defaultCodecsArray = [];
  755. if (!this.config_.disableVideo) {
  756. defaultCodecsArray.push('avc1.42E01E');
  757. }
  758. if (!this.config_.disableAudio) {
  759. defaultCodecsArray.push('mp4a.40.2');
  760. }
  761. const defaultCodecs = defaultCodecsArray.join(',');
  762. const codecsString = tag.getAttributeValue('CODECS', defaultCodecs);
  763. // Strip out internal whitespace while splitting on commas:
  764. /** @type {!Array.<string>} */
  765. const codecs = codecsString.split(/\s*,\s*/);
  766. // Filter out duplicate codecs.
  767. const seen = new Set();
  768. const ret = [];
  769. for (const codec of codecs) {
  770. // HLS says the CODECS field needs to include all codecs that appear in
  771. // the content. This means that if the content changes profiles, it should
  772. // include both. Since all known browsers support changing profiles
  773. // without any other work, just ignore them. See also:
  774. // https://github.com/shaka-project/shaka-player/issues/1817
  775. const shortCodec = shaka.util.MimeUtils.getCodecBase(codec);
  776. if (!seen.has(shortCodec)) {
  777. ret.push(codec);
  778. seen.add(shortCodec);
  779. } else {
  780. shaka.log.debug('Ignoring duplicate codec');
  781. }
  782. }
  783. return ret;
  784. }
  785. /**
  786. * Get the channel count information for an HLS audio track.
  787. * CHANNELS specifies an ordered, "/" separated list of parameters.
  788. * If the type is audio, the first parameter will be a decimal integer
  789. * specifying the number of independent, simultaneous audio channels.
  790. * No other channels parameters are currently defined.
  791. *
  792. * @param {!shaka.hls.Tag} tag
  793. * @return {?number}
  794. * @private
  795. */
  796. getChannelsCount_(tag) {
  797. const channels = tag.getAttributeValue('CHANNELS');
  798. if (!channels) {
  799. return null;
  800. }
  801. const channelcountstring = channels.split('/')[0];
  802. const count = parseInt(channelcountstring, 10);
  803. return count;
  804. }
  805. /**
  806. * Get the spatial audio information for an HLS audio track.
  807. * In HLS the channels field indicates the number of audio channels that the
  808. * stream has (eg: 2). In the case of Dolby Atmos, the complexity is
  809. * expressed with the number of channels followed by the word JOC
  810. * (eg: 16/JOC), so 16 would be the number of channels (eg: 7.3.6 layout),
  811. * and JOC indicates that the stream has spatial audio.
  812. * @see https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices/hls_authoring_specification_for_apple_devices_appendixes
  813. *
  814. * @param {!shaka.hls.Tag} tag
  815. * @return {boolean}
  816. * @private
  817. */
  818. isSpatialAudio_(tag) {
  819. const channels = tag.getAttributeValue('CHANNELS');
  820. if (!channels) {
  821. return false;
  822. }
  823. return channels.includes('/JOC');
  824. }
  825. /**
  826. * Get the closed captions map information for the EXT-X-STREAM-INF tag, to
  827. * create the stream info.
  828. * @param {!shaka.hls.Tag} tag
  829. * @param {string} type
  830. * @return {Map.<string, string>} closedCaptions
  831. * @private
  832. */
  833. getClosedCaptions_(tag, type) {
  834. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  835. // The attribute of closed captions is optional, and the value may be
  836. // 'NONE'.
  837. const closedCaptionsAttr = tag.getAttributeValue('CLOSED-CAPTIONS');
  838. // EXT-X-STREAM-INF tags may have CLOSED-CAPTIONS attributes.
  839. // The value can be either a quoted-string or an enumerated-string with
  840. // the value NONE. If the value is a quoted-string, it MUST match the
  841. // value of the GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the
  842. // Playlist whose TYPE attribute is CLOSED-CAPTIONS.
  843. if (type == ContentType.VIDEO && closedCaptionsAttr &&
  844. closedCaptionsAttr != 'NONE') {
  845. return this.groupIdToClosedCaptionsMap_.get(closedCaptionsAttr);
  846. }
  847. return null;
  848. }
  849. /**
  850. * Get the language value.
  851. *
  852. * @param {!shaka.hls.Tag} tag
  853. * @return {string}
  854. * @private
  855. */
  856. getLanguage_(tag) {
  857. const LanguageUtils = shaka.util.LanguageUtils;
  858. const languageValue = tag.getAttributeValue('LANGUAGE') || 'und';
  859. return LanguageUtils.normalize(languageValue);
  860. }
  861. /**
  862. * Get the type value.
  863. * Shaka recognizes the content types 'audio', 'video' and 'text'.
  864. * The HLS 'subtitles' type needs to be mapped to 'text'.
  865. * @param {!shaka.hls.Tag} tag
  866. * @return {string}
  867. * @private
  868. */
  869. getType_(tag) {
  870. let type = tag.getRequiredAttrValue('TYPE').toLowerCase();
  871. if (type == 'subtitles') {
  872. type = shaka.util.ManifestParserUtils.ContentType.TEXT;
  873. }
  874. return type;
  875. }
  876. /**
  877. * Filters out unsupported codec strings from an array of stream infos.
  878. * @param {shaka.hls.HlsParser.StreamInfos} streamInfos
  879. * @private
  880. */
  881. filterLegacyCodecs_(streamInfos) {
  882. for (const streamInfo of streamInfos.audio.concat(streamInfos.video)) {
  883. if (!streamInfo) {
  884. continue;
  885. }
  886. let codecs = streamInfo.stream.codecs.split(',');
  887. codecs = codecs.filter((codec) => {
  888. // mp4a.40.34 is a nonstandard codec string that is sometimes used in
  889. // HLS for legacy reasons. It is not recognized by non-Apple MSE.
  890. // See https://bugs.chromium.org/p/chromium/issues/detail?id=489520
  891. // Therefore, ignore this codec string.
  892. return codec != 'mp4a.40.34';
  893. });
  894. streamInfo.stream.codecs = codecs.join(',');
  895. }
  896. }
  897. /**
  898. * @param {!Array.<shaka.hls.HlsParser.StreamInfo>} audioInfos
  899. * @param {!Array.<shaka.hls.HlsParser.StreamInfo>} videoInfos
  900. * @param {number} bandwidth
  901. * @param {?string} width
  902. * @param {?string} height
  903. * @param {?string} frameRate
  904. * @param {?string} videoRange
  905. * @return {!Array.<!shaka.extern.Variant>}
  906. * @private
  907. */
  908. createVariants_(
  909. audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange) {
  910. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  911. const DrmEngine = shaka.media.DrmEngine;
  912. for (const info of videoInfos) {
  913. this.addVideoAttributes_(
  914. info.stream, width, height, frameRate, videoRange);
  915. }
  916. // In case of audio-only or video-only content or the audio/video is
  917. // disabled by the config, we create an array of one item containing
  918. // a null. This way, the double-loop works for all kinds of content.
  919. // NOTE: we currently don't have support for audio-only content.
  920. const disableAudio = this.config_.disableAudio;
  921. if (!audioInfos.length || disableAudio) {
  922. audioInfos = [null];
  923. }
  924. const disableVideo = this.config_.disableVideo;
  925. if (!videoInfos.length || disableVideo) {
  926. videoInfos = [null];
  927. }
  928. const variants = [];
  929. for (const audioInfo of audioInfos) {
  930. for (const videoInfo of videoInfos) {
  931. const audioStream = audioInfo ? audioInfo.stream : null;
  932. const videoStream = videoInfo ? videoInfo.stream : null;
  933. const audioDrmInfos = audioInfo ? audioInfo.stream.drmInfos : null;
  934. const videoDrmInfos = videoInfo ? videoInfo.stream.drmInfos : null;
  935. const videoStreamUri =
  936. videoInfo ? videoInfo.verbatimMediaPlaylistUri : '';
  937. const audioStreamUri =
  938. audioInfo ? audioInfo.verbatimMediaPlaylistUri : '';
  939. const variantUriKey = videoStreamUri + ' - ' + audioStreamUri;
  940. if (audioStream && videoStream) {
  941. if (!DrmEngine.areDrmCompatible(audioDrmInfos, videoDrmInfos)) {
  942. shaka.log.warning(
  943. 'Incompatible DRM info in HLS variant. Skipping.');
  944. continue;
  945. }
  946. }
  947. if (this.variantUriSet_.has(variantUriKey)) {
  948. // This happens when two variants only differ in their text streams.
  949. shaka.log.debug(
  950. 'Skipping variant which only differs in text streams.');
  951. continue;
  952. }
  953. // Since both audio and video are of the same type, this assertion will
  954. // catch certain mistakes at runtime that the compiler would miss.
  955. goog.asserts.assert(!audioStream ||
  956. audioStream.type == ContentType.AUDIO, 'Audio parameter mismatch!');
  957. goog.asserts.assert(!videoStream ||
  958. videoStream.type == ContentType.VIDEO, 'Video parameter mismatch!');
  959. const variant = {
  960. id: this.globalId_++,
  961. language: audioStream ? audioStream.language : 'und',
  962. primary: (!!audioStream && audioStream.primary) ||
  963. (!!videoStream && videoStream.primary),
  964. audio: audioStream,
  965. video: videoStream,
  966. bandwidth,
  967. allowedByApplication: true,
  968. allowedByKeySystem: true,
  969. decodingInfos: [],
  970. };
  971. variants.push(variant);
  972. this.variantUriSet_.add(variantUriKey);
  973. }
  974. }
  975. return variants;
  976. }
  977. /**
  978. * Parses an array of EXT-X-MEDIA tags, then stores the values of all tags
  979. * with TYPE="CLOSED-CAPTIONS" into a map of group id to closed captions.
  980. *
  981. * @param {!Array.<!shaka.hls.Tag>} mediaTags
  982. * @private
  983. */
  984. parseClosedCaptions_(mediaTags) {
  985. const closedCaptionsTags =
  986. shaka.hls.Utils.filterTagsByType(mediaTags, 'CLOSED-CAPTIONS');
  987. for (const tag of closedCaptionsTags) {
  988. goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
  989. 'Should only be called on media tags!');
  990. const language = this.getLanguage_(tag);
  991. // The GROUP-ID value is a quoted-string that specifies the group to which
  992. // the Rendition belongs.
  993. const groupId = tag.getRequiredAttrValue('GROUP-ID');
  994. // The value of INSTREAM-ID is a quoted-string that specifies a Rendition
  995. // within the segments in the Media Playlist. This attribute is REQUIRED
  996. // if the TYPE attribute is CLOSED-CAPTIONS.
  997. const instreamId = tag.getRequiredAttrValue('INSTREAM-ID');
  998. if (!this.groupIdToClosedCaptionsMap_.get(groupId)) {
  999. this.groupIdToClosedCaptionsMap_.set(groupId, new Map());
  1000. }
  1001. this.groupIdToClosedCaptionsMap_.get(groupId).set(instreamId, language);
  1002. }
  1003. }
  1004. /**
  1005. * Parse EXT-X-MEDIA media tag into a Stream object.
  1006. *
  1007. * @param {shaka.hls.Tag} tag
  1008. * @return {!Promise.<?shaka.hls.HlsParser.StreamInfo>}
  1009. * @private
  1010. */
  1011. async createStreamInfoFromMediaTag_(tag) {
  1012. goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
  1013. 'Should only be called on media tags!');
  1014. const groupId = tag.getRequiredAttrValue('GROUP-ID');
  1015. let codecs = '';
  1016. /** @type {string} */
  1017. const type = this.getType_(tag);
  1018. // Text does not require a codec.
  1019. if (type != shaka.util.ManifestParserUtils.ContentType.TEXT && groupId &&
  1020. this.groupIdToCodecsMap_.has(groupId)) {
  1021. codecs = this.groupIdToCodecsMap_.get(groupId);
  1022. }
  1023. const verbatimMediaPlaylistUri = this.variableSubstitution_(
  1024. tag.getRequiredAttrValue('URI'), this.globalVariables_);
  1025. // Check if the stream has already been created as part of another Variant
  1026. // and return it if it has.
  1027. if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
  1028. return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
  1029. }
  1030. const language = this.getLanguage_(tag);
  1031. const name = tag.getAttributeValue('NAME');
  1032. // NOTE: According to the HLS spec, "DEFAULT=YES" requires "AUTOSELECT=YES".
  1033. // However, we don't bother to validate "AUTOSELECT", since we don't
  1034. // actually use it in our streaming model, and we treat everything as
  1035. // "AUTOSELECT=YES". A value of "AUTOSELECT=NO" would imply that it may
  1036. // only be selected explicitly by the user, and we don't have a way to
  1037. // represent that in our model.
  1038. const defaultAttrValue = tag.getAttributeValue('DEFAULT');
  1039. const primary = defaultAttrValue == 'YES';
  1040. const channelsCount = type == 'audio' ? this.getChannelsCount_(tag) : null;
  1041. const spatialAudio = type == 'audio' ? this.isSpatialAudio_(tag) : false;
  1042. const characteristics = tag.getAttributeValue('CHARACTERISTICS');
  1043. const forcedAttrValue = tag.getAttributeValue('FORCED');
  1044. const forced = forcedAttrValue == 'YES';
  1045. // TODO: Should we take into account some of the currently ignored
  1046. // attributes: INSTREAM-ID, Attribute descriptions: https://bit.ly/2lpjOhj
  1047. const streamInfo = await this.createStreamInfo_(
  1048. verbatimMediaPlaylistUri, codecs, type, language, primary, name,
  1049. channelsCount, /* closedCaptions= */ null, characteristics, forced,
  1050. spatialAudio);
  1051. if (this.groupIdToStreamInfosMap_.has(groupId)) {
  1052. this.groupIdToStreamInfosMap_.get(groupId).push(streamInfo);
  1053. } else {
  1054. this.groupIdToStreamInfosMap_.set(groupId, [streamInfo]);
  1055. }
  1056. if (streamInfo == null) {
  1057. return null;
  1058. }
  1059. // TODO: This check is necessary because of the possibility of multiple
  1060. // calls to createStreamInfoFromMediaTag_ before either has resolved.
  1061. if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
  1062. return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
  1063. }
  1064. this.uriToStreamInfosMap_.set(verbatimMediaPlaylistUri, streamInfo);
  1065. return streamInfo;
  1066. }
  1067. /**
  1068. * Parse an EXT-X-STREAM-INF media tag into a Stream object.
  1069. *
  1070. * @param {!shaka.hls.Tag} tag
  1071. * @param {!Array.<string>} allCodecs
  1072. * @param {string} type
  1073. * @return {!Promise.<?shaka.hls.HlsParser.StreamInfo>}
  1074. * @private
  1075. */
  1076. async createStreamInfoFromVariantTag_(tag, allCodecs, type) {
  1077. goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
  1078. 'Should only be called on variant tags!');
  1079. const verbatimMediaPlaylistUri = this.variableSubstitution_(
  1080. tag.getRequiredAttrValue('URI'), this.globalVariables_);
  1081. if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
  1082. return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
  1083. }
  1084. const closedCaptions = this.getClosedCaptions_(tag, type);
  1085. const codecs = shaka.util.ManifestParserUtils.guessCodecs(type, allCodecs);
  1086. const streamInfo = await this.createStreamInfo_(verbatimMediaPlaylistUri,
  1087. codecs, type, /* language= */ 'und', /* primary= */ false,
  1088. /* name= */ null, /* channelcount= */ null, closedCaptions,
  1089. /* characteristics= */ null, /* forced= */ false,
  1090. /* spatialAudio= */ false);
  1091. if (streamInfo == null) {
  1092. return null;
  1093. }
  1094. // TODO: This check is necessary because of the possibility of multiple
  1095. // calls to createStreamInfoFromVariantTag_ before either has resolved.
  1096. if (this.uriToStreamInfosMap_.has(verbatimMediaPlaylistUri)) {
  1097. return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
  1098. }
  1099. this.uriToStreamInfosMap_.set(verbatimMediaPlaylistUri, streamInfo);
  1100. return streamInfo;
  1101. }
  1102. /**
  1103. * @param {string} verbatimMediaPlaylistUri
  1104. * @param {string} codecs
  1105. * @param {string} type
  1106. * @param {string} language
  1107. * @param {boolean} primary
  1108. * @param {?string} name
  1109. * @param {?number} channelsCount
  1110. * @param {Map.<string, string>} closedCaptions
  1111. * @param {?string} characteristics
  1112. * @param {boolean} forced
  1113. * @param {boolean} spatialAudio
  1114. * @return {!Promise.<?shaka.hls.HlsParser.StreamInfo>}
  1115. * @private
  1116. */
  1117. async createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, language,
  1118. primary, name, channelsCount, closedCaptions, characteristics, forced,
  1119. spatialAudio) {
  1120. // TODO: Refactor, too many parameters
  1121. let absoluteMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri(
  1122. this.masterPlaylistUri_, verbatimMediaPlaylistUri);
  1123. const response = await this.requestManifest_(absoluteMediaPlaylistUri);
  1124. // Record the final URI after redirects.
  1125. absoluteMediaPlaylistUri = response.uri;
  1126. // Record the redirected, final URI of this media playlist when we parse it.
  1127. /** @type {!shaka.hls.Playlist} */
  1128. const playlist = this.manifestTextParser_.parsePlaylist(
  1129. response.data, absoluteMediaPlaylistUri);
  1130. if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
  1131. // EXT-X-MEDIA tags should point to media playlists.
  1132. throw new shaka.util.Error(
  1133. shaka.util.Error.Severity.CRITICAL,
  1134. shaka.util.Error.Category.MANIFEST,
  1135. shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY);
  1136. }
  1137. /** @type {!Array.<!shaka.hls.Tag>} */
  1138. const drmTags = [];
  1139. if (playlist.segments) {
  1140. for (const segment of playlist.segments) {
  1141. const segmentKeyTags = shaka.hls.Utils.filterTagsByName(segment.tags,
  1142. 'EXT-X-KEY');
  1143. drmTags.push(...segmentKeyTags);
  1144. }
  1145. }
  1146. let encrypted = false;
  1147. /** @type {!Array.<shaka.extern.DrmInfo>}*/
  1148. const drmInfos = [];
  1149. const keyIds = new Set();
  1150. // TODO: May still need changes to support key rotation.
  1151. for (const drmTag of drmTags) {
  1152. const method = drmTag.getRequiredAttrValue('METHOD');
  1153. if (method != 'NONE') {
  1154. encrypted = true;
  1155. // We do not support AES-128 encryption with HLS yet. So, do not create
  1156. // StreamInfo for the playlist encrypted with AES-128.
  1157. // TODO: Remove the error message once we add support for AES-128.
  1158. if (method == 'AES-128') {
  1159. shaka.log.warning('Unsupported HLS Encryption', method);
  1160. this.aesEncrypted_ = true;
  1161. return null;
  1162. }
  1163. const keyFormat = drmTag.getRequiredAttrValue('KEYFORMAT');
  1164. const drmParser =
  1165. shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
  1166. const drmInfo = drmParser ? drmParser(drmTag) : null;
  1167. if (drmInfo) {
  1168. if (drmInfo.keyIds) {
  1169. for (const keyId of drmInfo.keyIds) {
  1170. keyIds.add(keyId);
  1171. }
  1172. }
  1173. drmInfos.push(drmInfo);
  1174. } else {
  1175. shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
  1176. }
  1177. }
  1178. }
  1179. if (encrypted && !drmInfos.length) {
  1180. throw new shaka.util.Error(
  1181. shaka.util.Error.Severity.CRITICAL,
  1182. shaka.util.Error.Category.MANIFEST,
  1183. shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED);
  1184. }
  1185. /** @type {!Array.<!shaka.hls.Tag>} */
  1186. const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
  1187. 'EXT-X-DEFINE');
  1188. const mediaVariables = this.parseMediaVariables_(variablesTags);
  1189. goog.asserts.assert(playlist.segments != null,
  1190. 'Media playlist should have segments!');
  1191. this.determinePresentationType_(playlist);
  1192. /** @type {string} */
  1193. const mimeType = await this.guessMimeType_(type, codecs, playlist,
  1194. mediaVariables);
  1195. // MediaSource expects no codec strings combined with raw formats.
  1196. // TODO(#2337): Instead, create a Stream flag indicating a raw format.
  1197. if (shaka.hls.HlsParser.RAW_FORMATS_.includes(mimeType)) {
  1198. codecs = '';
  1199. }
  1200. /** @type {!Map.<number, number>} */
  1201. const mediaSequenceToStartTime = new Map();
  1202. /**
  1203. * A map of a discontinuity sequence number, to the first segment's media
  1204. * sequence number with the discontinuity sequence number.
  1205. * Key: the discontinuity sequence number of a few segments
  1206. * Value: the first segment's media sequence number of the segments with
  1207. * this discontinuity sequence number.
  1208. * Used to get the discontinuity sequence number with playlist delta
  1209. * updates with lowLatencyMode enabled.
  1210. * {!Map.<number, number>}
  1211. */
  1212. const discontinuityToMediaSequence = new Map();
  1213. let segments;
  1214. try {
  1215. segments = await this.createSegments_(verbatimMediaPlaylistUri,
  1216. playlist, type, mimeType, mediaSequenceToStartTime, mediaVariables,
  1217. discontinuityToMediaSequence);
  1218. } catch (error) {
  1219. if (error.code == shaka.util.Error.Code.HLS_INTERNAL_SKIP_STREAM) {
  1220. shaka.log.alwaysWarn('Skipping unsupported HLS stream',
  1221. mimeType, verbatimMediaPlaylistUri);
  1222. return null;
  1223. }
  1224. throw error;
  1225. }
  1226. const minTimestamp = segments[0].startTime;
  1227. const lastEndTime = segments[segments.length - 1].endTime;
  1228. /** @type {!shaka.media.SegmentIndex} */
  1229. const segmentIndex = new shaka.media.SegmentIndex(segments);
  1230. const kind = (type == shaka.util.ManifestParserUtils.ContentType.TEXT) ?
  1231. shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE : undefined;
  1232. const roles = [];
  1233. if (characteristics) {
  1234. for (const characteristic of characteristics.split(',')) {
  1235. roles.push(characteristic);
  1236. }
  1237. }
  1238. const serverControlTag = shaka.hls.Utils.getFirstTagWithName(
  1239. playlist.tags, 'EXT-X-SERVER-CONTROL');
  1240. const canSkipSegments = serverControlTag ?
  1241. serverControlTag.getAttribute('CAN-SKIP-UNTIL') != null : false;
  1242. /** @type {shaka.extern.Stream} */
  1243. const stream = {
  1244. id: this.globalId_++,
  1245. originalId: name,
  1246. createSegmentIndex: () => Promise.resolve(),
  1247. segmentIndex,
  1248. mimeType,
  1249. codecs,
  1250. kind,
  1251. encrypted,
  1252. drmInfos,
  1253. keyIds,
  1254. language,
  1255. label: name, // For historical reasons, since before "originalId".
  1256. type,
  1257. primary,
  1258. // TODO: trick mode
  1259. trickModeVideo: null,
  1260. emsgSchemeIdUris: null,
  1261. frameRate: undefined,
  1262. pixelAspectRatio: undefined,
  1263. width: undefined,
  1264. height: undefined,
  1265. bandwidth: undefined,
  1266. roles: roles,
  1267. forced: forced,
  1268. channelsCount,
  1269. audioSamplingRate: null,
  1270. spatialAudio: spatialAudio,
  1271. closedCaptions,
  1272. hdr: undefined,
  1273. tilesLayout: undefined,
  1274. };
  1275. return {
  1276. stream,
  1277. verbatimMediaPlaylistUri,
  1278. absoluteMediaPlaylistUri,
  1279. minTimestamp,
  1280. maxTimestamp: lastEndTime,
  1281. mediaSequenceToStartTime,
  1282. discontinuityToMediaSequence,
  1283. canSkipSegments,
  1284. };
  1285. }
  1286. /**
  1287. * @param {!shaka.hls.Playlist} playlist
  1288. * @private
  1289. */
  1290. determinePresentationType_(playlist) {
  1291. const PresentationType = shaka.hls.HlsParser.PresentationType_;
  1292. const presentationTypeTag =
  1293. shaka.hls.Utils.getFirstTagWithName(playlist.tags,
  1294. 'EXT-X-PLAYLIST-TYPE');
  1295. const endListTag =
  1296. shaka.hls.Utils.getFirstTagWithName(playlist.tags, 'EXT-X-ENDLIST');
  1297. const isVod = (presentationTypeTag && presentationTypeTag.value == 'VOD') ||
  1298. endListTag;
  1299. const isEvent = presentationTypeTag &&
  1300. presentationTypeTag.value == 'EVENT' && !isVod;
  1301. const isLive = !isVod && !isEvent;
  1302. if (isVod) {
  1303. this.setPresentationType_(PresentationType.VOD);
  1304. } else {
  1305. // If it's not VOD, it must be presentation type LIVE or an ongoing EVENT.
  1306. if (isLive) {
  1307. this.setPresentationType_(PresentationType.LIVE);
  1308. } else {
  1309. this.setPresentationType_(PresentationType.EVENT);
  1310. }
  1311. const targetDurationTag = this.getRequiredTag_(playlist.tags,
  1312. 'EXT-X-TARGETDURATION');
  1313. const targetDuration = Number(targetDurationTag.value);
  1314. const partialTargetDurationTag =
  1315. shaka.hls.Utils.getFirstTagWithName(playlist.tags, 'EXT-X-PART-INF');
  1316. // According to the HLS spec, updates should not happen more often than
  1317. // once in targetDuration. It also requires us to only update the active
  1318. // variant. We might implement that later, but for now every variant
  1319. // will be updated. To get the update period, choose the smallest
  1320. // targetDuration value across all playlists.
  1321. // 1. Update the shortest one to use as update period and segment
  1322. // availability time (for LIVE).
  1323. if (this.lowLatencyMode_ && partialTargetDurationTag) {
  1324. // For low latency streaming, use the partial segment target duration.
  1325. this.partialTargetDuration_ = Number(
  1326. partialTargetDurationTag.getRequiredAttrValue('PART-TARGET'));
  1327. this.minTargetDuration_ = Math.min(
  1328. this.partialTargetDuration_, this.minTargetDuration_);
  1329. // Get the server-recommended min distance from the live edge.
  1330. const serverControlTag = shaka.hls.Utils.getFirstTagWithName(
  1331. playlist.tags, 'EXT-X-SERVER-CONTROL');
  1332. // Use 'PART-HOLD-BACK' as the presentation delay for low latency mode.
  1333. this.lowLatencyPresentationDelay_ = serverControlTag ? Number(
  1334. serverControlTag.getRequiredAttrValue('PART-HOLD-BACK')) : 0;
  1335. } else {
  1336. // For regular HLS, use the target duration of regular segments.
  1337. this.minTargetDuration_ = Math.min(
  1338. targetDuration, this.minTargetDuration_);
  1339. }
  1340. // 2. Update the longest target duration if need be to use as a
  1341. // presentation delay later.
  1342. this.maxTargetDuration_ = Math.max(
  1343. targetDuration, this.maxTargetDuration_);
  1344. }
  1345. }
  1346. /**
  1347. * @private
  1348. */
  1349. createPresentationTimeline_() {
  1350. if (this.isLive_()) {
  1351. // The live edge will be calculated from segments, so we don't need to
  1352. // set a presentation start time. We will assert later that this is
  1353. // working as expected.
  1354. // The HLS spec (RFC 8216) states in 6.3.3:
  1355. //
  1356. // "The client SHALL choose which Media Segment to play first ... the
  1357. // client SHOULD NOT choose a segment that starts less than three target
  1358. // durations from the end of the Playlist file. Doing so can trigger
  1359. // playback stalls."
  1360. //
  1361. // We accomplish this in our DASH-y model by setting a presentation
  1362. // delay of configured value, or 3 segments duration if not configured.
  1363. // This will be the "live edge" of the presentation.
  1364. let presentationDelay;
  1365. if (this.config_.defaultPresentationDelay) {
  1366. presentationDelay = this.config_.defaultPresentationDelay;
  1367. } else if (this.lowLatencyPresentationDelay_) {
  1368. presentationDelay = this.lowLatencyPresentationDelay_;
  1369. } else {
  1370. presentationDelay = this.maxTargetDuration_ * 3;
  1371. }
  1372. this.presentationTimeline_ = new shaka.media.PresentationTimeline(
  1373. /* presentationStartTime= */ 0, /* delay= */ presentationDelay);
  1374. this.presentationTimeline_.setStatic(false);
  1375. } else {
  1376. this.presentationTimeline_ = new shaka.media.PresentationTimeline(
  1377. /* presentationStartTime= */ null, /* delay= */ 0);
  1378. this.presentationTimeline_.setStatic(true);
  1379. }
  1380. this.notifySegments_();
  1381. }
  1382. /**
  1383. * Get the InitSegmentReference for a segment if it has a EXT-X-MAP tag.
  1384. * @param {string} playlistUri The absolute uri of the media playlist.
  1385. * @param {!Array.<!shaka.hls.Tag>} tags Segment tags
  1386. * @param {!Map.<string, string>} variables
  1387. * @return {shaka.media.InitSegmentReference}
  1388. * @private
  1389. */
  1390. getInitSegmentReference_(playlistUri, tags, variables) {
  1391. /** @type {?shaka.hls.Tag} */
  1392. const mapTag = shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-MAP');
  1393. if (!mapTag) {
  1394. return null;
  1395. }
  1396. // Map tag example: #EXT-X-MAP:URI="main.mp4",BYTERANGE="720@0"
  1397. const verbatimInitSegmentUri = mapTag.getRequiredAttrValue('URI');
  1398. const absoluteInitSegmentUri = this.variableSubstitution_(
  1399. shaka.hls.Utils.constructAbsoluteUri(
  1400. playlistUri, verbatimInitSegmentUri),
  1401. variables);
  1402. const mapTagKey = [
  1403. absoluteInitSegmentUri,
  1404. mapTag.getAttributeValue('BYTERANGE', ''),
  1405. ].join('-');
  1406. if (!this.mapTagToInitSegmentRefMap_.has(mapTagKey)) {
  1407. const initSegmentRef = this.createInitSegmentReference_(
  1408. absoluteInitSegmentUri, mapTag);
  1409. this.mapTagToInitSegmentRefMap_.set(mapTagKey, initSegmentRef);
  1410. }
  1411. return this.mapTagToInitSegmentRefMap_.get(mapTagKey);
  1412. }
  1413. /**
  1414. * Create an InitSegmentReference object for the EXT-X-MAP tag in the media
  1415. * playlist.
  1416. * @param {string} absoluteInitSegmentUri
  1417. * @param {!shaka.hls.Tag} mapTag EXT-X-MAP
  1418. * @return {!shaka.media.InitSegmentReference}
  1419. * @private
  1420. */
  1421. createInitSegmentReference_(absoluteInitSegmentUri, mapTag) {
  1422. let startByte = 0;
  1423. let endByte = null;
  1424. const byterange = mapTag.getAttributeValue('BYTERANGE');
  1425. // If a BYTERANGE attribute is not specified, the segment consists
  1426. // of the entire resource.
  1427. if (byterange) {
  1428. const blocks = byterange.split('@');
  1429. const byteLength = Number(blocks[0]);
  1430. startByte = Number(blocks[1]);
  1431. endByte = startByte + byteLength - 1;
  1432. }
  1433. const initSegmentRef = new shaka.media.InitSegmentReference(
  1434. () => [absoluteInitSegmentUri],
  1435. startByte,
  1436. endByte);
  1437. return initSegmentRef;
  1438. }
  1439. /**
  1440. * Parses one shaka.hls.Segment object into a shaka.media.SegmentReference.
  1441. *
  1442. * @param {shaka.media.InitSegmentReference} initSegmentReference
  1443. * @param {shaka.media.SegmentReference} previousReference
  1444. * @param {!shaka.hls.Segment} hlsSegment
  1445. * @param {number} startTime
  1446. * @param {number} timestampOffset
  1447. * @param {!Map.<string, string>} variables
  1448. * @param {string} absoluteMediaPlaylistUri
  1449. * @return {!shaka.media.SegmentReference}
  1450. * @private
  1451. */
  1452. createSegmentReference_(
  1453. initSegmentReference, previousReference, hlsSegment, startTime,
  1454. timestampOffset, variables, absoluteMediaPlaylistUri) {
  1455. const tags = hlsSegment.tags;
  1456. const absoluteSegmentUri = this.variableSubstitution_(
  1457. hlsSegment.absoluteUri, variables);
  1458. const extinfTag =
  1459. shaka.hls.Utils.getFirstTagWithName(tags, 'EXTINF');
  1460. let endTime = 0;
  1461. let startByte = 0;
  1462. let endByte = null;
  1463. // Create SegmentReferences for the partial segments.
  1464. const partialSegmentRefs = [];
  1465. if (this.lowLatencyMode_ && hlsSegment.partialSegments.length) {
  1466. for (let i = 0; i < hlsSegment.partialSegments.length; i++) {
  1467. const item = hlsSegment.partialSegments[i];
  1468. const pPreviousReference = i == 0 ?
  1469. previousReference : partialSegmentRefs[partialSegmentRefs.length - 1];
  1470. const pStartTime = (i == 0) ? startTime : pPreviousReference.endTime;
  1471. const pDuration = Number(item.getAttributeValue('DURATION'));
  1472. // A preload hinted partial segment doesn't have duration information,
  1473. // so its startTime and endTime are the same.
  1474. const pEndTime = pStartTime + pDuration;
  1475. let pStartByte = 0;
  1476. let pEndByte = null;
  1477. if (item.name == 'EXT-X-PRELOAD-HINT') {
  1478. // A preload hinted partial segment may have byterange start info.
  1479. const pByterangeStart = item.getAttributeValue('BYTERANGE-START');
  1480. pStartByte = pByterangeStart ? Number(pByterangeStart) : 0;
  1481. } else {
  1482. const pByterange = item.getAttributeValue('BYTERANGE');
  1483. [pStartByte, pEndByte] =
  1484. this.parseByteRange_(pPreviousReference, pByterange);
  1485. }
  1486. const pUri = item.getAttributeValue('URI');
  1487. if (!pUri) {
  1488. continue;
  1489. }
  1490. const pAbsoluteUri = shaka.hls.Utils.constructAbsoluteUri(
  1491. absoluteMediaPlaylistUri, pUri);
  1492. const partial = new shaka.media.SegmentReference(
  1493. pStartTime,
  1494. pEndTime,
  1495. () => [pAbsoluteUri],
  1496. pStartByte,
  1497. pEndByte,
  1498. initSegmentReference,
  1499. timestampOffset,
  1500. /* appendWindowStart= */ 0,
  1501. /* appendWindowEnd= */ Infinity);
  1502. partialSegmentRefs.push(partial);
  1503. } // for-loop of hlsSegment.partialSegments
  1504. } else {
  1505. // EXTINF tag must be available if the segment has no partial segments.
  1506. if (!extinfTag) {
  1507. throw new shaka.util.Error(
  1508. shaka.util.Error.Severity.CRITICAL,
  1509. shaka.util.Error.Category.MANIFEST,
  1510. shaka.util.Error.Code.HLS_REQUIRED_TAG_MISSING, 'EXTINF');
  1511. }
  1512. }
  1513. // If the segment has EXTINF tag, set the segment's end time, start byte
  1514. // and end byte based on the duration and byterange information.
  1515. // Otherwise, calculate the end time, start / end byte based on its partial
  1516. // segments.
  1517. // Note that the sum of partial segments durations may be slightly different
  1518. // from the parent segment's duration. In this case, use the duration from
  1519. // the parent segment tag.
  1520. if (extinfTag) {
  1521. // The EXTINF tag format is '#EXTINF:<duration>,[<title>]'.
  1522. // We're interested in the duration part.
  1523. const extinfValues = extinfTag.value.split(',');
  1524. const duration = Number(extinfValues[0]);
  1525. endTime = startTime + duration;
  1526. } else {
  1527. endTime = partialSegmentRefs[partialSegmentRefs.length - 1].endTime;
  1528. }
  1529. // If the segment has EXT-X-BYTERANGE tag, set the start byte and end byte
  1530. // base on the byterange information. If segment has no EXT-X-BYTERANGE tag
  1531. // and has partial segments, set the start byte and end byte base on the
  1532. // partial segments.
  1533. const byterangeTag =
  1534. shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-BYTERANGE');
  1535. if (byterangeTag) {
  1536. [startByte, endByte] =
  1537. this.parseByteRange_(previousReference, byterangeTag.value);
  1538. } else if (partialSegmentRefs.length) {
  1539. startByte = partialSegmentRefs[0].startByte;
  1540. endByte = partialSegmentRefs[partialSegmentRefs.length - 1].endByte;
  1541. }
  1542. return new shaka.media.SegmentReference(
  1543. startTime,
  1544. endTime,
  1545. () => absoluteSegmentUri.length ? [absoluteSegmentUri] : [],
  1546. startByte,
  1547. endByte,
  1548. initSegmentReference,
  1549. timestampOffset,
  1550. /* appendWindowStart= */ 0,
  1551. /* appendWindowEnd= */ Infinity,
  1552. partialSegmentRefs,
  1553. );
  1554. }
  1555. /**
  1556. * Parse the startByte and endByte.
  1557. * @param {shaka.media.SegmentReference} previousReference
  1558. * @param {?string} byterange
  1559. * @return {!Array.<number>} An array with the start byte and end byte.
  1560. * @private
  1561. */
  1562. parseByteRange_(previousReference, byterange) {
  1563. let startByte = 0;
  1564. let endByte = null;
  1565. // If BYTERANGE is not specified, the segment consists of the entire
  1566. // resource.
  1567. if (byterange) {
  1568. const blocks = byterange.split('@');
  1569. const byteLength = Number(blocks[0]);
  1570. if (blocks[1]) {
  1571. startByte = Number(blocks[1]);
  1572. } else {
  1573. goog.asserts.assert(previousReference,
  1574. 'Cannot refer back to previous HLS segment!');
  1575. startByte = previousReference.endByte + 1;
  1576. }
  1577. endByte = startByte + byteLength - 1;
  1578. }
  1579. return [startByte, endByte];
  1580. }
  1581. /** @private */
  1582. notifySegments_() {
  1583. // The presentation timeline may or may not be set yet.
  1584. // If it does not yet exist, hold onto the segments until it does.
  1585. if (!this.presentationTimeline_) {
  1586. return;
  1587. }
  1588. for (const segments of this.segmentsToNotifyByStream_) {
  1589. this.presentationTimeline_.notifySegments(segments);
  1590. }
  1591. this.segmentsToNotifyByStream_ = [];
  1592. }
  1593. /**
  1594. * Parses shaka.hls.Segment objects into shaka.media.SegmentReferences.
  1595. *
  1596. * @param {string} verbatimMediaPlaylistUri
  1597. * @param {!shaka.hls.Playlist} playlist
  1598. * @param {string} type
  1599. * @param {string} mimeType
  1600. * @param {!Map.<number, number>} mediaSequenceToStartTime
  1601. * @param {!Map.<string, string>} variables
  1602. * @param {!Map.<number, number>} discontinuityToMediaSequence
  1603. * @return {!Promise<!Array.<!shaka.media.SegmentReference>>}
  1604. * @private
  1605. */
  1606. async createSegments_(verbatimMediaPlaylistUri, playlist, type, mimeType,
  1607. mediaSequenceToStartTime, variables, discontinuityToMediaSequence) {
  1608. /** @type {Array.<!shaka.hls.Segment>} */
  1609. const hlsSegments = playlist.segments;
  1610. goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
  1611. /** @type {shaka.media.InitSegmentReference} */
  1612. let initSegmentRef;
  1613. // We may need to look at the media itself to determine a segment start
  1614. // time.
  1615. const mediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber(
  1616. playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0);
  1617. const skipTag = shaka.hls.Utils.getFirstTagWithName(playlist.tags,
  1618. 'EXT-X-SKIP');
  1619. const skippedSegments =
  1620. skipTag ? Number(skipTag.getAttributeValue('SKIPPED-SEGMENTS')) : 0;
  1621. let position = mediaSequenceNumber + skippedSegments;
  1622. let firstStartTime;
  1623. // For live stream, use the cached value in the mediaSequenceToStartTime
  1624. // map if available.
  1625. // Since createSegments_() is asynchronous and we are updating the streams
  1626. // in parallel, the global playlistStartTime_ may get updated by other
  1627. // playlist updates rather than the current one.
  1628. if (this.isLive_() && mediaSequenceToStartTime.has(position)) {
  1629. firstStartTime = mediaSequenceToStartTime.get(position);
  1630. } else {
  1631. if (this.playlistStartTime_ == null) {
  1632. // For VOD and EVENT playlists, all variants must start at the same
  1633. // time, so we can fetch the start time once and reuse for the others.
  1634. // This is not guaranteed when updating a LIVE stream. We assume the
  1635. // first segment in each live playlist is no more than one segment out
  1636. // of sync with the other playlists, so we can fetch the start time for
  1637. // once.
  1638. initSegmentRef = this.getInitSegmentReference_(
  1639. playlist.absoluteUri, hlsSegments[0].tags, variables);
  1640. goog.asserts.assert(
  1641. type != shaka.util.ManifestParserUtils.ContentType.TEXT,
  1642. 'Should only get start time from audio or video streams');
  1643. this.playlistStartTime_ = await this.getStartTime_(
  1644. verbatimMediaPlaylistUri, initSegmentRef, mimeType,
  1645. position, /* isDiscontinuity= */ false,
  1646. hlsSegments[0], variables);
  1647. }
  1648. firstStartTime = this.playlistStartTime_;
  1649. }
  1650. const firstSegmentUri = hlsSegments[0].absoluteUri;
  1651. shaka.log.debug('First segment', firstSegmentUri.split('/').pop(),
  1652. 'starts at', firstStartTime);
  1653. let discontintuitySequenceNum = shaka.hls.Utils.getFirstTagWithNameAsNumber(
  1654. playlist.tags, 'EXT-X-DISCONTINUITY-SEQUENCE');
  1655. if (this.lowLatencyMode_) {
  1656. if (!discontinuityToMediaSequence.has(discontintuitySequenceNum)) {
  1657. discontinuityToMediaSequence.set(discontintuitySequenceNum, position);
  1658. }
  1659. if (skippedSegments) {
  1660. // With delta updates, the DISCONTINUITY may be skipped. Check if
  1661. // the discontintuity Sequence Number based on the media sequence
  1662. // number.
  1663. const disconMap = discontinuityToMediaSequence;
  1664. while (disconMap.has(discontintuitySequenceNum + 1) &&
  1665. disconMap.get(discontintuitySequenceNum + 1) < position) {
  1666. discontintuitySequenceNum++;
  1667. }
  1668. }
  1669. }
  1670. let timestampOffset =
  1671. this.discontinuityToTso_.get(discontintuitySequenceNum) || 0;
  1672. /** @type {!Array.<!shaka.media.SegmentReference>} */
  1673. const references = [];
  1674. for (let i = 0; i < hlsSegments.length; i++) {
  1675. const item = hlsSegments[i];
  1676. const previousReference = references[references.length - 1];
  1677. const startTime = (i == 0) ? firstStartTime :
  1678. previousReference.endTime;
  1679. position = mediaSequenceNumber + skippedSegments + i;
  1680. mediaSequenceToStartTime.set(position, startTime);
  1681. initSegmentRef = this.getInitSegmentReference_(playlist.absoluteUri,
  1682. item.tags, variables);
  1683. const discontintuityTag = shaka.hls.Utils.getFirstTagWithName(item.tags,
  1684. 'EXT-X-DISCONTINUITY');
  1685. if (discontintuityTag) {
  1686. discontintuitySequenceNum++;
  1687. discontinuityToMediaSequence.set(discontintuitySequenceNum, position);
  1688. // eslint-disable-next-line no-await-in-loop
  1689. timestampOffset = await this.getTimestampOffset_(
  1690. discontintuitySequenceNum, verbatimMediaPlaylistUri, initSegmentRef,
  1691. mimeType, position, item, variables, startTime);
  1692. }
  1693. // If the stream is low latency and the user has not configured the
  1694. // lowLatencyMode, but if it has been configured to activate the
  1695. // lowLatencyMode if a stream of this type is detected, we automatically
  1696. // activate the lowLatencyMode.
  1697. if (!this.lowLatencyMode_) {
  1698. const autoLowLatencyMode = this.playerInterface_.isAutoLowLatencyMode();
  1699. if (autoLowLatencyMode) {
  1700. this.playerInterface_.enableLowLatencyMode();
  1701. this.lowLatencyMode_ = this.playerInterface_.isLowLatencyMode();
  1702. }
  1703. }
  1704. const extinfTag =
  1705. shaka.hls.Utils.getFirstTagWithName(item.tags, 'EXTINF');
  1706. if (this.lowLatencyMode_ || extinfTag) {
  1707. const reference = this.createSegmentReference_(
  1708. initSegmentRef,
  1709. previousReference,
  1710. item,
  1711. startTime,
  1712. timestampOffset,
  1713. variables,
  1714. playlist.absoluteUri);
  1715. references.push(reference);
  1716. } else if (!this.lowLatencyMode_) {
  1717. // If a segment has no extinfTag, it must contain partial segments.
  1718. shaka.log.alwaysWarn('Low-latency HLS live stream detected, but ' +
  1719. 'low-latency streaming mode is not enabled in Shaka Player. ' +
  1720. 'Set streaming.lowLatencyMode configuration to true, and see ' +
  1721. 'https://bit.ly/3clctcj for details.');
  1722. }
  1723. }
  1724. return references;
  1725. }
  1726. /**
  1727. * Gets the start time of the first segment of the playlist from existing
  1728. * value (if possible) or by downloading it and parsing it otherwise.
  1729. *
  1730. * @param {number} discontintuitySequenceNum
  1731. * @param {string} verbatimMediaPlaylistUri
  1732. * @param {shaka.media.InitSegmentReference} initSegmentRef
  1733. * @param {string} mimeType
  1734. * @param {number} mediaSequenceNumber
  1735. * @param {!shaka.hls.Segment} segment
  1736. * @param {!Map.<string, string>} variables
  1737. * @param {number} startTime
  1738. * @return {!Promise.<number>}
  1739. * @throws {shaka.util.Error}
  1740. * @private
  1741. */
  1742. async getTimestampOffset_(discontintuitySequenceNum,
  1743. verbatimMediaPlaylistUri, initSegmentRef,
  1744. mimeType, mediaSequenceNumber, segment, variables, startTime) {
  1745. let timestampOffset = 0;
  1746. if (this.discontinuityToTso_.has(discontintuitySequenceNum)) {
  1747. timestampOffset =
  1748. this.discontinuityToTso_.get(discontintuitySequenceNum);
  1749. } else {
  1750. const mediaStartTime = await this.getStartTime_(
  1751. verbatimMediaPlaylistUri, initSegmentRef, mimeType,
  1752. mediaSequenceNumber, /* isDiscontinuity= */ true, segment,
  1753. variables);
  1754. timestampOffset = startTime - mediaStartTime;
  1755. shaka.log.v1('Segment timestampOffset =', timestampOffset);
  1756. this.discontinuityToTso_.set(
  1757. discontintuitySequenceNum, timestampOffset);
  1758. }
  1759. return timestampOffset;
  1760. }
  1761. /**
  1762. * Try to fetch the starting part of a segment, and fall back to a full
  1763. * segment if we have to.
  1764. *
  1765. * @param {!shaka.media.AnySegmentReference} reference
  1766. * @return {!Promise.<shaka.extern.Response>}
  1767. * @private
  1768. */
  1769. async fetchStartOfSegment_(reference) {
  1770. const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
  1771. // Create two requests:
  1772. // 1. A partial request meant to fetch the smallest part of the segment
  1773. // required to get the time stamp.
  1774. // 2. A full request meant as a fallback for when the server does not
  1775. // support partial requests.
  1776. const fullRequest = shaka.util.Networking.createSegmentRequest(
  1777. reference.getUris(),
  1778. reference.startByte,
  1779. reference.endByte,
  1780. this.config_.retryParameters);
  1781. if (this.config_.hls.useFullSegmentsForStartTime) {
  1782. return this.makeNetworkRequest_(fullRequest, requestType);
  1783. }
  1784. const partialRequest = shaka.util.Networking.createSegmentRequest(
  1785. reference.getUris(),
  1786. reference.startByte,
  1787. reference.startByte + shaka.hls.HlsParser.START_OF_SEGMENT_SIZE_ - 1,
  1788. this.config_.retryParameters);
  1789. // TODO(vaage): The need to do fall back requests is not likely to be unique
  1790. // to here. It would be nice if the fallback(s) could be included into
  1791. // the same abortable operation as the original request.
  1792. //
  1793. // What would need to change with networking engine to support requests
  1794. // with fallback(s)?
  1795. try {
  1796. const response = await this.makeNetworkRequest_(
  1797. partialRequest, requestType);
  1798. return response;
  1799. } catch (e) {
  1800. // If the networking operation was aborted, we don't want to treat it as
  1801. // a request failure. We surface the error so that the OPERATION_ABORTED
  1802. // error will be handled correctly.
  1803. if (e.code == shaka.util.Error.Code.OPERATION_ABORTED) {
  1804. throw e;
  1805. }
  1806. // The partial request may fail for a number of reasons.
  1807. // Some servers do not support Range requests, and others do not support
  1808. // the OPTIONS request which must be made before any cross-origin Range
  1809. // request. Since this fallback is expensive, warn the app developer.
  1810. shaka.log.alwaysWarn('Unable to fetch the starting part of HLS ' +
  1811. 'segment! Falling back to a full segment request, ' +
  1812. 'which is expensive! Your server should ' +
  1813. 'support Range requests and CORS preflights.',
  1814. partialRequest.uris[0]);
  1815. const response = await this.makeNetworkRequest_(fullRequest, requestType);
  1816. return response;
  1817. }
  1818. }
  1819. /**
  1820. * Gets the start time of a segment from the existing manifest (if possible)
  1821. * or by downloading it and parsing it otherwise.
  1822. *
  1823. * @param {string} verbatimMediaPlaylistUri
  1824. * @param {shaka.media.InitSegmentReference} initSegmentRef
  1825. * @param {string} mimeType
  1826. * @param {number} mediaSequenceNumber
  1827. * @param {boolean} isDiscontinuity
  1828. * @param {!shaka.hls.Segment} segment
  1829. * @param {!Map.<string, string>} variables
  1830. * @return {!Promise.<number>}
  1831. * @private
  1832. */
  1833. async getStartTime_(
  1834. verbatimMediaPlaylistUri, initSegmentRef, mimeType, mediaSequenceNumber,
  1835. isDiscontinuity, segment, variables) {
  1836. const segmentRef = this.createSegmentReference_(
  1837. initSegmentRef,
  1838. /* previousReference= */ null,
  1839. segment,
  1840. /* startTime= */ 0,
  1841. /* timestampOffset= */ 0,
  1842. variables,
  1843. /* absoluteMediaPlaylistUri= */ '');
  1844. // If we are updating the manifest, we can usually skip fetching the segment
  1845. // by examining the references we already have. This won't be possible if
  1846. // there was some kind of lag or delay updating the manifest on the server,
  1847. // in which extreme case we would fall back to fetching a segment. This
  1848. // allows us to both avoid fetching segments when possible, and recover from
  1849. // certain server-side issues gracefully.
  1850. // Do not use cached start time for the segments with discontinuity tags.
  1851. if (this.manifest_ && !isDiscontinuity) {
  1852. const streamInfo =
  1853. this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
  1854. const startTime = streamInfo.mediaSequenceToStartTime.get(
  1855. mediaSequenceNumber);
  1856. if (startTime != undefined) {
  1857. // We found it! Avoid fetching and parsing the segment.
  1858. shaka.log.v1('Found segment start time in previous manifest',
  1859. startTime);
  1860. return startTime;
  1861. }
  1862. shaka.log.debug(
  1863. 'Unable to find segment start time in previous manifest!');
  1864. }
  1865. // TODO: Introduce a new tag to extend HLS and provide the first segment's
  1866. // start time. This will avoid the need for these fetches in content
  1867. // packaged with Shaka Packager. This web-friendly extension to HLS can
  1868. // then be proposed to Apple for inclusion in a future version of HLS.
  1869. // See https://github.com/google/shaka-packager/issues/294
  1870. shaka.log.v1('Fetching segment to find start time');
  1871. mimeType = mimeType.toLowerCase();
  1872. if (shaka.hls.HlsParser.RAW_FORMATS_.includes(mimeType)) {
  1873. // Raw formats contain no timestamps. Even if there is an ID3 tag with a
  1874. // timestamp, that's not going to be honored by MediaSource, which will
  1875. // use sequence mode for these segments. We don't yet support sequence
  1876. // mode, so we must reject these streams.
  1877. // TODO(#2337): Support sequence mode and align raw format timestamps to
  1878. // other streams.
  1879. shaka.log.alwaysWarn(
  1880. 'Raw formats are not yet supported. Skipping ' + mimeType);
  1881. throw new shaka.util.Error(
  1882. shaka.util.Error.Severity.RECOVERABLE,
  1883. shaka.util.Error.Category.MANIFEST,
  1884. shaka.util.Error.Code.HLS_INTERNAL_SKIP_STREAM);
  1885. }
  1886. if (mimeType == 'video/webm') {
  1887. shaka.log.alwaysWarn('WebM in HLS is not yet supported. Skipping.');
  1888. throw new shaka.util.Error(
  1889. shaka.util.Error.Severity.RECOVERABLE,
  1890. shaka.util.Error.Category.MANIFEST,
  1891. shaka.util.Error.Code.HLS_INTERNAL_SKIP_STREAM);
  1892. }
  1893. if (mimeType == 'video/mp4' || mimeType == 'audio/mp4') {
  1894. // We also need the init segment to get the correct timescale. But if the
  1895. // stream is self-initializing, use the same response for both.
  1896. const fetches = [this.fetchStartOfSegment_(segmentRef)];
  1897. if (initSegmentRef) {
  1898. fetches.push(this.fetchStartOfSegment_(initSegmentRef));
  1899. }
  1900. const responses = await Promise.all(fetches);
  1901. // If the stream is self-initializing, use the main segment in-place of
  1902. // the init segment.
  1903. const segmentResponse = responses[0];
  1904. const initSegmentResponse = responses[1] || responses[0];
  1905. return this.getStartTimeFromMp4Segment_(
  1906. verbatimMediaPlaylistUri, segmentResponse.uri,
  1907. segmentResponse.data, initSegmentResponse.data);
  1908. }
  1909. if (mimeType == 'video/mp2t') {
  1910. const response = await this.fetchStartOfSegment_(segmentRef);
  1911. goog.asserts.assert(response.data, 'Should have a response body!');
  1912. return this.getStartTimeFromTsSegment_(
  1913. verbatimMediaPlaylistUri, response.uri, response.data);
  1914. }
  1915. throw new shaka.util.Error(
  1916. shaka.util.Error.Severity.CRITICAL,
  1917. shaka.util.Error.Category.MANIFEST,
  1918. shaka.util.Error.Code.HLS_COULD_NOT_PARSE_SEGMENT_START_TIME,
  1919. verbatimMediaPlaylistUri);
  1920. }
  1921. /**
  1922. * Parses an mp4 segment to get its start time.
  1923. *
  1924. * @param {string} playlistUri
  1925. * @param {string} segmentUri
  1926. * @param {BufferSource} mediaData
  1927. * @param {BufferSource} initData
  1928. * @return {number}
  1929. * @private
  1930. */
  1931. getStartTimeFromMp4Segment_(playlistUri, segmentUri, mediaData, initData) {
  1932. const Mp4Parser = shaka.util.Mp4Parser;
  1933. let timescale = 0;
  1934. new Mp4Parser()
  1935. .box('moov', Mp4Parser.children)
  1936. .box('trak', Mp4Parser.children)
  1937. .box('mdia', Mp4Parser.children)
  1938. .fullBox('mdhd', (box) => {
  1939. goog.asserts.assert(
  1940. box.version == 0 || box.version == 1,
  1941. 'MDHD version can only be 0 or 1');
  1942. const parsedMDHDBox = shaka.util.Mp4BoxParsers.parseMDHD(
  1943. box.reader, box.version);
  1944. timescale = parsedMDHDBox.timescale;
  1945. box.parser.stop();
  1946. }).parse(initData, /* partialOkay= */ true);
  1947. if (!timescale) {
  1948. shaka.log.error('Unable to find timescale in init segment!');
  1949. throw new shaka.util.Error(
  1950. shaka.util.Error.Severity.CRITICAL,
  1951. shaka.util.Error.Category.MANIFEST,
  1952. shaka.util.Error.Code.HLS_COULD_NOT_PARSE_SEGMENT_START_TIME,
  1953. playlistUri, segmentUri);
  1954. }
  1955. let startTime = 0;
  1956. let parsedMedia = false;
  1957. new Mp4Parser()
  1958. .box('moof', Mp4Parser.children)
  1959. .box('traf', Mp4Parser.children)
  1960. .fullBox('tfdt', (box) => {
  1961. goog.asserts.assert(
  1962. box.version == 0 || box.version == 1,
  1963. 'TFDT version can only be 0 or 1');
  1964. const parsedTFDTBox = shaka.util.Mp4BoxParsers.parseTFDT(
  1965. box.reader, box.version);
  1966. const baseTime = parsedTFDTBox.baseMediaDecodeTime;
  1967. startTime = baseTime / timescale;
  1968. parsedMedia = true;
  1969. box.parser.stop();
  1970. }).parse(mediaData, /* partialOkay= */ true);
  1971. if (!parsedMedia) {
  1972. throw new shaka.util.Error(
  1973. shaka.util.Error.Severity.CRITICAL,
  1974. shaka.util.Error.Category.MANIFEST,
  1975. shaka.util.Error.Code.HLS_COULD_NOT_PARSE_SEGMENT_START_TIME,
  1976. playlistUri, segmentUri);
  1977. }
  1978. return startTime;
  1979. }
  1980. /**
  1981. * Parses a TS segment to get its start time.
  1982. *
  1983. * @param {string} playlistUri
  1984. * @param {string} segmentUri
  1985. * @param {BufferSource} data
  1986. * @return {number}
  1987. * @private
  1988. */
  1989. getStartTimeFromTsSegment_(playlistUri, segmentUri, data) {
  1990. const reader = new shaka.util.DataViewReader(
  1991. data, shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
  1992. const fail = () => {
  1993. throw new shaka.util.Error(
  1994. shaka.util.Error.Severity.CRITICAL,
  1995. shaka.util.Error.Category.MANIFEST,
  1996. shaka.util.Error.Code.HLS_COULD_NOT_PARSE_SEGMENT_START_TIME,
  1997. playlistUri, segmentUri);
  1998. };
  1999. let packetStart = 0;
  2000. let syncByte = 0;
  2001. const skipPacket = () => {
  2002. // 188-byte packets are standard, so assume that.
  2003. reader.seek(packetStart + 188);
  2004. syncByte = reader.readUint8();
  2005. if (syncByte != 0x47) {
  2006. // We haven't found the sync byte, so try it as a 192-byte packet.
  2007. reader.seek(packetStart + 192);
  2008. syncByte = reader.readUint8();
  2009. }
  2010. if (syncByte != 0x47) {
  2011. // We still haven't found the sync byte, so try as a 204-byte packet.
  2012. reader.seek(packetStart + 204);
  2013. syncByte = reader.readUint8();
  2014. }
  2015. if (syncByte != 0x47) {
  2016. // We still haven't found the sync byte, so the packet was of a
  2017. // non-standard size.
  2018. fail();
  2019. }
  2020. // Put the sync byte back so we can read it in the next loop.
  2021. reader.rewind(1);
  2022. };
  2023. // We will look a few packet-lengths forward to find the first sync byte.
  2024. // Note that we are using this method on what is already a subset of the
  2025. // file (the first |shaka.hls.HlsParser.START_OF_SEGMENT_SIZE_| bytes), so
  2026. // we can't look too far ahead to begin with.
  2027. let syncByteScanLength = Math.min(reader.getLength() - 188, 5 * 188);
  2028. // TODO: refactor this while loop for better readability.
  2029. // eslint-disable-next-line no-constant-condition
  2030. while (true) {
  2031. // Format reference: https://bit.ly/TsPacket
  2032. packetStart = reader.getPosition();
  2033. syncByte = reader.readUint8();
  2034. if (syncByte != 0x47) {
  2035. if (syncByteScanLength > 0) {
  2036. // This file could have started with a cut-off TS packet. Scan forward
  2037. // until we find a sync byte.
  2038. syncByteScanLength -= 1;
  2039. continue;
  2040. }
  2041. fail();
  2042. }
  2043. // If we've found a sync byte, stop scanning forward for future packets.
  2044. syncByteScanLength = 0;
  2045. const flagsAndPacketId = reader.readUint16();
  2046. const packetId = flagsAndPacketId & 0x1fff;
  2047. if (packetId == 0x1fff) {
  2048. // A "null" TS packet. Skip this TS packet and try again.
  2049. skipPacket();
  2050. continue;
  2051. }
  2052. const hasPesPacket = flagsAndPacketId & 0x4000;
  2053. if (!hasPesPacket) {
  2054. // Not a PES packet yet. Skip this TS packet and try again.
  2055. skipPacket();
  2056. continue;
  2057. }
  2058. const flags = reader.readUint8();
  2059. const adaptationFieldControl = (flags & 0x30) >> 4;
  2060. if (adaptationFieldControl == 0 /* reserved */ ||
  2061. adaptationFieldControl == 2 /* adaptation field, no payload */) {
  2062. fail();
  2063. }
  2064. if (adaptationFieldControl == 3) {
  2065. // Skip over adaptation field.
  2066. const length = reader.readUint8();
  2067. reader.skip(length);
  2068. }
  2069. // Now we come to the PES header (hopefully).
  2070. // Format reference: https://bit.ly/TsPES
  2071. const startCode = reader.readUint32();
  2072. const startCodePrefix = startCode >> 8;
  2073. if (startCodePrefix != 1) {
  2074. // Not a PES packet yet. Skip this TS packet and try again.
  2075. skipPacket();
  2076. continue;
  2077. }
  2078. // Skip the 16-bit PES length and the first 8 bits of the optional header.
  2079. reader.skip(3);
  2080. // The next 8 bits contain flags about DTS & PTS.
  2081. const ptsDtsIndicator = reader.readUint8() >> 6;
  2082. if (ptsDtsIndicator == 0 /* no timestamp */ ||
  2083. ptsDtsIndicator == 1 /* forbidden */) {
  2084. fail();
  2085. }
  2086. const pesHeaderLengthRemaining = reader.readUint8();
  2087. if (pesHeaderLengthRemaining == 0) {
  2088. fail();
  2089. }
  2090. if (ptsDtsIndicator == 2 /* PTS only */) {
  2091. goog.asserts.assert(pesHeaderLengthRemaining == 5, 'Bad PES header?');
  2092. } else if (ptsDtsIndicator == 3 /* PTS and DTS */) {
  2093. goog.asserts.assert(pesHeaderLengthRemaining == 10, 'Bad PES header?');
  2094. }
  2095. const pts0 = reader.readUint8();
  2096. const pts1 = reader.readUint16();
  2097. const pts2 = reader.readUint16();
  2098. // Reconstruct 33-bit PTS from the 5-byte, padded structure.
  2099. const ptsHigh3 = (pts0 & 0x0e) >> 1;
  2100. const ptsLow30 = ((pts1 & 0xfffe) << 14) | ((pts2 & 0xfffe) >> 1);
  2101. // Reconstruct the PTS as a float. Avoid bitwise operations to combine
  2102. // because bitwise ops treat the values as 32-bit ints.
  2103. const pts = ptsHigh3 * (1 << 30) + ptsLow30;
  2104. return pts / shaka.hls.HlsParser.TS_TIMESCALE_;
  2105. }
  2106. }
  2107. /**
  2108. * Replaces the variables of a given URI.
  2109. *
  2110. * @param {string} uri
  2111. * @param {!Map.<string, string>} variables
  2112. * @return {string}
  2113. * @private
  2114. */
  2115. variableSubstitution_(uri, variables) {
  2116. let newUri = String(uri).replace(/%7B/g, '{').replace(/%7D/g, '}');
  2117. const uriVariables = newUri.match(/{\$\w*}/g);
  2118. if (uriVariables) {
  2119. for (const variable of uriVariables) {
  2120. // Note: All variables have the structure {$...}
  2121. const variableName = variable.slice(2, variable.length - 1);
  2122. const replaceValue = variables.get(variableName);
  2123. if (replaceValue) {
  2124. newUri = newUri.replace(variable, replaceValue);
  2125. } else {
  2126. shaka.log.error('A variable has been found that is not declared',
  2127. variableName);
  2128. throw new shaka.util.Error(
  2129. shaka.util.Error.Severity.CRITICAL,
  2130. shaka.util.Error.Category.MANIFEST,
  2131. shaka.util.Error.Code.HLS_VARIABLE_NOT_FOUND,
  2132. variableName);
  2133. }
  2134. }
  2135. }
  2136. return newUri;
  2137. }
  2138. /**
  2139. * Attempts to guess stream's mime type based on content type and URI.
  2140. *
  2141. * @param {string} contentType
  2142. * @param {string} codecs
  2143. * @param {!shaka.hls.Playlist} playlist
  2144. * @param {!Map.<string, string>} variables
  2145. * @return {!Promise.<string>}
  2146. * @private
  2147. */
  2148. async guessMimeType_(contentType, codecs, playlist, variables) {
  2149. const HlsParser = shaka.hls.HlsParser;
  2150. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  2151. const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
  2152. goog.asserts.assert(playlist.segments.length,
  2153. 'Playlist should have segments!');
  2154. const firstSegmentUri = this.variableSubstitution_(
  2155. playlist.segments[0].absoluteUri, variables);
  2156. const parsedUri = new goog.Uri(firstSegmentUri);
  2157. const extension = parsedUri.getPath().split('.').pop();
  2158. const map = HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType];
  2159. const mimeType = map[extension];
  2160. if (mimeType) {
  2161. return mimeType;
  2162. }
  2163. if (contentType == ContentType.TEXT) {
  2164. // The extension map didn't work.
  2165. if (!codecs || codecs == 'vtt' || codecs == 'wvtt') {
  2166. // If codecs is 'vtt', it's WebVTT.
  2167. // If there was no codecs string, assume HLS text streams are WebVTT.
  2168. return 'text/vtt';
  2169. } else {
  2170. // Otherwise, assume MP4-embedded text, since text-based formats tend
  2171. // not to have a codecs string at all.
  2172. return 'application/mp4';
  2173. }
  2174. }
  2175. // If unable to guess mime type, request a segment and try getting it
  2176. // from the response.
  2177. const headRequest = shaka.net.NetworkingEngine.makeRequest(
  2178. [firstSegmentUri], this.config_.retryParameters);
  2179. headRequest.method = 'HEAD';
  2180. const response = await this.makeNetworkRequest_(
  2181. headRequest, requestType);
  2182. const contentMimeType = response.headers['content-type'];
  2183. if (!contentMimeType) {
  2184. // If the HLS content is lacking in both MIME type metadata and
  2185. // segment file extensions, we fall back to assuming it's MP4.
  2186. const fallbackMimeType = map['mp4'];
  2187. return fallbackMimeType;
  2188. }
  2189. // Split the MIME type in case the server sent additional parameters.
  2190. return contentMimeType.split(';')[0];
  2191. }
  2192. /**
  2193. * Returns a tag with a given name.
  2194. * Throws an error if tag was not found.
  2195. *
  2196. * @param {!Array.<shaka.hls.Tag>} tags
  2197. * @param {string} tagName
  2198. * @return {!shaka.hls.Tag}
  2199. * @private
  2200. */
  2201. getRequiredTag_(tags, tagName) {
  2202. const tag = shaka.hls.Utils.getFirstTagWithName(tags, tagName);
  2203. if (!tag) {
  2204. throw new shaka.util.Error(
  2205. shaka.util.Error.Severity.CRITICAL,
  2206. shaka.util.Error.Category.MANIFEST,
  2207. shaka.util.Error.Code.HLS_REQUIRED_TAG_MISSING, tagName);
  2208. }
  2209. return tag;
  2210. }
  2211. /**
  2212. * @param {shaka.extern.Stream} stream
  2213. * @param {?string} width
  2214. * @param {?string} height
  2215. * @param {?string} frameRate
  2216. * @param {?string} videoRange
  2217. * @private
  2218. */
  2219. addVideoAttributes_(stream, width, height, frameRate, videoRange) {
  2220. if (stream) {
  2221. stream.width = Number(width) || undefined;
  2222. stream.height = Number(height) || undefined;
  2223. stream.frameRate = Number(frameRate) || undefined;
  2224. stream.hdr = videoRange || undefined;
  2225. }
  2226. }
  2227. /**
  2228. * Makes a network request for the manifest and returns a Promise
  2229. * with the resulting data.
  2230. *
  2231. * @param {string} absoluteUri
  2232. * @return {!Promise.<!shaka.extern.Response>}
  2233. * @private
  2234. */
  2235. requestManifest_(absoluteUri) {
  2236. const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
  2237. const request = shaka.net.NetworkingEngine.makeRequest(
  2238. [absoluteUri], this.config_.retryParameters);
  2239. return this.makeNetworkRequest_(request, requestType);
  2240. }
  2241. /**
  2242. * Called when the update timer ticks. Because parsing a manifest is async,
  2243. * this method is async. To work with this, this method will schedule the next
  2244. * update when it finished instead of using a repeating-start.
  2245. *
  2246. * @return {!Promise}
  2247. * @private
  2248. */
  2249. async onUpdate_() {
  2250. shaka.log.info('Updating manifest...');
  2251. goog.asserts.assert(
  2252. this.updatePlaylistDelay_ > 0,
  2253. 'We should only call |onUpdate_| when we are suppose to be updating.');
  2254. // Detect a call to stop()
  2255. if (!this.playerInterface_) {
  2256. return;
  2257. }
  2258. try {
  2259. await this.update();
  2260. const delay = this.updatePlaylistDelay_;
  2261. this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
  2262. } catch (error) {
  2263. // Detect a call to stop() during this.update()
  2264. if (!this.playerInterface_) {
  2265. return;
  2266. }
  2267. goog.asserts.assert(error instanceof shaka.util.Error,
  2268. 'Should only receive a Shaka error');
  2269. // We will retry updating, so override the severity of the error.
  2270. error.severity = shaka.util.Error.Severity.RECOVERABLE;
  2271. this.playerInterface_.onError(error);
  2272. // Try again very soon.
  2273. this.updatePlaylistTimer_.tickAfter(/* seconds= */ 0.1);
  2274. }
  2275. }
  2276. /**
  2277. * @return {boolean}
  2278. * @private
  2279. */
  2280. isLive_() {
  2281. const PresentationType = shaka.hls.HlsParser.PresentationType_;
  2282. return this.presentationType_ != PresentationType.VOD;
  2283. }
  2284. /**
  2285. * @param {shaka.hls.HlsParser.PresentationType_} type
  2286. * @private
  2287. */
  2288. setPresentationType_(type) {
  2289. this.presentationType_ = type;
  2290. if (this.presentationTimeline_) {
  2291. this.presentationTimeline_.setStatic(!this.isLive_());
  2292. }
  2293. // If this manifest is not for live content, then we have no reason to
  2294. // update it.
  2295. if (!this.isLive_()) {
  2296. this.updatePlaylistTimer_.stop();
  2297. }
  2298. }
  2299. /**
  2300. * Create a networking request. This will manage the request using the
  2301. * parser's operation manager. If the parser has already been stopped, the
  2302. * request will not be made.
  2303. *
  2304. * @param {shaka.extern.Request} request
  2305. * @param {shaka.net.NetworkingEngine.RequestType} type
  2306. * @return {!Promise.<shaka.extern.Response>}
  2307. * @private
  2308. */
  2309. makeNetworkRequest_(request, type) {
  2310. if (!this.operationManager_) {
  2311. throw new shaka.util.Error(
  2312. shaka.util.Error.Severity.CRITICAL,
  2313. shaka.util.Error.Category.PLAYER,
  2314. shaka.util.Error.Code.OPERATION_ABORTED);
  2315. }
  2316. const op = this.playerInterface_.networkingEngine.request(type, request);
  2317. this.operationManager_.manage(op);
  2318. return op.promise;
  2319. }
  2320. /**
  2321. * @param {!shaka.hls.Tag} drmTag
  2322. * @return {?shaka.extern.DrmInfo}
  2323. * @private
  2324. */
  2325. static widevineDrmParser_(drmTag) {
  2326. const method = drmTag.getRequiredAttrValue('METHOD');
  2327. const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR'];
  2328. if (!VALID_METHODS.includes(method)) {
  2329. shaka.log.error('Widevine in HLS is only supported with [',
  2330. VALID_METHODS.join(', '), '], not', method);
  2331. return null;
  2332. }
  2333. const uri = drmTag.getRequiredAttrValue('URI');
  2334. const parsedData = shaka.net.DataUriPlugin.parseRaw(uri);
  2335. // The data encoded in the URI is a PSSH box to be used as init data.
  2336. const pssh = shaka.util.BufferUtils.toUint8(parsedData.data);
  2337. const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
  2338. 'com.widevine.alpha', [
  2339. {initDataType: 'cenc', initData: pssh},
  2340. ]);
  2341. const keyId = drmTag.getAttributeValue('KEYID');
  2342. if (keyId) {
  2343. const keyIdLowerCase = keyId.toLowerCase();
  2344. // This value should begin with '0x':
  2345. goog.asserts.assert(
  2346. keyIdLowerCase.startsWith('0x'), 'Incorrect KEYID format!');
  2347. // But the output should not contain the '0x':
  2348. drmInfo.keyIds = new Set([keyIdLowerCase.substr(2)]);
  2349. }
  2350. return drmInfo;
  2351. }
  2352. /**
  2353. * See: https://docs.microsoft.com/en-us/playready/packaging/mp4-based-formats-supported-by-playready-clients?tabs=case4
  2354. *
  2355. * @param {!shaka.hls.Tag} drmTag
  2356. * @return {?shaka.extern.DrmInfo}
  2357. * @private
  2358. */
  2359. static playreadyDrmParser_(drmTag) {
  2360. const method = drmTag.getRequiredAttrValue('METHOD');
  2361. const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR'];
  2362. if (!VALID_METHODS.includes(method)) {
  2363. shaka.log.error('PlayReady in HLS is only supported with [',
  2364. VALID_METHODS.join(', '), '], not', method);
  2365. return null;
  2366. }
  2367. const uri = drmTag.getRequiredAttrValue('URI');
  2368. const parsedData = shaka.net.DataUriPlugin.parseRaw(uri);
  2369. // The data encoded in the URI is a PlayReady Pro Object, so we need
  2370. // convert it to pssh.
  2371. const data = shaka.util.BufferUtils.toUint8(parsedData.data);
  2372. const systemId = new Uint8Array([
  2373. 0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86,
  2374. 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95,
  2375. ]);
  2376. const pssh = shaka.util.Pssh.createPssh(data, systemId);
  2377. const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
  2378. 'com.microsoft.playready', [
  2379. {initDataType: 'cenc', initData: pssh},
  2380. ]);
  2381. return drmInfo;
  2382. }
  2383. };
  2384. /**
  2385. * @typedef {{
  2386. * stream: !shaka.extern.Stream,
  2387. * verbatimMediaPlaylistUri: string,
  2388. * absoluteMediaPlaylistUri: string,
  2389. * minTimestamp: number,
  2390. * maxTimestamp: number,
  2391. * mediaSequenceToStartTime: !Map.<number, number>,
  2392. * discontinuityToMediaSequence: !Map.<number, number>,
  2393. * canSkipSegments: boolean
  2394. * }}
  2395. *
  2396. * @description
  2397. * Contains a stream and information about it.
  2398. *
  2399. * @property {!shaka.extern.Stream} stream
  2400. * The Stream itself.
  2401. * @property {string} verbatimMediaPlaylistUri
  2402. * The verbatim media playlist URI, as it appeared in the master playlist.
  2403. * This has not been canonicalized into an absolute URI. This gives us a
  2404. * consistent key for this playlist, even if redirects cause us to update
  2405. * from different origins each time.
  2406. * @property {string} absoluteMediaPlaylistUri
  2407. * The absolute media playlist URI, resolved relative to the master playlist
  2408. * and updated to reflect any redirects.
  2409. * @property {number} minTimestamp
  2410. * The minimum timestamp found in the stream.
  2411. * @property {number} maxTimestamp
  2412. * The maximum timestamp found in the stream.
  2413. * @property {!Map.<number, number>} mediaSequenceToStartTime
  2414. * A map of media sequence numbers to media start times.
  2415. * @property {!Map.<number, number>} discontinuityToMediaSequence
  2416. * A map of discontinuity sequence numbers to the media sequence number of the
  2417. * segment starting with that discontinuity sequence number.
  2418. * @property {boolean} canSkipSegments
  2419. * True if the server supports delta playlist updates, and we can send a
  2420. * request for a playlist that can skip older media segments.
  2421. */
  2422. shaka.hls.HlsParser.StreamInfo;
  2423. /**
  2424. * @typedef {{
  2425. * audio: !Array.<shaka.hls.HlsParser.StreamInfo>,
  2426. * video: !Array.<shaka.hls.HlsParser.StreamInfo>
  2427. * }}
  2428. *
  2429. * @description Audio and video stream infos.
  2430. * @property {!Array.<shaka.hls.HlsParser.StreamInfo>} audio
  2431. * @property {!Array.<shaka.hls.HlsParser.StreamInfo>} video
  2432. */
  2433. shaka.hls.HlsParser.StreamInfos;
  2434. /**
  2435. * @const {!Object.<string, string>}
  2436. * @private
  2437. */
  2438. shaka.hls.HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_ = {
  2439. 'mp4': 'audio/mp4',
  2440. 'mp4a': 'audio/mp4',
  2441. 'm4s': 'audio/mp4',
  2442. 'm4i': 'audio/mp4',
  2443. 'm4a': 'audio/mp4',
  2444. 'm4f': 'audio/mp4',
  2445. 'cmfa': 'audio/mp4',
  2446. // MPEG2-TS also uses video/ for audio: https://bit.ly/TsMse
  2447. 'ts': 'video/mp2t',
  2448. // Raw formats:
  2449. 'aac': 'audio/aac',
  2450. 'ac3': 'audio/ac3',
  2451. 'ec3': 'audio/ec3',
  2452. 'mp3': 'audio/mpeg',
  2453. };
  2454. /**
  2455. * MIME types of raw formats.
  2456. * TODO(#2337): Support raw formats and share this list among parsers.
  2457. *
  2458. * @const {!Array.<string>}
  2459. * @private
  2460. */
  2461. shaka.hls.HlsParser.RAW_FORMATS_ = [
  2462. 'audio/aac',
  2463. 'audio/ac3',
  2464. 'audio/ec3',
  2465. 'audio/mpeg',
  2466. ];
  2467. /**
  2468. * @const {!Object.<string, string>}
  2469. * @private
  2470. */
  2471. shaka.hls.HlsParser.VIDEO_EXTENSIONS_TO_MIME_TYPES_ = {
  2472. 'mp4': 'video/mp4',
  2473. 'mp4v': 'video/mp4',
  2474. 'm4s': 'video/mp4',
  2475. 'm4i': 'video/mp4',
  2476. 'm4v': 'video/mp4',
  2477. 'm4f': 'video/mp4',
  2478. 'cmfv': 'video/mp4',
  2479. 'ts': 'video/mp2t',
  2480. };
  2481. /**
  2482. * @const {!Object.<string, string>}
  2483. * @private
  2484. */
  2485. shaka.hls.HlsParser.TEXT_EXTENSIONS_TO_MIME_TYPES_ = {
  2486. 'mp4': 'application/mp4',
  2487. 'm4s': 'application/mp4',
  2488. 'm4i': 'application/mp4',
  2489. 'm4f': 'application/mp4',
  2490. 'cmft': 'application/mp4',
  2491. 'vtt': 'text/vtt',
  2492. 'ttml': 'application/ttml+xml',
  2493. };
  2494. /**
  2495. * @const {!Object.<string, !Object.<string, string>>}
  2496. * @private
  2497. */
  2498. shaka.hls.HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_ = {
  2499. 'audio': shaka.hls.HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_,
  2500. 'video': shaka.hls.HlsParser.VIDEO_EXTENSIONS_TO_MIME_TYPES_,
  2501. 'text': shaka.hls.HlsParser.TEXT_EXTENSIONS_TO_MIME_TYPES_,
  2502. };
  2503. /**
  2504. * @typedef {function(!shaka.hls.Tag):?shaka.extern.DrmInfo}
  2505. * @private
  2506. */
  2507. shaka.hls.HlsParser.DrmParser_;
  2508. /**
  2509. * @const {!Object.<string, shaka.hls.HlsParser.DrmParser_>}
  2510. * @private
  2511. */
  2512. shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_ = {
  2513. /* TODO: https://github.com/google/shaka-player/issues/382
  2514. 'com.apple.streamingkeydelivery':
  2515. shaka.hls.HlsParser.fairplayDrmParser_,
  2516. */
  2517. 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed':
  2518. shaka.hls.HlsParser.widevineDrmParser_,
  2519. 'com.microsoft.playready':
  2520. shaka.hls.HlsParser.playreadyDrmParser_,
  2521. };
  2522. /**
  2523. * @enum {string}
  2524. * @private
  2525. */
  2526. shaka.hls.HlsParser.PresentationType_ = {
  2527. VOD: 'VOD',
  2528. EVENT: 'EVENT',
  2529. LIVE: 'LIVE',
  2530. };
  2531. /**
  2532. * @const {number}
  2533. * @private
  2534. */
  2535. shaka.hls.HlsParser.TS_TIMESCALE_ = 90000;
  2536. /**
  2537. * The amount of data from the start of a segment we will try to fetch when we
  2538. * need to know the segment start time. This allows us to avoid fetching the
  2539. * entire segment in many cases.
  2540. *
  2541. * @const {number}
  2542. * @private
  2543. */
  2544. shaka.hls.HlsParser.START_OF_SEGMENT_SIZE_ = 2048;
  2545. shaka.media.ManifestParser.registerParserByExtension(
  2546. 'm3u8', () => new shaka.hls.HlsParser());
  2547. shaka.media.ManifestParser.registerParserByMime(
  2548. 'application/x-mpegurl', () => new shaka.hls.HlsParser());
  2549. shaka.media.ManifestParser.registerParserByMime(
  2550. 'application/vnd.apple.mpegurl', () => new shaka.hls.HlsParser());