shaka-player 3.3.10 → 3.3.11

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 (34) hide show
  1. package/.github/workflows/build-and-test.yaml +31 -16
  2. package/.github/workflows/custom-actions/set-commit-status/action.yaml +1 -5
  3. package/.github/workflows/deflake.yaml +28 -0
  4. package/.github/workflows/selenium-lab-tests.yaml +13 -4
  5. package/AUTHORS +3 -0
  6. package/CHANGELOG.md +13 -0
  7. package/CONTRIBUTORS +2 -0
  8. package/dist/locales.js +27 -27
  9. package/dist/shaka-player.compiled.debug.js +107 -106
  10. package/dist/shaka-player.compiled.debug.map +3 -3
  11. package/dist/shaka-player.compiled.js +531 -530
  12. package/dist/shaka-player.compiled.map +3 -3
  13. package/dist/shaka-player.ui.debug.js +171 -170
  14. package/dist/shaka-player.ui.debug.map +3 -3
  15. package/dist/shaka-player.ui.js +650 -649
  16. package/dist/shaka-player.ui.map +3 -3
  17. package/docs/jsdoc-template/publish.js +33 -0
  18. package/docs/jsdoc-template/tmpl/container.tmpl +4 -1
  19. package/docs/jsdoc.conf.json +1 -0
  20. package/lib/cast/cast_sender.js +39 -14
  21. package/lib/cea/cea608_memory.js +4 -0
  22. package/lib/media/streaming_engine.js +41 -4
  23. package/lib/player.js +1 -1
  24. package/lib/text/ttml_text_parser.js +6 -0
  25. package/lib/util/platform.js +34 -0
  26. package/lib/util/stream_utils.js +2 -1
  27. package/maintained-branches.md +2 -2
  28. package/package.json +1 -1
  29. package/test/cea/cea608_memory_unit.js +88 -0
  30. package/test/media/streaming_engine_integration.js +8 -7
  31. package/test/offline/offline_integration.js +3 -3
  32. package/test/test/util/ttml_utils.js +0 -2
  33. package/test/text/ttml_text_parser_unit.js +46 -0
  34. package/test/ui/text_displayer_layout_unit.js +17 -2
@@ -440,6 +440,7 @@ function buildNav(members) {
440
440
  [members.externals, 'Externals', seen, linktoExternal],
441
441
  [members.namespaces, 'Namespaces', seen, linkto],
442
442
  [members.classes, 'Classes', seen, linkto],
443
+ [members.enums, 'Enums', seen, linkto],
443
444
  [members.interfaces, 'Interfaces', seen, linkto],
444
445
  [members.events, 'Events', seen, linkto],
445
446
  [members.mixins, 'Mixins', seen, linkto],
@@ -484,6 +485,7 @@ function fixUpType(type) {
484
485
  exports.publish = (taffyData, opts, tutorials) => {
485
486
  let classes;
486
487
  let conf;
488
+ let enums;
487
489
  let externals;
488
490
  let files;
489
491
  let fromDir;
@@ -733,6 +735,31 @@ exports.publish = (taffyData, opts, tutorials) => {
733
735
  });
734
736
 
735
737
  members = helper.getMembers(data);
738
+ members.enums = [];
739
+
740
+ data().each(doclet => {
741
+ if (doclet.isEnum) {
742
+ const hasParent = !!docletMap[doclet.memberof];
743
+
744
+ // Enums without parents (not attached to a class, but directly to a
745
+ // namespace) aren't recognized by jsdoc by default and need to be
746
+ // massaged into the docs separately.
747
+ if (!hasParent) {
748
+ // Temporarily call this a "class", so that we can fix the URL.
749
+ doclet.kind = 'class';
750
+ // Regenerate the URL, so that this enum gets its own page
751
+ // instead of a URL that links to an ID within a non-existent
752
+ // parent page.
753
+ delete helper.longnameToUrl[doclet.longname];
754
+ helper.createLink(doclet);
755
+ // Mark this as an enum (custom type), so that it doesn't get
756
+ // slotted with the classes.
757
+ doclet.kind = 'enum';
758
+ members.enums.push(doclet);
759
+ }
760
+ }
761
+ });
762
+
736
763
  members.tutorials = tutorials.children;
737
764
  if (conf.default.sortTutorialsByConfigOrder) {
738
765
  members.tutorials.sort((a, b) => {
@@ -778,6 +805,7 @@ exports.publish = (taffyData, opts, tutorials) => {
778
805
 
779
806
  // set up the lists that we'll use to generate pages
780
807
  classes = taffy(members.classes);
808
+ enums = taffy(members.enums);
781
809
  modules = taffy(members.modules);
782
810
  namespaces = taffy(members.namespaces);
783
811
  mixins = taffy(members.mixins);
@@ -786,6 +814,7 @@ exports.publish = (taffyData, opts, tutorials) => {
786
814
 
787
815
  Object.keys(helper.longnameToUrl).forEach(longname => {
788
816
  const myClasses = helper.find(classes, {longname: longname});
817
+ const myEnums = helper.find(enums, {longname: longname});
789
818
  const myExternals = helper.find(externals, {longname: longname});
790
819
  const myInterfaces = helper.find(interfaces, {longname: longname});
791
820
  const myMixins = helper.find(mixins, {longname: longname});
@@ -800,6 +829,10 @@ exports.publish = (taffyData, opts, tutorials) => {
800
829
  generate(`Class: ${myClasses[0].longname}`, myClasses, helper.longnameToUrl[longname]);
801
830
  }
802
831
 
832
+ if (myEnums.length) {
833
+ generate(`Enum: ${myEnums[0].longname}`, myEnums, helper.longnameToUrl[longname]);
834
+ }
835
+
803
836
  if (myNamespaces.length) {
804
837
  generate(`Namespace: ${myNamespaces[0].longname}`, myNamespaces, helper.longnameToUrl[longname]);
805
838
  }
@@ -144,7 +144,10 @@
144
144
  return m.longname && m.longname.indexOf('module:') !== 0;
145
145
  });
146
146
  }
147
- if (members && members.length && members.forEach) {
147
+
148
+ // Skip enums, since they already have their values listed under
149
+ // "Properties".
150
+ if (members && members.length && members.forEach && doc.kind != 'enum') {
148
151
  ?>
149
152
  <h3 class="subsection-title">Members</h3>
150
153
 
@@ -16,6 +16,7 @@
16
16
  "navOrder": [
17
17
  "Tutorials",
18
18
  "Classes",
19
+ "Enums",
19
20
  "Interfaces",
20
21
  "Events"
21
22
  ]
@@ -186,21 +186,14 @@ shaka.cast.CastSender = class {
186
186
  // If the API becomes available before this instance dies, init will be
187
187
  // called again.
188
188
 
189
- // A note about this value: In our testing environment, we load both
190
- // uncompiled and compiled code. This global callback in uncompiled mode
191
- // can be overwritten by the same in compiled mode. The two versions will
192
- // each have their own instances_ map. Therefore the callback must have a
193
- // name, as opposed to being anonymous. This way, the CastSender tests
194
- // can invoke the named static method instead of using a global that could
195
- // be overwritten.
196
- if (!window.__onGCastApiAvailable) {
197
- window.__onGCastApiAvailable = shaka.cast.CastSender.onSdkLoaded_;
198
- }
199
- if (window.__onGCastApiAvailable != shaka.cast.CastSender.onSdkLoaded_) {
200
- shaka.log.alwaysWarn('A global Cast SDK hook is already installed! ' +
201
- 'Shaka Player will be unable to receive a notification when the ' +
202
- 'Cast SDK is ready.');
189
+ // Check if our callback is already installed.
190
+ if (window.__onGCastApiAvailable !== CastSender.onGCastApiAvailable_) {
191
+ // Save pre-existing __onGCastApiAvailable in order to restore later.
192
+ CastSender.__onGCastApiAvailable_ =
193
+ window.__onGCastApiAvailable || null;
194
+ window.__onGCastApiAvailable = CastSender.onGCastApiAvailable_;
203
195
  }
196
+
204
197
  return;
205
198
  }
206
199
 
@@ -747,6 +740,9 @@ shaka.cast.CastSender.hasReceivers_ = false;
747
740
  /** @private {chrome.cast.Session} */
748
741
  shaka.cast.CastSender.session_ = null;
749
742
 
743
+ /** @private {?function(boolean)} */
744
+ shaka.cast.CastSender.__onGCastApiAvailable_ = null;
745
+
750
746
  /**
751
747
  * A set of all living CastSender instances. The constructor and destroy
752
748
  * methods will add and remove instances from this set.
@@ -774,3 +770,32 @@ shaka.cast.CastSender.onSdkLoaded_ = (loaded) => {
774
770
  }
775
771
  }
776
772
  };
773
+
774
+ /**
775
+ * @param {boolean} available
776
+ * @private
777
+ */
778
+ shaka.cast.CastSender.onGCastApiAvailable_ = (available) => {
779
+ // Restore callback from saved.
780
+ if (shaka.cast.CastSender.__onGCastApiAvailable_) {
781
+ window.__onGCastApiAvailable =
782
+ shaka.cast.CastSender.__onGCastApiAvailable_;
783
+ } else {
784
+ delete window.__onGCastApiAvailable;
785
+ }
786
+ shaka.cast.CastSender.__onGCastApiAvailable_ = null;
787
+
788
+ // A note about this value: In our testing environment, we load both
789
+ // uncompiled and compiled code. This global callback in uncompiled mode
790
+ // can be overwritten by the same in compiled mode. The two versions will
791
+ // each have their own instances_ map. Therefore the callback must have a
792
+ // name, as opposed to being anonymous. This way, the CastSender tests
793
+ // can invoke the named static method instead of using a global that could
794
+ // be overwritten.
795
+ shaka.cast.CastSender.onSdkLoaded_(available);
796
+
797
+ // call restored callback (if any)
798
+ if (typeof window.__onGCastApiAvailable === 'function') {
799
+ window.__onGCastApiAvailable(available);
800
+ }
801
+ };
@@ -184,6 +184,10 @@ shaka.cea.Cea608Memory = class {
184
184
  * @param {number} count Count of rows to move.
185
185
  */
186
186
  moveRows(dst, src, count) {
187
+ if (src < 0 || dst < 0) {
188
+ return;
189
+ }
190
+
187
191
  if (dst >= src) {
188
192
  for (let i = count-1; i >= 0; i--) {
189
193
  this.rows_[dst + i] = this.rows_[src + i].map((e) => e);
@@ -991,7 +991,11 @@ shaka.media.StreamingEngine = class {
991
991
  const timeUntilEnd =
992
992
  this.manifest_.presentationTimeline.getDuration() - timeNeeded;
993
993
  const oneMicrosecond = 1e-6;
994
- if (timeUntilEnd < oneMicrosecond) {
994
+
995
+ const bufferEnd =
996
+ this.playerInterface_.mediaSourceEngine.bufferEnd(mediaState.type);
997
+
998
+ if (timeUntilEnd < oneMicrosecond && !!bufferEnd) {
995
999
  // We shouldn't rebuffer if the playhead is close to the end of the
996
1000
  // presentation.
997
1001
  shaka.log.debug(logPrefix, 'buffered to end of presentation');
@@ -1021,8 +1025,6 @@ shaka.media.StreamingEngine = class {
1021
1025
  return this.config_.updateIntervalSeconds / 2;
1022
1026
  }
1023
1027
 
1024
- const bufferEnd =
1025
- this.playerInterface_.mediaSourceEngine.bufferEnd(mediaState.type);
1026
1028
  const reference = this.getSegmentReferenceNeeded_(
1027
1029
  mediaState, presentationTime, bufferEnd);
1028
1030
  if (!reference) {
@@ -1218,7 +1220,16 @@ shaka.media.StreamingEngine = class {
1218
1220
  // Enable MP4 low latency streaming with ReadableStream chunked data.
1219
1221
  if (this.config_.lowLatencyMode && isReadableStreamSupported && isMP4) {
1220
1222
  let remaining = new Uint8Array(0);
1223
+ let processingResult = false;
1224
+ let callbackCalled = false;
1221
1225
  const streamDataCallback = async (data) => {
1226
+ if (processingResult) {
1227
+ // If the fallback result processing was triggered, don't also
1228
+ // append the buffer here. In theory this should never happen,
1229
+ // but it does on some older TVs.
1230
+ return;
1231
+ }
1232
+ callbackCalled = true;
1222
1233
  this.destroyer_.ensureNotDestroyed();
1223
1234
  if (this.fatalError_) {
1224
1235
  return;
@@ -1249,7 +1260,33 @@ shaka.media.StreamingEngine = class {
1249
1260
  }
1250
1261
  };
1251
1262
 
1252
- await this.fetch_(mediaState, reference, streamDataCallback);
1263
+ const result =
1264
+ await this.fetch_(mediaState, reference, streamDataCallback);
1265
+ if (!callbackCalled) {
1266
+ // In some environments, we might be forced to use network plugins
1267
+ // that don't support streamDataCallback. In those cases, as a
1268
+ // fallback, append the buffer here.
1269
+ processingResult = true;
1270
+ this.destroyer_.ensureNotDestroyed();
1271
+ if (this.fatalError_) {
1272
+ return;
1273
+ }
1274
+
1275
+ // If the text stream gets switched between fetch_() and append_(),
1276
+ // the new text parser is initialized, but the new init segment is
1277
+ // not fetched yet. That would cause an error in
1278
+ // TextParser.parseMedia().
1279
+ // See http://b/168253400
1280
+ if (mediaState.waitingToClearBuffer) {
1281
+ shaka.log.info(logPrefix, 'waitingToClearBuffer, skip append');
1282
+ mediaState.performingUpdate = false;
1283
+ this.scheduleUpdate_(mediaState, 0);
1284
+ return;
1285
+ }
1286
+
1287
+ await this.append_(
1288
+ mediaState, presentationTime, stream, reference, result);
1289
+ }
1253
1290
  } else {
1254
1291
  if (this.config_.lowLatencyMode && !isReadableStreamSupported) {
1255
1292
  shaka.log.warning('Low latency streaming mode is enabled, but ' +
package/lib/player.js CHANGED
@@ -6635,7 +6635,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
6635
6635
  * @define {string} A version number taken from git at compile time.
6636
6636
  * @export
6637
6637
  */
6638
- shaka.Player.version = 'v3.3.10-uncompiled';
6638
+ shaka.Player.version = 'v3.3.11-uncompiled';
6639
6639
 
6640
6640
  // Initialize the deprecation system using the version string we just set
6641
6641
  // on the player.
@@ -156,6 +156,12 @@ shaka.text.TtmlTextParser = class {
156
156
  cellResolutionInfo, /* parentCueElement= */ null,
157
157
  /* isContent= */ false);
158
158
  if (cue) {
159
+ // According to the TTML spec, backgrounds default to transparent.
160
+ // So default the background of the top-level element to transparent.
161
+ // Nested elements may override that background color already.
162
+ if (!cue.backgroundColor) {
163
+ cue.backgroundColor = 'transparent';
164
+ }
159
165
  cues.push(cue);
160
166
  }
161
167
  }
@@ -163,6 +163,40 @@ shaka.util.Platform = class {
163
163
  return shaka.util.Platform.userAgentContains_('CrKey');
164
164
  }
165
165
 
166
+ /**
167
+ * Check if the current platform is a Android-based Cast devices.
168
+ *
169
+ * @return {boolean}
170
+ */
171
+ static isAndroidCastDevice() {
172
+ return shaka.util.Platform.isChromecast() &&
173
+ shaka.util.Platform.userAgentContains_('Android');
174
+ }
175
+
176
+ /**
177
+ * Returns a major version number for Chrome, or Chromium-based browsers.
178
+ *
179
+ * For example:
180
+ * - Chrome 106.0.5249.61 returns 106.
181
+ * - Edge 106.0.1370.34 returns 106 (since this is based on Chromium).
182
+ * - Safari returns null (since this is independent of Chromium).
183
+ *
184
+ * @return {?number} A major version number or null if not Chromium-based.
185
+ */
186
+ static chromeVersion() {
187
+ if (!shaka.util.Platform.userAgentContains_('Chrome')) {
188
+ return null;
189
+ }
190
+
191
+ // Looking for something like "Chrome/106.0.0.0".
192
+ const match = navigator.userAgent.match(/Chrome\/(\d+)/);
193
+ if (match) {
194
+ return parseInt(match[1], /* base= */ 10);
195
+ }
196
+
197
+ return null;
198
+ }
199
+
166
200
  /**
167
201
  * Check if the current platform is Google Chrome.
168
202
  *
@@ -467,7 +467,8 @@ shaka.util.StreamUtils = class {
467
467
  if (shaka.util.Platform.isXboxOne() && video &&
468
468
  ((video.width && video.width > 1920) ||
469
469
  (video.height && video.height > 1080)) &&
470
- video.codecs.includes('avc1.')) {
470
+ (video.codecs.includes('avc1.') ||
471
+ video.codecs.includes('avc3.'))) {
471
472
  shaka.log.debug('Dropping variant - not compatible with platform',
472
473
  shaka.util.StreamUtils.getVariantSummaryString_(variant));
473
474
  return false;
@@ -2,8 +2,8 @@
2
2
 
3
3
  We are currently maintaining the following branches of Shaka Player:
4
4
 
5
- - v4.1 (latest)
6
- - v4.0 (previous)
5
+ - v4.2 (latest)
6
+ - v4.1 (previous)
7
7
  - v3.3 (LTS until April 30, 2023)
8
8
  - v3.2 (in use by the Cast Application Framework)
9
9
 
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.10",
4
+ "version": "3.3.11",
5
5
  "homepage": "https://github.com/shaka-project/shaka-player",
6
6
  "author": "Google",
7
7
  "maintainers": [
@@ -295,5 +295,93 @@ describe('Cea608Memory', () => {
295
295
  const caption = memory.forceEmit(startTime, endTime);
296
296
  expect(caption).toEqual(expectedCaption);
297
297
  });
298
+
299
+ it('does not move rows if source row index is negative', () => {
300
+ const startTime = 1;
301
+ const endTime = 2;
302
+ const text = 'test';
303
+
304
+ // Add the text to the buffer, each character on separate rows.
305
+ // At this point, the memory looks like:
306
+ // [1]: t
307
+ // [2]: e
308
+ // [3]: s
309
+ // [4]: t
310
+ for (const c of text) {
311
+ memory.addChar(CharSet.BASIC_NORTH_AMERICAN,
312
+ c.charCodeAt(0));
313
+ memory.setRow(memory.getRow() + 1); // increment row
314
+ }
315
+
316
+ const srcRowIdx = -1;
317
+ const dstRowIdx = 2;
318
+ const rowsToMove = 3;
319
+ memory.moveRows(dstRowIdx, srcRowIdx, rowsToMove);
320
+
321
+ // Expected text is 't\ne\ns\nt'
322
+ const topLevelCue = new shaka.text.Cue(startTime, endTime, '');
323
+ topLevelCue.nestedCues = [
324
+ CeaUtils.createDefaultCue(startTime, endTime, 't'),
325
+ CeaUtils.createLineBreakCue(startTime, endTime),
326
+ CeaUtils.createDefaultCue(startTime, endTime, 'e'),
327
+ CeaUtils.createLineBreakCue(startTime, endTime),
328
+ CeaUtils.createDefaultCue(startTime, endTime, 's'),
329
+ CeaUtils.createLineBreakCue(startTime, endTime),
330
+ CeaUtils.createDefaultCue(startTime, endTime, 't'),
331
+ ];
332
+
333
+ const expectedCaption = {
334
+ stream,
335
+ cue: topLevelCue,
336
+ };
337
+
338
+ // Force out the new memory.
339
+ const caption = memory.forceEmit(startTime, endTime);
340
+ expect(caption).toEqual(expectedCaption);
341
+ });
342
+
343
+ it('does not move rows if destination row index is negative', () => {
344
+ const startTime = 1;
345
+ const endTime = 2;
346
+ const text = 'test';
347
+
348
+ // Add the text to the buffer, each character on separate rows.
349
+ // At this point, the memory looks like:
350
+ // [1]: t
351
+ // [2]: e
352
+ // [3]: s
353
+ // [4]: t
354
+ for (const c of text) {
355
+ memory.addChar(CharSet.BASIC_NORTH_AMERICAN,
356
+ c.charCodeAt(0));
357
+ memory.setRow(memory.getRow() + 1); // increment row
358
+ }
359
+
360
+ const srcRowIdx = 1;
361
+ const dstRowIdx = -2;
362
+ const rowsToMove = 3;
363
+ memory.moveRows(dstRowIdx, srcRowIdx, rowsToMove);
364
+
365
+ // Expected text is 't\ne\ns\nt'
366
+ const topLevelCue = new shaka.text.Cue(startTime, endTime, '');
367
+ topLevelCue.nestedCues = [
368
+ CeaUtils.createDefaultCue(startTime, endTime, 't'),
369
+ CeaUtils.createLineBreakCue(startTime, endTime),
370
+ CeaUtils.createDefaultCue(startTime, endTime, 'e'),
371
+ CeaUtils.createLineBreakCue(startTime, endTime),
372
+ CeaUtils.createDefaultCue(startTime, endTime, 's'),
373
+ CeaUtils.createLineBreakCue(startTime, endTime),
374
+ CeaUtils.createDefaultCue(startTime, endTime, 't'),
375
+ ];
376
+
377
+ const expectedCaption = {
378
+ stream,
379
+ cue: topLevelCue,
380
+ };
381
+
382
+ // Force out the new memory.
383
+ const caption = memory.forceEmit(startTime, endTime);
384
+ expect(caption).toEqual(expectedCaption);
385
+ });
298
386
  });
299
387
  });
@@ -90,17 +90,17 @@ describe('StreamingEngine', () => {
90
90
 
91
91
  segmentAvailability = {
92
92
  start: 0,
93
- end: 60,
93
+ end: 40,
94
94
  };
95
95
 
96
96
  timeline = shaka.test.StreamingEngineUtil.createFakePresentationTimeline(
97
97
  segmentAvailability,
98
- /* presentationDuration= */ 60,
98
+ /* presentationDuration= */ 40,
99
99
  /* maxSegmentDuration= */ metadata.video.segmentDuration,
100
100
  /* isLive= */ false);
101
101
 
102
102
  setupNetworkingEngine(
103
- /* presentationDuration= */ 60,
103
+ /* presentationDuration= */ 40,
104
104
  {
105
105
  audio: metadata.audio.segmentDuration,
106
106
  video: metadata.video.segmentDuration,
@@ -108,8 +108,8 @@ describe('StreamingEngine', () => {
108
108
 
109
109
  setupManifest(
110
110
  /* firstPeriodStartTime= */ 0,
111
- /* secondPeriodStartTime= */ 30,
112
- /* presentationDuration= */ 60);
111
+ /* secondPeriodStartTime= */ 20,
112
+ /* presentationDuration= */ 40);
113
113
 
114
114
  setupPlayhead();
115
115
 
@@ -263,10 +263,11 @@ describe('StreamingEngine', () => {
263
263
  streamingEngine.switchVariant(variant);
264
264
  await streamingEngine.start();
265
265
  video.play();
266
- // The overall test timeout is 120 seconds, and the content is 60
266
+ // The overall test timeout is 120 seconds, and the content is 40
267
267
  // seconds. It should be possible to complete this test in 100 seconds,
268
268
  // and if not, we want the error thrown to be within the overall test's
269
- // timeout window.
269
+ // timeout window. Note that we have seen some devices fail to play at
270
+ // full speed for reasons beyond our control, so we plan for >= 0.5x.
270
271
  await waiter.timeoutAfter(100).waitForEnd(video);
271
272
  });
272
273
 
@@ -68,7 +68,7 @@ filterDescribe('Offline', supportsStorage, () => {
68
68
  await player.load(contentUri);
69
69
 
70
70
  video.play();
71
- await playTo(/* end= */ 3, /* timeout= */ 10);
71
+ await playTo(/* end= */ 3, /* timeout= */ 20);
72
72
  await player.unload();
73
73
  await storage.remove(contentUri);
74
74
  });
@@ -104,7 +104,7 @@ filterDescribe('Offline', supportsStorage, () => {
104
104
  await player.load(contentUri);
105
105
 
106
106
  video.play();
107
- await playTo(/* end= */ 3, /* timeout= */ 10);
107
+ await playTo(/* end= */ 3, /* timeout= */ 20);
108
108
  await player.unload();
109
109
  await storage.remove(contentUri);
110
110
  });
@@ -147,7 +147,7 @@ filterDescribe('Offline', supportsStorage, () => {
147
147
  await player.load(contentUri);
148
148
 
149
149
  video.play();
150
- await playTo(/* end= */ 3, /* timeout= */ 10);
150
+ await playTo(/* end= */ 3, /* timeout= */ 20);
151
151
  await player.unload();
152
152
  await storage.remove(contentUri);
153
153
  });
@@ -51,8 +51,6 @@ shaka.test.TtmlUtils = class {
51
51
  region,
52
52
  nestedCues: jasmine.any(Object),
53
53
  payload: '',
54
- startTime: 0,
55
- endTime: Infinity,
56
54
  isContainer: true,
57
55
  });
58
56
  Object.assign(containerCue, properties);
@@ -1535,6 +1535,52 @@ describe('TtmlTextParser', () => {
1535
1535
  {startTime: 1, endTime: 2});
1536
1536
  });
1537
1537
 
1538
+ // Regression test for #4468
1539
+ it('defaults the body background to transparent', () => {
1540
+ verifyHelper(
1541
+ // One cue, don't care about the details
1542
+ [{}],
1543
+ '<tt xmlns:tts="http://www.w3.org/ns/ttml#styling">' +
1544
+ '<head>' +
1545
+ '<styling>' +
1546
+ '<style xml:id="s1" tts:backgroundColor="black" />' +
1547
+ '</styling>' +
1548
+ '</head>' +
1549
+ '<body><div>' +
1550
+ '<p begin="00:01.00" end="00:02.00">' +
1551
+ '<span style="s1">Test</span>' +
1552
+ '</p>' +
1553
+ '</div></body></tt>',
1554
+ {periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0},
1555
+ // The body must have these properties:
1556
+ {backgroundColor: 'transparent'},
1557
+ // The div must have these properties:
1558
+ {}); // don't care
1559
+ });
1560
+
1561
+ // Regression test for #4468
1562
+ it('allows the body background color to be set', () => {
1563
+ verifyHelper(
1564
+ // One cue, don't care about the details
1565
+ [{}],
1566
+ '<tt xmlns:tts="http://www.w3.org/ns/ttml#styling">' +
1567
+ '<head>' +
1568
+ '<styling>' +
1569
+ '<style xml:id="s1" tts:backgroundColor="black" />' +
1570
+ '</styling>' +
1571
+ '</head>' +
1572
+ '<body style="s1"><div>' +
1573
+ '<p begin="00:01.00" end="00:02.00">' +
1574
+ '<span>Test</span>' +
1575
+ '</p>' +
1576
+ '</div></body></tt>',
1577
+ {periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0},
1578
+ // The body must have these properties:
1579
+ {backgroundColor: 'black'},
1580
+ // The div must have these properties:
1581
+ {}); // don't care
1582
+ });
1583
+
1538
1584
  it('parses wrapping option', () => {
1539
1585
  verifyHelper(
1540
1586
  [
@@ -219,6 +219,21 @@ filterDescribe('TextDisplayer layout', supportsScreenshots, () => {
219
219
 
220
220
  /** @param {string} prefix Prepended to screenshot names */
221
221
  function defineTests(prefix) {
222
+ // Due to a Safari implementation bug, the browser only does the correct
223
+ // thing for "end-time-edge-case" on Safari 16+. Skip the tests on earlier
224
+ // versions.
225
+ const safariVersion = shaka.util.Platform.safariVersion();
226
+ if (prefix == 'native' && safariVersion && safariVersion < 16) {
227
+ return;
228
+ }
229
+
230
+ // Due to updates in the rendering and/or default styles in Chrome, the
231
+ // screenshots for native rendering only match in Chrome 106+.
232
+ const chromeVersion = shaka.util.Platform.chromeVersion();
233
+ if (prefix == 'native' && chromeVersion && chromeVersion < 106) {
234
+ return;
235
+ }
236
+
222
237
  it('basic cue', async () => {
223
238
  textDisplayer.append([
224
239
  new shaka.text.Cue(0, 1, 'Captain\'s log, stardate 41636.9'),
@@ -262,8 +277,8 @@ filterDescribe('TextDisplayer layout', supportsScreenshots, () => {
262
277
 
263
278
  // Regression test for #3151
264
279
  // Only one cue should be displayed. Note, however, that we don't control
265
- // this in a browser's native display. As of Feb 2021, only Firefox does
266
- // the right thing in native text display. Chrome, Edge, and Safari all
280
+ // this in a browser's native display. As of Sep 2022, both Firefox and
281
+ // Safari>=16 do the right thing in native text display. Chrome and Edge
267
282
  // show both cues in this edge case. When we control the display of text
268
283
  // through the UI & DOM, we can always get the timing right.
269
284
  it('cues ending exactly now', async () => {