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