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.asserts');
  19. goog.require('shaka.hls.ManifestTextParser');
  20. goog.require('shaka.hls.Playlist');
  21. goog.require('shaka.hls.PlaylistType');
  22. goog.require('shaka.hls.Tag');
  23. goog.require('shaka.hls.Utils');
  24. goog.require('shaka.log');
  25. goog.require('shaka.media.DrmEngine');
  26. goog.require('shaka.media.InitSegmentReference');
  27. goog.require('shaka.media.ManifestParser');
  28. goog.require('shaka.media.PresentationTimeline');
  29. goog.require('shaka.media.SegmentIndex');
  30. goog.require('shaka.media.SegmentReference');
  31. goog.require('shaka.net.DataUriPlugin');
  32. goog.require('shaka.net.NetworkingEngine');
  33. goog.require('shaka.util.Error');
  34. goog.require('shaka.util.Functional');
  35. goog.require('shaka.util.ManifestParserUtils');
  36. /**
  37. * Creates a new HLS parser.
  38. *
  39. * @struct
  40. * @constructor
  41. * @implements {shakaExtern.ManifestParser}
  42. * @export
  43. */
  44. shaka.hls.HlsParser = function() {
  45. /** @private {?shakaExtern.ManifestParser.PlayerInterface} */
  46. this.playerInterface_ = null;
  47. /** @private {?shakaExtern.ManifestConfiguration} */
  48. this.config_ = null;
  49. /** @private {number} */
  50. this.globalId_ = 1;
  51. /** @private {!Object.<number, shaka.hls.HlsParser.StreamInfo>} */
  52. this.mediaTagsToStreamInfosMap_ = {};
  53. /** @private {!Object.<number, !shaka.media.SegmentIndex>} */
  54. this.streamsToIndexMap_ = {};
  55. /**
  56. * A map from media playlists' uris to stream infos
  57. * representing the playlists.
  58. * @private {!Object.<string, shaka.hls.HlsParser.StreamInfo>}
  59. */
  60. this.uriToStreamInfosMap_ = {};
  61. /** @private {?shaka.media.PresentationTimeline} */
  62. this.presentationTimeline_ = null;
  63. /** @private {string} */
  64. this.manifestUri_ = '';
  65. /** @private {shaka.hls.ManifestTextParser} */
  66. this.manifestTextParser_ = new shaka.hls.ManifestTextParser();
  67. /**
  68. * The update period in seconds; or null for no updates.
  69. * @private {?number}
  70. */
  71. this.updatePeriod_ = null;
  72. /** @private {?number} */
  73. this.updateTimer_ = null;
  74. /** @private {boolean} */
  75. this.isLive_ = false;
  76. /** @private {?shakaExtern.Manifest} */
  77. this.manifest_ = null;
  78. /** @private {number} */
  79. this.maxTargetDuration_ = 0;
  80. };
  81. /**
  82. * @typedef {{
  83. * stream: !shakaExtern.Stream,
  84. * segmentIndex: !shaka.media.SegmentIndex,
  85. * drmInfos: !Array.<shakaExtern.DrmInfo>,
  86. * relativeUri: !string,
  87. * lastSegmentSeen: !shaka.media.SegmentReference
  88. * }}
  89. *
  90. * @description
  91. * Contains a stream and information about it.
  92. *
  93. * @property {!shakaExtern.Stream} stream
  94. * The Stream itself.
  95. * @property {!shaka.media.SegmentIndex} segmentIndex
  96. * SegmentIndex of the stream.
  97. * @property {!Array.<shakaExtern.DrmInfo>} drmInfos
  98. * DrmInfos of the stream. There may be multiple for multi-DRM content.
  99. * @property {!string} relativeUri
  100. * The uri associated with the stream, relative to the manifest.
  101. * @property {!shaka.media.SegmentReference} lastSegmentSeen
  102. * Last segment of the stream seen so far.
  103. */
  104. shaka.hls.HlsParser.StreamInfo;
  105. /**
  106. * @override
  107. * @exportInterface
  108. */
  109. shaka.hls.HlsParser.prototype.configure = function(config) {
  110. this.config_ = config;
  111. };
  112. /**
  113. * @override
  114. * @exportInterface
  115. */
  116. shaka.hls.HlsParser.prototype.start = function(uri, playerInterface) {
  117. goog.asserts.assert(this.config_, 'Must call configure() before start()!');
  118. this.playerInterface_ = playerInterface;
  119. this.manifestUri_ = uri;
  120. return this.requestManifest_(uri).then(function(response) {
  121. return this.parseManifest_(response.data, uri).then(function() {
  122. this.setUpdateTimer_(this.updatePeriod_);
  123. return this.manifest_;
  124. }.bind(this));
  125. }.bind(this));
  126. };
  127. /**
  128. * @override
  129. * @exportInterface
  130. */
  131. shaka.hls.HlsParser.prototype.stop = function() {
  132. this.playerInterface_ = null;
  133. this.config_ = null;
  134. this.mediaTagsToStreamInfosMap_ = {};
  135. this.manifest_ = null;
  136. return Promise.resolve();
  137. };
  138. /**
  139. * @override
  140. * @exportInterface
  141. */
  142. shaka.hls.HlsParser.prototype.update = function() {
  143. if (!this.isLive_)
  144. return;
  145. var promises = [];
  146. var uris = Object.keys(this.uriToStreamInfosMap_);
  147. for (var i = 0; i < uris.length; i++) {
  148. var uri = uris[i];
  149. var streamInfo = this.uriToStreamInfosMap_[uri];
  150. promises.push(this.updateStream_(streamInfo, uri));
  151. }
  152. return Promise.all(promises);
  153. };
  154. /**
  155. * Updates a stream.
  156. *
  157. * @param {!shaka.hls.HlsParser.StreamInfo} streamInfo
  158. * @param {string} uri
  159. * @throws shaka.util.Error
  160. * @private
  161. */
  162. shaka.hls.HlsParser.prototype.updateStream_ = function(streamInfo, uri) {
  163. this.requestManifest_(uri).then(function(response) {
  164. var Utils = shaka.hls.Utils;
  165. var playlistData = response.data;
  166. var playlist = this.manifestTextParser_.parsePlaylist(playlistData,
  167. response.uri);
  168. if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
  169. throw new shaka.util.Error(
  170. shaka.util.Error.Severity.CRITICAL,
  171. shaka.util.Error.Category.MANIFEST,
  172. shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY);
  173. }
  174. var mediaSequenceTag = Utils.getFirstTagWithName(playlist.tags,
  175. 'EXT-X-MEDIA-SEQUENCE');
  176. var startPosition = mediaSequenceTag ? Number(mediaSequenceTag.value) : 0;
  177. var segments = this.createSegments_(playlist, startPosition);
  178. segments = this.adjustSegments_(segments, streamInfo.lastSegmentSeen);
  179. streamInfo.segmentIndex.merge(segments);
  180. if (segments.length)
  181. streamInfo.lastSegmentSeen = segments[segments.length - 1];
  182. // Once the last segment has been added to the playlist, #EXT-X-ENDLIST tag
  183. // will be appended. If that happened, treat the rest of the presentation
  184. // as VOD.
  185. var endlistTag = Utils.getFirstTagWithName(playlist.tags, 'EXT-X-ENDLIST');
  186. if (endlistTag) {
  187. goog.asserts.assert(streamInfo.lastSegmentSeen != null,
  188. 'Should not be null!');
  189. var endTime = streamInfo.lastSegmentSeen.endTime;
  190. this.setLive_(false);
  191. this.presentationTimeline_.setDuration(endTime);
  192. }
  193. }.bind(this));
  194. };
  195. /**
  196. * The manifest doesn't specify segments' start and end times.
  197. * We assume the first segment starts at 0 and base the following
  198. * segments on this assumption (each segment's starts when previous ends).
  199. * This method adjusts new segments' (added on update) timeline with
  200. * respect to previously appended segments.
  201. *
  202. * @param {!Array.<!shaka.media.SegmentReference>} segments
  203. * @param {!shaka.media.SegmentReference} lastSegmentSeen
  204. * @return {!Array.<!shaka.media.SegmentReference>}
  205. * @private
  206. */
  207. shaka.hls.HlsParser.prototype.adjustSegments_ =
  208. function(segments, lastSegmentSeen) {
  209. var adjusted = [];
  210. var offset = lastSegmentSeen.endTime;
  211. for (var i = 0; i < segments.length; i++) {
  212. var segment = segments[i];
  213. if (segment.position > lastSegmentSeen.position) {
  214. var duration = segment.endTime - segment.startTime;
  215. var startTime = offset;
  216. var endTime = offset + duration;
  217. offset += duration;
  218. var adjustedSegment =
  219. new shaka.media.SegmentReference(segment.position,
  220. startTime,
  221. endTime,
  222. segment.getUris,
  223. segment.startByte,
  224. segment.endByte);
  225. adjusted.push(adjustedSegment);
  226. }
  227. }
  228. return adjusted;
  229. };
  230. /**
  231. * @override
  232. * @exportInterface
  233. */
  234. shaka.hls.HlsParser.prototype.onExpirationUpdated = function(
  235. sessionId, expiration) {
  236. // No-op
  237. };
  238. /**
  239. * Parses the manifest.
  240. *
  241. * @param {!ArrayBuffer} data
  242. * @param {string} uri
  243. * @throws shaka.util.Error When there is a parsing error.
  244. * @return {!Promise}
  245. * @private
  246. */
  247. shaka.hls.HlsParser.prototype.parseManifest_ = function(data, uri) {
  248. var playlist = this.manifestTextParser_.parsePlaylist(data, uri);
  249. // We don't support directly providing a Media Playlist.
  250. // See error code for details.
  251. if (playlist.type != shaka.hls.PlaylistType.MASTER) {
  252. throw new shaka.util.Error(
  253. shaka.util.Error.Severity.CRITICAL,
  254. shaka.util.Error.Category.MANIFEST,
  255. shaka.util.Error.Code.HLS_MASTER_PLAYLIST_NOT_PROVIDED);
  256. }
  257. return this.createPeriod_(playlist).then(function(period) {
  258. // HLS has no notion of periods. We're treating the whole presentation as
  259. // one period.
  260. this.playerInterface_.filterAllPeriods([period]);
  261. // Update presentationDelay with the largest target duration
  262. // across all variants.
  263. if (this.isLive_)
  264. this.presentationTimeline_.setDelay(this.maxTargetDuration_ * 3);
  265. goog.asserts.assert(this.presentationTimeline_ != null,
  266. 'presentationTimeline should already be created!');
  267. this.manifest_ = {
  268. presentationTimeline: this.presentationTimeline_,
  269. periods: [period],
  270. offlineSessionIds: [],
  271. minBufferTime: 0
  272. };
  273. }.bind(this));
  274. };
  275. /**
  276. * Parses a playlist into a Period object.
  277. *
  278. * @param {!shaka.hls.Playlist} playlist
  279. * @return {!Promise.<!shakaExtern.Period>}
  280. * @private
  281. */
  282. shaka.hls.HlsParser.prototype.createPeriod_ = function(playlist) {
  283. var Utils = shaka.hls.Utils;
  284. var Functional = shaka.util.Functional;
  285. var tags = playlist.tags;
  286. // Create Variants for every 'EXT-X-STREAM-INF' tag.
  287. var variantTags = Utils.filterTagsByName(tags, 'EXT-X-STREAM-INF');
  288. var variantsPromises = variantTags.map(function(tag) {
  289. return this.createVariantsForTag_(tag, playlist);
  290. }.bind(this));
  291. var mediaTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-MEDIA');
  292. var textStreamTags = mediaTags.filter(function(tag) {
  293. var type = shaka.hls.HlsParser.getRequiredAttributeValue_(tag, 'TYPE');
  294. return type == 'SUBTITLES';
  295. }.bind(this));
  296. // TODO: CLOSED-CAPTIONS requires the parsing of CEA-608 from the video.
  297. var textStreamPromises = textStreamTags.map(function(tag) {
  298. return this.createTextStream_(tag, playlist);
  299. }.bind(this));
  300. return Promise.all(variantsPromises).then(function(allVariants) {
  301. return Promise.all(textStreamPromises).then(function(textStreams) {
  302. var variants = allVariants.reduce(Functional.collapseArrays, []);
  303. if (!this.isLive_)
  304. this.fitSegments_(variants);
  305. return {
  306. startTime: 0,
  307. variants: variants,
  308. textStreams: textStreams
  309. };
  310. }.bind(this));
  311. }.bind(this));
  312. };
  313. /**
  314. * @param {!shaka.hls.Tag} tag
  315. * @param {!shaka.hls.Playlist} playlist
  316. * @return {!Promise.<!Array.<!shakaExtern.Variant>>}
  317. * @private
  318. */
  319. shaka.hls.HlsParser.prototype.createVariantsForTag_ = function(tag, playlist) {
  320. goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
  321. 'Should only be called on variant tags!');
  322. var ContentType = shaka.util.ManifestParserUtils.ContentType;
  323. var HlsParser = shaka.hls.HlsParser;
  324. var Utils = shaka.hls.Utils;
  325. var bandwidth =
  326. Number(HlsParser.getRequiredAttributeValue_(tag, 'BANDWIDTH'));
  327. // These are the default codecs to assume if none are specified.
  328. //
  329. // The video codec is H.264, with baseline profile and level 3.0.
  330. // http://blog.pearce.org.nz/2013/11/what-does-h264avc1-codecs-parameters.html
  331. //
  332. // The audio codec is "low-complexity" AAC.
  333. var defaultCodecs = 'avc1.42E01E,mp4a.40.2';
  334. /** @type {!Array.<string>} */
  335. var codecs = tag.getAttributeValue('CODECS', defaultCodecs).split(',');
  336. var resolutionAttr = tag.getAttribute('RESOLUTION');
  337. var width = null;
  338. var height = null;
  339. var frameRate = tag.getAttributeValue('FRAME-RATE');
  340. if (resolutionAttr) {
  341. var resBlocks = resolutionAttr.value.split('x');
  342. width = resBlocks[0];
  343. height = resBlocks[1];
  344. }
  345. var timeOffset = this.getTimeOffset_(playlist);
  346. var mediaTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-MEDIA');
  347. var audioGroupId = tag.getAttributeValue('AUDIO');
  348. var videoGroupId = tag.getAttributeValue('VIDEO');
  349. goog.asserts.assert(audioGroupId == null || videoGroupId == null,
  350. 'Unexpected: both video and audio described by media tags!');
  351. // Find any associated audio or video groups and create streams for them.
  352. if (audioGroupId) {
  353. mediaTags = Utils.findMediaTags(mediaTags, 'AUDIO', audioGroupId);
  354. } else if (videoGroupId) {
  355. mediaTags = Utils.findMediaTags(mediaTags, 'VIDEO', videoGroupId);
  356. }
  357. var promises = mediaTags.map(function(tag) {
  358. return this.createStreamInfoFromMediaTag_(tag, codecs, timeOffset);
  359. }.bind(this));
  360. var audioStreamInfos = [];
  361. var videoStreamInfos = [];
  362. return Promise.all(promises).then(function(data) {
  363. if (audioGroupId) {
  364. audioStreamInfos = data;
  365. } else if (videoGroupId) {
  366. videoStreamInfos = data;
  367. }
  368. // Make an educated guess about the stream type.
  369. shaka.log.debug('Guessing stream type for', tag.toString());
  370. var type;
  371. if (!audioStreamInfos.length && !videoStreamInfos.length) {
  372. // There are no associated streams. This is either an audio-only stream,
  373. // a video-only stream, or a multiplexed stream.
  374. var ignoreStream = false;
  375. if (codecs.length == 1) {
  376. // There is only one codec, so it shouldn't be multiplexed.
  377. if (resolutionAttr || frameRate) {
  378. // Assume video-only.
  379. shaka.log.debug('Guessing video-only.');
  380. type = ContentType.VIDEO;
  381. } else {
  382. // Assume audio-only.
  383. shaka.log.debug('Guessing audio-only.');
  384. type = ContentType.AUDIO;
  385. }
  386. } else {
  387. // There are multiple codecs, so assume multiplexed content.
  388. // Note that the default used when CODECS is missing assumes multiple
  389. // (and therefore multiplexed).
  390. // Recombine the codec strings into one so that MediaSource isn't
  391. // lied to later. (That would trigger an error in Chrome.)
  392. shaka.log.debug('Guessing multiplexed audio+video.');
  393. type = ContentType.VIDEO;
  394. codecs = [codecs.join(',')];
  395. }
  396. } else if (audioStreamInfos.length) {
  397. var streamURI = HlsParser.getRequiredAttributeValue_(tag, 'URI');
  398. var firstAudioStreamURI = audioStreamInfos[0].relativeUri;
  399. if (streamURI == firstAudioStreamURI) {
  400. // The Microsoft HLS manifest generators will make audio-only variants
  401. // that link to their URI both directly and through an audio tag.
  402. // In that case, ignore the local URI and use the version in the
  403. // AUDIO tag, so you inherit its language.
  404. // As an example, see the manifest linked in issue #860.
  405. shaka.log.debug('Guessing audio-only.');
  406. type = ContentType.AUDIO;
  407. ignoreStream = true;
  408. } else {
  409. // There are associated audio streams. Assume this is video.
  410. shaka.log.debug('Guessing video.');
  411. type = ContentType.VIDEO;
  412. }
  413. } else {
  414. // There are associated video streams. Assume this is audio.
  415. goog.asserts.assert(videoStreamInfos.length,
  416. 'No video streams! This should have been handled already!');
  417. shaka.log.debug('Guessing audio.');
  418. type = ContentType.AUDIO;
  419. }
  420. goog.asserts.assert(type, 'Type should have been set by now!');
  421. if (ignoreStream)
  422. return Promise.resolve();
  423. return this.createStreamInfoFromVariantTag_(tag, codecs, type, timeOffset);
  424. }.bind(this)).then(function(streamInfo) {
  425. if (streamInfo) {
  426. if (streamInfo.stream.type == ContentType.AUDIO) {
  427. audioStreamInfos = [streamInfo];
  428. } else {
  429. videoStreamInfos = [streamInfo];
  430. }
  431. }
  432. goog.asserts.assert(videoStreamInfos || audioStreamInfos,
  433. 'We should have created a stream!');
  434. return this.createVariants_(
  435. audioStreamInfos,
  436. videoStreamInfos,
  437. bandwidth,
  438. width,
  439. height,
  440. frameRate);
  441. }.bind(this));
  442. };
  443. /**
  444. * @param {!Array.<!shaka.hls.HlsParser.StreamInfo>} audioInfos
  445. * @param {!Array.<!shaka.hls.HlsParser.StreamInfo>} videoInfos
  446. * @param {number} bandwidth
  447. * @param {?string} width
  448. * @param {?string} height
  449. * @param {?string} frameRate
  450. * @return {!Array.<!shakaExtern.Variant>}
  451. * @private
  452. */
  453. shaka.hls.HlsParser.prototype.createVariants_ =
  454. function(audioInfos, videoInfos, bandwidth, width, height, frameRate) {
  455. var DrmEngine = shaka.media.DrmEngine;
  456. videoInfos.forEach(function(info) {
  457. this.addVideoAttributes_(info.stream, width, height, frameRate);
  458. }.bind(this));
  459. // In case of audio-only or video-only content, we create an array of
  460. // one item containing a null. This way, the double-loop works for all
  461. // kinds of content.
  462. // NOTE: we currently don't have support for audio-only content.
  463. if (!audioInfos.length)
  464. audioInfos = [null];
  465. if (!videoInfos.length)
  466. videoInfos = [null];
  467. var variants = [];
  468. for (var i = 0; i < audioInfos.length; i++) {
  469. for (var j = 0; j < videoInfos.length; j++) {
  470. var audioStream = audioInfos[i] ? audioInfos[i].stream : null;
  471. var videoStream = videoInfos[j] ? videoInfos[j].stream : null;
  472. var audioDrmInfos = audioInfos[i] ? audioInfos[i].drmInfos : null;
  473. var videoDrmInfos = videoInfos[j] ? videoInfos[j].drmInfos : null;
  474. var drmInfos;
  475. if (audioStream && videoStream) {
  476. if (DrmEngine.areDrmCompatible(audioDrmInfos, videoDrmInfos)) {
  477. drmInfos = DrmEngine.getCommonDrmInfos(audioDrmInfos, videoDrmInfos);
  478. } else {
  479. shaka.log.warning('Incompatible DRM info in HLS variant. Skipping.');
  480. continue;
  481. }
  482. } else if (audioStream) {
  483. drmInfos = audioDrmInfos;
  484. } else if (videoStream) {
  485. drmInfos = videoDrmInfos;
  486. }
  487. variants.push(this.createVariant_(
  488. audioStream, videoStream, bandwidth, drmInfos));
  489. }
  490. }
  491. return variants;
  492. };
  493. /**
  494. * @param {shakaExtern.Stream} audio
  495. * @param {shakaExtern.Stream} video
  496. * @param {number} bandwidth
  497. * @param {!Array.<shakaExtern.DrmInfo>} drmInfos
  498. * @return {!shakaExtern.Variant}
  499. * @private
  500. */
  501. shaka.hls.HlsParser.prototype.createVariant_ =
  502. function(audio, video, bandwidth, drmInfos) {
  503. var ContentType = shaka.util.ManifestParserUtils.ContentType;
  504. // Since both audio and video are of the same type, this assertion will catch
  505. // certain mistakes at runtime that the compiler would miss.
  506. goog.asserts.assert(!audio || audio.type == ContentType.AUDIO,
  507. 'Audio parameter mismatch!');
  508. goog.asserts.assert(!video || video.type == ContentType.VIDEO,
  509. 'Video parameter mismatch!');
  510. return {
  511. id: this.globalId_++,
  512. language: audio ? audio.language : 'und',
  513. primary: (!!audio && audio.primary) || (!!video && video.primary),
  514. audio: audio,
  515. video: video,
  516. bandwidth: bandwidth,
  517. drmInfos: drmInfos,
  518. allowedByApplication: true,
  519. allowedByKeySystem: true
  520. };
  521. };
  522. /**
  523. * Parses an EXT-X-MEDIA tag with TYPE="SUBTITLES" into a text stream.
  524. *
  525. * @param {!shaka.hls.Tag} tag
  526. * @param {!shaka.hls.Playlist} playlist
  527. * @return {!Promise.<?shakaExtern.Stream>}
  528. * @private
  529. */
  530. shaka.hls.HlsParser.prototype.createTextStream_ = function(tag, playlist) {
  531. goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
  532. 'Should only be called on media tags!');
  533. var type = shaka.hls.HlsParser.getRequiredAttributeValue_(tag, 'TYPE');
  534. goog.asserts.assert(type == 'SUBTITLES',
  535. 'Should only be called on tags with TYPE="SUBTITLES"!');
  536. var timeOffset = this.getTimeOffset_(playlist);
  537. return this.createStreamInfoFromMediaTag_(tag, [], timeOffset)
  538. .then(function(streamInfo) {
  539. return streamInfo.stream;
  540. });
  541. };
  542. /**
  543. * Parse EXT-X-MEDIA media tag into a Stream object.
  544. *
  545. * @param {shaka.hls.Tag} tag
  546. * @param {!Array.<!string>} allCodecs
  547. * @param {?number} timeOffset
  548. * @return {!Promise.<shaka.hls.HlsParser.StreamInfo>}
  549. * @private
  550. */
  551. shaka.hls.HlsParser.prototype.createStreamInfoFromMediaTag_ =
  552. function(tag, allCodecs, timeOffset) {
  553. goog.asserts.assert(tag.name == 'EXT-X-MEDIA',
  554. 'Should only be called on media tags!');
  555. // Check if the stream has already been created as part of another Variant
  556. // and return it if it has.
  557. if (this.mediaTagsToStreamInfosMap_[tag.id]) {
  558. return Promise.resolve().then(function() {
  559. return this.mediaTagsToStreamInfosMap_[tag.id];
  560. }.bind(this));
  561. }
  562. var HlsParser = shaka.hls.HlsParser;
  563. var type = HlsParser.getRequiredAttributeValue_(tag, 'TYPE').toLowerCase();
  564. // Shaka recognizes content types 'audio', 'video' and 'text'.
  565. // HLS 'subtitles' type needs to be mapped to 'text'.
  566. var ContentType = shaka.util.ManifestParserUtils.ContentType;
  567. if (type == 'subtitles') type = ContentType.TEXT;
  568. var LanguageUtils = shaka.util.LanguageUtils;
  569. var language = LanguageUtils.normalize(/** @type {string} */(
  570. tag.getAttributeValue('LANGUAGE', 'und')));
  571. var label = tag.getAttributeValue('NAME');
  572. var defaultAttr = tag.getAttribute('DEFAULT');
  573. var autoselectAttr = tag.getAttribute('AUTOSELECT');
  574. // TODO: Should we take into account some of the currently ignored attributes:
  575. // FORCED, INSTREAM-ID, CHARACTERISTICS, CHANNELS?
  576. // Attribute descriptions:
  577. // https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-4.3.4.1
  578. var channelsAttribute = tag.getAttributeValue('CHANNELS');
  579. var channelsCount = type == 'audio' ?
  580. this.getChannelsCount_(channelsAttribute) : null;
  581. var uri = HlsParser.getRequiredAttributeValue_(tag, 'URI');
  582. uri = shaka.hls.Utils.constructAbsoluteUri(this.manifestUri_, uri);
  583. var primary = !!defaultAttr || !!autoselectAttr;
  584. return this.createStreamInfo_(uri, allCodecs, type, timeOffset,
  585. language, primary, label, channelsCount).then(function(streamInfo) {
  586. this.mediaTagsToStreamInfosMap_[tag.id] = streamInfo;
  587. this.uriToStreamInfosMap_[uri] = streamInfo;
  588. return streamInfo;
  589. }.bind(this));
  590. };
  591. /**
  592. * Get the channels count information for HLS audio track.
  593. * The channels value is a string that specifies an ordered, "/" separated list
  594. * of parameters. If the type is audio, the first parameter will be a decimal
  595. * integer, as the number of independent, simultaneous audio channels.
  596. * No other channels parameters are currently defined.
  597. *
  598. * @param {?string} channels
  599. *
  600. * @return {?number} channelcount
  601. * @private
  602. */
  603. shaka.hls.HlsParser.prototype.getChannelsCount_ = function(channels) {
  604. if (!channels) return null;
  605. var channelscountstring = channels.split('/')[0];
  606. var count = parseInt(channelscountstring, 10);
  607. return count;
  608. };
  609. /**
  610. * Parse EXT-X-STREAM-INF media tag into a Stream object.
  611. *
  612. * @param {!shaka.hls.Tag} tag
  613. * @param {!Array.<!string>} allCodecs
  614. * @param {!string} type
  615. * @param {?number} timeOffset
  616. * @return {!Promise.<shaka.hls.HlsParser.StreamInfo>}
  617. * @private
  618. */
  619. shaka.hls.HlsParser.prototype.createStreamInfoFromVariantTag_ =
  620. function(tag, allCodecs, type, timeOffset) {
  621. goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
  622. 'Should only be called on media tags!');
  623. var uri = shaka.hls.HlsParser.getRequiredAttributeValue_(tag, 'URI');
  624. uri = shaka.hls.Utils.constructAbsoluteUri(this.manifestUri_, uri);
  625. return this.createStreamInfo_(uri, allCodecs, type, timeOffset,
  626. /* language */ 'und', /* primary */ false,
  627. /* label */ null, /* channelcount */ null).then(
  628. function(streamInfo) {
  629. this.uriToStreamInfosMap_[uri] = streamInfo;
  630. return streamInfo;
  631. }.bind(this));
  632. };
  633. /**
  634. * @param {!string} uri
  635. * @param {!Array.<!string>} allCodecs
  636. * @param {!string} type
  637. * @param {?number} timeOffset
  638. * @param {!string} language
  639. * @param {boolean} primary
  640. * @param {?string} label
  641. * @param {?number} channelsCount
  642. * @return {!Promise.<shaka.hls.HlsParser.StreamInfo>}
  643. * @throws shaka.util.Error
  644. * @private
  645. */
  646. shaka.hls.HlsParser.prototype.createStreamInfo_ = function(uri, allCodecs,
  647. type, timeOffset, language, primary, label, channelsCount) {
  648. var Utils = shaka.hls.Utils;
  649. var ContentType = shaka.util.ManifestParserUtils.ContentType;
  650. var HlsParser = shaka.hls.HlsParser;
  651. var relativeUri = uri;
  652. uri = Utils.constructAbsoluteUri(this.manifestUri_, uri);
  653. return this.requestManifest_(uri).then(function(response) {
  654. var playlistData = response.data;
  655. var playlist = this.manifestTextParser_.parsePlaylist(playlistData,
  656. response.uri);
  657. if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
  658. // EXT-X-MEDIA tags should point to media playlists.
  659. throw new shaka.util.Error(
  660. shaka.util.Error.Severity.CRITICAL,
  661. shaka.util.Error.Category.MANIFEST,
  662. shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY);
  663. }
  664. goog.asserts.assert(playlist.segments != null,
  665. 'Media playlist should have segments!');
  666. var mediaSequenceTag = Utils.getFirstTagWithName(playlist.tags,
  667. 'EXT-X-MEDIA-SEQUENCE');
  668. var startPosition = mediaSequenceTag ? Number(mediaSequenceTag.value) : 0;
  669. var segments = this.createSegments_(playlist, startPosition);
  670. var segmentIndex = new shaka.media.SegmentIndex(segments);
  671. this.setPresentationType_(playlist);
  672. if (!this.presentationTimeline_) {
  673. // The presentation started last available segment's end time ago.
  674. // All variants should be in sync in terms of timeline, so just grab
  675. // this from an arbitrary stream.
  676. this.createPresentationTimeline_(segments[segments.length - 1].endTime);
  677. }
  678. // Time offset can be specified on either Master or Media Playlist.
  679. // If Media Playlist provides it's own value, use that.
  680. // Otherwise, use value from the Master Playlist. If no offset
  681. // has been provided it will default to
  682. // this.config_.hls.defaultTimeOffset.
  683. var mediaPlaylistTimeOffset = this.getTimeOffset_(playlist);
  684. timeOffset = mediaPlaylistTimeOffset || timeOffset;
  685. var initSegmentReference = null;
  686. if (type != ContentType.TEXT) {
  687. initSegmentReference = this.createInitSegmentReference_(playlist);
  688. }
  689. this.presentationTimeline_.notifySegments(0, segments);
  690. if (!this.isLive_) {
  691. var duration =
  692. segments[segments.length - 1].endTime - segments[0].startTime;
  693. var presentationDuration = this.presentationTimeline_.getDuration();
  694. if (presentationDuration == Infinity || presentationDuration < duration) {
  695. this.presentationTimeline_.setDuration(duration);
  696. }
  697. }
  698. var codecs = this.guessCodecs_(type, allCodecs);
  699. var kind = undefined;
  700. var ManifestParserUtils = shaka.util.ManifestParserUtils;
  701. if (type == ManifestParserUtils.ContentType.TEXT)
  702. kind = ManifestParserUtils.TextStreamKind.SUBTITLE;
  703. // TODO: CLOSED-CAPTIONS requires the parsing of CEA-608 from the video.
  704. var drmTags = [];
  705. playlist.segments.forEach(function(segment) {
  706. var segmentKeyTags = Utils.filterTagsByName(segment.tags, 'EXT-X-KEY');
  707. drmTags.push.apply(drmTags, segmentKeyTags);
  708. });
  709. var encrypted = false;
  710. var drmInfos = [];
  711. var keyId = null;
  712. // TODO: may still need changes to support key rotation
  713. drmTags.forEach(function(drmTag) {
  714. var method = HlsParser.getRequiredAttributeValue_(drmTag, 'METHOD');
  715. if (method != 'NONE') {
  716. encrypted = true;
  717. var keyFormat =
  718. HlsParser.getRequiredAttributeValue_(drmTag, 'KEYFORMAT');
  719. var drmParser =
  720. shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
  721. var drmInfo = drmParser ? drmParser(drmTag) : null;
  722. if (drmInfo) {
  723. if (drmInfo.keyIds.length) {
  724. keyId = drmInfo.keyIds[0];
  725. }
  726. drmInfos.push(drmInfo);
  727. } else {
  728. shaka.log.warning('Unsupported HLS KEYFORMAT', keyFormat);
  729. }
  730. }
  731. });
  732. if (encrypted && !drmInfos.length) {
  733. throw new shaka.util.Error(
  734. shaka.util.Error.Severity.CRITICAL,
  735. shaka.util.Error.Category.MANIFEST,
  736. shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED);
  737. }
  738. return this.guessMimeType_(type, segments[0].getUris()[0])
  739. .then(function(mimeType) {
  740. var stream = {
  741. id: this.globalId_++,
  742. createSegmentIndex: Promise.resolve.bind(Promise),
  743. findSegmentPosition: segmentIndex.find.bind(segmentIndex),
  744. getSegmentReference: segmentIndex.get.bind(segmentIndex),
  745. initSegmentReference: initSegmentReference,
  746. presentationTimeOffset: timeOffset || 0,
  747. mimeType: mimeType,
  748. codecs: codecs,
  749. kind: kind,
  750. encrypted: encrypted,
  751. keyId: keyId,
  752. language: language,
  753. label: label || null,
  754. type: type,
  755. primary: primary,
  756. // TODO: trick mode
  757. trickModeVideo: null,
  758. containsEmsgBoxes: false,
  759. frameRate: undefined,
  760. width: undefined,
  761. height: undefined,
  762. bandwidth: undefined,
  763. roles: [],
  764. channelsCount: channelsCount
  765. };
  766. this.streamsToIndexMap_[stream.id] = segmentIndex;
  767. return {
  768. stream: stream,
  769. segmentIndex: segmentIndex,
  770. drmInfos: drmInfos,
  771. relativeUri: relativeUri,
  772. lastSegmentSeen: segments[segments.length - 1]
  773. };
  774. }.bind(this));
  775. }.bind(this));
  776. };
  777. /**
  778. * @param {!shaka.hls.Playlist} playlist
  779. * @throws shaka.util.Error
  780. * @private
  781. */
  782. shaka.hls.HlsParser.prototype.setPresentationType_ = function(playlist) {
  783. var Utils = shaka.hls.Utils;
  784. var presentationTypeTag = Utils.getFirstTagWithName(playlist.tags,
  785. 'EXT-X-PLAYLIST-TYPE');
  786. var endlistTag = Utils.getFirstTagWithName(playlist.tags, 'EXT-X-ENDLIST');
  787. var isVod = endlistTag || (presentationTypeTag &&
  788. presentationTypeTag.value == 'VOD');
  789. if (isVod) {
  790. this.setLive_(false);
  791. } else if (!presentationTypeTag) {
  792. throw new shaka.util.Error(
  793. shaka.util.Error.Severity.CRITICAL,
  794. shaka.util.Error.Category.MANIFEST,
  795. shaka.util.Error.Code.HLS_LIVE_CONTENT_NOT_SUPPORTED);
  796. } else {
  797. // presentation type EVENT
  798. var targetDurationTag = this.getRequiredTag_(playlist.tags,
  799. 'EXT-X-TARGETDURATION');
  800. var targetDuration = Number(targetDurationTag.value);
  801. // According to HLS spec, updates should not happen more often than
  802. // once in targetDuration. It also requires to only update the active
  803. // variant. We might implement that later, but for now every variant
  804. // will be updated. To get the update period, choose the smallest
  805. // targetDuration value across all playlists.
  806. if (!this.updatePeriod_) {
  807. this.setLive_(true);
  808. this.updatePeriod_ = targetDuration;
  809. } else if (this.updatePeriod_ > targetDuration) {
  810. this.updatePeriod_ = targetDuration;
  811. }
  812. // Update longest target duration if need be to use as a presentation
  813. // delay later.
  814. this.maxTargetDuration_ = Math.max(targetDuration, this.maxTargetDuration_);
  815. }
  816. };
  817. /**
  818. * @param {number} endTime
  819. * @throws shaka.util.Error
  820. * @private
  821. */
  822. shaka.hls.HlsParser.prototype.createPresentationTimeline_ = function(endTime) {
  823. var presentationStartTime = null;
  824. var delay = 0;
  825. if (this.isLive_) {
  826. presentationStartTime = (Date.now() / 1000) - endTime;
  827. // We should have a delay of at least 3 target durations.
  828. delay = this.maxTargetDuration_ * 3;
  829. }
  830. this.presentationTimeline_ = new shaka.media.PresentationTimeline(
  831. presentationStartTime, delay);
  832. this.presentationTimeline_.setStatic(!this.isLive_);
  833. };
  834. /**
  835. * @param {!shaka.hls.Playlist} playlist
  836. * @return {shaka.media.InitSegmentReference}
  837. * @private
  838. * @throws {shaka.util.Error}
  839. */
  840. shaka.hls.HlsParser.prototype.createInitSegmentReference_ = function(playlist) {
  841. var Utils = shaka.hls.Utils;
  842. var mapTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-MAP');
  843. // TODO: Support multiple map tags?
  844. // For now, we don't support multiple map tags and will throw an error.
  845. if (!mapTags.length) {
  846. return null;
  847. } else if (mapTags.length > 1) {
  848. throw new shaka.util.Error(
  849. shaka.util.Error.Severity.CRITICAL,
  850. shaka.util.Error.Category.MANIFEST,
  851. shaka.util.Error.Code.HLS_MULTIPLE_MEDIA_INIT_SECTIONS_FOUND);
  852. }
  853. // Map tag example: #EXT-X-MAP:URI="main.mp4",BYTERANGE="720@0"
  854. var mapTag = mapTags[0];
  855. var initUri = shaka.hls.HlsParser.getRequiredAttributeValue_(mapTag, 'URI');
  856. var uri = Utils.constructAbsoluteUri(playlist.uri, initUri);
  857. var startByte = 0;
  858. var endByte = null;
  859. var byterange = mapTag.getAttributeValue('BYTERANGE');
  860. // If BYTERANGE attribute is not specified, the segment consists
  861. // of the entire resourse.
  862. if (byterange) {
  863. var blocks = byterange.split('@');
  864. var byteLength = Number(blocks[0]);
  865. startByte = Number(blocks[1]);
  866. endByte = startByte + byteLength - 1;
  867. }
  868. return new shaka.media.InitSegmentReference(function() { return [uri]; },
  869. startByte,
  870. endByte);
  871. };
  872. /**
  873. * Parses shaka.hls.Segment objects into shaka.media.SegmentReferences.
  874. *
  875. * @param {!shaka.hls.Playlist} playlist
  876. * @param {number} startPosition
  877. * @return {!Array.<!shaka.media.SegmentReference>}
  878. * @private
  879. */
  880. shaka.hls.HlsParser.prototype.createSegments_ =
  881. function(playlist, startPosition) {
  882. var hlsSegments = playlist.segments;
  883. var segments = [];
  884. hlsSegments.forEach(function(segment) {
  885. var Utils = shaka.hls.Utils;
  886. var tags = segment.tags;
  887. var uri = Utils.constructAbsoluteUri(playlist.uri, segment.uri);
  888. // Start and end times
  889. var extinfTag = this.getRequiredTag_(tags, 'EXTINF');
  890. // EXTINF tag format is '#EXTINF:<duration>,[<title>]'.
  891. // We're interested in the duration part.
  892. var extinfValues = extinfTag.value.split(',');
  893. var duration = Number(extinfValues[0]);
  894. var startTime;
  895. var index = hlsSegments.indexOf(segment);
  896. if (index == 0) {
  897. startTime = 0;
  898. } else {
  899. startTime = segments[index - 1].endTime;
  900. }
  901. var endTime = startTime + duration;
  902. // StartByte and EndByte
  903. var startByte = 0;
  904. var endByte = null;
  905. var byterange = Utils.getFirstTagWithName(tags, 'EXT-X-BYTERANGE');
  906. // If BYTERANGE is not specified, the segment consists of the
  907. // entire resourse.
  908. if (byterange) {
  909. var blocks = byterange.value.split('@');
  910. var byteLength = Number(blocks[0]);
  911. if (blocks[1]) {
  912. startByte = Number(blocks[1]);
  913. } else {
  914. startByte = segments[index - 1].endByte + 1;
  915. }
  916. endByte = startByte + byteLength - 1;
  917. // Last segment has endByte of null to indicate that it extends
  918. // to the end of the resource.
  919. if (index == hlsSegments.length - 1)
  920. endByte = null;
  921. }
  922. segments.push(new shaka.media.SegmentReference(startPosition + index,
  923. startTime,
  924. endTime,
  925. function() { return [uri]; },
  926. startByte,
  927. endByte));
  928. }.bind(this));
  929. return segments;
  930. };
  931. /**
  932. * Adjusts segment references of every stream of every variant to the
  933. * timeline of the presentation.
  934. * @param {!Array.<!shakaExtern.Variant>} variants
  935. * @private
  936. */
  937. shaka.hls.HlsParser.prototype.fitSegments_ = function(variants) {
  938. variants.forEach(function(variant) {
  939. var duration = this.presentationTimeline_.getDuration();
  940. var video = variant.video;
  941. var audio = variant.audio;
  942. if (video && this.streamsToIndexMap_[video.id]) {
  943. this.streamsToIndexMap_[video.id].fit(duration);
  944. }
  945. if (audio && this.streamsToIndexMap_[audio.id]) {
  946. this.streamsToIndexMap_[audio.id].fit(duration);
  947. }
  948. }.bind(this));
  949. };
  950. /**
  951. * Attempts to guess which codecs from the codecs list belong
  952. * to a given content type.
  953. *
  954. * @param {!string} contentType
  955. * @param {!Array.<!string>} codecs
  956. * @return {string}
  957. * @private
  958. * @throws {shaka.util.Error}
  959. */
  960. shaka.hls.HlsParser.prototype.guessCodecs_ = function(contentType, codecs) {
  961. if (codecs.length == 1) {
  962. return codecs[0];
  963. }
  964. var ContentType = shaka.util.ManifestParserUtils.ContentType;
  965. if (contentType == ContentType.TEXT) {
  966. return '';
  967. }
  968. var HlsParser = shaka.hls.HlsParser;
  969. var formats = HlsParser.VIDEO_CODEC_FORMATS_;
  970. if (contentType == ContentType.AUDIO)
  971. formats = HlsParser.AUDIO_CODEC_FORMATS_;
  972. for (var i = 0; i < formats.length; i++) {
  973. for (var j = 0; j < codecs.length; j++) {
  974. if (formats[i].test(codecs[j].trim())) {
  975. return codecs[j].trim();
  976. }
  977. }
  978. }
  979. // Unable to guess codecs.
  980. throw new shaka.util.Error(
  981. shaka.util.Error.Severity.CRITICAL,
  982. shaka.util.Error.Category.MANIFEST,
  983. shaka.util.Error.Code.HLS_COULD_NOT_GUESS_CODECS,
  984. codecs);
  985. };
  986. /**
  987. * Attempts to guess stream's mime type based on content type and uri.
  988. *
  989. * @param {!string} contentType
  990. * @param {!string} uri
  991. * @return {!Promise.<!string>}
  992. * @private
  993. * @throws {shaka.util.Error}
  994. */
  995. shaka.hls.HlsParser.prototype.guessMimeType_ = function(contentType, uri) {
  996. var ContentType = shaka.util.ManifestParserUtils.ContentType;
  997. var blocks = uri.split('.');
  998. var extension = blocks[blocks.length - 1];
  999. if (contentType == ContentType.TEXT) {
  1000. // HLS only supports vtt at the moment.
  1001. return Promise.resolve('text/vtt');
  1002. }
  1003. var HlsParser = shaka.hls.HlsParser;
  1004. var map = HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_;
  1005. if (contentType == ContentType.VIDEO)
  1006. map = HlsParser.VIDEO_EXTENSIONS_TO_MIME_TYPES_;
  1007. var mimeType = map[extension];
  1008. if (mimeType)
  1009. return Promise.resolve(mimeType);
  1010. // If unable to guess mime type, request a segment and try getting it
  1011. // from the response.
  1012. var headRequest = shaka.net.NetworkingEngine.makeRequest(
  1013. [uri], this.config_.retryParameters);
  1014. headRequest.method = 'HEAD';
  1015. var requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
  1016. return this.playerInterface_.networkingEngine.request(
  1017. requestType, headRequest)
  1018. .then(function(response) {
  1019. var mimeType = response.headers['content-type'];
  1020. if (!mimeType) {
  1021. throw new shaka.util.Error(
  1022. shaka.util.Error.Severity.CRITICAL,
  1023. shaka.util.Error.Category.MANIFEST,
  1024. shaka.util.Error.Code.HLS_COULD_NOT_GUESS_MIME_TYPE,
  1025. extension);
  1026. }
  1027. return mimeType;
  1028. });
  1029. };
  1030. /**
  1031. * Get presentation time offset of the playlist if it has been specified.
  1032. * Return null otherwise.
  1033. *
  1034. * @param {!shaka.hls.Playlist} playlist
  1035. * @return {?number}
  1036. * @private
  1037. */
  1038. shaka.hls.HlsParser.prototype.getTimeOffset_ = function(playlist) {
  1039. var Utils = shaka.hls.Utils;
  1040. var startTag = Utils.getFirstTagWithName(playlist.tags, 'EXT-X-START');
  1041. // TODO: Should we respect the PRECISE flag?
  1042. // https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-4.3.5.2
  1043. if (startTag)
  1044. return Number(shaka.hls.HlsParser.getRequiredAttributeValue_(
  1045. startTag, 'TIME-OFFSET'));
  1046. return this.config_.hls.defaultTimeOffset;
  1047. };
  1048. /**
  1049. * Find the attribute and returns its value.
  1050. * Throws an error if attribute was not found.
  1051. *
  1052. * @param {shaka.hls.Tag} tag
  1053. * @param {!string} attributeName
  1054. * @return {!string}
  1055. * @private
  1056. * @throws {shaka.util.Error}
  1057. */
  1058. shaka.hls.HlsParser.getRequiredAttributeValue_ =
  1059. function(tag, attributeName) {
  1060. var attribute = tag.getAttribute(attributeName);
  1061. if (!attribute) {
  1062. throw new shaka.util.Error(
  1063. shaka.util.Error.Severity.CRITICAL,
  1064. shaka.util.Error.Category.MANIFEST,
  1065. shaka.util.Error.Code.HLS_REQUIRED_ATTRIBUTE_MISSING,
  1066. attributeName);
  1067. }
  1068. return attribute.value;
  1069. };
  1070. /**
  1071. * Returns a tag with a given name.
  1072. * Throws an error if tag was not found.
  1073. *
  1074. * @param {!Array.<shaka.hls.Tag>} tags
  1075. * @param {!string} tagName
  1076. * @return {!shaka.hls.Tag}
  1077. * @private
  1078. * @throws {shaka.util.Error}
  1079. */
  1080. shaka.hls.HlsParser.prototype.getRequiredTag_ = function(tags, tagName) {
  1081. var Utils = shaka.hls.Utils;
  1082. var tag = Utils.getFirstTagWithName(tags, tagName);
  1083. if (!tag) {
  1084. throw new shaka.util.Error(
  1085. shaka.util.Error.Severity.CRITICAL,
  1086. shaka.util.Error.Category.MANIFEST,
  1087. shaka.util.Error.Code.HLS_REQUIRED_TAG_MISSING, tagName);
  1088. }
  1089. return tag;
  1090. };
  1091. /**
  1092. * @param {shakaExtern.Stream} stream
  1093. * @param {?string} width
  1094. * @param {?string} height
  1095. * @param {?string} frameRate
  1096. * @private
  1097. */
  1098. shaka.hls.HlsParser.prototype.addVideoAttributes_ =
  1099. function(stream, width, height, frameRate) {
  1100. if (stream) {
  1101. stream.width = Number(width) || undefined;
  1102. stream.height = Number(height) || undefined;
  1103. stream.frameRate = Number(frameRate) || undefined;
  1104. }
  1105. };
  1106. /**
  1107. * Makes a network request for the manifest and returns a Promise
  1108. * with the resulting data.
  1109. *
  1110. * @param {!string} uri
  1111. * @return {!Promise.<!shakaExtern.Response>}
  1112. * @private
  1113. */
  1114. shaka.hls.HlsParser.prototype.requestManifest_ = function(uri) {
  1115. var requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
  1116. var request = shaka.net.NetworkingEngine.makeRequest(
  1117. [uri], this.config_.retryParameters);
  1118. return this.playerInterface_.networkingEngine.request(requestType, request);
  1119. };
  1120. /**
  1121. * A list of well-known video codecs formats.
  1122. *
  1123. * @const {!Array<!RegExp>}
  1124. * @private
  1125. */
  1126. shaka.hls.HlsParser.VIDEO_CODEC_FORMATS_ = [
  1127. /^(avc)/,
  1128. /^(hvc)/,
  1129. /^(vp[8-9])$/,
  1130. /^(av1)$/,
  1131. /^(mp4v)/
  1132. ];
  1133. /**
  1134. * A list of well-known audio codecs formats.
  1135. *
  1136. * @const {!Array<!RegExp>}
  1137. * @private
  1138. */
  1139. shaka.hls.HlsParser.AUDIO_CODEC_FORMATS_ = [
  1140. /^(vorbis)/,
  1141. /^(opus)/,
  1142. /^(mp4a)/,
  1143. /^(ac-3)$/,
  1144. /^(ec-3)$/
  1145. ];
  1146. /**
  1147. * @const {!Object<string, string>}
  1148. * @private
  1149. */
  1150. shaka.hls.HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_ = {
  1151. 'mp4': 'audio/mp4',
  1152. 'm4s': 'audio/mp4',
  1153. 'm4i': 'audio/mp4',
  1154. 'm4a': 'audio/mp4',
  1155. // mpeg2 ts aslo uses video/ for audio: http://goo.gl/tYHXiS
  1156. 'ts': 'video/mp2t'
  1157. };
  1158. /**
  1159. * @const {!Object<string, string>}
  1160. * @private
  1161. */
  1162. shaka.hls.HlsParser.VIDEO_EXTENSIONS_TO_MIME_TYPES_ = {
  1163. 'mp4': 'video/mp4',
  1164. 'm4s': 'video/mp4',
  1165. 'm4i': 'video/mp4',
  1166. 'm4v': 'video/mp4',
  1167. 'ts': 'video/mp2t'
  1168. };
  1169. /**
  1170. * @typedef {function(!shaka.hls.Tag):?shakaExtern.DrmInfo}
  1171. * @private
  1172. */
  1173. shaka.hls.HlsParser.DrmParser_;
  1174. /**
  1175. * @param {!shaka.hls.Tag} drmTag
  1176. * @return {?shakaExtern.DrmInfo}
  1177. * @private
  1178. */
  1179. shaka.hls.HlsParser.widevineDrmParser_ = function(drmTag) {
  1180. var HlsParser = shaka.hls.HlsParser;
  1181. var method = HlsParser.getRequiredAttributeValue_(drmTag, 'METHOD');
  1182. if (method != 'SAMPLE-AES-CENC') {
  1183. shaka.log.error(
  1184. 'Widevine in HLS is only supported with SAMPLE-AES-CENC, not', method);
  1185. return null;
  1186. }
  1187. var uri = HlsParser.getRequiredAttributeValue_(drmTag, 'URI');
  1188. var parsedData = shaka.net.DataUriPlugin.parse(uri);
  1189. // The data encoded in the URI is a PSSH box to be used as init data.
  1190. var pssh = new Uint8Array(parsedData.data);
  1191. var drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
  1192. 'com.widevine.alpha', [
  1193. {initDataType: 'cenc', initData: pssh}
  1194. ]);
  1195. var keyId = drmTag.getAttributeValue('KEYID');
  1196. if (keyId) {
  1197. // This value begins with '0x':
  1198. goog.asserts.assert(keyId.substr(0, 2) == '0x',
  1199. 'Incorrect KEYID format!');
  1200. // But the output does not contain the '0x':
  1201. drmInfo.keyIds = [keyId.substr(2).toLowerCase()];
  1202. }
  1203. return drmInfo;
  1204. };
  1205. /**
  1206. * Called when the update timer ticks.
  1207. *
  1208. * @private
  1209. */
  1210. shaka.hls.HlsParser.prototype.onUpdate_ = function() {
  1211. goog.asserts.assert(this.updateTimer_, 'Should only be called by timer');
  1212. goog.asserts.assert(this.updatePeriod_ != null,
  1213. 'There should be an update period');
  1214. shaka.log.info('Updating manifest...');
  1215. // Detect a call to stop()
  1216. if (!this.playerInterface_)
  1217. return;
  1218. this.updateTimer_ = null;
  1219. this.update().then(function() {
  1220. this.setUpdateTimer_(this.updatePeriod_);
  1221. }.bind(this)).catch(function(error) {
  1222. goog.asserts.assert(error instanceof shaka.util.Error,
  1223. 'Should only receive a Shaka error');
  1224. // Try updating again, but ensure we haven't been destroyed.
  1225. if (this.playerInterface_) {
  1226. // We will retry updating, so override the severity of the error.
  1227. error.severity = shaka.util.Error.Severity.RECOVERABLE;
  1228. this.playerInterface_.onError(error);
  1229. this.setUpdateTimer_(0);
  1230. }
  1231. }.bind(this));
  1232. };
  1233. /**
  1234. * Sets the update timer.
  1235. *
  1236. * @param {?number} time in seconds
  1237. * @private
  1238. */
  1239. shaka.hls.HlsParser.prototype.setUpdateTimer_ = function(time) {
  1240. if (this.updatePeriod_ == null || time == null)
  1241. return;
  1242. goog.asserts.assert(this.updateTimer_ == null,
  1243. 'Timer should not be already set');
  1244. var callback = this.onUpdate_.bind(this);
  1245. this.updateTimer_ = window.setTimeout(callback, time * 1000);
  1246. };
  1247. /**
  1248. * @param {boolean} live
  1249. * @private
  1250. */
  1251. shaka.hls.HlsParser.prototype.setLive_ = function(live) {
  1252. this.isLive_ = live;
  1253. if (this.presentationTimeline_)
  1254. this.presentationTimeline_.setStatic(!live);
  1255. if (!live) {
  1256. if (this.updateTimer_ != null) {
  1257. window.clearTimeout(this.updateTimer_);
  1258. this.updateTimer_ = null;
  1259. this.updatePeriod_ = null;
  1260. }
  1261. }
  1262. };
  1263. /**
  1264. * @const {!Object.<string, shaka.hls.HlsParser.DrmParser_>}
  1265. * @private
  1266. */
  1267. shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_ = {
  1268. /* TODO: https://github.com/google/shaka-player/issues/382
  1269. 'com.apple.streamingkeydelivery':
  1270. shaka.hls.HlsParser.fairplayDrmParser_,
  1271. */
  1272. 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed':
  1273. shaka.hls.HlsParser.widevineDrmParser_
  1274. };
  1275. shaka.media.ManifestParser.registerParserByExtension(
  1276. 'm3u8', shaka.hls.HlsParser);
  1277. shaka.media.ManifestParser.registerParserByMime(
  1278. 'application/x-mpegurl', shaka.hls.HlsParser);
  1279. shaka.media.ManifestParser.registerParserByMime(
  1280. 'application/vnd.apple.mpegurl', shaka.hls.HlsParser);