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.abr.Ewma');
  10. goog.require('shaka.drm.DrmUtils');
  11. goog.require('shaka.drm.FairPlay');
  12. goog.require('shaka.drm.PlayReady');
  13. goog.require('shaka.hls.Attribute');
  14. goog.require('shaka.hls.ManifestTextParser');
  15. goog.require('shaka.hls.Playlist');
  16. goog.require('shaka.hls.PlaylistType');
  17. goog.require('shaka.hls.Tag');
  18. goog.require('shaka.hls.Utils');
  19. goog.require('shaka.log');
  20. goog.require('shaka.media.InitSegmentReference');
  21. goog.require('shaka.media.ManifestParser');
  22. goog.require('shaka.media.PresentationTimeline');
  23. goog.require('shaka.media.QualityObserver');
  24. goog.require('shaka.media.SegmentIndex');
  25. goog.require('shaka.media.SegmentReference');
  26. goog.require('shaka.media.SegmentUtils');
  27. goog.require('shaka.net.DataUriPlugin');
  28. goog.require('shaka.net.NetworkingEngine');
  29. goog.require('shaka.net.NetworkingEngine.PendingRequest');
  30. goog.require('shaka.util.ArrayUtils');
  31. goog.require('shaka.util.BufferUtils');
  32. goog.require('shaka.util.ContentSteeringManager');
  33. goog.require('shaka.util.Error');
  34. goog.require('shaka.util.EventManager');
  35. goog.require('shaka.util.FakeEvent');
  36. goog.require('shaka.util.LanguageUtils');
  37. goog.require('shaka.util.ManifestParserUtils');
  38. goog.require('shaka.util.MimeUtils');
  39. goog.require('shaka.util.Networking');
  40. goog.require('shaka.util.OperationManager');
  41. goog.require('shaka.util.Pssh');
  42. goog.require('shaka.util.Timer');
  43. goog.require('shaka.util.TsParser');
  44. goog.require('shaka.util.TXml');
  45. goog.require('shaka.util.StreamUtils');
  46. goog.require('shaka.util.Uint8ArrayUtils');
  47. goog.requireType('shaka.hls.Segment');
  48. /**
  49. * HLS parser.
  50. *
  51. * @implements {shaka.extern.ManifestParser}
  52. * @export
  53. */
  54. shaka.hls.HlsParser = class {
  55. /**
  56. * Creates an Hls Parser object.
  57. */
  58. constructor() {
  59. /** @private {?shaka.extern.ManifestParser.PlayerInterface} */
  60. this.playerInterface_ = null;
  61. /** @private {?shaka.extern.ManifestConfiguration} */
  62. this.config_ = null;
  63. /** @private {number} */
  64. this.globalId_ = 1;
  65. /** @private {!Map<string, string>} */
  66. this.globalVariables_ = new Map();
  67. /**
  68. * A map from group id to stream infos created from the media tags.
  69. * @private {!Map<string, !Array<?shaka.hls.HlsParser.StreamInfo>>}
  70. */
  71. this.groupIdToStreamInfosMap_ = new Map();
  72. /**
  73. * For media playlist lazy-loading to work in livestreams, we have to assume
  74. * that each stream of a type (video, audio, etc) has the same mappings of
  75. * sequence number to start time.
  76. * This map stores those relationships.
  77. * Only used during livestreams; we do not assume that VOD content is
  78. * aligned in that way.
  79. * @private {!Map<string, !Map<number, number>>}
  80. */
  81. this.mediaSequenceToStartTimeByType_ = new Map();
  82. // Set initial maps.
  83. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  84. this.mediaSequenceToStartTimeByType_.set(ContentType.VIDEO, new Map());
  85. this.mediaSequenceToStartTimeByType_.set(ContentType.AUDIO, new Map());
  86. this.mediaSequenceToStartTimeByType_.set(ContentType.TEXT, new Map());
  87. this.mediaSequenceToStartTimeByType_.set(ContentType.IMAGE, new Map());
  88. /** @private {!Map<string, shaka.hls.HlsParser.DrmParser_>} */
  89. this.keyFormatsToDrmParsers_ = new Map()
  90. .set('com.apple.streamingkeydelivery',
  91. (tag, type, ref) => this.fairplayDrmParser_(tag, type, ref))
  92. .set('urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
  93. (tag, type, ref) => this.widevineDrmParser_(tag, type, ref))
  94. .set('com.microsoft.playready',
  95. (tag, type, ref) => this.playreadyDrmParser_(tag, type, ref))
  96. .set('urn:uuid:3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c',
  97. (tag, type, ref) => this.wiseplayDrmParser_(tag, type, ref));
  98. /**
  99. * The values are strings of the form "<VIDEO URI> - <AUDIO URI>",
  100. * where the URIs are the verbatim media playlist URIs as they appeared in
  101. * the master playlist.
  102. *
  103. * Used to avoid duplicates that vary only in their text stream.
  104. *
  105. * @private {!Set<string>}
  106. */
  107. this.variantUriSet_ = new Set();
  108. /**
  109. * A map from (verbatim) media playlist URI to stream infos representing the
  110. * playlists.
  111. *
  112. * On update, used to iterate through and update from media playlists.
  113. *
  114. * On initial parse, used to iterate through and determine minimum
  115. * timestamps, offsets, and to handle TS rollover.
  116. *
  117. * During parsing, used to avoid duplicates in the async methods
  118. * createStreamInfoFromMediaTags_, createStreamInfoFromImageTag_ and
  119. * createStreamInfoFromVariantTags_.
  120. *
  121. * @private {!Map<string, shaka.hls.HlsParser.StreamInfo>}
  122. */
  123. this.uriToStreamInfosMap_ = new Map();
  124. /** @private {?shaka.media.PresentationTimeline} */
  125. this.presentationTimeline_ = null;
  126. /**
  127. * The master playlist URI, after redirects.
  128. *
  129. * @private {string}
  130. */
  131. this.masterPlaylistUri_ = '';
  132. /** @private {shaka.hls.ManifestTextParser} */
  133. this.manifestTextParser_ = new shaka.hls.ManifestTextParser();
  134. /**
  135. * The minimum sequence number for generated segments, when ignoring
  136. * EXT-X-PROGRAM-DATE-TIME.
  137. *
  138. * @private {number}
  139. */
  140. this.minSequenceNumber_ = -1;
  141. /**
  142. * The lowest time value for any of the streams, as defined by the
  143. * EXT-X-PROGRAM-DATE-TIME value. Measured in seconds since January 1, 1970.
  144. *
  145. * @private {number}
  146. */
  147. this.lowestSyncTime_ = Infinity;
  148. /**
  149. * Flag to indicate if any of the media playlists use
  150. * EXT-X-PROGRAM-DATE-TIME.
  151. *
  152. * @private {boolean}
  153. */
  154. this.usesProgramDateTime_ = false;
  155. /**
  156. * Whether the streams have previously been "finalized"; that is to say,
  157. * whether we have loaded enough streams to know information about the asset
  158. * such as timing information, live status, etc.
  159. *
  160. * @private {boolean}
  161. */
  162. this.streamsFinalized_ = false;
  163. /**
  164. * Whether the manifest informs about the codec to use.
  165. *
  166. * @private
  167. */
  168. this.codecInfoInManifest_ = false;
  169. /**
  170. * This timer is used to trigger the start of a manifest update. A manifest
  171. * update is async. Once the update is finished, the timer will be restarted
  172. * to trigger the next update. The timer will only be started if the content
  173. * is live content.
  174. *
  175. * @private {shaka.util.Timer}
  176. */
  177. this.updatePlaylistTimer_ = new shaka.util.Timer(() => {
  178. if (this.mediaElement_ && !this.config_.continueLoadingWhenPaused) {
  179. this.eventManager_.unlisten(this.mediaElement_, 'timeupdate');
  180. if (this.mediaElement_.paused) {
  181. this.eventManager_.listenOnce(
  182. this.mediaElement_, 'timeupdate', () => this.onUpdate_());
  183. return;
  184. }
  185. }
  186. this.onUpdate_();
  187. });
  188. /** @private {shaka.hls.HlsParser.PresentationType_} */
  189. this.presentationType_ = shaka.hls.HlsParser.PresentationType_.VOD;
  190. /** @private {?shaka.extern.Manifest} */
  191. this.manifest_ = null;
  192. /** @private {number} */
  193. this.maxTargetDuration_ = 0;
  194. /** @private {number} */
  195. this.lastTargetDuration_ = Infinity;
  196. /**
  197. * Partial segments target duration.
  198. * @private {number}
  199. */
  200. this.partialTargetDuration_ = 0;
  201. /** @private {number} */
  202. this.presentationDelay_ = 0;
  203. /** @private {number} */
  204. this.lowLatencyPresentationDelay_ = 0;
  205. /** @private {shaka.util.OperationManager} */
  206. this.operationManager_ = new shaka.util.OperationManager();
  207. /**
  208. * A map from closed captions' group id, to a map of closed captions info.
  209. * {group id -> {closed captions channel id -> language}}
  210. * @private {Map<string, Map<string, string>>}
  211. */
  212. this.groupIdToClosedCaptionsMap_ = new Map();
  213. /** @private {Map<string, string>} */
  214. this.groupIdToCodecsMap_ = new Map();
  215. /**
  216. * A cache mapping EXT-X-MAP tag info to the InitSegmentReference created
  217. * from the tag.
  218. * The key is a string combining the EXT-X-MAP tag's absolute uri, and
  219. * its BYTERANGE if available.
  220. * @private {!Map<string, !shaka.media.InitSegmentReference>}
  221. */
  222. this.mapTagToInitSegmentRefMap_ = new Map();
  223. /** @private {Map<string, !shaka.extern.aesKey>} */
  224. this.aesKeyInfoMap_ = new Map();
  225. /** @private {Map<string, !Promise<shaka.extern.Response>>} */
  226. this.aesKeyMap_ = new Map();
  227. /** @private {Map<string, !Promise<shaka.extern.Response>>} */
  228. this.identityKeyMap_ = new Map();
  229. /** @private {Map<!shaka.media.InitSegmentReference, ?string>} */
  230. this.initSegmentToKidMap_ = new Map();
  231. /** @private {boolean} */
  232. this.lowLatencyMode_ = false;
  233. /** @private {boolean} */
  234. this.lowLatencyByterangeOptimization_ = false;
  235. /**
  236. * An ewma that tracks how long updates take.
  237. * This is to mitigate issues caused by slow parsing on embedded devices.
  238. * @private {!shaka.abr.Ewma}
  239. */
  240. this.averageUpdateDuration_ = new shaka.abr.Ewma(5);
  241. /** @private {?shaka.util.ContentSteeringManager} */
  242. this.contentSteeringManager_ = null;
  243. /** @private {boolean} */
  244. this.needsClosedCaptionsDetection_ = true;
  245. /** @private {Set<string>} */
  246. this.dateRangeIdsEmitted_ = new Set();
  247. /** @private {shaka.util.EventManager} */
  248. this.eventManager_ = new shaka.util.EventManager();
  249. /** @private {HTMLMediaElement} */
  250. this.mediaElement_ = null;
  251. /** @private {?number} */
  252. this.startTime_ = null;
  253. /** @private {function():boolean} */
  254. this.isPreloadFn_ = () => false;
  255. }
  256. /**
  257. * @param {shaka.extern.ManifestConfiguration} config
  258. * @param {(function():boolean)=} isPreloadFn
  259. * @override
  260. * @exportInterface
  261. */
  262. configure(config, isPreloadFn) {
  263. const needFireUpdate = this.playerInterface_ &&
  264. config.updatePeriod != this.config_.updatePeriod &&
  265. config.updatePeriod >= 0;
  266. this.config_ = config;
  267. if (isPreloadFn) {
  268. this.isPreloadFn_ = isPreloadFn;
  269. }
  270. if (this.contentSteeringManager_) {
  271. this.contentSteeringManager_.configure(this.config_);
  272. }
  273. if (needFireUpdate && this.manifest_ &&
  274. this.manifest_.presentationTimeline.isLive()) {
  275. this.updatePlaylistTimer_.tickNow();
  276. }
  277. }
  278. /**
  279. * @override
  280. * @exportInterface
  281. */
  282. async start(uri, playerInterface) {
  283. goog.asserts.assert(this.config_, 'Must call configure() before start()!');
  284. this.playerInterface_ = playerInterface;
  285. this.lowLatencyMode_ = playerInterface.isLowLatencyMode();
  286. const response = await this.requestManifest_([uri]).promise;
  287. // Record the master playlist URI after redirects.
  288. this.masterPlaylistUri_ = response.uri;
  289. goog.asserts.assert(response.data, 'Response data should be non-null!');
  290. await this.parseManifest_(response.data);
  291. goog.asserts.assert(this.manifest_, 'Manifest should be non-null');
  292. return this.manifest_;
  293. }
  294. /**
  295. * @override
  296. * @exportInterface
  297. */
  298. stop() {
  299. // Make sure we don't update the manifest again. Even if the timer is not
  300. // running, this is safe to call.
  301. if (this.updatePlaylistTimer_) {
  302. this.updatePlaylistTimer_.stop();
  303. this.updatePlaylistTimer_ = null;
  304. }
  305. /** @type {!Array<!Promise>} */
  306. const pending = [];
  307. if (this.operationManager_) {
  308. pending.push(this.operationManager_.destroy());
  309. this.operationManager_ = null;
  310. }
  311. this.playerInterface_ = null;
  312. this.config_ = null;
  313. this.variantUriSet_.clear();
  314. this.manifest_ = null;
  315. this.uriToStreamInfosMap_.clear();
  316. this.groupIdToStreamInfosMap_.clear();
  317. this.groupIdToCodecsMap_.clear();
  318. this.globalVariables_.clear();
  319. this.mapTagToInitSegmentRefMap_.clear();
  320. this.aesKeyInfoMap_.clear();
  321. this.aesKeyMap_.clear();
  322. this.identityKeyMap_.clear();
  323. this.initSegmentToKidMap_.clear();
  324. this.dateRangeIdsEmitted_.clear();
  325. if (this.contentSteeringManager_) {
  326. this.contentSteeringManager_.destroy();
  327. }
  328. if (this.eventManager_) {
  329. this.eventManager_.release();
  330. this.eventManager_ = null;
  331. }
  332. return Promise.all(pending);
  333. }
  334. /**
  335. * @override
  336. * @exportInterface
  337. */
  338. async update() {
  339. if (!this.isLive_()) {
  340. return;
  341. }
  342. /** @type {!Array<!Promise>} */
  343. const updates = [];
  344. const streamInfos = Array.from(this.uriToStreamInfosMap_.values());
  345. // This is necessary to calculate correctly the update time.
  346. this.lastTargetDuration_ = Infinity;
  347. this.manifest_.gapCount = 0;
  348. // Only update active streams.
  349. const activeStreamInfos = streamInfos.filter((s) => s.stream.segmentIndex);
  350. for (const streamInfo of activeStreamInfos) {
  351. updates.push(this.updateStream_(streamInfo));
  352. }
  353. await Promise.all(updates);
  354. // Now that streams have been updated, notify the presentation timeline.
  355. this.notifySegmentsForStreams_(activeStreamInfos.map((s) => s.stream));
  356. // If any hasEndList is false, the stream is still live.
  357. const stillLive = activeStreamInfos.some((s) => s.hasEndList == false);
  358. if (activeStreamInfos.length && !stillLive) {
  359. // Convert the presentation to VOD and set the duration.
  360. const PresentationType = shaka.hls.HlsParser.PresentationType_;
  361. this.setPresentationType_(PresentationType.VOD);
  362. // The duration is the minimum of the end times of all active streams.
  363. // Non-active streams are not guaranteed to have useful maxTimestamp
  364. // values, due to the lazy-loading system, so they are ignored.
  365. const maxTimestamps = activeStreamInfos.map((s) => s.maxTimestamp);
  366. // The duration is the minimum of the end times of all streams.
  367. this.presentationTimeline_.setDuration(Math.min(...maxTimestamps));
  368. this.playerInterface_.updateDuration();
  369. }
  370. if (stillLive) {
  371. this.determineDuration_();
  372. }
  373. // Check if any playlist does not have the first reference (due to a
  374. // problem in the live encoder for example), and disable the stream if
  375. // necessary.
  376. for (const streamInfo of activeStreamInfos) {
  377. if (!streamInfo.stream.isAudioMuxedInVideo &&
  378. streamInfo.stream.segmentIndex &&
  379. !streamInfo.stream.segmentIndex.earliestReference()) {
  380. this.playerInterface_.disableStream(streamInfo.stream);
  381. }
  382. }
  383. }
  384. /**
  385. * @param {!shaka.hls.HlsParser.StreamInfo} streamInfo
  386. * @return {!Map<number, number>}
  387. * @private
  388. */
  389. getMediaSequenceToStartTimeFor_(streamInfo) {
  390. if (this.isLive_()) {
  391. return this.mediaSequenceToStartTimeByType_.get(streamInfo.type);
  392. } else {
  393. return streamInfo.mediaSequenceToStartTime;
  394. }
  395. }
  396. /**
  397. * Updates a stream.
  398. *
  399. * @param {!shaka.hls.HlsParser.StreamInfo} streamInfo
  400. * @return {!Promise}
  401. * @private
  402. */
  403. async updateStream_(streamInfo) {
  404. if (streamInfo.stream.isAudioMuxedInVideo) {
  405. return;
  406. }
  407. const manifestUris = [];
  408. for (const uri of streamInfo.getUris()) {
  409. const uriObj = new goog.Uri(uri);
  410. const queryData = uriObj.getQueryData();
  411. if (streamInfo.canBlockReload) {
  412. if (streamInfo.nextMediaSequence >= 0) {
  413. // Indicates that the server must hold the request until a Playlist
  414. // contains a Media Segment with Media Sequence
  415. queryData.add('_HLS_msn', String(streamInfo.nextMediaSequence));
  416. }
  417. if (streamInfo.nextPart >= 0) {
  418. // Indicates, in combination with _HLS_msn, that the server must hold
  419. // the request until a Playlist contains Partial Segment N of Media
  420. // Sequence Number M or later.
  421. queryData.add('_HLS_part', String(streamInfo.nextPart));
  422. }
  423. }
  424. if (streamInfo.canSkipSegments) {
  425. // Enable delta updates. This will replace older segments with
  426. // 'EXT-X-SKIP' tag in the media playlist.
  427. queryData.add('_HLS_skip', 'YES');
  428. }
  429. if (queryData.getCount()) {
  430. uriObj.setQueryData(queryData.toDecodedString());
  431. }
  432. manifestUris.push(uriObj.toString());
  433. }
  434. let response;
  435. try {
  436. response = await this.requestManifest_(
  437. manifestUris, /* isPlaylist= */ true).promise;
  438. } catch (e) {
  439. if (this.playerInterface_) {
  440. this.playerInterface_.disableStream(streamInfo.stream);
  441. }
  442. throw e;
  443. }
  444. if (!streamInfo.stream.segmentIndex) {
  445. // The stream was closed since the update was first requested.
  446. return;
  447. }
  448. /** @type {shaka.hls.Playlist} */
  449. const playlist = this.manifestTextParser_.parsePlaylist(response.data);
  450. if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
  451. throw new shaka.util.Error(
  452. shaka.util.Error.Severity.CRITICAL,
  453. shaka.util.Error.Category.MANIFEST,
  454. shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY);
  455. }
  456. // Record the final URI after redirects.
  457. const responseUri = response.uri;
  458. if (responseUri != response.originalUri &&
  459. !streamInfo.getUris().includes(responseUri)) {
  460. streamInfo.redirectUris.push(responseUri);
  461. }
  462. /** @type {!Array<!shaka.hls.Tag>} */
  463. const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
  464. 'EXT-X-DEFINE');
  465. const mediaVariables = this.parseMediaVariables_(
  466. variablesTags, responseUri);
  467. const stream = streamInfo.stream;
  468. const mediaSequenceToStartTime =
  469. this.getMediaSequenceToStartTimeFor_(streamInfo);
  470. const {keyIds, drmInfos, encrypted, aesEncrypted} =
  471. await this.parseDrmInfo_(playlist, stream.mimeType,
  472. streamInfo.getUris, mediaVariables);
  473. if (!stream.encrypted && encrypted && !aesEncrypted) {
  474. stream.encrypted = true;
  475. }
  476. const keysAreEqual =
  477. (a, b) => a.size === b.size && [...a].every((value) => b.has(value));
  478. if (!keysAreEqual(stream.keyIds, keyIds)) {
  479. stream.keyIds = keyIds;
  480. stream.drmInfos = drmInfos;
  481. this.playerInterface_.newDrmInfo(stream);
  482. }
  483. const {segments, bandwidth} = this.createSegments_(
  484. playlist, mediaSequenceToStartTime, mediaVariables,
  485. streamInfo.getUris, streamInfo.type);
  486. if (bandwidth) {
  487. stream.bandwidth = bandwidth;
  488. }
  489. const qualityInfo =
  490. shaka.media.QualityObserver.createQualityInfo(stream);
  491. for (const segment of segments) {
  492. if (segment.initSegmentReference) {
  493. segment.initSegmentReference.mediaQuality = qualityInfo;
  494. }
  495. }
  496. stream.segmentIndex.mergeAndEvict(
  497. segments, this.presentationTimeline_.getSegmentAvailabilityStart());
  498. if (segments.length) {
  499. const mediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber(
  500. playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0);
  501. const skipTag = shaka.hls.Utils.getFirstTagWithName(
  502. playlist.tags, 'EXT-X-SKIP');
  503. const skippedSegments =
  504. skipTag ? Number(skipTag.getAttributeValue('SKIPPED-SEGMENTS')) : 0;
  505. const {nextMediaSequence, nextPart} =
  506. this.getNextMediaSequenceAndPart_(mediaSequenceNumber, segments);
  507. streamInfo.nextMediaSequence = nextMediaSequence + skippedSegments;
  508. streamInfo.nextPart = nextPart;
  509. const playlistStartTime = mediaSequenceToStartTime.get(
  510. mediaSequenceNumber);
  511. stream.segmentIndex.evict(playlistStartTime);
  512. }
  513. const oldSegment = stream.segmentIndex.earliestReference();
  514. if (oldSegment) {
  515. streamInfo.minTimestamp = oldSegment.startTime;
  516. const newestSegment = segments[segments.length - 1];
  517. goog.asserts.assert(newestSegment, 'Should have segments!');
  518. streamInfo.maxTimestamp = newestSegment.endTime;
  519. }
  520. // Once the last segment has been added to the playlist,
  521. // #EXT-X-ENDLIST tag will be appended.
  522. // If that happened, treat the rest of the EVENT presentation as VOD.
  523. const endListTag =
  524. shaka.hls.Utils.getFirstTagWithName(playlist.tags, 'EXT-X-ENDLIST');
  525. if (endListTag) {
  526. // Flag this for later. We don't convert the whole presentation into VOD
  527. // until we've seen the ENDLIST tag for all active playlists.
  528. streamInfo.hasEndList = true;
  529. }
  530. this.determineLastTargetDuration_(playlist);
  531. this.processDateRangeTags_(
  532. playlist.tags, stream.type, mediaVariables, streamInfo.getUris);
  533. }
  534. /**
  535. * @override
  536. * @exportInterface
  537. */
  538. onExpirationUpdated(sessionId, expiration) {
  539. // No-op
  540. }
  541. /**
  542. * @override
  543. * @exportInterface
  544. */
  545. onInitialVariantChosen(variant) {
  546. // No-op
  547. }
  548. /**
  549. * @override
  550. * @exportInterface
  551. */
  552. banLocation(uri) {
  553. if (this.contentSteeringManager_) {
  554. this.contentSteeringManager_.banLocation(uri);
  555. }
  556. }
  557. /**
  558. * @override
  559. * @exportInterface
  560. */
  561. setMediaElement(mediaElement) {
  562. this.mediaElement_ = mediaElement;
  563. }
  564. /**
  565. * Align the streams by sequence number by dropping early segments. Then
  566. * offset the streams to begin at presentation time 0.
  567. * @param {!Array<!shaka.hls.HlsParser.StreamInfo>} streamInfos
  568. * @param {boolean=} force
  569. * @private
  570. */
  571. syncStreamsWithSequenceNumber_(streamInfos, force = false) {
  572. // We assume that, when this is first called, we have enough info to
  573. // determine how to use the program date times (e.g. we have both a video
  574. // and an audio, and all other videos and audios match those).
  575. // Thus, we only need to calculate this once.
  576. const updateMinSequenceNumber = this.minSequenceNumber_ == -1;
  577. // Sync using media sequence number. Find the highest starting sequence
  578. // number among all streams. Later, we will drop any references to
  579. // earlier segments in other streams, then offset everything back to 0.
  580. for (const streamInfo of streamInfos) {
  581. const segmentIndex = streamInfo.stream.segmentIndex;
  582. goog.asserts.assert(segmentIndex,
  583. 'Only loaded streams should be synced');
  584. const mediaSequenceToStartTime =
  585. this.getMediaSequenceToStartTimeFor_(streamInfo);
  586. const segment0 = segmentIndex.earliestReference();
  587. if (segment0) {
  588. // This looks inefficient, but iteration order is insertion order.
  589. // So the very first entry should be the one we want.
  590. // We assert that this holds true so that we are alerted by debug
  591. // builds and tests if it changes. We still do a loop, though, so
  592. // that the code functions correctly in production no matter what.
  593. if (goog.DEBUG) {
  594. const firstSequenceStartTime =
  595. mediaSequenceToStartTime.values().next().value;
  596. if (firstSequenceStartTime != segment0.startTime) {
  597. shaka.log.warning(
  598. 'Sequence number map is not ordered as expected!');
  599. }
  600. }
  601. for (const [sequence, start] of mediaSequenceToStartTime) {
  602. if (start == segment0.startTime) {
  603. if (updateMinSequenceNumber) {
  604. this.minSequenceNumber_ = Math.max(
  605. this.minSequenceNumber_, sequence);
  606. }
  607. // Even if we already have decided on a value for
  608. // |this.minSequenceNumber_|, we still need to determine the first
  609. // sequence number for the stream, to offset it in the code below.
  610. streamInfo.firstSequenceNumber = sequence;
  611. break;
  612. }
  613. }
  614. }
  615. }
  616. if (this.minSequenceNumber_ < 0) {
  617. // Nothing to sync.
  618. return;
  619. }
  620. shaka.log.debug('Syncing HLS streams against base sequence number:',
  621. this.minSequenceNumber_);
  622. for (const streamInfo of streamInfos) {
  623. if (!this.ignoreManifestProgramDateTimeFor_(streamInfo.type) && !force) {
  624. continue;
  625. }
  626. const segmentIndex = streamInfo.stream.segmentIndex;
  627. if (segmentIndex) {
  628. // Drop any earlier references.
  629. const numSegmentsToDrop =
  630. this.minSequenceNumber_ - streamInfo.firstSequenceNumber;
  631. if (numSegmentsToDrop > 0) {
  632. segmentIndex.dropFirstReferences(numSegmentsToDrop);
  633. // Now adjust timestamps back to begin at 0.
  634. const segmentN = segmentIndex.earliestReference();
  635. if (segmentN) {
  636. const streamOffset = -segmentN.startTime;
  637. // Modify all SegmentReferences equally.
  638. streamInfo.stream.segmentIndex.offset(streamOffset);
  639. // Update other parts of streamInfo the same way.
  640. this.offsetStreamInfo_(streamInfo, streamOffset);
  641. }
  642. }
  643. }
  644. }
  645. }
  646. /**
  647. * Synchronize streams by the EXT-X-PROGRAM-DATE-TIME tags attached to their
  648. * segments. Also normalizes segment times so that the earliest segment in
  649. * any stream is at time 0.
  650. * @param {!Array<!shaka.hls.HlsParser.StreamInfo>} streamInfos
  651. * @private
  652. */
  653. syncStreamsWithProgramDateTime_(streamInfos) {
  654. // We assume that, when this is first called, we have enough info to
  655. // determine how to use the program date times (e.g. we have both a video
  656. // and an audio, and all other videos and audios match those).
  657. // Thus, we only need to calculate this once.
  658. if (this.lowestSyncTime_ == Infinity) {
  659. for (const streamInfo of streamInfos) {
  660. const segmentIndex = streamInfo.stream.segmentIndex;
  661. goog.asserts.assert(segmentIndex,
  662. 'Only loaded streams should be synced');
  663. const segment0 = segmentIndex.earliestReference();
  664. if (segment0 != null && segment0.syncTime != null) {
  665. this.lowestSyncTime_ =
  666. Math.min(this.lowestSyncTime_, segment0.syncTime);
  667. }
  668. }
  669. }
  670. const lowestSyncTime = this.lowestSyncTime_;
  671. if (lowestSyncTime == Infinity) {
  672. // Nothing to sync.
  673. return;
  674. }
  675. shaka.log.debug('Syncing HLS streams against base time:', lowestSyncTime);
  676. for (const streamInfo of this.uriToStreamInfosMap_.values()) {
  677. if (this.ignoreManifestProgramDateTimeFor_(streamInfo.type)) {
  678. continue;
  679. }
  680. const segmentIndex = streamInfo.stream.segmentIndex;
  681. if (segmentIndex != null) {
  682. // A segment's startTime should be based on its syncTime vs the lowest
  683. // syncTime across all streams. The earliest segment sync time from
  684. // any stream will become presentation time 0. If two streams start
  685. // e.g. 6 seconds apart in syncTime, then their first segments will
  686. // also start 6 seconds apart in presentation time.
  687. const segment0 = segmentIndex.earliestReference();
  688. if (!segment0) {
  689. continue;
  690. }
  691. if (segment0.syncTime == null) {
  692. shaka.log.alwaysError('Missing EXT-X-PROGRAM-DATE-TIME for stream',
  693. streamInfo.getUris(),
  694. 'Expect AV sync issues!');
  695. } else {
  696. // Stream metadata are offset by a fixed amount based on the
  697. // first segment.
  698. const segment0TargetTime = segment0.syncTime - lowestSyncTime;
  699. const streamOffset = segment0TargetTime - segment0.startTime;
  700. this.offsetStreamInfo_(streamInfo, streamOffset);
  701. // This is computed across all segments separately to manage
  702. // accumulated drift in durations.
  703. for (const segment of segmentIndex) {
  704. segment.syncAgainst(lowestSyncTime);
  705. }
  706. }
  707. }
  708. }
  709. }
  710. /**
  711. * @param {!shaka.hls.HlsParser.StreamInfo} streamInfo
  712. * @param {number} offset
  713. * @private
  714. */
  715. offsetStreamInfo_(streamInfo, offset) {
  716. // Due to float compute issue we can have some millisecond issue.
  717. // We don't apply the offset if it's the case.
  718. if (Math.abs(offset) < 0.001) {
  719. return;
  720. }
  721. // Adjust our accounting of the minimum timestamp.
  722. streamInfo.minTimestamp += offset;
  723. // Adjust our accounting of the maximum timestamp.
  724. streamInfo.maxTimestamp += offset;
  725. goog.asserts.assert(streamInfo.maxTimestamp >= 0,
  726. 'Negative maxTimestamp after adjustment!');
  727. // Update our map from sequence number to start time.
  728. const mediaSequenceToStartTime =
  729. this.getMediaSequenceToStartTimeFor_(streamInfo);
  730. for (const [key, value] of mediaSequenceToStartTime) {
  731. mediaSequenceToStartTime.set(key, value + offset);
  732. }
  733. shaka.log.debug('Offset', offset, 'applied to',
  734. streamInfo.getUris());
  735. }
  736. /**
  737. * Parses the manifest.
  738. *
  739. * @param {BufferSource} data
  740. * @return {!Promise}
  741. * @private
  742. */
  743. async parseManifest_(data) {
  744. const Utils = shaka.hls.Utils;
  745. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  746. goog.asserts.assert(this.masterPlaylistUri_,
  747. 'Master playlist URI must be set before calling parseManifest_!');
  748. const playlist = this.manifestTextParser_.parsePlaylist(data);
  749. /** @type {!Array<!shaka.hls.Tag>} */
  750. const variablesTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-DEFINE');
  751. /** @type {!Array<!shaka.extern.Variant>} */
  752. let variants = [];
  753. /** @type {!Array<!shaka.extern.Stream>} */
  754. let textStreams = [];
  755. /** @type {!Array<!shaka.extern.Stream>} */
  756. let imageStreams = [];
  757. // This assert is our own sanity check.
  758. goog.asserts.assert(this.presentationTimeline_ == null,
  759. 'Presentation timeline created early!');
  760. // We don't know if the presentation is VOD or live until we parse at least
  761. // one media playlist, so make a VOD-style presentation timeline for now
  762. // and change the type later if we discover this is live.
  763. // Since the player will load the first variant chosen early in the process,
  764. // there isn't a window during playback where the live-ness is unknown.
  765. this.presentationTimeline_ = new shaka.media.PresentationTimeline(
  766. /* presentationStartTime= */ null, /* delay= */ 0);
  767. this.presentationTimeline_.setStatic(true);
  768. const getUris = () => {
  769. return [this.masterPlaylistUri_];
  770. };
  771. /** @type {?string} */
  772. let mediaPlaylistType = null;
  773. /** @type {!Map<string, string>} */
  774. let mediaVariables = new Map();
  775. // Parsing a media playlist results in a single-variant stream.
  776. if (playlist.type == shaka.hls.PlaylistType.MEDIA) {
  777. this.needsClosedCaptionsDetection_ = false;
  778. /** @type {!Array<!shaka.hls.Tag>} */
  779. const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
  780. 'EXT-X-DEFINE');
  781. mediaVariables = this.parseMediaVariables_(
  782. variablesTags, this.masterPlaylistUri_);
  783. // By default we assume it is video, but in a later step the correct type
  784. // is obtained.
  785. mediaPlaylistType = ContentType.VIDEO;
  786. // These values can be obtained later so these default values are good.
  787. const codecs = '';
  788. const languageValue = '';
  789. const channelsCount = null;
  790. const sampleRate = null;
  791. const closedCaptions = new Map();
  792. const spatialAudio = false;
  793. const characteristics = null;
  794. const forced = false; // Only relevant for text.
  795. const primary = true; // This is the only stream!
  796. const name = 'Media Playlist';
  797. // Make the stream info, with those values.
  798. const streamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
  799. this.globalId_++, mediaVariables, playlist, getUris, codecs,
  800. mediaPlaylistType, languageValue, primary, name, channelsCount,
  801. closedCaptions, characteristics, forced, sampleRate, spatialAudio);
  802. this.uriToStreamInfosMap_.set(this.masterPlaylistUri_, streamInfo);
  803. if (streamInfo.stream) {
  804. const qualityInfo =
  805. shaka.media.QualityObserver.createQualityInfo(streamInfo.stream);
  806. streamInfo.stream.segmentIndex.forEachTopLevelReference(
  807. (reference) => {
  808. if (reference.initSegmentReference) {
  809. reference.initSegmentReference.mediaQuality = qualityInfo;
  810. }
  811. });
  812. }
  813. mediaPlaylistType = streamInfo.stream.type;
  814. // Wrap the stream from that stream info with a variant.
  815. variants.push({
  816. id: 0,
  817. language: this.getLanguage_(languageValue),
  818. disabledUntilTime: 0,
  819. primary: true,
  820. audio: mediaPlaylistType == 'audio' ? streamInfo.stream : null,
  821. video: mediaPlaylistType == 'video' ? streamInfo.stream : null,
  822. bandwidth: streamInfo.stream.bandwidth || 0,
  823. allowedByApplication: true,
  824. allowedByKeySystem: true,
  825. decodingInfos: [],
  826. });
  827. } else {
  828. this.parseMasterVariables_(variablesTags);
  829. /** @type {!Array<!shaka.hls.Tag>} */
  830. const mediaTags = Utils.filterTagsByName(
  831. playlist.tags, 'EXT-X-MEDIA');
  832. /** @type {!Array<!shaka.hls.Tag>} */
  833. const variantTags = Utils.filterTagsByName(
  834. playlist.tags, 'EXT-X-STREAM-INF');
  835. /** @type {!Array<!shaka.hls.Tag>} */
  836. const imageTags = Utils.filterTagsByName(
  837. playlist.tags, 'EXT-X-IMAGE-STREAM-INF');
  838. /** @type {!Array<!shaka.hls.Tag>} */
  839. const iFrameTags = Utils.filterTagsByName(
  840. playlist.tags, 'EXT-X-I-FRAME-STREAM-INF');
  841. /** @type {!Array<!shaka.hls.Tag>} */
  842. const sessionKeyTags = Utils.filterTagsByName(
  843. playlist.tags, 'EXT-X-SESSION-KEY');
  844. /** @type {!Array<!shaka.hls.Tag>} */
  845. const sessionDataTags = Utils.filterTagsByName(
  846. playlist.tags, 'EXT-X-SESSION-DATA');
  847. /** @type {!Array<!shaka.hls.Tag>} */
  848. const contentSteeringTags = Utils.filterTagsByName(
  849. playlist.tags, 'EXT-X-CONTENT-STEERING');
  850. this.processSessionData_(sessionDataTags);
  851. await this.processContentSteering_(contentSteeringTags);
  852. if (!this.config_.ignoreSupplementalCodecs) {
  853. // Duplicate variant tags with supplementalCodecs
  854. const newVariantTags = [];
  855. for (const tag of variantTags) {
  856. const supplementalCodecsString =
  857. tag.getAttributeValue('SUPPLEMENTAL-CODECS');
  858. if (!supplementalCodecsString) {
  859. continue;
  860. }
  861. const supplementalCodecs = supplementalCodecsString.split(/\s*,\s*/)
  862. .map((codec) => {
  863. return codec.split('/')[0];
  864. });
  865. const newAttributes = tag.attributes.map((attr) => {
  866. const name = attr.name;
  867. let value = attr.value;
  868. if (name == 'CODECS') {
  869. value = supplementalCodecs.join(',');
  870. const allCodecs = attr.value.split(',');
  871. if (allCodecs.length > 1) {
  872. const audioCodec =
  873. shaka.util.ManifestParserUtils.guessCodecsSafe(
  874. shaka.util.ManifestParserUtils.ContentType.AUDIO,
  875. allCodecs);
  876. if (audioCodec) {
  877. value += ',' + audioCodec;
  878. }
  879. }
  880. }
  881. return new shaka.hls.Attribute(name, value);
  882. });
  883. newVariantTags.push(
  884. new shaka.hls.Tag(tag.id, tag.name, newAttributes, null));
  885. }
  886. variantTags.push(...newVariantTags);
  887. // Duplicate iFrame tags with supplementalCodecs
  888. const newIFrameTags = [];
  889. for (const tag of iFrameTags) {
  890. const supplementalCodecsString =
  891. tag.getAttributeValue('SUPPLEMENTAL-CODECS');
  892. if (!supplementalCodecsString) {
  893. continue;
  894. }
  895. const supplementalCodecs = supplementalCodecsString.split(/\s*,\s*/)
  896. .map((codec) => {
  897. return codec.split('/')[0];
  898. });
  899. const newAttributes = tag.attributes.map((attr) => {
  900. const name = attr.name;
  901. let value = attr.value;
  902. if (name == 'CODECS') {
  903. value = supplementalCodecs.join(',');
  904. }
  905. return new shaka.hls.Attribute(name, value);
  906. });
  907. newIFrameTags.push(
  908. new shaka.hls.Tag(tag.id, tag.name, newAttributes, null));
  909. }
  910. iFrameTags.push(...newIFrameTags);
  911. }
  912. this.parseCodecs_(variantTags);
  913. this.parseClosedCaptions_(mediaTags);
  914. const iFrameStreams = this.parseIFrames_(iFrameTags);
  915. variants = await this.createVariantsForTags_(
  916. variantTags, sessionKeyTags, mediaTags, getUris,
  917. this.globalVariables_, iFrameStreams);
  918. textStreams = this.parseTexts_(mediaTags);
  919. imageStreams = await this.parseImages_(imageTags, iFrameTags);
  920. }
  921. // Make sure that the parser has not been destroyed.
  922. if (!this.playerInterface_) {
  923. throw new shaka.util.Error(
  924. shaka.util.Error.Severity.CRITICAL,
  925. shaka.util.Error.Category.PLAYER,
  926. shaka.util.Error.Code.OPERATION_ABORTED);
  927. }
  928. this.determineStartTime_(playlist);
  929. // Single-variant streams aren't lazy-loaded, so for them we already have
  930. // enough info here to determine the presentation type and duration.
  931. if (playlist.type == shaka.hls.PlaylistType.MEDIA) {
  932. if (this.isLive_()) {
  933. this.changePresentationTimelineToLive_(playlist);
  934. const delay = this.getUpdatePlaylistDelay_();
  935. this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
  936. }
  937. const streamInfos = Array.from(this.uriToStreamInfosMap_.values());
  938. this.finalizeStreams_(streamInfos);
  939. this.determineDuration_();
  940. goog.asserts.assert(mediaPlaylistType,
  941. 'mediaPlaylistType should be non-null');
  942. this.processDateRangeTags_(
  943. playlist.tags, mediaPlaylistType, mediaVariables, getUris);
  944. }
  945. this.manifest_ = {
  946. presentationTimeline: this.presentationTimeline_,
  947. variants,
  948. textStreams,
  949. imageStreams,
  950. offlineSessionIds: [],
  951. sequenceMode: this.config_.hls.sequenceMode,
  952. ignoreManifestTimestampsInSegmentsMode:
  953. this.config_.hls.ignoreManifestTimestampsInSegmentsMode,
  954. type: shaka.media.ManifestParser.HLS,
  955. serviceDescription: null,
  956. nextUrl: null,
  957. periodCount: 1,
  958. gapCount: 0,
  959. isLowLatency: false,
  960. startTime: this.startTime_,
  961. };
  962. // If there is no 'CODECS' attribute in the manifest and codec guessing is
  963. // disabled, we need to create the segment indexes now so that missing info
  964. // can be parsed from the media data and added to the stream objects.
  965. if (!this.codecInfoInManifest_ && this.config_.hls.disableCodecGuessing) {
  966. const createIndexes = [];
  967. for (const variant of this.manifest_.variants) {
  968. if (variant.audio && variant.audio.codecs === '') {
  969. createIndexes.push(variant.audio.createSegmentIndex());
  970. }
  971. if (variant.video && variant.video.codecs === '') {
  972. createIndexes.push(variant.video.createSegmentIndex());
  973. }
  974. }
  975. await Promise.all(createIndexes);
  976. }
  977. this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
  978. }
  979. /**
  980. * @param {!Array<!shaka.media.SegmentReference>} segments
  981. * @return {!Promise<shaka.media.SegmentUtils.BasicInfo>}
  982. * @private
  983. */
  984. async getBasicInfoFromSegments_(segments) {
  985. const HlsParser = shaka.hls.HlsParser;
  986. const defaultBasicInfo = shaka.media.SegmentUtils.getBasicInfoFromMimeType(
  987. this.config_.hls.mediaPlaylistFullMimeType);
  988. if (!segments.length) {
  989. return defaultBasicInfo;
  990. }
  991. const {segment, segmentIndex} = this.getAvailableSegment_(segments);
  992. const segmentUris = segment.getUris();
  993. const segmentUri = segmentUris[0];
  994. const parsedUri = new goog.Uri(segmentUri);
  995. const extension = parsedUri.getPath().split('.').pop();
  996. const rawMimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_.get(extension);
  997. if (rawMimeType) {
  998. return shaka.media.SegmentUtils.getBasicInfoFromMimeType(
  999. rawMimeType);
  1000. }
  1001. const basicInfos = await Promise.all([
  1002. this.getInfoFromSegment_(segment.initSegmentReference, 0),
  1003. this.getInfoFromSegment_(segment, segmentIndex),
  1004. ]);
  1005. const initMimeType = basicInfos[0].mimeType;
  1006. const contentMimeType = basicInfos[1].mimeType;
  1007. const initData = basicInfos[0].data;
  1008. const data = basicInfos[1].data;
  1009. const validMp4Extensions = [
  1010. 'mp4',
  1011. 'mp4a',
  1012. 'm4s',
  1013. 'm4i',
  1014. 'm4a',
  1015. 'm4f',
  1016. 'cmfa',
  1017. 'mp4v',
  1018. 'm4v',
  1019. 'cmfv',
  1020. 'fmp4',
  1021. ];
  1022. const validMp4MimeType = [
  1023. 'audio/mp4',
  1024. 'video/mp4',
  1025. 'video/iso.segment',
  1026. ];
  1027. if (shaka.util.TsParser.probe(
  1028. shaka.util.BufferUtils.toUint8(data))) {
  1029. const basicInfo = shaka.media.SegmentUtils.getBasicInfoFromTs(
  1030. data, this.config_.disableAudio, this.config_.disableVideo,
  1031. this.config_.disableText);
  1032. if (basicInfo) {
  1033. return basicInfo;
  1034. }
  1035. } else if (validMp4Extensions.includes(extension) ||
  1036. validMp4MimeType.includes(contentMimeType) ||
  1037. (initMimeType && validMp4MimeType.includes(initMimeType))) {
  1038. const basicInfo = shaka.media.SegmentUtils.getBasicInfoFromMp4(
  1039. initData, data, this.config_.disableText);
  1040. if (basicInfo) {
  1041. return basicInfo;
  1042. }
  1043. }
  1044. if (contentMimeType) {
  1045. return shaka.media.SegmentUtils.getBasicInfoFromMimeType(
  1046. contentMimeType);
  1047. }
  1048. if (initMimeType) {
  1049. return shaka.media.SegmentUtils.getBasicInfoFromMimeType(
  1050. initMimeType);
  1051. }
  1052. return defaultBasicInfo;
  1053. }
  1054. /**
  1055. * @param {?shaka.media.AnySegmentReference} segment
  1056. * @param {number} segmentIndex
  1057. * @return {!Promise<{mimeType: ?string, data: ?BufferSource}>}
  1058. * @private
  1059. */
  1060. async getInfoFromSegment_(segment, segmentIndex) {
  1061. if (!segment) {
  1062. return {mimeType: null, data: null};
  1063. }
  1064. const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
  1065. const segmentRequest = shaka.util.Networking.createSegmentRequest(
  1066. segment.getUris(), segment.getStartByte(), segment.getEndByte(),
  1067. this.config_.retryParameters);
  1068. const type = segment instanceof shaka.media.SegmentReference ?
  1069. shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT :
  1070. shaka.net.NetworkingEngine.AdvancedRequestType.INIT_SEGMENT;
  1071. const response = await this.makeNetworkRequest_(
  1072. segmentRequest, requestType, {type}).promise;
  1073. let data = response.data;
  1074. if (segment.aesKey) {
  1075. data = await shaka.media.SegmentUtils.aesDecrypt(
  1076. data, segment.aesKey, segmentIndex);
  1077. }
  1078. if (segment instanceof shaka.media.SegmentReference) {
  1079. segment.setSegmentData(data, /* singleUse= */ true);
  1080. } else {
  1081. segment.setSegmentData(data);
  1082. }
  1083. let mimeType = response.headers['content-type'];
  1084. if (mimeType) {
  1085. // Split the MIME type in case the server sent additional parameters.
  1086. mimeType = mimeType.split(';')[0].toLowerCase();
  1087. }
  1088. return {mimeType, data};
  1089. }
  1090. /** @private */
  1091. determineDuration_() {
  1092. goog.asserts.assert(this.presentationTimeline_,
  1093. 'Presentation timeline not created!');
  1094. if (this.isLive_()) {
  1095. // The spec says nothing much about seeking in live content, but Safari's
  1096. // built-in HLS implementation does not allow it. Therefore we will set
  1097. // the availability window equal to the presentation delay. The player
  1098. // will be able to buffer ahead three segments, but the seek window will
  1099. // be zero-sized.
  1100. const PresentationType = shaka.hls.HlsParser.PresentationType_;
  1101. if (this.presentationType_ == PresentationType.LIVE) {
  1102. let segmentAvailabilityDuration = this.getLiveDuration_() || 0;
  1103. // The app can override that with a longer duration, to allow seeking.
  1104. if (!isNaN(this.config_.availabilityWindowOverride)) {
  1105. segmentAvailabilityDuration = this.config_.availabilityWindowOverride;
  1106. }
  1107. this.presentationTimeline_.setSegmentAvailabilityDuration(
  1108. segmentAvailabilityDuration);
  1109. }
  1110. } else {
  1111. // Use the minimum duration as the presentation duration.
  1112. this.presentationTimeline_.setDuration(this.getMinDuration_());
  1113. }
  1114. if (!this.presentationTimeline_.isStartTimeLocked()) {
  1115. for (const streamInfo of this.uriToStreamInfosMap_.values()) {
  1116. if (!streamInfo.stream.segmentIndex) {
  1117. continue; // Not active.
  1118. }
  1119. if (streamInfo.type != 'audio' && streamInfo.type != 'video') {
  1120. continue;
  1121. }
  1122. const firstReference =
  1123. streamInfo.stream.segmentIndex.earliestReference();
  1124. if (firstReference && firstReference.syncTime) {
  1125. const syncTime = firstReference.syncTime;
  1126. this.presentationTimeline_.setInitialProgramDateTime(syncTime);
  1127. }
  1128. }
  1129. }
  1130. // This is the first point where we have a meaningful presentation start
  1131. // time, and we need to tell PresentationTimeline that so that it can
  1132. // maintain consistency from here on.
  1133. this.presentationTimeline_.lockStartTime();
  1134. // This asserts that the live edge is being calculated from segment times.
  1135. // For VOD and event streams, this check should still pass.
  1136. goog.asserts.assert(
  1137. !this.presentationTimeline_.usingPresentationStartTime(),
  1138. 'We should not be using the presentation start time in HLS!');
  1139. }
  1140. /**
  1141. * Get the variables of each variant tag, and store in a map.
  1142. * @param {!Array<!shaka.hls.Tag>} tags Variant tags from the playlist.
  1143. * @private
  1144. */
  1145. parseMasterVariables_(tags) {
  1146. const queryParams = new goog.Uri(this.masterPlaylistUri_).getQueryData();
  1147. for (const variableTag of tags) {
  1148. const name = variableTag.getAttributeValue('NAME');
  1149. const value = variableTag.getAttributeValue('VALUE');
  1150. const queryParam = variableTag.getAttributeValue('QUERYPARAM');
  1151. if (name && value) {
  1152. if (!this.globalVariables_.has(name)) {
  1153. this.globalVariables_.set(name, value);
  1154. }
  1155. }
  1156. if (queryParam) {
  1157. const queryParamValue = queryParams.get(queryParam)[0];
  1158. if (queryParamValue && !this.globalVariables_.has(queryParamValue)) {
  1159. this.globalVariables_.set(queryParam, queryParamValue);
  1160. }
  1161. }
  1162. }
  1163. }
  1164. /**
  1165. * Get the variables of each variant tag, and store in a map.
  1166. * @param {!Array<!shaka.hls.Tag>} tags Variant tags from the playlist.
  1167. * @param {string} uri Media playlist URI.
  1168. * @return {!Map<string, string>}
  1169. * @private
  1170. */
  1171. parseMediaVariables_(tags, uri) {
  1172. const queryParams = new goog.Uri(uri).getQueryData();
  1173. const mediaVariables = new Map();
  1174. for (const variableTag of tags) {
  1175. const name = variableTag.getAttributeValue('NAME');
  1176. const value = variableTag.getAttributeValue('VALUE');
  1177. const queryParam = variableTag.getAttributeValue('QUERYPARAM');
  1178. const mediaImport = variableTag.getAttributeValue('IMPORT');
  1179. if (name && value) {
  1180. if (!mediaVariables.has(name)) {
  1181. mediaVariables.set(name, value);
  1182. }
  1183. }
  1184. if (queryParam) {
  1185. const queryParamValue = queryParams.get(queryParam)[0];
  1186. if (queryParamValue && !mediaVariables.has(queryParamValue)) {
  1187. mediaVariables.set(queryParam, queryParamValue);
  1188. }
  1189. }
  1190. if (mediaImport) {
  1191. const globalValue = this.globalVariables_.get(mediaImport);
  1192. if (globalValue) {
  1193. mediaVariables.set(mediaImport, globalValue);
  1194. }
  1195. }
  1196. }
  1197. return mediaVariables;
  1198. }
  1199. /**
  1200. * Get the codecs of each variant tag, and store in a map from
  1201. * audio/video/subtitle group id to the codecs array list.
  1202. * @param {!Array<!shaka.hls.Tag>} tags Variant tags from the playlist.
  1203. * @private
  1204. */
  1205. parseCodecs_(tags) {
  1206. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1207. for (const variantTag of tags) {
  1208. const audioGroupId = variantTag.getAttributeValue('AUDIO');
  1209. const videoGroupId = variantTag.getAttributeValue('VIDEO');
  1210. const subGroupId = variantTag.getAttributeValue('SUBTITLES');
  1211. const allCodecs = this.getCodecsForVariantTag_(variantTag);
  1212. if (subGroupId) {
  1213. const textCodecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
  1214. ContentType.TEXT, allCodecs);
  1215. goog.asserts.assert(textCodecs != null, 'Text codecs should be valid.');
  1216. this.groupIdToCodecsMap_.set(subGroupId, textCodecs);
  1217. shaka.util.ArrayUtils.remove(allCodecs, textCodecs);
  1218. }
  1219. if (audioGroupId) {
  1220. let codecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
  1221. ContentType.AUDIO, allCodecs);
  1222. if (!codecs) {
  1223. codecs = this.config_.hls.defaultAudioCodec;
  1224. }
  1225. this.groupIdToCodecsMap_.set(audioGroupId, codecs);
  1226. }
  1227. if (videoGroupId) {
  1228. let codecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
  1229. ContentType.VIDEO, allCodecs);
  1230. if (!codecs) {
  1231. codecs = this.config_.hls.defaultVideoCodec;
  1232. }
  1233. this.groupIdToCodecsMap_.set(videoGroupId, codecs);
  1234. }
  1235. }
  1236. }
  1237. /**
  1238. * Process EXT-X-SESSION-DATA tags.
  1239. *
  1240. * @param {!Array<!shaka.hls.Tag>} tags
  1241. * @private
  1242. */
  1243. processSessionData_(tags) {
  1244. for (const tag of tags) {
  1245. const id = tag.getAttributeValue('DATA-ID');
  1246. const uri = tag.getAttributeValue('URI');
  1247. const language = tag.getAttributeValue('LANGUAGE');
  1248. const value = tag.getAttributeValue('VALUE');
  1249. const data = (new Map()).set('id', id);
  1250. if (uri) {
  1251. data.set('uri', shaka.hls.Utils.constructSegmentUris(
  1252. [this.masterPlaylistUri_], uri, this.globalVariables_)[0]);
  1253. }
  1254. if (language) {
  1255. data.set('language', language);
  1256. }
  1257. if (value) {
  1258. data.set('value', value);
  1259. }
  1260. const event = new shaka.util.FakeEvent('sessiondata', data);
  1261. if (this.playerInterface_) {
  1262. this.playerInterface_.onEvent(event);
  1263. }
  1264. }
  1265. }
  1266. /**
  1267. * Process EXT-X-CONTENT-STEERING tags.
  1268. *
  1269. * @param {!Array<!shaka.hls.Tag>} tags
  1270. * @return {!Promise}
  1271. * @private
  1272. */
  1273. async processContentSteering_(tags) {
  1274. if (!this.playerInterface_ || !this.config_) {
  1275. return;
  1276. }
  1277. let contentSteeringPromise;
  1278. for (const tag of tags) {
  1279. const defaultPathwayId = tag.getAttributeValue('PATHWAY-ID');
  1280. const uri = tag.getAttributeValue('SERVER-URI');
  1281. if (!defaultPathwayId || !uri) {
  1282. continue;
  1283. }
  1284. this.contentSteeringManager_ =
  1285. new shaka.util.ContentSteeringManager(this.playerInterface_);
  1286. this.contentSteeringManager_.configure(this.config_);
  1287. this.contentSteeringManager_.setBaseUris([this.masterPlaylistUri_]);
  1288. this.contentSteeringManager_.setManifestType(
  1289. shaka.media.ManifestParser.HLS);
  1290. this.contentSteeringManager_.setDefaultPathwayId(defaultPathwayId);
  1291. contentSteeringPromise =
  1292. this.contentSteeringManager_.requestInfo(uri);
  1293. break;
  1294. }
  1295. await contentSteeringPromise;
  1296. }
  1297. /**
  1298. * Parse Subtitles and Closed Captions from 'EXT-X-MEDIA' tags.
  1299. * Create text streams for Subtitles, but not Closed Captions.
  1300. *
  1301. * @param {!Array<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
  1302. * @return {!Array<!shaka.extern.Stream>}
  1303. * @private
  1304. */
  1305. parseTexts_(mediaTags) {
  1306. // Create text stream for each Subtitle media tag.
  1307. const subtitleTags =
  1308. shaka.hls.Utils.filterTagsByType(mediaTags, 'SUBTITLES');
  1309. const textStreams = subtitleTags.map((tag) => {
  1310. const disableText = this.config_.disableText;
  1311. if (disableText) {
  1312. return null;
  1313. }
  1314. try {
  1315. return this.createStreamInfoFromMediaTags_([tag], new Map()).stream;
  1316. } catch (e) {
  1317. if (this.config_.hls.ignoreTextStreamFailures) {
  1318. return null;
  1319. }
  1320. throw e;
  1321. }
  1322. });
  1323. const type = shaka.util.ManifestParserUtils.ContentType.TEXT;
  1324. // Set the codecs for text streams.
  1325. for (const tag of subtitleTags) {
  1326. const groupId = tag.getRequiredAttrValue('GROUP-ID');
  1327. const codecs = this.groupIdToCodecsMap_.get(groupId);
  1328. if (codecs) {
  1329. const textStreamInfos = this.groupIdToStreamInfosMap_.get(groupId);
  1330. if (textStreamInfos) {
  1331. for (const textStreamInfo of textStreamInfos) {
  1332. textStreamInfo.stream.codecs = codecs;
  1333. textStreamInfo.stream.mimeType =
  1334. this.guessMimeTypeBeforeLoading_(type, codecs) ||
  1335. this.guessMimeTypeFallback_(type);
  1336. this.setFullTypeForStream_(textStreamInfo.stream);
  1337. }
  1338. }
  1339. }
  1340. }
  1341. // Do not create text streams for Closed captions.
  1342. return textStreams.filter((s) => s);
  1343. }
  1344. /**
  1345. * @param {!shaka.extern.Stream} stream
  1346. * @private
  1347. */
  1348. setFullTypeForStream_(stream) {
  1349. const combinations = new Set([shaka.util.MimeUtils.getFullType(
  1350. stream.mimeType, stream.codecs)]);
  1351. if (stream.segmentIndex) {
  1352. stream.segmentIndex.forEachTopLevelReference((reference) => {
  1353. if (reference.mimeType) {
  1354. combinations.add(shaka.util.MimeUtils.getFullType(
  1355. reference.mimeType, stream.codecs));
  1356. }
  1357. });
  1358. }
  1359. stream.fullMimeTypes = combinations;
  1360. }
  1361. /**
  1362. * @param {!Array<!shaka.hls.Tag>} imageTags from the playlist.
  1363. * @param {!Array<!shaka.hls.Tag>} iFrameTags from the playlist.
  1364. * @return {!Promise<!Array<!shaka.extern.Stream>>}
  1365. * @private
  1366. */
  1367. async parseImages_(imageTags, iFrameTags) {
  1368. // Create image stream for each image tag.
  1369. const imageStreamPromises = imageTags.map(async (tag) => {
  1370. const disableThumbnails = this.config_.disableThumbnails;
  1371. if (disableThumbnails) {
  1372. return null;
  1373. }
  1374. try {
  1375. const streamInfo = await this.createStreamInfoFromImageTag_(tag);
  1376. return streamInfo.stream;
  1377. } catch (e) {
  1378. if (this.config_.hls.ignoreImageStreamFailures) {
  1379. return null;
  1380. }
  1381. throw e;
  1382. }
  1383. }).concat(iFrameTags.map((tag) => {
  1384. const disableThumbnails = this.config_.disableThumbnails;
  1385. if (disableThumbnails) {
  1386. return null;
  1387. }
  1388. try {
  1389. const streamInfo = this.createStreamInfoFromIframeTag_(tag);
  1390. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1391. if (streamInfo.stream.type !== ContentType.IMAGE) {
  1392. return null;
  1393. }
  1394. return streamInfo.stream;
  1395. } catch (e) {
  1396. if (this.config_.hls.ignoreImageStreamFailures) {
  1397. return null;
  1398. }
  1399. throw e;
  1400. }
  1401. }));
  1402. const imageStreams = await Promise.all(imageStreamPromises);
  1403. return imageStreams.filter((s) => s);
  1404. }
  1405. /**
  1406. * @param {!Array<!shaka.hls.Tag>} mediaTags Media tags from the playlist.
  1407. * @param {!Map<string, string>} groupIdPathwayIdMapping
  1408. * @private
  1409. */
  1410. createStreamInfosFromMediaTags_(mediaTags, groupIdPathwayIdMapping) {
  1411. // Filter out subtitles and media tags without uri (except audio).
  1412. mediaTags = mediaTags.filter((tag) => {
  1413. const uri = tag.getAttributeValue('URI') || '';
  1414. const type = tag.getAttributeValue('TYPE');
  1415. return type != 'SUBTITLES' && (uri != '' || type == 'AUDIO');
  1416. });
  1417. const groupedTags = {};
  1418. for (const tag of mediaTags) {
  1419. const key = tag.getTagKey(!this.contentSteeringManager_);
  1420. if (!groupedTags[key]) {
  1421. groupedTags[key] = [tag];
  1422. } else {
  1423. groupedTags[key].push(tag);
  1424. }
  1425. }
  1426. for (const key in groupedTags) {
  1427. // Create stream info for each audio / video media grouped tag.
  1428. this.createStreamInfoFromMediaTags_(
  1429. groupedTags[key], groupIdPathwayIdMapping, /* requireUri= */ false);
  1430. }
  1431. }
  1432. /**
  1433. * @param {!Array<!shaka.hls.Tag>} iFrameTags from the playlist.
  1434. * @return {!Array<!shaka.extern.Stream>}
  1435. * @private
  1436. */
  1437. parseIFrames_(iFrameTags) {
  1438. // Create iFrame stream for each iFrame tag.
  1439. const iFrameStreams = iFrameTags.map((tag) => {
  1440. const streamInfo = this.createStreamInfoFromIframeTag_(tag);
  1441. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1442. if (streamInfo.stream.type !== ContentType.VIDEO) {
  1443. return null;
  1444. }
  1445. return streamInfo.stream;
  1446. });
  1447. // Filter mjpg iFrames
  1448. return iFrameStreams.filter((s) => s);
  1449. }
  1450. /**
  1451. * @param {!Array<!shaka.hls.Tag>} tags Variant tags from the playlist.
  1452. * @param {!Array<!shaka.hls.Tag>} sessionKeyTags EXT-X-SESSION-KEY tags
  1453. * from the playlist.
  1454. * @param {!Array<!shaka.hls.Tag>} mediaTags EXT-X-MEDIA tags from the
  1455. * playlist.
  1456. * @param {function(): !Array<string>} getUris
  1457. * @param {?Map<string, string>} variables
  1458. * @param {!Array<!shaka.extern.Stream>} iFrameStreams
  1459. * @return {!Promise<!Array<!shaka.extern.Variant>>}
  1460. * @private
  1461. */
  1462. async createVariantsForTags_(tags, sessionKeyTags, mediaTags, getUris,
  1463. variables, iFrameStreams) {
  1464. // EXT-X-SESSION-KEY processing
  1465. const drmInfos = [];
  1466. const keyIds = new Set();
  1467. if (!this.config_.ignoreDrmInfo && sessionKeyTags.length > 0) {
  1468. for (const drmTag of sessionKeyTags) {
  1469. const method = drmTag.getRequiredAttrValue('METHOD');
  1470. // According to the HLS spec, KEYFORMAT is optional and implicitly
  1471. // defaults to "identity".
  1472. // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-4.4.4.4
  1473. const keyFormat =
  1474. drmTag.getAttributeValue('KEYFORMAT') || 'identity';
  1475. let drmInfo = null;
  1476. if (method == 'NONE') {
  1477. continue;
  1478. } else if (this.isAesMethod_(method)) {
  1479. const keyUris = shaka.hls.Utils.constructSegmentUris(
  1480. getUris(), drmTag.getRequiredAttrValue('URI'), variables);
  1481. const keyMapKey = keyUris.sort().join('');
  1482. if (!this.aesKeyMap_.has(keyMapKey)) {
  1483. const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
  1484. const request = shaka.net.NetworkingEngine.makeRequest(
  1485. keyUris, this.config_.retryParameters);
  1486. const keyResponse = this.makeNetworkRequest_(request, requestType)
  1487. .promise;
  1488. this.aesKeyMap_.set(keyMapKey, keyResponse);
  1489. }
  1490. continue;
  1491. } else if (keyFormat == 'identity') {
  1492. // eslint-disable-next-line no-await-in-loop
  1493. drmInfo = await this.identityDrmParser_(
  1494. drmTag, /* mimeType= */ '', getUris,
  1495. /* initSegmentRef= */ null, variables);
  1496. } else {
  1497. const drmParser =
  1498. this.keyFormatsToDrmParsers_.get(keyFormat);
  1499. drmInfo = drmParser ?
  1500. // eslint-disable-next-line no-await-in-loop
  1501. await drmParser(drmTag, /* mimeType= */ '',
  1502. /* initSegmentRef= */ null) : null;
  1503. }
  1504. if (drmInfo) {
  1505. if (drmInfo.keyIds) {
  1506. for (const keyId of drmInfo.keyIds) {
  1507. keyIds.add(keyId);
  1508. }
  1509. }
  1510. drmInfos.push(drmInfo);
  1511. } else {
  1512. shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
  1513. }
  1514. }
  1515. }
  1516. const groupedTags = {};
  1517. for (const tag of tags) {
  1518. const key = tag.getTagKey(!this.contentSteeringManager_);
  1519. if (!groupedTags[key]) {
  1520. groupedTags[key] = [tag];
  1521. } else {
  1522. groupedTags[key].push(tag);
  1523. }
  1524. }
  1525. const allVariants = [];
  1526. // Create variants for each group of variant tag.
  1527. for (const key in groupedTags) {
  1528. const tags = groupedTags[key];
  1529. const firstTag = tags[0];
  1530. const frameRate = firstTag.getAttributeValue('FRAME-RATE');
  1531. const bandwidth =
  1532. Number(firstTag.getAttributeValue('AVERAGE-BANDWIDTH')) ||
  1533. Number(firstTag.getRequiredAttrValue('BANDWIDTH'));
  1534. const resolution = firstTag.getAttributeValue('RESOLUTION');
  1535. const [width, height] = resolution ? resolution.split('x') : [null, null];
  1536. const videoRange = firstTag.getAttributeValue('VIDEO-RANGE');
  1537. let videoLayout = firstTag.getAttributeValue('REQ-VIDEO-LAYOUT');
  1538. if (videoLayout && videoLayout.includes(',')) {
  1539. // If multiple video layout strings are present, pick the first valid
  1540. // one.
  1541. const layoutStrings = videoLayout.split(',').filter((layoutString) => {
  1542. return layoutString == 'CH-STEREO' || layoutString == 'CH-MONO';
  1543. });
  1544. videoLayout = layoutStrings[0];
  1545. }
  1546. // According to the HLS spec:
  1547. // By default a video variant is monoscopic, so an attribute
  1548. // consisting entirely of REQ-VIDEO-LAYOUT="CH-MONO" is unnecessary
  1549. // and SHOULD NOT be present.
  1550. videoLayout = videoLayout || 'CH-MONO';
  1551. const streamInfos = this.createStreamInfosForVariantTags_(tags,
  1552. mediaTags, resolution, frameRate);
  1553. goog.asserts.assert(streamInfos.audio.length ||
  1554. streamInfos.video.length, 'We should have created a stream!');
  1555. allVariants.push(...this.createVariants_(
  1556. streamInfos.audio,
  1557. streamInfos.video,
  1558. bandwidth,
  1559. width,
  1560. height,
  1561. frameRate,
  1562. videoRange,
  1563. videoLayout,
  1564. drmInfos,
  1565. keyIds,
  1566. iFrameStreams));
  1567. }
  1568. return allVariants.filter((variant) => variant != null);
  1569. }
  1570. /**
  1571. * Create audio and video streamInfos from an 'EXT-X-STREAM-INF' tag and its
  1572. * related media tags.
  1573. *
  1574. * @param {!Array<!shaka.hls.Tag>} tags
  1575. * @param {!Array<!shaka.hls.Tag>} mediaTags
  1576. * @param {?string} resolution
  1577. * @param {?string} frameRate
  1578. * @return {!shaka.hls.HlsParser.StreamInfos}
  1579. * @private
  1580. */
  1581. createStreamInfosForVariantTags_(tags, mediaTags, resolution, frameRate) {
  1582. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1583. /** @type {shaka.hls.HlsParser.StreamInfos} */
  1584. const res = {
  1585. audio: [],
  1586. video: [],
  1587. };
  1588. const groupIdPathwayIdMapping = new Map();
  1589. const globalGroupIds = [];
  1590. let isAudioGroup = false;
  1591. let isVideoGroup = false;
  1592. for (const tag of tags) {
  1593. const audioGroupId = tag.getAttributeValue('AUDIO');
  1594. const videoGroupId = tag.getAttributeValue('VIDEO');
  1595. goog.asserts.assert(audioGroupId == null || videoGroupId == null,
  1596. 'Unexpected: both video and audio described by media tags!');
  1597. const groupId = audioGroupId || videoGroupId;
  1598. if (!groupId) {
  1599. continue;
  1600. }
  1601. if (!globalGroupIds.includes(groupId)) {
  1602. globalGroupIds.push(groupId);
  1603. }
  1604. const pathwayId = tag.getAttributeValue('PATHWAY-ID');
  1605. if (pathwayId) {
  1606. groupIdPathwayIdMapping.set(groupId, pathwayId);
  1607. }
  1608. if (audioGroupId) {
  1609. isAudioGroup = true;
  1610. } else if (videoGroupId) {
  1611. isVideoGroup = true;
  1612. }
  1613. // Make an educated guess about the stream type.
  1614. shaka.log.debug('Guessing stream type for', tag.toString());
  1615. }
  1616. if (globalGroupIds.length && mediaTags.length) {
  1617. const mediaTagsForVariant = mediaTags.filter((tag) => {
  1618. return globalGroupIds.includes(tag.getRequiredAttrValue('GROUP-ID'));
  1619. });
  1620. this.createStreamInfosFromMediaTags_(
  1621. mediaTagsForVariant, groupIdPathwayIdMapping);
  1622. }
  1623. const globalGroupId = globalGroupIds.sort().join(',');
  1624. const streamInfos =
  1625. (globalGroupId && this.groupIdToStreamInfosMap_.has(globalGroupId)) ?
  1626. this.groupIdToStreamInfosMap_.get(globalGroupId) : [];
  1627. if (isAudioGroup) {
  1628. res.audio.push(...streamInfos);
  1629. } else if (isVideoGroup) {
  1630. res.video.push(...streamInfos);
  1631. }
  1632. let type;
  1633. let ignoreStream = false;
  1634. // The Microsoft HLS manifest generators will make audio-only variants
  1635. // that link to their URI both directly and through an audio tag.
  1636. // In that case, ignore the local URI and use the version in the
  1637. // AUDIO tag, so you inherit its language.
  1638. // As an example, see the manifest linked in issue #860.
  1639. const allStreamUris = tags.map((tag) => tag.getRequiredAttrValue('URI'));
  1640. const hasSameUri = res.audio.find((audio) => {
  1641. return audio && audio.getUris().find((uri) => {
  1642. return allStreamUris.includes(uri);
  1643. });
  1644. });
  1645. /** @type {!Array<string>} */
  1646. let allCodecs = this.getCodecsForVariantTag_(tags[0]);
  1647. const videoCodecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
  1648. ContentType.VIDEO, allCodecs);
  1649. const audioCodecs = shaka.util.ManifestParserUtils.guessCodecsSafe(
  1650. ContentType.AUDIO, allCodecs);
  1651. if (audioCodecs && !videoCodecs) {
  1652. // There are no associated media tags, and there's only audio codec,
  1653. // and no video codec, so it should be audio.
  1654. type = ContentType.AUDIO;
  1655. shaka.log.debug('Guessing audio-only.');
  1656. ignoreStream = res.audio.length > 0;
  1657. } else if (!res.audio.length && !res.video.length &&
  1658. audioCodecs && videoCodecs) {
  1659. // There are both audio and video codecs, so assume multiplexed content.
  1660. // Note that the default used when CODECS is missing assumes multiple
  1661. // (and therefore multiplexed).
  1662. // Recombine the codec strings into one so that MediaSource isn't
  1663. // lied to later. (That would trigger an error in Chrome.)
  1664. shaka.log.debug('Guessing multiplexed audio+video.');
  1665. type = ContentType.VIDEO;
  1666. allCodecs = [[videoCodecs, audioCodecs].join(',')];
  1667. } else if (res.audio.length && hasSameUri) {
  1668. shaka.log.debug('Guessing audio-only.');
  1669. type = ContentType.AUDIO;
  1670. ignoreStream = true;
  1671. } else if (res.video.length && !res.audio.length) {
  1672. // There are associated video streams. Assume this is audio.
  1673. shaka.log.debug('Guessing audio-only.');
  1674. type = ContentType.AUDIO;
  1675. } else {
  1676. shaka.log.debug('Guessing video-only.');
  1677. type = ContentType.VIDEO;
  1678. }
  1679. if (!ignoreStream) {
  1680. const streamInfo =
  1681. this.createStreamInfoFromVariantTags_(tags, allCodecs, type);
  1682. if (globalGroupId) {
  1683. streamInfo.stream.groupId = globalGroupId;
  1684. }
  1685. res[streamInfo.stream.type] = [streamInfo];
  1686. }
  1687. return res;
  1688. }
  1689. /**
  1690. * Get the codecs from the 'EXT-X-STREAM-INF' tag.
  1691. *
  1692. * @param {!shaka.hls.Tag} tag
  1693. * @return {!Array<string>} codecs
  1694. * @private
  1695. */
  1696. getCodecsForVariantTag_(tag) {
  1697. let codecsString = tag.getAttributeValue('CODECS') || '';
  1698. this.codecInfoInManifest_ = codecsString.length > 0;
  1699. if (!this.codecInfoInManifest_ && !this.config_.hls.disableCodecGuessing) {
  1700. // These are the default codecs to assume if none are specified.
  1701. const defaultCodecsArray = [];
  1702. if (!this.config_.disableVideo) {
  1703. defaultCodecsArray.push(this.config_.hls.defaultVideoCodec);
  1704. }
  1705. if (!this.config_.disableAudio) {
  1706. defaultCodecsArray.push(this.config_.hls.defaultAudioCodec);
  1707. }
  1708. codecsString = defaultCodecsArray.join(',');
  1709. }
  1710. // Strip out internal whitespace while splitting on commas:
  1711. /** @type {!Array<string>} */
  1712. const codecs = codecsString.split(/\s*,\s*/);
  1713. return shaka.media.SegmentUtils.codecsFiltering(codecs);
  1714. }
  1715. /**
  1716. * Get the channel count information for an HLS audio track.
  1717. * CHANNELS specifies an ordered, "/" separated list of parameters.
  1718. * If the type is audio, the first parameter will be a decimal integer
  1719. * specifying the number of independent, simultaneous audio channels.
  1720. * No other channels parameters are currently defined.
  1721. *
  1722. * @param {!shaka.hls.Tag} tag
  1723. * @return {?number}
  1724. * @private
  1725. */
  1726. getChannelsCount_(tag) {
  1727. const channels = tag.getAttributeValue('CHANNELS');
  1728. if (!channels) {
  1729. return null;
  1730. }
  1731. const channelCountString = channels.split('/')[0];
  1732. const count = parseInt(channelCountString, 10);
  1733. return count;
  1734. }
  1735. /**
  1736. * Get the sample rate information for an HLS audio track.
  1737. *
  1738. * @param {!shaka.hls.Tag} tag
  1739. * @return {?number}
  1740. * @private
  1741. */
  1742. getSampleRate_(tag) {
  1743. const sampleRate = tag.getAttributeValue('SAMPLE-RATE');
  1744. if (!sampleRate) {
  1745. return null;
  1746. }
  1747. return parseInt(sampleRate, 10);
  1748. }
  1749. /**
  1750. * Get the spatial audio information for an HLS audio track.
  1751. * In HLS the channels field indicates the number of audio channels that the
  1752. * stream has (eg: 2). In the case of Dolby Atmos (EAC-3), the complexity is
  1753. * expressed with the number of channels followed by the word JOC
  1754. * (eg: 16/JOC), so 16 would be the number of channels (eg: 7.3.6 layout),
  1755. * and JOC indicates that the stream has spatial audio. For Dolby AC-4 ATMOS,
  1756. * it's necessary search ATMOS word.
  1757. * @see https://developer.apple.com/documentation/http-live-streaming/hls-authoring-specification-for-apple-devices-appendixes
  1758. * @see https://ott.dolby.com/OnDelKits/AC-4/Dolby_AC-4_Online_Delivery_Kit_1.5/Documentation/Specs/AC4_HLS/help_files/topics/hls_playlist_c_codec_indication_ims.html
  1759. *
  1760. * @param {!shaka.hls.Tag} tag
  1761. * @return {boolean}
  1762. * @private
  1763. */
  1764. isSpatialAudio_(tag) {
  1765. const channels = tag.getAttributeValue('CHANNELS');
  1766. if (!channels) {
  1767. return false;
  1768. }
  1769. const channelsParts = channels.split('/');
  1770. if (channelsParts.length != 2) {
  1771. return false;
  1772. }
  1773. return channelsParts[1] === 'JOC' || channelsParts[1].includes('ATMOS');
  1774. }
  1775. /**
  1776. * Get the closed captions map information for the EXT-X-STREAM-INF tag, to
  1777. * create the stream info.
  1778. * @param {!shaka.hls.Tag} tag
  1779. * @param {string} type
  1780. * @return {Map<string, string>} closedCaptions
  1781. * @private
  1782. */
  1783. getClosedCaptions_(tag, type) {
  1784. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1785. // The attribute of closed captions is optional, and the value may be
  1786. // 'NONE'.
  1787. const closedCaptionsAttr = tag.getAttributeValue('CLOSED-CAPTIONS');
  1788. // EXT-X-STREAM-INF tags may have CLOSED-CAPTIONS attributes.
  1789. // The value can be either a quoted-string or an enumerated-string with
  1790. // the value NONE. If the value is a quoted-string, it MUST match the
  1791. // value of the GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the
  1792. // Playlist whose TYPE attribute is CLOSED-CAPTIONS.
  1793. if (type == ContentType.VIDEO ) {
  1794. if (this.config_.disableText) {
  1795. this.needsClosedCaptionsDetection_ = false;
  1796. return null;
  1797. }
  1798. if (closedCaptionsAttr) {
  1799. if (closedCaptionsAttr != 'NONE') {
  1800. return this.groupIdToClosedCaptionsMap_.get(closedCaptionsAttr);
  1801. }
  1802. this.needsClosedCaptionsDetection_ = false;
  1803. } else if (!closedCaptionsAttr && this.groupIdToClosedCaptionsMap_.size) {
  1804. for (const key of this.groupIdToClosedCaptionsMap_.keys()) {
  1805. return this.groupIdToClosedCaptionsMap_.get(key);
  1806. }
  1807. }
  1808. }
  1809. return null;
  1810. }
  1811. /**
  1812. * Get the normalized language value.
  1813. *
  1814. * @param {?string} languageValue
  1815. * @return {string}
  1816. * @private
  1817. */
  1818. getLanguage_(languageValue) {
  1819. const LanguageUtils = shaka.util.LanguageUtils;
  1820. return LanguageUtils.normalize(languageValue || 'und');
  1821. }
  1822. /**
  1823. * Get the type value.
  1824. * Shaka recognizes the content types 'audio', 'video', 'text', and 'image'.
  1825. * The HLS 'subtitles' type needs to be mapped to 'text'.
  1826. * @param {!shaka.hls.Tag} tag
  1827. * @return {string}
  1828. * @private
  1829. */
  1830. getType_(tag) {
  1831. let type = tag.getRequiredAttrValue('TYPE').toLowerCase();
  1832. if (type == 'subtitles') {
  1833. type = shaka.util.ManifestParserUtils.ContentType.TEXT;
  1834. }
  1835. return type;
  1836. }
  1837. /**
  1838. * @param {!Array<shaka.hls.HlsParser.StreamInfo>} audioInfos
  1839. * @param {!Array<shaka.hls.HlsParser.StreamInfo>} videoInfos
  1840. * @param {number} bandwidth
  1841. * @param {?string} width
  1842. * @param {?string} height
  1843. * @param {?string} frameRate
  1844. * @param {?string} videoRange
  1845. * @param {?string} videoLayout
  1846. * @param {!Array<shaka.extern.DrmInfo>} drmInfos
  1847. * @param {!Set<string>} keyIds
  1848. * @param {!Array<!shaka.extern.Stream>} iFrameStreams
  1849. * @return {!Array<!shaka.extern.Variant>}
  1850. * @private
  1851. */
  1852. createVariants_(
  1853. audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange,
  1854. videoLayout, drmInfos, keyIds, iFrameStreams) {
  1855. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1856. const DrmUtils = shaka.drm.DrmUtils;
  1857. for (const info of videoInfos) {
  1858. this.addVideoAttributes_(
  1859. info.stream, width, height, frameRate, videoRange, videoLayout,
  1860. /** colorGamut= */ null);
  1861. }
  1862. // In case of audio-only or video-only content or the audio/video is
  1863. // disabled by the config, we create an array of one item containing
  1864. // a null. This way, the double-loop works for all kinds of content.
  1865. // NOTE: we currently don't have support for audio-only content.
  1866. const disableAudio = this.config_.disableAudio;
  1867. if (!audioInfos.length || disableAudio) {
  1868. audioInfos = [null];
  1869. }
  1870. const disableVideo = this.config_.disableVideo;
  1871. if (!videoInfos.length || disableVideo) {
  1872. videoInfos = [null];
  1873. }
  1874. const variants = [];
  1875. for (const audioInfo of audioInfos) {
  1876. for (const videoInfo of videoInfos) {
  1877. const audioStream = audioInfo ? audioInfo.stream : null;
  1878. if (audioStream) {
  1879. audioStream.drmInfos = drmInfos;
  1880. audioStream.keyIds = keyIds;
  1881. }
  1882. const videoStream = videoInfo ? videoInfo.stream : null;
  1883. if (videoStream) {
  1884. videoStream.drmInfos = drmInfos;
  1885. videoStream.keyIds = keyIds;
  1886. if (!this.config_.disableIFrames) {
  1887. shaka.util.StreamUtils.setBetterIFrameStream(
  1888. videoStream, iFrameStreams);
  1889. }
  1890. }
  1891. if (videoStream && !audioStream) {
  1892. videoStream.bandwidth = bandwidth;
  1893. }
  1894. if (!videoStream && audioStream) {
  1895. audioStream.bandwidth = bandwidth;
  1896. }
  1897. const audioDrmInfos = audioInfo ? audioInfo.stream.drmInfos : null;
  1898. const videoDrmInfos = videoInfo ? videoInfo.stream.drmInfos : null;
  1899. const videoStreamUri =
  1900. videoInfo ? videoInfo.getUris().sort().join(',') : '';
  1901. const audioStreamUri =
  1902. audioInfo ? audioInfo.getUris().sort().join(',') : '';
  1903. const codecs = [];
  1904. if (audioStream && audioStream.codecs) {
  1905. codecs.push(audioStream.codecs);
  1906. }
  1907. if (videoStream && videoStream.codecs) {
  1908. codecs.push(videoStream.codecs);
  1909. }
  1910. const variantUriKey = [
  1911. videoStreamUri,
  1912. audioStreamUri,
  1913. codecs.sort(),
  1914. ].join('-');
  1915. if (audioStream && videoStream) {
  1916. if (!DrmUtils.areDrmCompatible(audioDrmInfos, videoDrmInfos)) {
  1917. shaka.log.warning(
  1918. 'Incompatible DRM info in HLS variant. Skipping.');
  1919. continue;
  1920. }
  1921. }
  1922. if (this.variantUriSet_.has(variantUriKey)) {
  1923. // This happens when two variants only differ in their text streams.
  1924. shaka.log.debug(
  1925. 'Skipping variant which only differs in text streams.');
  1926. continue;
  1927. }
  1928. // Since both audio and video are of the same type, this assertion will
  1929. // catch certain mistakes at runtime that the compiler would miss.
  1930. goog.asserts.assert(!audioStream ||
  1931. audioStream.type == ContentType.AUDIO, 'Audio parameter mismatch!');
  1932. goog.asserts.assert(!videoStream ||
  1933. videoStream.type == ContentType.VIDEO, 'Video parameter mismatch!');
  1934. const variant = {
  1935. id: this.globalId_++,
  1936. language: audioStream ? audioStream.language : 'und',
  1937. disabledUntilTime: 0,
  1938. primary: (!!audioStream && audioStream.primary) ||
  1939. (!!videoStream && videoStream.primary),
  1940. audio: audioStream,
  1941. video: videoStream,
  1942. bandwidth,
  1943. allowedByApplication: true,
  1944. allowedByKeySystem: true,
  1945. decodingInfos: [],
  1946. };
  1947. variants.push(variant);
  1948. this.variantUriSet_.add(variantUriKey);
  1949. }
  1950. }
  1951. return variants;
  1952. }
  1953. /**
  1954. * Parses an array of EXT-X-MEDIA tags, then stores the values of all tags
  1955. * with TYPE="CLOSED-CAPTIONS" into a map of group id to closed captions.
  1956. *
  1957. * @param {!Array<!shaka.hls.Tag>} mediaTags
  1958. * @private
  1959. */
  1960. parseClosedCaptions_(mediaTags) {
  1961. const closedCaptionsTags =
  1962. shaka.hls.Utils.filterTagsByType(mediaTags, 'CLOSED-CAPTIONS');
  1963. this.needsClosedCaptionsDetection_ = closedCaptionsTags.length == 0;
  1964. for (const tag of closedCaptionsTags) {
  1965. goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
  1966. 'Should only be called on media tags!');
  1967. const languageValue = tag.getAttributeValue('LANGUAGE');
  1968. let language = this.getLanguage_(languageValue);
  1969. if (!languageValue) {
  1970. const nameValue = tag.getAttributeValue('NAME');
  1971. if (nameValue) {
  1972. language = nameValue;
  1973. }
  1974. }
  1975. // The GROUP-ID value is a quoted-string that specifies the group to which
  1976. // the Rendition belongs.
  1977. const groupId = tag.getRequiredAttrValue('GROUP-ID');
  1978. // The value of INSTREAM-ID is a quoted-string that specifies a Rendition
  1979. // within the segments in the Media Playlist. This attribute is REQUIRED
  1980. // if the TYPE attribute is CLOSED-CAPTIONS.
  1981. // We need replace SERVICE string by our internal svc string.
  1982. const instreamId = tag.getRequiredAttrValue('INSTREAM-ID')
  1983. .replace('SERVICE', 'svc');
  1984. if (!this.groupIdToClosedCaptionsMap_.get(groupId)) {
  1985. this.groupIdToClosedCaptionsMap_.set(groupId, new Map());
  1986. }
  1987. this.groupIdToClosedCaptionsMap_.get(groupId).set(instreamId, language);
  1988. }
  1989. }
  1990. /**
  1991. * Parse EXT-X-MEDIA media tag into a Stream object.
  1992. *
  1993. * @param {!Array<!shaka.hls.Tag>} tags
  1994. * @param {!Map<string, string>} groupIdPathwayIdMapping
  1995. * @param {boolean=} requireUri
  1996. * @return {!shaka.hls.HlsParser.StreamInfo}
  1997. * @private
  1998. */
  1999. createStreamInfoFromMediaTags_(tags, groupIdPathwayIdMapping,
  2000. requireUri = true) {
  2001. const verbatimMediaPlaylistUris = [];
  2002. const globalGroupIds = [];
  2003. const groupIdUriMapping = new Map();
  2004. for (const tag of tags) {
  2005. goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
  2006. 'Should only be called on media tags!');
  2007. const uri = requireUri ? tag.getRequiredAttrValue('URI') :
  2008. (tag.getAttributeValue('URI') || shaka.hls.HlsParser.FAKE_MUXED_URL_);
  2009. const groupId = tag.getRequiredAttrValue('GROUP-ID');
  2010. verbatimMediaPlaylistUris.push(uri);
  2011. globalGroupIds.push(groupId);
  2012. groupIdUriMapping.set(groupId, uri);
  2013. }
  2014. const globalGroupId = globalGroupIds.sort().join(',');
  2015. const firstTag = tags[0];
  2016. let codecs = '';
  2017. /** @type {string} */
  2018. const type = this.getType_(firstTag);
  2019. if (type == shaka.util.ManifestParserUtils.ContentType.TEXT) {
  2020. codecs = firstTag.getAttributeValue('CODECS') || '';
  2021. } else {
  2022. for (const groupId of globalGroupIds) {
  2023. if (this.groupIdToCodecsMap_.has(groupId)) {
  2024. codecs = this.groupIdToCodecsMap_.get(groupId);
  2025. break;
  2026. }
  2027. }
  2028. }
  2029. // Check if the stream has already been created as part of another Variant
  2030. // and return it if it has.
  2031. const key = verbatimMediaPlaylistUris.sort().join(',');
  2032. if (this.uriToStreamInfosMap_.has(key)) {
  2033. return this.uriToStreamInfosMap_.get(key);
  2034. }
  2035. const streamId = this.globalId_++;
  2036. if (this.contentSteeringManager_) {
  2037. for (const [groupId, uri] of groupIdUriMapping) {
  2038. const pathwayId = groupIdPathwayIdMapping.get(groupId);
  2039. if (pathwayId) {
  2040. this.contentSteeringManager_.addLocation(streamId, pathwayId, uri);
  2041. }
  2042. }
  2043. }
  2044. const language = firstTag.getAttributeValue('LANGUAGE');
  2045. const name = firstTag.getAttributeValue('NAME');
  2046. // NOTE: According to the HLS spec, "DEFAULT=YES" requires "AUTOSELECT=YES".
  2047. // However, we don't bother to validate "AUTOSELECT", since we don't
  2048. // actually use it in our streaming model, and we treat everything as
  2049. // "AUTOSELECT=YES". A value of "AUTOSELECT=NO" would imply that it may
  2050. // only be selected explicitly by the user, and we don't have a way to
  2051. // represent that in our model.
  2052. const defaultAttrValue = firstTag.getAttributeValue('DEFAULT');
  2053. const primary = defaultAttrValue == 'YES';
  2054. const channelsCount =
  2055. type == 'audio' ? this.getChannelsCount_(firstTag) : null;
  2056. const spatialAudio =
  2057. type == 'audio' ? this.isSpatialAudio_(firstTag) : false;
  2058. const characteristics = firstTag.getAttributeValue('CHARACTERISTICS');
  2059. const forcedAttrValue = firstTag.getAttributeValue('FORCED');
  2060. const forced = forcedAttrValue == 'YES';
  2061. const sampleRate = type == 'audio' ? this.getSampleRate_(firstTag) : null;
  2062. // TODO: Should we take into account some of the currently ignored
  2063. // attributes: INSTREAM-ID, Attribute descriptions: https://bit.ly/2lpjOhj
  2064. const streamInfo = this.createStreamInfo_(
  2065. streamId, verbatimMediaPlaylistUris, codecs, type, language,
  2066. primary, name, channelsCount, /* closedCaptions= */ null,
  2067. characteristics, forced, sampleRate, spatialAudio);
  2068. if (streamInfo.stream) {
  2069. streamInfo.stream.groupId = globalGroupId;
  2070. }
  2071. if (this.groupIdToStreamInfosMap_.has(globalGroupId)) {
  2072. this.groupIdToStreamInfosMap_.get(globalGroupId).push(streamInfo);
  2073. } else {
  2074. this.groupIdToStreamInfosMap_.set(globalGroupId, [streamInfo]);
  2075. }
  2076. this.uriToStreamInfosMap_.set(key, streamInfo);
  2077. return streamInfo;
  2078. }
  2079. /**
  2080. * Parse EXT-X-IMAGE-STREAM-INF media tag into a Stream object.
  2081. *
  2082. * @param {shaka.hls.Tag} tag
  2083. * @return {!Promise<!shaka.hls.HlsParser.StreamInfo>}
  2084. * @private
  2085. */
  2086. async createStreamInfoFromImageTag_(tag) {
  2087. goog.asserts.assert(tag.name == 'EXT-X-IMAGE-STREAM-INF',
  2088. 'Should only be called on image tags!');
  2089. /** @type {string} */
  2090. const type = shaka.util.ManifestParserUtils.ContentType.IMAGE;
  2091. const verbatimImagePlaylistUri = tag.getRequiredAttrValue('URI');
  2092. const codecs = tag.getAttributeValue('CODECS', 'jpeg') || '';
  2093. // Check if the stream has already been created as part of another Variant
  2094. // and return it if it has.
  2095. if (this.uriToStreamInfosMap_.has(verbatimImagePlaylistUri)) {
  2096. return this.uriToStreamInfosMap_.get(verbatimImagePlaylistUri);
  2097. }
  2098. const language = tag.getAttributeValue('LANGUAGE');
  2099. const name = tag.getAttributeValue('NAME');
  2100. const characteristics = tag.getAttributeValue('CHARACTERISTICS');
  2101. const streamInfo = this.createStreamInfo_(
  2102. this.globalId_++, [verbatimImagePlaylistUri], codecs, type, language,
  2103. /* primary= */ false, name, /* channelsCount= */ null,
  2104. /* closedCaptions= */ null, characteristics, /* forced= */ false,
  2105. /* sampleRate= */ null, /* spatialAudio= */ false);
  2106. // Parse misc attributes.
  2107. const resolution = tag.getAttributeValue('RESOLUTION');
  2108. if (resolution) {
  2109. // The RESOLUTION tag represents the resolution of a single thumbnail, not
  2110. // of the entire sheet at once (like we expect in the output).
  2111. // So multiply by the layout size.
  2112. // Since we need to have generated the segment index for this, we can't
  2113. // lazy-load in this situation.
  2114. await streamInfo.stream.createSegmentIndex();
  2115. const reference = streamInfo.stream.segmentIndex.earliestReference();
  2116. const layout = reference.getTilesLayout();
  2117. if (layout) {
  2118. streamInfo.stream.width =
  2119. Number(resolution.split('x')[0]) * Number(layout.split('x')[0]);
  2120. streamInfo.stream.height =
  2121. Number(resolution.split('x')[1]) * Number(layout.split('x')[1]);
  2122. // TODO: What happens if there are multiple grids, with different
  2123. // layout sizes, inside this image stream?
  2124. }
  2125. }
  2126. const bandwidth = tag.getAttributeValue('BANDWIDTH');
  2127. if (bandwidth) {
  2128. streamInfo.stream.bandwidth = Number(bandwidth);
  2129. }
  2130. this.uriToStreamInfosMap_.set(verbatimImagePlaylistUri, streamInfo);
  2131. return streamInfo;
  2132. }
  2133. /**
  2134. * Parse EXT-X-I-FRAME-STREAM-INF media tag into a Stream object.
  2135. *
  2136. * @param {shaka.hls.Tag} tag
  2137. * @return {!shaka.hls.HlsParser.StreamInfo}
  2138. * @private
  2139. */
  2140. createStreamInfoFromIframeTag_(tag) {
  2141. goog.asserts.assert(tag.name == 'EXT-X-I-FRAME-STREAM-INF',
  2142. 'Should only be called on iframe tags!');
  2143. /** @type {string} */
  2144. let type = shaka.util.ManifestParserUtils.ContentType.VIDEO;
  2145. const verbatimIFramePlaylistUri = tag.getRequiredAttrValue('URI');
  2146. const codecs = tag.getAttributeValue('CODECS') || '';
  2147. if (codecs == 'mjpg') {
  2148. type = shaka.util.ManifestParserUtils.ContentType.IMAGE;
  2149. }
  2150. // Check if the stream has already been created as part of another Variant
  2151. // and return it if it has.
  2152. if (this.uriToStreamInfosMap_.has(verbatimIFramePlaylistUri)) {
  2153. return this.uriToStreamInfosMap_.get(verbatimIFramePlaylistUri);
  2154. }
  2155. const language = tag.getAttributeValue('LANGUAGE');
  2156. const name = tag.getAttributeValue('NAME');
  2157. const characteristics = tag.getAttributeValue('CHARACTERISTICS');
  2158. const streamInfo = this.createStreamInfo_(
  2159. this.globalId_++, [verbatimIFramePlaylistUri], codecs, type, language,
  2160. /* primary= */ false, name, /* channelsCount= */ null,
  2161. /* closedCaptions= */ null, characteristics, /* forced= */ false,
  2162. /* sampleRate= */ null, /* spatialAudio= */ false);
  2163. // Parse misc attributes.
  2164. const resolution = tag.getAttributeValue('RESOLUTION');
  2165. const [width, height] = resolution ? resolution.split('x') : [null, null];
  2166. streamInfo.stream.width = Number(width) || undefined;
  2167. streamInfo.stream.height = Number(height) || undefined;
  2168. const bandwidth = tag.getAttributeValue('BANDWIDTH');
  2169. if (bandwidth) {
  2170. streamInfo.stream.bandwidth = Number(bandwidth);
  2171. }
  2172. this.uriToStreamInfosMap_.set(verbatimIFramePlaylistUri, streamInfo);
  2173. return streamInfo;
  2174. }
  2175. /**
  2176. * Parse an EXT-X-STREAM-INF media tag into a Stream object.
  2177. *
  2178. * @param {!Array<!shaka.hls.Tag>} tags
  2179. * @param {!Array<string>} allCodecs
  2180. * @param {string} type
  2181. * @return {!shaka.hls.HlsParser.StreamInfo}
  2182. * @private
  2183. */
  2184. createStreamInfoFromVariantTags_(tags, allCodecs, type) {
  2185. const streamId = this.globalId_++;
  2186. const verbatimMediaPlaylistUris = [];
  2187. for (const tag of tags) {
  2188. goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
  2189. 'Should only be called on variant tags!');
  2190. const uri = tag.getRequiredAttrValue('URI');
  2191. const pathwayId = tag.getAttributeValue('PATHWAY-ID');
  2192. if (this.contentSteeringManager_ && pathwayId) {
  2193. this.contentSteeringManager_.addLocation(streamId, pathwayId, uri);
  2194. }
  2195. verbatimMediaPlaylistUris.push(uri);
  2196. }
  2197. const key = verbatimMediaPlaylistUris.sort().join(',') +
  2198. allCodecs.sort().join(',');
  2199. if (this.uriToStreamInfosMap_.has(key)) {
  2200. return this.uriToStreamInfosMap_.get(key);
  2201. }
  2202. const name = verbatimMediaPlaylistUris.join(',');
  2203. const closedCaptions = this.getClosedCaptions_(tags[0], type);
  2204. const codecs = shaka.util.ManifestParserUtils.guessCodecs(type, allCodecs);
  2205. const streamInfo = this.createStreamInfo_(
  2206. streamId, verbatimMediaPlaylistUris,
  2207. codecs, type, /* language= */ null, /* primary= */ false,
  2208. name, /* channelCount= */ null, closedCaptions,
  2209. /* characteristics= */ null, /* forced= */ false,
  2210. /* sampleRate= */ null, /* spatialAudio= */ false);
  2211. this.uriToStreamInfosMap_.set(key, streamInfo);
  2212. return streamInfo;
  2213. }
  2214. /**
  2215. * @param {number} streamId
  2216. * @param {!Array<string>} verbatimMediaPlaylistUris
  2217. * @param {string} codecs
  2218. * @param {string} type
  2219. * @param {?string} languageValue
  2220. * @param {boolean} primary
  2221. * @param {?string} name
  2222. * @param {?number} channelsCount
  2223. * @param {Map<string, string>} closedCaptions
  2224. * @param {?string} characteristics
  2225. * @param {boolean} forced
  2226. * @param {?number} sampleRate
  2227. * @param {boolean} spatialAudio
  2228. * @return {!shaka.hls.HlsParser.StreamInfo}
  2229. * @private
  2230. */
  2231. createStreamInfo_(streamId, verbatimMediaPlaylistUris, codecs, type,
  2232. languageValue, primary, name, channelsCount, closedCaptions,
  2233. characteristics, forced, sampleRate, spatialAudio) {
  2234. // TODO: Refactor, too many parameters
  2235. // This stream is lazy-loaded inside the createSegmentIndex function.
  2236. // So we start out with a stream object that does not contain the actual
  2237. // segment index, then download when createSegmentIndex is called.
  2238. const stream = this.makeStreamObject_(streamId, codecs, type,
  2239. languageValue, primary, name, channelsCount, closedCaptions,
  2240. characteristics, forced, sampleRate, spatialAudio);
  2241. const FAKE_MUXED_URL_ = shaka.hls.HlsParser.FAKE_MUXED_URL_;
  2242. if (verbatimMediaPlaylistUris.includes(FAKE_MUXED_URL_)) {
  2243. stream.isAudioMuxedInVideo = true;
  2244. // We assigned the TS mimetype because it is the only one that works
  2245. // with this functionality. MP4 is not supported right now.
  2246. stream.mimeType = 'video/mp2t';
  2247. this.setFullTypeForStream_(stream);
  2248. }
  2249. const streamInfo = {
  2250. stream,
  2251. type,
  2252. redirectUris: [],
  2253. getUris: () => {},
  2254. // These values are filled out or updated after lazy-loading:
  2255. minTimestamp: 0,
  2256. maxTimestamp: 0,
  2257. mediaSequenceToStartTime: new Map(),
  2258. canSkipSegments: false,
  2259. canBlockReload: false,
  2260. hasEndList: false,
  2261. firstSequenceNumber: -1,
  2262. nextMediaSequence: -1,
  2263. nextPart: -1,
  2264. loadedOnce: false,
  2265. };
  2266. const getUris = () => {
  2267. if (this.contentSteeringManager_ &&
  2268. verbatimMediaPlaylistUris.length > 1) {
  2269. return this.contentSteeringManager_.getLocations(streamId);
  2270. }
  2271. return streamInfo.redirectUris.concat(shaka.hls.Utils.constructUris(
  2272. [this.masterPlaylistUri_], verbatimMediaPlaylistUris,
  2273. this.globalVariables_));
  2274. };
  2275. streamInfo.getUris = getUris;
  2276. /** @param {!shaka.net.NetworkingEngine.PendingRequest} pendingRequest */
  2277. const downloadSegmentIndex = async (pendingRequest) => {
  2278. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  2279. try {
  2280. // Download the actual manifest.
  2281. const response = await pendingRequest.promise;
  2282. if (pendingRequest.aborted) {
  2283. return;
  2284. }
  2285. // Record the final URI after redirects.
  2286. const responseUri = response.uri;
  2287. if (responseUri != response.originalUri) {
  2288. const uris = streamInfo.getUris();
  2289. if (!uris.includes(responseUri)) {
  2290. streamInfo.redirectUris.push(responseUri);
  2291. }
  2292. }
  2293. // Record the redirected, final URI of this media playlist when we parse
  2294. // it.
  2295. /** @type {!shaka.hls.Playlist} */
  2296. const playlist = this.manifestTextParser_.parsePlaylist(response.data);
  2297. if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
  2298. throw new shaka.util.Error(
  2299. shaka.util.Error.Severity.CRITICAL,
  2300. shaka.util.Error.Category.MANIFEST,
  2301. shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY);
  2302. }
  2303. /** @type {!Array<!shaka.hls.Tag>} */
  2304. const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
  2305. 'EXT-X-DEFINE');
  2306. const mediaVariables =
  2307. this.parseMediaVariables_(variablesTags, responseUri);
  2308. const mimeType = undefined;
  2309. let requestBasicInfo = false;
  2310. // If no codec info was provided in the manifest and codec guessing is
  2311. // disabled we try to get necessary info from the media data.
  2312. if ((!this.codecInfoInManifest_ &&
  2313. this.config_.hls.disableCodecGuessing) ||
  2314. (this.needsClosedCaptionsDetection_ && type == ContentType.VIDEO &&
  2315. !this.config_.hls.disableClosedCaptionsDetection)) {
  2316. if (playlist.segments.length > 0) {
  2317. this.needsClosedCaptionsDetection_ = false;
  2318. requestBasicInfo = true;
  2319. }
  2320. }
  2321. const allowOverrideMimeType = !this.codecInfoInManifest_ &&
  2322. this.config_.hls.disableCodecGuessing;
  2323. const wasLive = this.isLive_();
  2324. const realStreamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
  2325. streamId, mediaVariables, playlist, getUris, codecs,
  2326. type, languageValue, primary, name, channelsCount, closedCaptions,
  2327. characteristics, forced, sampleRate, spatialAudio, mimeType,
  2328. requestBasicInfo, allowOverrideMimeType);
  2329. if (pendingRequest.aborted) {
  2330. return;
  2331. }
  2332. const realStream = realStreamInfo.stream;
  2333. this.determineStartTime_(playlist);
  2334. if (this.isLive_() && !wasLive) {
  2335. // Now that we know that the presentation is live, convert the
  2336. // timeline to live.
  2337. this.changePresentationTimelineToLive_(playlist);
  2338. }
  2339. // Copy values from the real stream info to our initial one.
  2340. streamInfo.minTimestamp = realStreamInfo.minTimestamp;
  2341. streamInfo.maxTimestamp = realStreamInfo.maxTimestamp;
  2342. streamInfo.canSkipSegments = realStreamInfo.canSkipSegments;
  2343. streamInfo.canBlockReload = realStreamInfo.canBlockReload;
  2344. streamInfo.hasEndList = realStreamInfo.hasEndList;
  2345. streamInfo.mediaSequenceToStartTime =
  2346. realStreamInfo.mediaSequenceToStartTime;
  2347. streamInfo.nextMediaSequence = realStreamInfo.nextMediaSequence;
  2348. streamInfo.nextPart = realStreamInfo.nextPart;
  2349. streamInfo.loadedOnce = true;
  2350. stream.segmentIndex = realStream.segmentIndex;
  2351. stream.encrypted = realStream.encrypted;
  2352. stream.drmInfos = realStream.drmInfos;
  2353. stream.keyIds = realStream.keyIds;
  2354. stream.mimeType = realStream.mimeType;
  2355. stream.bandwidth = stream.bandwidth || realStream.bandwidth;
  2356. stream.codecs = stream.codecs || realStream.codecs;
  2357. stream.closedCaptions =
  2358. stream.closedCaptions || realStream.closedCaptions;
  2359. stream.width = stream.width || realStream.width;
  2360. stream.height = stream.height || realStream.height;
  2361. stream.hdr = stream.hdr || realStream.hdr;
  2362. stream.colorGamut = stream.colorGamut || realStream.colorGamut;
  2363. stream.frameRate = stream.frameRate || realStream.frameRate;
  2364. if (stream.language == 'und' && realStream.language != 'und') {
  2365. stream.language = realStream.language;
  2366. }
  2367. stream.language = stream.language || realStream.language;
  2368. stream.channelsCount = stream.channelsCount || realStream.channelsCount;
  2369. stream.audioSamplingRate =
  2370. stream.audioSamplingRate || realStream.audioSamplingRate;
  2371. this.setFullTypeForStream_(stream);
  2372. // Since we lazy-loaded this content, the player may need to create new
  2373. // sessions for the DRM info in this stream.
  2374. if (stream.drmInfos.length) {
  2375. this.playerInterface_.newDrmInfo(stream);
  2376. }
  2377. let closedCaptionsUpdated = false;
  2378. if ((!closedCaptions && stream.closedCaptions) ||
  2379. (closedCaptions && stream.closedCaptions &&
  2380. closedCaptions.size != stream.closedCaptions.size)) {
  2381. closedCaptionsUpdated = true;
  2382. }
  2383. if (this.manifest_ && closedCaptionsUpdated) {
  2384. this.playerInterface_.makeTextStreamsForClosedCaptions(
  2385. this.manifest_);
  2386. }
  2387. if (type == ContentType.VIDEO || type == ContentType.AUDIO) {
  2388. for (const otherStreamInfo of this.uriToStreamInfosMap_.values()) {
  2389. if (!otherStreamInfo.loadedOnce && otherStreamInfo.type == type) {
  2390. // To aid manifest filtering, assume before loading that all video
  2391. // renditions have the same MIME type. (And likewise for audio.)
  2392. otherStreamInfo.stream.mimeType = realStream.mimeType;
  2393. this.setFullTypeForStream_(otherStreamInfo.stream);
  2394. }
  2395. }
  2396. }
  2397. if (type == ContentType.TEXT) {
  2398. const firstSegment = realStream.segmentIndex.earliestReference();
  2399. if (firstSegment && firstSegment.initSegmentReference) {
  2400. stream.mimeType = 'application/mp4';
  2401. this.setFullTypeForStream_(stream);
  2402. }
  2403. }
  2404. const qualityInfo =
  2405. shaka.media.QualityObserver.createQualityInfo(stream);
  2406. stream.segmentIndex.forEachTopLevelReference((reference) => {
  2407. if (reference.initSegmentReference) {
  2408. reference.initSegmentReference.mediaQuality = qualityInfo;
  2409. }
  2410. });
  2411. // Add finishing touches to the stream that can only be done once we
  2412. // have more full context on the media as a whole.
  2413. if (this.hasEnoughInfoToFinalizeStreams_()) {
  2414. if (!this.streamsFinalized_) {
  2415. // Mark this manifest as having been finalized, so we don't go
  2416. // through this whole process of finishing touches a second time.
  2417. this.streamsFinalized_ = true;
  2418. // Finalize all of the currently-loaded streams.
  2419. const streamInfos = Array.from(this.uriToStreamInfosMap_.values());
  2420. const activeStreamInfos =
  2421. streamInfos.filter((s) => s.stream.segmentIndex);
  2422. this.finalizeStreams_(activeStreamInfos);
  2423. // With the addition of this new stream, we now have enough info to
  2424. // figure out how long the streams should be. So process all streams
  2425. // we have downloaded up until this point.
  2426. this.determineDuration_();
  2427. // Finally, start the update timer, if this asset has been
  2428. // determined to be a livestream.
  2429. const delay = this.getUpdatePlaylistDelay_();
  2430. if (delay > 0) {
  2431. this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
  2432. }
  2433. } else {
  2434. // We don't need to go through the full process; just finalize this
  2435. // single stream.
  2436. this.finalizeStreams_([streamInfo]);
  2437. }
  2438. }
  2439. this.processDateRangeTags_(
  2440. playlist.tags, stream.type, mediaVariables, getUris);
  2441. if (this.manifest_) {
  2442. this.manifest_.startTime = this.startTime_;
  2443. }
  2444. } catch (e) {
  2445. stream.closeSegmentIndex();
  2446. if (e.code === shaka.util.Error.Code.OPERATION_ABORTED) {
  2447. return;
  2448. }
  2449. const handled = this.playerInterface_.disableStream(stream);
  2450. if (!handled) {
  2451. throw e;
  2452. }
  2453. }
  2454. };
  2455. /** @type {Promise} */
  2456. let creationPromise = null;
  2457. /** @type {!shaka.net.NetworkingEngine.PendingRequest} */
  2458. let pendingRequest;
  2459. const safeCreateSegmentIndex = () => {
  2460. // An operation is already in progress. The second and subsequent
  2461. // callers receive the same Promise as the first caller, and only one
  2462. // download operation will occur.
  2463. if (creationPromise) {
  2464. return creationPromise;
  2465. }
  2466. if (stream.isAudioMuxedInVideo) {
  2467. const segmentIndex = new shaka.media.SegmentIndex([]);
  2468. stream.segmentIndex = segmentIndex;
  2469. return Promise.resolve();
  2470. }
  2471. // Create a new PendingRequest to be able to cancel this specific
  2472. // download.
  2473. pendingRequest = this.requestManifest_(streamInfo.getUris(),
  2474. /* isPlaylist= */ true);
  2475. // Create a Promise tied to the outcome of downloadSegmentIndex(). If
  2476. // downloadSegmentIndex is rejected, creationPromise will also be
  2477. // rejected.
  2478. creationPromise = new Promise((resolve) => {
  2479. resolve(downloadSegmentIndex(pendingRequest));
  2480. });
  2481. return creationPromise;
  2482. };
  2483. stream.createSegmentIndex = safeCreateSegmentIndex;
  2484. stream.closeSegmentIndex = () => {
  2485. // If we're mid-creation, cancel it.
  2486. if (creationPromise && !stream.segmentIndex) {
  2487. pendingRequest.abort();
  2488. }
  2489. // If we have a segment index, release it.
  2490. if (stream.segmentIndex) {
  2491. stream.segmentIndex.release();
  2492. stream.segmentIndex = null;
  2493. }
  2494. // Clear the creation Promise so that a new operation can begin.
  2495. creationPromise = null;
  2496. };
  2497. return streamInfo;
  2498. }
  2499. /**
  2500. * @return {number}
  2501. * @private
  2502. */
  2503. getMinDuration_() {
  2504. let minDuration = Infinity;
  2505. for (const streamInfo of this.uriToStreamInfosMap_.values()) {
  2506. if (streamInfo.stream.segmentIndex && streamInfo.stream.type != 'text' &&
  2507. !streamInfo.stream.isAudioMuxedInVideo) {
  2508. // Since everything is already offset to 0 (either by sync or by being
  2509. // VOD), only maxTimestamp is necessary to compute the duration.
  2510. minDuration = Math.min(minDuration, streamInfo.maxTimestamp);
  2511. }
  2512. }
  2513. return minDuration;
  2514. }
  2515. /**
  2516. * @return {number}
  2517. * @private
  2518. */
  2519. getLiveDuration_() {
  2520. let maxTimestamp = Infinity;
  2521. let minTimestamp = Infinity;
  2522. for (const streamInfo of this.uriToStreamInfosMap_.values()) {
  2523. if (streamInfo.stream.segmentIndex && streamInfo.stream.type != 'text' &&
  2524. !streamInfo.stream.isAudioMuxedInVideo) {
  2525. maxTimestamp = Math.min(maxTimestamp, streamInfo.maxTimestamp);
  2526. minTimestamp = Math.min(minTimestamp, streamInfo.minTimestamp);
  2527. }
  2528. }
  2529. return maxTimestamp - minTimestamp;
  2530. }
  2531. /**
  2532. * @param {!Array<!shaka.extern.Stream>} streams
  2533. * @private
  2534. */
  2535. notifySegmentsForStreams_(streams) {
  2536. const references = [];
  2537. for (const stream of streams) {
  2538. if (!stream.segmentIndex) {
  2539. // The stream was closed since the list of streams was built.
  2540. continue;
  2541. }
  2542. stream.segmentIndex.forEachTopLevelReference((reference) => {
  2543. references.push(reference);
  2544. });
  2545. }
  2546. this.presentationTimeline_.notifySegments(references);
  2547. }
  2548. /**
  2549. * @param {!Array<!shaka.hls.HlsParser.StreamInfo>} streamInfos
  2550. * @private
  2551. */
  2552. finalizeStreams_(streamInfos) {
  2553. if (!this.isLive_()) {
  2554. const minDuration = this.getMinDuration_();
  2555. for (const streamInfo of streamInfos) {
  2556. streamInfo.stream.segmentIndex.fit(/* periodStart= */ 0, minDuration);
  2557. }
  2558. }
  2559. this.notifySegmentsForStreams_(streamInfos.map((s) => s.stream));
  2560. const activeStreamInfos = Array.from(this.uriToStreamInfosMap_.values())
  2561. .filter((s) => s.stream.segmentIndex);
  2562. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  2563. const hasAudio =
  2564. activeStreamInfos.some((s) => s.stream.type == ContentType.AUDIO);
  2565. const hasVideo =
  2566. activeStreamInfos.some((s) => s.stream.type == ContentType.VIDEO);
  2567. const liveWithNoProgramDateTime =
  2568. this.isLive_() && !this.usesProgramDateTime_;
  2569. const vodWithOnlyAudioOrVideo = !this.isLive_() &&
  2570. this.usesProgramDateTime_ && !(hasAudio && hasVideo);
  2571. if (this.config_.hls.ignoreManifestProgramDateTime ||
  2572. liveWithNoProgramDateTime || vodWithOnlyAudioOrVideo) {
  2573. this.syncStreamsWithSequenceNumber_(
  2574. streamInfos, liveWithNoProgramDateTime);
  2575. } else {
  2576. this.syncStreamsWithProgramDateTime_(streamInfos);
  2577. if (this.config_.hls.ignoreManifestProgramDateTimeForTypes.length > 0) {
  2578. this.syncStreamsWithSequenceNumber_(streamInfos);
  2579. }
  2580. }
  2581. }
  2582. /**
  2583. * @param {string} type
  2584. * @return {boolean}
  2585. * @private
  2586. */
  2587. ignoreManifestProgramDateTimeFor_(type) {
  2588. if (this.config_.hls.ignoreManifestProgramDateTime) {
  2589. return true;
  2590. }
  2591. const forTypes = this.config_.hls.ignoreManifestProgramDateTimeForTypes;
  2592. return forTypes.includes(type);
  2593. }
  2594. /**
  2595. * There are some values on streams that can only be set once we know about
  2596. * both the video and audio content, if present.
  2597. * This checks if there is at least one video downloaded (if the media has
  2598. * video), and that there is at least one audio downloaded (if the media has
  2599. * audio).
  2600. * @return {boolean}
  2601. * @private
  2602. */
  2603. hasEnoughInfoToFinalizeStreams_() {
  2604. if (!this.manifest_) {
  2605. return false;
  2606. }
  2607. const videos = [];
  2608. const audios = [];
  2609. for (const variant of this.manifest_.variants) {
  2610. if (variant.video) {
  2611. videos.push(variant.video);
  2612. }
  2613. if (variant.audio) {
  2614. audios.push(variant.audio);
  2615. }
  2616. }
  2617. if (videos.length > 0 && !videos.some((stream) => stream.segmentIndex)) {
  2618. return false;
  2619. }
  2620. if (audios.length > 0 && !audios.some((stream) => stream.segmentIndex)) {
  2621. return false;
  2622. }
  2623. return true;
  2624. }
  2625. /**
  2626. * @param {number} streamId
  2627. * @param {!Map<string, string>} variables
  2628. * @param {!shaka.hls.Playlist} playlist
  2629. * @param {function(): !Array<string>} getUris
  2630. * @param {string} codecs
  2631. * @param {string} type
  2632. * @param {?string} languageValue
  2633. * @param {boolean} primary
  2634. * @param {?string} name
  2635. * @param {?number} channelsCount
  2636. * @param {Map<string, string>} closedCaptions
  2637. * @param {?string} characteristics
  2638. * @param {boolean} forced
  2639. * @param {?number} sampleRate
  2640. * @param {boolean} spatialAudio
  2641. * @param {(string|undefined)} mimeType
  2642. * @param {boolean=} requestBasicInfo
  2643. * @param {boolean=} allowOverrideMimeType
  2644. * @return {!Promise<!shaka.hls.HlsParser.StreamInfo>}
  2645. * @private
  2646. */
  2647. async convertParsedPlaylistIntoStreamInfo_(streamId, variables, playlist,
  2648. getUris, codecs, type, languageValue, primary, name,
  2649. channelsCount, closedCaptions, characteristics, forced, sampleRate,
  2650. spatialAudio, mimeType = undefined, requestBasicInfo = true,
  2651. allowOverrideMimeType = true) {
  2652. const playlistSegments = playlist.segments || [];
  2653. const allAreMissing = playlistSegments.every((seg) => {
  2654. if (shaka.hls.Utils.getFirstTagWithName(seg.tags, 'EXT-X-GAP')) {
  2655. return true;
  2656. }
  2657. return false;
  2658. });
  2659. if (!playlistSegments.length || allAreMissing) {
  2660. throw new shaka.util.Error(
  2661. shaka.util.Error.Severity.CRITICAL,
  2662. shaka.util.Error.Category.MANIFEST,
  2663. shaka.util.Error.Code.HLS_EMPTY_MEDIA_PLAYLIST);
  2664. }
  2665. this.determinePresentationType_(playlist);
  2666. if (this.isLive_()) {
  2667. this.determineLastTargetDuration_(playlist);
  2668. }
  2669. const mediaSequenceToStartTime = this.isLive_() ?
  2670. this.mediaSequenceToStartTimeByType_.get(type) : new Map();
  2671. const {segments, bandwidth} = this.createSegments_(
  2672. playlist, mediaSequenceToStartTime, variables, getUris, type);
  2673. let width = null;
  2674. let height = null;
  2675. let videoRange = null;
  2676. let colorGamut = null;
  2677. let frameRate = null;
  2678. if (segments.length > 0 && requestBasicInfo) {
  2679. const basicInfo = await this.getBasicInfoFromSegments_(segments);
  2680. type = basicInfo.type;
  2681. languageValue = basicInfo.language;
  2682. channelsCount = basicInfo.channelCount;
  2683. sampleRate = basicInfo.sampleRate;
  2684. if (!this.config_.disableText) {
  2685. closedCaptions = basicInfo.closedCaptions;
  2686. }
  2687. height = basicInfo.height;
  2688. width = basicInfo.width;
  2689. videoRange = basicInfo.videoRange;
  2690. colorGamut = basicInfo.colorGamut;
  2691. frameRate = basicInfo.frameRate;
  2692. if (allowOverrideMimeType) {
  2693. mimeType = basicInfo.mimeType;
  2694. codecs = basicInfo.codecs;
  2695. }
  2696. }
  2697. if (!mimeType) {
  2698. mimeType = await this.guessMimeType_(type, codecs, segments);
  2699. // Some manifests don't say what text codec they use, this is a problem
  2700. // if the cmft extension is used because we identify the mimeType as
  2701. // application/mp4. In this case if we don't detect initialization
  2702. // segments, we assume that the mimeType is text/vtt.
  2703. if (type == shaka.util.ManifestParserUtils.ContentType.TEXT &&
  2704. !codecs && mimeType == 'application/mp4' &&
  2705. segments[0] && !segments[0].initSegmentReference) {
  2706. mimeType = 'text/vtt';
  2707. }
  2708. }
  2709. const {drmInfos, keyIds, encrypted, aesEncrypted} =
  2710. await this.parseDrmInfo_(playlist, mimeType, getUris, variables);
  2711. if (encrypted && !drmInfos.length && !aesEncrypted) {
  2712. throw new shaka.util.Error(
  2713. shaka.util.Error.Severity.CRITICAL,
  2714. shaka.util.Error.Category.MANIFEST,
  2715. shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED);
  2716. }
  2717. const stream = this.makeStreamObject_(streamId, codecs, type,
  2718. languageValue, primary, name, channelsCount, closedCaptions,
  2719. characteristics, forced, sampleRate, spatialAudio);
  2720. stream.encrypted = encrypted && !aesEncrypted;
  2721. stream.drmInfos = drmInfos;
  2722. stream.keyIds = keyIds;
  2723. stream.mimeType = mimeType;
  2724. if (bandwidth) {
  2725. stream.bandwidth = bandwidth;
  2726. }
  2727. this.setFullTypeForStream_(stream);
  2728. if (type == shaka.util.ManifestParserUtils.ContentType.VIDEO &&
  2729. (width || height || videoRange || colorGamut)) {
  2730. this.addVideoAttributes_(stream, width, height,
  2731. frameRate, videoRange, /* videoLayout= */ null, colorGamut);
  2732. }
  2733. // This new calculation is necessary for Low Latency streams.
  2734. if (this.isLive_()) {
  2735. this.determineLastTargetDuration_(playlist);
  2736. }
  2737. const firstStartTime = segments[0].startTime;
  2738. const lastSegment = segments[segments.length - 1];
  2739. const lastEndTime = lastSegment.endTime;
  2740. /** @type {!shaka.media.SegmentIndex} */
  2741. const segmentIndex = new shaka.media.SegmentIndex(segments);
  2742. stream.segmentIndex = segmentIndex;
  2743. const serverControlTag = shaka.hls.Utils.getFirstTagWithName(
  2744. playlist.tags, 'EXT-X-SERVER-CONTROL');
  2745. const canSkipSegments = serverControlTag ?
  2746. serverControlTag.getAttribute('CAN-SKIP-UNTIL') != null : false;
  2747. const canBlockReload = serverControlTag ?
  2748. serverControlTag.getAttribute('CAN-BLOCK-RELOAD') != null : false;
  2749. const mediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber(
  2750. playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0);
  2751. const {nextMediaSequence, nextPart} =
  2752. this.getNextMediaSequenceAndPart_(mediaSequenceNumber, segments);
  2753. return {
  2754. stream,
  2755. type,
  2756. redirectUris: [],
  2757. getUris,
  2758. minTimestamp: firstStartTime,
  2759. maxTimestamp: lastEndTime,
  2760. canSkipSegments,
  2761. canBlockReload,
  2762. hasEndList: false,
  2763. firstSequenceNumber: -1,
  2764. nextMediaSequence,
  2765. nextPart,
  2766. mediaSequenceToStartTime,
  2767. loadedOnce: false,
  2768. };
  2769. }
  2770. /**
  2771. * Get the next msn and part
  2772. *
  2773. * @param {number} mediaSequenceNumber
  2774. * @param {!Array<!shaka.media.SegmentReference>} segments
  2775. * @return {{nextMediaSequence: number, nextPart:number}}}
  2776. * @private
  2777. */
  2778. getNextMediaSequenceAndPart_(mediaSequenceNumber, segments) {
  2779. const currentMediaSequence = mediaSequenceNumber + segments.length - 1;
  2780. let nextMediaSequence = currentMediaSequence;
  2781. let nextPart = -1;
  2782. if (!segments.length) {
  2783. nextMediaSequence++;
  2784. return {
  2785. nextMediaSequence,
  2786. nextPart,
  2787. };
  2788. }
  2789. const lastSegment = segments[segments.length - 1];
  2790. const partialReferences = lastSegment.partialReferences;
  2791. if (!lastSegment.partialReferences.length) {
  2792. nextMediaSequence++;
  2793. if (lastSegment.hasByterangeOptimization()) {
  2794. nextPart = 0;
  2795. }
  2796. return {
  2797. nextMediaSequence,
  2798. nextPart,
  2799. };
  2800. }
  2801. nextPart = partialReferences.length - 1;
  2802. const lastPartialReference =
  2803. partialReferences[partialReferences.length - 1];
  2804. if (!lastPartialReference.isPreload()) {
  2805. nextMediaSequence++;
  2806. nextPart = 0;
  2807. }
  2808. return {
  2809. nextMediaSequence,
  2810. nextPart,
  2811. };
  2812. }
  2813. /**
  2814. * Creates a stream object with the given parameters.
  2815. * The parameters that are passed into here are only the things that can be
  2816. * known without downloading the media playlist; other values must be set
  2817. * manually on the object after creation.
  2818. * @param {number} id
  2819. * @param {string} codecs
  2820. * @param {string} type
  2821. * @param {?string} languageValue
  2822. * @param {boolean} primary
  2823. * @param {?string} name
  2824. * @param {?number} channelsCount
  2825. * @param {Map<string, string>} closedCaptions
  2826. * @param {?string} characteristics
  2827. * @param {boolean} forced
  2828. * @param {?number} sampleRate
  2829. * @param {boolean} spatialAudio
  2830. * @return {!shaka.extern.Stream}
  2831. * @private
  2832. */
  2833. makeStreamObject_(id, codecs, type, languageValue, primary, name,
  2834. channelsCount, closedCaptions, characteristics, forced, sampleRate,
  2835. spatialAudio) {
  2836. // Fill out a "best-guess" mimeType, for now. It will be replaced once the
  2837. // stream is lazy-loaded.
  2838. const mimeType = this.guessMimeTypeBeforeLoading_(type, codecs) ||
  2839. this.guessMimeTypeFallback_(type);
  2840. const roles = [];
  2841. if (characteristics) {
  2842. for (const characteristic of characteristics.split(',')) {
  2843. roles.push(characteristic);
  2844. }
  2845. }
  2846. let kind = undefined;
  2847. let accessibilityPurpose = null;
  2848. if (type == shaka.util.ManifestParserUtils.ContentType.TEXT) {
  2849. if (roles.includes('public.accessibility.transcribes-spoken-dialog') &&
  2850. roles.includes('public.accessibility.describes-music-and-sound')) {
  2851. kind = shaka.util.ManifestParserUtils.TextStreamKind.CLOSED_CAPTION;
  2852. } else {
  2853. kind = shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE;
  2854. }
  2855. } else {
  2856. if (roles.includes('public.accessibility.describes-video')) {
  2857. accessibilityPurpose =
  2858. shaka.media.ManifestParser.AccessibilityPurpose.VISUALLY_IMPAIRED;
  2859. }
  2860. }
  2861. // If there are no roles, and we have defaulted to the subtitle "kind" for
  2862. // this track, add the implied subtitle role.
  2863. if (!roles.length &&
  2864. kind === shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE) {
  2865. roles.push(shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE);
  2866. }
  2867. const stream = {
  2868. id: this.globalId_++,
  2869. originalId: name,
  2870. groupId: null,
  2871. createSegmentIndex: () => Promise.resolve(),
  2872. segmentIndex: null,
  2873. mimeType,
  2874. codecs,
  2875. kind: (type == shaka.util.ManifestParserUtils.ContentType.TEXT) ?
  2876. shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE : undefined,
  2877. encrypted: false,
  2878. drmInfos: [],
  2879. keyIds: new Set(),
  2880. language: this.getLanguage_(languageValue),
  2881. originalLanguage: languageValue,
  2882. label: name, // For historical reasons, since before "originalId".
  2883. type,
  2884. primary,
  2885. // TODO: trick mode
  2886. trickModeVideo: null,
  2887. dependencyStream: null,
  2888. emsgSchemeIdUris: null,
  2889. frameRate: undefined,
  2890. pixelAspectRatio: undefined,
  2891. width: undefined,
  2892. height: undefined,
  2893. bandwidth: undefined,
  2894. roles,
  2895. forced,
  2896. channelsCount,
  2897. audioSamplingRate: sampleRate,
  2898. spatialAudio,
  2899. closedCaptions,
  2900. hdr: undefined,
  2901. colorGamut: undefined,
  2902. videoLayout: undefined,
  2903. tilesLayout: undefined,
  2904. accessibilityPurpose: accessibilityPurpose,
  2905. external: false,
  2906. fastSwitching: false,
  2907. fullMimeTypes: new Set(),
  2908. isAudioMuxedInVideo: false,
  2909. baseOriginalId: null,
  2910. };
  2911. this.setFullTypeForStream_(stream);
  2912. return stream;
  2913. }
  2914. /**
  2915. * @param {!shaka.hls.Playlist} playlist
  2916. * @param {string} mimeType
  2917. * @param {function(): !Array<string>} getUris
  2918. * @param {?Map<string, string>=} variables
  2919. * @return {Promise<{
  2920. * drmInfos: !Array<shaka.extern.DrmInfo>,
  2921. * keyIds: !Set<string>,
  2922. * encrypted: boolean,
  2923. * aesEncrypted: boolean
  2924. * }>}
  2925. * @private
  2926. */
  2927. async parseDrmInfo_(playlist, mimeType, getUris, variables) {
  2928. /** @type {!Map<!shaka.hls.Tag, ?shaka.media.InitSegmentReference>} */
  2929. const drmTagsMap = new Map();
  2930. if (!this.config_.ignoreDrmInfo && playlist.segments) {
  2931. for (const segment of playlist.segments) {
  2932. const segmentKeyTags = shaka.hls.Utils.filterTagsByName(segment.tags,
  2933. 'EXT-X-KEY');
  2934. let initSegmentRef = null;
  2935. if (segmentKeyTags.length) {
  2936. initSegmentRef = this.getInitSegmentReference_(playlist,
  2937. segment.tags, getUris, variables);
  2938. for (const segmentKeyTag of segmentKeyTags) {
  2939. drmTagsMap.set(segmentKeyTag, initSegmentRef);
  2940. }
  2941. }
  2942. }
  2943. }
  2944. let encrypted = false;
  2945. let aesEncrypted = false;
  2946. /** @type {!Array<shaka.extern.DrmInfo>}*/
  2947. const drmInfos = [];
  2948. const keyIds = new Set();
  2949. for (const [key, value] of drmTagsMap) {
  2950. const drmTag = /** @type {!shaka.hls.Tag} */ (key);
  2951. const initSegmentRef =
  2952. /** @type {?shaka.media.InitSegmentReference} */ (value);
  2953. const method = drmTag.getRequiredAttrValue('METHOD');
  2954. if (method != 'NONE') {
  2955. encrypted = true;
  2956. // According to the HLS spec, KEYFORMAT is optional and implicitly
  2957. // defaults to "identity".
  2958. // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-4.4.4.4
  2959. const keyFormat =
  2960. drmTag.getAttributeValue('KEYFORMAT') || 'identity';
  2961. let drmInfo = null;
  2962. if (this.isAesMethod_(method)) {
  2963. // These keys are handled separately.
  2964. aesEncrypted = true;
  2965. continue;
  2966. } else if (keyFormat == 'identity') {
  2967. // eslint-disable-next-line no-await-in-loop
  2968. drmInfo = await this.identityDrmParser_(
  2969. drmTag, mimeType, getUris, initSegmentRef, variables);
  2970. } else {
  2971. const drmParser =
  2972. this.keyFormatsToDrmParsers_.get(keyFormat);
  2973. drmInfo = drmParser ?
  2974. // eslint-disable-next-line no-await-in-loop
  2975. await drmParser(drmTag, mimeType, initSegmentRef) :
  2976. null;
  2977. }
  2978. if (drmInfo) {
  2979. if (drmInfo.keyIds) {
  2980. for (const keyId of drmInfo.keyIds) {
  2981. keyIds.add(keyId);
  2982. }
  2983. }
  2984. drmInfos.push(drmInfo);
  2985. } else {
  2986. shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
  2987. }
  2988. }
  2989. }
  2990. return {drmInfos, keyIds, encrypted, aesEncrypted};
  2991. }
  2992. /**
  2993. * @param {!shaka.hls.Tag} drmTag
  2994. * @param {!shaka.hls.Playlist} playlist
  2995. * @param {function(): !Array<string>} getUris
  2996. * @param {?Map<string, string>=} variables
  2997. * @return {!shaka.extern.aesKey}
  2998. * @private
  2999. */
  3000. parseAESDrmTag_(drmTag, playlist, getUris, variables) {
  3001. // Check if the Web Crypto API is available.
  3002. if (!window.crypto || !window.crypto.subtle) {
  3003. shaka.log.alwaysWarn('Web Crypto API is not available to decrypt ' +
  3004. 'AES. (Web Crypto only exists in secure origins like https)');
  3005. throw new shaka.util.Error(
  3006. shaka.util.Error.Severity.CRITICAL,
  3007. shaka.util.Error.Category.MANIFEST,
  3008. shaka.util.Error.Code.NO_WEB_CRYPTO_API);
  3009. }
  3010. // HLS RFC 8216 Section 5.2:
  3011. // An EXT-X-KEY tag with a KEYFORMAT of "identity" that does not have an IV
  3012. // attribute indicates that the Media Sequence Number is to be used as the
  3013. // IV when decrypting a Media Segment, by putting its big-endian binary
  3014. // representation into a 16-octet (128-bit) buffer and padding (on the left)
  3015. // with zeros.
  3016. let firstMediaSequenceNumber = 0;
  3017. let iv;
  3018. const ivHex = drmTag.getAttributeValue('IV', '');
  3019. if (!ivHex) {
  3020. // Media Sequence Number will be used as IV.
  3021. firstMediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber(
  3022. playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0);
  3023. } else {
  3024. // Exclude 0x at the start of string.
  3025. iv = shaka.util.Uint8ArrayUtils.fromHex(ivHex.substr(2));
  3026. if (iv.byteLength != 16) {
  3027. throw new shaka.util.Error(
  3028. shaka.util.Error.Severity.CRITICAL,
  3029. shaka.util.Error.Category.MANIFEST,
  3030. shaka.util.Error.Code.AES_128_INVALID_IV_LENGTH);
  3031. }
  3032. }
  3033. const keyUris = shaka.hls.Utils.constructSegmentUris(
  3034. getUris(), drmTag.getRequiredAttrValue('URI'), variables);
  3035. const keyMapKey = keyUris.sort().join('');
  3036. const aesKeyInfoKey =
  3037. `${drmTag.toString()}-${firstMediaSequenceNumber}-${keyMapKey}`;
  3038. if (!this.aesKeyInfoMap_.has(aesKeyInfoKey)) {
  3039. // Default AES-128
  3040. const keyInfo = {
  3041. bitsKey: 128,
  3042. blockCipherMode: 'CBC',
  3043. iv,
  3044. firstMediaSequenceNumber,
  3045. };
  3046. const method = drmTag.getRequiredAttrValue('METHOD');
  3047. switch (method) {
  3048. case 'AES-256':
  3049. keyInfo.bitsKey = 256;
  3050. break;
  3051. case 'AES-256-CTR':
  3052. keyInfo.bitsKey = 256;
  3053. keyInfo.blockCipherMode = 'CTR';
  3054. break;
  3055. }
  3056. // Don't download the key object until the segment is parsed, to avoid a
  3057. // startup delay for long manifests with lots of keys.
  3058. keyInfo.fetchKey = async () => {
  3059. if (!this.aesKeyMap_.has(keyMapKey)) {
  3060. const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
  3061. const request = shaka.net.NetworkingEngine.makeRequest(
  3062. keyUris, this.config_.retryParameters);
  3063. const keyResponse = this.makeNetworkRequest_(request, requestType)
  3064. .promise;
  3065. this.aesKeyMap_.set(keyMapKey, keyResponse);
  3066. }
  3067. const keyResponse = await this.aesKeyMap_.get(keyMapKey);
  3068. // keyResponse.status is undefined when URI is "data:text/plain;base64,"
  3069. if (!keyResponse.data ||
  3070. keyResponse.data.byteLength != (keyInfo.bitsKey / 8)) {
  3071. throw new shaka.util.Error(
  3072. shaka.util.Error.Severity.CRITICAL,
  3073. shaka.util.Error.Category.MANIFEST,
  3074. shaka.util.Error.Code.AES_128_INVALID_KEY_LENGTH);
  3075. }
  3076. const algorithm = {
  3077. name: keyInfo.blockCipherMode == 'CTR' ? 'AES-CTR' : 'AES-CBC',
  3078. length: keyInfo.bitsKey,
  3079. };
  3080. keyInfo.cryptoKey = await window.crypto.subtle.importKey(
  3081. 'raw', keyResponse.data, algorithm, true, ['decrypt']);
  3082. keyInfo.fetchKey = undefined; // No longer needed.
  3083. };
  3084. this.aesKeyInfoMap_.set(aesKeyInfoKey, keyInfo);
  3085. }
  3086. return this.aesKeyInfoMap_.get(aesKeyInfoKey);
  3087. }
  3088. /**
  3089. * @param {!shaka.hls.Playlist} playlist
  3090. * @private
  3091. */
  3092. determineStartTime_(playlist) {
  3093. // If we already have a starttime we avoid processing this again.
  3094. if (this.startTime_ != null) {
  3095. return;
  3096. }
  3097. const startTimeTag =
  3098. shaka.hls.Utils.getFirstTagWithName(playlist.tags, 'EXT-X-START');
  3099. if (startTimeTag) {
  3100. this.startTime_ =
  3101. Number(startTimeTag.getRequiredAttrValue('TIME-OFFSET'));
  3102. }
  3103. }
  3104. /**
  3105. * @param {!shaka.hls.Playlist} playlist
  3106. * @private
  3107. */
  3108. determinePresentationType_(playlist) {
  3109. const PresentationType = shaka.hls.HlsParser.PresentationType_;
  3110. const presentationTypeTag =
  3111. shaka.hls.Utils.getFirstTagWithName(playlist.tags,
  3112. 'EXT-X-PLAYLIST-TYPE');
  3113. const endListTag =
  3114. shaka.hls.Utils.getFirstTagWithName(playlist.tags, 'EXT-X-ENDLIST');
  3115. const isVod = (presentationTypeTag && presentationTypeTag.value == 'VOD') ||
  3116. endListTag;
  3117. const isEvent = presentationTypeTag &&
  3118. presentationTypeTag.value == 'EVENT' && !isVod;
  3119. const isLive = !isVod && !isEvent;
  3120. if (isVod) {
  3121. this.setPresentationType_(PresentationType.VOD);
  3122. } else {
  3123. // If it's not VOD, it must be presentation type LIVE or an ongoing EVENT.
  3124. if (isLive) {
  3125. this.setPresentationType_(PresentationType.LIVE);
  3126. } else {
  3127. this.setPresentationType_(PresentationType.EVENT);
  3128. }
  3129. }
  3130. }
  3131. /**
  3132. * @param {!shaka.hls.Playlist} playlist
  3133. * @private
  3134. */
  3135. determineLastTargetDuration_(playlist) {
  3136. let lastTargetDuration = Infinity;
  3137. const segments = playlist.segments;
  3138. if (segments.length) {
  3139. let segmentIndex = segments.length - 1;
  3140. while (segmentIndex >= 0) {
  3141. const segment = segments[segmentIndex];
  3142. const extinfTag =
  3143. shaka.hls.Utils.getFirstTagWithName(segment.tags, 'EXTINF');
  3144. if (extinfTag) {
  3145. // The EXTINF tag format is '#EXTINF:<duration>,[<title>]'.
  3146. // We're interested in the duration part.
  3147. const extinfValues = extinfTag.value.split(',');
  3148. lastTargetDuration = Number(extinfValues[0]);
  3149. break;
  3150. }
  3151. segmentIndex--;
  3152. }
  3153. }
  3154. const targetDurationTag = this.getRequiredTag_(playlist.tags,
  3155. 'EXT-X-TARGETDURATION');
  3156. const targetDuration = Number(targetDurationTag.value);
  3157. const partialTargetDurationTag =
  3158. shaka.hls.Utils.getFirstTagWithName(playlist.tags, 'EXT-X-PART-INF');
  3159. if (partialTargetDurationTag) {
  3160. this.partialTargetDuration_ = Number(
  3161. partialTargetDurationTag.getRequiredAttrValue('PART-TARGET'));
  3162. }
  3163. // Get the server-recommended min distance from the live edge.
  3164. const serverControlTag = shaka.hls.Utils.getFirstTagWithName(
  3165. playlist.tags, 'EXT-X-SERVER-CONTROL');
  3166. // According to the HLS spec, updates should not happen more often than
  3167. // once in targetDuration. It also requires us to only update the active
  3168. // variant. We might implement that later, but for now every variant
  3169. // will be updated. To get the update period, choose the smallest
  3170. // targetDuration value across all playlists.
  3171. // 1. Update the shortest one to use as update period and segment
  3172. // availability time (for LIVE).
  3173. if (this.lowLatencyMode_ && this.partialTargetDuration_) {
  3174. // For low latency streaming, use the partial segment target duration.
  3175. if (this.lowLatencyByterangeOptimization_) {
  3176. // We always have at least 1 partial segment part, and most servers
  3177. // allow you to make a request with _HLS_msn=X&_HLS_part=0 with a
  3178. // distance of 4 partial segments. With this we ensure that we
  3179. // obtain the minimum latency in this type of case.
  3180. if (this.partialTargetDuration_ * 5 <= lastTargetDuration) {
  3181. this.lastTargetDuration_ = Math.min(
  3182. this.partialTargetDuration_, this.lastTargetDuration_);
  3183. } else {
  3184. this.lastTargetDuration_ = Math.min(
  3185. lastTargetDuration, this.lastTargetDuration_);
  3186. }
  3187. } else {
  3188. this.lastTargetDuration_ = Math.min(
  3189. this.partialTargetDuration_, this.lastTargetDuration_);
  3190. }
  3191. // Use 'PART-HOLD-BACK' as the presentation delay for low latency mode.
  3192. this.lowLatencyPresentationDelay_ = serverControlTag ? Number(
  3193. serverControlTag.getRequiredAttrValue('PART-HOLD-BACK')) : 0;
  3194. } else {
  3195. this.lastTargetDuration_ = Math.min(
  3196. lastTargetDuration, this.lastTargetDuration_);
  3197. // Use 'HOLD-BACK' as the presentation delay for default if defined.
  3198. const holdBack = serverControlTag ?
  3199. serverControlTag.getAttribute('HOLD-BACK') : null;
  3200. this.presentationDelay_ = holdBack ? Number(holdBack.value) : 0;
  3201. }
  3202. // 2. Update the longest target duration if need be to use as a
  3203. // presentation delay later.
  3204. this.maxTargetDuration_ = Math.max(
  3205. targetDuration, this.maxTargetDuration_);
  3206. }
  3207. /**
  3208. * @param {!shaka.hls.Playlist} playlist
  3209. * @private
  3210. */
  3211. changePresentationTimelineToLive_(playlist) {
  3212. // The live edge will be calculated from segments, so we don't need to
  3213. // set a presentation start time. We will assert later that this is
  3214. // working as expected.
  3215. // The HLS spec (RFC 8216) states in 6.3.3:
  3216. //
  3217. // "The client SHALL choose which Media Segment to play first ... the
  3218. // client SHOULD NOT choose a segment that starts less than three target
  3219. // durations from the end of the Playlist file. Doing so can trigger
  3220. // playback stalls."
  3221. //
  3222. // We accomplish this in our DASH-y model by setting a presentation
  3223. // delay of configured value, or 3 segments duration if not configured.
  3224. // This will be the "live edge" of the presentation.
  3225. let presentationDelay = 0;
  3226. if (this.config_.defaultPresentationDelay) {
  3227. presentationDelay = this.config_.defaultPresentationDelay;
  3228. } else if (this.lowLatencyPresentationDelay_) {
  3229. presentationDelay = this.lowLatencyPresentationDelay_;
  3230. } else if (this.presentationDelay_) {
  3231. presentationDelay = this.presentationDelay_;
  3232. } else {
  3233. const totalSegments = playlist.segments.length;
  3234. let delaySegments = this.config_.hls.liveSegmentsDelay;
  3235. if (delaySegments > (totalSegments - 2)) {
  3236. delaySegments = Math.max(1, totalSegments - 2);
  3237. }
  3238. for (let i = totalSegments - delaySegments; i < totalSegments; i++) {
  3239. const extinfTag = shaka.hls.Utils.getFirstTagWithName(
  3240. playlist.segments[i].tags, 'EXTINF');
  3241. if (extinfTag) {
  3242. const extinfValues = extinfTag.value.split(',');
  3243. const duration = Number(extinfValues[0]);
  3244. presentationDelay += Math.ceil(duration);
  3245. } else {
  3246. presentationDelay += this.maxTargetDuration_;
  3247. }
  3248. }
  3249. }
  3250. if (this.startTime_ && this.startTime_ < 0) {
  3251. presentationDelay = Math.min(-this.startTime_, presentationDelay);
  3252. this.startTime_ += presentationDelay;
  3253. }
  3254. this.presentationTimeline_.setPresentationStartTime(0);
  3255. this.presentationTimeline_.setDelay(presentationDelay);
  3256. this.presentationTimeline_.setStatic(false);
  3257. }
  3258. /**
  3259. * Get the InitSegmentReference for a segment if it has a EXT-X-MAP tag.
  3260. * @param {!shaka.hls.Playlist} playlist
  3261. * @param {!Array<!shaka.hls.Tag>} tags Segment tags
  3262. * @param {function(): !Array<string>} getUris
  3263. * @param {?Map<string, string>=} variables
  3264. * @return {shaka.media.InitSegmentReference}
  3265. * @private
  3266. */
  3267. getInitSegmentReference_(playlist, tags, getUris, variables) {
  3268. /** @type {?shaka.hls.Tag} */
  3269. const mapTag = shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-MAP');
  3270. if (!mapTag) {
  3271. return null;
  3272. }
  3273. // Map tag example: #EXT-X-MAP:URI="main.mp4",BYTERANGE="720@0"
  3274. const verbatimInitSegmentUri = mapTag.getRequiredAttrValue('URI');
  3275. const absoluteInitSegmentUris = shaka.hls.Utils.constructSegmentUris(
  3276. getUris(), verbatimInitSegmentUri, variables);
  3277. const mapTagKey = [
  3278. absoluteInitSegmentUris.toString(),
  3279. mapTag.getAttributeValue('BYTERANGE', ''),
  3280. ].join('-');
  3281. if (!this.mapTagToInitSegmentRefMap_.has(mapTagKey)) {
  3282. /** @type {shaka.extern.aesKey|undefined} */
  3283. let aesKey = undefined;
  3284. let byteRangeTag = null;
  3285. let encrypted = false;
  3286. for (const tag of tags) {
  3287. if (tag.name == 'EXT-X-KEY') {
  3288. const method = tag.getRequiredAttrValue('METHOD');
  3289. if (this.isAesMethod_(method) && tag.id < mapTag.id) {
  3290. encrypted = false;
  3291. aesKey =
  3292. this.parseAESDrmTag_(tag, playlist, getUris, variables);
  3293. } else {
  3294. encrypted = method != 'NONE';
  3295. }
  3296. } else if (tag.name == 'EXT-X-BYTERANGE' && tag.id < mapTag.id) {
  3297. byteRangeTag = tag;
  3298. }
  3299. }
  3300. const initSegmentRef = this.createInitSegmentReference_(
  3301. absoluteInitSegmentUris, mapTag, byteRangeTag, aesKey, encrypted);
  3302. this.mapTagToInitSegmentRefMap_.set(mapTagKey, initSegmentRef);
  3303. }
  3304. return this.mapTagToInitSegmentRefMap_.get(mapTagKey);
  3305. }
  3306. /**
  3307. * Create an InitSegmentReference object for the EXT-X-MAP tag in the media
  3308. * playlist.
  3309. * @param {!Array<string>} absoluteInitSegmentUris
  3310. * @param {!shaka.hls.Tag} mapTag EXT-X-MAP
  3311. * @param {shaka.hls.Tag=} byteRangeTag EXT-X-BYTERANGE
  3312. * @param {shaka.extern.aesKey=} aesKey
  3313. * @param {boolean=} encrypted
  3314. * @return {!shaka.media.InitSegmentReference}
  3315. * @private
  3316. */
  3317. createInitSegmentReference_(absoluteInitSegmentUris, mapTag, byteRangeTag,
  3318. aesKey, encrypted) {
  3319. let startByte = 0;
  3320. let endByte = null;
  3321. let byterange = mapTag.getAttributeValue('BYTERANGE');
  3322. if (!byterange && byteRangeTag) {
  3323. byterange = byteRangeTag.value;
  3324. }
  3325. // If a BYTERANGE attribute is not specified, the segment consists
  3326. // of the entire resource.
  3327. if (byterange) {
  3328. const blocks = byterange.split('@');
  3329. const byteLength = Number(blocks[0]);
  3330. startByte = Number(blocks[1]);
  3331. endByte = startByte + byteLength - 1;
  3332. if (aesKey) {
  3333. // MAP segment encrypted with method AES, when served with
  3334. // HTTP Range, has the unencrypted size specified in the range.
  3335. // See: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-6.3.6
  3336. const length = (endByte + 1) - startByte;
  3337. if (length % 16) {
  3338. endByte += (16 - (length % 16));
  3339. }
  3340. }
  3341. }
  3342. const initSegmentRef = new shaka.media.InitSegmentReference(
  3343. () => absoluteInitSegmentUris,
  3344. startByte,
  3345. endByte,
  3346. /* mediaQuality= */ null,
  3347. /* timescale= */ null,
  3348. /* segmentData= */ null,
  3349. aesKey,
  3350. encrypted);
  3351. return initSegmentRef;
  3352. }
  3353. /**
  3354. * Parses one shaka.hls.Segment object into a shaka.media.SegmentReference.
  3355. *
  3356. * @param {shaka.media.InitSegmentReference} initSegmentReference
  3357. * @param {shaka.media.SegmentReference} previousReference
  3358. * @param {!shaka.hls.Segment} hlsSegment
  3359. * @param {number} startTime
  3360. * @param {!Map<string, string>} variables
  3361. * @param {!shaka.hls.Playlist} playlist
  3362. * @param {string} type
  3363. * @param {function(): !Array<string>} getUris
  3364. * @param {shaka.extern.aesKey=} aesKey
  3365. * @return {shaka.media.SegmentReference}
  3366. * @private
  3367. */
  3368. createSegmentReference_(
  3369. initSegmentReference, previousReference, hlsSegment, startTime,
  3370. variables, playlist, type, getUris, aesKey) {
  3371. const HlsParser = shaka.hls.HlsParser;
  3372. const getMimeType = (uri) => {
  3373. const parsedUri = new goog.Uri(uri);
  3374. const extension = parsedUri.getPath().split('.').pop();
  3375. const map = HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_.get(type);
  3376. let mimeType = map.get(extension);
  3377. if (!mimeType) {
  3378. mimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_.get(extension);
  3379. }
  3380. return mimeType;
  3381. };
  3382. const tags = hlsSegment.tags;
  3383. const extinfTag =
  3384. shaka.hls.Utils.getFirstTagWithName(tags, 'EXTINF');
  3385. let endTime = 0;
  3386. let startByte = 0;
  3387. let endByte = null;
  3388. if (hlsSegment.partialSegments.length) {
  3389. this.manifest_.isLowLatency = true;
  3390. }
  3391. let syncTime = null;
  3392. if (!this.config_.hls.ignoreManifestProgramDateTime) {
  3393. const dateTimeTag =
  3394. shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-PROGRAM-DATE-TIME');
  3395. if (dateTimeTag && dateTimeTag.value) {
  3396. syncTime = shaka.util.TXml.parseDate(dateTimeTag.value);
  3397. goog.asserts.assert(syncTime != null,
  3398. 'EXT-X-PROGRAM-DATE-TIME format not valid');
  3399. this.usesProgramDateTime_ = true;
  3400. }
  3401. }
  3402. let status = shaka.media.SegmentReference.Status.AVAILABLE;
  3403. if (shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-GAP')) {
  3404. this.manifest_.gapCount++;
  3405. status = shaka.media.SegmentReference.Status.MISSING;
  3406. }
  3407. if (!extinfTag) {
  3408. if (hlsSegment.partialSegments.length == 0) {
  3409. // EXTINF tag must be available if the segment has no partial segments.
  3410. throw new shaka.util.Error(
  3411. shaka.util.Error.Severity.CRITICAL,
  3412. shaka.util.Error.Category.MANIFEST,
  3413. shaka.util.Error.Code.HLS_REQUIRED_TAG_MISSING, 'EXTINF');
  3414. } else if (!this.lowLatencyMode_) {
  3415. // Without EXTINF and without low-latency mode, partial segments get
  3416. // ignored.
  3417. return null;
  3418. }
  3419. }
  3420. // Create SegmentReferences for the partial segments.
  3421. let partialSegmentRefs = [];
  3422. // Optimization for LL-HLS with byterange
  3423. // More info in https://tinyurl.com/hls-open-byte-range
  3424. let segmentWithByteRangeOptimization = false;
  3425. let getUrisOptimization = null;
  3426. let somePartialSegmentWithGap = false;
  3427. let isPreloadSegment = false;
  3428. if (this.lowLatencyMode_ && hlsSegment.partialSegments.length) {
  3429. const byterangeOptimizationSupport =
  3430. initSegmentReference && window.ReadableStream &&
  3431. this.config_.hls.allowLowLatencyByteRangeOptimization;
  3432. let partialSyncTime = syncTime;
  3433. for (let i = 0; i < hlsSegment.partialSegments.length; i++) {
  3434. const item = hlsSegment.partialSegments[i];
  3435. const pPreviousReference = i == 0 ?
  3436. previousReference : partialSegmentRefs[partialSegmentRefs.length - 1];
  3437. const pStartTime = (i == 0) ? startTime : pPreviousReference.endTime;
  3438. // If DURATION is missing from this partial segment, use the target
  3439. // partial duration from the top of the playlist, which is a required
  3440. // attribute for content with partial segments.
  3441. const pDuration = Number(item.getAttributeValue('DURATION')) ||
  3442. this.partialTargetDuration_;
  3443. // If for some reason we have neither an explicit duration, nor a target
  3444. // partial duration, we should SKIP this partial segment to avoid
  3445. // duplicating content in the presentation timeline.
  3446. if (!pDuration) {
  3447. continue;
  3448. }
  3449. const pEndTime = pStartTime + pDuration;
  3450. let pStartByte = 0;
  3451. let pEndByte = null;
  3452. if (item.name == 'EXT-X-PRELOAD-HINT') {
  3453. // A preload hinted partial segment may have byterange start info.
  3454. const pByterangeStart = item.getAttributeValue('BYTERANGE-START');
  3455. pStartByte = pByterangeStart ? Number(pByterangeStart) : 0;
  3456. // A preload hinted partial segment may have byterange length info.
  3457. const pByterangeLength = item.getAttributeValue('BYTERANGE-LENGTH');
  3458. if (pByterangeLength) {
  3459. pEndByte = pStartByte + Number(pByterangeLength) - 1;
  3460. } else if (pStartByte) {
  3461. // If we have a non-zero start byte, but no end byte, follow the
  3462. // recommendation of https://tinyurl.com/hls-open-byte-range and
  3463. // set the end byte explicitly to a large integer.
  3464. pEndByte = Number.MAX_SAFE_INTEGER;
  3465. }
  3466. } else {
  3467. const pByterange = item.getAttributeValue('BYTERANGE');
  3468. [pStartByte, pEndByte] =
  3469. this.parseByteRange_(pPreviousReference, pByterange);
  3470. }
  3471. const pUri = item.getAttributeValue('URI');
  3472. if (!pUri) {
  3473. continue;
  3474. }
  3475. let partialStatus = shaka.media.SegmentReference.Status.AVAILABLE;
  3476. if (item.getAttributeValue('GAP') == 'YES') {
  3477. this.manifest_.gapCount++;
  3478. partialStatus = shaka.media.SegmentReference.Status.MISSING;
  3479. somePartialSegmentWithGap = true;
  3480. }
  3481. let uris = null;
  3482. const getPartialUris = () => {
  3483. if (uris == null) {
  3484. goog.asserts.assert(pUri, 'Partial uri should be defined!');
  3485. uris = shaka.hls.Utils.constructSegmentUris(
  3486. getUris(), pUri, variables);
  3487. }
  3488. return uris;
  3489. };
  3490. if (byterangeOptimizationSupport &&
  3491. pStartByte >= 0 && pEndByte != null) {
  3492. getUrisOptimization = getPartialUris;
  3493. segmentWithByteRangeOptimization = true;
  3494. }
  3495. const partial = new shaka.media.SegmentReference(
  3496. pStartTime,
  3497. pEndTime,
  3498. getPartialUris,
  3499. pStartByte,
  3500. pEndByte,
  3501. initSegmentReference,
  3502. /* timestampOffset= */ 0,
  3503. /* appendWindowStart= */ 0,
  3504. /* appendWindowEnd= */ Infinity,
  3505. /* partialReferences= */ [],
  3506. /* tilesLayout= */ '',
  3507. /* tileDuration= */ null,
  3508. partialSyncTime,
  3509. partialStatus,
  3510. aesKey);
  3511. if (item.name == 'EXT-X-PRELOAD-HINT') {
  3512. partial.markAsPreload();
  3513. isPreloadSegment = true;
  3514. }
  3515. // The spec doesn't say that we can assume INDEPENDENT=YES for the
  3516. // first partial segment. It does call the flag "optional", though, and
  3517. // that cases where there are no such flags on any partial segments, it
  3518. // is sensible to assume the first one is independent.
  3519. if (item.getAttributeValue('INDEPENDENT') != 'YES' && i > 0) {
  3520. partial.markAsNonIndependent();
  3521. }
  3522. const pMimeType = getMimeType(pUri);
  3523. if (pMimeType) {
  3524. partial.mimeType = pMimeType;
  3525. if (HlsParser.MIME_TYPES_WITHOUT_INIT_SEGMENT_.has(pMimeType)) {
  3526. partial.initSegmentReference = null;
  3527. }
  3528. }
  3529. partialSegmentRefs.push(partial);
  3530. if (partialSyncTime) {
  3531. partialSyncTime += pDuration;
  3532. }
  3533. } // for-loop of hlsSegment.partialSegments
  3534. }
  3535. // If the segment has EXTINF tag, set the segment's end time, start byte
  3536. // and end byte based on the duration and byterange information.
  3537. // Otherwise, calculate the end time, start / end byte based on its partial
  3538. // segments.
  3539. // Note that the sum of partial segments durations may be slightly different
  3540. // from the parent segment's duration. In this case, use the duration from
  3541. // the parent segment tag.
  3542. if (extinfTag) {
  3543. // The EXTINF tag format is '#EXTINF:<duration>,[<title>]'.
  3544. // We're interested in the duration part.
  3545. const extinfValues = extinfTag.value.split(',');
  3546. const duration = Number(extinfValues[0]);
  3547. // Skip segments without duration
  3548. if (duration == 0) {
  3549. return null;
  3550. }
  3551. endTime = startTime + duration;
  3552. } else if (partialSegmentRefs.length) {
  3553. endTime = partialSegmentRefs[partialSegmentRefs.length - 1].endTime;
  3554. } else {
  3555. // Skip segments without duration and without partial segments
  3556. return null;
  3557. }
  3558. if (segmentWithByteRangeOptimization) {
  3559. // We cannot optimize segments with gaps, or with a start byte that is
  3560. // not 0.
  3561. if (somePartialSegmentWithGap || partialSegmentRefs[0].startByte != 0) {
  3562. segmentWithByteRangeOptimization = false;
  3563. getUrisOptimization = null;
  3564. } else {
  3565. partialSegmentRefs = [];
  3566. }
  3567. }
  3568. // If the segment has EXT-X-BYTERANGE tag, set the start byte and end byte
  3569. // base on the byterange information. If segment has no EXT-X-BYTERANGE tag
  3570. // and has partial segments, set the start byte and end byte base on the
  3571. // partial segments.
  3572. const byterangeTag =
  3573. shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-BYTERANGE');
  3574. if (byterangeTag) {
  3575. [startByte, endByte] =
  3576. this.parseByteRange_(previousReference, byterangeTag.value);
  3577. } else if (partialSegmentRefs.length) {
  3578. startByte = partialSegmentRefs[0].startByte;
  3579. endByte = partialSegmentRefs[partialSegmentRefs.length - 1].endByte;
  3580. }
  3581. let tilesLayout = '';
  3582. let tileDuration = null;
  3583. if (type == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
  3584. // By default in HLS the tilesLayout is 1x1
  3585. tilesLayout = '1x1';
  3586. const tilesTag =
  3587. shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-TILES');
  3588. if (tilesTag) {
  3589. tilesLayout = tilesTag.getRequiredAttrValue('LAYOUT');
  3590. const duration = tilesTag.getAttributeValue('DURATION');
  3591. if (duration) {
  3592. tileDuration = Number(duration);
  3593. }
  3594. }
  3595. }
  3596. let uris = null;
  3597. const getSegmentUris = () => {
  3598. if (getUrisOptimization) {
  3599. return getUrisOptimization();
  3600. }
  3601. if (uris == null) {
  3602. uris = shaka.hls.Utils.constructSegmentUris(getUris(),
  3603. hlsSegment.verbatimSegmentUri, variables);
  3604. }
  3605. return uris || [];
  3606. };
  3607. const allPartialSegments = partialSegmentRefs.length > 0 &&
  3608. !!hlsSegment.verbatimSegmentUri;
  3609. const reference = new shaka.media.SegmentReference(
  3610. startTime,
  3611. endTime,
  3612. getSegmentUris,
  3613. startByte,
  3614. endByte,
  3615. initSegmentReference,
  3616. /* timestampOffset= */ 0,
  3617. /* appendWindowStart= */ 0,
  3618. /* appendWindowEnd= */ Infinity,
  3619. partialSegmentRefs,
  3620. tilesLayout,
  3621. tileDuration,
  3622. syncTime,
  3623. status,
  3624. aesKey,
  3625. allPartialSegments,
  3626. );
  3627. const mimeType = getMimeType(hlsSegment.verbatimSegmentUri);
  3628. if (mimeType) {
  3629. reference.mimeType = mimeType;
  3630. if (HlsParser.MIME_TYPES_WITHOUT_INIT_SEGMENT_.has(mimeType)) {
  3631. reference.initSegmentReference = null;
  3632. }
  3633. }
  3634. if (segmentWithByteRangeOptimization) {
  3635. this.lowLatencyByterangeOptimization_ = true;
  3636. reference.markAsByterangeOptimization();
  3637. if (isPreloadSegment) {
  3638. reference.markAsPreload();
  3639. }
  3640. }
  3641. return reference;
  3642. }
  3643. /**
  3644. * Parse the startByte and endByte.
  3645. * @param {shaka.media.SegmentReference} previousReference
  3646. * @param {?string} byterange
  3647. * @return {!Array<number>} An array with the start byte and end byte.
  3648. * @private
  3649. */
  3650. parseByteRange_(previousReference, byterange) {
  3651. let startByte = 0;
  3652. let endByte = null;
  3653. // If BYTERANGE is not specified, the segment consists of the entire
  3654. // resource.
  3655. if (byterange) {
  3656. const blocks = byterange.split('@');
  3657. const byteLength = Number(blocks[0]);
  3658. if (blocks[1]) {
  3659. startByte = Number(blocks[1]);
  3660. } else {
  3661. goog.asserts.assert(previousReference,
  3662. 'Cannot refer back to previous HLS segment!');
  3663. startByte = previousReference.endByte + 1;
  3664. }
  3665. endByte = startByte + byteLength - 1;
  3666. }
  3667. return [startByte, endByte];
  3668. }
  3669. /**
  3670. * @param {!Array<!shaka.hls.Tag>} tags
  3671. * @param {string} contentType
  3672. * @param {!Map<string, string>} variables
  3673. * @param {function(): !Array<string>} getUris
  3674. * @private
  3675. */
  3676. processDateRangeTags_(tags, contentType, variables, getUris) {
  3677. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  3678. if (contentType != ContentType.VIDEO && contentType != ContentType.AUDIO) {
  3679. // DATE-RANGE should only appear in AUDIO or VIDEO playlists.
  3680. // We ignore those that appear in other playlists.
  3681. return;
  3682. }
  3683. const Utils = shaka.hls.Utils;
  3684. const initialProgramDateTime =
  3685. this.presentationTimeline_.getInitialProgramDateTime();
  3686. if (!initialProgramDateTime ||
  3687. this.ignoreManifestProgramDateTimeFor_(contentType)) {
  3688. return;
  3689. }
  3690. let dateRangeTags =
  3691. shaka.hls.Utils.filterTagsByName(tags, 'EXT-X-DATERANGE');
  3692. dateRangeTags = dateRangeTags.filter((tag) => {
  3693. return tag.getAttribute('START-DATE') != null;
  3694. }).sort((a, b) => {
  3695. const aStartDateValue = a.getRequiredAttrValue('START-DATE');
  3696. const bStartDateValue = b.getRequiredAttrValue('START-DATE');
  3697. if (aStartDateValue < bStartDateValue) {
  3698. return -1;
  3699. }
  3700. if (aStartDateValue > bStartDateValue) {
  3701. return 1;
  3702. }
  3703. return 0;
  3704. });
  3705. for (let i = 0; i < dateRangeTags.length; i++) {
  3706. const tag = dateRangeTags[i];
  3707. try {
  3708. const id = tag.getRequiredAttrValue('ID');
  3709. if (this.dateRangeIdsEmitted_.has(id)) {
  3710. continue;
  3711. }
  3712. const startDateValue = tag.getRequiredAttrValue('START-DATE');
  3713. const startDate = shaka.util.TXml.parseDate(startDateValue);
  3714. if (isNaN(startDate)) {
  3715. // Invalid START-DATE
  3716. continue;
  3717. }
  3718. goog.asserts.assert(startDate != null,
  3719. 'Start date should not be null!');
  3720. const startTime = Math.max(0, startDate - initialProgramDateTime);
  3721. let endTime = null;
  3722. const endDateValue = tag.getAttributeValue('END-DATE');
  3723. if (endDateValue) {
  3724. const endDate = shaka.util.TXml.parseDate(endDateValue);
  3725. if (!isNaN(endDate)) {
  3726. goog.asserts.assert(endDate != null,
  3727. 'End date should not be null!');
  3728. endTime = endDate - initialProgramDateTime;
  3729. if (endTime < 0) {
  3730. // Date range in the past
  3731. continue;
  3732. }
  3733. }
  3734. }
  3735. if (endTime == null) {
  3736. const durationValue = tag.getAttributeValue('DURATION') ||
  3737. tag.getAttributeValue('PLANNED-DURATION');
  3738. if (durationValue) {
  3739. const duration = parseFloat(durationValue);
  3740. if (!isNaN(duration)) {
  3741. endTime = startTime + duration;
  3742. }
  3743. const realEndTime = startDate - initialProgramDateTime + duration;
  3744. if (realEndTime < 0) {
  3745. // Date range in the past
  3746. continue;
  3747. }
  3748. }
  3749. }
  3750. const type =
  3751. tag.getAttributeValue('CLASS') || 'com.apple.quicktime.HLS';
  3752. const endOnNext = tag.getAttributeValue('END-ON-NEXT') == 'YES';
  3753. if (endTime == null && endOnNext) {
  3754. for (let j = i + 1; j < dateRangeTags.length; j++) {
  3755. const otherDateRangeType =
  3756. dateRangeTags[j].getAttributeValue('CLASS') ||
  3757. 'com.apple.quicktime.HLS';
  3758. if (type != otherDateRangeType) {
  3759. continue;
  3760. }
  3761. const otherDateRangeStartDateValue =
  3762. dateRangeTags[j].getRequiredAttrValue('START-DATE');
  3763. const otherDateRangeStartDate =
  3764. shaka.util.TXml.parseDate(otherDateRangeStartDateValue);
  3765. if (isNaN(otherDateRangeStartDate)) {
  3766. // Invalid START-DATE
  3767. continue;
  3768. }
  3769. if (otherDateRangeStartDate &&
  3770. otherDateRangeStartDate > startDate) {
  3771. endTime = Math.max(0,
  3772. otherDateRangeStartDate - initialProgramDateTime);
  3773. break;
  3774. }
  3775. }
  3776. if (endTime == null) {
  3777. // Since we cannot know when it ends, we omit it for now and in the
  3778. // future with an update we will be able to have more information.
  3779. continue;
  3780. }
  3781. }
  3782. // Exclude these attributes from the metadata since they already go into
  3783. // other fields (eg: startTime or endTime) or are not necessary..
  3784. const excludedAttributes = [
  3785. 'CLASS',
  3786. 'START-DATE',
  3787. 'END-DATE',
  3788. 'DURATION',
  3789. 'END-ON-NEXT',
  3790. ];
  3791. /* @type {!Array<shaka.extern.MetadataFrame>} */
  3792. const values = [];
  3793. for (const attribute of tag.attributes) {
  3794. if (excludedAttributes.includes(attribute.name)) {
  3795. continue;
  3796. }
  3797. let data = Utils.variableSubstitution(attribute.value, variables);
  3798. if (attribute.name == 'X-ASSET-URI' ||
  3799. attribute.name == 'X-ASSET-LIST') {
  3800. data = Utils.constructSegmentUris(
  3801. getUris(), attribute.value, variables)[0];
  3802. }
  3803. const metadataFrame = {
  3804. key: attribute.name,
  3805. description: '',
  3806. data,
  3807. mimeType: null,
  3808. pictureType: null,
  3809. };
  3810. values.push(metadataFrame);
  3811. }
  3812. // ID is always required. So we need more than 1 value.
  3813. if (values.length > 1) {
  3814. this.playerInterface_.onMetadata(type, startTime, endTime, values);
  3815. }
  3816. this.dateRangeIdsEmitted_.add(id);
  3817. } catch (e) {
  3818. shaka.log.warning('Ignoring DATERANGE with errors', tag.toString());
  3819. }
  3820. }
  3821. }
  3822. /**
  3823. * Parses shaka.hls.Segment objects into shaka.media.SegmentReferences and
  3824. * get the bandwidth necessary for this segments If it's defined in the
  3825. * playlist.
  3826. *
  3827. * @param {!shaka.hls.Playlist} playlist
  3828. * @param {!Map<number, number>} mediaSequenceToStartTime
  3829. * @param {!Map<string, string>} variables
  3830. * @param {function(): !Array<string>} getUris
  3831. * @param {string} type
  3832. * @return {{segments: !Array<!shaka.media.SegmentReference>,
  3833. * bandwidth: (number|undefined)}}
  3834. * @private
  3835. */
  3836. createSegments_(playlist, mediaSequenceToStartTime, variables,
  3837. getUris, type) {
  3838. /** @type {Array<!shaka.hls.Segment>} */
  3839. const hlsSegments = playlist.segments;
  3840. goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
  3841. /** @type {shaka.media.InitSegmentReference} */
  3842. let initSegmentRef;
  3843. /** @type {shaka.extern.aesKey|undefined} */
  3844. let aesKey = undefined;
  3845. let discontinuitySequence = shaka.hls.Utils.getFirstTagWithNameAsNumber(
  3846. playlist.tags, 'EXT-X-DISCONTINUITY-SEQUENCE', -1);
  3847. const mediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber(
  3848. playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0);
  3849. const skipTag = shaka.hls.Utils.getFirstTagWithName(
  3850. playlist.tags, 'EXT-X-SKIP');
  3851. const skippedSegments =
  3852. skipTag ? Number(skipTag.getAttributeValue('SKIPPED-SEGMENTS')) : 0;
  3853. let position = mediaSequenceNumber + skippedSegments;
  3854. let firstStartTime = 0;
  3855. // For live stream, use the cached value in the mediaSequenceToStartTime
  3856. // map if available.
  3857. if (this.isLive_() && mediaSequenceToStartTime.has(position)) {
  3858. firstStartTime = mediaSequenceToStartTime.get(position);
  3859. }
  3860. // This is for recovering from disconnects.
  3861. if (firstStartTime === 0 &&
  3862. this.presentationType_ == shaka.hls.HlsParser.PresentationType_.LIVE &&
  3863. mediaSequenceToStartTime.size > 0 &&
  3864. !mediaSequenceToStartTime.has(position) &&
  3865. this.presentationTimeline_.getPresentationStartTime() != null) {
  3866. firstStartTime = this.presentationTimeline_.getSegmentAvailabilityStart();
  3867. }
  3868. /** @type {!Array<!shaka.media.SegmentReference>} */
  3869. const references = [];
  3870. let previousReference = null;
  3871. /** @type {!Array<{bitrate: number, duration: number}>} */
  3872. const bitrates = [];
  3873. for (let i = 0; i < hlsSegments.length; i++) {
  3874. const item = hlsSegments[i];
  3875. const startTime =
  3876. (i == 0) ? firstStartTime : previousReference.endTime;
  3877. position = mediaSequenceNumber + skippedSegments + i;
  3878. const discontinuityTag = shaka.hls.Utils.getFirstTagWithName(
  3879. item.tags, 'EXT-X-DISCONTINUITY');
  3880. if (discontinuityTag) {
  3881. discontinuitySequence++;
  3882. if (previousReference && previousReference.initSegmentReference) {
  3883. previousReference.initSegmentReference.boundaryEnd = startTime;
  3884. }
  3885. }
  3886. // Apply new AES tags as you see them, keeping a running total.
  3887. for (const drmTag of item.tags) {
  3888. if (drmTag.name == 'EXT-X-KEY') {
  3889. if (this.isAesMethod_(drmTag.getRequiredAttrValue('METHOD'))) {
  3890. aesKey =
  3891. this.parseAESDrmTag_(drmTag, playlist, getUris, variables);
  3892. } else {
  3893. aesKey = undefined;
  3894. }
  3895. }
  3896. }
  3897. mediaSequenceToStartTime.set(position, startTime);
  3898. initSegmentRef = this.getInitSegmentReference_(playlist,
  3899. item.tags, getUris, variables);
  3900. const reference = this.createSegmentReference_(
  3901. initSegmentRef,
  3902. previousReference,
  3903. item,
  3904. startTime,
  3905. variables,
  3906. playlist,
  3907. type,
  3908. getUris,
  3909. aesKey);
  3910. if (reference) {
  3911. const bitrate = shaka.hls.Utils.getFirstTagWithNameAsNumber(
  3912. item.tags, 'EXT-X-BITRATE');
  3913. if (bitrate) {
  3914. bitrates.push({
  3915. bitrate,
  3916. duration: reference.endTime - reference.startTime,
  3917. });
  3918. } else if (bitrates.length) {
  3919. // It applies to every segment between it and the next EXT-X-BITRATE,
  3920. // so we use the latest bitrate value
  3921. const prevBitrate = bitrates.pop();
  3922. prevBitrate.duration += reference.endTime - reference.startTime;
  3923. bitrates.push(prevBitrate);
  3924. }
  3925. previousReference = reference;
  3926. reference.discontinuitySequence = discontinuitySequence;
  3927. if (this.ignoreManifestProgramDateTimeFor_(type) &&
  3928. this.minSequenceNumber_ != null &&
  3929. position < this.minSequenceNumber_) {
  3930. // This segment is ignored as part of our fallback synchronization
  3931. // method.
  3932. } else {
  3933. references.push(reference);
  3934. }
  3935. }
  3936. }
  3937. let bandwidth = undefined;
  3938. if (bitrates.length) {
  3939. const duration = bitrates.reduce((sum, value) => {
  3940. return sum + value.duration;
  3941. }, 0);
  3942. bandwidth = Math.round(bitrates.reduce((sum, value) => {
  3943. return sum + value.bitrate * value.duration;
  3944. }, 0) / duration * 1000);
  3945. }
  3946. // If some segments have sync times, but not all, extrapolate the sync
  3947. // times of the ones with none.
  3948. const someSyncTime = references.some((ref) => ref.syncTime != null);
  3949. if (someSyncTime) {
  3950. for (let i = 0; i < references.length; i++) {
  3951. const reference = references[i];
  3952. if (reference.syncTime != null) {
  3953. // No need to extrapolate.
  3954. continue;
  3955. }
  3956. // Find the nearest segment with syncTime, in either direction.
  3957. // This looks forward and backward simultaneously, keeping track of what
  3958. // to offset the syncTime it finds by as it goes.
  3959. let forwardAdd = 0;
  3960. let forwardI = i;
  3961. /**
  3962. * Look forwards one reference at a time, summing all durations as we
  3963. * go, until we find a reference with a syncTime to use as a basis.
  3964. * This DOES count the original reference, but DOESN'T count the first
  3965. * reference with a syncTime (as we approach it from behind).
  3966. * @return {?number}
  3967. */
  3968. const lookForward = () => {
  3969. const other = references[forwardI];
  3970. if (other) {
  3971. if (other.syncTime != null) {
  3972. return other.syncTime + forwardAdd;
  3973. }
  3974. forwardAdd -= other.endTime - other.startTime;
  3975. forwardI += 1;
  3976. }
  3977. return null;
  3978. };
  3979. let backwardAdd = 0;
  3980. let backwardI = i;
  3981. /**
  3982. * Look backwards one reference at a time, summing all durations as we
  3983. * go, until we find a reference with a syncTime to use as a basis.
  3984. * This DOESN'T count the original reference, but DOES count the first
  3985. * reference with a syncTime (as we approach it from ahead).
  3986. * @return {?number}
  3987. */
  3988. const lookBackward = () => {
  3989. const other = references[backwardI];
  3990. if (other) {
  3991. if (other != reference) {
  3992. backwardAdd += other.endTime - other.startTime;
  3993. }
  3994. if (other.syncTime != null) {
  3995. return other.syncTime + backwardAdd;
  3996. }
  3997. backwardI -= 1;
  3998. }
  3999. return null;
  4000. };
  4001. while (reference.syncTime == null) {
  4002. reference.syncTime = lookBackward();
  4003. if (reference.syncTime == null) {
  4004. reference.syncTime = lookForward();
  4005. }
  4006. }
  4007. }
  4008. }
  4009. // Split the sync times properly among partial segments.
  4010. if (someSyncTime) {
  4011. for (const reference of references) {
  4012. let syncTime = reference.syncTime;
  4013. for (const partial of reference.partialReferences) {
  4014. partial.syncTime = syncTime;
  4015. syncTime += partial.endTime - partial.startTime;
  4016. }
  4017. }
  4018. }
  4019. // lowestSyncTime is a value from a previous playlist update. Use it to
  4020. // set reference start times. If this is the first playlist parse, we will
  4021. // skip this step, and wait until we have sync time across stream types.
  4022. const lowestSyncTime = this.lowestSyncTime_;
  4023. if (someSyncTime && lowestSyncTime != Infinity) {
  4024. if (!this.ignoreManifestProgramDateTimeFor_(type)) {
  4025. for (const reference of references) {
  4026. reference.syncAgainst(lowestSyncTime);
  4027. }
  4028. }
  4029. }
  4030. return {
  4031. segments: references,
  4032. bandwidth,
  4033. };
  4034. }
  4035. /**
  4036. * Attempts to guess stream's mime type based on content type and URI.
  4037. *
  4038. * @param {string} contentType
  4039. * @param {string} codecs
  4040. * @return {?string}
  4041. * @private
  4042. */
  4043. guessMimeTypeBeforeLoading_(contentType, codecs) {
  4044. if (contentType == shaka.util.ManifestParserUtils.ContentType.TEXT) {
  4045. if (codecs == 'vtt' || codecs == 'wvtt') {
  4046. // If codecs is 'vtt', it's WebVTT.
  4047. return 'text/vtt';
  4048. } else if (codecs && codecs !== '') {
  4049. // Otherwise, assume MP4-embedded text, since text-based formats tend
  4050. // not to have a codecs string at all.
  4051. return 'application/mp4';
  4052. }
  4053. }
  4054. if (contentType == shaka.util.ManifestParserUtils.ContentType.IMAGE) {
  4055. if (!codecs || codecs == 'jpeg') {
  4056. return 'image/jpeg';
  4057. }
  4058. }
  4059. if (contentType == shaka.util.ManifestParserUtils.ContentType.AUDIO) {
  4060. // See: https://bugs.chromium.org/p/chromium/issues/detail?id=489520
  4061. if (codecs == 'mp4a.40.34') {
  4062. return 'audio/mpeg';
  4063. }
  4064. }
  4065. if (codecs == 'mjpg') {
  4066. return 'application/mp4';
  4067. }
  4068. // Not enough information to guess from the content type and codecs.
  4069. return null;
  4070. }
  4071. /**
  4072. * Get a fallback mime type for the content. Used if all the better methods
  4073. * for determining the mime type have failed.
  4074. *
  4075. * @param {string} contentType
  4076. * @return {string}
  4077. * @private
  4078. */
  4079. guessMimeTypeFallback_(contentType) {
  4080. if (contentType == shaka.util.ManifestParserUtils.ContentType.TEXT) {
  4081. // If there was no codecs string and no content-type, assume HLS text
  4082. // streams are WebVTT.
  4083. return 'text/vtt';
  4084. }
  4085. // If the HLS content is lacking in both MIME type metadata and
  4086. // segment file extensions, we fall back to assuming it's MP4.
  4087. const map =
  4088. shaka.hls.HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_.get(contentType);
  4089. return map.get('mp4');
  4090. }
  4091. /**
  4092. * @param {!Array<!shaka.media.SegmentReference>} segments
  4093. * @return {{segment: !shaka.media.SegmentReference, segmentIndex: number}}
  4094. * @private
  4095. */
  4096. getAvailableSegment_(segments) {
  4097. goog.asserts.assert(segments.length, 'Should have segments!');
  4098. // If you wait long enough, requesting the first segment can fail
  4099. // because it has fallen off the left edge of DVR, so to be safer,
  4100. // let's request the middle segment.
  4101. let segmentIndex = this.isLive_() ?
  4102. Math.trunc((segments.length - 1) / 2) : 0;
  4103. let segment = segments[segmentIndex];
  4104. while (segment.getStatus() == shaka.media.SegmentReference.Status.MISSING &&
  4105. (segmentIndex + 1) < segments.length) {
  4106. segmentIndex ++;
  4107. segment = segments[segmentIndex];
  4108. }
  4109. return {segment, segmentIndex};
  4110. }
  4111. /**
  4112. * Attempts to guess stream's mime type.
  4113. *
  4114. * @param {string} contentType
  4115. * @param {string} codecs
  4116. * @param {!Array<!shaka.media.SegmentReference>} segments
  4117. * @return {!Promise<string>}
  4118. * @private
  4119. */
  4120. async guessMimeType_(contentType, codecs, segments) {
  4121. const HlsParser = shaka.hls.HlsParser;
  4122. const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
  4123. const {segment} = this.getAvailableSegment_(segments);
  4124. if (segment.status == shaka.media.SegmentReference.Status.MISSING) {
  4125. return this.guessMimeTypeFallback_(contentType);
  4126. }
  4127. const segmentUris = segment.getUris();
  4128. const parsedUri = new goog.Uri(segmentUris[0]);
  4129. const extension = parsedUri.getPath().split('.').pop();
  4130. const map = HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_.get(contentType);
  4131. let mimeType = map.get(extension);
  4132. if (mimeType) {
  4133. return mimeType;
  4134. }
  4135. mimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_.get(extension);
  4136. if (mimeType) {
  4137. return mimeType;
  4138. }
  4139. // The extension map didn't work, so guess based on codecs.
  4140. mimeType = this.guessMimeTypeBeforeLoading_(contentType, codecs);
  4141. if (mimeType) {
  4142. return mimeType;
  4143. }
  4144. // If unable to guess mime type, request a segment and try getting it
  4145. // from the response.
  4146. let contentMimeType;
  4147. const type = shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
  4148. const headRequest = shaka.net.NetworkingEngine.makeRequest(
  4149. segmentUris, this.config_.retryParameters);
  4150. let response;
  4151. try {
  4152. headRequest.method = 'HEAD';
  4153. response = await this.makeNetworkRequest_(
  4154. headRequest, requestType, {type}).promise;
  4155. contentMimeType = response.headers['content-type'];
  4156. } catch (error) {
  4157. if (error &&
  4158. (error.code == shaka.util.Error.Code.HTTP_ERROR ||
  4159. error.code == shaka.util.Error.Code.BAD_HTTP_STATUS)) {
  4160. headRequest.method = 'GET';
  4161. if (this.config_.hls.allowRangeRequestsToGuessMimeType) {
  4162. // Only requesting first byte
  4163. headRequest.headers['Range'] = 'bytes=0-0';
  4164. }
  4165. response = await this.makeNetworkRequest_(
  4166. headRequest, requestType, {type}).promise;
  4167. contentMimeType = response.headers['content-type'];
  4168. }
  4169. }
  4170. if (contentMimeType) {
  4171. // Split the MIME type in case the server sent additional parameters.
  4172. mimeType = contentMimeType.toLowerCase().split(';')[0];
  4173. if (mimeType == 'application/octet-stream') {
  4174. if (!response.data.byteLength) {
  4175. headRequest.method = 'GET';
  4176. response = await this.makeNetworkRequest_(
  4177. headRequest, requestType, {type}).promise;
  4178. }
  4179. if (shaka.util.TsParser.probe(
  4180. shaka.util.BufferUtils.toUint8(response.data))) {
  4181. mimeType = 'video/mp2t';
  4182. }
  4183. }
  4184. if (mimeType != 'application/octet-stream') {
  4185. return mimeType;
  4186. }
  4187. }
  4188. return this.guessMimeTypeFallback_(contentType);
  4189. }
  4190. /**
  4191. * Returns a tag with a given name.
  4192. * Throws an error if tag was not found.
  4193. *
  4194. * @param {!Array<shaka.hls.Tag>} tags
  4195. * @param {string} tagName
  4196. * @return {!shaka.hls.Tag}
  4197. * @private
  4198. */
  4199. getRequiredTag_(tags, tagName) {
  4200. const tag = shaka.hls.Utils.getFirstTagWithName(tags, tagName);
  4201. if (!tag) {
  4202. throw new shaka.util.Error(
  4203. shaka.util.Error.Severity.CRITICAL,
  4204. shaka.util.Error.Category.MANIFEST,
  4205. shaka.util.Error.Code.HLS_REQUIRED_TAG_MISSING, tagName);
  4206. }
  4207. return tag;
  4208. }
  4209. /**
  4210. * @param {shaka.extern.Stream} stream
  4211. * @param {?string} width
  4212. * @param {?string} height
  4213. * @param {?string} frameRate
  4214. * @param {?string} videoRange
  4215. * @param {?string} videoLayout
  4216. * @param {?string} colorGamut
  4217. * @private
  4218. */
  4219. addVideoAttributes_(stream, width, height, frameRate, videoRange,
  4220. videoLayout, colorGamut) {
  4221. if (stream) {
  4222. stream.width = Number(width) || undefined;
  4223. stream.height = Number(height) || undefined;
  4224. stream.frameRate = Number(frameRate) || undefined;
  4225. stream.hdr = videoRange || undefined;
  4226. stream.videoLayout = videoLayout || undefined;
  4227. stream.colorGamut = colorGamut || undefined;
  4228. }
  4229. }
  4230. /**
  4231. * Makes a network request for the manifest and returns a Promise
  4232. * with the resulting data.
  4233. *
  4234. * @param {!Array<string>} uris
  4235. * @param {boolean=} isPlaylist
  4236. * @return {!shaka.net.NetworkingEngine.PendingRequest}
  4237. * @private
  4238. */
  4239. requestManifest_(uris, isPlaylist) {
  4240. const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
  4241. const request = shaka.net.NetworkingEngine.makeRequest(
  4242. uris, this.config_.retryParameters);
  4243. const type = isPlaylist ?
  4244. shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_PLAYLIST :
  4245. shaka.net.NetworkingEngine.AdvancedRequestType.MASTER_PLAYLIST;
  4246. return this.makeNetworkRequest_(request, requestType, {type});
  4247. }
  4248. /**
  4249. * Called when the update timer ticks. Because parsing a manifest is async,
  4250. * this method is async. To work with this, this method will schedule the next
  4251. * update when it finished instead of using a repeating-start.
  4252. *
  4253. * @return {!Promise}
  4254. * @private
  4255. */
  4256. async onUpdate_() {
  4257. shaka.log.info('Updating manifest...');
  4258. goog.asserts.assert(
  4259. this.getUpdatePlaylistDelay_() > 0,
  4260. 'We should only call |onUpdate_| when we are suppose to be updating.');
  4261. // Detect a call to stop()
  4262. if (!this.playerInterface_) {
  4263. return;
  4264. }
  4265. try {
  4266. const startTime = Date.now();
  4267. await this.update();
  4268. // Keep track of how long the longest manifest update took.
  4269. const endTime = Date.now();
  4270. // This may have converted to VOD, in which case we stop updating.
  4271. if (this.isLive_()) {
  4272. const updateDuration = (endTime - startTime) / 1000.0;
  4273. this.averageUpdateDuration_.sample(1, updateDuration);
  4274. const delay = this.config_.updatePeriod > 0 ?
  4275. this.config_.updatePeriod : this.getUpdatePlaylistDelay_();
  4276. const finalDelay = Math.max(0,
  4277. delay - this.averageUpdateDuration_.getEstimate());
  4278. this.updatePlaylistTimer_.tickAfter(/* seconds= */ finalDelay);
  4279. }
  4280. } catch (error) {
  4281. // Detect a call to stop() during this.update()
  4282. if (!this.playerInterface_) {
  4283. return;
  4284. }
  4285. goog.asserts.assert(error instanceof shaka.util.Error,
  4286. 'Should only receive a Shaka error');
  4287. if (this.config_.raiseFatalErrorOnManifestUpdateRequestFailure) {
  4288. this.playerInterface_.onError(error);
  4289. return;
  4290. }
  4291. // We will retry updating, so override the severity of the error.
  4292. error.severity = shaka.util.Error.Severity.RECOVERABLE;
  4293. this.playerInterface_.onError(error);
  4294. // Try again very soon.
  4295. this.updatePlaylistTimer_.tickAfter(/* seconds= */ 0.1);
  4296. }
  4297. // Detect a call to stop()
  4298. if (!this.playerInterface_) {
  4299. return;
  4300. }
  4301. this.playerInterface_.onManifestUpdated();
  4302. }
  4303. /**
  4304. * @return {boolean}
  4305. * @private
  4306. */
  4307. isLive_() {
  4308. const PresentationType = shaka.hls.HlsParser.PresentationType_;
  4309. return this.presentationType_ != PresentationType.VOD;
  4310. }
  4311. /**
  4312. * @return {number}
  4313. * @private
  4314. */
  4315. getUpdatePlaylistDelay_() {
  4316. // The HLS spec (RFC 8216) states in 6.3.4:
  4317. // "the client MUST wait for at least the target duration before
  4318. // attempting to reload the Playlist file again".
  4319. // For LL-HLS, the server must add a new partial segment to the Playlist
  4320. // every part target duration.
  4321. return this.lastTargetDuration_;
  4322. }
  4323. /**
  4324. * @param {shaka.hls.HlsParser.PresentationType_} type
  4325. * @private
  4326. */
  4327. setPresentationType_(type) {
  4328. this.presentationType_ = type;
  4329. if (this.presentationTimeline_) {
  4330. this.presentationTimeline_.setStatic(!this.isLive_());
  4331. }
  4332. // If this manifest is not for live content, then we have no reason to
  4333. // update it.
  4334. if (!this.isLive_()) {
  4335. this.updatePlaylistTimer_.stop();
  4336. }
  4337. }
  4338. /**
  4339. * Create a networking request. This will manage the request using the
  4340. * parser's operation manager. If the parser has already been stopped, the
  4341. * request will not be made.
  4342. *
  4343. * @param {shaka.extern.Request} request
  4344. * @param {shaka.net.NetworkingEngine.RequestType} type
  4345. * @param {shaka.extern.RequestContext=} context
  4346. * @return {!shaka.net.NetworkingEngine.PendingRequest}
  4347. * @private
  4348. */
  4349. makeNetworkRequest_(request, type, context) {
  4350. if (!this.operationManager_) {
  4351. throw new shaka.util.Error(
  4352. shaka.util.Error.Severity.CRITICAL,
  4353. shaka.util.Error.Category.PLAYER,
  4354. shaka.util.Error.Code.OPERATION_ABORTED);
  4355. }
  4356. if (!context) {
  4357. context = {};
  4358. }
  4359. context.isPreload = this.isPreloadFn_();
  4360. const op = this.playerInterface_.networkingEngine.request(
  4361. type, request, context);
  4362. this.operationManager_.manage(op);
  4363. return op;
  4364. }
  4365. /**
  4366. * @param {string} method
  4367. * @return {boolean}
  4368. * @private
  4369. */
  4370. isAesMethod_(method) {
  4371. return method == 'AES-128' ||
  4372. method == 'AES-256' ||
  4373. method == 'AES-256-CTR';
  4374. }
  4375. /**
  4376. * @param {!shaka.hls.Tag} drmTag
  4377. * @param {string} mimeType
  4378. * @param {?shaka.media.InitSegmentReference} initSegmentRef
  4379. * @return {!Promise<?shaka.extern.DrmInfo>}
  4380. * @private
  4381. */
  4382. async fairplayDrmParser_(drmTag, mimeType, initSegmentRef) {
  4383. if (mimeType == 'video/mp2t') {
  4384. throw new shaka.util.Error(
  4385. shaka.util.Error.Severity.CRITICAL,
  4386. shaka.util.Error.Category.MANIFEST,
  4387. shaka.util.Error.Code.HLS_MSE_ENCRYPTED_MP2T_NOT_SUPPORTED);
  4388. }
  4389. if (shaka.drm.DrmUtils.isMediaKeysPolyfilled('apple')) {
  4390. throw new shaka.util.Error(
  4391. shaka.util.Error.Severity.CRITICAL,
  4392. shaka.util.Error.Category.MANIFEST,
  4393. shaka.util.Error.Code
  4394. .HLS_MSE_ENCRYPTED_LEGACY_APPLE_MEDIA_KEYS_NOT_SUPPORTED);
  4395. }
  4396. const method = drmTag.getRequiredAttrValue('METHOD');
  4397. const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR'];
  4398. if (!VALID_METHODS.includes(method)) {
  4399. shaka.log.error('FairPlay in HLS is only supported with [',
  4400. VALID_METHODS.join(', '), '], not', method);
  4401. return null;
  4402. }
  4403. let encryptionScheme = 'cenc';
  4404. if (method == 'SAMPLE-AES') {
  4405. // It should be 'cbcs-1-9' but Safari doesn't support it.
  4406. // See: https://github.com/WebKit/WebKit/blob/main/Source/WebCore/Modules/encryptedmedia/MediaKeyEncryptionScheme.idl
  4407. encryptionScheme = 'cbcs';
  4408. }
  4409. const uri = drmTag.getRequiredAttrValue('URI');
  4410. /*
  4411. * Even if we're not able to construct initData through the HLS tag, adding
  4412. * a DRMInfo will allow DRM Engine to request a media key system access
  4413. * with the correct keySystem and initDataType
  4414. */
  4415. const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
  4416. 'com.apple.fps', encryptionScheme, [
  4417. {initDataType: 'sinf', initData: new Uint8Array(0), keyId: null},
  4418. ], uri);
  4419. let keyId = shaka.drm.FairPlay.defaultGetKeyId(uri);
  4420. if (!keyId && initSegmentRef) {
  4421. keyId = await this.getKeyIdFromInitSegment_(initSegmentRef);
  4422. }
  4423. if (keyId) {
  4424. drmInfo.keyIds.add(keyId);
  4425. }
  4426. return drmInfo;
  4427. }
  4428. /**
  4429. * @param {!shaka.hls.Tag} drmTag
  4430. * @param {string} mimeType
  4431. * @param {?shaka.media.InitSegmentReference} initSegmentRef
  4432. * @return {!Promise<?shaka.extern.DrmInfo>}
  4433. * @private
  4434. */
  4435. widevineDrmParser_(drmTag, mimeType, initSegmentRef) {
  4436. const method = drmTag.getRequiredAttrValue('METHOD');
  4437. const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR'];
  4438. if (!VALID_METHODS.includes(method)) {
  4439. shaka.log.error('Widevine in HLS is only supported with [',
  4440. VALID_METHODS.join(', '), '], not', method);
  4441. return Promise.resolve(null);
  4442. }
  4443. let encryptionScheme = 'cenc';
  4444. if (method == 'SAMPLE-AES') {
  4445. encryptionScheme = 'cbcs';
  4446. }
  4447. const uri = drmTag.getRequiredAttrValue('URI');
  4448. const parsedData = shaka.net.DataUriPlugin.parseRaw(uri.split('?')[0]);
  4449. // The data encoded in the URI is a PSSH box to be used as init data.
  4450. const pssh = shaka.util.BufferUtils.toUint8(parsedData.data);
  4451. const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
  4452. 'com.widevine.alpha', encryptionScheme, [
  4453. {initDataType: 'cenc', initData: pssh},
  4454. ]);
  4455. const keyId = drmTag.getAttributeValue('KEYID');
  4456. if (keyId) {
  4457. const keyIdLowerCase = keyId.toLowerCase();
  4458. // This value should begin with '0x':
  4459. goog.asserts.assert(
  4460. keyIdLowerCase.startsWith('0x'), 'Incorrect KEYID format!');
  4461. // But the output should not contain the '0x':
  4462. drmInfo.keyIds.add(keyIdLowerCase.substr(2));
  4463. }
  4464. return Promise.resolve(drmInfo);
  4465. }
  4466. /**
  4467. * See: https://docs.microsoft.com/en-us/playready/packaging/mp4-based-formats-supported-by-playready-clients?tabs=case4
  4468. *
  4469. * @param {!shaka.hls.Tag} drmTag
  4470. * @param {string} mimeType
  4471. * @param {?shaka.media.InitSegmentReference} initSegmentRef
  4472. * @return {!Promise<?shaka.extern.DrmInfo>}
  4473. * @private
  4474. */
  4475. playreadyDrmParser_(drmTag, mimeType, initSegmentRef) {
  4476. const method = drmTag.getRequiredAttrValue('METHOD');
  4477. const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR'];
  4478. if (!VALID_METHODS.includes(method)) {
  4479. shaka.log.error('PlayReady in HLS is only supported with [',
  4480. VALID_METHODS.join(', '), '], not', method);
  4481. return Promise.resolve(null);
  4482. }
  4483. let encryptionScheme = 'cenc';
  4484. if (method == 'SAMPLE-AES') {
  4485. encryptionScheme = 'cbcs';
  4486. }
  4487. const uri = drmTag.getRequiredAttrValue('URI');
  4488. const parsedData = shaka.net.DataUriPlugin.parseRaw(uri.split('?')[0]);
  4489. // The data encoded in the URI is a PlayReady Pro Object, so we need
  4490. // convert it to pssh.
  4491. const data = shaka.util.BufferUtils.toUint8(parsedData.data);
  4492. const systemId = new Uint8Array([
  4493. 0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86,
  4494. 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95,
  4495. ]);
  4496. const keyIds = new Set();
  4497. const psshVersion = 0;
  4498. const pssh =
  4499. shaka.util.Pssh.createPssh(data, systemId, keyIds, psshVersion);
  4500. const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
  4501. 'com.microsoft.playready', encryptionScheme, [
  4502. {initDataType: 'cenc', initData: pssh},
  4503. ]);
  4504. const input = shaka.util.TXml.parseXmlString([
  4505. '<PLAYREADY>',
  4506. shaka.util.Uint8ArrayUtils.toBase64(data),
  4507. '</PLAYREADY>',
  4508. ].join('\n'));
  4509. if (input) {
  4510. drmInfo.licenseServerUri = shaka.drm.PlayReady.getLicenseUrl(input);
  4511. }
  4512. return Promise.resolve(drmInfo);
  4513. }
  4514. /**
  4515. * @param {!shaka.hls.Tag} drmTag
  4516. * @param {string} mimeType
  4517. * @param {?shaka.media.InitSegmentReference} initSegmentRef
  4518. * @return {!Promise<?shaka.extern.DrmInfo>}
  4519. * @private
  4520. */
  4521. wiseplayDrmParser_(drmTag, mimeType, initSegmentRef) {
  4522. const method = drmTag.getRequiredAttrValue('METHOD');
  4523. const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR'];
  4524. if (!VALID_METHODS.includes(method)) {
  4525. shaka.log.error('WisePlay in HLS is only supported with [',
  4526. VALID_METHODS.join(', '), '], not', method);
  4527. return Promise.resolve(null);
  4528. }
  4529. let encryptionScheme = 'cenc';
  4530. if (method == 'SAMPLE-AES') {
  4531. encryptionScheme = 'cbcs';
  4532. }
  4533. const uri = drmTag.getRequiredAttrValue('URI');
  4534. const parsedData = shaka.net.DataUriPlugin.parseRaw(uri.split('?')[0]);
  4535. // The data encoded in the URI is a PSSH box to be used as init data.
  4536. const pssh = shaka.util.BufferUtils.toUint8(parsedData.data);
  4537. const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
  4538. 'com.huawei.wiseplay', encryptionScheme, [
  4539. {initDataType: 'cenc', initData: pssh},
  4540. ]);
  4541. const keyId = drmTag.getAttributeValue('KEYID');
  4542. if (keyId) {
  4543. const keyIdLowerCase = keyId.toLowerCase();
  4544. // This value should begin with '0x':
  4545. goog.asserts.assert(
  4546. keyIdLowerCase.startsWith('0x'), 'Incorrect KEYID format!');
  4547. // But the output should not contain the '0x':
  4548. drmInfo.keyIds.add(keyIdLowerCase.substr(2));
  4549. }
  4550. return Promise.resolve(drmInfo);
  4551. }
  4552. /**
  4553. * See: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-5.1
  4554. *
  4555. * @param {!shaka.hls.Tag} drmTag
  4556. * @param {string} mimeType
  4557. * @param {function(): !Array<string>} getUris
  4558. * @param {?shaka.media.InitSegmentReference} initSegmentRef
  4559. * @param {?Map<string, string>=} variables
  4560. * @return {!Promise<?shaka.extern.DrmInfo>}
  4561. * @private
  4562. */
  4563. async identityDrmParser_(drmTag, mimeType, getUris, initSegmentRef,
  4564. variables) {
  4565. if (mimeType == 'video/mp2t') {
  4566. throw new shaka.util.Error(
  4567. shaka.util.Error.Severity.CRITICAL,
  4568. shaka.util.Error.Category.MANIFEST,
  4569. shaka.util.Error.Code.HLS_MSE_ENCRYPTED_MP2T_NOT_SUPPORTED);
  4570. }
  4571. if (shaka.drm.DrmUtils.isMediaKeysPolyfilled('apple')) {
  4572. throw new shaka.util.Error(
  4573. shaka.util.Error.Severity.CRITICAL,
  4574. shaka.util.Error.Category.MANIFEST,
  4575. shaka.util.Error.Code
  4576. .HLS_MSE_ENCRYPTED_LEGACY_APPLE_MEDIA_KEYS_NOT_SUPPORTED);
  4577. }
  4578. const method = drmTag.getRequiredAttrValue('METHOD');
  4579. const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR'];
  4580. if (!VALID_METHODS.includes(method)) {
  4581. shaka.log.error('Identity (ClearKey) in HLS is only supported with [',
  4582. VALID_METHODS.join(', '), '], not', method);
  4583. return null;
  4584. }
  4585. const keyUris = shaka.hls.Utils.constructSegmentUris(
  4586. getUris(), drmTag.getRequiredAttrValue('URI'), variables);
  4587. let key;
  4588. if (keyUris[0].startsWith('data:text/plain;base64,')) {
  4589. key = shaka.util.Uint8ArrayUtils.toHex(
  4590. shaka.util.Uint8ArrayUtils.fromBase64(
  4591. keyUris[0].split('data:text/plain;base64,').pop()));
  4592. } else {
  4593. const keyMapKey = keyUris.sort().join('');
  4594. if (!this.identityKeyMap_.has(keyMapKey)) {
  4595. const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
  4596. const request = shaka.net.NetworkingEngine.makeRequest(
  4597. keyUris, this.config_.retryParameters);
  4598. const keyResponse = this.makeNetworkRequest_(request, requestType)
  4599. .promise;
  4600. this.identityKeyMap_.set(keyMapKey, keyResponse);
  4601. }
  4602. const keyResponse = await this.identityKeyMap_.get(keyMapKey);
  4603. key = shaka.util.Uint8ArrayUtils.toHex(keyResponse.data);
  4604. }
  4605. // NOTE: The ClearKey CDM requires a key-id to key mapping. HLS doesn't
  4606. // provide a key ID anywhere. So although we could use the 'URI' attribute
  4607. // to fetch the actual 16-byte key, without a key ID, we can't provide this
  4608. // automatically to the ClearKey CDM. By default we assume that keyId is 0,
  4609. // but we will try to get key ID from Init Segment.
  4610. // If the application want override this behavior will have to use
  4611. // player.configure('drm.clearKeys', { ... }) to provide the key IDs
  4612. // and keys or player.configure('drm.servers.org\.w3\.clearkey', ...) to
  4613. // provide a ClearKey license server URI.
  4614. let keyId = '00000000000000000000000000000000';
  4615. if (initSegmentRef) {
  4616. const defaultKID = await this.getKeyIdFromInitSegment_(initSegmentRef);
  4617. if (defaultKID) {
  4618. keyId = defaultKID;
  4619. }
  4620. }
  4621. const clearkeys = new Map();
  4622. clearkeys.set(keyId, key);
  4623. let encryptionScheme = 'cenc';
  4624. if (method == 'SAMPLE-AES') {
  4625. encryptionScheme = 'cbcs';
  4626. }
  4627. return shaka.util.ManifestParserUtils.createDrmInfoFromClearKeys(
  4628. clearkeys, encryptionScheme);
  4629. }
  4630. /**
  4631. * @param {!shaka.media.InitSegmentReference} initSegmentRef
  4632. * @return {!Promise<?string>}
  4633. * @private
  4634. */
  4635. async getKeyIdFromInitSegment_(initSegmentRef) {
  4636. let keyId = null;
  4637. if (this.initSegmentToKidMap_.has(initSegmentRef)) {
  4638. keyId = this.initSegmentToKidMap_.get(initSegmentRef);
  4639. } else {
  4640. const initSegmentRequest = shaka.util.Networking.createSegmentRequest(
  4641. initSegmentRef.getUris(),
  4642. initSegmentRef.getStartByte(),
  4643. initSegmentRef.getEndByte(),
  4644. this.config_.retryParameters);
  4645. const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
  4646. const initType =
  4647. shaka.net.NetworkingEngine.AdvancedRequestType.INIT_SEGMENT;
  4648. const initResponse = await this.makeNetworkRequest_(
  4649. initSegmentRequest, requestType, {type: initType}).promise;
  4650. initSegmentRef.setSegmentData(initResponse.data);
  4651. keyId = shaka.media.SegmentUtils.getDefaultKID(
  4652. initResponse.data);
  4653. this.initSegmentToKidMap_.set(initSegmentRef, keyId);
  4654. }
  4655. return keyId;
  4656. }
  4657. };
  4658. /**
  4659. * @typedef {{
  4660. * stream: !shaka.extern.Stream,
  4661. * type: string,
  4662. * redirectUris: !Array<string>,
  4663. * getUris: function():!Array<string>,
  4664. * minTimestamp: number,
  4665. * maxTimestamp: number,
  4666. * mediaSequenceToStartTime: !Map<number, number>,
  4667. * canSkipSegments: boolean,
  4668. * canBlockReload: boolean,
  4669. * hasEndList: boolean,
  4670. * firstSequenceNumber: number,
  4671. * nextMediaSequence: number,
  4672. * nextPart: number,
  4673. * loadedOnce: boolean
  4674. * }}
  4675. *
  4676. * @description
  4677. * Contains a stream and information about it.
  4678. *
  4679. * @property {!shaka.extern.Stream} stream
  4680. * The Stream itself.
  4681. * @property {string} type
  4682. * The type value. Could be 'video', 'audio', 'text', or 'image'.
  4683. * @property {!Array<string>} redirectUris
  4684. * The redirect URIs.
  4685. * @property {function():!Array<string>} getUris
  4686. * The verbatim media playlist URIs, as it appeared in the master playlist.
  4687. * @property {number} minTimestamp
  4688. * The minimum timestamp found in the stream.
  4689. * @property {number} maxTimestamp
  4690. * The maximum timestamp found in the stream.
  4691. * @property {!Map<number, number>} mediaSequenceToStartTime
  4692. * A map of media sequence numbers to media start times.
  4693. * Only used for VOD content.
  4694. * @property {boolean} canSkipSegments
  4695. * True if the server supports delta playlist updates, and we can send a
  4696. * request for a playlist that can skip older media segments.
  4697. * @property {boolean} canBlockReload
  4698. * True if the server supports blocking playlist reload, and we can send a
  4699. * request for a playlist that can block reload until some segments are
  4700. * present.
  4701. * @property {boolean} hasEndList
  4702. * True if the stream has an EXT-X-ENDLIST tag.
  4703. * @property {number} firstSequenceNumber
  4704. * The sequence number of the first reference. Only calculated if needed.
  4705. * @property {number} nextMediaSequence
  4706. * The next media sequence.
  4707. * @property {number} nextPart
  4708. * The next part.
  4709. * @property {boolean} loadedOnce
  4710. * True if the stream has been loaded at least once.
  4711. */
  4712. shaka.hls.HlsParser.StreamInfo;
  4713. /**
  4714. * @typedef {{
  4715. * audio: !Array<shaka.hls.HlsParser.StreamInfo>,
  4716. * video: !Array<shaka.hls.HlsParser.StreamInfo>
  4717. * }}
  4718. *
  4719. * @description Audio and video stream infos.
  4720. * @property {!Array<shaka.hls.HlsParser.StreamInfo>} audio
  4721. * @property {!Array<shaka.hls.HlsParser.StreamInfo>} video
  4722. */
  4723. shaka.hls.HlsParser.StreamInfos;
  4724. /**
  4725. * @const {!Map<string, string>}
  4726. * @private
  4727. */
  4728. shaka.hls.HlsParser.RAW_FORMATS_TO_MIME_TYPES_ = new Map()
  4729. .set('aac', 'audio/aac')
  4730. .set('ac3', 'audio/ac3')
  4731. .set('ec3', 'audio/ec3')
  4732. .set('mp3', 'audio/mpeg');
  4733. /**
  4734. * @const {!Map<string, string>}
  4735. * @private
  4736. */
  4737. shaka.hls.HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_ = new Map()
  4738. .set('mp4', 'audio/mp4')
  4739. .set('mp4a', 'audio/mp4')
  4740. .set('m4s', 'audio/mp4')
  4741. .set('m4i', 'audio/mp4')
  4742. .set('m4a', 'audio/mp4')
  4743. .set('m4f', 'audio/mp4')
  4744. .set('cmfa', 'audio/mp4')
  4745. // MPEG2-TS also uses video/ for audio: https://bit.ly/TsMse
  4746. .set('ts', 'video/mp2t')
  4747. .set('tsa', 'video/mp2t');
  4748. /**
  4749. * @const {!Map<string, string>}
  4750. * @private
  4751. */
  4752. shaka.hls.HlsParser.VIDEO_EXTENSIONS_TO_MIME_TYPES_ = new Map()
  4753. .set('mp4', 'video/mp4')
  4754. .set('mp4v', 'video/mp4')
  4755. .set('m4s', 'video/mp4')
  4756. .set('m4i', 'video/mp4')
  4757. .set('m4v', 'video/mp4')
  4758. .set('m4f', 'video/mp4')
  4759. .set('cmfv', 'video/mp4')
  4760. .set('ts', 'video/mp2t')
  4761. .set('tsv', 'video/mp2t');
  4762. /**
  4763. * @const {!Map<string, string>}
  4764. * @private
  4765. */
  4766. shaka.hls.HlsParser.TEXT_EXTENSIONS_TO_MIME_TYPES_ = new Map()
  4767. .set('mp4', 'application/mp4')
  4768. .set('m4s', 'application/mp4')
  4769. .set('m4i', 'application/mp4')
  4770. .set('m4f', 'application/mp4')
  4771. .set('cmft', 'application/mp4')
  4772. .set('vtt', 'text/vtt')
  4773. .set('webvtt', 'text/vtt')
  4774. .set('ttml', 'application/ttml+xml');
  4775. /**
  4776. * @const {!Map<string, string>}
  4777. * @private
  4778. */
  4779. shaka.hls.HlsParser.IMAGE_EXTENSIONS_TO_MIME_TYPES_ = new Map()
  4780. .set('jpg', 'image/jpeg')
  4781. .set('png', 'image/png')
  4782. .set('svg', 'image/svg+xml')
  4783. .set('webp', 'image/webp')
  4784. .set('avif', 'image/avif');
  4785. /**
  4786. * @const {!Map<string, !Map<string, string>>}
  4787. * @private
  4788. */
  4789. shaka.hls.HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_ = new Map()
  4790. .set('audio', shaka.hls.HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_)
  4791. .set('video', shaka.hls.HlsParser.VIDEO_EXTENSIONS_TO_MIME_TYPES_)
  4792. .set('text', shaka.hls.HlsParser.TEXT_EXTENSIONS_TO_MIME_TYPES_)
  4793. .set('image', shaka.hls.HlsParser.IMAGE_EXTENSIONS_TO_MIME_TYPES_);
  4794. /**
  4795. * MIME types without init segment.
  4796. *
  4797. * @const {!Set<string>}
  4798. * @private
  4799. */
  4800. shaka.hls.HlsParser.MIME_TYPES_WITHOUT_INIT_SEGMENT_ = new Set([
  4801. 'video/mp2t',
  4802. // Containerless types
  4803. ...shaka.util.MimeUtils.RAW_FORMATS,
  4804. ]);
  4805. /**
  4806. * @typedef {function(!shaka.hls.Tag,
  4807. * string,
  4808. * ?shaka.media.InitSegmentReference):
  4809. * !Promise<?shaka.extern.DrmInfo>}
  4810. * @private
  4811. */
  4812. shaka.hls.HlsParser.DrmParser_;
  4813. /**
  4814. * @enum {string}
  4815. * @private
  4816. */
  4817. shaka.hls.HlsParser.PresentationType_ = {
  4818. VOD: 'VOD',
  4819. EVENT: 'EVENT',
  4820. LIVE: 'LIVE',
  4821. };
  4822. /**
  4823. * @const {string}
  4824. * @private
  4825. */
  4826. shaka.hls.HlsParser.FAKE_MUXED_URL_ = 'shaka://hls-muxed';
  4827. shaka.media.ManifestParser.registerParserByMime(
  4828. 'application/x-mpegurl', () => new shaka.hls.HlsParser());
  4829. shaka.media.ManifestParser.registerParserByMime(
  4830. 'application/vnd.apple.mpegurl', () => new shaka.hls.HlsParser());