bitmovin-player-ui
Version:
Bitmovin Player UI Framework
984 lines (983 loc) • 48.9 kB
JavaScript
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.SeekBar = void 0;
var GroupPlaybackAPI_1 = require("../../utils/GroupPlaybackAPI");
var Component_1 = require("../Component");
var DOM_1 = require("../../DOM");
var EventDispatcher_1 = require("../../EventDispatcher");
var Timeout_1 = require("../../utils/Timeout");
var PlayerUtils_1 = require("../../utils/PlayerUtils");
var StringUtils_1 = require("../../utils/StringUtils");
var SeekBarController_1 = require("../../utils/SeekBarController");
var i18n_1 = require("../../localization/i18n");
var BrowserUtils_1 = require("../../utils/BrowserUtils");
var TimelineMarkersHandler_1 = require("../../utils/TimelineMarkersHandler");
var getMinBufferLevel_1 = require("../../utils/getMinBufferLevel");
/**
* A seek bar to seek within the player's media. It displays the current playback position, amount of buffed data, seek
* target, and keeps status about an ongoing seek.
*
* The seek bar displays different 'bars':
* - the playback position, i.e. the position in the media at which the player current playback pointer is positioned
* - the buffer position, which usually is the playback position plus the time span that is already buffered ahead
* - the seek position, used to preview to where in the timeline a seek will jump to
*
* @category Components
*/
var SeekBar = /** @class */ (function (_super) {
__extends(SeekBar, _super);
function SeekBar(config) {
if (config === void 0) { config = {}; }
var _this = _super.call(this, config) || this;
/**
* Buffer of the the current playback position. The position must be buffered in case the element
* needs to be refreshed with {@link #refreshPlaybackPosition}.
* @type {number}
*/
_this.playbackPositionPercentage = 0;
_this.isUserSeeking = false;
// We need to track the current ad state manually since our mobile SDKs don't expose an API to check if an ad is active.
_this.isAdPlaying = false;
_this.seekBarEvents = {
/**
* Fired when a scrubbing seek operation is started.
*/
onSeek: new EventDispatcher_1.EventDispatcher(),
/**
* Fired during a scrubbing seek to indicate that the seek preview (i.e. the video frame) should be updated.
*/
onSeekPreview: new EventDispatcher_1.EventDispatcher(),
/**
* Fired when a scrubbing seek has finished or when a direct seek is issued.
*/
onSeeked: new EventDispatcher_1.EventDispatcher(),
};
_this.seekWhileScrubbing = function (sender, args) {
if (args.scrubbing) {
_this.seek(args.position);
}
};
_this.getTargetSeekPosition = function (percentage) {
var target;
if (_this.player.isLive()) {
var maxTimeShift = _this.player.getMaxTimeShift();
target = maxTimeShift - maxTimeShift * (percentage / 100);
}
else {
var seekableRangeStart = PlayerUtils_1.PlayerUtils.getSeekableRangeStart(_this.player, 0);
var relativeSeekTarget = _this.player.getDuration() * (percentage / 100);
target = relativeSeekTarget + seekableRangeStart;
}
return target;
};
_this.seek = function (percentage) {
var targetPlaybackPosition = _this.getTargetSeekPosition(percentage);
if (_this.player.isLive()) {
_this.player.timeShift(targetPlaybackPosition, 'ui');
}
else {
_this.player.seek(targetPlaybackPosition, 'ui');
}
};
_this.updateLabelPosition = function (pixelPosition) {
if (!_this.uiBoundingRect) {
_this.uiBoundingRect = _this.uiManager.getUI().getDomElement().get(0).getBoundingClientRect();
}
_this.label.setPositionInBounds(pixelPosition, _this.uiBoundingRect);
};
var keyStepIncrements = _this.config.keyStepIncrements || {
leftRight: 1,
upDown: 5,
};
_this.config = _this.mergeConfig(config, {
cssClass: 'ui-seekbar',
vertical: false,
smoothPlaybackPositionUpdateIntervalMs: 50,
keyStepIncrements: keyStepIncrements,
ariaLabel: i18n_1.i18n.getLocalizer('seekBar'),
tabIndex: 0,
snappingRange: 1,
enableSeekPreview: true,
snappingEnabled: true,
}, _this.config);
_this.label = _this.config.label;
return _this;
}
SeekBar.prototype.initialize = function () {
_super.prototype.initialize.call(this);
if (this.hasLabel()) {
this.getLabel().initialize();
}
};
SeekBar.prototype.setAriaSliderMinMax = function (min, max) {
this.getDomElement().attr('aria-valuemin', min);
this.getDomElement().attr('aria-valuemax', max);
};
SeekBar.prototype.setAriaSliderValues = function () {
if (this.seekBarType === SeekBarController_1.SeekBarType.Live) {
var timeshiftValue = Math.ceil(this.player.getTimeShift()).toString();
var timeShiftText = "".concat(i18n_1.i18n.performLocalization(i18n_1.i18n.getLocalizer('seekBar.timeshift')), " ").concat(i18n_1.i18n.performLocalization(i18n_1.i18n.getLocalizer('seekBar.value')), ": ").concat(timeshiftValue);
this.getDomElement().attr('aria-valuenow', timeshiftValue);
this.getDomElement().attr('aria-valuetext', timeShiftText);
if (this.config.addCurrentTimeToAriaLabel) {
this.getDomElement().attr('aria-label', "".concat(i18n_1.i18n.performLocalization(this.config.ariaLabel), ": ").concat(timeShiftText));
}
}
else if (this.seekBarType === SeekBarController_1.SeekBarType.Vod) {
var ariaValueText = "".concat(StringUtils_1.StringUtils.secondsToText(this.player.getCurrentTime()), " ").concat(i18n_1.i18n.performLocalization(i18n_1.i18n.getLocalizer('seekBar.durationText')), " ").concat(StringUtils_1.StringUtils.secondsToText(this.player.getDuration()));
this.getDomElement().attr('aria-valuenow', Math.floor(this.player.getCurrentTime()).toString());
this.getDomElement().attr('aria-valuetext', ariaValueText);
if (this.config.addCurrentTimeToAriaLabel) {
this.getDomElement().attr('aria-label', "".concat(i18n_1.i18n.performLocalization(this.config.ariaLabel), ": ").concat(StringUtils_1.StringUtils.secondsToText(this.player.getCurrentTime())));
}
}
};
SeekBar.prototype.getPlaybackPositionPercentage = function () {
if (this.player.isLive()) {
return 100 - (100 / this.player.getMaxTimeShift()) * this.player.getTimeShift();
}
return (100 / this.player.getDuration()) * this.getRelativeCurrentTime();
};
SeekBar.prototype.updateBufferLevel = function (playbackPositionPercentage) {
var bufferLoadedPercentageLevel;
if (this.player.isLive()) {
// Always show full buffer for live streams
bufferLoadedPercentageLevel = 100;
}
else {
bufferLoadedPercentageLevel = playbackPositionPercentage + (0, getMinBufferLevel_1.getMinBufferLevel)(this.player);
}
this.setBufferPosition(bufferLoadedPercentageLevel);
};
SeekBar.prototype.configure = function (player, uimanager, configureSeek) {
var _this = this;
if (configureSeek === void 0) { configureSeek = true; }
_super.prototype.configure.call(this, player, uimanager);
this.player = player;
this.uiManager = uimanager;
// Apply scaling transform to the backdrop bar to have all bars rendered similarly
// (the call must be up here to be executed for the volume slider as well)
this.setPosition(this.seekBarBackdrop, 100);
// Add seekbar controls to the seekbar
var seekBarController = new SeekBarController_1.SeekBarController(this.config.keyStepIncrements, player, uimanager.getConfig().volumeController);
seekBarController.setSeekBarControls(this.getDomElement(), function () { return _this.seekBarType; });
// The configureSeek flag can be used by subclasses to disable configuration as seek bar. E.g. the volume
// slider is reusing this component but adds its own functionality, and does not need the seek functionality.
// This is actually a hack, the proper solution would be for both seek bar and volume sliders to extend
// a common base slider component and implement their functionality there.
if (!configureSeek) {
this.seekBarType = SeekBarController_1.SeekBarType.Volume;
return;
}
var resumeSeekBarUpdates = function () {
_this.isUiShown = true;
if (_this.smoothPlaybackPositionUpdater && !player.isLive() && !_this.smoothPlaybackPositionUpdater.isActive()) {
playbackPositionHandler(null, true);
_this.smoothPlaybackPositionUpdater.start();
}
};
player.on(player.exports.PlayerEvent.AdStarted, function () {
_this.isAdPlaying = true;
resumeSeekBarUpdates();
});
player.on(player.exports.PlayerEvent.AdBreakFinished, function () {
_this.isAdPlaying = false;
});
player.on(player.exports.PlayerEvent.SourceUnloaded, function () {
_this.isAdPlaying = false;
});
uimanager.onControlsShow.subscribe(resumeSeekBarUpdates);
uimanager.onControlsHide.subscribe(function () {
// Keep seekbar always active during the playback of a linear ad
if (_this.isAdPlaying) {
return;
}
_this.isUiShown = false;
if (_this.smoothPlaybackPositionUpdater && _this.smoothPlaybackPositionUpdater.isActive()) {
_this.smoothPlaybackPositionUpdater.clear();
}
});
var isPlaying = false;
var scrubbing = false;
var isPlayerSeeking = false;
var suspension;
// Update playback and buffer positions
var playbackPositionHandler = function (event, forceUpdate) {
if (event === void 0) { event = null; }
if (forceUpdate === void 0) { forceUpdate = false; }
if (_this.isUserSeeking) {
// We caught a seek preview seek, do not update the seekbar
return;
}
var playbackPositionPercentage = _this.getPlaybackPositionPercentage();
_this.updateBufferLevel(playbackPositionPercentage);
// The segment request finished is used to help the playback position move, when the smooth playback position is not enabled.
// At the same time when the user is scrubbing, we also move the position of the seekbar to display a preview during scrubbing.
// When the user is scrubbing we do not record this as a user seek operation, as the user has yet to finish their seek,
// but we should not move the playback position to not create a jumping behaviour.
if (scrubbing &&
event &&
event.type === player.exports.PlayerEvent.SegmentRequestFinished &&
playbackPositionPercentage !== _this.playbackPositionPercentage) {
playbackPositionPercentage = _this.playbackPositionPercentage;
}
if (player.isLive()) {
if (player.getMaxTimeShift() === 0) {
// This case must be explicitly handled to avoid division by zero
_this.setPlaybackPosition(100);
}
else {
if (!_this.isSeeking()) {
_this.setPlaybackPosition(playbackPositionPercentage);
}
_this.setAriaSliderMinMax(player.getMaxTimeShift().toString(), '0');
}
}
else {
// Update playback position only in paused state or in the initial startup state where player is neither
// paused nor playing. Playback updates are handled in the Timeout below.
var isInInitialStartupState = _this.config.smoothPlaybackPositionUpdateIntervalMs === SeekBar.SMOOTH_PLAYBACK_POSITION_UPDATE_DISABLED ||
forceUpdate ||
player.isPaused();
var isNeitherPausedNorPlaying = player.isPaused() === player.isPlaying();
if ((isInInitialStartupState || isNeitherPausedNorPlaying) && !_this.isSeeking()) {
_this.setPlaybackPosition(playbackPositionPercentage);
}
_this.setAriaSliderMinMax('0', player.getDuration().toString());
}
if (_this.isUiShown) {
_this.setAriaSliderValues();
}
};
// Update seekbar upon these events
// init playback position when the player is ready
player.on(player.exports.PlayerEvent.Ready, playbackPositionHandler);
// update playback position when it changes
player.on(player.exports.PlayerEvent.TimeChanged, playbackPositionHandler);
// update bufferlevel when buffering is complete
player.on(player.exports.PlayerEvent.StallEnded, playbackPositionHandler);
// update playback position when a timeshift has finished
player.on(player.exports.PlayerEvent.TimeShifted, playbackPositionHandler);
// update bufferlevel when a segment has been downloaded
player.on(player.exports.PlayerEvent.SegmentRequestFinished, playbackPositionHandler);
this.configureLivePausedTimeshiftUpdater(player, uimanager, playbackPositionHandler);
// Seek handling
var onPlayerSeek = function () {
isPlayerSeeking = true;
_this.setSeeking(true);
scrubbing = false;
};
var onPlayerSeeked = function (event) {
if (event === void 0) { event = null; }
isPlayerSeeking = false;
_this.setSeeking(false);
// update playback position when a seek has finished
playbackPositionHandler(event, true);
};
var restorePlayingState = function () {
// Continue playback after seek if player was playing when seek started
if (isPlaying) {
// use the same issuer here as in the pause on seek
player.play('ui-seek');
}
};
player.on(player.exports.PlayerEvent.Seek, onPlayerSeek);
player.on(player.exports.PlayerEvent.Seeked, onPlayerSeeked);
player.on(player.exports.PlayerEvent.TimeShift, onPlayerSeek);
player.on(player.exports.PlayerEvent.TimeShifted, onPlayerSeeked);
var isGroupPlaybackAPIAvailable = function (player) {
return !!player.groupPlayback;
};
this.onSeek.subscribe(function (sender) {
// track seeking status so we can catch events from seek preview seeks
_this.isUserSeeking = true;
// Notify UI manager of started seek
uimanager.onSeek.dispatch(sender);
if (isGroupPlaybackAPIAvailable(player) && player.groupPlayback.hasJoined() && !suspension) {
suspension = player.groupPlayback.beginSuspension(GroupPlaybackAPI_1.GroupPlaybackSuspensionReason.UserIsScrubbing);
}
// Save current playback state before performing the seek
if (!isPlayerSeeking) {
isPlaying = player.isPlaying();
// Pause playback while seeking
if (isPlaying) {
// use a different issuer here, as play/pause on seek is not "really" triggerd by the user
player.pause('ui-seek');
}
}
});
this.onSeekPreview.subscribe(function (sender, args) {
// Notify UI manager of seek preview
uimanager.onSeekPreview.dispatch(sender, args);
scrubbing = args.scrubbing;
});
// Set enableSeekPreview if set in the uimanager config
if (typeof uimanager.getConfig().enableSeekPreview === 'boolean') {
this.config.enableSeekPreview = uimanager.getConfig().enableSeekPreview;
}
// Rate-limited scrubbing seek
if (this.config.enableSeekPreview) {
this.onSeekPreview.subscribeRateLimited(this.seekWhileScrubbing, 200);
}
this.onSeeked.subscribe(function (sender, percentage) {
_this.isUserSeeking = false;
// Do the seek
_this.seek(percentage);
// Notify UI manager of finished seek
uimanager.onSeeked.dispatch(sender);
// Continue playback after seek if player was playing when seek started
restorePlayingState();
if (isGroupPlaybackAPIAvailable(player) && player.groupPlayback.hasJoined() && suspension) {
var proposedPlaybackTime = _this.getTargetSeekPosition(percentage);
player.groupPlayback.endSuspension(suspension, { proposedPlaybackTime: proposedPlaybackTime });
suspension = undefined;
}
});
if (this.hasLabel()) {
// Configure a seekbar label that is internal to the seekbar)
this.getLabel().configure(player, uimanager);
}
// Hide seekbar for live sources without timeshift
var isLive = false;
var hasTimeShift = false;
var switchVisibility = function (isLive, hasTimeShift) {
if (isLive && !hasTimeShift) {
_this.hide();
}
else {
_this.show();
}
playbackPositionHandler(null, true);
_this.refreshPlaybackPosition();
};
var liveStreamDetector = new PlayerUtils_1.PlayerUtils.LiveStreamDetector(player, uimanager);
liveStreamDetector.onLiveChanged.subscribe(function (sender, args) {
isLive = args.live;
if (isLive && _this.smoothPlaybackPositionUpdater != null) {
_this.smoothPlaybackPositionUpdater.clear();
_this.seekBarType = SeekBarController_1.SeekBarType.Live;
}
else {
_this.seekBarType = SeekBarController_1.SeekBarType.Vod;
}
switchVisibility(isLive, hasTimeShift);
});
var timeShiftDetector = new PlayerUtils_1.PlayerUtils.TimeShiftAvailabilityDetector(player);
timeShiftDetector.onTimeShiftAvailabilityChanged.subscribe(function (sender, args) {
hasTimeShift = args.timeShiftAvailable;
switchVisibility(isLive, hasTimeShift);
});
// Initial detection
liveStreamDetector.detect();
timeShiftDetector.detect();
// Refresh the playback position when the player resized or the UI is configured. The playback position marker
// is positioned absolutely and must therefore be updated when the size of the seekbar changes.
player.on(player.exports.PlayerEvent.PlayerResized, function () {
_this.refreshPlaybackPosition();
_this.uiBoundingRect = _this.uiManager.getUI().getDomElement().get(0).getBoundingClientRect();
});
// Additionally, when this code is called, the seekbar is not part of the UI yet and therefore does not have a size,
// resulting in a wrong initial position of the marker. Refreshing it once the UI is configured solved this issue.
uimanager.onConfigured.subscribe(function () {
_this.refreshPlaybackPosition();
});
// It can also happen when a new source is loaded
player.on(player.exports.PlayerEvent.SourceLoaded, function () {
_this.refreshPlaybackPosition();
});
// Add markers when a source is loaded or update when a marker is added or removed
uimanager.getConfig().events.onUpdated.subscribe(function () {
playbackPositionHandler();
});
// Set the snappingRange if set in the uimanager config
if (typeof uimanager.getConfig().seekbarSnappingRange === 'number') {
this.config.snappingRange = uimanager.getConfig().seekbarSnappingRange;
}
if (typeof uimanager.getConfig().seekbarSnappingEnabled === 'boolean') {
this.config.snappingEnabled = uimanager.getConfig().seekbarSnappingEnabled;
}
// Initialize seekbar
playbackPositionHandler(); // Set the playback position
this.setBufferPosition(0);
this.setSeekPosition(0);
if (this.config.smoothPlaybackPositionUpdateIntervalMs !== SeekBar.SMOOTH_PLAYBACK_POSITION_UPDATE_DISABLED) {
this.configureSmoothPlaybackPositionUpdater(player, uimanager);
}
// Initialize markers
this.initializeTimelineMarkers(player, uimanager);
};
SeekBar.prototype.initializeTimelineMarkers = function (player, uimanager) {
var _this = this;
var timelineMarkerConfig = {
cssPrefix: this.config.cssPrefix,
snappingRange: this.config.snappingRange,
};
this.timelineMarkersHandler = new TimelineMarkersHandler_1.TimelineMarkersHandler(timelineMarkerConfig, function () { return _this.seekBar.width(); }, this.seekBarMarkersContainer);
this.timelineMarkersHandler.initialize(player, uimanager);
};
/**
* Update seekbar while a live stream with DVR window is paused.
* The playback position stays still and the position indicator visually moves towards the back.
*/
SeekBar.prototype.configureLivePausedTimeshiftUpdater = function (player, uimanager, playbackPositionHandler) {
var _this = this;
// Regularly update the playback position while the timeout is active
this.pausedTimeshiftUpdater = new Timeout_1.Timeout(1000, playbackPositionHandler, true);
// Start updater when a live stream with timeshift window is paused
player.on(player.exports.PlayerEvent.Paused, function () {
if (player.isLive() && player.getMaxTimeShift() < 0) {
_this.pausedTimeshiftUpdater.start();
}
});
// Stop updater when playback continues (no matter if the updater was started before)
player.on(player.exports.PlayerEvent.Play, function () { return _this.pausedTimeshiftUpdater.clear(); });
};
SeekBar.prototype.configureSmoothPlaybackPositionUpdater = function (player, uimanager) {
var _this = this;
/*
* Playback position update
*
* We do not update the position directly from the TimeChanged event, because it arrives very jittery and
* results in a jittery position indicator since the CSS transition time is statically set.
* To work around this issue, we maintain a local playback position that is updated in a stable regular interval
* and kept in sync with the player.
*/
var currentTimeSeekBar = 0;
var currentTimePlayer = 0;
var updateIntervalMs = 50;
var currentTimeUpdateDeltaSecs = updateIntervalMs / 1000;
this.smoothPlaybackPositionUpdater = new Timeout_1.Timeout(updateIntervalMs, function () {
if (_this.isSeeking()) {
return;
}
try {
// Reset the currentTimeSeekBar and set the position to 0 if the player has no duration
if (_this.player.getDuration() === 0) {
_this.setPlaybackPosition(0);
currentTimeSeekBar = 0;
return;
}
currentTimeSeekBar += currentTimeUpdateDeltaSecs;
currentTimePlayer = _this.getRelativeCurrentTime();
}
catch (error) {
// Detect if the player has been destroyed and stop updating if so
if (error instanceof player.exports.PlayerAPINotAvailableError) {
_this.smoothPlaybackPositionUpdater.clear();
}
// If the current time cannot be read it makes no sense to continue
return;
}
// Sync currentTime of seekbar to player
var currentTimeDelta = currentTimeSeekBar - currentTimePlayer;
// If the delta is larger that 2 secs, directly jump the seekbar to the
// player time instead of smoothly fast forwarding/rewinding.
if (Math.abs(currentTimeDelta) > 2) {
currentTimeSeekBar = currentTimePlayer;
}
// If currentTimeDelta is negative and below the adjustment threshold,
// the player is ahead of the seekbar and we 'fast forward' the seekbar
else if (currentTimeDelta <= -currentTimeUpdateDeltaSecs) {
currentTimeSeekBar += currentTimeUpdateDeltaSecs;
}
// If currentTimeDelta is positive and above the adjustment threshold,
// the player is behind the seekbar and we 'rewind' the seekbar
else if (currentTimeDelta >= currentTimeUpdateDeltaSecs) {
currentTimeSeekBar -= currentTimeUpdateDeltaSecs;
}
var playbackPositionPercentage = (100 / player.getDuration()) * currentTimeSeekBar;
_this.setPlaybackPosition(playbackPositionPercentage);
}, true);
var startSmoothPlaybackPositionUpdater = function () {
if (!player.isLive()) {
currentTimeSeekBar = _this.getRelativeCurrentTime();
_this.smoothPlaybackPositionUpdater.start();
}
};
var stopSmoothPlaybackPositionUpdater = function () {
_this.smoothPlaybackPositionUpdater.clear();
};
player.on(player.exports.PlayerEvent.Play, startSmoothPlaybackPositionUpdater);
player.on(player.exports.PlayerEvent.Playing, startSmoothPlaybackPositionUpdater);
player.on(player.exports.PlayerEvent.Paused, stopSmoothPlaybackPositionUpdater);
player.on(player.exports.PlayerEvent.PlaybackFinished, stopSmoothPlaybackPositionUpdater);
player.on(player.exports.PlayerEvent.Seeked, function () {
currentTimeSeekBar = _this.getRelativeCurrentTime();
});
player.on(player.exports.PlayerEvent.SourceUnloaded, stopSmoothPlaybackPositionUpdater);
if (player.isPlaying()) {
startSmoothPlaybackPositionUpdater();
}
};
SeekBar.prototype.getRelativeCurrentTime = function () {
return PlayerUtils_1.PlayerUtils.getCurrentTimeRelativeToSeekableRange(this.player);
};
SeekBar.prototype.release = function () {
_super.prototype.release.call(this);
if (this.smoothPlaybackPositionUpdater) {
// object must not necessarily exist, e.g. in volume slider subclass
this.smoothPlaybackPositionUpdater.clear();
}
if (this.pausedTimeshiftUpdater) {
this.pausedTimeshiftUpdater.clear();
}
if (this.config.enableSeekPreview) {
this.onSeekPreview.unsubscribe(this.seekWhileScrubbing);
}
};
SeekBar.prototype.toDomElement = function () {
var _this = this;
if (this.config.vertical) {
this.config.cssClasses.push('vertical');
}
var seekBarContainer = new DOM_1.DOM('div', {
id: this.config.id,
class: this.getCssClasses(),
role: 'slider',
'aria-label': i18n_1.i18n.performLocalization(this.config.ariaLabel),
tabindex: this.config.tabIndex.toString(),
}, this);
var seekBar = new DOM_1.DOM('div', {
class: this.prefixCss('seekbar'),
});
this.seekBar = seekBar;
var seekBarBarsContainer = new DOM_1.DOM('div', {
class: this.prefixCss('seekbar-bars'),
});
// Indicator that shows the buffer fill level
var seekBarBufferLevel = new DOM_1.DOM('div', {
class: this.prefixCss('seekbar-bufferlevel'),
});
this.seekBarBufferPosition = seekBarBufferLevel;
// Indicator that shows the current playback position
var seekBarPlaybackPosition = new DOM_1.DOM('div', {
class: this.prefixCss('seekbar-playbackposition'),
});
this.seekBarPlaybackPosition = seekBarPlaybackPosition;
// A marker of the current playback position, e.g. a dot or line
var seekBarPlaybackPositionMarker = new DOM_1.DOM('div', {
class: this.prefixCss('seekbar-playbackposition-marker'),
});
this.seekBarPlaybackPositionMarker = seekBarPlaybackPositionMarker;
// Indicator that show where a seek will go to
var seekBarSeekPosition = new DOM_1.DOM('div', {
class: this.prefixCss('seekbar-seekposition'),
});
this.seekBarSeekPosition = seekBarSeekPosition;
// Indicator that shows the full seekbar
var seekBarBackdrop = new DOM_1.DOM('div', {
class: this.prefixCss('seekbar-backdrop'),
});
this.seekBarBackdrop = seekBarBackdrop;
var seekBarChapterMarkersContainer = new DOM_1.DOM('div', {
class: this.prefixCss('seekbar-markers'),
});
this.seekBarMarkersContainer = seekBarChapterMarkersContainer;
seekBarBarsContainer.append(this.seekBarBackdrop, this.seekBarBufferPosition, this.seekBarSeekPosition, this.seekBarPlaybackPosition);
seekBar.append(seekBarBarsContainer, this.seekBarMarkersContainer, this.seekBarPlaybackPositionMarker);
var seeking = false;
// Define handler functions so we can attach/remove them later
var mouseTouchMoveHandler = function (e) {
e.preventDefault();
// Avoid propagation to VR handler
if (_this.player.vr != null) {
e.stopPropagation();
}
var offset = _this.getOffset(e);
var targetPercentage = 100 * offset;
var seekPositionPx = offset * _this.seekBar.width();
_this.setSeekPosition(targetPercentage);
_this.setPlaybackPosition(targetPercentage);
_this.onSeekPreviewEvent(targetPercentage, seekPositionPx, true);
};
var mouseTouchUpHandler = function (e) {
var _a;
e.preventDefault();
// Remove handlers, seek operation is finished
new DOM_1.DOM(document).off('touchmove mousemove', mouseTouchMoveHandler);
new DOM_1.DOM(document).off('touchend mouseup', mouseTouchUpHandler);
var targetPercentage = 100 * _this.getOffset(e);
if (_this.config.snappingEnabled) {
var matchingMarker = (_a = _this.timelineMarkersHandler) === null || _a === void 0 ? void 0 : _a.getMarkerAtPosition(targetPercentage);
targetPercentage = matchingMarker ? matchingMarker.position : targetPercentage;
}
_this.setSeeking(false);
seeking = false;
// Update the UI in case we only have a click or touch
_this.setSeekPosition(targetPercentage);
_this.setPlaybackPosition(targetPercentage);
// Fire seeked event
_this.onSeekedEvent(targetPercentage);
};
// A seek always start with a touchstart or mousedown directly on the seekbar.
// To track a mouse seek also outside the seekbar (for touch events this works automatically),
// so the user does not need to take care that the mouse always stays on the seekbar, we attach the mousemove
// and mouseup handlers to the whole document. A seek is triggered when the user lifts the mouse key.
// A seek mouse gesture is thus basically a click with a long time frame between down and up events.
seekBar.on('touchstart mousedown', function (e) {
var isTouchEvent = BrowserUtils_1.BrowserUtils.isTouchSupported && _this.isTouchEvent(e);
// Prevent selection of DOM elements (also prevents mousedown if current event is touchstart)
e.preventDefault();
// Avoid propagation to VR handler
if (_this.player.vr != null) {
e.stopPropagation();
}
_this.setSeeking(true); // Set seeking class on DOM element
seeking = true; // Set seek tracking flag
// Fire seeked event
_this.onSeekEvent();
// Add handler to track the seek operation over the whole document
// This enables that scrubbing doesn't require the mouse to stay inside the UI elements itself and works
// on the whole document.
new DOM_1.DOM(document).on(isTouchEvent ? 'touchmove' : 'mousemove', mouseTouchMoveHandler);
new DOM_1.DOM(document).on(isTouchEvent ? 'touchend' : 'mouseup', mouseTouchUpHandler);
});
// Display seek target indicator when mouse hovers or finger slides over seekbar
seekBar.on('touchmove mousemove', function (e) {
e.preventDefault();
if (seeking) {
mouseTouchMoveHandler(e);
}
var offset = _this.getOffset(e);
var seekPositionPercentage = 100 * offset;
var seekPositionPx = offset * _this.seekBar.width();
_this.setSeekPosition(seekPositionPercentage);
_this.onSeekPreviewEvent(seekPositionPercentage, seekPositionPx, false);
if (_this.hasLabel() && _this.getLabel().isHidden()) {
_this.getLabel().show();
}
});
// Hide seek target indicator when mouse or finger leaves seekbar
seekBar.on('touchend mouseleave', function (e) {
e.preventDefault();
_this.setSeekPosition(0);
if (_this.hasLabel()) {
_this.getLabel().hide();
}
});
seekBarContainer.append(seekBar);
if (this.label) {
seekBarContainer.append(this.label.getDomElement());
}
return seekBarContainer;
};
/**
* Gets the horizontal offset of a mouse/touch event point from the left edge of the seek bar.
* @param eventPageX the pageX coordinate of an event to calculate the offset from
* @returns {number} a number in the range of [0, 1], where 0 is the left edge and 1 is the right edge
*/
SeekBar.prototype.getHorizontalOffset = function (eventPageX) {
var elementOffsetPx = this.seekBar.offset().left;
var widthPx = this.seekBar.width();
var offsetPx = eventPageX - elementOffsetPx;
var offset = (1 / widthPx) * offsetPx;
return this.sanitizeOffset(offset);
};
/**
* Gets the vertical offset of a mouse/touch event point from the bottom edge of the seek bar.
* @param eventPageY the pageX coordinate of an event to calculate the offset from
* @returns {number} a number in the range of [0, 1], where 0 is the bottom edge and 1 is the top edge
*/
SeekBar.prototype.getVerticalOffset = function (eventPageY) {
var elementOffsetPx = this.seekBar.offset().top;
var widthPx = this.seekBar.height();
var offsetPx = eventPageY - elementOffsetPx;
var offset = (1 / widthPx) * offsetPx;
return 1 - this.sanitizeOffset(offset);
};
/**
* Gets the mouse or touch event offset for the current configuration (horizontal or vertical).
* @param e the event to calculate the offset from
* @returns {number} a number in the range of [0, 1]
* @see #getHorizontalOffset
* @see #getVerticalOffset
*/
SeekBar.prototype.getOffset = function (e) {
if (BrowserUtils_1.BrowserUtils.isTouchSupported && this.isTouchEvent(e)) {
if (this.config.vertical) {
return this.getVerticalOffset(e.type === 'touchend' ? e.changedTouches[0].pageY : e.touches[0].pageY);
}
else {
return this.getHorizontalOffset(e.type === 'touchend' ? e.changedTouches[0].pageX : e.touches[0].pageX);
}
}
else if (e instanceof MouseEvent) {
if (this.config.vertical) {
return this.getVerticalOffset(e.pageY);
}
else {
return this.getHorizontalOffset(e.pageX);
}
}
else {
if (console) {
console.warn('invalid event');
}
return 0;
}
};
/**
* Sanitizes the mouse offset to the range of [0, 1].
*
* When tracking the mouse outside the seek bar, the offset can be outside the desired range and this method
* limits it to the desired range. E.g. a mouse event left of the left edge of a seek bar yields an offset below
* zero, but to display the seek target on the seek bar, we need to limit it to zero.
*
* @param offset the offset to sanitize
* @returns {number} the sanitized offset.
*/
SeekBar.prototype.sanitizeOffset = function (offset) {
// Since we track mouse moves over the whole document, the target can be outside the seek range,
// and we need to limit it to the [0, 1] range.
if (offset < 0) {
offset = 0;
}
else if (offset > 1) {
offset = 1;
}
return offset;
};
/**
* Sets the position of the playback position indicator.
* @param percent a number between 0 and 100 as returned by the player
*/
SeekBar.prototype.setPlaybackPosition = function (percent) {
this.playbackPositionPercentage = percent;
// Set position of the bar
this.setPosition(this.seekBarPlaybackPosition, percent);
// Set position of the marker
var totalSize = this.config.vertical
? this.seekBar.height() - this.seekBarPlaybackPositionMarker.height()
: this.seekBar.width();
var px = (totalSize / 100) * percent;
if (this.config.vertical) {
px = this.seekBar.height() - px - this.seekBarPlaybackPositionMarker.height();
}
var style = this.config.vertical
? // -ms-transform required for IE9
// -webkit-transform required for Android 4.4 WebView
{
transform: 'translateY(' + px + 'px)',
'-ms-transform': 'translateY(' + px + 'px)',
'-webkit-transform': 'translateY(' + px + 'px)',
}
: {
transform: 'translateX(' + px + 'px)',
'-ms-transform': 'translateX(' + px + 'px)',
'-webkit-transform': 'translateX(' + px + 'px)',
};
this.seekBarPlaybackPositionMarker.css(style);
};
/**
* Refreshes the playback position. Can be used by subclasses to refresh the position when
* the size of the component changes.
*/
SeekBar.prototype.refreshPlaybackPosition = function () {
this.setPlaybackPosition(this.playbackPositionPercentage);
};
/**
* Sets the position until which media is buffered.
* @param percent a number between 0 and 100
*/
SeekBar.prototype.setBufferPosition = function (percent) {
this.setPosition(this.seekBarBufferPosition, percent);
};
/**
* Sets the position where a seek, if executed, would jump to.
* @param percent a number between 0 and 100
*/
SeekBar.prototype.setSeekPosition = function (percent) {
this.setPosition(this.seekBarSeekPosition, percent);
};
/**
* Set the actual position (width or height) of a DOM element that represent a bar in the seek bar.
* @param element the element to set the position for
* @param percent a number between 0 and 100
*/
SeekBar.prototype.setPosition = function (element, percent) {
var scale = percent / 100;
// When the scale is exactly 1 or very near 1 (and the browser internally rounds it to 1), browsers seem to render
// the elements differently and the height gets slightly off, leading to mismatching heights when e.g. the buffer
// level bar has a width of 1 and the playback position bar has a width < 1. A jittering buffer level around 1
// leads to an even worse flickering effect.
// Various changes in CSS styling and DOM hierarchy did not solve the issue so the workaround is to avoid a scale
// of exactly 1.
if (scale >= 0.99999 && scale <= 1.00001) {
scale = 0.99999;
}
var style = this.config.vertical
? // -ms-transform required for IE9
// -webkit-transform required for Android 4.4 WebView
{
transform: 'scaleY(' + scale + ')',
'-ms-transform': 'scaleY(' + scale + ')',
'-webkit-transform': 'scaleY(' + scale + ')',
}
: {
transform: 'scaleX(' + scale + ')',
'-ms-transform': 'scaleX(' + scale + ')',
'-webkit-transform': 'scaleX(' + scale + ')',
};
element.css(style);
};
/**
* Puts the seek bar into or out of seeking state by adding/removing a class to the DOM element. This can be used
* to adjust the styling while seeking.
*
* @param seeking should be true when entering seek state, false when exiting the seek state
*/
SeekBar.prototype.setSeeking = function (seeking) {
if (seeking) {
this.getDomElement().addClass(this.prefixCss(SeekBar.CLASS_SEEKING));
}
else {
this.getDomElement().removeClass(this.prefixCss(SeekBar.CLASS_SEEKING));
}
};
/**
* Checks if the seek bar is currently in the seek state.
* @returns {boolean} true if in seek state, else false
*/
SeekBar.prototype.isSeeking = function () {
return this.getDomElement().hasClass(this.prefixCss(SeekBar.CLASS_SEEKING));
};
/**
* Checks if the seek bar has a {@link SeekBarLabel}.
* @returns {boolean} true if the seek bar has a label, else false
*/
SeekBar.prototype.hasLabel = function () {
return this.label != null;
};
/**
* Gets the label of this seek bar.
* @returns {SeekBarLabel} the label if this seek bar has a label, else null
*/
SeekBar.prototype.getLabel = function () {
return this.label;
};
SeekBar.prototype.onSeekEvent = function () {
this.seekBarEvents.onSeek.dispatch(this);
};
SeekBar.prototype.clearAllThickenedMarkers = function () {
var _a;
(_a = this.seekBarMarkersContainer
.find(".".concat(this.prefixCss('seekbar-marker')))) === null || _a === void 0 ? void 0 : _a.removeClass(this.prefixCss(SeekBar.CLASS_CHAPTER_HOVERED));
};
SeekBar.prototype.thickenMarker = function (marker) {
if (marker === null || marker === void 0 ? void 0 : marker.element) {
marker.element.addClass(this.prefixCss(SeekBar.CLASS_CHAPTER_HOVERED));
}
};
SeekBar.prototype.onSeekPreviewEvent = function (percentage, targetOffsetPx, scrubbing) {
var snappedMarker = this.timelineMarkersHandler && this.timelineMarkersHandler.getMarkerAtPosition(percentage);
var seekPositionPercentage = percentage;
if (snappedMarker) {
if (snappedMarker.duration > 0) {
if (percentage < snappedMarker.position) {
// Snap the position to the start of the interval if the seek is within the left snap margin
// We know that we are within a snap margin when we are outside the marker interval but still
// have a snappedMarker
seekPositionPercentage = snappedMarker.position;
}
else if (percentage > snappedMarker.position + snappedMarker.duration) {
// Snap the position to the end of the interval if the seek is within the right snap margin
seekPositionPercentage = snappedMarker.position + snappedMarker.duration;
}
}
else {
// Position markers always snap to their marker position
seekPositionPercentage = snappedMarker.position;
}
}
if (this.label) {
this.updateLabelPosition(targetOffsetPx);
}
if (scrubbing) {
this.clearAllThickenedMarkers();
this.thickenMarker(snappedMarker);
}
this.seekBarEvents.onSeekPreview.dispatch(this, {
scrubbing: scrubbing,
position: seekPositionPercentage,
marker: snappedMarker,
});
};
SeekBar.prototype.onSeekedEvent = function (percentage) {
this.clearAllThickenedMarkers();
this.seekBarEvents.onSeeked.dispatch(this, percentage);
};
Object.defineProperty(SeekBar.prototype, "onSeek", {
/**
* Gets the event that is fired when a scrubbing seek operation is started.
* @returns {Event<SeekBar, NoArgs>}
*/
get: function () {
return this.seekBarEvents.onSeek.getEvent();
},
enumerable: false,
configurable: true
});
Object.defineProperty(SeekBar.prototype, "onSeekPreview", {
/**
* Gets the event that is fired during a scrubbing seek (to indicate that the seek preview, i.e. the video frame,
* should be updated), or during a normal seek preview when the seek bar is hovered (and the seek target,
* i.e. the seek bar label, should be updated).
* @returns {Event<SeekBar, SeekPreviewEventArgs>}
*/
get: function () {
return this.seekBarEvents.onSeekPreview.getEvent();
},
enumerable: false,
configurable: true
});
Object.defineProperty(SeekBar.prototype, "onSeeked", {
/**
* Gets the event that is fired when a scrubbing seek has finished or when a direct seek is issued.
* @returns {Event<SeekBar, number>}
*/
get: function () {
return this.seekBarEvents.onSeeked.getEvent();
},
enumerable: false,
configurable: true
});
SeekBar.prototype.onShowEvent = function () {
_super.prototype.onShowEvent.call(this);
// Refresh the position of the playback position when the seek bar becomes visible. To correctly set the position,
// the DOM element must be fully initialized and have its size calculated, because the position is set as an
// absolute value calculated from the size. This required size is not known when it is hidden.
// For such cases, we refresh the position here in onShow because here it is guaranteed that the component knows
// its size and can set the position correctly.
this.refreshPlaybackPosition();
};
/**
* Checks if TouchEvent is supported.
* @returns {boolean} true if TouchEvent not undefined, else false
*/
SeekBar.prototype.isTouchEvent = function (e) {
return window.TouchEvent && e instanceof TouchEvent;
};
SeekBar.SMOOTH_PLAYBACK_POSITION_UPDATE_DISABLED = -1;
/**
* The CSS class that is added to the DOM element while the seek bar is in 'seeking' state.
*/
SeekBar.CLASS_SEEKING = 'seeking';
SeekBar.CLASS_CHAPTER_HOVERED = 'hovered';
return SeekBar;
}(Component_1.Component));
exports.SeekBar = SeekBar;