mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-14 15:56:38 +03:00
feat(UI): Update playback rate menu with slider and preset pills (#10134)
Before: <img width="258" height="314" alt="before" src="https://github.com/user-attachments/assets/07cdb649-aa99-4898-8a0c-262ff239e29d" /> After: <img width="314" height="175" alt="after" src="https://github.com/user-attachments/assets/fa150d58-8bcd-4e2f-8a80-bf4e7bf2abd3" /> --------- Co-authored-by: Wojciech Tyczyński <tykus160@gmail.com>
This commit is contained in:
committed by
GitHub
parent
61e64a9c2d
commit
f2de74885e
@@ -106,6 +106,7 @@ shakaDemo.Config = class {
|
||||
this.addUISection_();
|
||||
this.addUISeekBarColorsSection_();
|
||||
this.addUIVolumeBarColorsSection_();
|
||||
this.addUIPlaybackRateBarColorsSection_();
|
||||
this.addUIQualityMarksSection_();
|
||||
this.addUIMediaSessionSection_();
|
||||
this.addUIDocumentPiPSection_();
|
||||
@@ -1201,6 +1202,12 @@ shakaDemo.Config = class {
|
||||
.addUIArrayStringInput_('Statistics List', 'statisticsList')
|
||||
.addUIArrayStringInput_('Ad Statistics List', 'adStatisticsList')
|
||||
.addUIArrayNumberInput_('Playback Rates', 'playbackRates')
|
||||
.addUINumberInput_('Playback Rate Slider Min',
|
||||
'playbackRateSliderMin',
|
||||
/* canBeDecimal= */ true)
|
||||
.addUINumberInput_('Playback Rate Slider Max',
|
||||
'playbackRateSliderMax',
|
||||
/* canBeDecimal= */ true)
|
||||
.addUIArrayNumberInput_('Fast Forward Rates', 'fastForwardRates')
|
||||
.addUIArrayNumberInput_('Rewind Rates', 'rewindRates')
|
||||
.addUIArrayNumberInput_('Captions Font Scale Factors',
|
||||
@@ -1227,6 +1234,14 @@ shakaDemo.Config = class {
|
||||
.addUITextInput_('Level Color', 'volumeBarColors.level');
|
||||
}
|
||||
|
||||
/** @private */
|
||||
addUIPlaybackRateBarColorsSection_() {
|
||||
const docLink = this.resolveExternLink_('.UIPlaybackRateBarColors');
|
||||
this.addSection_('UI: Playback Rate Bar Colors', docLink)
|
||||
.addUITextInput_('Base Color', 'playbackRateBarColors.base')
|
||||
.addUITextInput_('Level Color', 'playbackRateBarColors.level');
|
||||
}
|
||||
|
||||
/** @private */
|
||||
addUIQualityMarksSection_() {
|
||||
const docLink = this.resolveExternLink_('.UIQualityMarks');
|
||||
|
||||
@@ -30,7 +30,9 @@ shaka.test.FakeDemoMain = class {
|
||||
contextMenuElements: [],
|
||||
statisticsList: [],
|
||||
adStatisticsList: [],
|
||||
playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
|
||||
playbackRates: [1, 1.25, 1.5, 2, 3],
|
||||
playbackRateSliderMin: 0.5,
|
||||
playbackRateSliderMax: 3,
|
||||
fastForwardRates: [2, 4, 8, 1],
|
||||
rewindRates: [-1, -2, -4, -8],
|
||||
addSeekBar: true,
|
||||
@@ -50,6 +52,10 @@ shaka.test.FakeDemoMain = class {
|
||||
base: 'rgba(255, 255, 255, 0.54)',
|
||||
level: 'rgb(255, 255, 255)',
|
||||
},
|
||||
playbackRateBarColors: {
|
||||
base: 'rgba(255, 255, 255, 0.54)',
|
||||
level: 'rgb(255, 255, 255)',
|
||||
},
|
||||
qualityMarks: {
|
||||
'720': '',
|
||||
'1080': 'HD',
|
||||
|
||||
@@ -13,5 +13,6 @@
|
||||
@import "less/ad_controls.less";
|
||||
@import "less/tooltip.less";
|
||||
@import "less/thumbnails.less";
|
||||
@import "less/playback_rate.less";
|
||||
@import "less/material_svg_icon.less";
|
||||
@import (css, inline) "https://fonts.googleapis.com/css?family=Roboto";
|
||||
|
||||
+51
-1
@@ -60,6 +60,22 @@ shaka.extern.UISeekBarColors;
|
||||
*/
|
||||
shaka.extern.UIVolumeBarColors;
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* base: string,
|
||||
* level: string,
|
||||
* }}
|
||||
*
|
||||
* @property {string} base
|
||||
* The CSS background color applied to the base of the playback rate bar, on
|
||||
* top of which the current playback rate level is shown.
|
||||
* @property {string} level
|
||||
* The CSS background color applied to the portion of the playback rate bar
|
||||
* showing the current playback rate level.
|
||||
* @exportDoc
|
||||
*/
|
||||
shaka.extern.UIPlaybackRateBarColors;
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* 720: string,
|
||||
@@ -288,6 +304,8 @@ shaka.extern.UITrackLabelCallback;
|
||||
* statisticsList: !Array<string>,
|
||||
* adStatisticsList: !Array<string>,
|
||||
* playbackRates: !Array<number>,
|
||||
* playbackRateSliderMin: number,
|
||||
* playbackRateSliderMax: number,
|
||||
* fastForwardRates: !Array<number>,
|
||||
* rewindRates: !Array<number>,
|
||||
* addSeekBar: boolean,
|
||||
@@ -298,6 +316,7 @@ shaka.extern.UITrackLabelCallback;
|
||||
* showUnbufferedStart: boolean,
|
||||
* seekBarColors: shaka.extern.UISeekBarColors,
|
||||
* volumeBarColors: shaka.extern.UIVolumeBarColors,
|
||||
* playbackRateBarColors: shaka.extern.UIPlaybackRateBarColors,
|
||||
* qualityMarks: shaka.extern.UIQualityMarks,
|
||||
* trackLabelFormat: shaka.ui.Overlay.TrackLabelFormat,
|
||||
* textTrackLabelFormat: shaka.ui.Overlay.TrackLabelFormat,
|
||||
@@ -359,7 +378,19 @@ shaka.extern.UITrackLabelCallback;
|
||||
* @property {!Array<number>} playbackRates
|
||||
* The ordered list of rates for playback selection.
|
||||
* <br>
|
||||
* Defaults to <code>[0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]</code>.
|
||||
* Defaults to <code>[1, 1.25, 1.5, 2, 3]</code>.
|
||||
* @property {number} playbackRateSliderMin
|
||||
* The minimum playback rate available in the playback-rate slider.
|
||||
* This only affects the continuous slider range and does not add a preset
|
||||
* button to the playback-rate menu.
|
||||
* <br>
|
||||
* Defaults to <code>0.5</code>.
|
||||
* @property {number} playbackRateSliderMax
|
||||
* The maximum playback rate available in the playback-rate slider.
|
||||
* This only affects the continuous slider range and does not add a preset
|
||||
* button to the playback-rate menu.
|
||||
* <br>
|
||||
* Defaults to <code>3</code>.
|
||||
* @property {!Array<number>} fastForwardRates
|
||||
* The ordered list of rates for fast forward selection.
|
||||
* <br>
|
||||
@@ -414,6 +445,10 @@ shaka.extern.UITrackLabelCallback;
|
||||
* The CSS colors applied to the volume bar. This allows you to override the
|
||||
* colors used in the linear gradient constructed in JavaScript, since you
|
||||
* cannot do this in pure CSS.
|
||||
* @property {shaka.extern.UIPlaybackRateBarColors} playbackRateBarColors
|
||||
* The CSS colors applied to the playback rate bar. This allows you to
|
||||
* override the colors used in the linear gradient constructed in JavaScript,
|
||||
* since you cannot do this in pure CSS.
|
||||
* @property {shaka.extern.UIQualityMarks} qualityMarks
|
||||
* The name of the quality marks.
|
||||
* @property {shaka.ui.Overlay.TrackLabelFormat} trackLabelFormat
|
||||
@@ -807,6 +842,21 @@ shaka.extern.IUIRangeElement = class {
|
||||
*/
|
||||
setStep(step) {}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getMin() {}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getMax() {}
|
||||
|
||||
/**
|
||||
* @param {string} background
|
||||
*/
|
||||
setBackground(background) {}
|
||||
|
||||
/**
|
||||
* Called when user interaction begins.
|
||||
* To be overridden by subclasses.
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/** @license
|
||||
* Shaka Player
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// Step buttons (− / +)
|
||||
@playback-rate-step-btn-border: fade(@general-font-color, 50%);
|
||||
@playback-rate-step-btn-border-active: fade(@general-font-color, 85%);
|
||||
@playback-rate-step-btn-bg-active: fade(@general-font-color, 10%);
|
||||
|
||||
// Slider track
|
||||
@playback-rate-slider-track: fade(@general-font-color, 25%);
|
||||
|
||||
// Preset pill buttons
|
||||
@playback-rate-preset-bg: fade(@general-font-color, 10%);
|
||||
@playback-rate-preset-bg-hover: fade(@general-font-color, 18%);
|
||||
@playback-rate-preset-selected-bg: fade(@general-font-color, 20%);
|
||||
@playback-rate-preset-selected-border: fade(@general-font-color, 70%);
|
||||
|
||||
/* Menu container */
|
||||
|
||||
.shaka-playback-rates {
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
/* Slider section */
|
||||
|
||||
.shaka-playback-rate-slider-section {
|
||||
padding: 8px 0 4px;
|
||||
}
|
||||
|
||||
.shaka-playback-rate-value {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
color: @general-font-color;
|
||||
font-variant-numeric: tabular-nums;
|
||||
padding: 2px 0 10px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.shaka-playback-rate-slider-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
/* Step buttons (− and +) */
|
||||
|
||||
.shaka-playback-rate-step-btn {
|
||||
/* Reset inherited .shaka-overflow-menu button styles. */
|
||||
min-height: unset !important;
|
||||
padding: 0 !important;
|
||||
flex-shrink: 0;
|
||||
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50% !important;
|
||||
border: 1.5px solid @playback-rate-step-btn-border !important;
|
||||
background: transparent !important;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
color: @general-font-color;
|
||||
|
||||
transition:
|
||||
border-color 150ms ease,
|
||||
background 150ms ease,
|
||||
opacity 150ms ease;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: @playback-rate-step-btn-border-active !important;
|
||||
background: @playback-rate-step-btn-bg-active !important;
|
||||
}
|
||||
|
||||
.shaka-keyboard-navigation &:focus:not(:disabled) {
|
||||
border-color: @playback-rate-step-btn-border-active !important;
|
||||
background: @playback-rate-step-btn-bg-active !important;
|
||||
}
|
||||
}
|
||||
|
||||
.shaka-playback-rate-slider-container {
|
||||
flex: 1;
|
||||
background: @playback-rate-slider-track;
|
||||
}
|
||||
|
||||
/* Preset pill buttons */
|
||||
|
||||
/* Horizontal strip. */
|
||||
.shaka-playback-rate-presets {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
/* Centre pills when they fit; safe-center degrades to flex-start on
|
||||
* overflow so the leftmost pill stays reachable via scroll. */
|
||||
justify-content: safe center;
|
||||
/* stylelint-disable-next-line declaration-block-no-duplicate-properties */
|
||||
justify-content: center; /* fallback for browsers without "safe" */
|
||||
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
|
||||
gap: 6px;
|
||||
padding: 6px 10px 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.shaka-playback-rate-preset-btn {
|
||||
/* Reset inherited .shaka-overflow-menu button styles. */
|
||||
min-height: unset !important;
|
||||
padding: 5px 14px !important;
|
||||
border-radius: 20px !important;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
font-size: 13px;
|
||||
color: @general-font-color;
|
||||
background: @playback-rate-preset-bg !important;
|
||||
border: 1.5px solid transparent !important;
|
||||
|
||||
transition: background 150ms ease, border-color 150ms ease;
|
||||
|
||||
&:hover {
|
||||
background: @playback-rate-preset-bg-hover !important;
|
||||
}
|
||||
|
||||
.shaka-keyboard-navigation &:focus {
|
||||
background: @playback-rate-preset-bg-hover !important;
|
||||
}
|
||||
|
||||
&.shaka-chosen-item {
|
||||
background: @playback-rate-preset-selected-bg !important;
|
||||
border-color: @playback-rate-preset-selected-border !important;
|
||||
font-weight: 600;
|
||||
color: @general-font-color;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
+253
-55
@@ -7,13 +7,15 @@
|
||||
|
||||
goog.provide('shaka.ui.PlaybackRateSelection');
|
||||
|
||||
goog.require('goog.asserts');
|
||||
goog.require('shaka.ui.Controls');
|
||||
goog.require('shaka.ui.Enums');
|
||||
goog.require('shaka.ui.Locales');
|
||||
goog.require('shaka.ui.OverflowMenu');
|
||||
goog.require('shaka.ui.RangeElement');
|
||||
goog.require('shaka.ui.SettingsMenu');
|
||||
goog.require('shaka.ui.Utils');
|
||||
goog.require('shaka.util.Dom');
|
||||
goog.require('shaka.util.NumberUtils');
|
||||
goog.requireType('shaka.ui.Controls');
|
||||
|
||||
/**
|
||||
@@ -30,16 +32,34 @@ shaka.ui.PlaybackRateSelection = class extends shaka.ui.SettingsMenu {
|
||||
super(parent, controls,
|
||||
shaka.ui.Enums.MaterialDesignSVGIcons['PLAYBACK_RATE']);
|
||||
|
||||
/** @private {!shaka.extern.UIConfiguration} */
|
||||
this.config_ = this.controls.getConfig();
|
||||
|
||||
this.button.classList.add('shaka-playbackrate-button');
|
||||
this.menu.classList.add('shaka-playback-rates');
|
||||
this.button.classList.add('shaka-tooltip-status');
|
||||
|
||||
if (!this.isSubMenu) {
|
||||
this.playbackRateMark = shaka.util.Dom.createHTMLElement('span');
|
||||
this.playbackRateMark.classList.add('shaka-overflow-playback-rate-mark');
|
||||
this.button.appendChild(this.playbackRateMark);
|
||||
/** @private {HTMLElement} */
|
||||
this.playbackRateMark_ = shaka.util.Dom.createHTMLElement('span');
|
||||
this.playbackRateMark_.classList.add('shaka-overflow-playback-rate-mark');
|
||||
this.button.appendChild(this.playbackRateMark_);
|
||||
}
|
||||
|
||||
/** @private {shaka.ui.RangeElement} */
|
||||
this.rateSlider_ = null;
|
||||
|
||||
/** @private {HTMLElement} */
|
||||
this.speedValue_ = null;
|
||||
|
||||
/** @private {HTMLButtonElement} */
|
||||
this.decreaseButton_ = null;
|
||||
|
||||
/** @private {HTMLButtonElement} */
|
||||
this.increaseButton_ = null;
|
||||
|
||||
this.buildUI_();
|
||||
|
||||
this.eventManager.listenMulti(
|
||||
this.player,
|
||||
[
|
||||
@@ -49,12 +69,19 @@ shaka.ui.PlaybackRateSelection = class extends shaka.ui.SettingsMenu {
|
||||
this.updatePlaybackRateSelection_();
|
||||
});
|
||||
|
||||
// Set up all the strings in the user's preferred language.
|
||||
this.updateLocalizedStrings();
|
||||
this.addPlaybackRates_();
|
||||
this.updatePlaybackRateSelection_();
|
||||
}
|
||||
|
||||
/** @override */
|
||||
release() {
|
||||
if (this.rateSlider_) {
|
||||
this.rateSlider_.release();
|
||||
this.rateSlider_ = null;
|
||||
}
|
||||
super.release();
|
||||
}
|
||||
|
||||
/** @override */
|
||||
updateLocalizedStrings() {
|
||||
const LocIds = shaka.ui.Locales.Ids;
|
||||
@@ -65,73 +92,244 @@ shaka.ui.PlaybackRateSelection = class extends shaka.ui.SettingsMenu {
|
||||
this.backSpan.textContent = this.localization.resolve(LocIds.PLAYBACK_RATE);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
buildUI_() {
|
||||
// Slider section
|
||||
const sliderSection = shaka.util.Dom.createHTMLElement('div');
|
||||
sliderSection.classList.add('shaka-playback-rate-slider-section');
|
||||
|
||||
|
||||
this.speedValue_ = shaka.util.Dom.createHTMLElement('div');
|
||||
this.speedValue_.classList.add('shaka-playback-rate-value');
|
||||
sliderSection.appendChild(this.speedValue_);
|
||||
|
||||
// Slider row: [−] ──────slider────── [+]
|
||||
const sliderRow = shaka.util.Dom.createHTMLElement('div');
|
||||
sliderRow.classList.add('shaka-playback-rate-slider-row');
|
||||
|
||||
// Decrease button (−)
|
||||
this.decreaseButton_ = shaka.util.Dom.createButton();
|
||||
this.decreaseButton_.classList.add('shaka-playback-rate-step-btn');
|
||||
this.decreaseButton_.classList.add('shaka-no-propagation');
|
||||
this.decreaseButton_.setAttribute('aria-label', '−');
|
||||
this.decreaseButton_.textContent = '−';
|
||||
sliderRow.appendChild(this.decreaseButton_);
|
||||
|
||||
// Range slider.
|
||||
goog.asserts.assert(this.controls, 'Controls should not be null!');
|
||||
this.rateSlider_ = new shaka.ui.RangeElement(
|
||||
sliderRow,
|
||||
this.controls,
|
||||
/* containerClassNames= */
|
||||
['shaka-playback-rate-slider-container', 'shaka-no-propagation'],
|
||||
/* barClassNames= */ ['shaka-playback-rate-slider'],
|
||||
/* enableWheel= */ true);
|
||||
|
||||
this.rateSlider_.setStep(shaka.ui.PlaybackRateSelection.SLIDER_STEP);
|
||||
|
||||
this.rateSlider_.onChange = () => {
|
||||
this.applyRate_(this.rateSlider_.getValue());
|
||||
};
|
||||
|
||||
// Increase button (+)
|
||||
this.increaseButton_ = shaka.util.Dom.createButton();
|
||||
this.increaseButton_.classList.add('shaka-playback-rate-step-btn');
|
||||
this.increaseButton_.classList.add('shaka-no-propagation');
|
||||
this.increaseButton_.setAttribute('aria-label', '+');
|
||||
this.increaseButton_.textContent = '+';
|
||||
sliderRow.appendChild(this.increaseButton_);
|
||||
|
||||
sliderSection.appendChild(sliderRow);
|
||||
this.menu.appendChild(sliderSection);
|
||||
|
||||
// Step-button listeners.
|
||||
this.eventManager.listen(this.decreaseButton_, 'click', () => {
|
||||
this.stepRate_(-shaka.ui.PlaybackRateSelection.SLIDER_STEP);
|
||||
});
|
||||
this.eventManager.listen(this.increaseButton_, 'click', () => {
|
||||
this.stepRate_(shaka.ui.PlaybackRateSelection.SLIDER_STEP);
|
||||
});
|
||||
|
||||
// Preset pill buttons (horizontal)
|
||||
|
||||
const presetsRow = shaka.util.Dom.createHTMLElement('div');
|
||||
presetsRow.classList.add('shaka-playback-rate-presets');
|
||||
|
||||
for (const rate of this.controls.getConfig().playbackRates) {
|
||||
const btn = shaka.util.Dom.createButton();
|
||||
btn.classList.add('shaka-playback-rate-preset-btn');
|
||||
btn.setAttribute('role', 'menuitemradio');
|
||||
btn.setAttribute('aria-checked', 'false');
|
||||
btn.dataset['rate'] = String(rate);
|
||||
btn.textContent = this.formatPresetLabel_(rate);
|
||||
|
||||
this.eventManager.listen(btn, 'click', () => {
|
||||
this.applyRate_(rate);
|
||||
});
|
||||
|
||||
presetsRow.appendChild(btn);
|
||||
}
|
||||
|
||||
this.menu.appendChild(presetsRow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update checkmark icon and related class and attribute for the chosen rate
|
||||
* button.
|
||||
* @param {number} rate
|
||||
* @private
|
||||
*/
|
||||
applyRate_(rate) {
|
||||
if (rate === this.video.defaultPlaybackRate) {
|
||||
this.player.cancelTrickPlay();
|
||||
} else {
|
||||
this.player.trickPlay(rate, /* useTrickPlayTrack= */ false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Steps the playback rate by `delta`, snapped to SLIDER_STEP and clamped to
|
||||
* the configured [playbackRateSliderMin, playbackRateSliderMax] range.
|
||||
*
|
||||
* Snapping the current rate to the step grid before adding delta ensures that
|
||||
* a programmatic value like 0.97 is brought back onto the grid (0.95 or
|
||||
* 1.00) on the next user interaction rather than accumulating floating-point
|
||||
* drift.
|
||||
*
|
||||
* @param {number} delta
|
||||
* @private
|
||||
*/
|
||||
stepRate_(delta) {
|
||||
const config = this.controls.getConfig();
|
||||
const min = config.playbackRateSliderMin;
|
||||
const max = config.playbackRateSliderMax;
|
||||
const step = shaka.ui.PlaybackRateSelection.SLIDER_STEP;
|
||||
|
||||
const current = this.player.getPlaybackRate();
|
||||
// Snap current rate to the nearest step grid point, then apply delta.
|
||||
const snapped = Math.round(current / step) * step;
|
||||
const raw = snapped + delta;
|
||||
// Eliminate floating-point noise (e.g. 0.05 * 20 → 1.0000000000000002).
|
||||
const next = parseFloat((Math.round(raw / step) * step).toPrecision(10));
|
||||
const clamped = Math.max(min, Math.min(max, next));
|
||||
|
||||
this.applyRate_(clamped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the effective slider range.
|
||||
*
|
||||
* Normally this is [playbackRateSliderMin, playbackRateSliderMax] from the
|
||||
* config. If the current rate was set programmatically outside that range,
|
||||
* the bounds are extended just enough to keep the thumb visible.
|
||||
*
|
||||
* @return {{min: number, max: number}}
|
||||
* @private
|
||||
*/
|
||||
getSliderRange_() {
|
||||
const config = this.controls.getConfig();
|
||||
let min = config.playbackRateSliderMin;
|
||||
let max = config.playbackRateSliderMax;
|
||||
const currentRate = this.player.getPlaybackRate();
|
||||
if (currentRate < min) {
|
||||
min = currentRate;
|
||||
}
|
||||
if (currentRate > max) {
|
||||
max = currentRate;
|
||||
}
|
||||
return {min, max};
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs slider range/value, live value label, step-button disabled state,
|
||||
* and preset-pill highlights to the current player rate.
|
||||
* @private
|
||||
*/
|
||||
updatePlaybackRateSelection_() {
|
||||
const config = this.controls.getConfig();
|
||||
const rate = this.player.getPlaybackRate();
|
||||
// Remove the old checkmark icon and related tags and classes if it exists.
|
||||
const checkmarkIcon = shaka.ui.Utils.getDescendantIfExists(
|
||||
this.menu, 'shaka-ui-icon shaka-chosen-item');
|
||||
if (checkmarkIcon) {
|
||||
const previouslySelectedButton = checkmarkIcon.parentElement;
|
||||
previouslySelectedButton.removeAttribute('aria-selected');
|
||||
const previouslySelectedSpan =
|
||||
previouslySelectedButton.getElementsByTagName('span')[0];
|
||||
if (previouslySelectedSpan) {
|
||||
previouslySelectedSpan.classList.remove('shaka-chosen-item');
|
||||
}
|
||||
previouslySelectedButton.removeChild(checkmarkIcon);
|
||||
}
|
||||
// Find the button that represents the newly selected playback rate.
|
||||
// Add the checkmark icon, related tags and classes to the newly selected
|
||||
// button.
|
||||
const span = Array.from(this.menu.querySelectorAll('span')).find((el) => {
|
||||
return el.textContent == (rate + 'x');
|
||||
});
|
||||
if (span) {
|
||||
const button = span.parentElement;
|
||||
button.appendChild(shaka.ui.Utils.checkmarkIcon());
|
||||
button.setAttribute('aria-checked', 'true');
|
||||
span.classList.add('shaka-chosen-item');
|
||||
|
||||
// Update slider range first (may be extended for out-of-config rates).
|
||||
const {min, max} = this.getSliderRange_();
|
||||
this.rateSlider_.setRange(min, max);
|
||||
this.rateSlider_.setValue(rate);
|
||||
|
||||
// Large centred value label.
|
||||
this.speedValue_.textContent = rate.toFixed(2) + 'x';
|
||||
|
||||
// Disable step buttons at the configured hard limits (not the extended
|
||||
// ones) so the user cannot go beyond the intended range by clicking.
|
||||
this.decreaseButton_.disabled = rate <= config.playbackRateSliderMin;
|
||||
this.increaseButton_.disabled = rate >= config.playbackRateSliderMax;
|
||||
|
||||
// Highlight the matching preset pill, if any.
|
||||
const presetBtns =
|
||||
this.menu.querySelectorAll('.shaka-playback-rate-preset-btn');
|
||||
for (const btn of presetBtns) {
|
||||
const button = /** @type {!HTMLButtonElement} */ (btn);
|
||||
const btnRate = parseFloat(button.dataset['rate']);
|
||||
const isChosen =
|
||||
shaka.util.NumberUtils.isFloatEqual(btnRate, rate, 0.001);
|
||||
button.setAttribute('aria-checked', isChosen ? 'true' : 'false');
|
||||
button.classList.toggle('shaka-chosen-item', isChosen);
|
||||
}
|
||||
|
||||
// Set the label to display the current playback rate in the overflow menu,
|
||||
// in the format of '1x', '1.5x', etc.
|
||||
// Overflow-menu badge / tooltip.
|
||||
this.currentSelection.textContent = rate + 'x';
|
||||
this.button.setAttribute('shaka-status', rate + 'x');
|
||||
if (this.playbackRateMark) {
|
||||
this.playbackRateMark.textContent = rate + 'x';
|
||||
if (this.playbackRateMark_) {
|
||||
this.playbackRateMark_.textContent = rate + 'x';
|
||||
}
|
||||
this.updateColors_();
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a preset rate for display inside a pill button.
|
||||
* Rules
|
||||
* - Comma as decimal separator.
|
||||
* - Always at least one decimal place (e.g. 1 to "1,0", 3 to "3,0").
|
||||
*
|
||||
* @param {number} rate
|
||||
* @return {string}
|
||||
* @private
|
||||
*/
|
||||
formatPresetLabel_(rate) {
|
||||
// Determine how many decimal places the value naturally has.
|
||||
const str = rate.toString(); // e.g. "1", "1.25", "0.5"
|
||||
const dotIndex = str.indexOf('.');
|
||||
const decimals = dotIndex === -1 ? 0 : str.length - dotIndex - 1;
|
||||
// Show at least one decimal place.
|
||||
return rate.toFixed(Math.max(1, decimals)).replace('.', ',');
|
||||
}
|
||||
|
||||
/** @private */
|
||||
addPlaybackRates_() {
|
||||
for (const rate of this.controls.getConfig().playbackRates) {
|
||||
const button = shaka.util.Dom.createButton();
|
||||
// ARIA: single-select menu item
|
||||
button.setAttribute('role', 'menuitemradio');
|
||||
button.setAttribute('aria-checked', 'false');
|
||||
const span = shaka.util.Dom.createHTMLElement('span');
|
||||
span.textContent = rate + 'x';
|
||||
button.appendChild(span);
|
||||
updateColors_() {
|
||||
const colors = this.config_.playbackRateBarColors;
|
||||
|
||||
this.eventManager.listen(button, 'click', () => {
|
||||
if (rate == this.video.defaultPlaybackRate) {
|
||||
this.player.cancelTrickPlay();
|
||||
} else {
|
||||
this.player.trickPlay(rate, /* useTrickPlayTrack= */ false);
|
||||
}
|
||||
});
|
||||
const value = this.rateSlider_.getValue();
|
||||
const min = this.rateSlider_.getMin();
|
||||
const max = this.rateSlider_.getMax();
|
||||
|
||||
this.menu.appendChild(button);
|
||||
}
|
||||
shaka.ui.Utils.focusOnTheChosenItem(this.menu);
|
||||
// Convert current value to percentage within slider range.
|
||||
const percent = ((value - min) / (max - min)) * 100;
|
||||
|
||||
const gradient = ['to right'];
|
||||
gradient.push(colors.level + '0%');
|
||||
gradient.push(colors.level + percent + '%');
|
||||
gradient.push(colors.base + percent + '%');
|
||||
gradient.push(colors.base + '100%');
|
||||
|
||||
this.rateSlider_.setBackground(
|
||||
'linear-gradient(' + gradient.join(',') + ')');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Step size used for slider interaction and the +/- buttons.
|
||||
* @const {number}
|
||||
*/
|
||||
shaka.ui.PlaybackRateSelection.SLIDER_STEP = 0.05;
|
||||
|
||||
|
||||
/**
|
||||
* @implements {shaka.extern.IUIElement.Factory}
|
||||
* @final
|
||||
|
||||
@@ -219,6 +219,30 @@ shaka.ui.RangeElement = class extends shaka.ui.Element {
|
||||
this.bar.step = step;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @export
|
||||
*/
|
||||
getMin() {
|
||||
return parseFloat(this.bar.min);
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @export
|
||||
*/
|
||||
getMax() {
|
||||
return parseFloat(this.bar.max);
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @export
|
||||
*/
|
||||
setBackground(background) {
|
||||
this.container.style.background = background;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when user interaction begins.
|
||||
* To be overridden by subclasses.
|
||||
|
||||
@@ -363,7 +363,9 @@ shaka.ui.Overlay = class {
|
||||
'copy_video_frame',
|
||||
'save_video_frame',
|
||||
],
|
||||
playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
|
||||
playbackRates: [1, 1.25, 1.5, 2, 3],
|
||||
playbackRateSliderMin: 0.5,
|
||||
playbackRateSliderMax: 3,
|
||||
fastForwardRates: [2, 4, 8, 1],
|
||||
rewindRates: [-1, -2, -4, -8],
|
||||
addSeekBar: true,
|
||||
@@ -383,6 +385,10 @@ shaka.ui.Overlay = class {
|
||||
base: 'rgba(255, 255, 255, 0.54)',
|
||||
level: 'rgb(255, 255, 255)',
|
||||
},
|
||||
playbackRateBarColors: {
|
||||
base: 'rgba(255, 255, 255, 0.54)',
|
||||
level: 'rgb(255, 255, 255)',
|
||||
},
|
||||
qualityMarks: {
|
||||
'720': '',
|
||||
'1080': 'HD',
|
||||
|
||||
Reference in New Issue
Block a user