forked from shaka-project/shaka-player
-
Notifications
You must be signed in to change notification settings - Fork 0
/
region_observer.js
234 lines (207 loc) · 7.04 KB
/
region_observer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.media.RegionObserver');
goog.require('shaka.media.IPlayheadObserver');
goog.require('shaka.media.RegionTimeline');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.FakeEventTarget');
/**
* The region observer watches a region timeline and playhead, and fires events
* ('enter', 'exit', 'skip') as the playhead moves.
*
* @implements {shaka.media.IPlayheadObserver}
* @final
*/
shaka.media.RegionObserver = class extends shaka.util.FakeEventTarget {
/**
* Create a region observer for the given timeline. The observer does not
* own the timeline, only uses it. This means that the observer should NOT
* destroy the timeline.
*
* @param {!shaka.media.RegionTimeline} timeline
* @param {boolean} startsPastZero
*/
constructor(timeline, startsPastZero) {
super();
/** @private {shaka.media.RegionTimeline} */
this.timeline_ = timeline;
/**
* Whether the asset is expected to start at a time beyond 0 seconds.
* For example, if the asset is a live stream.
* If true, we will not start polling for regions until the playhead has
* moved past 0 seconds, to avoid bad behaviors where the current time is
* briefly 0 before we have enough data to play.
* @private {boolean}
*/
this.startsPastZero_ = startsPastZero;
/**
* A mapping between a region and where we previously were relative to it.
* When the value here differs from what we calculate, it means we moved and
* should fire an event.
*
* @private {!Map.<shaka.extern.TimelineRegionInfo,
* shaka.media.RegionObserver.RelativePosition_>}
*/
this.oldPosition_ = new Map();
// To make the rules easier to read, alias all the relative positions.
const RelativePosition = shaka.media.RegionObserver.RelativePosition_;
const BEFORE_THE_REGION = RelativePosition.BEFORE_THE_REGION;
const IN_THE_REGION = RelativePosition.IN_THE_REGION;
const AFTER_THE_REGION = RelativePosition.AFTER_THE_REGION;
/**
* A read-only collection of rules for what to do when we change position
* relative to a region.
*
* @private {!Iterable.<shaka.media.RegionObserver.Rule_>}
*/
this.rules_ = [
{
weWere: null,
weAre: IN_THE_REGION,
invoke: (region, seeking) => this.onEvent_('enter', region, seeking),
},
{
weWere: BEFORE_THE_REGION,
weAre: IN_THE_REGION,
invoke: (region, seeking) => this.onEvent_('enter', region, seeking),
},
{
weWere: AFTER_THE_REGION,
weAre: IN_THE_REGION,
invoke: (region, seeking) => this.onEvent_('enter', region, seeking),
},
{
weWere: IN_THE_REGION,
weAre: BEFORE_THE_REGION,
invoke: (region, seeking) => this.onEvent_('exit', region, seeking),
},
{
weWere: IN_THE_REGION,
weAre: AFTER_THE_REGION,
invoke: (region, seeking) => this.onEvent_('exit', region, seeking),
},
{
weWere: BEFORE_THE_REGION,
weAre: AFTER_THE_REGION,
invoke: (region, seeking) => this.onEvent_('skip', region, seeking),
},
{
weWere: AFTER_THE_REGION,
weAre: BEFORE_THE_REGION,
invoke: (region, seeking) => this.onEvent_('skip', region, seeking),
},
];
/** @private {shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
this.eventManager_.listen(this.timeline_, 'regionremove', (event) => {
/** @type {shaka.extern.TimelineRegionInfo} */
const region = event['region'];
this.oldPosition_.delete(region);
});
}
/** @override */
release() {
this.timeline_ = null;
// Clear our maps so that we are not holding onto any more information than
// needed.
this.oldPosition_.clear();
this.eventManager_.release();
this.eventManager_ = null;
super.release();
}
/** @override */
poll(positionInSeconds, wasSeeking) {
const RegionObserver = shaka.media.RegionObserver;
if (this.startsPastZero_ && positionInSeconds == 0) {
// Don't start checking regions until the timeline has begun moving.
return;
}
// Now that we have seen the playhead go past 0, it's okay if it goes
// back there (e.g. seeking back to the start).
this.startsPastZero_ = false;
for (const region of this.timeline_.regions()) {
const previousPosition = this.oldPosition_.get(region);
const currentPosition = RegionObserver.determinePositionRelativeTo_(
region, positionInSeconds);
// We will only use |previousPosition| and |currentPosition|, so we can
// update our state now.
this.oldPosition_.set(region, currentPosition);
for (const rule of this.rules_) {
if (rule.weWere == previousPosition && rule.weAre == currentPosition) {
rule.invoke(region, wasSeeking);
}
}
}
}
/**
* Dispatch events of the given type. All event types in this class have the
* same parameters: region and seeking.
*
* @param {string} eventType
* @param {shaka.extern.TimelineRegionInfo} region
* @param {boolean} seeking
* @private
*/
onEvent_(eventType, region, seeking) {
const event = new shaka.util.FakeEvent(eventType, new Map([
['region', region],
['seeking', seeking],
]));
this.dispatchEvent(event);
}
/**
* Get the relative position of the playhead to |region| when the playhead is
* at |seconds|. We treat the region's start and end times as inclusive
* bounds.
*
* @param {shaka.extern.TimelineRegionInfo} region
* @param {number} seconds
* @return {shaka.media.RegionObserver.RelativePosition_}
* @private
*/
static determinePositionRelativeTo_(region, seconds) {
const RelativePosition = shaka.media.RegionObserver.RelativePosition_;
if (seconds < region.startTime) {
return RelativePosition.BEFORE_THE_REGION;
}
if (seconds > region.endTime) {
return RelativePosition.AFTER_THE_REGION;
}
return RelativePosition.IN_THE_REGION;
}
};
/**
* An enum of relative positions between the playhead and a region. Each is
* phrased so that it works in "The playhead is X" where "X" is any value in
* the enum.
*
* @enum {number}
* @private
*/
shaka.media.RegionObserver.RelativePosition_ = {
BEFORE_THE_REGION: 1,
IN_THE_REGION: 2,
AFTER_THE_REGION: 3,
};
/**
* All region observer events (onEnter, onExit, and onSkip) will be passed the
* region that the playhead is interacting with and whether or not the playhead
* moving is part of a seek event.
*
* @typedef {function(shaka.extern.TimelineRegionInfo, boolean)}
*/
shaka.media.RegionObserver.EventListener;
/**
* @typedef {{
* weWere: ?shaka.media.RegionObserver.RelativePosition_,
* weAre: ?shaka.media.RegionObserver.RelativePosition_,
* invoke: shaka.media.RegionObserver.EventListener
* }}
*
* @private
*/
shaka.media.RegionObserver.Rule_;