shaka-player 3.3.14 → 3.3.16

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.
@@ -19,8 +19,8 @@ goog.require('shaka.util.IReleasable');
19
19
 
20
20
 
21
21
  /**
22
- * @event shaka.ads.AdManager.ADS_LOADED
23
- * @description Fired when an ad has started playing.
22
+ * @event shaka.ads.AdManager.AdsLoadedEvent
23
+ * @description Fired when a sequence of ads has been loaded.
24
24
  * @property {string} type
25
25
  * 'ads-loaded'
26
26
  * @property {number} loadTime
@@ -603,6 +603,11 @@ shaka.media.StreamingEngine = class {
603
603
  * within the presentation timeline.
604
604
  */
605
605
  seeked() {
606
+ if (!this.playerInterface_) {
607
+ // Already destroyed.
608
+ return;
609
+ }
610
+
606
611
  const presentationTime = this.playerInterface_.getPresentationTime();
607
612
  const smallGapLimit = this.config_.smallGapLimit;
608
613
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
@@ -657,7 +662,7 @@ shaka.media.StreamingEngine = class {
657
662
 
658
663
  /**
659
664
  * Clear the buffer for a given stream. Unlike clearBuffer_, this will handle
660
- * cases where a MediaState is performing an update. After this runs, every
665
+ * cases where a MediaState is performing an update. After this runs, the
661
666
  * MediaState will have a pending update.
662
667
  * @param {!shaka.media.StreamingEngine.MediaState_} mediaState
663
668
  * @private
@@ -772,9 +777,9 @@ shaka.media.StreamingEngine = class {
772
777
  for (const type of streamsByType.keys()) {
773
778
  const stream = streamsByType.get(type);
774
779
  if (!this.mediaStates_.has(type)) {
775
- const state = this.createMediaState_(stream);
776
- this.mediaStates_.set(type, state);
777
- this.scheduleUpdate_(state, 0);
780
+ const mediaState = this.createMediaState_(stream);
781
+ this.mediaStates_.set(type, mediaState);
782
+ this.scheduleUpdate_(mediaState, 0);
778
783
  }
779
784
  }
780
785
  }
@@ -884,7 +889,7 @@ shaka.media.StreamingEngine = class {
884
889
  'mediastate.stream should not have segmentIndex yet.');
885
890
  thisStream.closeSegmentIndex();
886
891
  }
887
- if (mediaState.updateTimer == null) {
892
+ if (!mediaState.performingUpdate && !mediaState.updateTimer) {
888
893
  this.scheduleUpdate_(mediaState, 0);
889
894
  }
890
895
  return;
@@ -1352,7 +1357,7 @@ shaka.media.StreamingEngine = class {
1352
1357
  // If the network slows down, abort the current fetch request and start
1353
1358
  // a new one, and ignore the error message.
1354
1359
  mediaState.performingUpdate = false;
1355
- mediaState.updateTimer = null;
1360
+ this.cancelUpdate_(mediaState);
1356
1361
  this.scheduleUpdate_(mediaState, 0);
1357
1362
  } else if (mediaState.type == ContentType.TEXT &&
1358
1363
  this.config_.ignoreTextStreamFailures) {
@@ -1397,7 +1402,10 @@ shaka.media.StreamingEngine = class {
1397
1402
 
1398
1403
  for (const mediaState of this.mediaStates_.values()) {
1399
1404
  const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
1400
- if (mediaState.hasError) {
1405
+ // Only schedule an update if it has an error, but it's not mid-update
1406
+ // and there is not already an update scheduled.
1407
+ if (mediaState.hasError && !mediaState.performingUpdate &&
1408
+ !mediaState.updateTimer) {
1401
1409
  shaka.log.info(logPrefix, 'Retrying after failure...');
1402
1410
  mediaState.hasError = false;
1403
1411
  this.scheduleUpdate_(mediaState, delaySeconds);
@@ -1880,7 +1888,11 @@ shaka.media.StreamingEngine = class {
1880
1888
  shaka.log.debug(logPrefix, 'cleared buffer');
1881
1889
  mediaState.clearingBuffer = false;
1882
1890
  mediaState.endOfStream = false;
1883
- this.scheduleUpdate_(mediaState, 0);
1891
+ // Since the clear operation was async, check to make sure we're not doing
1892
+ // another update and we don't have one scheduled yet.
1893
+ if (!mediaState.performingUpdate && !mediaState.updateTimer) {
1894
+ this.scheduleUpdate_(mediaState, 0);
1895
+ }
1884
1896
  }
1885
1897
 
1886
1898
 
package/lib/player.js CHANGED
@@ -1419,6 +1419,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1419
1419
  has.startTime = null;
1420
1420
  has.uri = null;
1421
1421
 
1422
+ // Release the region timeline, which is created when parsing the manifest.
1423
+ if (this.regionTimeline_) {
1424
+ this.regionTimeline_.release();
1425
+ this.regionTimeline_ = null;
1426
+ }
1427
+
1422
1428
  // In most cases we should have a media element. The one exception would
1423
1429
  // be if there was an error and we, by chance, did not have a media element.
1424
1430
  if (has.mediaElement) {
@@ -4116,16 +4122,25 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4116
4122
  return [];
4117
4123
  }
4118
4124
  const chapters = [];
4125
+ const uniqueChapters = new Set();
4119
4126
  for (const chaptersTrack of chaptersTracksWithLanguage) {
4120
4127
  if (chaptersTrack && chaptersTrack.cues) {
4121
4128
  for (const cue of chaptersTrack.cues) {
4129
+ let id = cue.id;
4130
+ if (!id || id == '') {
4131
+ id = cue.startTime + '-' + cue.endTime + '-' + cue.text;
4132
+ }
4122
4133
  /** @type {shaka.extern.Chapter} */
4123
4134
  const chapter = {
4135
+ id: id,
4124
4136
  title: cue.text,
4125
4137
  startTime: cue.startTime,
4126
4138
  endTime: cue.endTime,
4127
4139
  };
4128
- chapters.push(chapter);
4140
+ if (!uniqueChapters.has(id)) {
4141
+ chapters.push(chapter);
4142
+ uniqueChapters.add(id);
4143
+ }
4129
4144
  }
4130
4145
  }
4131
4146
  }
@@ -6648,7 +6663,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
6648
6663
  * @define {string} A version number taken from git at compile time.
6649
6664
  * @export
6650
6665
  */
6651
- shaka.Player.version = 'v3.3.14-uncompiled';
6666
+ shaka.Player.version = 'v3.3.16-uncompiled';
6652
6667
 
6653
6668
  // Initialize the deprecation system using the version string we just set
6654
6669
  // on the player.
@@ -372,6 +372,25 @@ shaka.text.UITextDisplayer = class {
372
372
  }
373
373
  }
374
374
 
375
+ /**
376
+ * Compute a unique internal id:
377
+ * Regions can reuse the id but have different dimensions, we need to
378
+ * consider those differences
379
+ * @param {shaka.extern.CueRegion} region
380
+ * @private
381
+ */
382
+ generateRegionId_(region) {
383
+ const percentageUnit = shaka.text.CueRegion.units.PERCENTAGE;
384
+ const heightUnit = region.heightUnits == percentageUnit ? '%' : 'px';
385
+ const viewportAnchorUnit =
386
+ region.viewportAnchorUnits == percentageUnit ? '%' : 'px';
387
+ const uniqueRegionId = `${region.id}_${
388
+ region.width}x${region.height}${heightUnit}-${
389
+ region.viewportAnchorX}x${region.viewportAnchorY}${viewportAnchorUnit}`;
390
+
391
+ return uniqueRegionId;
392
+ }
393
+
375
394
  /**
376
395
  * Get or create a region element corresponding to the cue region. These are
377
396
  * cached by ID.
@@ -383,8 +402,9 @@ shaka.text.UITextDisplayer = class {
383
402
  getRegionElement_(cue) {
384
403
  const region = cue.region;
385
404
 
386
- if (this.regionElements_.has(region.id)) {
387
- return this.regionElements_.get(region.id);
405
+ const regionId = this.generateRegionId_(region);
406
+ if (this.regionElements_.has(regionId)) {
407
+ return this.regionElements_.get(regionId);
388
408
  }
389
409
 
390
410
  const regionElement = shaka.util.Dom.createHTMLElement('span');
@@ -395,7 +415,7 @@ shaka.text.UITextDisplayer = class {
395
415
  const viewportAnchorUnit =
396
416
  region.viewportAnchorUnits == percentageUnit ? '%' : 'px';
397
417
 
398
- regionElement.id = 'shaka-text-region---' + region.id;
418
+ regionElement.id = 'shaka-text-region---' + regionId;
399
419
  regionElement.classList.add('shaka-text-region');
400
420
 
401
421
  regionElement.style.height = region.height + heightUnit;
@@ -416,7 +436,7 @@ shaka.text.UITextDisplayer = class {
416
436
  regionElement.style.justifyContent = 'flex-end';
417
437
  }
418
438
 
419
- this.regionElements_.set(region.id, regionElement);
439
+ this.regionElements_.set(regionId, regionElement);
420
440
  return regionElement;
421
441
  }
422
442
 
@@ -87,16 +87,28 @@ shaka.text.WebVttGenerator = class {
87
87
  // We don't want to modify the array or objects passed in, since we don't
88
88
  // technically own them. So we build a new array and replace certain items
89
89
  // in it if they need to be flattened.
90
- const flattenedCues = cues.map((cue) => {
91
- if (cue.nestedCues.length) {
92
- const flatCue = cue.clone();
93
- flatCue.nestedCues = [];
94
- flatCue.payload = flattenPayload(cue);
95
- return flatCue;
96
- } else {
97
- return cue;
90
+ // We also don't want to flatten the text payloads starting at a container
91
+ // element; otherwise, for containers encapsulating multiple caption lines,
92
+ // the lines would merge into a single cue. This is undesirable when a
93
+ // subset of the captions are outside of the append time window. To fix
94
+ // this, we only call flattenPayload() starting at elements marked as
95
+ // isContainer = false.
96
+ const getCuesToFlatten = (cues, result) => {
97
+ for (const cue of cues) {
98
+ if (cue.isContainer) {
99
+ // Recurse to find the actual text payload cues.
100
+ getCuesToFlatten(cue.nestedCues, result);
101
+ } else {
102
+ // Flatten the payload.
103
+ const flatCue = cue.clone();
104
+ flatCue.nestedCues = [];
105
+ flatCue.payload = flattenPayload(cue);
106
+ result.push(flatCue);
107
+ }
98
108
  }
99
- });
109
+ return result;
110
+ };
111
+ const flattenedCues = getCuesToFlatten(cues, []);
100
112
 
101
113
  let webvttString = 'WEBVTT\n\n';
102
114
  for (const cue of flattenedCues) {
@@ -36,7 +36,9 @@ shaka.util.Dom = class {
36
36
  * @return {!HTMLButtonElement}
37
37
  */
38
38
  static createButton() {
39
- return /** @type {!HTMLButtonElement} */(document.createElement('button'));
39
+ const button = document.createElement('button');
40
+ button.setAttribute('type', 'button');
41
+ return /** @type {!HTMLButtonElement} */ (button);
40
42
  }
41
43
 
42
44
 
@@ -450,12 +450,16 @@ shaka.util.StreamUtils = class {
450
450
  if (!MediaSource.isTypeSupported(audioFullType)) {
451
451
  return false;
452
452
  }
453
+ // Update the codec string with the (possibly) converted codecs.
454
+ videoCodecs = [videoCodecs, audioCodecs].join(',');
453
455
  }
454
456
  const fullType = shaka.util.MimeUtils.getFullOrConvertedType(
455
457
  video.mimeType, videoCodecs, ContentType.VIDEO);
456
458
  if (!MediaSource.isTypeSupported(fullType)) {
457
459
  return false;
458
460
  }
461
+ // Update the codec string with the (possibly) converted codecs.
462
+ video.codecs = videoCodecs;
459
463
  }
460
464
  const audio = variant.audio;
461
465
  if (audio) {
@@ -466,6 +470,8 @@ shaka.util.StreamUtils = class {
466
470
  if (!MediaSource.isTypeSupported(fullType)) {
467
471
  return false;
468
472
  }
473
+ // Update the codec string with the (possibly) converted codecs.
474
+ audio.codecs = codecs;
469
475
  }
470
476
 
471
477
  // See: https://github.com/shaka-project/shaka-player/issues/3380
package/lib/util/timer.js CHANGED
@@ -78,6 +78,11 @@ shaka.util.Timer = class {
78
78
  tickEvery(seconds) {
79
79
  this.stop();
80
80
 
81
+ if (goog.DEBUG) {
82
+ // Capture the stack trace by making a fake error.
83
+ const stackTrace = Error('Timer created').stack;
84
+ shaka.util.Timer.activeTimers.set(this, stackTrace);
85
+ }
81
86
  this.ticker_ = new shaka.util.DelayedTick(() => {
82
87
  // Schedule the timer again first. |onTick_| could cancel the timer and
83
88
  // rescheduling first simplifies the implementation.
@@ -99,5 +104,17 @@ shaka.util.Timer = class {
99
104
  this.ticker_.stop();
100
105
  this.ticker_ = null;
101
106
  }
107
+ if (goog.DEBUG) {
108
+ shaka.util.Timer.activeTimers.delete(this);
109
+ }
102
110
  }
103
111
  };
112
+
113
+ if (goog.DEBUG) {
114
+ /**
115
+ * Tracks all active timer instances, along with the stack trace that created
116
+ * that timer.
117
+ * @type {!Map.<!shaka.util.Timer, string>}
118
+ */
119
+ shaka.util.Timer.activeTimers = new Map();
120
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shaka-player",
3
3
  "description": "DASH/EME video player library",
4
- "version": "3.3.14",
4
+ "version": "3.3.16",
5
5
  "homepage": "https://github.com/shaka-project/shaka-player",
6
6
  "author": "Google",
7
7
  "maintainers": [
@@ -72,6 +72,33 @@ describe('Player', () => {
72
72
  });
73
73
  }); // describe('attach')
74
74
 
75
+ describe('destroy', () => {
76
+ // Regression test for:
77
+ // https://github.com/shaka-project/shaka-player/issues/4850
78
+ it('does not leave any lingering timers', async () => {
79
+ shaka.util.Timer.activeTimers.clear();
80
+
81
+ // Unlike the other tests in this file, this uses an uncompiled build of
82
+ // Shaka, so that we don't need to expose shaka.util.Timer.activeTimers.
83
+ player = new shaka.Player(video);
84
+ waiter.setPlayer(player);
85
+
86
+ // Play the video for a little while.
87
+ await player.load('test:sintel');
88
+ video.play();
89
+ await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 1, 10);
90
+
91
+ // Destroy the player.
92
+ await player.destroy();
93
+
94
+ // Are there any timers left?
95
+ for (const timer of shaka.util.Timer.activeTimers.keys()) {
96
+ const stackTrace = shaka.util.Timer.activeTimers.get(timer);
97
+ fail('Lingering timer exists! Stack trace of creation: ' + stackTrace);
98
+ }
99
+ });
100
+ });
101
+
75
102
  describe('updateStartTime() in manifestparsed event handler', () => {
76
103
  it('does not get segments prior to startTime', async () => {
77
104
  player.addEventListener('manifestparsed', () => {
@@ -879,7 +906,7 @@ describe('Player', () => {
879
906
  expect(getBufferedBehind()).toBe(20); // Buffered to start still.
880
907
  video.currentTime = 50;
881
908
  await waitUntilBuffered(30);
882
- expect(getBufferedBehind()).toBeLessThan(30);
909
+ expect(getBufferedBehind()).toBeLessThan(40); // 30 + segment_size
883
910
 
884
911
  player.configure('streaming.bufferBehind', 10);
885
912
  // We only evict content when we append a segment, so increase the
@@ -906,7 +933,7 @@ describe('Player', () => {
906
933
  }
907
934
 
908
935
  async function waitUntilBuffered(amount) {
909
- for (let i = 0; i < 25; i++) {
936
+ for (let i = 0; i < 50; i++) {
910
937
  // We buffer from an internal segment, so this shouldn't take long to
911
938
  // buffer.
912
939
  await Util.delay(0.1); // eslint-disable-line no-await-in-loop
@@ -914,7 +941,14 @@ describe('Player', () => {
914
941
  return;
915
942
  }
916
943
  }
917
- throw new Error('Timeout waiting to buffer');
944
+
945
+ const ranges =
946
+ shaka.media.TimeRangesUtils.getBufferedInfo(video.buffered);
947
+ const currentTime = video.currentTime;
948
+ const target = currentTime + amount;
949
+
950
+ throw new Error('Timeout waiting to buffer! ' +
951
+ JSON.stringify({ranges, currentTime, target}));
918
952
  }
919
953
  }); // describe('buffering')
920
954
 
@@ -3185,7 +3185,9 @@ describe('Player', () => {
3185
3185
  variant.addAudio(0, (stream) => {
3186
3186
  stream.channelsCount = 6;
3187
3187
  stream.audioSamplingRate = 48000;
3188
- stream.codecs = 'ac-3';
3188
+ // ac-3 is rewritten as ec-3 on Tizen, so for the stability of this
3189
+ // test case, use ec-3.
3190
+ stream.codecs = 'ec-3';
3189
3191
  });
3190
3192
  });
3191
3193
 
@@ -3210,7 +3212,7 @@ describe('Player', () => {
3210
3212
  expect(abrManager.variants.length).toBe(1);
3211
3213
  // It should be the 6-channel variant, based on our preference.
3212
3214
  expect(abrManager.variants[0].audio.channelsCount).toBe(6);
3213
- expect(abrManager.variants[0].audio.codecs).toBe('ac-3');
3215
+ expect(abrManager.variants[0].audio.codecs).toBe('ec-3');
3214
3216
  });
3215
3217
  });
3216
3218
 
package/test/test/boot.js CHANGED
@@ -66,22 +66,40 @@ function failTestsOnNamespacedElementOrAttributeNames() {
66
66
  }
67
67
 
68
68
  /**
69
- * Listen for unhandled Promise rejections (which may occur after a test) and
70
- * convert them into test failures.
69
+ * Fail the current test on a given error, constructed with a given header and
70
+ * the full stack trace of the error.
71
+ *
72
+ * @param {string} messageHeader
73
+ * @param {!Error} error
71
74
  */
72
- function failTestsOnUnhandledRejections() {
75
+ function failOnError(messageHeader, error) {
76
+ let message = `${messageHeader}: ${error}`;
77
+ // Shaka errors have the stack trace in their toString() already, so don't
78
+ // add it again. For native errors, we need to see where it came from.
79
+ if (error && error.stack && !(error instanceof shaka.util.Error)) {
80
+ message += '\n' + error.stack;
81
+ }
82
+ fail(message);
83
+ }
84
+
85
+ /**
86
+ * Listen for unhandled errors and Promise rejections (which may occur after a
87
+ * test) and convert them into test failures.
88
+ */
89
+ function failTestsOnUnhandledErrors() {
73
90
  // https://developer.mozilla.org/en-US/docs/Web/Events/unhandledrejection
74
91
  window.addEventListener('unhandledrejection', (event) => {
75
92
  /** @type {?} */
76
93
  const error = event.reason;
77
- let message = 'Unhandled rejection in Promise: ' + error;
94
+ failOnError('Unhandled rejection in Promise', error);
95
+ });
78
96
 
79
- // Shaka errors have the stack trace in their toString() already, so don't
80
- // add it again. For native errors, we need to see where it came from.
81
- if (error && error.stack && !(error instanceof shaka.util.Error)) {
82
- message += '\n' + error.stack;
83
- }
84
- fail(message);
97
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/error_event
98
+ // https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
99
+ window.addEventListener('error', (event) => {
100
+ /** @type {?} */
101
+ const error = event['error'];
102
+ failOnError('Unhandled error', error);
85
103
  });
86
104
  }
87
105
 
@@ -387,7 +405,7 @@ function configureJasmineEnvironment() {
387
405
  function setupTestEnvironment() {
388
406
  failTestsOnFailedAssertions();
389
407
  failTestsOnNamespacedElementOrAttributeNames();
390
- failTestsOnUnhandledRejections();
408
+ failTestsOnUnhandledErrors();
391
409
  disableScrollbars();
392
410
  workAroundLegacyEdgePromiseIssues();
393
411
 
@@ -402,7 +402,7 @@ const sintelAudioSegment = {
402
402
  mdhdOffset: 0x1b6,
403
403
  segmentUri: '/base/test/test/assets/sintel-audio-segment.mp4',
404
404
  tfdtOffset: 0x3c,
405
- segmentDuration: 10.005,
405
+ segmentDuration: 10,
406
406
  mimeType: 'audio/mp4',
407
407
  codecs: 'mp4a.40.2',
408
408
  };
@@ -428,7 +428,7 @@ const sintelEncryptedAudio = {
428
428
  mdhdOffset: 0x1b6,
429
429
  segmentUri: '/base/test/test/assets/encrypted-sintel-audio-segment.mp4',
430
430
  tfdtOffset: 0x3c,
431
- segmentDuration: 10.005,
431
+ segmentDuration: 10,
432
432
  mimeType: 'audio/mp4',
433
433
  codecs: 'mp4a.40.2',
434
434
  initData:
@@ -537,4 +537,74 @@ describe('UITextDisplayer', () => {
537
537
  '.shaka-text-region');
538
538
  expect(allRegionElements.length).toBe(1);
539
539
  });
540
+
541
+ it('creates separate regions when dimensions differ but id same', () => {
542
+ const identicalRegionId = 'regionId';
543
+
544
+ const cueRegion1 = new shaka.text.CueRegion();
545
+ const cueRegion2 = new shaka.text.CueRegion();
546
+ cueRegion1.id = identicalRegionId;
547
+ cueRegion2.id = identicalRegionId;
548
+
549
+ cueRegion1.height = 80;
550
+ cueRegion1.heightUnits = shaka.text.CueRegion.units.PERCENTAGE;
551
+ cueRegion1.width = 80;
552
+ cueRegion1.widthUnits = shaka.text.CueRegion.units.PERCENTAGE;
553
+
554
+ cueRegion2.height = 160; // the only difference!
555
+ cueRegion2.heightUnits = shaka.text.CueRegion.units.PERCENTAGE;
556
+ cueRegion2.width = 80;
557
+ cueRegion2.widthUnits = shaka.text.CueRegion.units.PERCENTAGE;
558
+
559
+ cueRegion1.viewportAnchorX = 10;
560
+ cueRegion1.viewportAnchorY = 10;
561
+ cueRegion1.viewportAnchorUnits = shaka.text.CueRegion.units.PERCENTAGE;
562
+
563
+ cueRegion2.viewportAnchorX = 10;
564
+ cueRegion2.viewportAnchorY = 10;
565
+ cueRegion2.viewportAnchorUnits = shaka.text.CueRegion.units.PERCENTAGE;
566
+
567
+ // These all attach to the same region, but only one region element should
568
+ // be created.
569
+ const firstBatchOfCues = [
570
+ new shaka.text.Cue(0, 100, ''),
571
+ new shaka.text.Cue(0, 100, ''),
572
+ new shaka.text.Cue(0, 100, ''),
573
+ ];
574
+ for (const cue of firstBatchOfCues) {
575
+ cue.displayAlign = shaka.text.Cue.displayAlign.CENTER;
576
+ cue.region = cueRegion1;
577
+ }
578
+
579
+ // Another batch for the other region
580
+ const secondBatchOfCues = [
581
+ new shaka.text.Cue(0, 100, ''),
582
+ new shaka.text.Cue(0, 100, ''),
583
+ new shaka.text.Cue(0, 100, ''),
584
+ ];
585
+ for (const cue of secondBatchOfCues) {
586
+ cue.displayAlign = shaka.text.Cue.displayAlign.CENTER;
587
+ cue.region = cueRegion2;
588
+ }
589
+
590
+ textDisplayer.setTextVisibility(true);
591
+ textDisplayer.append(firstBatchOfCues);
592
+ textDisplayer.append(secondBatchOfCues);
593
+ updateCaptions();
594
+
595
+ const textContainer = videoContainer.querySelector('.shaka-text-container');
596
+ const allRegionElements = textContainer.querySelectorAll(
597
+ '.shaka-text-region');
598
+
599
+ // Verify that the nested cues are attached to respective region element.
600
+ expect(allRegionElements.length).toBe(2);
601
+
602
+ const childrenOfOne = Array.from(allRegionElements[0].childNodes).filter(
603
+ (e) => e.nodeType == Node.ELEMENT_NODE);
604
+ expect(childrenOfOne.length).toBe(3);
605
+
606
+ const childrenOfTwo = Array.from(allRegionElements[1].childNodes).filter(
607
+ (e) => e.nodeType == Node.ELEMENT_NODE);
608
+ expect(childrenOfTwo.length).toBe(3);
609
+ });
540
610
  });