- 1 :
/**
- 2 :
* @file seek-bar.js
- 3 :
*/
- 4 :
import Slider from '../../slider/slider.js';
- 5 :
import Component from '../../component.js';
- 6 :
import {IS_IOS, IS_ANDROID} from '../../utils/browser.js';
- 7 :
import * as Dom from '../../utils/dom.js';
- 8 :
import * as Fn from '../../utils/fn.js';
- 9 :
import formatTime from '../../utils/format-time.js';
- 10 :
import {silencePromise} from '../../utils/promise';
- 11 :
import keycode from 'keycode';
- 12 :
import document from 'global/document';
- 13 :
- 14 :
import './load-progress-bar.js';
- 15 :
import './play-progress-bar.js';
- 16 :
import './mouse-time-display.js';
- 17 :
- 18 :
// The number of seconds the `step*` functions move the timeline.
- 19 :
const STEP_SECONDS = 5;
- 20 :
- 21 :
// The multiplier of STEP_SECONDS that PgUp/PgDown move the timeline.
- 22 :
const PAGE_KEY_MULTIPLIER = 12;
- 23 :
- 24 :
/**
- 25 :
* Seek bar and container for the progress bars. Uses {@link PlayProgressBar}
- 26 :
* as its `bar`.
- 27 :
*
- 28 :
* @extends Slider
- 29 :
*/
- 30 :
class SeekBar extends Slider {
- 31 :
- 32 :
/**
- 33 :
* Creates an instance of this class.
- 34 :
*
- 35 :
* @param {Player} player
- 36 :
* The `Player` that this class should be attached to.
- 37 :
*
- 38 :
* @param {Object} [options]
- 39 :
* The key/value store of player options.
- 40 :
*/
- 41 :
constructor(player, options) {
- 42 :
super(player, options);
- 43 :
this.setEventHandlers_();
- 44 :
}
- 45 :
- 46 :
/**
- 47 :
* Sets the event handlers
- 48 :
*
- 49 :
* @private
- 50 :
*/
- 51 :
setEventHandlers_() {
- 52 :
this.update_ = Fn.bind(this, this.update);
- 53 :
this.update = Fn.throttle(this.update_, Fn.UPDATE_REFRESH_INTERVAL);
- 54 :
- 55 :
this.on(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update);
- 56 :
if (this.player_.liveTracker) {
- 57 :
this.on(this.player_.liveTracker, 'liveedgechange', this.update);
- 58 :
}
- 59 :
- 60 :
// when playing, let's ensure we smoothly update the play progress bar
- 61 :
// via an interval
- 62 :
this.updateInterval = null;
- 63 :
- 64 :
this.enableIntervalHandler_ = (e) => this.enableInterval_(e);
- 65 :
this.disableIntervalHandler_ = (e) => this.disableInterval_(e);
- 66 :
- 67 :
this.on(this.player_, ['playing'], this.enableIntervalHandler_);
- 68 :
- 69 :
this.on(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);
- 70 :
- 71 :
// we don't need to update the play progress if the document is hidden,
- 72 :
// also, this causes the CPU to spike and eventually crash the page on IE11.
- 73 :
if ('hidden' in document && 'visibilityState' in document) {
- 74 :
this.on(document, 'visibilitychange', this.toggleVisibility_);
- 75 :
}
- 76 :
}
- 77 :
- 78 :
toggleVisibility_(e) {
- 79 :
if (document.visibilityState === 'hidden') {
- 80 :
this.cancelNamedAnimationFrame('SeekBar#update');
- 81 :
this.cancelNamedAnimationFrame('Slider#update');
- 82 :
this.disableInterval_(e);
- 83 :
} else {
- 84 :
if (!this.player_.ended() && !this.player_.paused()) {
- 85 :
this.enableInterval_();
- 86 :
}
- 87 :
- 88 :
// we just switched back to the page and someone may be looking, so, update ASAP
- 89 :
this.update();
- 90 :
}
- 91 :
}
- 92 :
- 93 :
enableInterval_() {
- 94 :
if (this.updateInterval) {
- 95 :
return;
- 96 :
- 97 :
}
- 98 :
this.updateInterval = this.setInterval(this.update, Fn.UPDATE_REFRESH_INTERVAL);
- 99 :
}
- 100 :
- 101 :
disableInterval_(e) {
- 102 :
if (this.player_.liveTracker && this.player_.liveTracker.isLive() && e && e.type !== 'ended') {
- 103 :
return;
- 104 :
}
- 105 :
- 106 :
if (!this.updateInterval) {
- 107 :
return;
- 108 :
}
- 109 :
- 110 :
this.clearInterval(this.updateInterval);
- 111 :
this.updateInterval = null;
- 112 :
}
- 113 :
- 114 :
/**
- 115 :
* Create the `Component`'s DOM element
- 116 :
*
- 117 :
* @return {Element}
- 118 :
* The element that was created.
- 119 :
*/
- 120 :
createEl() {
- 121 :
return super.createEl('div', {
- 122 :
className: 'vjs-progress-holder'
- 123 :
}, {
- 124 :
'aria-label': this.localize('Progress Bar')
- 125 :
});
- 126 :
}
- 127 :
- 128 :
/**
- 129 :
* This function updates the play progress bar and accessibility
- 130 :
* attributes to whatever is passed in.
- 131 :
*
- 132 :
* @param {EventTarget~Event} [event]
- 133 :
* The `timeupdate` or `ended` event that caused this to run.
- 134 :
*
- 135 :
* @listens Player#timeupdate
- 136 :
*
- 137 :
* @return {number}
- 138 :
* The current percent at a number from 0-1
- 139 :
*/
- 140 :
update(event) {
- 141 :
// ignore updates while the tab is hidden
- 142 :
if (document.visibilityState === 'hidden') {
- 143 :
return;
- 144 :
}
- 145 :
- 146 :
const percent = super.update();
- 147 :
- 148 :
this.requestNamedAnimationFrame('SeekBar#update', () => {
- 149 :
const currentTime = this.player_.ended() ?
- 150 :
this.player_.duration() : this.getCurrentTime_();
- 151 :
const liveTracker = this.player_.liveTracker;
- 152 :
let duration = this.player_.duration();
- 153 :
- 154 :
if (liveTracker && liveTracker.isLive()) {
- 155 :
duration = this.player_.liveTracker.liveCurrentTime();
- 156 :
}
- 157 :
- 158 :
if (this.percent_ !== percent) {
- 159 :
// machine readable value of progress bar (percentage complete)
- 160 :
this.el_.setAttribute('aria-valuenow', (percent * 100).toFixed(2));
- 161 :
this.percent_ = percent;
- 162 :
}
- 163 :
- 164 :
if (this.currentTime_ !== currentTime || this.duration_ !== duration) {
- 165 :
// human readable value of progress bar (time complete)
- 166 :
this.el_.setAttribute(
- 167 :
'aria-valuetext',
- 168 :
this.localize(
- 169 :
'progress bar timing: currentTime={1} duration={2}',
- 170 :
[formatTime(currentTime, duration),
- 171 :
formatTime(duration, duration)],
- 172 :
'{1} of {2}'
- 173 :
)
- 174 :
);
- 175 :
- 176 :
this.currentTime_ = currentTime;
- 177 :
this.duration_ = duration;
- 178 :
}
- 179 :
- 180 :
// update the progress bar time tooltip with the current time
- 181 :
if (this.bar) {
- 182 :
this.bar.update(Dom.getBoundingClientRect(this.el()), this.getProgress());
- 183 :
}
- 184 :
});
- 185 :
- 186 :
return percent;
- 187 :
}
- 188 :
- 189 :
/**
- 190 :
* Prevent liveThreshold from causing seeks to seem like they
- 191 :
* are not happening from a user perspective.
- 192 :
*
- 193 :
* @param {number} ct
- 194 :
* current time to seek to
- 195 :
*/
- 196 :
userSeek_(ct) {
- 197 :
if (this.player_.liveTracker && this.player_.liveTracker.isLive()) {
- 198 :
this.player_.liveTracker.nextSeekedFromUser();
- 199 :
}
- 200 :
- 201 :
this.player_.currentTime(ct);
- 202 :
}
- 203 :
- 204 :
/**
- 205 :
* Get the value of current time but allows for smooth scrubbing,
- 206 :
* when player can't keep up.
- 207 :
*
- 208 :
* @return {number}
- 209 :
* The current time value to display
- 210 :
*
- 211 :
* @private
- 212 :
*/
- 213 :
getCurrentTime_() {
- 214 :
return (this.player_.scrubbing()) ?
- 215 :
this.player_.getCache().currentTime :
- 216 :
this.player_.currentTime();
- 217 :
}
- 218 :
- 219 :
/**
- 220 :
* Get the percentage of media played so far.
- 221 :
*
- 222 :
* @return {number}
- 223 :
* The percentage of media played so far (0 to 1).
- 224 :
*/
- 225 :
getPercent() {
- 226 :
const currentTime = this.getCurrentTime_();
- 227 :
let percent;
- 228 :
const liveTracker = this.player_.liveTracker;
- 229 :
- 230 :
if (liveTracker && liveTracker.isLive()) {
- 231 :
percent = (currentTime - liveTracker.seekableStart()) / liveTracker.liveWindow();
- 232 :
- 233 :
// prevent the percent from changing at the live edge
- 234 :
if (liveTracker.atLiveEdge()) {
- 235 :
percent = 1;
- 236 :
}
- 237 :
} else {
- 238 :
percent = currentTime / this.player_.duration();
- 239 :
}
- 240 :
- 241 :
return percent;
- 242 :
}
- 243 :
- 244 :
/**
- 245 :
* Handle mouse down on seek bar
- 246 :
*
- 247 :
* @param {EventTarget~Event} event
- 248 :
* The `mousedown` event that caused this to run.
- 249 :
*
- 250 :
* @listens mousedown
- 251 :
*/
- 252 :
handleMouseDown(event) {
- 253 :
if (!Dom.isSingleLeftClick(event)) {
- 254 :
return;
- 255 :
}
- 256 :
- 257 :
// Stop event propagation to prevent double fire in progress-control.js
- 258 :
event.stopPropagation();
- 259 :
- 260 :
this.videoWasPlaying = !this.player_.paused();
- 261 :
this.player_.pause();
- 262 :
- 263 :
super.handleMouseDown(event);
- 264 :
}
- 265 :
- 266 :
/**
- 267 :
* Handle mouse move on seek bar
- 268 :
*
- 269 :
* @param {EventTarget~Event} event
- 270 :
* The `mousemove` event that caused this to run.
- 271 :
* @param {boolean} mouseDown this is a flag that should be set to true if `handleMouseMove` is called directly. It allows us to skip things that should not happen if coming from mouse down but should happen on regular mouse move handler. Defaults to false
- 272 :
*
- 273 :
* @listens mousemove
- 274 :
*/
- 275 :
handleMouseMove(event, mouseDown = false) {
- 276 :
if (!Dom.isSingleLeftClick(event)) {
- 277 :
return;
- 278 :
}
- 279 :
- 280 :
if (!mouseDown && !this.player_.scrubbing()) {
- 281 :
this.player_.scrubbing(true);
- 282 :
}
- 283 :
- 284 :
let newTime;
- 285 :
const distance = this.calculateDistance(event);
- 286 :
const liveTracker = this.player_.liveTracker;
- 287 :
- 288 :
if (!liveTracker || !liveTracker.isLive()) {
- 289 :
newTime = distance * this.player_.duration();
- 290 :
- 291 :
// Don't let video end while scrubbing.
- 292 :
if (newTime === this.player_.duration()) {
- 293 :
newTime = newTime - 0.1;
- 294 :
}
- 295 :
} else {
- 296 :
- 297 :
if (distance >= 0.99) {
- 298 :
liveTracker.seekToLiveEdge();
- 299 :
return;
- 300 :
}
- 301 :
const seekableStart = liveTracker.seekableStart();
- 302 :
const seekableEnd = liveTracker.liveCurrentTime();
- 303 :
- 304 :
newTime = seekableStart + (distance * liveTracker.liveWindow());
- 305 :
- 306 :
// Don't let video end while scrubbing.
- 307 :
if (newTime >= seekableEnd) {
- 308 :
newTime = seekableEnd;
- 309 :
}
- 310 :
- 311 :
// Compensate for precision differences so that currentTime is not less
- 312 :
// than seekable start
- 313 :
if (newTime <= seekableStart) {
- 314 :
newTime = seekableStart + 0.1;
- 315 :
}
- 316 :
- 317 :
// On android seekableEnd can be Infinity sometimes,
- 318 :
// this will cause newTime to be Infinity, which is
- 319 :
// not a valid currentTime.
- 320 :
if (newTime === Infinity) {
- 321 :
return;
- 322 :
}
- 323 :
}
- 324 :
- 325 :
// Set new time (tell player to seek to new time)
- 326 :
this.userSeek_(newTime);
- 327 :
}
- 328 :
- 329 :
enable() {
- 330 :
super.enable();
- 331 :
const mouseTimeDisplay = this.getChild('mouseTimeDisplay');
- 332 :
- 333 :
if (!mouseTimeDisplay) {
- 334 :
return;
- 335 :
}
- 336 :
- 337 :
mouseTimeDisplay.show();
- 338 :
}
- 339 :
- 340 :
disable() {
- 341 :
super.disable();
- 342 :
const mouseTimeDisplay = this.getChild('mouseTimeDisplay');
- 343 :
- 344 :
if (!mouseTimeDisplay) {
- 345 :
return;
- 346 :
}
- 347 :
- 348 :
mouseTimeDisplay.hide();
- 349 :
}
- 350 :
- 351 :
/**
- 352 :
* Handle mouse up on seek bar
- 353 :
*
- 354 :
* @param {EventTarget~Event} event
- 355 :
* The `mouseup` event that caused this to run.
- 356 :
*
- 357 :
* @listens mouseup
- 358 :
*/
- 359 :
handleMouseUp(event) {
- 360 :
super.handleMouseUp(event);
- 361 :
- 362 :
// Stop event propagation to prevent double fire in progress-control.js
- 363 :
if (event) {
- 364 :
event.stopPropagation();
- 365 :
}
- 366 :
this.player_.scrubbing(false);
- 367 :
- 368 :
/**
- 369 :
* Trigger timeupdate because we're done seeking and the time has changed.
- 370 :
* This is particularly useful for if the player is paused to time the time displays.
- 371 :
*
- 372 :
* @event Tech#timeupdate
- 373 :
* @type {EventTarget~Event}
- 374 :
*/
- 375 :
this.player_.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true });
- 376 :
if (this.videoWasPlaying) {
- 377 :
silencePromise(this.player_.play());
- 378 :
} else {
- 379 :
// We're done seeking and the time has changed.
- 380 :
// If the player is paused, make sure we display the correct time on the seek bar.
- 381 :
this.update_();
- 382 :
}
- 383 :
}
- 384 :
- 385 :
/**
- 386 :
* Move more quickly fast forward for keyboard-only users
- 387 :
*/
- 388 :
stepForward() {
- 389 :
this.userSeek_(this.player_.currentTime() + STEP_SECONDS);
- 390 :
}
- 391 :
- 392 :
/**
- 393 :
* Move more quickly rewind for keyboard-only users
- 394 :
*/
- 395 :
stepBack() {
- 396 :
this.userSeek_(this.player_.currentTime() - STEP_SECONDS);
- 397 :
}
- 398 :
- 399 :
/**
- 400 :
* Toggles the playback state of the player
- 401 :
* This gets called when enter or space is used on the seekbar
- 402 :
*
- 403 :
* @param {EventTarget~Event} event
- 404 :
* The `keydown` event that caused this function to be called
- 405 :
*
- 406 :
*/
- 407 :
handleAction(event) {
- 408 :
if (this.player_.paused()) {
- 409 :
this.player_.play();
- 410 :
} else {
- 411 :
this.player_.pause();
- 412 :
}
- 413 :
}
- 414 :
- 415 :
/**
- 416 :
* Called when this SeekBar has focus and a key gets pressed down.
- 417 :
* Supports the following keys:
- 418 :
*
- 419 :
* Space or Enter key fire a click event
- 420 :
* Home key moves to start of the timeline
- 421 :
* End key moves to end of the timeline
- 422 :
* Digit "0" through "9" keys move to 0%, 10% ... 80%, 90% of the timeline
- 423 :
* PageDown key moves back a larger step than ArrowDown
- 424 :
* PageUp key moves forward a large step
- 425 :
*
- 426 :
* @param {EventTarget~Event} event
- 427 :
* The `keydown` event that caused this function to be called.
- 428 :
*
- 429 :
* @listens keydown
- 430 :
*/
- 431 :
handleKeyDown(event) {
- 432 :
const liveTracker = this.player_.liveTracker;
- 433 :
- 434 :
if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) {
- 435 :
event.preventDefault();
- 436 :
event.stopPropagation();
- 437 :
this.handleAction(event);
- 438 :
} else if (keycode.isEventKey(event, 'Home')) {
- 439 :
event.preventDefault();
- 440 :
event.stopPropagation();
- 441 :
this.userSeek_(0);
- 442 :
} else if (keycode.isEventKey(event, 'End')) {
- 443 :
event.preventDefault();
- 444 :
event.stopPropagation();
- 445 :
if (liveTracker && liveTracker.isLive()) {
- 446 :
this.userSeek_(liveTracker.liveCurrentTime());
- 447 :
} else {
- 448 :
this.userSeek_(this.player_.duration());
- 449 :
}
- 450 :
} else if (/^[0-9]$/.test(keycode(event))) {
- 451 :
event.preventDefault();
- 452 :
event.stopPropagation();
- 453 :
const gotoFraction = (keycode.codes[keycode(event)] - keycode.codes['0']) * 10.0 / 100.0;
- 454 :
- 455 :
if (liveTracker && liveTracker.isLive()) {
- 456 :
this.userSeek_(liveTracker.seekableStart() + (liveTracker.liveWindow() * gotoFraction));
- 457 :
} else {
- 458 :
this.userSeek_(this.player_.duration() * gotoFraction);
- 459 :
}
- 460 :
} else if (keycode.isEventKey(event, 'PgDn')) {
- 461 :
event.preventDefault();
- 462 :
event.stopPropagation();
- 463 :
this.userSeek_(this.player_.currentTime() - (STEP_SECONDS * PAGE_KEY_MULTIPLIER));
- 464 :
} else if (keycode.isEventKey(event, 'PgUp')) {
- 465 :
event.preventDefault();
- 466 :
event.stopPropagation();
- 467 :
this.userSeek_(this.player_.currentTime() + (STEP_SECONDS * PAGE_KEY_MULTIPLIER));
- 468 :
} else {
- 469 :
// Pass keydown handling up for unsupported keys
- 470 :
super.handleKeyDown(event);
- 471 :
}
- 472 :
}
- 473 :
- 474 :
dispose() {
- 475 :
this.disableInterval_();
- 476 :
- 477 :
this.off(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update);
- 478 :
if (this.player_.liveTracker) {
- 479 :
this.off(this.player_.liveTracker, 'liveedgechange', this.update);
- 480 :
}
- 481 :
- 482 :
this.off(this.player_, ['playing'], this.enableIntervalHandler_);
- 483 :
this.off(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);
- 484 :
- 485 :
// we don't need to update the play progress if the document is hidden,
- 486 :
// also, this causes the CPU to spike and eventually crash the page on IE11.
- 487 :
if ('hidden' in document && 'visibilityState' in document) {
- 488 :
this.off(document, 'visibilitychange', this.toggleVisibility_);
- 489 :
}
- 490 :
- 491 :
super.dispose();
- 492 :
}
- 493 :
}
- 494 :
- 495 :
/**
- 496 :
* Default options for the `SeekBar`
- 497 :
*
- 498 :
* @type {Object}
- 499 :
* @private
- 500 :
*/
- 501 :
SeekBar.prototype.options_ = {
- 502 :
children: [
- 503 :
'loadProgressBar',
- 504 :
'playProgressBar'
- 505 :
],
- 506 :
barName: 'playProgressBar'
- 507 :
};
- 508 :
- 509 :
// MouseTimeDisplay tooltips should not be added to a player on mobile devices
- 510 :
if (!IS_IOS && !IS_ANDROID) {
- 511 :
SeekBar.prototype.options_.children.splice(1, 0, 'mouseTimeDisplay');
- 512 :
}
- 513 :
- 514 :
Component.registerComponent('SeekBar', SeekBar);
- 515 :
export default SeekBar;