Skip to content

Commit 8c92cbf

Browse files
authored
feat: async change events in TextTrackList with EventTarget#queueTrigger (videojs#5332)
Trigger the change event on the next tick. This means that multiple changes to a track's mode will only result in a single change event on its associated TextTrackList rather than 3 events as it may be currently. Fixes videojs#5159
1 parent 31a0bac commit 8c92cbf

File tree

5 files changed

+105
-3
lines changed

5 files changed

+105
-3
lines changed

src/js/event-target.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* @file src/js/event-target.js
33
*/
44
import * as Events from './utils/events.js';
5+
import window from 'global/window';
56

67
/**
78
* `EventTarget` is a class that can have the same API as the DOM `EventTarget`. It
@@ -158,4 +159,38 @@ EventTarget.prototype.trigger = function(event) {
158159
*/
159160
EventTarget.prototype.dispatchEvent = EventTarget.prototype.trigger;
160161

162+
let EVENT_MAP;
163+
164+
EventTarget.prototype.queueTrigger = function(event) {
165+
// only set up EVENT_MAP if it'll be used
166+
if (!EVENT_MAP) {
167+
EVENT_MAP = new Map();
168+
}
169+
170+
const type = event.type || event;
171+
let map = EVENT_MAP.get(this);
172+
173+
if (!map) {
174+
map = new Map();
175+
EVENT_MAP.set(this, map);
176+
}
177+
178+
const oldTimeout = map.get(type);
179+
180+
map.delete(type);
181+
window.clearTimeout(oldTimeout);
182+
183+
const timeout = window.setTimeout(() => {
184+
// if we cleared out all timeouts for the current target, delete its map
185+
if (map.size === 0) {
186+
map = null;
187+
EVENT_MAP.delete(this);
188+
}
189+
190+
this.trigger(event);
191+
}, 0);
192+
193+
map.set(type, timeout);
194+
};
195+
161196
export default EventTarget;

src/js/tracks/text-track-list.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class TextTrackList extends TrackList {
2828
* @fires TrackList#change
2929
*/
3030
track.addEventListener('modechange', Fn.bind(this, function() {
31-
this.trigger('change');
31+
this.queueTrigger('change');
3232
}));
3333

3434
const nonLanguageTextTrackKind = ['metadata', 'chapters'];

test/unit/event-target.test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/* eslint-env qunit */
2+
import EventTarget from '../../src/js/event-target.js';
3+
import sinon from 'sinon';
4+
5+
const { test } = QUnit;
6+
7+
QUnit.module('EventTarget', {
8+
beforeEach() {
9+
this.clock = sinon.useFakeTimers();
10+
},
11+
afterEach() {
12+
this.clock.restore();
13+
}
14+
});
15+
16+
test('EventTarget queueTrigger queues the event', function(t) {
17+
const et = new EventTarget();
18+
let changes = 0;
19+
const changeHandler = function() {
20+
changes++;
21+
};
22+
23+
et.on('change', changeHandler);
24+
25+
et.queueTrigger('change');
26+
t.equal(changes, 0, 'EventTarget did not trigger a change event yet');
27+
28+
this.clock.tick(1);
29+
t.equal(changes, 1, 'EventTarget triggered a change event once the clock ticked');
30+
});
31+
32+
test('EventTarget will only trigger event once with queueTrigger', function(t) {
33+
const et = new EventTarget();
34+
let changes = 0;
35+
const changeHandler = function() {
36+
changes++;
37+
};
38+
39+
et.on('change', changeHandler);
40+
41+
et.queueTrigger('change');
42+
t.equal(changes, 0, 'EventTarget did not trigger a change event yet');
43+
et.queueTrigger('change');
44+
t.equal(changes, 0, 'EventTarget did not trigger a change event yet');
45+
et.queueTrigger('change');
46+
t.equal(changes, 0, 'EventTarget did not trigger a change event yet');
47+
et.queueTrigger('change');
48+
49+
this.clock.tick(100);
50+
t.equal(changes, 1, 'EventTarget *only* triggered a change event once');
51+
});

test/unit/tracks/text-track-list.test.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,17 @@ import TextTrackList from '../../../src/js/tracks/text-track-list.js';
33
import TextTrack from '../../../src/js/tracks/text-track.js';
44
import EventTarget from '../../../src/js/event-target.js';
55
import TechFaker from '../tech/tech-faker';
6+
import sinon from 'sinon';
7+
8+
QUnit.module('Text Track List', {
9+
beforeEach() {
10+
this.clock = sinon.useFakeTimers();
11+
},
12+
afterEach() {
13+
this.clock.restore();
14+
}
15+
});
616

7-
QUnit.module('Text Track List');
817
QUnit.test('trigger "change" event when "modechange" is fired on a track', function(assert) {
918
const tt = new EventTarget();
1019
const ttl = new TextTrackList([tt]);
@@ -15,11 +24,13 @@ QUnit.test('trigger "change" event when "modechange" is fired on a track', funct
1524

1625
ttl.on('change', changeHandler);
1726
tt.trigger('modechange');
27+
this.clock.tick(1);
1828

1929
ttl.off('change', changeHandler);
2030
ttl.onchange = changeHandler;
2131

2232
tt.trigger('modechange');
33+
this.clock.tick(1);
2334
assert.equal(changes, 2, 'two change events should have fired');
2435
});
2536

@@ -33,12 +44,14 @@ QUnit.test('trigger "change" event when mode changes on a TextTrack', function(a
3344

3445
ttl.on('change', changeHandler);
3546
tt.mode = 'showing';
47+
this.clock.tick(1);
3648

3749
ttl.off('change', changeHandler);
3850
ttl.onchange = changeHandler;
3951

4052
tt.mode = 'hidden';
4153
tt.mode = 'disabled';
54+
this.clock.tick(1);
4255

43-
assert.equal(changes, 3, 'three change events should have fired');
56+
assert.equal(changes, 2, 'two change events should have fired');
4457
});

test/unit/tracks/text-tracks.test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,9 +349,11 @@ QUnit.test('removes cuechange event when text track is hidden for emulated track
349349
});
350350

351351
tt.mode = 'showing';
352+
this.clock.tick(1);
352353
assert.equal(numTextTrackChanges, 1,
353354
'texttrackchange should be called once for mode change');
354355
tt.mode = 'showing';
356+
this.clock.tick(1);
355357
assert.equal(numTextTrackChanges, 2,
356358
'texttrackchange should be called once for mode change');
357359

@@ -363,6 +365,7 @@ QUnit.test('removes cuechange event when text track is hidden for emulated track
363365
'texttrackchange should be triggered once for the cuechange');
364366

365367
tt.mode = 'hidden';
368+
this.clock.tick(1);
366369
assert.equal(numTextTrackChanges, 4,
367370
'texttrackchange should be called once for the mode change');
368371

0 commit comments

Comments
 (0)