mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-15 16:06:41 +03:00
0f92d8221f
Issue #1553 Change-Id: I8c0412eda3d68f2ad475bf8282afcaeffd80852a
300 lines
8.4 KiB
JavaScript
300 lines
8.4 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2016 Google Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
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.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 {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.
|
|
if (video.readyState > 0) {
|
|
this.setStartTime_(startTime);
|
|
} else {
|
|
this.delaySetStartTime_(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.video_.readyState > 0 ?
|
|
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 {
|
|
this.delaySetStartTime_(time);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If the media element is not ready, we can't set |currentTime|. To work
|
|
* around this we will listen for the "loadedmetadata" event so that we can
|
|
* set the start time once the element is ready.
|
|
*
|
|
* @param {number} startTime
|
|
* @private
|
|
*/
|
|
delaySetStartTime_(startTime) {
|
|
const readyEvent = 'loadedmetadata';
|
|
|
|
// Since we are going to override what the start time should be, we need to
|
|
// save it so that |getTime| can return the most accurate start time
|
|
// possible.
|
|
this.startTime_ = startTime;
|
|
|
|
// The media element is not ready to accept changes to current time. We need
|
|
// to cache them and then execute them once the media element is ready.
|
|
this.eventManager_.unlisten(this.video_, readyEvent);
|
|
|
|
this.eventManager_.listenOnce(this.video_, readyEvent, () => {
|
|
this.setStartTime_(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.');
|
|
|
|
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_--;
|
|
}
|
|
};
|