- 1 :
import Component from './component.js';
- 2 :
import {merge} from './utils/obj.js';
- 3 :
import window from 'global/window';
- 4 :
import * as Fn from './utils/fn.js';
- 5 :
- 6 :
const defaults = {
- 7 :
trackingThreshold: 20,
- 8 :
liveTolerance: 15
- 9 :
};
- 10 :
- 11 :
/*
- 12 :
track when we are at the live edge, and other helpers for live playback */
- 13 :
- 14 :
/**
- 15 :
* A class for checking live current time and determining when the player
- 16 :
* is at or behind the live edge.
- 17 :
*/
- 18 :
class LiveTracker extends Component {
- 19 :
- 20 :
/**
- 21 :
* Creates an instance of this class.
- 22 :
*
- 23 :
* @param { import('./player').default } player
- 24 :
* The `Player` that this class should be attached to.
- 25 :
*
- 26 :
* @param {Object} [options]
- 27 :
* The key/value store of player options.
- 28 :
*
- 29 :
* @param {number} [options.trackingThreshold=20]
- 30 :
* Number of seconds of live window (seekableEnd - seekableStart) that
- 31 :
* media needs to have before the liveui will be shown.
- 32 :
*
- 33 :
* @param {number} [options.liveTolerance=15]
- 34 :
* Number of seconds behind live that we have to be
- 35 :
* before we will be considered non-live. Note that this will only
- 36 :
* be used when playing at the live edge. This allows large seekable end
- 37 :
* changes to not effect whether we are live or not.
- 38 :
*/
- 39 :
constructor(player, options) {
- 40 :
// LiveTracker does not need an element
- 41 :
const options_ = merge(defaults, options, {createEl: false});
- 42 :
- 43 :
super(player, options_);
- 44 :
- 45 :
this.trackLiveHandler_ = () => this.trackLive_();
- 46 :
this.handlePlay_ = (e) => this.handlePlay(e);
- 47 :
this.handleFirstTimeupdate_ = (e) => this.handleFirstTimeupdate(e);
- 48 :
this.handleSeeked_ = (e) => this.handleSeeked(e);
- 49 :
this.seekToLiveEdge_ = (e) => this.seekToLiveEdge(e);
- 50 :
- 51 :
this.reset_();
- 52 :
- 53 :
this.on(this.player_, 'durationchange', (e) => this.handleDurationchange(e));
- 54 :
// we should try to toggle tracking on canplay as native playback engines, like Safari
- 55 :
// may not have the proper values for things like seekableEnd until then
- 56 :
this.on(this.player_, 'canplay', () => this.toggleTracking());
- 57 :
}
- 58 :
- 59 :
/**
- 60 :
* all the functionality for tracking when seek end changes
- 61 :
* and for tracking how far past seek end we should be
- 62 :
*/
- 63 :
trackLive_() {
- 64 :
const seekable = this.player_.seekable();
- 65 :
- 66 :
// skip undefined seekable
- 67 :
if (!seekable || !seekable.length) {
- 68 :
return;
- 69 :
}
- 70 :
- 71 :
const newTime = Number(window.performance.now().toFixed(4));
- 72 :
const deltaTime = this.lastTime_ === -1 ? 0 : (newTime - this.lastTime_) / 1000;
- 73 :
- 74 :
this.lastTime_ = newTime;
- 75 :
- 76 :
this.pastSeekEnd_ = this.pastSeekEnd() + deltaTime;
- 77 :
- 78 :
const liveCurrentTime = this.liveCurrentTime();
- 79 :
const currentTime = this.player_.currentTime();
- 80 :
- 81 :
// we are behind live if any are true
- 82 :
// 1. the player is paused
- 83 :
// 2. the user seeked to a location 2 seconds away from live
- 84 :
// 3. the difference between live and current time is greater
- 85 :
// liveTolerance which defaults to 15s
- 86 :
let isBehind = this.player_.paused() || this.seekedBehindLive_ ||
- 87 :
Math.abs(liveCurrentTime - currentTime) > this.options_.liveTolerance;
- 88 :
- 89 :
// we cannot be behind if
- 90 :
// 1. until we have not seen a timeupdate yet
- 91 :
// 2. liveCurrentTime is Infinity, which happens on Android and Native Safari
- 92 :
if (!this.timeupdateSeen_ || liveCurrentTime === Infinity) {
- 93 :
isBehind = false;
- 94 :
}
- 95 :
- 96 :
if (isBehind !== this.behindLiveEdge_) {
- 97 :
this.behindLiveEdge_ = isBehind;
- 98 :
this.trigger('liveedgechange');
- 99 :
}
- 100 :
}
- 101 :
- 102 :
/**
- 103 :
* handle a durationchange event on the player
- 104 :
* and start/stop tracking accordingly.
- 105 :
*/
- 106 :
handleDurationchange() {
- 107 :
this.toggleTracking();
- 108 :
}
- 109 :
- 110 :
/**
- 111 :
* start/stop tracking
- 112 :
*/
- 113 :
toggleTracking() {
- 114 :
if (this.player_.duration() === Infinity && this.liveWindow() >= this.options_.trackingThreshold) {
- 115 :
if (this.player_.options_.liveui) {
- 116 :
this.player_.addClass('vjs-liveui');
- 117 :
}
- 118 :
this.startTracking();
- 119 :
} else {
- 120 :
this.player_.removeClass('vjs-liveui');
- 121 :
this.stopTracking();
- 122 :
}
- 123 :
}
- 124 :
- 125 :
/**
- 126 :
* start tracking live playback
- 127 :
*/
- 128 :
startTracking() {
- 129 :
if (this.isTracking()) {
- 130 :
return;
- 131 :
}
- 132 :
- 133 :
// If we haven't seen a timeupdate, we need to check whether playback
- 134 :
// began before this component started tracking. This can happen commonly
- 135 :
// when using autoplay.
- 136 :
if (!this.timeupdateSeen_) {
- 137 :
this.timeupdateSeen_ = this.player_.hasStarted();
- 138 :
}
- 139 :
- 140 :
this.trackingInterval_ = this.setInterval(this.trackLiveHandler_, Fn.UPDATE_REFRESH_INTERVAL);
- 141 :
this.trackLive_();
- 142 :
- 143 :
this.on(this.player_, ['play', 'pause'], this.trackLiveHandler_);
- 144 :
- 145 :
if (!this.timeupdateSeen_) {
- 146 :
this.one(this.player_, 'play', this.handlePlay_);
- 147 :
this.one(this.player_, 'timeupdate', this.handleFirstTimeupdate_);
- 148 :
} else {
- 149 :
this.on(this.player_, 'seeked', this.handleSeeked_);
- 150 :
}
- 151 :
}
- 152 :
- 153 :
/**
- 154 :
* handle the first timeupdate on the player if it wasn't already playing
- 155 :
* when live tracker started tracking.
- 156 :
*/
- 157 :
handleFirstTimeupdate() {
- 158 :
this.timeupdateSeen_ = true;
- 159 :
this.on(this.player_, 'seeked', this.handleSeeked_);
- 160 :
}
- 161 :
- 162 :
/**
- 163 :
* Keep track of what time a seek starts, and listen for seeked
- 164 :
* to find where a seek ends.
- 165 :
*/
- 166 :
handleSeeked() {
- 167 :
const timeDiff = Math.abs(this.liveCurrentTime() - this.player_.currentTime());
- 168 :
- 169 :
this.seekedBehindLive_ = this.nextSeekedFromUser_ && timeDiff > 2;
- 170 :
this.nextSeekedFromUser_ = false;
- 171 :
this.trackLive_();
- 172 :
}
- 173 :
- 174 :
/**
- 175 :
* handle the first play on the player, and make sure that we seek
- 176 :
* right to the live edge.
- 177 :
*/
- 178 :
handlePlay() {
- 179 :
this.one(this.player_, 'timeupdate', this.seekToLiveEdge_);
- 180 :
}
- 181 :
- 182 :
/**
- 183 :
* Stop tracking, and set all internal variables to
- 184 :
* their initial value.
- 185 :
*/
- 186 :
reset_() {
- 187 :
this.lastTime_ = -1;
- 188 :
this.pastSeekEnd_ = 0;
- 189 :
this.lastSeekEnd_ = -1;
- 190 :
this.behindLiveEdge_ = true;
- 191 :
this.timeupdateSeen_ = false;
- 192 :
this.seekedBehindLive_ = false;
- 193 :
this.nextSeekedFromUser_ = false;
- 194 :
- 195 :
this.clearInterval(this.trackingInterval_);
- 196 :
this.trackingInterval_ = null;
- 197 :
- 198 :
this.off(this.player_, ['play', 'pause'], this.trackLiveHandler_);
- 199 :
this.off(this.player_, 'seeked', this.handleSeeked_);
- 200 :
this.off(this.player_, 'play', this.handlePlay_);
- 201 :
this.off(this.player_, 'timeupdate', this.handleFirstTimeupdate_);
- 202 :
this.off(this.player_, 'timeupdate', this.seekToLiveEdge_);
- 203 :
}
- 204 :
- 205 :
/**
- 206 :
* The next seeked event is from the user. Meaning that any seek
- 207 :
* > 2s behind live will be considered behind live for real and
- 208 :
* liveTolerance will be ignored.
- 209 :
*/
- 210 :
nextSeekedFromUser() {
- 211 :
this.nextSeekedFromUser_ = true;
- 212 :
}
- 213 :
- 214 :
/**
- 215 :
* stop tracking live playback
- 216 :
*/
- 217 :
stopTracking() {
- 218 :
if (!this.isTracking()) {
- 219 :
return;
- 220 :
}
- 221 :
this.reset_();
- 222 :
this.trigger('liveedgechange');
- 223 :
}
- 224 :
- 225 :
/**
- 226 :
* A helper to get the player seekable end
- 227 :
* so that we don't have to null check everywhere
- 228 :
*
- 229 :
* @return {number}
- 230 :
* The furthest seekable end or Infinity.
- 231 :
*/
- 232 :
seekableEnd() {
- 233 :
const seekable = this.player_.seekable();
- 234 :
const seekableEnds = [];
- 235 :
let i = seekable ? seekable.length : 0;
- 236 :
- 237 :
while (i--) {
- 238 :
seekableEnds.push(seekable.end(i));
- 239 :
}
- 240 :
- 241 :
// grab the furthest seekable end after sorting, or if there are none
- 242 :
// default to Infinity
- 243 :
return seekableEnds.length ? seekableEnds.sort()[seekableEnds.length - 1] : Infinity;
- 244 :
}
- 245 :
- 246 :
/**
- 247 :
* A helper to get the player seekable start
- 248 :
* so that we don't have to null check everywhere
- 249 :
*
- 250 :
* @return {number}
- 251 :
* The earliest seekable start or 0.
- 252 :
*/
- 253 :
seekableStart() {
- 254 :
const seekable = this.player_.seekable();
- 255 :
const seekableStarts = [];
- 256 :
let i = seekable ? seekable.length : 0;
- 257 :
- 258 :
while (i--) {
- 259 :
seekableStarts.push(seekable.start(i));
- 260 :
}
- 261 :
- 262 :
// grab the first seekable start after sorting, or if there are none
- 263 :
// default to 0
- 264 :
return seekableStarts.length ? seekableStarts.sort()[0] : 0;
- 265 :
}
- 266 :
- 267 :
/**
- 268 :
* Get the live time window aka
- 269 :
* the amount of time between seekable start and
- 270 :
* live current time.
- 271 :
*
- 272 :
* @return {number}
- 273 :
* The amount of seconds that are seekable in
- 274 :
* the live video.
- 275 :
*/
- 276 :
liveWindow() {
- 277 :
const liveCurrentTime = this.liveCurrentTime();
- 278 :
- 279 :
// if liveCurrenTime is Infinity then we don't have a liveWindow at all
- 280 :
if (liveCurrentTime === Infinity) {
- 281 :
return 0;
- 282 :
}
- 283 :
- 284 :
return liveCurrentTime - this.seekableStart();
- 285 :
}
- 286 :
- 287 :
/**
- 288 :
* Determines if the player is live, only checks if this component
- 289 :
* is tracking live playback or not
- 290 :
*
- 291 :
* @return {boolean}
- 292 :
* Whether liveTracker is tracking
- 293 :
*/
- 294 :
isLive() {
- 295 :
return this.isTracking();
- 296 :
}
- 297 :
- 298 :
/**
- 299 :
* Determines if currentTime is at the live edge and won't fall behind
- 300 :
* on each seekableendchange
- 301 :
*
- 302 :
* @return {boolean}
- 303 :
* Whether playback is at the live edge
- 304 :
*/
- 305 :
atLiveEdge() {
- 306 :
return !this.behindLiveEdge();
- 307 :
}
- 308 :
- 309 :
/**
- 310 :
* get what we expect the live current time to be
- 311 :
*
- 312 :
* @return {number}
- 313 :
* The expected live current time
- 314 :
*/
- 315 :
liveCurrentTime() {
- 316 :
return this.pastSeekEnd() + this.seekableEnd();
- 317 :
}
- 318 :
- 319 :
/**
- 320 :
* The number of seconds that have occurred after seekable end
- 321 :
* changed. This will be reset to 0 once seekable end changes.
- 322 :
*
- 323 :
* @return {number}
- 324 :
* Seconds past the current seekable end
- 325 :
*/
- 326 :
pastSeekEnd() {
- 327 :
const seekableEnd = this.seekableEnd();
- 328 :
- 329 :
if (this.lastSeekEnd_ !== -1 && seekableEnd !== this.lastSeekEnd_) {
- 330 :
this.pastSeekEnd_ = 0;
- 331 :
}
- 332 :
this.lastSeekEnd_ = seekableEnd;
- 333 :
return this.pastSeekEnd_;
- 334 :
}
- 335 :
- 336 :
/**
- 337 :
* If we are currently behind the live edge, aka currentTime will be
- 338 :
* behind on a seekableendchange
- 339 :
*
- 340 :
* @return {boolean}
- 341 :
* If we are behind the live edge
- 342 :
*/
- 343 :
behindLiveEdge() {
- 344 :
return this.behindLiveEdge_;
- 345 :
}
- 346 :
- 347 :
/**
- 348 :
* Whether live tracker is currently tracking or not.
- 349 :
*/
- 350 :
isTracking() {
- 351 :
return typeof this.trackingInterval_ === 'number';
- 352 :
}
- 353 :
- 354 :
/**
- 355 :
* Seek to the live edge if we are behind the live edge
- 356 :
*/
- 357 :
seekToLiveEdge() {
- 358 :
this.seekedBehindLive_ = false;
- 359 :
if (this.atLiveEdge()) {
- 360 :
return;
- 361 :
}
- 362 :
this.nextSeekedFromUser_ = false;
- 363 :
this.player_.currentTime(this.liveCurrentTime());
- 364 :
- 365 :
}
- 366 :
- 367 :
/**
- 368 :
* Dispose of liveTracker
- 369 :
*/
- 370 :
dispose() {
- 371 :
this.stopTracking();
- 372 :
super.dispose();
- 373 :
}
- 374 :
}
- 375 :
- 376 :
Component.registerComponent('LiveTracker', LiveTracker);
- 377 :
export default LiveTracker;