Source: lib/util/periods.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.util.PeriodCombiner');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.log');
  9. goog.require('shaka.media.DrmEngine');
  10. goog.require('shaka.media.MetaSegmentIndex');
  11. goog.require('shaka.media.SegmentIndex');
  12. goog.require('shaka.util.Error');
  13. goog.require('shaka.util.IReleasable');
  14. goog.require('shaka.util.LanguageUtils');
  15. goog.require('shaka.util.ManifestParserUtils');
  16. goog.require('shaka.util.MimeUtils');
  17. /**
  18. * A utility to combine streams across periods.
  19. *
  20. * @implements {shaka.util.IReleasable}
  21. * @final
  22. * @export
  23. */
  24. shaka.util.PeriodCombiner = class {
  25. /** */
  26. constructor() {
  27. /** @private {!Array.<shaka.extern.Variant>} */
  28. this.variants_ = [];
  29. /** @private {!Array.<shaka.extern.Stream>} */
  30. this.audioStreams_ = [];
  31. /** @private {!Array.<shaka.extern.Stream>} */
  32. this.videoStreams_ = [];
  33. /** @private {!Array.<shaka.extern.Stream>} */
  34. this.textStreams_ = [];
  35. /** @private {!Array.<shaka.extern.Stream>} */
  36. this.imageStreams_ = [];
  37. /** @private {boolean} */
  38. this.multiTypeVariantsAllowed_ = false;
  39. /** @private {boolean} */
  40. this.useStreamOnce_ = false;
  41. /**
  42. * The IDs of the periods we have already used to generate streams.
  43. * This helps us identify the periods which have been added when a live
  44. * stream is updated.
  45. *
  46. * @private {!Set.<string>}
  47. */
  48. this.usedPeriodIds_ = new Set();
  49. }
  50. /** @override */
  51. release() {
  52. const allStreams =
  53. this.audioStreams_.concat(this.videoStreams_, this.textStreams_,
  54. this.imageStreams_);
  55. for (const stream of allStreams) {
  56. if (stream.segmentIndex) {
  57. stream.segmentIndex.release();
  58. }
  59. }
  60. this.audioStreams_ = [];
  61. this.videoStreams_ = [];
  62. this.textStreams_ = [];
  63. this.imageStreams_ = [];
  64. this.variants_ = [];
  65. }
  66. /**
  67. * @return {!Array.<shaka.extern.Variant>}
  68. *
  69. * @export
  70. */
  71. getVariants() {
  72. return this.variants_;
  73. }
  74. /**
  75. * @return {!Array.<shaka.extern.Stream>}
  76. *
  77. * @export
  78. */
  79. getTextStreams() {
  80. // Return a copy of the array because makeTextStreamsForClosedCaptions
  81. // may make changes to the contents of the array. Those changes should not
  82. // propagate back to the PeriodCombiner.
  83. return this.textStreams_.slice();
  84. }
  85. /**
  86. * @return {!Array.<shaka.extern.Stream>}
  87. *
  88. * @export
  89. */
  90. getImageStreams() {
  91. return this.imageStreams_;
  92. }
  93. /**
  94. * Returns an object that contains arrays of streams by type
  95. * @param {!Array<shaka.extern.Period>} periods
  96. * @param {boolean} addDummy
  97. * @return {{
  98. * audioStreamsPerPeriod: !Array<!Map<string, shaka.extern.Stream>>,
  99. * videoStreamsPerPeriod: !Array<!Map<string, shaka.extern.Stream>>,
  100. * textStreamsPerPeriod: !Array<!Map<string, shaka.extern.Stream>>,
  101. * imageStreamsPerPeriod: !Array<!Map<string, shaka.extern.Stream>>
  102. * }}
  103. * @private
  104. */
  105. getStreamsPerPeriod_(periods, addDummy) {
  106. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  107. const PeriodCombiner = shaka.util.PeriodCombiner;
  108. const audioStreamsPerPeriod = [];
  109. const videoStreamsPerPeriod = [];
  110. const textStreamsPerPeriod = [];
  111. const imageStreamsPerPeriod = [];
  112. for (const period of periods) {
  113. const audioMap = new Map(period.audioStreams.map((s) =>
  114. [PeriodCombiner.generateAudioKey_(s), s]));
  115. const videoMap = new Map(period.videoStreams.map((s) =>
  116. [PeriodCombiner.generateVideoKey_(s), s]));
  117. const textMap = new Map(period.textStreams.map((s) =>
  118. [PeriodCombiner.generateTextKey_(s), s]));
  119. const imageMap = new Map(period.imageStreams.map((s) =>
  120. [PeriodCombiner.generateImageKey_(s), s]));
  121. // It's okay to have a period with no text or images, but our algorithm
  122. // fails on any period without matching streams. So we add dummy streams
  123. // to each period. Since we combine text streams by language and image
  124. // streams by resolution, we might need a dummy even in periods with these
  125. // streams already.
  126. if (addDummy) {
  127. const dummyText = PeriodCombiner.dummyStream_(ContentType.TEXT);
  128. textMap.set(PeriodCombiner.generateTextKey_(dummyText), dummyText);
  129. const dummyImage = PeriodCombiner.dummyStream_(ContentType.IMAGE);
  130. imageMap.set(PeriodCombiner.generateImageKey_(dummyImage), dummyImage);
  131. }
  132. audioStreamsPerPeriod.push(audioMap);
  133. videoStreamsPerPeriod.push(videoMap);
  134. textStreamsPerPeriod.push(textMap);
  135. imageStreamsPerPeriod.push(imageMap);
  136. }
  137. return {
  138. audioStreamsPerPeriod,
  139. videoStreamsPerPeriod,
  140. textStreamsPerPeriod,
  141. imageStreamsPerPeriod,
  142. };
  143. }
  144. /**
  145. * @param {!Array.<shaka.extern.Period>} periods
  146. * @param {boolean} isDynamic
  147. * @param {boolean=} isPatchUpdate
  148. * @return {!Promise}
  149. *
  150. * @export
  151. */
  152. async combinePeriods(periods, isDynamic, isPatchUpdate = false) {
  153. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  154. // Optimization: for single-period VOD, do nothing. This makes sure
  155. // single-period DASH content will be 100% accurately represented in the
  156. // output.
  157. if (!isDynamic && periods.length == 1) {
  158. // We need to filter out duplicates, so call getStreamsPerPeriod()
  159. // so it will do that by usage of Map.
  160. const {
  161. audioStreamsPerPeriod,
  162. videoStreamsPerPeriod,
  163. textStreamsPerPeriod,
  164. imageStreamsPerPeriod,
  165. } = this.getStreamsPerPeriod_(periods, /* addDummy= */ false);
  166. this.audioStreams_ = Array.from(audioStreamsPerPeriod[0].values());
  167. this.videoStreams_ = Array.from(videoStreamsPerPeriod[0].values());
  168. this.textStreams_ = Array.from(textStreamsPerPeriod[0].values());
  169. this.imageStreams_ = Array.from(imageStreamsPerPeriod[0].values());
  170. } else {
  171. // How many periods we've seen before which are not included in this call.
  172. const periodsMissing = isPatchUpdate ? this.usedPeriodIds_.size : 0;
  173. // Find the first period we haven't seen before. Tag all the periods we
  174. // see now as "used".
  175. let firstNewPeriodIndex = -1;
  176. for (let i = 0; i < periods.length; i++) {
  177. const period = periods[i];
  178. if (this.usedPeriodIds_.has(period.id)) {
  179. // This isn't new.
  180. } else {
  181. // This one _is_ new.
  182. this.usedPeriodIds_.add(period.id);
  183. if (firstNewPeriodIndex == -1) {
  184. // And it's the _first_ new one.
  185. firstNewPeriodIndex = i;
  186. }
  187. }
  188. }
  189. if (firstNewPeriodIndex == -1) {
  190. // Nothing new? Nothing to do.
  191. return;
  192. }
  193. const {
  194. audioStreamsPerPeriod,
  195. videoStreamsPerPeriod,
  196. textStreamsPerPeriod,
  197. imageStreamsPerPeriod,
  198. } = this.getStreamsPerPeriod_(periods, /* addDummy= */ true);
  199. await Promise.all([
  200. this.combine_(
  201. this.audioStreams_,
  202. audioStreamsPerPeriod,
  203. firstNewPeriodIndex,
  204. shaka.util.PeriodCombiner.cloneStream_,
  205. shaka.util.PeriodCombiner.concatenateStreams_,
  206. periodsMissing),
  207. this.combine_(
  208. this.videoStreams_,
  209. videoStreamsPerPeriod,
  210. firstNewPeriodIndex,
  211. shaka.util.PeriodCombiner.cloneStream_,
  212. shaka.util.PeriodCombiner.concatenateStreams_,
  213. periodsMissing),
  214. this.combine_(
  215. this.textStreams_,
  216. textStreamsPerPeriod,
  217. firstNewPeriodIndex,
  218. shaka.util.PeriodCombiner.cloneStream_,
  219. shaka.util.PeriodCombiner.concatenateStreams_,
  220. periodsMissing),
  221. this.combine_(
  222. this.imageStreams_,
  223. imageStreamsPerPeriod,
  224. firstNewPeriodIndex,
  225. shaka.util.PeriodCombiner.cloneStream_,
  226. shaka.util.PeriodCombiner.concatenateStreams_,
  227. periodsMissing),
  228. ]);
  229. }
  230. // Create variants for all audio/video combinations.
  231. let nextVariantId = 0;
  232. const variants = [];
  233. if (!this.videoStreams_.length || !this.audioStreams_.length) {
  234. // For audio-only or video-only content, just give each stream its own
  235. // variant.
  236. const streams = this.videoStreams_.length ? this.videoStreams_ :
  237. this.audioStreams_;
  238. for (const stream of streams) {
  239. const id = nextVariantId++;
  240. variants.push({
  241. id,
  242. language: stream.language,
  243. disabledUntilTime: 0,
  244. primary: stream.primary,
  245. audio: stream.type == ContentType.AUDIO ? stream : null,
  246. video: stream.type == ContentType.VIDEO ? stream : null,
  247. bandwidth: stream.bandwidth || 0,
  248. drmInfos: stream.drmInfos,
  249. allowedByApplication: true,
  250. allowedByKeySystem: true,
  251. decodingInfos: [],
  252. });
  253. }
  254. } else {
  255. for (const audio of this.audioStreams_) {
  256. for (const video of this.videoStreams_) {
  257. const commonDrmInfos = shaka.media.DrmEngine.getCommonDrmInfos(
  258. audio.drmInfos, video.drmInfos);
  259. if (audio.drmInfos.length && video.drmInfos.length &&
  260. !commonDrmInfos.length) {
  261. shaka.log.warning(
  262. 'Incompatible DRM in audio & video, skipping variant creation.',
  263. audio, video);
  264. continue;
  265. }
  266. const id = nextVariantId++;
  267. variants.push({
  268. id,
  269. language: audio.language,
  270. disabledUntilTime: 0,
  271. primary: audio.primary,
  272. audio,
  273. video,
  274. bandwidth: (audio.bandwidth || 0) + (video.bandwidth || 0),
  275. drmInfos: commonDrmInfos,
  276. allowedByApplication: true,
  277. allowedByKeySystem: true,
  278. decodingInfos: [],
  279. });
  280. }
  281. }
  282. }
  283. this.variants_ = variants;
  284. }
  285. /**
  286. * Stitch together DB streams across periods, taking a mix of stream types.
  287. * The offline database does not separate these by type.
  288. *
  289. * Unlike the DASH case, this does not need to maintain any state for manifest
  290. * updates.
  291. *
  292. * @param {!Array.<!Array.<shaka.extern.StreamDB>>} streamDbsPerPeriod
  293. * @return {!Promise.<!Array.<shaka.extern.StreamDB>>}
  294. */
  295. static async combineDbStreams(streamDbsPerPeriod) {
  296. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  297. const PeriodCombiner = shaka.util.PeriodCombiner;
  298. // Optimization: for single-period content, do nothing. This makes sure
  299. // single-period DASH or any HLS content stored offline will be 100%
  300. // accurately represented in the output.
  301. if (streamDbsPerPeriod.length == 1) {
  302. return streamDbsPerPeriod[0];
  303. }
  304. const audioStreamDbsPerPeriod = streamDbsPerPeriod.map(
  305. (streams) => new Map(streams
  306. .filter((s) => s.type === ContentType.AUDIO)
  307. .map((s) => [PeriodCombiner.generateAudioKey_(s), s])));
  308. const videoStreamDbsPerPeriod = streamDbsPerPeriod.map(
  309. (streams) => new Map(streams
  310. .filter((s) => s.type === ContentType.VIDEO)
  311. .map((s) => [PeriodCombiner.generateVideoKey_(s), s])));
  312. const textStreamDbsPerPeriod = streamDbsPerPeriod.map(
  313. (streams) => new Map(streams
  314. .filter((s) => s.type === ContentType.TEXT)
  315. .map((s) => [PeriodCombiner.generateTextKey_(s), s])));
  316. const imageStreamDbsPerPeriod = streamDbsPerPeriod.map(
  317. (streams) => new Map(streams
  318. .filter((s) => s.type === ContentType.IMAGE)
  319. .map((s) => [PeriodCombiner.generateImageKey_(s), s])));
  320. // It's okay to have a period with no text or images, but our algorithm
  321. // fails on any period without matching streams. So we add dummy streams to
  322. // each period. Since we combine text streams by language and image streams
  323. // by resolution, we might need a dummy even in periods with these streams
  324. // already.
  325. for (const textStreams of textStreamDbsPerPeriod) {
  326. const dummy = PeriodCombiner.dummyStreamDB_(ContentType.TEXT);
  327. textStreams.set(PeriodCombiner.generateTextKey_(dummy), dummy);
  328. }
  329. for (const imageStreams of imageStreamDbsPerPeriod) {
  330. const dummy = PeriodCombiner.dummyStreamDB_(ContentType.IMAGE);
  331. imageStreams.set(PeriodCombiner.generateImageKey_(dummy), dummy);
  332. }
  333. const periodCombiner = new shaka.util.PeriodCombiner();
  334. const combinedAudioStreamDbs = await periodCombiner.combine_(
  335. /* outputStreams= */ [],
  336. audioStreamDbsPerPeriod,
  337. /* firstNewPeriodIndex= */ 0,
  338. shaka.util.PeriodCombiner.cloneStreamDB_,
  339. shaka.util.PeriodCombiner.concatenateStreamDBs_,
  340. /* periodsMissing= */ 0);
  341. const combinedVideoStreamDbs = await periodCombiner.combine_(
  342. /* outputStreams= */ [],
  343. videoStreamDbsPerPeriod,
  344. /* firstNewPeriodIndex= */ 0,
  345. shaka.util.PeriodCombiner.cloneStreamDB_,
  346. shaka.util.PeriodCombiner.concatenateStreamDBs_,
  347. /* periodsMissing= */ 0);
  348. const combinedTextStreamDbs = await periodCombiner.combine_(
  349. /* outputStreams= */ [],
  350. textStreamDbsPerPeriod,
  351. /* firstNewPeriodIndex= */ 0,
  352. shaka.util.PeriodCombiner.cloneStreamDB_,
  353. shaka.util.PeriodCombiner.concatenateStreamDBs_,
  354. /* periodsMissing= */ 0);
  355. const combinedImageStreamDbs = await periodCombiner.combine_(
  356. /* outputStreams= */ [],
  357. imageStreamDbsPerPeriod,
  358. /* firstNewPeriodIndex= */ 0,
  359. shaka.util.PeriodCombiner.cloneStreamDB_,
  360. shaka.util.PeriodCombiner.concatenateStreamDBs_,
  361. /* periodsMissing= */ 0);
  362. // Recreate variantIds from scratch in the output.
  363. // HLS content is always single-period, so the early return at the top of
  364. // this method would catch all HLS content. DASH content stored with v3.0
  365. // will already be flattened before storage. Therefore the only content
  366. // that reaches this point is multi-period DASH content stored before v3.0.
  367. // Such content always had variants generated from all combinations of audio
  368. // and video, so we can simply do that now without loss of correctness.
  369. let nextVariantId = 0;
  370. if (!combinedVideoStreamDbs.length || !combinedAudioStreamDbs.length) {
  371. // For audio-only or video-only content, just give each stream its own
  372. // variant ID.
  373. const combinedStreamDbs =
  374. combinedVideoStreamDbs.concat(combinedAudioStreamDbs);
  375. for (const stream of combinedStreamDbs) {
  376. stream.variantIds = [nextVariantId++];
  377. }
  378. } else {
  379. for (const audio of combinedAudioStreamDbs) {
  380. for (const video of combinedVideoStreamDbs) {
  381. const id = nextVariantId++;
  382. video.variantIds.push(id);
  383. audio.variantIds.push(id);
  384. }
  385. }
  386. }
  387. return combinedVideoStreamDbs
  388. .concat(combinedAudioStreamDbs)
  389. .concat(combinedTextStreamDbs)
  390. .concat(combinedImageStreamDbs);
  391. }
  392. /**
  393. * Combine input Streams per period into flat output Streams.
  394. * Templatized to handle both DASH Streams and offline StreamDBs.
  395. *
  396. * @param {!Array.<T>} outputStreams A list of existing output streams, to
  397. * facilitate updates for live DASH content. Will be modified and returned.
  398. * @param {!Array<!Map<string, T>>} streamsPerPeriod A list of maps of Streams
  399. * from each period.
  400. * @param {number} firstNewPeriodIndex An index into streamsPerPeriod which
  401. * represents the first new period that hasn't been processed yet.
  402. * @param {function(T):T} clone Make a clone of an input stream.
  403. * @param {function(T, T)} concat Concatenate the second stream onto the end
  404. * of the first.
  405. * @param {number} periodsMissing The number of periods missing
  406. *
  407. * @return {!Promise.<!Array.<T>>} The same array passed to outputStreams,
  408. * modified to include any newly-created streams.
  409. *
  410. * @template T
  411. * Accepts either a StreamDB or Stream type.
  412. *
  413. * @private
  414. */
  415. async combine_(
  416. outputStreams, streamsPerPeriod, firstNewPeriodIndex, clone, concat,
  417. periodsMissing) {
  418. const unusedStreamsPerPeriod = [];
  419. for (let i = 0; i < streamsPerPeriod.length; i++) {
  420. if (i >= firstNewPeriodIndex) {
  421. // This periods streams are all new.
  422. unusedStreamsPerPeriod.push(new Set(streamsPerPeriod[i].values()));
  423. } else {
  424. // This period's streams have all been used already.
  425. unusedStreamsPerPeriod.push(new Set());
  426. }
  427. }
  428. // First, extend all existing output Streams into the new periods.
  429. for (const outputStream of outputStreams) {
  430. // eslint-disable-next-line no-await-in-loop
  431. const ok = await this.extendExistingOutputStream_(
  432. outputStream, streamsPerPeriod, firstNewPeriodIndex, concat,
  433. unusedStreamsPerPeriod, periodsMissing);
  434. if (!ok) {
  435. // This output Stream was not properly extended to include streams from
  436. // the new period. This is likely a bug in our algorithm, so throw an
  437. // error.
  438. throw new shaka.util.Error(
  439. shaka.util.Error.Severity.CRITICAL,
  440. shaka.util.Error.Category.MANIFEST,
  441. shaka.util.Error.Code.PERIOD_FLATTENING_FAILED);
  442. }
  443. // This output stream is now complete with content from all known
  444. // periods.
  445. } // for (const outputStream of outputStreams)
  446. for (const unusedStreams of unusedStreamsPerPeriod) {
  447. for (const stream of unusedStreams) {
  448. // Create a new output stream which includes this input stream.
  449. const outputStream = this.createNewOutputStream_(
  450. stream, streamsPerPeriod, clone, concat,
  451. unusedStreamsPerPeriod);
  452. if (outputStream) {
  453. outputStreams.push(outputStream);
  454. } else {
  455. // This is not a stream we can build output from, but it may become
  456. // part of another output based on another period's stream.
  457. }
  458. } // for (const stream of unusedStreams)
  459. } // for (const unusedStreams of unusedStreamsPerPeriod)
  460. for (const unusedStreams of unusedStreamsPerPeriod) {
  461. for (const stream of unusedStreams) {
  462. if (shaka.util.PeriodCombiner.isDummy_(stream)) {
  463. // This is one of our dummy streams, so ignore it. We may not use
  464. // them all, and that's fine.
  465. continue;
  466. }
  467. // If this stream has a different codec/MIME than any other stream,
  468. // then we can't play it.
  469. const hasCodec = outputStreams.some((s) => {
  470. return this.areAVStreamsCompatible_(stream, s);
  471. });
  472. if (!hasCodec) {
  473. continue;
  474. }
  475. // Any other unused stream is likely a bug in our algorithm, so throw
  476. // an error.
  477. shaka.log.error('Unused stream in period-flattening!',
  478. stream, outputStreams);
  479. throw new shaka.util.Error(
  480. shaka.util.Error.Severity.CRITICAL,
  481. shaka.util.Error.Category.MANIFEST,
  482. shaka.util.Error.Code.PERIOD_FLATTENING_FAILED);
  483. }
  484. }
  485. return outputStreams;
  486. }
  487. /**
  488. * @param {T} outputStream An existing output stream which needs to be
  489. * extended into new periods.
  490. * @param {!Array<!Map<string, T>>} streamsPerPeriod A list of maps of Streams
  491. * from each period.
  492. * @param {number} firstNewPeriodIndex An index into streamsPerPeriod which
  493. * represents the first new period that hasn't been processed yet.
  494. * @param {function(T, T)} concat Concatenate the second stream onto the end
  495. * of the first.
  496. * @param {!Array.<!Set.<T>>} unusedStreamsPerPeriod An array of sets of
  497. * unused streams from each period.
  498. * @param {number} periodsMissing How many periods are missing in this update.
  499. *
  500. * @return {!Promise.<boolean>}
  501. *
  502. * @template T
  503. * Should only be called with a Stream type in practice, but has call sites
  504. * from other templated functions that also accept a StreamDB.
  505. *
  506. * @private
  507. */
  508. async extendExistingOutputStream_(
  509. outputStream, streamsPerPeriod, firstNewPeriodIndex, concat,
  510. unusedStreamsPerPeriod, periodsMissing) {
  511. this.findMatchesInAllPeriods_(streamsPerPeriod,
  512. outputStream, periodsMissing > 0);
  513. // This only exists where T == Stream, and this should only ever be called
  514. // on Stream types. StreamDB should not have pre-existing output streams.
  515. goog.asserts.assert(outputStream.createSegmentIndex,
  516. 'outputStream should be a Stream type!');
  517. if (!outputStream.matchedStreams) {
  518. // We were unable to extend this output stream.
  519. shaka.log.error('No matches extending output stream!',
  520. outputStream, streamsPerPeriod);
  521. return false;
  522. }
  523. // We need to create all the per-period segment indexes and append them to
  524. // the output's MetaSegmentIndex.
  525. if (outputStream.segmentIndex) {
  526. await shaka.util.PeriodCombiner.extendOutputSegmentIndex_(outputStream,
  527. firstNewPeriodIndex + periodsMissing);
  528. }
  529. shaka.util.PeriodCombiner.extendOutputStream_(outputStream,
  530. firstNewPeriodIndex, concat, unusedStreamsPerPeriod, periodsMissing);
  531. return true;
  532. }
  533. /**
  534. * Creates the segment indexes for an array of input streams, and append them
  535. * to the output stream's segment index.
  536. *
  537. * @param {shaka.extern.Stream} outputStream
  538. * @param {number} firstNewPeriodIndex An index into streamsPerPeriod which
  539. * represents the first new period that hasn't been processed yet.
  540. * @private
  541. */
  542. static async extendOutputSegmentIndex_(outputStream, firstNewPeriodIndex) {
  543. const operations = [];
  544. const streams = outputStream.matchedStreams;
  545. goog.asserts.assert(streams, 'matched streams should be valid');
  546. for (const stream of streams) {
  547. operations.push(stream.createSegmentIndex());
  548. if (stream.trickModeVideo && !stream.trickModeVideo.segmentIndex) {
  549. operations.push(stream.trickModeVideo.createSegmentIndex());
  550. }
  551. }
  552. await Promise.all(operations);
  553. // Concatenate the new matches onto the stream, starting at the first new
  554. // period.
  555. // Satisfy the compiler about the type.
  556. // Also checks if the segmentIndex is still valid after the async
  557. // operations, to make sure we stop if the active stream has changed.
  558. if (outputStream.segmentIndex instanceof shaka.media.MetaSegmentIndex) {
  559. for (let i = firstNewPeriodIndex; i < streams.length; i++) {
  560. const match = streams[i];
  561. goog.asserts.assert(match.segmentIndex,
  562. 'stream should have a segmentIndex.');
  563. if (match.segmentIndex) {
  564. outputStream.segmentIndex.appendSegmentIndex(match.segmentIndex);
  565. }
  566. }
  567. }
  568. }
  569. /**
  570. * Create a new output Stream based on a particular input Stream. Locates
  571. * matching Streams in all other periods and combines them into an output
  572. * Stream.
  573. * Templatized to handle both DASH Streams and offline StreamDBs.
  574. *
  575. * @param {T} stream An input stream on which to base the output stream.
  576. * @param {!Array<!Map<string, T>>} streamsPerPeriod A list of maps of Streams
  577. * from each period.
  578. * @param {function(T):T} clone Make a clone of an input stream.
  579. * @param {function(T, T)} concat Concatenate the second stream onto the end
  580. * of the first.
  581. * @param {!Array.<!Set.<T>>} unusedStreamsPerPeriod An array of sets of
  582. * unused streams from each period.
  583. *
  584. * @return {?T} A newly-created output Stream, or null if matches
  585. * could not be found.`
  586. *
  587. * @template T
  588. * Accepts either a StreamDB or Stream type.
  589. *
  590. * @private
  591. */
  592. createNewOutputStream_(
  593. stream, streamsPerPeriod, clone, concat, unusedStreamsPerPeriod) {
  594. // Check do we want to create output stream from dummy stream
  595. // and if so, return quickly.
  596. if (shaka.util.PeriodCombiner.isDummy_(stream)) {
  597. return null;
  598. }
  599. // Start by cloning the stream without segments, key IDs, etc.
  600. const outputStream = clone(stream);
  601. // Find best-matching streams in all periods.
  602. this.findMatchesInAllPeriods_(streamsPerPeriod, outputStream);
  603. // This only exists where T == Stream.
  604. if (outputStream.createSegmentIndex) {
  605. // Override the createSegmentIndex function of the outputStream.
  606. outputStream.createSegmentIndex = async () => {
  607. if (!outputStream.segmentIndex) {
  608. outputStream.segmentIndex = new shaka.media.MetaSegmentIndex();
  609. await shaka.util.PeriodCombiner.extendOutputSegmentIndex_(
  610. outputStream, /* firstNewPeriodIndex= */ 0);
  611. }
  612. };
  613. // For T == Stream, we need to create all the per-period segment indexes
  614. // in advance. concat() will add them to the output's MetaSegmentIndex.
  615. }
  616. if (!outputStream.matchedStreams || !outputStream.matchedStreams.length) {
  617. // This is not a stream we can build output from, but it may become part
  618. // of another output based on another period's stream.
  619. return null;
  620. }
  621. shaka.util.PeriodCombiner.extendOutputStream_(outputStream,
  622. /* firstNewPeriodIndex= */ 0, concat, unusedStreamsPerPeriod,
  623. /* periodsMissing= */ 0);
  624. return outputStream;
  625. }
  626. /**
  627. * @param {T} outputStream An existing output stream which needs to be
  628. * extended into new periods.
  629. * @param {number} firstNewPeriodIndex An index into streamsPerPeriod which
  630. * represents the first new period that hasn't been processed yet.
  631. * @param {function(T, T)} concat Concatenate the second stream onto the end
  632. * of the first.
  633. * @param {!Array.<!Set.<T>>} unusedStreamsPerPeriod An array of sets of
  634. * unused streams from each period.
  635. * @param {number} periodsMissing How many periods are missing in this update
  636. *
  637. * @template T
  638. * Accepts either a StreamDB or Stream type.
  639. *
  640. * @private
  641. */
  642. static extendOutputStream_(
  643. outputStream, firstNewPeriodIndex, concat, unusedStreamsPerPeriod,
  644. periodsMissing) {
  645. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  646. const LanguageUtils = shaka.util.LanguageUtils;
  647. const matches = outputStream.matchedStreams;
  648. // Assure the compiler that matches didn't become null during the async
  649. // operation before.
  650. goog.asserts.assert(outputStream.matchedStreams,
  651. 'matchedStreams should be non-null');
  652. // Concatenate the new matches onto the stream, starting at the first new
  653. // period.
  654. const start = firstNewPeriodIndex + periodsMissing;
  655. for (let i = start; i < matches.length; i++) {
  656. const match = matches[i];
  657. concat(outputStream, match);
  658. // We only consider an audio stream "used" if its language is related to
  659. // the output language. There are scenarios where we want to generate
  660. // separate tracks for each language, even when we are forced to connect
  661. // unrelated languages across periods.
  662. let used = true;
  663. if (outputStream.type == ContentType.AUDIO) {
  664. const relatedness = LanguageUtils.relatedness(
  665. outputStream.language, match.language);
  666. if (relatedness == 0) {
  667. used = false;
  668. }
  669. }
  670. if (used) {
  671. unusedStreamsPerPeriod[i - periodsMissing].delete(match);
  672. // Add the full mimetypes to the stream.
  673. if (match.fullMimeTypes) {
  674. for (const fullMimeType of match.fullMimeTypes.values()) {
  675. outputStream.fullMimeTypes.add(fullMimeType);
  676. }
  677. }
  678. }
  679. }
  680. }
  681. /**
  682. * Clone a Stream to make an output Stream for combining others across
  683. * periods.
  684. *
  685. * @param {shaka.extern.Stream} stream
  686. * @return {shaka.extern.Stream}
  687. * @private
  688. */
  689. static cloneStream_(stream) {
  690. const clone = /** @type {shaka.extern.Stream} */(Object.assign({}, stream));
  691. // These are wiped out now and rebuilt later from the various per-period
  692. // streams that match this output.
  693. clone.originalId = null;
  694. clone.createSegmentIndex = () => Promise.resolve();
  695. clone.closeSegmentIndex = () => {
  696. if (clone.segmentIndex) {
  697. clone.segmentIndex.release();
  698. clone.segmentIndex = null;
  699. }
  700. // Close the segment index of the matched streams.
  701. if (clone.matchedStreams) {
  702. for (const match of clone.matchedStreams) {
  703. if (match.segmentIndex) {
  704. match.segmentIndex.release();
  705. match.segmentIndex = null;
  706. }
  707. }
  708. }
  709. };
  710. // Clone roles array so this output stream can own it.
  711. clone.roles = clone.roles.slice();
  712. clone.segmentIndex = null;
  713. clone.emsgSchemeIdUris = [];
  714. clone.keyIds = new Set();
  715. clone.closedCaptions = null;
  716. clone.trickModeVideo = null;
  717. return clone;
  718. }
  719. /**
  720. * Clone a StreamDB to make an output stream for combining others across
  721. * periods.
  722. *
  723. * @param {shaka.extern.StreamDB} streamDb
  724. * @return {shaka.extern.StreamDB}
  725. * @private
  726. */
  727. static cloneStreamDB_(streamDb) {
  728. const clone = /** @type {shaka.extern.StreamDB} */(Object.assign(
  729. {}, streamDb));
  730. // Clone roles array so this output stream can own it.
  731. clone.roles = clone.roles.slice();
  732. // These are wiped out now and rebuilt later from the various per-period
  733. // streams that match this output.
  734. clone.keyIds = new Set();
  735. clone.segments = [];
  736. clone.variantIds = [];
  737. clone.closedCaptions = null;
  738. return clone;
  739. }
  740. /**
  741. * Combine the various fields of the input Stream into the output.
  742. *
  743. * @param {shaka.extern.Stream} output
  744. * @param {shaka.extern.Stream} input
  745. * @private
  746. */
  747. static concatenateStreams_(output, input) {
  748. // We keep the original stream's bandwidth, resolution, frame rate,
  749. // sample rate, and channel count to ensure that it's properly
  750. // matched with similar content in other periods further down
  751. // the line.
  752. // Combine arrays, keeping only the unique elements
  753. const combineArrays = (output, input) => {
  754. if (!output) {
  755. output = [];
  756. }
  757. for (const item of input) {
  758. if (!output.includes(item)) {
  759. output.push(item);
  760. }
  761. }
  762. return output;
  763. };
  764. output.roles = combineArrays(output.roles, input.roles);
  765. if (input.emsgSchemeIdUris) {
  766. output.emsgSchemeIdUris = combineArrays(
  767. output.emsgSchemeIdUris, input.emsgSchemeIdUris);
  768. }
  769. for (const keyId of input.keyIds) {
  770. output.keyIds.add(keyId);
  771. }
  772. if (output.originalId == null) {
  773. output.originalId = input.originalId;
  774. } else {
  775. output.originalId += ',' + (input.originalId || '');
  776. }
  777. const commonDrmInfos = shaka.media.DrmEngine.getCommonDrmInfos(
  778. output.drmInfos, input.drmInfos);
  779. if (input.drmInfos.length && output.drmInfos.length &&
  780. !commonDrmInfos.length) {
  781. throw new shaka.util.Error(
  782. shaka.util.Error.Severity.CRITICAL,
  783. shaka.util.Error.Category.MANIFEST,
  784. shaka.util.Error.Code.INCONSISTENT_DRM_ACROSS_PERIODS);
  785. }
  786. output.drmInfos = commonDrmInfos;
  787. // The output is encrypted if any input was encrypted.
  788. output.encrypted = output.encrypted || input.encrypted;
  789. // Combine the closed captions maps.
  790. if (input.closedCaptions) {
  791. if (!output.closedCaptions) {
  792. output.closedCaptions = new Map();
  793. }
  794. for (const [key, value] of input.closedCaptions) {
  795. output.closedCaptions.set(key, value);
  796. }
  797. }
  798. // Combine trick-play video streams, if present.
  799. if (input.trickModeVideo) {
  800. if (!output.trickModeVideo) {
  801. // Create a fresh output stream for trick-mode playback.
  802. output.trickModeVideo = shaka.util.PeriodCombiner.cloneStream_(
  803. input.trickModeVideo);
  804. // TODO: fix the createSegmentIndex function for trickModeVideo.
  805. // The trick-mode tracks in multi-period content should have trick-mode
  806. // segment indexes whenever available, rather than only regular-mode
  807. // segment indexes.
  808. output.trickModeVideo.createSegmentIndex = () => {
  809. // Satisfy the compiler about the type.
  810. goog.asserts.assert(
  811. output.segmentIndex instanceof shaka.media.MetaSegmentIndex,
  812. 'The stream should have a MetaSegmentIndex.');
  813. output.trickModeVideo.segmentIndex = output.segmentIndex.clone();
  814. return Promise.resolve();
  815. };
  816. }
  817. // Concatenate the trick mode input onto the trick mode output.
  818. shaka.util.PeriodCombiner.concatenateStreams_(
  819. output.trickModeVideo, input.trickModeVideo);
  820. } else if (output.trickModeVideo) {
  821. // We have a trick mode output, but no input from this Period. Fill it in
  822. // from the standard input Stream.
  823. shaka.util.PeriodCombiner.concatenateStreams_(
  824. output.trickModeVideo, input);
  825. }
  826. }
  827. /**
  828. * Combine the various fields of the input StreamDB into the output.
  829. *
  830. * @param {shaka.extern.StreamDB} output
  831. * @param {shaka.extern.StreamDB} input
  832. * @private
  833. */
  834. static concatenateStreamDBs_(output, input) {
  835. // Combine arrays, keeping only the unique elements
  836. const combineArrays = (output, input) => {
  837. if (!output) {
  838. output = [];
  839. }
  840. for (const item of input) {
  841. if (!output.includes(item)) {
  842. output.push(item);
  843. }
  844. }
  845. return output;
  846. };
  847. output.roles = combineArrays(output.roles, input.roles);
  848. for (const keyId of input.keyIds) {
  849. output.keyIds.add(keyId);
  850. }
  851. // The output is encrypted if any input was encrypted.
  852. output.encrypted = output.encrypted && input.encrypted;
  853. // Concatenate segments without de-duping.
  854. output.segments.push(...input.segments);
  855. // Combine the closed captions maps.
  856. if (input.closedCaptions) {
  857. if (!output.closedCaptions) {
  858. output.closedCaptions = new Map();
  859. }
  860. for (const [key, value] of input.closedCaptions) {
  861. output.closedCaptions.set(key, value);
  862. }
  863. }
  864. }
  865. /**
  866. * Finds streams in all periods which match the output stream.
  867. *
  868. * @param {!Array<!Map<string, T>>} streamsPerPeriod
  869. * @param {T} outputStream
  870. * @param {boolean=} shouldAppend
  871. *
  872. * @template T
  873. * Accepts either a StreamDB or Stream type.
  874. *
  875. * @private
  876. */
  877. findMatchesInAllPeriods_(streamsPerPeriod, outputStream,
  878. shouldAppend = false) {
  879. const matches = shouldAppend ? outputStream.matchedStreams : [];
  880. for (const streams of streamsPerPeriod) {
  881. const match = this.findBestMatchInPeriod_(streams, outputStream);
  882. if (!match) {
  883. return;
  884. }
  885. matches.push(match);
  886. }
  887. outputStream.matchedStreams = matches;
  888. }
  889. /**
  890. * Find the best match for the output stream.
  891. *
  892. * @param {!Map<string, T>} streams
  893. * @param {T} outputStream
  894. * @return {?T} Returns null if no match can be found.
  895. *
  896. * @template T
  897. * Accepts either a StreamDB or Stream type.
  898. *
  899. * @private
  900. */
  901. findBestMatchInPeriod_(streams, outputStream) {
  902. const getKey = {
  903. 'audio': shaka.util.PeriodCombiner.generateAudioKey_,
  904. 'video': shaka.util.PeriodCombiner.generateVideoKey_,
  905. 'text': shaka.util.PeriodCombiner.generateTextKey_,
  906. 'image': shaka.util.PeriodCombiner.generateImageKey_,
  907. }[outputStream.type];
  908. let best = null;
  909. const key = getKey(outputStream);
  910. if (streams.has(key)) {
  911. // We've found exact match by hashing.
  912. best = streams.get(key);
  913. } else {
  914. // We haven't found exact match, try to find the best one via
  915. // linear search.
  916. const areCompatible = {
  917. 'audio': (os, s) => this.areAVStreamsCompatible_(os, s),
  918. 'video': (os, s) => this.areAVStreamsCompatible_(os, s),
  919. 'text': shaka.util.PeriodCombiner.areTextStreamsCompatible_,
  920. 'image': shaka.util.PeriodCombiner.areImageStreamsCompatible_,
  921. }[outputStream.type];
  922. const isBetterMatch = {
  923. 'audio': shaka.util.PeriodCombiner.isAudioStreamBetterMatch_,
  924. 'video': shaka.util.PeriodCombiner.isVideoStreamBetterMatch_,
  925. 'text': shaka.util.PeriodCombiner.isTextStreamBetterMatch_,
  926. 'image': shaka.util.PeriodCombiner.isImageStreamBetterMatch_,
  927. }[outputStream.type];
  928. for (const stream of streams.values()) {
  929. if (!areCompatible(outputStream, stream)) {
  930. continue;
  931. }
  932. if (outputStream.fastSwitching != stream.fastSwitching) {
  933. continue;
  934. }
  935. if (!best || isBetterMatch(outputStream, best, stream)) {
  936. best = stream;
  937. }
  938. }
  939. }
  940. // Remove just found stream if configured to, so possible future linear
  941. // searches can be faster.
  942. if (this.useStreamOnce_ && !shaka.util.PeriodCombiner.isDummy_(best)) {
  943. streams.delete(getKey(best));
  944. }
  945. return best;
  946. }
  947. /**
  948. * @param {T} a
  949. * @param {T} b
  950. * @return {boolean}
  951. *
  952. * @template T
  953. * Accepts either a StreamDB or Stream type.
  954. *
  955. * @private
  956. */
  957. static areAVStreamsExactMatch_(a, b) {
  958. if (a.mimeType != b.mimeType) {
  959. return false;
  960. }
  961. /**
  962. * @param {string} codecs
  963. * @return {string}
  964. */
  965. const getCodec = (codecs) => {
  966. if (!shaka.util.PeriodCombiner.memoizedCodecs.has(codecs)) {
  967. const normalizedCodec = shaka.util.MimeUtils.getNormalizedCodec(codecs);
  968. shaka.util.PeriodCombiner.memoizedCodecs.set(codecs, normalizedCodec);
  969. }
  970. return shaka.util.PeriodCombiner.memoizedCodecs.get(codecs);
  971. };
  972. return getCodec(a.codecs) == getCodec(b.codecs);
  973. }
  974. /**
  975. * @param {boolean} allowed If set to true, multi-mimeType or multi-codec
  976. * variants will be allowed.
  977. * @export
  978. */
  979. setAllowMultiTypeVariants(allowed) {
  980. this.multiTypeVariantsAllowed_ = allowed;
  981. }
  982. /**
  983. * @param {boolean} useOnce if true, stream will be used only once in period
  984. * flattening algoritnm.
  985. * @export
  986. */
  987. setUseStreamOnce(useOnce) {
  988. this.useStreamOnce_ = useOnce;
  989. }
  990. /**
  991. * @param {T} outputStream An audio or video output stream
  992. * @param {T} candidate A candidate stream to be combined with the output
  993. * @return {boolean} True if the candidate could be combined with the
  994. * output stream
  995. *
  996. * @template T
  997. * Accepts either a StreamDB or Stream type.
  998. *
  999. * @private
  1000. */
  1001. areAVStreamsCompatible_(outputStream, candidate) {
  1002. // Check for an exact match.
  1003. if (!shaka.util.PeriodCombiner.areAVStreamsExactMatch_(
  1004. outputStream, candidate)) {
  1005. // It's not an exact match. See if we can do multi-codec or multi-mimeType
  1006. // stream instead, using SourceBuffer.changeType.
  1007. if (!this.multiTypeVariantsAllowed_) {
  1008. return false;
  1009. }
  1010. }
  1011. // This field is only available on Stream, not StreamDB.
  1012. if (outputStream.drmInfos) {
  1013. // Check for compatible DRM systems. Note that clear streams are
  1014. // implicitly compatible with any DRM and with each other.
  1015. if (!shaka.media.DrmEngine.areDrmCompatible(outputStream.drmInfos,
  1016. candidate.drmInfos)) {
  1017. return false;
  1018. }
  1019. }
  1020. return true;
  1021. }
  1022. /**
  1023. * @param {T} outputStream A text output stream
  1024. * @param {T} candidate A candidate stream to be combined with the output
  1025. * @return {boolean} True if the candidate could be combined with the
  1026. * output
  1027. *
  1028. * @template T
  1029. * Accepts either a StreamDB or Stream type.
  1030. *
  1031. * @private
  1032. */
  1033. static areTextStreamsCompatible_(outputStream, candidate) {
  1034. const LanguageUtils = shaka.util.LanguageUtils;
  1035. // For text, we don't care about MIME type or codec. We can always switch
  1036. // between text types.
  1037. // If the candidate is a dummy, then it is compatible, and we could use it
  1038. // if nothing else matches.
  1039. if (!candidate.language) {
  1040. return true;
  1041. }
  1042. // Forced subtitles should be treated as unique streams
  1043. if (outputStream.forced !== candidate.forced) {
  1044. return false;
  1045. }
  1046. const languageRelatedness = LanguageUtils.relatedness(
  1047. outputStream.language, candidate.language);
  1048. // We will strictly avoid combining text across languages or "kinds"
  1049. // (caption vs subtitle).
  1050. if (languageRelatedness == 0 ||
  1051. candidate.kind != outputStream.kind) {
  1052. return false;
  1053. }
  1054. return true;
  1055. }
  1056. /**
  1057. * @param {T} outputStream A image output stream
  1058. * @param {T} candidate A candidate stream to be combined with the output
  1059. * @return {boolean} True if the candidate could be combined with the
  1060. * output
  1061. *
  1062. * @template T
  1063. * Accepts either a StreamDB or Stream type.
  1064. *
  1065. * @private
  1066. */
  1067. static areImageStreamsCompatible_(outputStream, candidate) {
  1068. // For image, we don't care about MIME type. We can always switch
  1069. // between image types.
  1070. return true;
  1071. }
  1072. /**
  1073. * @param {T} outputStream An audio output stream
  1074. * @param {T} best The best match so far for this period
  1075. * @param {T} candidate A candidate stream which might be better
  1076. * @return {boolean} True if the candidate is a better match
  1077. *
  1078. * @template T
  1079. * Accepts either a StreamDB or Stream type.
  1080. *
  1081. * @private
  1082. */
  1083. static isAudioStreamBetterMatch_(outputStream, best, candidate) {
  1084. const LanguageUtils = shaka.util.LanguageUtils;
  1085. const {BETTER, EQUAL, WORSE} = shaka.util.PeriodCombiner.BetterOrWorse;
  1086. // An exact match is better than a non-exact match.
  1087. const bestIsExact = shaka.util.PeriodCombiner.areAVStreamsExactMatch_(
  1088. outputStream, best);
  1089. const candidateIsExact = shaka.util.PeriodCombiner.areAVStreamsExactMatch_(
  1090. outputStream, candidate);
  1091. if (bestIsExact && !candidateIsExact) {
  1092. return false;
  1093. }
  1094. if (!bestIsExact && candidateIsExact) {
  1095. return true;
  1096. }
  1097. // The most important thing is language. In some cases, we will accept a
  1098. // different language across periods when we must.
  1099. const bestRelatedness = LanguageUtils.relatedness(
  1100. outputStream.language, best.language);
  1101. const candidateRelatedness = LanguageUtils.relatedness(
  1102. outputStream.language, candidate.language);
  1103. if (candidateRelatedness > bestRelatedness) {
  1104. return true;
  1105. }
  1106. if (candidateRelatedness < bestRelatedness) {
  1107. return false;
  1108. }
  1109. // If language-based differences haven't decided this, look at labels.
  1110. // If available options differ, look does any matches with output stream.
  1111. if (best.label !== candidate.label) {
  1112. if (outputStream.label === best.label) {
  1113. return false;
  1114. }
  1115. if (outputStream.label === candidate.label) {
  1116. return true;
  1117. }
  1118. }
  1119. // If label-based differences haven't decided this, look at roles. If
  1120. // the candidate has more roles in common with the output, upgrade to the
  1121. // candidate.
  1122. if (outputStream.roles.length) {
  1123. const bestRoleMatches =
  1124. best.roles.filter((role) => outputStream.roles.includes(role));
  1125. const candidateRoleMatches =
  1126. candidate.roles.filter((role) => outputStream.roles.includes(role));
  1127. if (candidateRoleMatches.length > bestRoleMatches.length) {
  1128. return true;
  1129. } else if (candidateRoleMatches.length < bestRoleMatches.length) {
  1130. return false;
  1131. } else {
  1132. // Both streams have the same role overlap with the outputStream
  1133. // If this is the case, choose the stream with the fewer roles overall.
  1134. // Streams that match best together tend to be streams with the same
  1135. // roles, e g stream1 with roles [r1, r2] is likely a better match
  1136. // for stream2 with roles [r1, r2] vs stream3 with roles
  1137. // [r1, r2, r3, r4].
  1138. // If we match stream1 with stream3 due to the same role overlap,
  1139. // stream2 is likely to be left unmatched and error out later.
  1140. // See https://github.com/shaka-project/shaka-player/issues/2542 for
  1141. // more details.
  1142. return candidate.roles.length < best.roles.length;
  1143. }
  1144. } else if (!candidate.roles.length && best.roles.length) {
  1145. // If outputStream has no roles, and only one of the streams has no roles,
  1146. // choose the one with no roles.
  1147. return true;
  1148. } else if (candidate.roles.length && !best.roles.length) {
  1149. return false;
  1150. }
  1151. // If the language doesn't match, but the candidate is the "primary"
  1152. // language, then that should be preferred as a fallback.
  1153. if (!best.primary && candidate.primary) {
  1154. return true;
  1155. }
  1156. if (best.primary && !candidate.primary) {
  1157. return false;
  1158. }
  1159. // If language-based and role-based features are equivalent, take the audio
  1160. // with the closes channel count to the output.
  1161. const channelsBetterOrWorse =
  1162. shaka.util.PeriodCombiner.compareClosestPreferLower(
  1163. outputStream.channelsCount,
  1164. best.channelsCount,
  1165. candidate.channelsCount);
  1166. if (channelsBetterOrWorse == BETTER) {
  1167. return true;
  1168. } else if (channelsBetterOrWorse == WORSE) {
  1169. return false;
  1170. }
  1171. // If channels are equal, take the closest sample rate to the output.
  1172. const sampleRateBetterOrWorse =
  1173. shaka.util.PeriodCombiner.compareClosestPreferLower(
  1174. outputStream.audioSamplingRate,
  1175. best.audioSamplingRate,
  1176. candidate.audioSamplingRate);
  1177. if (sampleRateBetterOrWorse == BETTER) {
  1178. return true;
  1179. } else if (sampleRateBetterOrWorse == WORSE) {
  1180. return false;
  1181. }
  1182. if (outputStream.bandwidth) {
  1183. // Take the audio with the closest bandwidth to the output.
  1184. const bandwidthBetterOrWorse =
  1185. shaka.util.PeriodCombiner.compareClosestPreferMinimalAbsDiff_(
  1186. outputStream.bandwidth,
  1187. best.bandwidth,
  1188. candidate.bandwidth);
  1189. if (bandwidthBetterOrWorse == BETTER) {
  1190. return true;
  1191. } else if (bandwidthBetterOrWorse == WORSE) {
  1192. return false;
  1193. }
  1194. }
  1195. // If the result of each comparison was inconclusive, default to false.
  1196. return false;
  1197. }
  1198. /**
  1199. * @param {T} outputStream A video output stream
  1200. * @param {T} best The best match so far for this period
  1201. * @param {T} candidate A candidate stream which might be better
  1202. * @return {boolean} True if the candidate is a better match
  1203. *
  1204. * @template T
  1205. * Accepts either a StreamDB or Stream type.
  1206. *
  1207. * @private
  1208. */
  1209. static isVideoStreamBetterMatch_(outputStream, best, candidate) {
  1210. const {BETTER, EQUAL, WORSE} = shaka.util.PeriodCombiner.BetterOrWorse;
  1211. // An exact match is better than a non-exact match.
  1212. const bestIsExact = shaka.util.PeriodCombiner.areAVStreamsExactMatch_(
  1213. outputStream, best);
  1214. const candidateIsExact = shaka.util.PeriodCombiner.areAVStreamsExactMatch_(
  1215. outputStream, candidate);
  1216. if (bestIsExact && !candidateIsExact) {
  1217. return false;
  1218. }
  1219. if (!bestIsExact && candidateIsExact) {
  1220. return true;
  1221. }
  1222. // Take the video with the closest resolution to the output.
  1223. const resolutionBetterOrWorse =
  1224. shaka.util.PeriodCombiner.compareClosestPreferLower(
  1225. outputStream.width * outputStream.height,
  1226. best.width * best.height,
  1227. candidate.width * candidate.height);
  1228. if (resolutionBetterOrWorse == BETTER) {
  1229. return true;
  1230. } else if (resolutionBetterOrWorse == WORSE) {
  1231. return false;
  1232. }
  1233. // We may not know the frame rate for the content, in which case this gets
  1234. // skipped.
  1235. if (outputStream.frameRate) {
  1236. // Take the video with the closest frame rate to the output.
  1237. const frameRateBetterOrWorse =
  1238. shaka.util.PeriodCombiner.compareClosestPreferLower(
  1239. outputStream.frameRate,
  1240. best.frameRate,
  1241. candidate.frameRate);
  1242. if (frameRateBetterOrWorse == BETTER) {
  1243. return true;
  1244. } else if (frameRateBetterOrWorse == WORSE) {
  1245. return false;
  1246. }
  1247. }
  1248. if (outputStream.bandwidth) {
  1249. // Take the video with the closest bandwidth to the output.
  1250. const bandwidthBetterOrWorse =
  1251. shaka.util.PeriodCombiner.compareClosestPreferMinimalAbsDiff_(
  1252. outputStream.bandwidth,
  1253. best.bandwidth,
  1254. candidate.bandwidth);
  1255. if (bandwidthBetterOrWorse == BETTER) {
  1256. return true;
  1257. } else if (bandwidthBetterOrWorse == WORSE) {
  1258. return false;
  1259. }
  1260. }
  1261. // If the result of each comparison was inconclusive, default to false.
  1262. return false;
  1263. }
  1264. /**
  1265. * @param {T} outputStream A text output stream
  1266. * @param {T} best The best match so far for this period
  1267. * @param {T} candidate A candidate stream which might be better
  1268. * @return {boolean} True if the candidate is a better match
  1269. *
  1270. * @template T
  1271. * Accepts either a StreamDB or Stream type.
  1272. *
  1273. * @private
  1274. */
  1275. static isTextStreamBetterMatch_(outputStream, best, candidate) {
  1276. const LanguageUtils = shaka.util.LanguageUtils;
  1277. // The most important thing is language. In some cases, we will accept a
  1278. // different language across periods when we must.
  1279. const bestRelatedness = LanguageUtils.relatedness(
  1280. outputStream.language, best.language);
  1281. const candidateRelatedness = LanguageUtils.relatedness(
  1282. outputStream.language, candidate.language);
  1283. if (candidateRelatedness > bestRelatedness) {
  1284. return true;
  1285. }
  1286. if (candidateRelatedness < bestRelatedness) {
  1287. return false;
  1288. }
  1289. // If the language doesn't match, but the candidate is the "primary"
  1290. // language, then that should be preferred as a fallback.
  1291. if (!best.primary && candidate.primary) {
  1292. return true;
  1293. }
  1294. if (best.primary && !candidate.primary) {
  1295. return false;
  1296. }
  1297. // If language-based differences haven't decided this, look at labels.
  1298. // If available options differ, look does any matches with output stream.
  1299. if (best.label !== candidate.label) {
  1300. if (outputStream.label === best.label) {
  1301. return false;
  1302. }
  1303. if (outputStream.label === candidate.label) {
  1304. return true;
  1305. }
  1306. }
  1307. // If the candidate has more roles in common with the output, upgrade to the
  1308. // candidate.
  1309. if (outputStream.roles.length) {
  1310. const bestRoleMatches =
  1311. best.roles.filter((role) => outputStream.roles.includes(role));
  1312. const candidateRoleMatches =
  1313. candidate.roles.filter((role) => outputStream.roles.includes(role));
  1314. if (candidateRoleMatches.length > bestRoleMatches.length) {
  1315. return true;
  1316. }
  1317. if (candidateRoleMatches.length < bestRoleMatches.length) {
  1318. return false;
  1319. }
  1320. } else if (!candidate.roles.length && best.roles.length) {
  1321. // If outputStream has no roles, and only one of the streams has no roles,
  1322. // choose the one with no roles.
  1323. return true;
  1324. } else if (candidate.roles.length && !best.roles.length) {
  1325. return false;
  1326. }
  1327. // If the candidate has the same MIME type and codec, upgrade to the
  1328. // candidate. It's not required that text streams use the same format
  1329. // across periods, but it's a helpful signal. Some content in our demo app
  1330. // contains the same languages repeated with two different text formats in
  1331. // each period. This condition ensures that all text streams are used.
  1332. // Otherwise, we wind up with some one stream of each language left unused,
  1333. // triggering a failure.
  1334. if (candidate.mimeType == outputStream.mimeType &&
  1335. candidate.codecs == outputStream.codecs &&
  1336. (best.mimeType != outputStream.mimeType ||
  1337. best.codecs != outputStream.codecs)) {
  1338. return true;
  1339. }
  1340. // If the result of each comparison was inconclusive, default to false.
  1341. return false;
  1342. }
  1343. /**
  1344. * @param {T} outputStream A image output stream
  1345. * @param {T} best The best match so far for this period
  1346. * @param {T} candidate A candidate stream which might be better
  1347. * @return {boolean} True if the candidate is a better match
  1348. *
  1349. * @template T
  1350. * Accepts either a StreamDB or Stream type.
  1351. *
  1352. * @private
  1353. */
  1354. static isImageStreamBetterMatch_(outputStream, best, candidate) {
  1355. const {BETTER, EQUAL, WORSE} = shaka.util.PeriodCombiner.BetterOrWorse;
  1356. // Take the image with the closest resolution to the output.
  1357. const resolutionBetterOrWorse =
  1358. shaka.util.PeriodCombiner.compareClosestPreferLower(
  1359. outputStream.width * outputStream.height,
  1360. best.width * best.height,
  1361. candidate.width * candidate.height);
  1362. if (resolutionBetterOrWorse == BETTER) {
  1363. return true;
  1364. } else if (resolutionBetterOrWorse == WORSE) {
  1365. return false;
  1366. }
  1367. // If the result of each comparison was inconclusive, default to false.
  1368. return false;
  1369. }
  1370. /**
  1371. * Create a dummy StreamDB to fill in periods that are missing a certain type,
  1372. * to avoid failing the general flattening algorithm. This won't be used for
  1373. * audio or video, since those are strictly required in all periods if they
  1374. * exist in any period.
  1375. *
  1376. * @param {shaka.util.ManifestParserUtils.ContentType} type
  1377. * @return {shaka.extern.StreamDB}
  1378. * @private
  1379. */
  1380. static dummyStreamDB_(type) {
  1381. return {
  1382. id: 0,
  1383. originalId: '',
  1384. groupId: null,
  1385. primary: false,
  1386. type,
  1387. mimeType: '',
  1388. codecs: '',
  1389. language: '',
  1390. originalLanguage: null,
  1391. label: null,
  1392. width: null,
  1393. height: null,
  1394. encrypted: false,
  1395. keyIds: new Set(),
  1396. segments: [],
  1397. variantIds: [],
  1398. roles: [],
  1399. forced: false,
  1400. channelsCount: null,
  1401. audioSamplingRate: null,
  1402. spatialAudio: false,
  1403. closedCaptions: null,
  1404. external: false,
  1405. fastSwitching: false,
  1406. };
  1407. }
  1408. /**
  1409. * Create a dummy Stream to fill in periods that are missing a certain type,
  1410. * to avoid failing the general flattening algorithm. This won't be used for
  1411. * audio or video, since those are strictly required in all periods if they
  1412. * exist in any period.
  1413. *
  1414. * @param {shaka.util.ManifestParserUtils.ContentType} type
  1415. * @return {shaka.extern.Stream}
  1416. * @private
  1417. */
  1418. static dummyStream_(type) {
  1419. return {
  1420. id: 0,
  1421. originalId: '',
  1422. groupId: null,
  1423. createSegmentIndex: () => Promise.resolve(),
  1424. segmentIndex: new shaka.media.SegmentIndex([]),
  1425. mimeType: '',
  1426. codecs: '',
  1427. encrypted: false,
  1428. drmInfos: [],
  1429. keyIds: new Set(),
  1430. language: '',
  1431. originalLanguage: null,
  1432. label: null,
  1433. type,
  1434. primary: false,
  1435. trickModeVideo: null,
  1436. emsgSchemeIdUris: null,
  1437. roles: [],
  1438. forced: false,
  1439. channelsCount: null,
  1440. audioSamplingRate: null,
  1441. spatialAudio: false,
  1442. closedCaptions: null,
  1443. accessibilityPurpose: null,
  1444. external: false,
  1445. fastSwitching: false,
  1446. fullMimeTypes: new Set(),
  1447. };
  1448. }
  1449. /**
  1450. * Compare the best value so far with the candidate value and the output
  1451. * value. Decide if the candidate is better, equal, or worse than the best
  1452. * so far. Any value less than or equal to the output is preferred over a
  1453. * larger value, and closer to the output is better than farther.
  1454. *
  1455. * This provides us a generic way to choose things that should match as
  1456. * closely as possible, like resolution, frame rate, audio channels, or
  1457. * sample rate. If we have to go higher to make a match, we will. But if
  1458. * the user selects 480p, for example, we don't want to surprise them with
  1459. * 720p and waste bandwidth if there's another choice available to us.
  1460. *
  1461. * @param {number} outputValue
  1462. * @param {number} bestValue
  1463. * @param {number} candidateValue
  1464. * @return {shaka.util.PeriodCombiner.BetterOrWorse}
  1465. */
  1466. static compareClosestPreferLower(outputValue, bestValue, candidateValue) {
  1467. const {BETTER, EQUAL, WORSE} = shaka.util.PeriodCombiner.BetterOrWorse;
  1468. // If one is the exact match for the output value, and the other isn't,
  1469. // prefer the one that is the exact match.
  1470. if (bestValue == outputValue && outputValue != candidateValue) {
  1471. return WORSE;
  1472. } else if (candidateValue == outputValue && outputValue != bestValue) {
  1473. return BETTER;
  1474. }
  1475. if (bestValue > outputValue) {
  1476. if (candidateValue <= outputValue) {
  1477. // Any smaller-or-equal-to-output value is preferable to a
  1478. // bigger-than-output value.
  1479. return BETTER;
  1480. }
  1481. // Both "best" and "candidate" are greater than the output. Take
  1482. // whichever is closer.
  1483. if (candidateValue - outputValue < bestValue - outputValue) {
  1484. return BETTER;
  1485. } else if (candidateValue - outputValue > bestValue - outputValue) {
  1486. return WORSE;
  1487. }
  1488. } else {
  1489. // The "best" so far is less than or equal to the output. If the
  1490. // candidate is bigger than the output, we don't want it.
  1491. if (candidateValue > outputValue) {
  1492. return WORSE;
  1493. }
  1494. // Both "best" and "candidate" are less than or equal to the output.
  1495. // Take whichever is closer.
  1496. if (outputValue - candidateValue < outputValue - bestValue) {
  1497. return BETTER;
  1498. } else if (outputValue - candidateValue > outputValue - bestValue) {
  1499. return WORSE;
  1500. }
  1501. }
  1502. return EQUAL;
  1503. }
  1504. /**
  1505. * @param {number} outputValue
  1506. * @param {number} bestValue
  1507. * @param {number} candidateValue
  1508. * @return {shaka.util.PeriodCombiner.BetterOrWorse}
  1509. * @private
  1510. */
  1511. static compareClosestPreferMinimalAbsDiff_(
  1512. outputValue, bestValue, candidateValue) {
  1513. const {BETTER, EQUAL, WORSE} = shaka.util.PeriodCombiner.BetterOrWorse;
  1514. const absDiffBest = Math.abs(outputValue - bestValue);
  1515. const absDiffCandidate = Math.abs(outputValue - candidateValue);
  1516. if (absDiffCandidate < absDiffBest) {
  1517. return BETTER;
  1518. } else if (absDiffBest < absDiffCandidate) {
  1519. return WORSE;
  1520. }
  1521. return EQUAL;
  1522. }
  1523. /**
  1524. * @param {T} stream
  1525. * @return {boolean}
  1526. * @template T
  1527. * Accepts either a StreamDB or Stream type.
  1528. * @private
  1529. */
  1530. static isDummy_(stream) {
  1531. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1532. switch (stream.type) {
  1533. case ContentType.TEXT:
  1534. return !stream.language;
  1535. case ContentType.IMAGE:
  1536. return !stream.tilesLayout;
  1537. default:
  1538. return false;
  1539. }
  1540. }
  1541. /**
  1542. * @param {T} v
  1543. * @return {string}
  1544. * @template T
  1545. * Accepts either a StreamDB or Stream type.
  1546. * @private
  1547. */
  1548. static generateVideoKey_(v) {
  1549. return shaka.util.PeriodCombiner.generateKey_([
  1550. v.fastSwitching,
  1551. v.width,
  1552. v.frameRate,
  1553. v.codecs,
  1554. v.mimeType,
  1555. v.label,
  1556. v.roles,
  1557. v.closedCaptions ? Array.from(v.closedCaptions.entries()) : null,
  1558. v.bandwidth,
  1559. ]);
  1560. }
  1561. /**
  1562. * @param {T} a
  1563. * @return {string}
  1564. * @template T
  1565. * Accepts either a StreamDB or Stream type.
  1566. * @private
  1567. */
  1568. static generateAudioKey_(a) {
  1569. return shaka.util.PeriodCombiner.generateKey_([
  1570. a.fastSwitching,
  1571. a.channelsCount,
  1572. a.language,
  1573. a.bandwidth,
  1574. a.label,
  1575. a.codecs,
  1576. a.mimeType,
  1577. a.roles,
  1578. a.audioSamplingRate,
  1579. a.primary,
  1580. ]);
  1581. }
  1582. /**
  1583. * @param {T} t
  1584. * @return {string}
  1585. * @template T
  1586. * Accepts either a StreamDB or Stream type.
  1587. * @private
  1588. */
  1589. static generateTextKey_(t) {
  1590. return shaka.util.PeriodCombiner.generateKey_([
  1591. t.language,
  1592. t.label,
  1593. t.codecs,
  1594. t.mimeType,
  1595. t.bandwidth,
  1596. t.roles,
  1597. ]);
  1598. }
  1599. /**
  1600. * @param {T} i
  1601. * @return {string}
  1602. * @template T
  1603. * Accepts either a StreamDB or Stream type.
  1604. * @private
  1605. */
  1606. static generateImageKey_(i) {
  1607. return shaka.util.PeriodCombiner.generateKey_([
  1608. i.width,
  1609. i.codecs,
  1610. i.mimeType,
  1611. ]);
  1612. }
  1613. /**
  1614. * @param {!Array<*>} values
  1615. * @return {string}
  1616. * @private
  1617. */
  1618. static generateKey_(values) {
  1619. return JSON.stringify(values);
  1620. }
  1621. };
  1622. /**
  1623. * @enum {number}
  1624. */
  1625. shaka.util.PeriodCombiner.BetterOrWorse = {
  1626. BETTER: 1,
  1627. EQUAL: 0,
  1628. WORSE: -1,
  1629. };
  1630. /**
  1631. * @private {Map<string, string>}
  1632. */
  1633. shaka.util.PeriodCombiner.memoizedCodecs = new Map();