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