diff --git a/demo/config.js b/demo/config.js index ef70cde7d..3fb20d4d4 100644 --- a/demo/config.js +++ b/demo/config.js @@ -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'); diff --git a/test/test/util/fake_demo_main.js b/test/test/util/fake_demo_main.js index ecd348768..4d73d4fe2 100644 --- a/test/test/util/fake_demo_main.js +++ b/test/test/util/fake_demo_main.js @@ -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', diff --git a/ui/controls.less b/ui/controls.less index d71f08659..9b318a142 100644 --- a/ui/controls.less +++ b/ui/controls.less @@ -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"; diff --git a/ui/externs/ui.js b/ui/externs/ui.js index 322380edd..166064333 100644 --- a/ui/externs/ui.js +++ b/ui/externs/ui.js @@ -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, * adStatisticsList: !Array, * playbackRates: !Array, + * playbackRateSliderMin: number, + * playbackRateSliderMax: number, * fastForwardRates: !Array, * rewindRates: !Array, * 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} playbackRates * The ordered list of rates for playback selection. *
- * Defaults to [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]. + * Defaults to [1, 1.25, 1.5, 2, 3]. + * @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. + *
+ * Defaults to 0.5. + * @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. + *
+ * Defaults to 3. * @property {!Array} fastForwardRates * The ordered list of rates for fast forward selection. *
@@ -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. diff --git a/ui/less/playback_rate.less b/ui/less/playback_rate.less new file mode 100644 index 000000000..1c5bb31cf --- /dev/null +++ b/ui/less/playback_rate.less @@ -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; + } +} diff --git a/ui/playback_rate_selection.js b/ui/playback_rate_selection.js index 5931d005a..b396fc1da 100644 --- a/ui/playback_rate_selection.js +++ b/ui/playback_rate_selection.js @@ -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 diff --git a/ui/range_element.js b/ui/range_element.js index cff2f3858..14e5a7e1a 100644 --- a/ui/range_element.js +++ b/ui/range_element.js @@ -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. diff --git a/ui/ui.js b/ui/ui.js index 04a54bbeb..e49a8d05d 100644 --- a/ui/ui.js +++ b/ui/ui.js @@ -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',