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:
Álvaro Velad Galván
2026-05-28 10:34:48 +02:00
committed by GitHub
parent 61e64a9c2d
commit f2de74885e
8 changed files with 516 additions and 58 deletions
+15
View File
@@ -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');
+7 -1
View File
@@ -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',
+1
View File
@@ -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
View File
@@ -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.
+158
View File
@@ -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
View File
@@ -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
+24
View File
@@ -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.
+7 -1
View File
@@ -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',