Source: lib/hls/hls_parser.js

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