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