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