/
video_wrapper.js
274 lines (233 loc) · 7.5 KB
/
video_wrapper.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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
/** @license
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.media.VideoWrapper');
goog.provide('shaka.media.VideoWrapper.PlayheadMover');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.IReleasable');
goog.require('shaka.util.MediaReadyState');
goog.require('shaka.util.Timer');
/**
* Creates a new VideoWrapper that manages setting current time and playback
* rate. This handles seeks before content is loaded and ensuring the video
* time is set properly. This doesn't handle repositioning within the
* presentation window.
*
* @implements {shaka.util.IReleasable}
*/
shaka.media.VideoWrapper = class {
/**
* @param {!HTMLMediaElement} video
* @param {function()} onSeek Called when the video seeks.
* @param {number} startTime The time to start at.
*/
constructor(video, onSeek, startTime) {
/** @private {HTMLMediaElement} */
this.video_ = video;
/** @private {function()} */
this.onSeek_ = onSeek;
/** @private {number} */
this.startTime_ = startTime;
/** @private {boolean} */
this.started_ = false;
/** @private {shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
/** @private {shaka.media.VideoWrapper.PlayheadMover} */
this.mover_ = new shaka.media.VideoWrapper.PlayheadMover(
/* mediaElement= */ video,
/* maxAttempts= */ 10);
// Before we can set the start time, we must check if the video element is
// ready. If the video element is not ready, we cannot set the time. To work
// around this, we will wait for the "loadedmetadata" event which tells us
// that the media element is now ready.
shaka.util.MediaReadyState.waitForReadyState(this.video_,
HTMLMediaElement.HAVE_METADATA,
this.eventManager_,
() => {
this.setStartTime_(this.startTime_);
});
}
/** @override */
release() {
if (this.eventManager_) {
this.eventManager_.release();
this.eventManager_ = null;
}
if (this.mover_ != null) {
this.mover_.release();
this.mover_ = null;
}
this.onSeek_ = () => {};
this.video_ = null;
}
/**
* Gets the video's current (logical) position.
*
* @return {number}
*/
getTime() {
return this.started_ ? this.video_.currentTime : this.startTime_;
}
/**
* Sets the current time of the video.
*
* @param {number} time
*/
setTime(time) {
if (this.video_.readyState > 0) {
this.mover_.moveTo(time);
} else {
shaka.util.MediaReadyState.waitForReadyState(this.video_,
HTMLMediaElement.HAVE_METADATA,
this.eventManager_,
() => {
this.setStartTime_(this.startTime_);
});
}
}
/**
* Set the start time for the content. The given start time will be ignored if
* the content does not start at 0.
*
* @param {number} startTime
* @private
*/
setStartTime_(startTime) {
// If we start close enough to our intended start time, then we won't do
// anything special.
if (Math.abs(this.video_.currentTime - startTime) < 0.001) {
this.startListeningToSeeks_();
return;
}
// We will need to delay adding our normal seeking listener until we have
// seen the first seek event. We will force the first seek event later in
// this method.
this.eventManager_.listenOnce(this.video_, 'seeking', () => {
this.startListeningToSeeks_();
});
// If the currentTime != 0, it indicates that the user has seeked after
// calling |Player.load|, meaning that |currentTime| is more meaningful than
// |startTime|.
//
// Seeking to the current time is a work around for Issue 1298. If we don't
// do this, the video may get stuck and not play.
//
// TODO: Need further investigation why it happens. Before and after
// setting the current time, video.readyState is 1, video.paused is true,
// and video.buffered's TimeRanges length is 0.
// See: https://github.com/google/shaka-player/issues/1298
this.mover_.moveTo(
this.video_.currentTime == 0 ?
startTime :
this.video_.currentTime);
}
/**
* Add the listener for seek-events. This will call the externally-provided
* |onSeek| callback whenever the media element seeks.
*
* @private
*/
startListeningToSeeks_() {
goog.asserts.assert(
this.video_.readyState > 0,
'The media element should be ready before we listen for seeking.');
// Now that any startup seeking is complete, we can trust the video element
// for currentTime.
this.started_ = true;
this.eventManager_.listen(this.video_, 'seeking', () => this.onSeek_());
}
};
/**
* A class used to move the playhead away from its current time. Sometimes, IE
* and Edge ignore re-seeks. After changing the current time, check every 100ms,
* retrying if the change was not accepted.
*
* Delay stats over 100 runs of a re-seeking integration test:
* IE - 0ms - 47%
* IE - 100ms - 63%
* Edge - 0ms - 2%
* Edge - 100ms - 40%
* Edge - 200ms - 32%
* Edge - 300ms - 24%
* Edge - 400ms - 2%
* Chrome - 0ms - 100%
*
* TODO: File a bug on IE/Edge about this.
*
* @implements {shaka.util.IReleasable}
* @final
*/
shaka.media.VideoWrapper.PlayheadMover = class {
/**
* @param {!HTMLMediaElement} mediaElement
* The media element that the mover can manipulate.
*
* @param {number} maxAttempts
* To prevent us from infinitely trying to change the current time, the
* mover accepts a max attempts value. At most, the mover will check if the
* video moved |maxAttempts| times. If this is zero of negative, no
* attempts will be made.
*/
constructor(mediaElement, maxAttempts) {
/** @private {HTMLMediaElement} */
this.mediaElement_ = mediaElement;
/** @private {number} */
this.maxAttempts_ = maxAttempts;
/** @private {number} */
this.remainingAttempts_ = 0;
/** @private {number} */
this.originTime_ = 0;
/** @private {number} */
this.targetTime_ = 0;
/** @private {shaka.util.Timer} */
this.timer_ = new shaka.util.Timer(() => this.onTick_());
}
/** @override */
release() {
if (this.timer_) {
this.timer_.stop();
this.timer_ = null;
}
this.mediaElement_ = null;
}
/**
* Try forcing the media element to move to |timeInSeconds|. If a previous
* call to |moveTo| is still in progress, this will override it.
*
* @param {number} timeInSeconds
*/
moveTo(timeInSeconds) {
this.originTime_ = this.mediaElement_.currentTime;
this.targetTime_ = timeInSeconds;
this.remainingAttempts_ = this.maxAttempts_;
// Set the time and then start the timer. The timer will check if the set
// was successful, and retry if not.
this.mediaElement_.currentTime = timeInSeconds;
this.timer_.tickEvery(/* seconds= */ 0.1);
}
/**
* @private
*/
onTick_() {
// Sigh... We ran out of retries...
if (this.remainingAttempts_ <= 0) {
shaka.log.warning([
'Failed to move playhead from', this.originTime_,
'to', this.targetTime_,
].join(' '));
this.timer_.stop();
return;
}
// Yay! We were successful.
if (this.mediaElement_.currentTime != this.originTime_) {
this.timer_.stop();
return;
}
// Sigh... Try again...
this.mediaElement_.currentTime = this.targetTime_;
this.remainingAttempts_--;
}
};