diff options
Diffstat (limited to 'javascript/videojs/src/js/live-tracker.js')
| -rw-r--r-- | javascript/videojs/src/js/live-tracker.js | 379 |
1 files changed, 379 insertions, 0 deletions
diff --git a/javascript/videojs/src/js/live-tracker.js b/javascript/videojs/src/js/live-tracker.js new file mode 100644 index 0000000..3e18d73 --- /dev/null +++ b/javascript/videojs/src/js/live-tracker.js @@ -0,0 +1,379 @@ +import Component from './component.js'; +import {merge} from './utils/obj.js'; +import window from 'global/window'; +import * as Fn from './utils/fn.js'; + +/** @import Player from './player' */ + +const defaults = { + trackingThreshold: 20, + liveTolerance: 15 +}; + +/* + track when we are at the live edge, and other helpers for live playback */ + +/** + * A class for checking live current time and determining when the player + * is at or behind the live edge. + */ +class LiveTracker extends Component { + + /** + * Creates an instance of this class. + * + * @param {Player} player + * The `Player` that this class should be attached to. + * + * @param {Object} [options] + * The key/value store of player options. + * + * @param {number} [options.trackingThreshold=20] + * Number of seconds of live window (seekableEnd - seekableStart) that + * media needs to have before the liveui will be shown. + * + * @param {number} [options.liveTolerance=15] + * Number of seconds behind live that we have to be + * before we will be considered non-live. Note that this will only + * be used when playing at the live edge. This allows large seekable end + * changes to not effect whether we are live or not. + */ + constructor(player, options) { + // LiveTracker does not need an element + const options_ = merge(defaults, options, {createEl: false}); + + super(player, options_); + + this.trackLiveHandler_ = () => this.trackLive_(); + this.handlePlay_ = (e) => this.handlePlay(e); + this.handleFirstTimeupdate_ = (e) => this.handleFirstTimeupdate(e); + this.handleSeeked_ = (e) => this.handleSeeked(e); + this.seekToLiveEdge_ = (e) => this.seekToLiveEdge(e); + + this.reset_(); + + this.on(this.player_, 'durationchange', (e) => this.handleDurationchange(e)); + // we should try to toggle tracking on canplay as native playback engines, like Safari + // may not have the proper values for things like seekableEnd until then + this.on(this.player_, 'canplay', () => this.toggleTracking()); + } + + /** + * all the functionality for tracking when seek end changes + * and for tracking how far past seek end we should be + */ + trackLive_() { + const seekable = this.player_.seekable(); + + // skip undefined seekable + if (!seekable || !seekable.length) { + return; + } + + const newTime = Number(window.performance.now().toFixed(4)); + const deltaTime = this.lastTime_ === -1 ? 0 : (newTime - this.lastTime_) / 1000; + + this.lastTime_ = newTime; + + this.pastSeekEnd_ = this.pastSeekEnd() + deltaTime; + + const liveCurrentTime = this.liveCurrentTime(); + const currentTime = this.player_.currentTime(); + + // we are behind live if any are true + // 1. the player is paused + // 2. the user seeked to a location 2 seconds away from live + // 3. the difference between live and current time is greater + // liveTolerance which defaults to 15s + let isBehind = this.player_.paused() || this.seekedBehindLive_ || + Math.abs(liveCurrentTime - currentTime) > this.options_.liveTolerance; + + // we cannot be behind if + // 1. until we have not seen a timeupdate yet + // 2. liveCurrentTime is Infinity, which happens on Android and Native Safari + if (!this.timeupdateSeen_ || liveCurrentTime === Infinity) { + isBehind = false; + } + + if (isBehind !== this.behindLiveEdge_) { + this.behindLiveEdge_ = isBehind; + this.trigger('liveedgechange'); + } + } + + /** + * handle a durationchange event on the player + * and start/stop tracking accordingly. + */ + handleDurationchange() { + this.toggleTracking(); + } + + /** + * start/stop tracking + */ + toggleTracking() { + if (this.player_.duration() === Infinity && this.liveWindow() >= this.options_.trackingThreshold) { + if (this.player_.options_.liveui) { + this.player_.addClass('vjs-liveui'); + } + this.startTracking(); + } else { + this.player_.removeClass('vjs-liveui'); + this.stopTracking(); + } + } + + /** + * start tracking live playback + */ + startTracking() { + if (this.isTracking()) { + return; + } + + // If we haven't seen a timeupdate, we need to check whether playback + // began before this component started tracking. This can happen commonly + // when using autoplay. + if (!this.timeupdateSeen_) { + this.timeupdateSeen_ = this.player_.hasStarted(); + } + + this.trackingInterval_ = this.setInterval(this.trackLiveHandler_, Fn.UPDATE_REFRESH_INTERVAL); + this.trackLive_(); + + this.on(this.player_, ['play', 'pause'], this.trackLiveHandler_); + + if (!this.timeupdateSeen_) { + this.one(this.player_, 'play', this.handlePlay_); + this.one(this.player_, 'timeupdate', this.handleFirstTimeupdate_); + } else { + this.on(this.player_, 'seeked', this.handleSeeked_); + } + } + + /** + * handle the first timeupdate on the player if it wasn't already playing + * when live tracker started tracking. + */ + handleFirstTimeupdate() { + this.timeupdateSeen_ = true; + this.on(this.player_, 'seeked', this.handleSeeked_); + } + + /** + * Keep track of what time a seek starts, and listen for seeked + * to find where a seek ends. + */ + handleSeeked() { + const timeDiff = Math.abs(this.liveCurrentTime() - this.player_.currentTime()); + + this.seekedBehindLive_ = this.nextSeekedFromUser_ && timeDiff > 2; + this.nextSeekedFromUser_ = false; + this.trackLive_(); + } + + /** + * handle the first play on the player, and make sure that we seek + * right to the live edge. + */ + handlePlay() { + this.one(this.player_, 'timeupdate', this.seekToLiveEdge_); + } + + /** + * Stop tracking, and set all internal variables to + * their initial value. + */ + reset_() { + this.lastTime_ = -1; + this.pastSeekEnd_ = 0; + this.lastSeekEnd_ = -1; + this.behindLiveEdge_ = true; + this.timeupdateSeen_ = false; + this.seekedBehindLive_ = false; + this.nextSeekedFromUser_ = false; + + this.clearInterval(this.trackingInterval_); + this.trackingInterval_ = null; + + this.off(this.player_, ['play', 'pause'], this.trackLiveHandler_); + this.off(this.player_, 'seeked', this.handleSeeked_); + this.off(this.player_, 'play', this.handlePlay_); + this.off(this.player_, 'timeupdate', this.handleFirstTimeupdate_); + this.off(this.player_, 'timeupdate', this.seekToLiveEdge_); + } + + /** + * The next seeked event is from the user. Meaning that any seek + * > 2s behind live will be considered behind live for real and + * liveTolerance will be ignored. + */ + nextSeekedFromUser() { + this.nextSeekedFromUser_ = true; + } + + /** + * stop tracking live playback + */ + stopTracking() { + if (!this.isTracking()) { + return; + } + this.reset_(); + this.trigger('liveedgechange'); + } + + /** + * A helper to get the player seekable end + * so that we don't have to null check everywhere + * + * @return {number} + * The furthest seekable end or Infinity. + */ + seekableEnd() { + const seekable = this.player_.seekable(); + const seekableEnds = []; + let i = seekable ? seekable.length : 0; + + while (i--) { + seekableEnds.push(seekable.end(i)); + } + + // grab the furthest seekable end after sorting, or if there are none + // default to Infinity + return seekableEnds.length ? seekableEnds.sort()[seekableEnds.length - 1] : Infinity; + } + + /** + * A helper to get the player seekable start + * so that we don't have to null check everywhere + * + * @return {number} + * The earliest seekable start or 0. + */ + seekableStart() { + const seekable = this.player_.seekable(); + const seekableStarts = []; + let i = seekable ? seekable.length : 0; + + while (i--) { + seekableStarts.push(seekable.start(i)); + } + + // grab the first seekable start after sorting, or if there are none + // default to 0 + return seekableStarts.length ? seekableStarts.sort()[0] : 0; + } + + /** + * Get the live time window aka + * the amount of time between seekable start and + * live current time. + * + * @return {number} + * The amount of seconds that are seekable in + * the live video. + */ + liveWindow() { + const liveCurrentTime = this.liveCurrentTime(); + + // if liveCurrenTime is Infinity then we don't have a liveWindow at all + if (liveCurrentTime === Infinity) { + return 0; + } + + return liveCurrentTime - this.seekableStart(); + } + + /** + * Determines if the player is live, only checks if this component + * is tracking live playback or not + * + * @return {boolean} + * Whether liveTracker is tracking + */ + isLive() { + return this.isTracking(); + } + + /** + * Determines if currentTime is at the live edge and won't fall behind + * on each seekableendchange + * + * @return {boolean} + * Whether playback is at the live edge + */ + atLiveEdge() { + return !this.behindLiveEdge(); + } + + /** + * get what we expect the live current time to be + * + * @return {number} + * The expected live current time + */ + liveCurrentTime() { + return this.pastSeekEnd() + this.seekableEnd(); + } + + /** + * The number of seconds that have occurred after seekable end + * changed. This will be reset to 0 once seekable end changes. + * + * @return {number} + * Seconds past the current seekable end + */ + pastSeekEnd() { + const seekableEnd = this.seekableEnd(); + + if (this.lastSeekEnd_ !== -1 && seekableEnd !== this.lastSeekEnd_) { + this.pastSeekEnd_ = 0; + } + this.lastSeekEnd_ = seekableEnd; + return this.pastSeekEnd_; + } + + /** + * If we are currently behind the live edge, aka currentTime will be + * behind on a seekableendchange + * + * @return {boolean} + * If we are behind the live edge + */ + behindLiveEdge() { + return this.behindLiveEdge_; + } + + /** + * Whether live tracker is currently tracking or not. + */ + isTracking() { + return typeof this.trackingInterval_ === 'number'; + } + + /** + * Seek to the live edge if we are behind the live edge + */ + seekToLiveEdge() { + this.seekedBehindLive_ = false; + if (this.atLiveEdge()) { + return; + } + this.nextSeekedFromUser_ = false; + this.player_.currentTime(this.liveCurrentTime()); + + } + + /** + * Dispose of liveTracker + */ + dispose() { + this.stopTracking(); + super.dispose(); + } +} + +Component.registerComponent('LiveTracker', LiveTracker); +export default LiveTracker; |
