shaka-player 3.3.1 → 3.3.2

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 (49) hide show
  1. package/.github/workflows/build_and_test.yaml +73 -0
  2. package/.github/workflows/release.yaml +53 -0
  3. package/CHANGELOG.md +13 -1
  4. package/build/checkversion.py +1 -1
  5. package/build/test.py +6 -0
  6. package/dist/controls.css +1 -1
  7. package/dist/deps.js +4 -4
  8. package/dist/locales.js +29 -29
  9. package/dist/shaka-player.compiled.d.ts +2 -1
  10. package/dist/shaka-player.compiled.debug.d.ts +2 -1
  11. package/dist/shaka-player.compiled.debug.externs.js +17 -12
  12. package/dist/shaka-player.compiled.debug.js +93 -90
  13. package/dist/shaka-player.compiled.debug.map +4 -4
  14. package/dist/shaka-player.compiled.externs.js +17 -12
  15. package/dist/shaka-player.compiled.js +687 -686
  16. package/dist/shaka-player.compiled.map +3 -3
  17. package/dist/shaka-player.ui.d.ts +4 -12
  18. package/dist/shaka-player.ui.debug.d.ts +4 -12
  19. package/dist/shaka-player.ui.debug.externs.js +19 -23
  20. package/dist/shaka-player.ui.debug.js +107 -105
  21. package/dist/shaka-player.ui.debug.map +4 -4
  22. package/dist/shaka-player.ui.externs.js +19 -23
  23. package/dist/shaka-player.ui.js +952 -952
  24. package/dist/shaka-player.ui.map +2 -2
  25. package/karma.conf.js +19 -15
  26. package/lib/ads/ad_manager.js +1 -0
  27. package/lib/cast/cast_proxy.js +3 -0
  28. package/lib/cast/cast_receiver.js +3 -0
  29. package/lib/dash/dash_parser.js +31 -19
  30. package/lib/hls/manifest_text_parser.js +1 -0
  31. package/lib/media/quality_observer.js +17 -34
  32. package/lib/media/region_observer.js +39 -33
  33. package/lib/media/region_timeline.js +17 -19
  34. package/lib/media/streaming_engine.js +1 -1
  35. package/lib/net/networking_engine.js +4 -0
  36. package/lib/offline/storage.js +8 -9
  37. package/lib/player.js +32 -10
  38. package/lib/util/fake_event_target.js +21 -1
  39. package/package.json +3 -6
  40. package/test/dash/dash_parser_live_unit.js +43 -0
  41. package/test/dash/dash_parser_manifest_unit.js +2 -1
  42. package/test/hls/hls_parser_unit.js +74 -0
  43. package/test/media/quality_observer_unit.js +4 -1
  44. package/test/media/region_observer_unit.js +40 -5
  45. package/test/media/region_timeline_unit.js +40 -22
  46. package/test/test/util/util.js +10 -7
  47. package/test/ui/text_displayer_layout_unit.js +8 -7
  48. package/ui/controls.js +3 -0
  49. package/ui/localization.js +12 -36
package/lib/player.js CHANGED
@@ -787,6 +787,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
787
787
  await this.networkingEngine_.destroy();
788
788
  this.networkingEngine_ = null;
789
789
  }
790
+
791
+ // FakeEventTarget implements IReleasable
792
+ super.release();
790
793
  }
791
794
 
792
795
  /**
@@ -1691,17 +1694,26 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1691
1694
  // initialize it now even through it appears a little out-of-place.
1692
1695
  this.regionTimeline_ =
1693
1696
  new shaka.media.RegionTimeline(() => this.seekRange());
1694
- this.regionTimeline_.setListeners(/* onRegionAdded= */ (region) => {
1697
+ this.regionTimeline_.addEventListener('regionadd', (event) => {
1698
+ /** @type {shaka.extern.TimelineRegionInfo} */
1699
+ const region = event['region'];
1695
1700
  this.onRegionEvent_(shaka.Player.EventName.TimelineRegionAdded, region);
1701
+
1696
1702
  if (this.adManager_) {
1697
1703
  this.adManager_.onDashTimedMetadata(region);
1698
1704
  }
1699
1705
  });
1706
+
1700
1707
  this.qualityObserver_ = null;
1701
1708
  if (this.config_.streaming.observeQualityChanges) {
1702
1709
  this.qualityObserver_ = new shaka.media.QualityObserver(
1703
1710
  () => this.getBufferedInfo());
1704
- this.qualityObserver_.setListeners((mediaQualityInfo, position) => {
1711
+
1712
+ this.qualityObserver_.addEventListener('qualitychange', (event) => {
1713
+ /** @type {shaka.extern.MediaQualityInfo} */
1714
+ const mediaQualityInfo = event['quality'];
1715
+ /** @type {number} */
1716
+ const position = event['position'];
1705
1717
  this.onMediaQualityChange_(mediaQualityInfo, position);
1706
1718
  });
1707
1719
  }
@@ -2674,21 +2686,31 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2674
2686
  // Create the region observer. This will allow us to notify the app when we
2675
2687
  // move in and out of timeline regions.
2676
2688
  const regionObserver = new shaka.media.RegionObserver(this.regionTimeline_);
2677
- const onEnterRegion = (region, seeking) => {
2689
+
2690
+ regionObserver.addEventListener('enter', (event) => {
2691
+ /** @type {shaka.extern.TimelineRegionInfo} */
2692
+ const region = event['region'];
2678
2693
  this.onRegionEvent_(shaka.Player.EventName.TimelineRegionEnter, region);
2679
- };
2680
- const onExitRegion = (region, seeking) => {
2694
+ });
2695
+
2696
+ regionObserver.addEventListener('exit', (event) => {
2697
+ /** @type {shaka.extern.TimelineRegionInfo} */
2698
+ const region = event['region'];
2681
2699
  this.onRegionEvent_(shaka.Player.EventName.TimelineRegionExit, region);
2682
- };
2683
- const onSkipRegion = (region, seeking) => {
2700
+ });
2701
+
2702
+ regionObserver.addEventListener('skip', (event) => {
2703
+ /** @type {shaka.extern.TimelineRegionInfo} */
2704
+ const region = event['region'];
2705
+ /** @type {boolean} */
2706
+ const seeking = event['seeking'];
2684
2707
  // If we are seeking, we don't want to surface the enter/exit events since
2685
2708
  // they didn't play through them.
2686
2709
  if (!seeking) {
2687
2710
  this.onRegionEvent_(shaka.Player.EventName.TimelineRegionEnter, region);
2688
2711
  this.onRegionEvent_(shaka.Player.EventName.TimelineRegionExit, region);
2689
2712
  }
2690
- };
2691
- regionObserver.setListeners(onEnterRegion, onExitRegion, onSkipRegion);
2713
+ });
2692
2714
 
2693
2715
  // Now that we have all our observers, create a manager for them.
2694
2716
  const manager = new shaka.media.PlayheadObserverManager(this.video_);
@@ -6536,7 +6558,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
6536
6558
  * @define {string} A version number taken from git at compile time.
6537
6559
  * @export
6538
6560
  */
6539
- shaka.Player.version = 'v3.3.1-uncompiled';
6561
+ shaka.Player.version = 'v3.3.2-uncompiled'; // x-release-please-version
6540
6562
 
6541
6563
  // Initialize the deprecation system using the version string we just set
6542
6564
  // on the player.
@@ -9,6 +9,7 @@ goog.provide('shaka.util.FakeEventTarget');
9
9
  goog.require('goog.asserts');
10
10
  goog.require('shaka.log');
11
11
  goog.require('shaka.util.FakeEvent');
12
+ goog.require('shaka.util.IReleasable');
12
13
  goog.require('shaka.util.MultiMap');
13
14
 
14
15
 
@@ -18,13 +19,14 @@ goog.require('shaka.util.MultiMap');
18
19
  * to non-DOM classes. Only FakeEvents should be dispatched.
19
20
  *
20
21
  * @implements {EventTarget}
22
+ * @implements {shaka.util.IReleasable}
21
23
  * @exportInterface
22
24
  */
23
25
  shaka.util.FakeEventTarget = class {
24
26
  /** */
25
27
  constructor() {
26
28
  /**
27
- * @private {!shaka.util.MultiMap.<shaka.util.FakeEventTarget.ListenerType>}
29
+ * @private {shaka.util.MultiMap.<shaka.util.FakeEventTarget.ListenerType>}
28
30
  */
29
31
  this.listeners_ = new shaka.util.MultiMap();
30
32
 
@@ -46,6 +48,9 @@ shaka.util.FakeEventTarget = class {
46
48
  * @exportInterface
47
49
  */
48
50
  addEventListener(type, listener, options) {
51
+ if (!this.listeners_) {
52
+ return;
53
+ }
49
54
  this.listeners_.push(type, listener);
50
55
  }
51
56
 
@@ -73,6 +78,9 @@ shaka.util.FakeEventTarget = class {
73
78
  * @exportInterface
74
79
  */
75
80
  removeEventListener(type, listener, options) {
81
+ if (!this.listeners_) {
82
+ return;
83
+ }
76
84
  this.listeners_.remove(type, listener);
77
85
  }
78
86
 
@@ -90,6 +98,10 @@ shaka.util.FakeEventTarget = class {
90
98
  goog.asserts.assert(event instanceof shaka.util.FakeEvent,
91
99
  'FakeEventTarget can only dispatch FakeEvents!');
92
100
 
101
+ if (!this.listeners_) {
102
+ return true;
103
+ }
104
+
93
105
  let listeners = this.listeners_.get(event.type) || [];
94
106
  const universalListeners =
95
107
  this.listeners_.get(shaka.util.FakeEventTarget.ALL_EVENTS_);
@@ -129,6 +141,14 @@ shaka.util.FakeEventTarget = class {
129
141
 
130
142
  return event.defaultPrevented;
131
143
  }
144
+
145
+ /**
146
+ * @override
147
+ * @exportInterface
148
+ */
149
+ release() {
150
+ this.listeners_ = null;
151
+ }
132
152
  };
133
153
 
134
154
  /**
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.1",
4
+ "version": "3.3.2",
5
5
  "homepage": "https://github.com/google/shaka-player",
6
6
  "author": "Google",
7
7
  "maintainers": [
@@ -11,8 +11,6 @@
11
11
  }
12
12
  ],
13
13
  "devDependencies": {
14
- "@chiragrupani/karma-chromium-edge-launcher": "^2.1.0",
15
- "@onslip/karma-safari-launcher": "^1.0.1",
16
14
  "@teppeis/clutz": "^1.0.29-4c95e12.v20190929",
17
15
  "awesomplete": "^1.1.4",
18
16
  "babel-core": "^6.26.3",
@@ -37,13 +35,11 @@
37
35
  "jsdoc": "github:joeyparrish/jsdoc#45c271fa",
38
36
  "karma": "^6.2.0",
39
37
  "karma-babel-preprocessor": "^7.0.0",
40
- "karma-chrome-launcher": "^2.2.0",
41
38
  "karma-coverage": "^1.1.2",
42
- "karma-edge-launcher": "^0.4.2",
43
- "karma-firefox-launcher": "^1.1.0",
44
39
  "karma-ie-launcher": "^1.0.0",
45
40
  "karma-jasmine": "^3.3.1",
46
41
  "karma-jasmine-ajax": "^0.1.13",
42
+ "karma-local-wd-launcher": "^1.4.1",
47
43
  "karma-opera-launcher": "^1.0.0",
48
44
  "karma-sourcemap-loader": "^0.3.7",
49
45
  "karma-spec-reporter": "0.0.32",
@@ -55,6 +51,7 @@
55
51
  "pwacompat": "^2.0.10",
56
52
  "rimraf": "^2.6.3",
57
53
  "sprintf-js": "^1.1.2",
54
+ "ssim.js": "^3.5.0",
58
55
  "stylelint": "^13.8.0",
59
56
  "stylelint-config-standard": "^20.0.0",
60
57
  "tippy.js": "^4.3.1",
@@ -1389,6 +1389,49 @@ describe('DashParser Live', () => {
1389
1389
  .toHaveBeenCalledWith(
1390
1390
  jasmine.objectContaining({id: '2', startTime: 10, endTime: 25}));
1391
1391
  });
1392
+
1393
+ it('will not add timeline regions outside the DVR window', async () => {
1394
+ const manifest = [
1395
+ '<MPD type="dynamic" minimumUpdatePeriod="PT' + updateTime + 'S"',
1396
+ ' availabilityStartTime="1970-01-01T00:00:00Z"',
1397
+ ' suggestedPresentationDelay="PT0S"',
1398
+ ' timeShiftBufferDepth="PT30S">',
1399
+ ' <Period id="1">',
1400
+ ' <EventStream schemeIdUri="http://example.com" timescale="1">',
1401
+ ' <Event id="0" presentationTime="0" duration="10"/>',
1402
+ ' <Event id="1" presentationTime="10" duration="10"/>',
1403
+ ' <Event id="2" presentationTime="20" duration="10"/>',
1404
+ ' <Event id="3" presentationTime="30" duration="10"/>',
1405
+ ' <Event id="4" presentationTime="40" duration="10"/>',
1406
+ ' </EventStream>',
1407
+ ' <AdaptationSet mimeType="video/mp4">',
1408
+ ' <Representation bandwidth="1">',
1409
+ ' <SegmentTemplate startNumber="1" media="s$Number$.mp4"',
1410
+ ' duration="2" />',
1411
+ ' </Representation>',
1412
+ ' </AdaptationSet>',
1413
+ ' </Period>',
1414
+ '</MPD>',
1415
+ ].join('\n');
1416
+
1417
+ // 45 seconds after the epoch, which is also 45 seconds after the
1418
+ // presentation started. Only the last 30 seconds are available, from
1419
+ // time 15 to 45.
1420
+ Date.now = () => 45 * 1000;
1421
+
1422
+ fakeNetEngine.setResponseText('dummy://foo', manifest);
1423
+ await parser.start('dummy://foo', playerInterface);
1424
+
1425
+ expect(onTimelineRegionAddedSpy).toHaveBeenCalledTimes(4);
1426
+ expect(onTimelineRegionAddedSpy).toHaveBeenCalledWith(
1427
+ jasmine.objectContaining({id: '1'}));
1428
+ expect(onTimelineRegionAddedSpy).toHaveBeenCalledWith(
1429
+ jasmine.objectContaining({id: '2'}));
1430
+ expect(onTimelineRegionAddedSpy).toHaveBeenCalledWith(
1431
+ jasmine.objectContaining({id: '3'}));
1432
+ expect(onTimelineRegionAddedSpy).toHaveBeenCalledWith(
1433
+ jasmine.objectContaining({id: '4'}));
1434
+ });
1392
1435
  });
1393
1436
 
1394
1437
  it('honors clockSyncUri for in-progress recordings', async () => {
@@ -985,7 +985,8 @@ describe('DashParser Manifest', () => {
985
985
 
986
986
  it('duplicate Representation ids with live', async () => {
987
987
  const source = [
988
- '<MPD minBufferTime="PT75S" type="dynamic">',
988
+ '<MPD minBufferTime="PT75S" type="dynamic"',
989
+ ' availabilityStartTime="1970-01-01T00:00:00Z">',
989
990
  ' <Period id="1" duration="PT30S">',
990
991
  ' <AdaptationSet mimeType="video/mp4">',
991
992
  ' <Representation id="1" bandwidth="1">',
@@ -211,6 +211,80 @@ describe('HlsParser', () => {
211
211
  expect(actual).toEqual(manifest);
212
212
  });
213
213
 
214
+ it('parses manifest attributes with space', async () => {
215
+ const master = [
216
+ '#EXTM3U\n',
217
+ '#EXT-X-MEDIA:TYPE=AUDIO, GROUP-ID="aud1", LANGUAGE="eng", ',
218
+ 'CHANNELS="16/JOC", URI="audio"\n',
219
+ '#EXT-X-MEDIA:TYPE=SUBTITLES, GROUP-ID="sub1", LANGUAGE="eng", ',
220
+ 'URI="text"\n',
221
+ '#EXT-X-MEDIA:TYPE=SUBTITLES, GROUP-ID="sub2", LANGUAGE="es", ',
222
+ 'URI="text2"\n',
223
+ '#EXT-X-STREAM-INF:BANDWIDTH=200, CODECS="avc1,mp4a", ',
224
+ 'RESOLUTION=960x540, FRAME-RATE=60, AUDIO="aud1"\n',
225
+ 'video\n',
226
+ ].join('');
227
+
228
+ const media = [
229
+ '#EXTM3U\n',
230
+ '#EXT-X-PLAYLIST-TYPE:VOD\n',
231
+ '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
232
+ '#EXTINF:5,\n',
233
+ '#EXT-X-BYTERANGE:121090@616\n',
234
+ 'main.mp4',
235
+ ].join('');
236
+
237
+ const textMedia = [
238
+ '#EXTM3U\n',
239
+ '#EXT-X-PLAYLIST-TYPE:VOD\n',
240
+ '#EXTINF:5,\n',
241
+ '#EXT-X-BYTERANGE:121090@616\n',
242
+ 'main.vtt',
243
+ ].join('');
244
+
245
+ const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
246
+ manifest.anyTimeline();
247
+ manifest.addPartialVariant((variant) => {
248
+ variant.language = 'en';
249
+ variant.bandwidth = 200;
250
+ variant.addPartialStream(ContentType.VIDEO, (stream) => {
251
+ stream.frameRate = 60;
252
+ stream.mime('video/mp4', 'avc1');
253
+ stream.size(960, 540);
254
+ });
255
+ variant.addPartialStream(ContentType.AUDIO, (stream) => {
256
+ stream.language = 'en';
257
+ stream.channelsCount = 16;
258
+ stream.spatialAudio = true;
259
+ stream.mime('audio/mp4', 'mp4a');
260
+ });
261
+ });
262
+ manifest.addPartialTextStream((stream) => {
263
+ stream.language = 'en';
264
+ stream.kind = TextStreamKind.SUBTITLE;
265
+ stream.mime('text/vtt', '');
266
+ });
267
+ manifest.addPartialTextStream((stream) => {
268
+ stream.language = 'es';
269
+ stream.kind = TextStreamKind.SUBTITLE;
270
+ stream.mime('text/vtt', '');
271
+ });
272
+ });
273
+
274
+ fakeNetEngine
275
+ .setResponseText('test:/master', master)
276
+ .setResponseText('test:/audio', media)
277
+ .setResponseText('test:/video', media)
278
+ .setResponseText('test:/text', textMedia)
279
+ .setResponseText('test:/text2', textMedia)
280
+ .setResponseText('test:/main.vtt', vttText)
281
+ .setResponseValue('test:/init.mp4', initSegmentData)
282
+ .setResponseValue('test:/main.mp4', segmentData);
283
+
284
+ const actual = await parser.start('test:/master', playerInterface);
285
+ expect(actual).toEqual(manifest);
286
+ });
287
+
214
288
  it('prioritize AVERAGE-BANDWIDTH to BANDWIDTH', async () => {
215
289
  const master = [
216
290
  '#EXTM3U\n',
@@ -50,7 +50,10 @@ describe('QualityObserver', () => {
50
50
  beforeEach(() => {
51
51
  onQualityChange = jasmine.createSpy('onQualityChange');
52
52
  observer = new shaka.media.QualityObserver(getBufferedInfo);
53
- observer.setListeners(shaka.test.Util.spyFunc(onQualityChange));
53
+ observer.addEventListener('qualitychange', (event) => {
54
+ shaka.test.Util.spyFunc(onQualityChange)(
55
+ event['quality'], event['position']);
56
+ });
54
57
  emptyBuffer = true;
55
58
  bufferStart = 0;
56
59
  bufferEnd = 0;
@@ -15,6 +15,9 @@ describe('RegionObserver', () => {
15
15
  /** @type {!shaka.media.RegionObserver} */
16
16
  let observer;
17
17
 
18
+ /** @type {!jasmine.Spy} */
19
+ let onSeekRange;
20
+
18
21
  /** @type {!jasmine.Spy} */
19
22
  let onEnterRegion;
20
23
  /** @type {!jasmine.Spy} */
@@ -23,18 +26,26 @@ describe('RegionObserver', () => {
23
26
  let onSkipRegion;
24
27
 
25
28
  beforeEach(() => {
29
+ onSeekRange = jasmine.createSpy('onSeekRange');
30
+ onSeekRange.and.returnValue({start: 0, end: 100});
31
+
26
32
  onEnterRegion = jasmine.createSpy('onEnterRegion');
27
33
  onExitRegion = jasmine.createSpy('onExitRegion');
28
34
  onSkipRegion = jasmine.createSpy('onSkipRegion');
29
35
 
30
36
  timeline = new shaka.media.RegionTimeline(
31
- () => { return {start: 0, end: 100}; });
37
+ shaka.test.Util.spyFunc(onSeekRange));
32
38
 
33
39
  observer = new shaka.media.RegionObserver(timeline);
34
- observer.setListeners(
35
- /* onEnter= */ shaka.test.Util.spyFunc(onEnterRegion),
36
- /* onExit= */ shaka.test.Util.spyFunc(onExitRegion),
37
- /* onSkip= */ shaka.test.Util.spyFunc(onSkipRegion));
40
+ observer.addEventListener('enter', (event) => {
41
+ shaka.test.Util.spyFunc(onEnterRegion)(event['region'], event['seeking']);
42
+ });
43
+ observer.addEventListener('exit', (event) => {
44
+ shaka.test.Util.spyFunc(onExitRegion)(event['region'], event['seeking']);
45
+ });
46
+ observer.addEventListener('skip', (event) => {
47
+ shaka.test.Util.spyFunc(onSkipRegion)(event['region'], event['seeking']);
48
+ });
38
49
  });
39
50
 
40
51
  it('fires enter event when adding a region the playhead is in', () => {
@@ -313,6 +324,30 @@ describe('RegionObserver', () => {
313
324
  expect(onSkipRegion).not.toHaveBeenCalled();
314
325
  });
315
326
 
327
+ // See https://github.com/google/shaka-player/issues/3949
328
+ it('cleans up references to regions', async () => {
329
+ const region = createRegion('my-region', 0, 10);
330
+ timeline.addRegion(region);
331
+
332
+ // Poll so that RegionObserver will store regions.
333
+ poll(observer,
334
+ /* timeInSeconds= */ 5,
335
+ /* seeking= */ false);
336
+
337
+ // Hack to get a private member without the compiler complaining:
338
+ /** @type {Map} */
339
+ const regionMap = (/** @type {?} */(observer))['oldPosition_'];
340
+ expect(regionMap.size).not.toBe(0);
341
+
342
+ // Move the seek range so that the region will be removed.
343
+ onSeekRange.and.returnValue({start: 20, end: 100});
344
+ // Give the timeline time to filter regions
345
+ await shaka.test.Util.delay(
346
+ shaka.media.RegionTimeline.REGION_FILTER_INTERVAL * 2);
347
+
348
+ expect(regionMap.size).toBe(0);
349
+ });
350
+
316
351
  /**
317
352
  * @param {string} id
318
353
  * @param {number} startTimeSeconds
@@ -14,18 +14,27 @@ describe('RegionTimeline', () => {
14
14
  /** @type {!jasmine.Spy} */
15
15
  let onNewRegion;
16
16
 
17
+ /** @type {!jasmine.Spy} */
18
+ let onRemoveRegion;
19
+
17
20
  /** @type {!jasmine.Spy} */
18
21
  let onSeekRange;
19
22
 
20
23
  beforeEach(() => {
21
- onNewRegion = jasmine.createSpy('onNewRegion');
22
-
23
24
  onSeekRange = jasmine.createSpy('onSeekRange');
24
25
  onSeekRange.and.returnValue({start: 0, end: 100});
25
-
26
26
  timeline = new shaka.media.RegionTimeline(
27
27
  shaka.test.Util.spyFunc(onSeekRange));
28
- timeline.setListeners(shaka.test.Util.spyFunc(onNewRegion));
28
+
29
+ onNewRegion = jasmine.createSpy('onNewRegion');
30
+ timeline.addEventListener('regionadd', (event) => {
31
+ shaka.test.Util.spyFunc(onNewRegion)(event['region']);
32
+ });
33
+
34
+ onRemoveRegion = jasmine.createSpy('onRemoveRegion');
35
+ timeline.addEventListener('regionremove', (event) => {
36
+ shaka.test.Util.spyFunc(onRemoveRegion)(event['region']);
37
+ });
29
38
  });
30
39
 
31
40
  afterEach(() => {
@@ -85,24 +94,33 @@ describe('RegionTimeline', () => {
85
94
  ]);
86
95
  });
87
96
 
88
- it('filters out regions that end before the start of the seek range',
89
- async () => {
90
- onSeekRange.and.returnValue({start: 5, end: 100});
91
- const region1 = createRegion('urn:foo', 'my-region', 0, 3);
92
- const region2 = createRegion('urn:foo', 'my-region', 3, 10);
93
- const region3 = createRegion('urn:foo', 'my-region', 5, 10);
94
- timeline.addRegion(region1);
95
- timeline.addRegion(region2);
96
- timeline.addRegion(region3);
97
- expect(onNewRegion).toHaveBeenCalledTimes(3);
98
- let regions = Array.from(timeline.regions());
99
- expect(regions.length).toBe(3);
100
- // Give the timeline time to filter regions
101
- await shaka.test.Util.delay(
102
- shaka.media.RegionTimeline.REGION_FILTER_INTERVAL * 2);
103
- regions = Array.from(timeline.regions());
104
- expect(regions).toEqual([region2, region3]);
105
- });
97
+ it('filters regions that end before the seek range', async () => {
98
+ onSeekRange.and.returnValue({start: 5, end: 100});
99
+
100
+ const region1 = createRegion('urn:foo', 'my-region', 0, 3);
101
+ const region2 = createRegion('urn:foo', 'my-region', 3, 10);
102
+ const region3 = createRegion('urn:foo', 'my-region', 5, 10);
103
+
104
+ timeline.addRegion(region1);
105
+ timeline.addRegion(region2);
106
+ timeline.addRegion(region3);
107
+ expect(onNewRegion).toHaveBeenCalledTimes(3);
108
+ expect(onNewRegion).toHaveBeenCalledWith(region1);
109
+ expect(onNewRegion).toHaveBeenCalledWith(region2);
110
+ expect(onNewRegion).toHaveBeenCalledWith(region3);
111
+
112
+ let regions = Array.from(timeline.regions());
113
+ expect(regions.length).toBe(3);
114
+
115
+ // Give the timeline time to filter regions
116
+ await shaka.test.Util.delay(
117
+ shaka.media.RegionTimeline.REGION_FILTER_INTERVAL * 2);
118
+
119
+ regions = Array.from(timeline.regions());
120
+ expect(regions).toEqual([region2, region3]);
121
+
122
+ expect(onRemoveRegion).toHaveBeenCalledWith(region1);
123
+ });
106
124
 
107
125
  /**
108
126
  * @param {string} schemeIdUri
@@ -401,10 +401,10 @@ shaka.test.Util = class {
401
401
  * within the bounds of the viewport.
402
402
  * @param {string} name An identifier for the screenshot. Use alphanumeric
403
403
  * plus dash and underscore only.
404
- * @param {number} threshold A change threshold in pixels.
404
+ * @param {number} minSimilarity A minimum similarity score between 0 and 1.
405
405
  * @return {!Promise}
406
406
  */
407
- static async checkScreenshot(element, name, threshold=0) {
407
+ static async checkScreenshot(element, name, minSimilarity=1) {
408
408
  // Make sure the DOM is up-to-date and layout has settled before continuing.
409
409
  // Without this delay, or with a shorter delay, we sometimes get missing
410
410
  // elements in our UITextDisplayer tests on some platforms.
@@ -448,13 +448,16 @@ shaka.test.Util = class {
448
448
  const buffer = await shaka.test.Util.fetch(
449
449
  '/screenshot/diff' + parentUrlParams + paramsString);
450
450
  const json = shaka.util.StringUtils.fromUTF8(buffer);
451
- const pixelsChanged = /** @type {number} */(JSON.parse(json));
451
+ const similarity = /** @type {number} */(JSON.parse(json));
452
452
 
453
- // If the change threshold is exceeded, you can review the new screenshot
453
+ // If the minimum similarity is not met, you can review the new screenshot
454
454
  // and the diff image in the screenshots folder. Look for images that end
455
- // with "-new" and "-diff". If cropping doesn't work right, you can view
456
- // the full-page screenshot in the image that ends with "-full".
457
- expect(pixelsChanged).withContext(name).not.toBeGreaterThan(threshold);
455
+ // with "-new" and "-diff". (NOTE: The diff is a pixel-wise diff for human
456
+ // review, and is not produced with the same structural similarity
457
+ // algorithm used to detect changes in the test.) If cropping doesn't work
458
+ // right, you can view the full-page screenshot in the image that ends with
459
+ // "-full".
460
+ expect(similarity).withContext(name).not.toBeLessThan(minSimilarity);
458
461
  }
459
462
  };
460
463
 
@@ -34,11 +34,8 @@ filterDescribe('TextDisplayer layout', supportsScreenshots, () => {
34
34
  */
35
35
  let beforeScreenshot;
36
36
 
37
- // Legacy Edge seems to have inconsistent font kerning. A one-pixel offset in
38
- // the position of one character appears about 60% of the time, requiring us
39
- // to have this change tolerance in our tests. So far, all past bugs in our
40
- // implementation that we have tests for would exceed this threshold by a lot.
41
- const threshold = 160; // px
37
+ // A minimum similarity score for screenshots, between 0 and 1.
38
+ const minSimilarity = 0.95;
42
39
 
43
40
  const originalCast = window.chrome && window.chrome.cast;
44
41
 
@@ -196,6 +193,7 @@ filterDescribe('TextDisplayer layout', supportsScreenshots, () => {
196
193
 
197
194
  // The video must be played a little now, after the cues were appended,
198
195
  // but before the screenshot.
196
+ video.playbackRate = 1;
199
197
  video.play();
200
198
  await waiter.failOnTimeout(false).timeoutAfter(5)
201
199
  .waitForMovement(video);
@@ -203,10 +201,13 @@ filterDescribe('TextDisplayer layout', supportsScreenshots, () => {
203
201
 
204
202
  // Seek to a time when cues should be showing.
205
203
  video.currentTime = time;
204
+ // Get into a playing state, but without movement.
205
+ video.playbackRate = 0;
206
+ video.play();
206
207
 
207
208
  // Add a short delay to ensure that the system has caught up and that
208
209
  // native text displayers have been updated by the browser.
209
- await Util.shortDelay();
210
+ await Util.delay(0.1);
210
211
  };
211
212
  });
212
213
 
@@ -475,6 +476,6 @@ filterDescribe('TextDisplayer layout', supportsScreenshots, () => {
475
476
  return Util.checkScreenshot(
476
477
  /* element= */ videoContainer,
477
478
  prefix + '-' + baseName,
478
- threshold);
479
+ minSimilarity);
479
480
  }
480
481
  });
package/ui/controls.js CHANGED
@@ -254,6 +254,9 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
254
254
 
255
255
  this.localization_ = null;
256
256
  this.pressedKeys_.clear();
257
+
258
+ // FakeEventTarget implements IReleasable
259
+ super.release();
257
260
  }
258
261
 
259
262