shaka-player 4.2.2 → 4.2.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 (68) hide show
  1. package/.github/workflows/build-and-test.yaml +11 -6
  2. package/.github/workflows/compute-incremental-coverage.py +14 -3
  3. package/.github/workflows/demo-version-index.yaml +1 -1
  4. package/.github/workflows/nightly-demo.yaml +1 -1
  5. package/.github/workflows/release-please.yaml +5 -5
  6. package/.github/workflows/report-incremental-coverage.yaml +1 -1
  7. package/.github/workflows/selenium-lab-tests.yaml +8 -6
  8. package/AUTHORS +2 -0
  9. package/CHANGELOG.md +32 -0
  10. package/CONTRIBUTORS +2 -0
  11. package/dist/deps.js +5 -5
  12. package/dist/locales.js +33 -33
  13. package/dist/shaka-player.compiled.d.ts +6 -1
  14. package/dist/shaka-player.compiled.debug.d.ts +6 -1
  15. package/dist/shaka-player.compiled.debug.externs.js +3 -1
  16. package/dist/shaka-player.compiled.debug.js +119 -115
  17. package/dist/shaka-player.compiled.debug.map +4 -4
  18. package/dist/shaka-player.compiled.externs.js +3 -1
  19. package/dist/shaka-player.compiled.js +719 -716
  20. package/dist/shaka-player.compiled.map +4 -4
  21. package/dist/shaka-player.ui.d.ts +6 -1
  22. package/dist/shaka-player.ui.debug.d.ts +6 -1
  23. package/dist/shaka-player.ui.debug.externs.js +3 -1
  24. package/dist/shaka-player.ui.debug.js +135 -132
  25. package/dist/shaka-player.ui.debug.map +4 -4
  26. package/dist/shaka-player.ui.externs.js +3 -1
  27. package/dist/shaka-player.ui.js +846 -843
  28. package/dist/shaka-player.ui.map +4 -4
  29. package/docs/tutorials/welcome.md +2 -2
  30. package/externs/mux.js +3 -0
  31. package/lib/ads/client_side_ad_manager.js +5 -2
  32. package/lib/cast/cast_proxy.js +2 -1
  33. package/lib/cast/cast_receiver.js +19 -5
  34. package/lib/cast/cast_sender.js +2 -1
  35. package/lib/cast/cast_utils.js +16 -6
  36. package/lib/hls/hls_parser.js +27 -9
  37. package/lib/media/drm_engine.js +7 -6
  38. package/lib/media/media_source_engine.js +6 -0
  39. package/lib/media/playhead.js +24 -2
  40. package/lib/media/streaming_engine.js +3 -12
  41. package/lib/media/transmuxer.js +6 -0
  42. package/lib/offline/indexeddb/storage_mechanism.js +24 -0
  43. package/lib/player.js +24 -11
  44. package/lib/polyfill/media_capabilities.js +7 -6
  45. package/lib/text/ttml_text_parser.js +69 -93
  46. package/lib/text/vtt_text_parser.js +1 -1
  47. package/lib/util/error.js +7 -0
  48. package/lib/util/manifest_parser_utils.js +4 -0
  49. package/lib/util/periods.js +9 -9
  50. package/lib/util/platform.js +0 -10
  51. package/lib/util/xml_utils.js +45 -6
  52. package/package.json +1 -1
  53. package/roadmap.md +24 -6
  54. package/test/cast/cast_receiver_integration.js +97 -16
  55. package/test/cast/cast_receiver_unit.js +17 -12
  56. package/test/cast/cast_utils_unit.js +1 -0
  57. package/test/media/media_source_engine_integration.js +31 -0
  58. package/test/media/playhead_unit.js +41 -0
  59. package/test/media/streaming_engine_integration.js +13 -0
  60. package/test/media/streaming_engine_unit.js +36 -23
  61. package/test/offline/storage_integration.js +30 -0
  62. package/test/player_unit.js +87 -0
  63. package/test/test/util/simple_fakes.js +3 -0
  64. package/test/text/vtt_text_parser_unit.js +23 -1
  65. package/test/util/periods_unit.js +149 -11
  66. package/test/util/xml_utils_unit.js +64 -0
  67. package/ui/controls.js +4 -1
  68. package/ui/settings_menu.js +11 -2
@@ -49,8 +49,6 @@ shaka.text.TtmlTextParser = class {
49
49
  const ttsNs = TtmlTextParser.styleNs_;
50
50
  const str = shaka.util.StringUtils.fromUTF8(data);
51
51
  const cues = [];
52
- const parser = new DOMParser();
53
- let xml = null;
54
52
 
55
53
  // dont try to parse empty string as
56
54
  // DOMParser will not throw error but return an errored xml
@@ -58,9 +56,8 @@ shaka.text.TtmlTextParser = class {
58
56
  return cues;
59
57
  }
60
58
 
61
- try {
62
- xml = parser.parseFromString(str, 'text/xml');
63
- } catch (exception) {
59
+ const tt = XmlUtils.parseXmlString(str, 'tt');
60
+ if (!tt) {
64
61
  throw new shaka.util.Error(
65
62
  shaka.util.Error.Severity.CRITICAL,
66
63
  shaka.util.Error.Category.TEXT,
@@ -68,110 +65,89 @@ shaka.text.TtmlTextParser = class {
68
65
  'Failed to parse TTML.');
69
66
  }
70
67
 
71
- if (xml) {
72
- const parserError = xml.getElementsByTagName('parsererror')[0];
73
- if (parserError) {
74
- throw new shaka.util.Error(
75
- shaka.util.Error.Severity.CRITICAL,
76
- shaka.util.Error.Category.TEXT,
77
- shaka.util.Error.Code.INVALID_XML,
78
- parserError.textContent);
79
- }
80
-
81
- const tt = xml.getElementsByTagName('tt')[0];
82
- // TTML should always have tt element.
83
- if (!tt) {
84
- throw new shaka.util.Error(
85
- shaka.util.Error.Severity.CRITICAL,
86
- shaka.util.Error.Category.TEXT,
87
- shaka.util.Error.Code.INVALID_XML,
88
- 'TTML does not contain <tt> tag.');
89
- }
90
-
91
- const body = tt.getElementsByTagName('body')[0];
92
- if (!body) {
93
- return [];
94
- }
68
+ const body = tt.getElementsByTagName('body')[0];
69
+ if (!body) {
70
+ return [];
71
+ }
95
72
 
96
- // Get the framerate, subFrameRate and frameRateMultiplier if applicable.
97
- const frameRate = XmlUtils.getAttributeNSList(tt, ttpNs, 'frameRate');
98
- const subFrameRate = XmlUtils.getAttributeNSList(
99
- tt, ttpNs, 'subFrameRate');
100
- const frameRateMultiplier =
101
- XmlUtils.getAttributeNSList(tt, ttpNs, 'frameRateMultiplier');
102
- const tickRate = XmlUtils.getAttributeNSList(tt, ttpNs, 'tickRate');
73
+ // Get the framerate, subFrameRate and frameRateMultiplier if applicable.
74
+ const frameRate = XmlUtils.getAttributeNSList(tt, ttpNs, 'frameRate');
75
+ const subFrameRate = XmlUtils.getAttributeNSList(
76
+ tt, ttpNs, 'subFrameRate');
77
+ const frameRateMultiplier =
78
+ XmlUtils.getAttributeNSList(tt, ttpNs, 'frameRateMultiplier');
79
+ const tickRate = XmlUtils.getAttributeNSList(tt, ttpNs, 'tickRate');
103
80
 
104
- const cellResolution = XmlUtils.getAttributeNSList(
105
- tt, ttpNs, 'cellResolution');
106
- const spaceStyle = tt.getAttribute('xml:space') || 'default';
107
- const extent = XmlUtils.getAttributeNSList(tt, ttsNs, 'extent');
81
+ const cellResolution = XmlUtils.getAttributeNSList(
82
+ tt, ttpNs, 'cellResolution');
83
+ const spaceStyle = tt.getAttribute('xml:space') || 'default';
84
+ const extent = XmlUtils.getAttributeNSList(tt, ttsNs, 'extent');
108
85
 
109
- if (spaceStyle != 'default' && spaceStyle != 'preserve') {
110
- throw new shaka.util.Error(
111
- shaka.util.Error.Severity.CRITICAL,
112
- shaka.util.Error.Category.TEXT,
113
- shaka.util.Error.Code.INVALID_XML,
114
- 'Invalid xml:space value: ' + spaceStyle);
115
- }
116
- const whitespaceTrim = spaceStyle == 'default';
86
+ if (spaceStyle != 'default' && spaceStyle != 'preserve') {
87
+ throw new shaka.util.Error(
88
+ shaka.util.Error.Severity.CRITICAL,
89
+ shaka.util.Error.Category.TEXT,
90
+ shaka.util.Error.Code.INVALID_XML,
91
+ 'Invalid xml:space value: ' + spaceStyle);
92
+ }
93
+ const whitespaceTrim = spaceStyle == 'default';
117
94
 
118
- const rateInfo = new TtmlTextParser.RateInfo_(
119
- frameRate, subFrameRate, frameRateMultiplier, tickRate);
95
+ const rateInfo = new TtmlTextParser.RateInfo_(
96
+ frameRate, subFrameRate, frameRateMultiplier, tickRate);
120
97
 
121
- const cellResolutionInfo =
122
- TtmlTextParser.getCellResolution_(cellResolution);
98
+ const cellResolutionInfo =
99
+ TtmlTextParser.getCellResolution_(cellResolution);
123
100
 
124
- const metadata = tt.getElementsByTagName('metadata')[0];
125
- const metadataElements = metadata ? XmlUtils.getChildren(metadata) : [];
126
- const styles = Array.from(tt.getElementsByTagName('style'));
127
- const regionElements = Array.from(tt.getElementsByTagName('region'));
101
+ const metadata = tt.getElementsByTagName('metadata')[0];
102
+ const metadataElements = metadata ? XmlUtils.getChildren(metadata) : [];
103
+ const styles = Array.from(tt.getElementsByTagName('style'));
104
+ const regionElements = Array.from(tt.getElementsByTagName('region'));
128
105
 
129
- const cueRegions = [];
130
- for (const region of regionElements) {
131
- const cueRegion =
132
- TtmlTextParser.parseCueRegion_(region, styles, extent);
133
- if (cueRegion) {
134
- cueRegions.push(cueRegion);
135
- }
106
+ const cueRegions = [];
107
+ for (const region of regionElements) {
108
+ const cueRegion =
109
+ TtmlTextParser.parseCueRegion_(region, styles, extent);
110
+ if (cueRegion) {
111
+ cueRegions.push(cueRegion);
136
112
  }
113
+ }
137
114
 
138
- // A <body> element should only contain <div> elements, not <p> or <span>
139
- // elements. We used to allow this, but it is non-compliant, and the
140
- // loose nature of our previous parser made it difficult to implement TTML
141
- // nesting more fully.
142
- if (XmlUtils.findChildren(body, 'p').length) {
115
+ // A <body> element should only contain <div> elements, not <p> or <span>
116
+ // elements. We used to allow this, but it is non-compliant, and the
117
+ // loose nature of our previous parser made it difficult to implement TTML
118
+ // nesting more fully.
119
+ if (XmlUtils.findChildren(body, 'p').length) {
120
+ throw new shaka.util.Error(
121
+ shaka.util.Error.Severity.CRITICAL,
122
+ shaka.util.Error.Category.TEXT,
123
+ shaka.util.Error.Code.INVALID_TEXT_CUE,
124
+ '<p> can only be inside <div> in TTML');
125
+ }
126
+
127
+ for (const div of XmlUtils.findChildren(body, 'div')) {
128
+ // A <div> element should only contain <p>, not <span>.
129
+ if (XmlUtils.findChildren(div, 'span').length) {
143
130
  throw new shaka.util.Error(
144
131
  shaka.util.Error.Severity.CRITICAL,
145
132
  shaka.util.Error.Category.TEXT,
146
133
  shaka.util.Error.Code.INVALID_TEXT_CUE,
147
- '<p> can only be inside <div> in TTML');
148
- }
149
-
150
- for (const div of XmlUtils.findChildren(body, 'div')) {
151
- // A <div> element should only contain <p>, not <span>.
152
- if (XmlUtils.findChildren(div, 'span').length) {
153
- throw new shaka.util.Error(
154
- shaka.util.Error.Severity.CRITICAL,
155
- shaka.util.Error.Category.TEXT,
156
- shaka.util.Error.Code.INVALID_TEXT_CUE,
157
- '<span> can only be inside <p> in TTML');
158
- }
134
+ '<span> can only be inside <p> in TTML');
159
135
  }
136
+ }
160
137
 
161
- const cue = TtmlTextParser.parseCue_(
162
- body, time.periodStart, rateInfo, metadataElements, styles,
163
- regionElements, cueRegions, whitespaceTrim,
164
- cellResolutionInfo, /* parentCueElement= */ null,
165
- /* isContent= */ false);
166
- if (cue) {
167
- // According to the TTML spec, backgrounds default to transparent.
168
- // So default the background of the top-level element to transparent.
169
- // Nested elements may override that background color already.
170
- if (!cue.backgroundColor) {
171
- cue.backgroundColor = 'transparent';
172
- }
173
- cues.push(cue);
138
+ const cue = TtmlTextParser.parseCue_(
139
+ body, time.periodStart, rateInfo, metadataElements, styles,
140
+ regionElements, cueRegions, whitespaceTrim,
141
+ cellResolutionInfo, /* parentCueElement= */ null,
142
+ /* isContent= */ false);
143
+ if (cue) {
144
+ // According to the TTML spec, backgrounds default to transparent.
145
+ // So default the background of the top-level element to transparent.
146
+ // Nested elements may override that background color already.
147
+ if (!cue.backgroundColor) {
148
+ cue.backgroundColor = 'transparent';
174
149
  }
150
+ cues.push(cue);
175
151
  }
176
152
 
177
153
  return cues;
@@ -105,7 +105,7 @@ shaka.text.VttTextParser = class {
105
105
 
106
106
  const rolloverSeconds =
107
107
  shaka.text.VttTextParser.TS_ROLLOVER_ / mpegTimescale;
108
- let segmentStart = time.segmentStart;
108
+ let segmentStart = time.segmentStart - time.periodStart;
109
109
  while (segmentStart >= rolloverSeconds) {
110
110
  segmentStart -= rolloverSeconds;
111
111
  mpegTime += shaka.text.VttTextParser.TS_ROLLOVER_;
package/lib/util/error.js CHANGED
@@ -1013,6 +1013,13 @@ shaka.util.Error.Code = {
1013
1013
  */
1014
1014
  'MODIFY_OPERATION_NOT_SUPPORTED': 9016,
1015
1015
 
1016
+ /**
1017
+ * When attempting to open an indexedDB instance, nothing happened for long
1018
+ * enough for us to time out. This keeps the storage mechanism from hanging
1019
+ * indefinitely, if neither the success nor error callbacks are called.
1020
+ */
1021
+ 'INDEXED_DB_INIT_TIMED_OUT': 9017,
1022
+
1016
1023
  /**
1017
1024
  * CS IMA SDK, required for ad insertion, has not been included on the page.
1018
1025
  */
@@ -168,6 +168,7 @@ shaka.util.ManifestParserUtils.VIDEO_CODEC_REGEXPS_ = [
168
168
  /^hvc/,
169
169
  /^vp0?[89]/,
170
170
  /^av01/,
171
+ /^dvhe/,
171
172
  ];
172
173
 
173
174
 
@@ -183,6 +184,9 @@ shaka.util.ManifestParserUtils.AUDIO_CODEC_REGEXPS_ = [
183
184
  /^flac$/,
184
185
  /^mp4a/,
185
186
  /^[ae]c-3$/,
187
+ /^ac-4$/,
188
+ /^dts[cx]$/, // DTS Surround Sound
189
+ /^[du]dts$/, // DTS Surround Sound
186
190
  ];
187
191
 
188
192
 
@@ -1180,15 +1180,6 @@ shaka.util.PeriodCombiner = class {
1180
1180
  return false;
1181
1181
  }
1182
1182
 
1183
- // If the language doesn't match, but the candidate is the "primary"
1184
- // language, then that should be preferred as a fallback.
1185
- if (!best.primary && candidate.primary) {
1186
- return true;
1187
- }
1188
- if (best.primary && !candidate.primary) {
1189
- return false;
1190
- }
1191
-
1192
1183
  // If language-based differences haven't decided this, look at roles. If
1193
1184
  // the candidate has more roles in common with the output, upgrade to the
1194
1185
  // candidate.
@@ -1222,6 +1213,15 @@ shaka.util.PeriodCombiner = class {
1222
1213
  return false;
1223
1214
  }
1224
1215
 
1216
+ // If the language doesn't match, but the candidate is the "primary"
1217
+ // language, then that should be preferred as a fallback.
1218
+ if (!best.primary && candidate.primary) {
1219
+ return true;
1220
+ }
1221
+ if (best.primary && !candidate.primary) {
1222
+ return false;
1223
+ }
1224
+
1225
1225
  // If language-based and role-based features are equivalent, take the audio
1226
1226
  // with the closes channel count to the output.
1227
1227
  const channelsBetterOrWorse =
@@ -163,16 +163,6 @@ 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
166
  /**
177
167
  * Returns a major version number for Chrome, or Chromium-based browsers.
178
168
  *
@@ -8,6 +8,7 @@ goog.provide('shaka.util.XmlUtils');
8
8
 
9
9
  goog.require('goog.asserts');
10
10
  goog.require('shaka.log');
11
+ goog.require('shaka.util.Lazy');
11
12
  goog.require('shaka.util.StringUtils');
12
13
 
13
14
 
@@ -348,19 +349,21 @@ shaka.util.XmlUtils = class {
348
349
  */
349
350
  static parseXmlString(xmlString, expectedRootElemName) {
350
351
  const parser = new DOMParser();
351
- let xml = null;
352
+ const unsafeXmlString =
353
+ shaka.util.XmlUtils.trustedHTMLFromString_.value()(xmlString);
354
+ let unsafeXml = null;
352
355
  try {
353
- xml = parser.parseFromString(xmlString, 'text/xml');
356
+ unsafeXml = parser.parseFromString(unsafeXmlString, 'text/xml');
354
357
  } catch (exception) {
355
358
  shaka.log.error('XML parsing exception:', exception);
356
359
  return null;
357
360
  }
358
361
 
359
362
  // According to MDN, parseFromString never returns null.
360
- goog.asserts.assert(xml, 'Parsed XML document cannot be null!');
363
+ goog.asserts.assert(unsafeXml, 'Parsed XML document cannot be null!');
361
364
 
362
365
  // Check for empty documents.
363
- const rootElem = xml.documentElement;
366
+ const rootElem = unsafeXml.documentElement;
364
367
  if (!rootElem) {
365
368
  shaka.log.error('XML document was empty!');
366
369
  return null;
@@ -374,13 +377,28 @@ shaka.util.XmlUtils = class {
374
377
  }
375
378
 
376
379
  // The top-level element in the loaded XML should have the name we expect.
377
- if (xml.documentElement.tagName != expectedRootElemName) {
380
+ if (rootElem.tagName != expectedRootElemName) {
378
381
  shaka.log.error(
379
382
  `XML tag name does not match expected "${expectedRootElemName}":`,
380
- xml.documentElement.tagName);
383
+ rootElem.tagName);
381
384
  return null;
382
385
  }
383
386
 
387
+ // SECURITY: Verify that the document does not contain elements from the
388
+ // HTML or SVG namespaces, which could trigger script execution and XSS.
389
+ const iterator = document.createNodeIterator(
390
+ unsafeXml,
391
+ NodeFilter.SHOW_ALL,
392
+ );
393
+ let currentNode;
394
+ while (currentNode = iterator.nextNode()) {
395
+ if (currentNode instanceof HTMLElement ||
396
+ currentNode instanceof SVGElement) {
397
+ shaka.log.error('XML document embeds unsafe content!');
398
+ return null;
399
+ }
400
+ }
401
+
384
402
  return rootElem;
385
403
  }
386
404
 
@@ -402,3 +420,24 @@ shaka.util.XmlUtils = class {
402
420
  }
403
421
  }
404
422
  };
423
+
424
+ /**
425
+ * Promote a string to TrustedHTML. This function is security-sensitive and
426
+ * should only be used with security approval where the string is guaranteed not
427
+ * to cause an XSS vulnerability.
428
+ *
429
+ * @private {!shaka.util.Lazy.<function(!string): (!TrustedHTML|!string)>}
430
+ */
431
+ shaka.util.XmlUtils.trustedHTMLFromString_ = new shaka.util.Lazy(() => {
432
+ if (typeof trustedTypes !== 'undefined') {
433
+ // Create a Trusted Types policy for promoting the string to TrustedHTML.
434
+ // The Lazy wrapper ensures this policy is only created once.
435
+ const policy = trustedTypes.createPolicy('shaka-player#xml', {
436
+ createHTML: (s) => s,
437
+ });
438
+ return (s) => policy.createHTML(s);
439
+ }
440
+ // Fall back to strings in environments that don't support Trusted Types.
441
+ return (s) => s;
442
+ });
443
+
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": "4.2.2",
4
+ "version": "4.2.4",
5
5
  "homepage": "https://github.com/shaka-project/shaka-player",
6
6
  "author": "Google",
7
7
  "maintainers": [
package/roadmap.md CHANGED
@@ -9,28 +9,46 @@ The goals of future milestones are fluid until we begin that development cycle,
9
9
  so the exact milestone for future features is not pre-determined.
10
10
 
11
11
  Priority features up next:
12
- - Preload API
13
- https://github.com/shaka-project/shaka-player/issues/880
12
+ - HLS improvements
13
+ https://github.com/shaka-project/shaka-player/projects/6
14
14
 
15
15
  Candidate features for future release cycles:
16
+ - Preload API
17
+ https://github.com/shaka-project/shaka-player/issues/880
16
18
  - Official TypeScript defs, generated from source
17
19
  https://github.com/shaka-project/shaka-player/issues/1030
18
20
  - Codec-switching
19
21
  https://github.com/shaka-project/shaka-player/issues/1528
20
22
  - Background fetch for offline storage
21
23
  https://github.com/shaka-project/shaka-player/issues/879
24
+ - Smaller, more modular binary
25
+
26
+ v4.3 - 2022 Q4 / 2023 Q1
22
27
  - Support ID3 tags in mp4
23
28
  https://github.com/shaka-project/shaka-player/issues/3351
24
- - Smaller, more modular binary
29
+ - Lazy-load HLS playlists on adaptation
30
+ https://github.com/shaka-project/shaka-player/issues/1936
25
31
 
26
- v3.4 - 2022 Q1 or Q2
32
+ =====
33
+
34
+ v4.2 - 2022 Q3
35
+ - Automatic ABR quality restrictions based on size
36
+ https://github.com/shaka-project/shaka-player/issues/2333
37
+ - HLS support for AES-128 encryption
38
+ https://github.com/shaka-project/shaka-player/issues/850
39
+
40
+ v4.1 - 2022 Q2
41
+ - HLS support for EXT-X-GAP
42
+ https://github.com/shaka-project/shaka-player/issues/1308
43
+ - Temporarily disable the active variant after `HTTP_ERROR`
44
+ https://github.com/shaka-project/shaka-player/issues/1542
45
+
46
+ v4.0 - 2022 Q2
27
47
  - MSE support for FairPlay
28
48
  https://github.com/shaka-project/shaka-player/issues/3346
29
49
  - Support containerless formats
30
50
  https://github.com/shaka-project/shaka-player/issues/2337
31
51
 
32
- =====
33
-
34
52
  v3.3 - 2022 Q1
35
53
  - Common Media Client Data (CMCD) logging support
36
54
  https://github.com/shaka-project/shaka-player/issues/3619
@@ -34,6 +34,8 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
34
34
 
35
35
  /** @type {shaka.util.PublicPromise} */
36
36
  let messageWaitPromise;
37
+ /** @type {Array.<string>} */
38
+ let pendingMessages = null;
37
39
 
38
40
  /** @type {!Array.<function()>} */
39
41
  let toRestore;
@@ -91,6 +93,9 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
91
93
  toRestore = [];
92
94
  pendingWaitWrapperCalls = 0;
93
95
 
96
+ messageWaitPromise = null;
97
+ pendingMessages = null;
98
+
94
99
  fakeInitState = {
95
100
  player: {
96
101
  configure: {},
@@ -118,6 +123,12 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
118
123
  player = null;
119
124
  video = null;
120
125
  receiver = null;
126
+
127
+ if (messageWaitPromise) {
128
+ messageWaitPromise.resolve([]);
129
+ }
130
+ messageWaitPromise = null;
131
+ pendingMessages = null;
121
132
  });
122
133
 
123
134
  afterAll(() => {
@@ -128,6 +139,66 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
128
139
  }
129
140
  });
130
141
 
142
+ describe('without drm', () => {
143
+ it('sends reasonably-sized updates', async () => {
144
+ // Use an unencrypted asset.
145
+ fakeInitState.manifest = 'test:sintel';
146
+
147
+ const p = waitForLoadedData();
148
+
149
+ // Start the process of loading by sending a fake init message.
150
+ fakeConnectedSenders(1);
151
+ fakeIncomingMessage({
152
+ type: 'init',
153
+ initState: fakeInitState,
154
+ appData: {},
155
+ }, mockShakaMessageBus);
156
+
157
+ await p;
158
+ // Wait for an update message.
159
+ const messages = await waitForUpdateMessages();
160
+ for (const message of messages) {
161
+ // Check that the update message is of a reasonable size. From previous
162
+ // testing we found that the socket would silently reject data that got
163
+ // too big. 6KB is safely below the limit.
164
+ expect(message.length).toBeLessThan(6000);
165
+ }
166
+ });
167
+
168
+ it('has reasonable average message size', async () => {
169
+ // Use an unencrypted asset.
170
+ fakeInitState.manifest = 'test:sintel';
171
+
172
+ const p = waitForLoadedData();
173
+
174
+ // Start the process of loading by sending a fake init message.
175
+ fakeConnectedSenders(1);
176
+ fakeIncomingMessage({
177
+ type: 'init',
178
+ initState: fakeInitState,
179
+ appData: {},
180
+ }, mockShakaMessageBus);
181
+
182
+ await p;
183
+
184
+ // Collect messages over 50 update cycles, and average their length.
185
+ // Not all properties are passed along on every update message, so
186
+ // the average length is expected to be lower than the length of the first
187
+ // update message.
188
+ let totalLength = 0;
189
+ let totalMessages = 0;
190
+ for (let i = 0; i < 50; i++) {
191
+ // eslint-disable-next-line no-await-in-loop
192
+ const messages = await waitForUpdateMessages();
193
+ for (const message of messages) {
194
+ totalLength += message.length;
195
+ totalMessages += 1;
196
+ }
197
+ }
198
+ expect(totalLength / totalMessages).toBeLessThan(2000);
199
+ });
200
+ });
201
+
131
202
  filterDescribe('with drm', () => support['com.widevine.alpha'], () => {
132
203
  drmIt('sends reasonably-sized updates', async () => {
133
204
  // Use an encrypted asset, to make sure DRM info doesn't balloon the size.
@@ -150,11 +221,13 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
150
221
 
151
222
  await p;
152
223
  // Wait for an update message.
153
- const message = await waitForUpdateMessage();
154
- // Check that the update message is of a reasonable size. From previous
155
- // testing we found that the socket would silently reject data that got
156
- // too big. 6KB is safely below the limit.
157
- expect(message.length).toBeLessThan(7 * 1024);
224
+ const messages = await waitForUpdateMessages();
225
+ for (const message of messages) {
226
+ // Check that the update message is of a reasonable size. From previous
227
+ // testing we found that the socket would silently reject data that got
228
+ // too big. 6KB is safely below the limit.
229
+ expect(message.length).toBeLessThan(6000);
230
+ }
158
231
  });
159
232
 
160
233
  drmIt('has reasonable average message size', async () => {
@@ -177,17 +250,22 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
177
250
  }, mockShakaMessageBus);
178
251
 
179
252
  await p;
180
- // Collect 50 update messages, and average their length.
253
+
254
+ // Collect messages over 50 update cycles, and average their length.
181
255
  // Not all properties are passed along on every update message, so
182
256
  // the average length is expected to be lower than the length of the first
183
257
  // update message.
184
258
  let totalLength = 0;
259
+ let totalMessages = 0;
185
260
  for (let i = 0; i < 50; i++) {
186
261
  // eslint-disable-next-line no-await-in-loop
187
- const message = await waitForUpdateMessage();
188
- totalLength += message.length;
262
+ const messages = await waitForUpdateMessages();
263
+ for (const message of messages) {
264
+ totalLength += message.length;
265
+ totalMessages += 1;
266
+ }
189
267
  }
190
- expect(totalLength / 50).toBeLessThan(3000);
268
+ expect(totalLength / totalMessages).toBeLessThan(2000);
191
269
  });
192
270
  });
193
271
 
@@ -227,12 +305,12 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
227
305
  expect(pendingWaitWrapperCalls).toBe(0);
228
306
 
229
307
  // Wait for a final update message before proceeding.
230
- await waitForUpdateMessage();
308
+ await waitForUpdateMessages();
231
309
  });
232
310
 
233
311
  /**
234
312
  * Creates a wrapper around a method on a given prototype, which makes it
235
- * wait on waitForUpdateMessage before returning, and registers that wrapper
313
+ * wait on waitForUpdateMessages before returning, and registers that wrapper
236
314
  * to be uninstalled afterwards.
237
315
  * The replaced method is expected to be a method that returns a promise.
238
316
  * @param {!Object} prototype
@@ -249,7 +327,7 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
249
327
  'Waiting for update message before calling ' +
250
328
  name + '.' + methodName + '...');
251
329
  const originalArguments = Array.from(arguments);
252
- await waitForUpdateMessage();
330
+ await waitForUpdateMessages();
253
331
  // eslint-disable-next-line no-restricted-syntax
254
332
  return original.apply(this, originalArguments);
255
333
  };
@@ -265,7 +343,8 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
265
343
  });
266
344
  }
267
345
 
268
- function waitForUpdateMessage() {
346
+ function waitForUpdateMessages() {
347
+ pendingMessages = [];
269
348
  messageWaitPromise = new shaka.util.PublicPromise();
270
349
  return messageWaitPromise;
271
350
  }
@@ -316,10 +395,12 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
316
395
  bus.messages.push(CastUtils.deserialize(message));
317
396
  // Check to see if it's an update message.
318
397
  const parsed = CastUtils.deserialize(message);
319
- if (parsed.type == 'update' && messageWaitPromise) {
398
+ if (parsed.type == 'update' && pendingMessages) {
320
399
  shaka.log.debug('Received update message. Proceeding...');
321
- messageWaitPromise.resolve(message);
322
- messageWaitPromise = null;
400
+ // The code waiting on this Promise will get an array of all of the
401
+ // messages processed within this tick.
402
+ pendingMessages.push(message);
403
+ messageWaitPromise.resolve(pendingMessages);
323
404
  }
324
405
  });
325
406
  const channel = {