shaka-player 4.7.2 → 4.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +3 -1
  2. package/dist/deps.js +1 -1
  3. package/dist/locales.js +31 -31
  4. package/dist/shaka-player.compiled.d.ts +4 -0
  5. package/dist/shaka-player.compiled.debug.d.ts +4 -0
  6. package/dist/shaka-player.compiled.debug.externs.js +5 -0
  7. package/dist/shaka-player.compiled.debug.js +314 -311
  8. package/dist/shaka-player.compiled.debug.map +3 -3
  9. package/dist/shaka-player.compiled.externs.js +5 -0
  10. package/dist/shaka-player.compiled.js +1120 -1121
  11. package/dist/shaka-player.compiled.map +3 -3
  12. package/dist/shaka-player.ui.d.ts +4 -0
  13. package/dist/shaka-player.ui.debug.d.ts +4 -0
  14. package/dist/shaka-player.ui.debug.externs.js +5 -0
  15. package/dist/shaka-player.ui.debug.js +339 -334
  16. package/dist/shaka-player.ui.debug.map +3 -3
  17. package/dist/shaka-player.ui.externs.js +5 -0
  18. package/dist/shaka-player.ui.js +1207 -1208
  19. package/dist/shaka-player.ui.map +3 -3
  20. package/lib/ads/client_side_ad.js +13 -2
  21. package/lib/ads/client_side_ad_manager.js +8 -5
  22. package/lib/cea/cea608_memory.js +1 -1
  23. package/lib/cea/cea_decoder.js +1 -1
  24. package/lib/dash/dash_parser.js +0 -1
  25. package/lib/hls/hls_parser.js +27 -2
  26. package/lib/media/media_source_engine.js +14 -9
  27. package/lib/media/presentation_timeline.js +11 -0
  28. package/lib/net/networking_utils.js +3 -1
  29. package/lib/offline/session_deleter.js +0 -8
  30. package/lib/player.js +12 -6
  31. package/lib/text/vtt_text_parser.js +5 -0
  32. package/lib/util/periods.js +284 -230
  33. package/package.json +1 -1
  34. package/ui/hidden_seek_button.js +5 -1
  35. package/ui/play_button.js +17 -0
  36. package/ui/small_play_button.js +2 -2
@@ -11,12 +11,10 @@ goog.require('shaka.log');
11
11
  goog.require('shaka.media.DrmEngine');
12
12
  goog.require('shaka.media.MetaSegmentIndex');
13
13
  goog.require('shaka.media.SegmentIndex');
14
- goog.require('shaka.util.ArrayUtils');
15
14
  goog.require('shaka.util.Error');
16
15
  goog.require('shaka.util.IReleasable');
17
16
  goog.require('shaka.util.LanguageUtils');
18
17
  goog.require('shaka.util.ManifestParserUtils');
19
- goog.require('shaka.util.MapUtils');
20
18
  goog.require('shaka.util.MimeUtils');
21
19
 
22
20
  /**
@@ -107,27 +105,52 @@ shaka.util.PeriodCombiner = class {
107
105
  }
108
106
 
109
107
  /**
110
- * Returns an object that contains arrays of streams by type
111
- * @param {!Array.<shaka.extern.Period>} periods
112
- * @return {{
113
- * audioStreamsPerPeriod: !Array.<!Array.<shaka.extern.Stream>>,
114
- * videoStreamsPerPeriod: !Array.<!Array.<shaka.extern.Stream>>,
115
- * textStreamsPerPeriod: !Array.<!Array.<shaka.extern.Stream>>,
116
- * imageStreamsPerPeriod: !Array.<!Array.<shaka.extern.Stream>>
117
- * }}
108
+ * Returns an object that contains arrays of streams by type
109
+ * @param {!Array<shaka.extern.Period>} periods
110
+ * @param {boolean} addDummy
111
+ * @return {{
112
+ * audioStreamsPerPeriod: !Array<!Map<string, shaka.extern.Stream>>,
113
+ * videoStreamsPerPeriod: !Array<!Map<string, shaka.extern.Stream>>,
114
+ * textStreamsPerPeriod: !Array<!Map<string, shaka.extern.Stream>>,
115
+ * imageStreamsPerPeriod: !Array<!Map<string, shaka.extern.Stream>>
116
+ * }}
118
117
  * @private
119
- */
120
- getStreamsPerPeriod_(periods) {
118
+ */
119
+ getStreamsPerPeriod_(periods, addDummy) {
120
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
121
+ const PeriodCombiner = shaka.util.PeriodCombiner;
122
+
121
123
  const audioStreamsPerPeriod = [];
122
124
  const videoStreamsPerPeriod = [];
123
125
  const textStreamsPerPeriod = [];
124
126
  const imageStreamsPerPeriod = [];
125
127
 
126
128
  for (const period of periods) {
127
- audioStreamsPerPeriod.push(period.audioStreams);
128
- videoStreamsPerPeriod.push(period.videoStreams);
129
- textStreamsPerPeriod.push(period.textStreams);
130
- imageStreamsPerPeriod.push(period.imageStreams);
129
+ const audioMap = new Map(period.audioStreams.map((s) =>
130
+ [PeriodCombiner.generateAudioKey_(s), s]));
131
+ const videoMap = new Map(period.videoStreams.map((s) =>
132
+ [PeriodCombiner.generateVideoKey_(s), s]));
133
+ const textMap = new Map(period.textStreams.map((s) =>
134
+ [PeriodCombiner.generateTextKey_(s), s]));
135
+ const imageMap = new Map(period.imageStreams.map((s) =>
136
+ [PeriodCombiner.generateImageKey_(s), s]));
137
+
138
+ // It's okay to have a period with no text or images, but our algorithm
139
+ // fails on any period without matching streams. So we add dummy streams
140
+ // to each period. Since we combine text streams by language and image
141
+ // streams by resolution, we might need a dummy even in periods with these
142
+ // streams already.
143
+ if (addDummy) {
144
+ const dummyText = PeriodCombiner.dummyStream_(ContentType.TEXT);
145
+ textMap.set(PeriodCombiner.generateTextKey_(dummyText), dummyText);
146
+ const dummyImage = PeriodCombiner.dummyStream_(ContentType.IMAGE);
147
+ imageMap.set(PeriodCombiner.generateImageKey_(dummyImage), dummyImage);
148
+ }
149
+
150
+ audioStreamsPerPeriod.push(audioMap);
151
+ videoStreamsPerPeriod.push(videoMap);
152
+ textStreamsPerPeriod.push(textMap);
153
+ imageStreamsPerPeriod.push(imageMap);
131
154
  }
132
155
  return {
133
156
  audioStreamsPerPeriod,
@@ -147,17 +170,22 @@ shaka.util.PeriodCombiner = class {
147
170
  async combinePeriods(periods, isDynamic) {
148
171
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
149
172
 
150
- shaka.util.PeriodCombiner.filterOutDuplicates_(periods);
151
-
152
173
  // Optimization: for single-period VOD, do nothing. This makes sure
153
174
  // single-period DASH content will be 100% accurately represented in the
154
175
  // output.
155
176
  if (!isDynamic && periods.length == 1) {
156
- const firstPeriod = periods[0];
157
- this.audioStreams_ = firstPeriod.audioStreams;
158
- this.videoStreams_ = firstPeriod.videoStreams;
159
- this.textStreams_ = firstPeriod.textStreams;
160
- this.imageStreams_ = firstPeriod.imageStreams;
177
+ // We need to filter out duplicates, so call getStreamsPerPeriod()
178
+ // so it will do that by usage of Map.
179
+ const {
180
+ audioStreamsPerPeriod,
181
+ videoStreamsPerPeriod,
182
+ textStreamsPerPeriod,
183
+ imageStreamsPerPeriod,
184
+ } = this.getStreamsPerPeriod_(periods, /* addDummy= */ false);
185
+ this.audioStreams_ = Array.from(audioStreamsPerPeriod[0].values());
186
+ this.videoStreams_ = Array.from(videoStreamsPerPeriod[0].values());
187
+ this.textStreams_ = Array.from(textStreamsPerPeriod[0].values());
188
+ this.imageStreams_ = Array.from(imageStreamsPerPeriod[0].values());
161
189
  } else {
162
190
  // Find the first period we haven't seen before. Tag all the periods we
163
191
  // see now as "used".
@@ -187,21 +215,7 @@ shaka.util.PeriodCombiner = class {
187
215
  videoStreamsPerPeriod,
188
216
  textStreamsPerPeriod,
189
217
  imageStreamsPerPeriod,
190
- } = this.getStreamsPerPeriod_(periods);
191
-
192
- // It's okay to have a period with no text or images, but our algorithm
193
- // fails on any period without matching streams. So we add dummy streams
194
- // to each period. Since we combine text streams by language and image
195
- // streams by resolution, we might need a dummy even in periods with these
196
- // streams already.
197
- for (const textStreams of textStreamsPerPeriod) {
198
- textStreams.push(shaka.util.PeriodCombiner.dummyStream_(
199
- ContentType.TEXT));
200
- }
201
- for (const imageStreams of imageStreamsPerPeriod) {
202
- imageStreams.push(shaka.util.PeriodCombiner.dummyStream_(
203
- ContentType.IMAGE));
204
- }
218
+ } = this.getStreamsPerPeriod_(periods, /* addDummy= */ true);
205
219
 
206
220
  await Promise.all([
207
221
  this.combine_(
@@ -290,92 +304,6 @@ shaka.util.PeriodCombiner = class {
290
304
  this.variants_ = variants;
291
305
  }
292
306
 
293
- /**
294
- * @param {!Array<shaka.extern.Period>} periods
295
- * @private
296
- */
297
- static filterOutDuplicates_(periods) {
298
- const PeriodCombiner = shaka.util.PeriodCombiner;
299
- const ArrayUtils = shaka.util.ArrayUtils;
300
- const MapUtils = shaka.util.MapUtils;
301
-
302
- for (const period of periods) {
303
- // Two video streams are considered to be duplicates of
304
- // one another if their ids are different, but all the other
305
- // information is the same.
306
- period.videoStreams = PeriodCombiner.filterOutStreamDuplicates_(
307
- period.videoStreams, (v1, v2) => {
308
- return v1.id !== v2.id &&
309
- v1.fastSwitching == v2.fastSwitching &&
310
- v1.width === v2.width &&
311
- v1.frameRate === v2.frameRate &&
312
- v1.codecs === v2.codecs &&
313
- v1.mimeType === v2.mimeType &&
314
- v1.label === v2.label &&
315
- ArrayUtils.hasSameElements(v1.roles, v2.roles) &&
316
- MapUtils.hasSameElements(v1.closedCaptions, v2.closedCaptions) &&
317
- v1.bandwidth === v2.bandwidth;
318
- });
319
- // Two audio streams are considered to be duplicates of
320
- // one another if their ids are different, but all the other
321
- // information is the same.
322
- period.audioStreams = PeriodCombiner.filterOutStreamDuplicates_(
323
- period.audioStreams, (a1, a2) => {
324
- return a1.id !== a2.id &&
325
- a1.fastSwitching == a2.fastSwitching &&
326
- a1.channelsCount === a2.channelsCount &&
327
- a1.language === a2.language &&
328
- a1.bandwidth === a2.bandwidth &&
329
- a1.label === a2.label &&
330
- a1.codecs === a2.codecs &&
331
- a1.mimeType === a2.mimeType &&
332
- ArrayUtils.hasSameElements(a1.roles, a2.roles) &&
333
- a1.audioSamplingRate === a2.audioSamplingRate &&
334
- a1.primary === a2.primary;
335
- });
336
- // Two text streams are considered to be duplicates of
337
- // one another if their ids are different, but all the other
338
- // information is the same.
339
- period.textStreams = PeriodCombiner.filterOutStreamDuplicates_(
340
- period.textStreams, (t1, t2) => {
341
- return t1.id !== t2.id &&
342
- t1.language === t2.language &&
343
- t1.label === t2.label &&
344
- t1.codecs === t2.codecs &&
345
- t1.mimeType === t2.mimeType &&
346
- t1.bandwidth === t2.bandwidth &&
347
- ArrayUtils.hasSameElements(t1.roles, t2.roles);
348
- });
349
- // Two image streams are considered to be duplicates of
350
- // one another if their ids are different, but all the other
351
- // information is the same.
352
- period.imageStreams = PeriodCombiner.filterOutStreamDuplicates_(
353
- period.imageStreams, (i1, i2) => {
354
- return i1.id !== i2.id &&
355
- i1.width === i2.width &&
356
- i1.codecs === i2.codecs &&
357
- i1.mimeType === i2.mimeType;
358
- });
359
- }
360
- }
361
-
362
- /**
363
- * @param {!Array<shaka.extern.Stream>} streams
364
- * @param {function(
365
- * shaka.extern.Stream, shaka.extern.Stream): boolean} isDuplicate
366
- * @return {!Array<shaka.extern.Stream>}
367
- * @private
368
- */
369
- static filterOutStreamDuplicates_(streams, isDuplicate) {
370
- const filteredStreams = [];
371
- for (const s1 of streams) {
372
- const duplicate = filteredStreams.some((s2) => isDuplicate(s1, s2));
373
- if (!duplicate) {
374
- filteredStreams.push(s1);
375
- }
376
- }
377
- return filteredStreams;
378
- }
379
307
 
380
308
  /**
381
309
  * Stitch together DB streams across periods, taking a mix of stream types.
@@ -389,6 +317,7 @@ shaka.util.PeriodCombiner = class {
389
317
  */
390
318
  static async combineDbStreams(streamDbsPerPeriod) {
391
319
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
320
+ const PeriodCombiner = shaka.util.PeriodCombiner;
392
321
 
393
322
  // Optimization: for single-period content, do nothing. This makes sure
394
323
  // single-period DASH or any HLS content stored offline will be 100%
@@ -398,13 +327,21 @@ shaka.util.PeriodCombiner = class {
398
327
  }
399
328
 
400
329
  const audioStreamDbsPerPeriod = streamDbsPerPeriod.map(
401
- (streams) => streams.filter((s) => s.type == ContentType.AUDIO));
330
+ (streams) => new Map(streams
331
+ .filter((s) => s.type === ContentType.AUDIO)
332
+ .map((s) => [PeriodCombiner.generateAudioKey_(s), s])));
402
333
  const videoStreamDbsPerPeriod = streamDbsPerPeriod.map(
403
- (streams) => streams.filter((s) => s.type == ContentType.VIDEO));
334
+ (streams) => new Map(streams
335
+ .filter((s) => s.type === ContentType.VIDEO)
336
+ .map((s) => [PeriodCombiner.generateVideoKey_(s), s])));
404
337
  const textStreamDbsPerPeriod = streamDbsPerPeriod.map(
405
- (streams) => streams.filter((s) => s.type == ContentType.TEXT));
338
+ (streams) => new Map(streams
339
+ .filter((s) => s.type === ContentType.TEXT)
340
+ .map((s) => [PeriodCombiner.generateTextKey_(s), s])));
406
341
  const imageStreamDbsPerPeriod = streamDbsPerPeriod.map(
407
- (streams) => streams.filter((s) => s.type == ContentType.IMAGE));
342
+ (streams) => new Map(streams
343
+ .filter((s) => s.type === ContentType.IMAGE)
344
+ .map((s) => [PeriodCombiner.generateImageKey_(s), s])));
408
345
 
409
346
  // It's okay to have a period with no text or images, but our algorithm
410
347
  // fails on any period without matching streams. So we add dummy streams to
@@ -412,12 +349,12 @@ shaka.util.PeriodCombiner = class {
412
349
  // by resolution, we might need a dummy even in periods with these streams
413
350
  // already.
414
351
  for (const textStreams of textStreamDbsPerPeriod) {
415
- textStreams.push(shaka.util.PeriodCombiner.dummyStreamDB_(
416
- ContentType.TEXT));
352
+ const dummy = PeriodCombiner.dummyStreamDB_(ContentType.TEXT);
353
+ textStreams.set(PeriodCombiner.generateTextKey_(dummy), dummy);
417
354
  }
418
355
  for (const imageStreams of imageStreamDbsPerPeriod) {
419
- imageStreams.push(shaka.util.PeriodCombiner.dummyStreamDB_(
420
- ContentType.IMAGE));
356
+ const dummy = PeriodCombiner.dummyStreamDB_(ContentType.IMAGE);
357
+ imageStreams.set(PeriodCombiner.generateImageKey_(dummy), dummy);
421
358
  }
422
359
 
423
360
  const periodCombiner = new shaka.util.PeriodCombiner();
@@ -488,7 +425,7 @@ shaka.util.PeriodCombiner = class {
488
425
  *
489
426
  * @param {!Array.<T>} outputStreams A list of existing output streams, to
490
427
  * facilitate updates for live DASH content. Will be modified and returned.
491
- * @param {!Array.<!Array.<T>>} streamsPerPeriod A list of lists of Streams
428
+ * @param {!Array<!Map<string, T>>} streamsPerPeriod A list of maps of Streams
492
429
  * from each period.
493
430
  * @param {number} firstNewPeriodIndex An index into streamsPerPeriod which
494
431
  * represents the first new period that hasn't been processed yet.
@@ -506,17 +443,14 @@ shaka.util.PeriodCombiner = class {
506
443
  */
507
444
  async combine_(
508
445
  outputStreams, streamsPerPeriod, firstNewPeriodIndex, clone, concat) {
509
- const ContentType = shaka.util.ManifestParserUtils.ContentType;
510
-
511
446
  const unusedStreamsPerPeriod = [];
512
- for (let i = 0; i < streamsPerPeriod.length; i++) {
513
- if (i >= firstNewPeriodIndex) {
514
- // This periods streams are all new.
515
- unusedStreamsPerPeriod.push(new Set(streamsPerPeriod[i]));
516
- } else {
517
- // This period's streams have all been used already.
518
- unusedStreamsPerPeriod.push(new Set());
519
- }
447
+ for (let i = 0; i < firstNewPeriodIndex; i++) {
448
+ // This period's streams have all been used already.
449
+ unusedStreamsPerPeriod.push(new Set());
450
+ }
451
+ for (let i = firstNewPeriodIndex; i < streamsPerPeriod.length; i++) {
452
+ // This periods streams are all new.
453
+ unusedStreamsPerPeriod.push(new Set(streamsPerPeriod[i].values()));
520
454
  }
521
455
 
522
456
  // First, extend all existing output Streams into the new periods.
@@ -556,10 +490,7 @@ shaka.util.PeriodCombiner = class {
556
490
 
557
491
  for (const unusedStreams of unusedStreamsPerPeriod) {
558
492
  for (const stream of unusedStreams) {
559
- const isDummyText = stream.type == ContentType.TEXT && !stream.language;
560
- const isDummyImage = stream.type == ContentType.IMAGE &&
561
- !stream.tilesLayout;
562
- if (isDummyText || isDummyImage) {
493
+ if (shaka.util.PeriodCombiner.isDummy_(stream)) {
563
494
  // This is one of our dummy streams, so ignore it. We may not use
564
495
  // them all, and that's fine.
565
496
  continue;
@@ -590,7 +521,7 @@ shaka.util.PeriodCombiner = class {
590
521
  /**
591
522
  * @param {T} outputStream An existing output stream which needs to be
592
523
  * extended into new periods.
593
- * @param {!Array.<!Array.<T>>} streamsPerPeriod A list of lists of Streams
524
+ * @param {!Array<!Map<string, T>>} streamsPerPeriod A list of maps of Streams
594
525
  * from each period.
595
526
  * @param {number} firstNewPeriodIndex An index into streamsPerPeriod which
596
527
  * represents the first new period that hasn't been processed yet.
@@ -689,7 +620,7 @@ shaka.util.PeriodCombiner = class {
689
620
  * Templatized to handle both DASH Streams and offline StreamDBs.
690
621
  *
691
622
  * @param {T} stream An input stream on which to base the output stream.
692
- * @param {!Array.<!Array.<T>>} streamsPerPeriod A list of lists of Streams
623
+ * @param {!Array<!Map<string, T>>} streamsPerPeriod A list of maps of Streams
693
624
  * from each period.
694
625
  * @param {function(T):T} clone Make a clone of an input stream.
695
626
  * @param {function(T, T)} concat Concatenate the second stream onto the end
@@ -707,6 +638,11 @@ shaka.util.PeriodCombiner = class {
707
638
  */
708
639
  createNewOutputStream_(
709
640
  stream, streamsPerPeriod, clone, concat, unusedStreamsPerPeriod) {
641
+ // Check do we want to create output stream from dummy stream
642
+ // and if so, return quickly.
643
+ if (shaka.util.PeriodCombiner.isDummy_(stream)) {
644
+ return null;
645
+ }
710
646
  // Start by cloning the stream without segments, key IDs, etc.
711
647
  const outputStream = clone(stream);
712
648
 
@@ -790,9 +726,32 @@ shaka.util.PeriodCombiner = class {
790
726
  for (const codec of match.codecs.split(',')) {
791
727
  codecs.add(codec);
792
728
  }
793
- outputStream.codecs = Array.from(codecs).join(',');
729
+ const PeriodCombiner = shaka.util.PeriodCombiner;
730
+ outputStream.codecs = PeriodCombiner.filterDuplicateCodecs_(
731
+ Array.from(codecs)).join(',');
732
+ }
733
+ }
734
+ }
735
+
736
+ /**
737
+ * @param {!Array.<string>} codecs
738
+ * @return {!Array.<string>} codecs
739
+ * @private
740
+ */
741
+ static filterDuplicateCodecs_(codecs) {
742
+ // Filter out duplicate codecs.
743
+ const seen = new Set();
744
+ const ret = [];
745
+ for (const codec of codecs) {
746
+ const shortCodec = shaka.util.MimeUtils.getCodecBase(codec);
747
+ if (!seen.has(shortCodec)) {
748
+ ret.push(codec);
749
+ seen.add(shortCodec);
750
+ } else {
751
+ shaka.log.debug('Ignoring duplicate codec');
794
752
  }
795
753
  }
754
+ return ret;
796
755
  }
797
756
 
798
757
  /**
@@ -826,6 +785,8 @@ shaka.util.PeriodCombiner = class {
826
785
  }
827
786
  };
828
787
 
788
+ // Clone roles array so this output stream can own it.
789
+ clone.roles = clone.roles.slice();
829
790
  clone.segmentIndex = null;
830
791
  clone.emsgSchemeIdUris = [];
831
792
  clone.keyIds = new Set();
@@ -847,6 +808,8 @@ shaka.util.PeriodCombiner = class {
847
808
  const clone = /** @type {shaka.extern.StreamDB} */(Object.assign(
848
809
  {}, streamDb));
849
810
 
811
+ // Clone roles array so this output stream can own it.
812
+ clone.roles = clone.roles.slice();
850
813
  // These are wiped out now and rebuilt later from the various per-period
851
814
  // streams that match this output.
852
815
  clone.keyIds = new Set();
@@ -871,7 +834,17 @@ shaka.util.PeriodCombiner = class {
871
834
  // the line.
872
835
 
873
836
  // Combine arrays, keeping only the unique elements
874
- const combineArrays = (a, b) => Array.from(new Set(a.concat(b)));
837
+ const combineArrays = (output, input) => {
838
+ if (!output) {
839
+ output = [];
840
+ }
841
+ for (const item of input) {
842
+ if (!output.includes(item)) {
843
+ output.push(item);
844
+ }
845
+ }
846
+ return output;
847
+ };
875
848
  output.roles = combineArrays(output.roles, input.roles);
876
849
 
877
850
  if (input.emsgSchemeIdUris) {
@@ -879,8 +852,9 @@ shaka.util.PeriodCombiner = class {
879
852
  output.emsgSchemeIdUris, input.emsgSchemeIdUris);
880
853
  }
881
854
 
882
- const combineSets = (a, b) => new Set([...a, ...b]);
883
- output.keyIds = combineSets(output.keyIds, input.keyIds);
855
+ for (const keyId of input.keyIds) {
856
+ output.keyIds.add(keyId);
857
+ }
884
858
 
885
859
  if (output.originalId == null) {
886
860
  output.originalId = input.originalId;
@@ -952,11 +926,22 @@ shaka.util.PeriodCombiner = class {
952
926
  */
953
927
  static concatenateStreamDBs_(output, input) {
954
928
  // Combine arrays, keeping only the unique elements
955
- const combineArrays = (a, b) => Array.from(new Set(a.concat(b)));
929
+ const combineArrays = (output, input) => {
930
+ if (!output) {
931
+ output = [];
932
+ }
933
+ for (const item of input) {
934
+ if (!output.includes(item)) {
935
+ output.push(item);
936
+ }
937
+ }
938
+ return output;
939
+ };
956
940
  output.roles = combineArrays(output.roles, input.roles);
957
941
 
958
- const combineSets = (a, b) => new Set([...a, ...b]);
959
- output.keyIds = combineSets(output.keyIds, input.keyIds);
942
+ for (const keyId of input.keyIds) {
943
+ output.keyIds.add(keyId);
944
+ }
960
945
 
961
946
  // The output is encrypted if any input was encrypted.
962
947
  output.encrypted = output.encrypted && input.encrypted;
@@ -978,7 +963,7 @@ shaka.util.PeriodCombiner = class {
978
963
  /**
979
964
  * Finds streams in all periods which match the output stream.
980
965
  *
981
- * @param {!Array.<!Array.<T>>} streamsPerPeriod
966
+ * @param {!Array<!Map<string, T>>} streamsPerPeriod
982
967
  * @param {T} outputStream
983
968
  *
984
969
  * @template T
@@ -1001,7 +986,7 @@ shaka.util.PeriodCombiner = class {
1001
986
  /**
1002
987
  * Find the best match for the output stream.
1003
988
  *
1004
- * @param {!Array.<T>} streams
989
+ * @param {!Map<string, T>} streams
1005
990
  * @param {T} outputStream
1006
991
  * @return {?T} Returns null if no match can be found.
1007
992
  *
@@ -1011,29 +996,42 @@ shaka.util.PeriodCombiner = class {
1011
996
  * @private
1012
997
  */
1013
998
  findBestMatchInPeriod_(streams, outputStream) {
1014
- const areCompatible = {
1015
- 'audio': (os, s) => this.areAVStreamsCompatible_(os, s),
1016
- 'video': (os, s) => this.areAVStreamsCompatible_(os, s),
1017
- 'text': shaka.util.PeriodCombiner.areTextStreamsCompatible_,
1018
- 'image': shaka.util.PeriodCombiner.areImageStreamsCompatible_,
1019
- }[outputStream.type];
1020
-
1021
- const isBetterMatch = {
1022
- 'audio': shaka.util.PeriodCombiner.isAudioStreamBetterMatch_,
1023
- 'video': shaka.util.PeriodCombiner.isVideoStreamBetterMatch_,
1024
- 'text': shaka.util.PeriodCombiner.isTextStreamBetterMatch_,
1025
- 'image': shaka.util.PeriodCombiner.isImageStreamBetterMatch_,
999
+ const getKey = {
1000
+ 'audio': shaka.util.PeriodCombiner.generateAudioKey_,
1001
+ 'video': shaka.util.PeriodCombiner.generateVideoKey_,
1002
+ 'text': shaka.util.PeriodCombiner.generateTextKey_,
1003
+ 'image': shaka.util.PeriodCombiner.generateImageKey_,
1026
1004
  }[outputStream.type];
1027
1005
 
1028
1006
  let best = null;
1007
+ const key = getKey(outputStream);
1008
+ if (streams.has(key)) {
1009
+ // We've found exact match by hashing.
1010
+ best = streams.get(key);
1011
+ } else {
1012
+ // We haven't found exact match, try to find the best one via
1013
+ // linear search.
1014
+ const areCompatible = {
1015
+ 'audio': (os, s) => this.areAVStreamsCompatible_(os, s),
1016
+ 'video': (os, s) => this.areAVStreamsCompatible_(os, s),
1017
+ 'text': shaka.util.PeriodCombiner.areTextStreamsCompatible_,
1018
+ 'image': shaka.util.PeriodCombiner.areImageStreamsCompatible_,
1019
+ }[outputStream.type];
1020
+ const isBetterMatch = {
1021
+ 'audio': shaka.util.PeriodCombiner.isAudioStreamBetterMatch_,
1022
+ 'video': shaka.util.PeriodCombiner.isVideoStreamBetterMatch_,
1023
+ 'text': shaka.util.PeriodCombiner.isTextStreamBetterMatch_,
1024
+ 'image': shaka.util.PeriodCombiner.isImageStreamBetterMatch_,
1025
+ }[outputStream.type];
1026
+
1027
+ for (const stream of streams.values()) {
1028
+ if (!areCompatible(outputStream, stream)) {
1029
+ continue;
1030
+ }
1029
1031
 
1030
- for (const stream of streams) {
1031
- if (!areCompatible(outputStream, stream)) {
1032
- continue;
1033
- }
1034
-
1035
- if (!best || isBetterMatch(outputStream, best, stream)) {
1036
- best = stream;
1032
+ if (!best || isBetterMatch(outputStream, best, stream)) {
1033
+ best = stream;
1034
+ }
1037
1035
  }
1038
1036
  }
1039
1037
 
@@ -1128,20 +1126,17 @@ shaka.util.PeriodCombiner = class {
1128
1126
  // For text, we don't care about MIME type or codec. We can always switch
1129
1127
  // between text types.
1130
1128
 
1131
- // The output stream should not be a dummy stream inserted to fill a period
1132
- // gap. So reject any candidate if the output has no language. This would
1133
- // cause findMatchesInAllPeriods_ to return null and this output stream to
1134
- // be skipped (meaning no output streams based on it).
1135
- if (!outputStream.language) {
1136
- return false;
1137
- }
1138
-
1139
1129
  // If the candidate is a dummy, then it is compatible, and we could use it
1140
1130
  // if nothing else matches.
1141
1131
  if (!candidate.language) {
1142
1132
  return true;
1143
1133
  }
1144
1134
 
1135
+ // Forced subtitles should be treated as unique streams
1136
+ if (outputStream.forced !== candidate.forced) {
1137
+ return false;
1138
+ }
1139
+
1145
1140
  const languageRelatedness = LanguageUtils.relatedness(
1146
1141
  outputStream.language, candidate.language);
1147
1142
 
@@ -1170,14 +1165,6 @@ shaka.util.PeriodCombiner = class {
1170
1165
  // For image, we don't care about MIME type. We can always switch
1171
1166
  // between image types.
1172
1167
 
1173
- // The output stream should not be a dummy stream inserted to fill a period
1174
- // gap. So reject any candidate if the output has no tilesLayout. This
1175
- // would cause findMatchesInAllPeriods_ to return null and this output
1176
- // stream to be skipped (meaning no output streams based on it).
1177
- if (!outputStream.tilesLayout) {
1178
- return false;
1179
- }
1180
-
1181
1168
  return true;
1182
1169
  }
1183
1170
 
@@ -1196,13 +1183,6 @@ shaka.util.PeriodCombiner = class {
1196
1183
  const LanguageUtils = shaka.util.LanguageUtils;
1197
1184
  const {BETTER, EQUAL, WORSE} = shaka.util.PeriodCombiner.BetterOrWorse;
1198
1185
 
1199
- // If the output stream was based on the candidate stream, the candidate
1200
- // stream should be considered a better match. We can check this by
1201
- // comparing their ids.
1202
- if (outputStream.id == candidate.id) {
1203
- return true;
1204
- }
1205
-
1206
1186
  // An exact match is better than a non-exact match.
1207
1187
  const bestIsExact = shaka.util.PeriodCombiner.areAVStreamsExactMatch_(
1208
1188
  outputStream, best);
@@ -1215,9 +1195,6 @@ shaka.util.PeriodCombiner = class {
1215
1195
  return true;
1216
1196
  }
1217
1197
 
1218
- // Otherwise, compare the streams' characteristics to determine the best
1219
- // match.
1220
-
1221
1198
  // The most important thing is language. In some cases, we will accept a
1222
1199
  // different language across periods when we must.
1223
1200
  const bestRelatedness = LanguageUtils.relatedness(
@@ -1331,13 +1308,6 @@ shaka.util.PeriodCombiner = class {
1331
1308
  static isVideoStreamBetterMatch_(outputStream, best, candidate) {
1332
1309
  const {BETTER, EQUAL, WORSE} = shaka.util.PeriodCombiner.BetterOrWorse;
1333
1310
 
1334
- // If the output stream was based on the candidate stream, the candidate
1335
- // stream should be considered a better match. We can check this by
1336
- // comparing their ids.
1337
- if (outputStream.id == candidate.id) {
1338
- return true;
1339
- }
1340
-
1341
1311
  // An exact match is better than a non-exact match.
1342
1312
  const bestIsExact = shaka.util.PeriodCombiner.areAVStreamsExactMatch_(
1343
1313
  outputStream, best);
@@ -1350,9 +1320,6 @@ shaka.util.PeriodCombiner = class {
1350
1320
  return true;
1351
1321
  }
1352
1322
 
1353
- // Otherwise, compare the streams' characteristics to determine the best
1354
- // match.
1355
-
1356
1323
  // Take the video with the closest resolution to the output.
1357
1324
  const resolutionBetterOrWorse =
1358
1325
  shaka.util.PeriodCombiner.compareClosestPreferLower(
@@ -1414,16 +1381,6 @@ shaka.util.PeriodCombiner = class {
1414
1381
  static isTextStreamBetterMatch_(outputStream, best, candidate) {
1415
1382
  const LanguageUtils = shaka.util.LanguageUtils;
1416
1383
 
1417
- // If the output stream was based on the candidate stream, the candidate
1418
- // stream should be considered a better match. We can check this by
1419
- // comparing their ids.
1420
- if (outputStream.id == candidate.id) {
1421
- return true;
1422
- }
1423
-
1424
- // Otherwise, compare the streams' characteristics to determine the best
1425
- // match.
1426
-
1427
1384
  // The most important thing is language. In some cases, we will accept a
1428
1385
  // different language across periods when we must.
1429
1386
  const bestRelatedness = LanguageUtils.relatedness(
@@ -1500,13 +1457,6 @@ shaka.util.PeriodCombiner = class {
1500
1457
  static isImageStreamBetterMatch_(outputStream, best, candidate) {
1501
1458
  const {BETTER, EQUAL, WORSE} = shaka.util.PeriodCombiner.BetterOrWorse;
1502
1459
 
1503
- // If the output stream was based on the candidate stream, the candidate
1504
- // stream should be considered a better match. We can check this by
1505
- // comparing their ids.
1506
- if (outputStream.id == candidate.id) {
1507
- return true;
1508
- }
1509
-
1510
1460
  // Take the image with the closest resolution to the output.
1511
1461
  const resolutionBetterOrWorse =
1512
1462
  shaka.util.PeriodCombiner.compareClosestPreferLower(
@@ -1685,6 +1635,110 @@ shaka.util.PeriodCombiner = class {
1685
1635
 
1686
1636
  return EQUAL;
1687
1637
  }
1638
+
1639
+ /**
1640
+ * @param {T} stream
1641
+ * @return {boolean}
1642
+ * @template T
1643
+ * Accepts either a StreamDB or Stream type.
1644
+ * @private
1645
+ */
1646
+ static isDummy_(stream) {
1647
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
1648
+ switch (stream.type) {
1649
+ case ContentType.TEXT:
1650
+ return !stream.language;
1651
+ case ContentType.IMAGE:
1652
+ return !stream.tilesLayout;
1653
+ default:
1654
+ return false;
1655
+ }
1656
+ }
1657
+
1658
+ /**
1659
+ * @param {T} v
1660
+ * @return {string}
1661
+ * @template T
1662
+ * Accepts either a StreamDB or Stream type.
1663
+ * @private
1664
+ */
1665
+ static generateVideoKey_(v) {
1666
+ return shaka.util.PeriodCombiner.generateKey_([
1667
+ v.fastSwitching,
1668
+ v.width,
1669
+ v.frameRate,
1670
+ v.codecs,
1671
+ v.mimeType,
1672
+ v.label,
1673
+ v.roles,
1674
+ v.closedCaptions ? Array.from(v.closedCaptions.entries()) : null,
1675
+ v.bandwidth,
1676
+ ]);
1677
+ }
1678
+
1679
+ /**
1680
+ * @param {T} a
1681
+ * @return {string}
1682
+ * @template T
1683
+ * Accepts either a StreamDB or Stream type.
1684
+ * @private
1685
+ */
1686
+ static generateAudioKey_(a) {
1687
+ return shaka.util.PeriodCombiner.generateKey_([
1688
+ a.fastSwitching,
1689
+ a.channelsCount,
1690
+ a.language,
1691
+ a.bandwidth,
1692
+ a.label,
1693
+ a.codecs,
1694
+ a.mimeType,
1695
+ a.roles,
1696
+ a.audioSamplingRate,
1697
+ a.primary,
1698
+ ]);
1699
+ }
1700
+
1701
+ /**
1702
+ * @param {T} t
1703
+ * @return {string}
1704
+ * @template T
1705
+ * Accepts either a StreamDB or Stream type.
1706
+ * @private
1707
+ */
1708
+ static generateTextKey_(t) {
1709
+ return shaka.util.PeriodCombiner.generateKey_([
1710
+ t.language,
1711
+ t.label,
1712
+ t.codecs,
1713
+ t.mimeType,
1714
+ t.bandwidth,
1715
+ t.roles,
1716
+ ]);
1717
+ }
1718
+
1719
+ /**
1720
+ * @param {T} i
1721
+ * @return {string}
1722
+ * @template T
1723
+ * Accepts either a StreamDB or Stream type.
1724
+ * @private
1725
+ */
1726
+ static generateImageKey_(i) {
1727
+ return shaka.util.PeriodCombiner.generateKey_([
1728
+ i.width,
1729
+ i.codecs,
1730
+ i.mimeType,
1731
+ ]);
1732
+ }
1733
+
1734
+ /**
1735
+ * @param {!Array<*>} values
1736
+ * @return {string}
1737
+ * @private
1738
+ */
1739
+ static generateKey_(values) {
1740
+ return JSON.stringify(values);
1741
+ }
1688
1742
  };
1689
1743
 
1690
1744
  /**