add quality selector, share menu, ios playback fixes

This commit is contained in:
Spendable1569 2026-08-24 12:31:35 +08:00
parent 9141550a5a
commit 0164a87b8e
2 changed files with 704 additions and 52 deletions

View File

@ -108,7 +108,8 @@ ul.vjs-menu-content::-webkit-scrollbar {
} }
.vjs-quality-selector, .vjs-quality-selector,
.video-js .vjs-quality-menu-wrapper { .video-js .vjs-quality-menu-wrapper,
.vjs-quality-multi-selector {
order: 4; order: 4;
} }
@ -260,3 +261,119 @@ video.video-js {
overflow-x: hidden; overflow-x: hidden;
} }
} }
/* --- Share Dialog & Button Styles --- */
.vjs-share-control .vjs-icon-placeholder svg {
display: block;
margin: 0 auto;
}
.vjs-share-dialog .vjs-modal-dialog-content {
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
overflow-y: auto;
max-height: 100%;
box-sizing: border-box;
}
.share-dialog-content {
width: 100%;
max-width: 750px;
max-height: 100%;
overflow-y: auto;
box-sizing: border-box;
}
.share-dialog-header {
text-align: center;
font-size: 24px;
font-weight: 600;
color: #fff;
margin-bottom: 15px;
letter-spacing: 0.5px;
}
.share-input-group {
margin-bottom: 15px;
}
.share-input-group label {
display: block;
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
margin-bottom: 6px;
}
.share-input-wrapper {
display: flex;
background: rgba(255, 255, 255, 0.08);
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 4px;
}
.share-input-wrapper:focus-within {
border-color: rgba(0, 182, 240, 0.6);
}
.share-input-wrapper input {
flex: 1;
background: transparent;
border: none;
padding: 10px 12px;
color: #fff;
font-size: 14px;
outline: none;
width: 100%;
}
.share-input-wrapper input::selection {
background: rgba(0, 182, 240, 0.3);
}
.share-copy-btn {
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.6);
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
}
.share-copy-btn:hover {
color: #fff;
background: rgba(255, 255, 255, 0.1);
}
.share-dialog-socials {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.social-btn {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 6px;
text-decoration: none;
color: #fff;
font-size: 20px;
font-weight: bold;
line-height: 1;
}
.social-fb { background: #1877F2; }
.social-tw { background: #000; }
.social-reddit { background: #FF4500; }
.social-email { background: #0072C6; }

View File

@ -3,6 +3,504 @@ var player_data = JSON.parse(document.getElementById('player_data').textContent)
var video_data = JSON.parse(document.getElementById('video_data').textContent); var video_data = JSON.parse(document.getElementById('video_data').textContent);
const CONFIG = JSON.parse(document.getElementById('config').textContent); const CONFIG = JSON.parse(document.getElementById('config').textContent);
// ------------------ Invidious custom quality selector ------------------
const ITAG_MAP = {
'17': { height: 144, label: '144p' },
'18': { height: 360, label: '360p' },
'22': { height: 720, label: '720p' }
};
const AUDIO_ITAG_MAP = {
'139': { bitrate: 48, codec: 'm4a', label: 'm4a48k' },
'140': { bitrate: 128, codec: 'm4a', label: 'm4a128k' },
'141': { bitrate: 256, codec: 'm4a', label: 'm4a256k' },
'249': { bitrate: 50, codec: 'opus', label: 'opus50k' },
'250': { bitrate: 70, codec: 'opus', label: 'opus70k' },
'251': { bitrate: 160, codec: 'opus', label: 'opus160k' }
};
class QualityMenuItem extends videojs.getComponent('MenuItem') {
handleClick(event) {
super.handleClick(event);
if (this.options_.handler) {
this.options_.handler.call(this, event);
}
const menu = this.parentComponent_;
if (menu && menu.menuButton_) {
menu.menuButton_.unpressButton();
}
}
}
class QualityMultiSelector extends videojs.getComponent('MenuButton') {
constructor(player, options = {}) {
super(player, options);
this.controlText('Quality');
this.addClass('vjs-quality-multi-selector');
const iconSpan = this.el().querySelector('.vjs-icon-placeholder');
if (iconSpan) {
iconSpan.classList.add('vjs-icon-cog');
}
this._onDocumentTouchEnd = (event) => {
if (!this.buttonPressed_) return;
const target = event.target;
if (
target &&
!this.el().contains(target) &&
!this.menu.el().contains(target)
) {
this.unpressButton();
}
};
document.addEventListener('touchend', this._onDocumentTouchEnd);
this.on('dispose', () => {
document.removeEventListener('touchend', this._onDocumentTouchEnd);
});
Object.assign(this, {
_mode: 'dash',
_dashLevels: [],
_mp4Sources: [],
_selectedIndex: -1,
_autoSelected: true,
_suppressEventsUntilLoaded: false
});
player._qualityMultiSelector = this;
this._updateModeAndData();
this._setupListeners();
this._rebuildMenu();
}
_setupListeners() {
const player = this.player();
const update = () => {
if (this._suppressEventsUntilLoaded) return;
this._updateModeAndData();
this._rebuildMenu();
};
if (player.qualityLevels) {
const qLevels = player.qualityLevels();
qLevels.on('addqualitylevel', update);
qLevels.on('removequalitylevel', update);
}
player.on('sourceset', update);
player.on('loadedmetadata', update);
}
_updateModeAndData() {
const player = this.player();
const qLevels = player.qualityLevels ? player.qualityLevels() : null;
if (qLevels && qLevels.length > 0) {
this._mode = 'dash';
this._mp4Sources = [];
this._dashLevels = Array.prototype.map.call(qLevels, (level, originalIndex) => ({ level, originalIndex }))
.sort((a, b) =>
(this._getHeight(b.level) - this._getHeight(a.level)) ||
(this._getFrameRate(b.level) - this._getFrameRate(a.level)) ||
(this._getBitrate(b.level) - this._getBitrate(a.level))
);
this._selectedIndex = -1;
} else {
this._dashLevels = [];
const sources = player.currentSources ? player.currentSources() : [];
const allAudio = sources.length > 0 && sources.every(source => this._isAudioSource(source));
this._mode = allAudio ? 'audio' : 'mp4';
this._mp4Sources = sources
.slice()
.sort((a, b) => {
if (this._mode === 'audio') {
return this._getAudioBitrate(b) - this._getAudioBitrate(a);
}
return this._heightFromSource(b) - this._heightFromSource(a);
});
this._selectedIndex = -1;
this._autoSelected = true;
const currentSrc = player.currentSrc();
if (currentSrc) {
const normalizedCurrent = this._normalizeSrc(currentSrc);
const index = this._mp4Sources.findIndex(
source => this._normalizeSrc(source.src) === normalizedCurrent
);
if (index !== -1) {
this._selectedIndex = index;
this._autoSelected = false;
}
}
}
}
_normalizeSrc(src) {
if (!src) return '';
try {
return new URL(src, document.baseURI).href;
} catch (e) {
return src;
}
}
_isAudioSource(source) {
if (!source) return false;
const src = source.src || '';
const itag = (src.match(/itag=(\d+)/i) || [])[1];
if (itag && AUDIO_ITAG_MAP[itag]) return true;
return (source.type || '').toLowerCase().startsWith('audio/');
}
_getAudioBitrate(source) {
const src = source.src || '';
const itag = (src.match(/itag=(\d+)/i) || [])[1];
if (itag && AUDIO_ITAG_MAP[itag]) return AUDIO_ITAG_MAP[itag].bitrate;
return source.bitrate || 0;
}
_rebuildMenu() {
if (!this.menu) return;
while (this.menu.children_.length) {
this.menu.removeChild(this.menu.children_[0]);
}
this.createItems().forEach(item => this.menu.addChild(item));
}
_updateSelectedMenuItem(selectedIndex) {
if (!this.menu) return;
const items = this.menu.children_;
for (let i = 0; i < items.length; i++) {
if (i === selectedIndex) {
items[i].addClass('vjs-selected');
} else {
items[i].removeClass('vjs-selected');
}
}
}
createItems() {
if (!this._dashLevels) this._dashLevels = [];
if (!this._mp4Sources) this._mp4Sources = [];
if (this._mode === undefined) this._mode = 'dash';
if (this._autoSelected === undefined) this._autoSelected = true;
if (this._selectedIndex === undefined) this._selectedIndex = -1;
const items = [];
const addItem = (label, className, handler, selected) => {
const item = new QualityMenuItem(this.player(), { label, handler });
if (className) item.addClass(className);
if (selected) item.addClass('vjs-selected');
items.push(item);
};
if (this._mode === 'dash') {
addItem('Auto', 'vjs-quality-menu-item-auto', () => this.selectAuto(), this._autoSelected);
for (const { level, originalIndex } of this._dashLevels) {
addItem(
this.labelForDashLevel(level),
'vjs-quality-menu-item',
() => this.selectDashQuality(originalIndex),
!this._autoSelected && this._selectedIndex === originalIndex
);
}
} else if (this._mode === 'audio') {
this._mp4Sources.forEach((source, index) => {
addItem(
this.labelForAudioSource(source),
'vjs-quality-menu-item',
() => this.selectMp4Quality(index),
this._selectedIndex === index
);
});
} else {
this._mp4Sources.forEach((source, index) => {
addItem(
this.labelForMp4Source(source),
'vjs-quality-menu-item',
() => this.selectMp4Quality(index),
this._selectedIndex === index
);
});
}
return items;
}
labelForDashLevel(level) {
const height = this._getHeight(level);
if (height > 0) {
const fps = this._getFrameRate(level);
return fps > 0 ? `${height}p@${fps}` : `${height}p`;
}
if (level.width) return `${level.width}p`;
if (level.bitrate) return this._formatBitrate(level.bitrate);
return `Quality ${level.id}`;
}
labelForMp4Source(source) {
const src = source.src || '';
const itag = (src.match(/itag=(\d+)/i) || [])[1];
const info = ITAG_MAP[itag];
const label = info ? info.label : (source.label || 'Source');
return `${label}-${src.toLowerCase().includes('local=true') ? 'local' : 'google'}`;
}
labelForAudioSource(source) {
const src = source.src || '';
const itag = (src.match(/itag=(\d+)/i) || [])[1];
const info = AUDIO_ITAG_MAP[itag];
const label = info ? info.label : (source.label || 'Audio');
return `${label}-${src.toLowerCase().includes('local=true') ? 'local' : 'google'}`;
}
_heightFromSource(source) {
const src = source.src || '';
const itag = (src.match(/itag=(\d+)/i) || [])[1];
if (itag) return ITAG_MAP[itag] ? ITAG_MAP[itag].height : 0;
return source.height || 0;
}
_getHeight(level) { return level.height || 0; }
_getFrameRate(level) {
let fps = level.frameRate;
if (fps === undefined || fps === null) return 0;
if (typeof fps === 'string') {
const match = fps.match(/(\d+(?:\.\d+)?)/);
if (!match) return 0;
fps = parseFloat(match[1]);
}
const num = Math.round(Number(fps));
return isNaN(num) ? 0 : num;
}
_getBitrate(level) { return level.bitrate || 0; }
_formatBitrate(bits) {
return bits >= 1000000
? `${(bits / 1000000).toFixed(1)} Mbps`
: `${Math.round(bits / 1000)} kbps`;
}
selectAuto() {
const levels = this.player().qualityLevels();
for (let i = 0; i < levels.length; i++) {
levels[i].enabled = true;
}
this._autoSelected = true;
this._selectedIndex = -1;
this._updateSelectedMenuItem(0);
}
selectDashQuality(originalIndex) {
const levels = this.player().qualityLevels();
for (let i = 0; i < levels.length; i++) {
levels[i].enabled = (i === originalIndex);
}
this._autoSelected = false;
this._selectedIndex = originalIndex;
const itemIndex = this._dashLevels.findIndex(
({ originalIndex: idx }) => idx === originalIndex
);
if (itemIndex !== -1) {
this._updateSelectedMenuItem(itemIndex + 1);
}
}
selectMp4Quality(index) {
const source = this._mp4Sources[index];
if (!source) return;
const player = this.player();
const currentTime = player.currentTime();
const wasPaused = player.paused();
this._suppressEventsUntilLoaded = true;
player.src(source);
player.one('loadedmetadata', () => {
player.currentTime(currentTime);
if (!wasPaused) player.play();
this._suppressEventsUntilLoaded = false;
});
this._autoSelected = false;
this._selectedIndex = index;
this._updateSelectedMenuItem(index);
}
setAuto() {
if (this._mode === 'dash') this.selectAuto();
}
setQualityIndex(index) {
if (this._mode === 'dash') this.selectDashQuality(index);
else this.selectMp4Quality(index);
}
buildCSSClass() {
return `vjs-quality-multi-selector ${super.buildCSSClass()}`;
}
}
videojs.registerComponent('QualityMultiSelector', QualityMultiSelector);
// ------------------ SHARE BUTTON COMPONENT ------------------
class ShareButton extends videojs.getComponent('Button') {
constructor(player, options) {
super(player, options);
this.controlText('Share');
this.addClass('vjs-share-control');
const iconSpan = this.el().querySelector('.vjs-icon-placeholder');
if (iconSpan) {
iconSpan.classList.add('vjs-icon-share');
}
}
handleClick() {
this.openShareDialog();
}
openShareDialog() {
if (!this.player_.shareDialog || this.player_.shareDialog.isDisposed()) {
this.player_.shareDialog = new ShareDialog(this.player_, {});
this.player_.addChild(this.player_.shareDialog);
}
this.player_.shareDialog.open();
}
}
videojs.registerComponent('ShareButton', ShareButton);
// ------------------ SHARE DIALOG COMPONENT ------------------
class ShareDialog extends videojs.getComponent('ModalDialog') {
constructor(player, options) {
super(player, Object.assign({
temporary: false, // keep dialog in DOM for reopening
closeable: true,
label: 'Share Menu',
content: () => this.buildContent()
}, options));
this.addClass('vjs-share-dialog');
}
buildContent() {
const container = document.createElement('div');
container.className = 'share-dialog-content';
const header = document.createElement('div');
header.className = 'share-dialog-header';
header.textContent = 'Share';
container.appendChild(header);
const urlGroup = this.createInputGroup('Direct Link:', 'share-url-input');
const embedGroup = this.createInputGroup('Embed Code:', 'share-embed-input');
container.appendChild(urlGroup);
container.appendChild(embedGroup);
const socialsDiv = document.createElement('div');
socialsDiv.className = 'share-dialog-socials';
const socialItems = [
{ id: 'fbFeed', css: 'social-fb', label: 'f' },
{ id: 'tw', css: 'social-tw', label: '𝕏' },
{ id: 'reddit', css: 'social-reddit', label: 'R' },
{ id: 'email', css: 'social-email', label: '✉' }
];
socialItems.forEach(item => {
const link = document.createElement('a');
link.className = `social-btn ${item.css}`;
link.textContent = item.label;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.dataset.socialId = item.id;
socialsDiv.appendChild(link);
});
container.appendChild(socialsDiv);
return container;
}
createInputGroup(labelText, inputId) {
const group = document.createElement('div');
group.className = 'share-input-group';
const label = document.createElement('label');
label.textContent = labelText;
const wrapper = document.createElement('div');
wrapper.className = 'share-input-wrapper';
const input = document.createElement('input');
input.type = 'text';
input.readOnly = true;
input.id = inputId;
input.style.color = '#fff';
const copyBtn = document.createElement('button');
copyBtn.className = 'share-copy-btn';
copyBtn.innerHTML = `
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
</svg>
`;
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(input.value).then(() => {
copyBtn.style.color = '#4caf50';
setTimeout(() => copyBtn.style.color = '', 2000);
});
});
wrapper.appendChild(input);
wrapper.appendChild(copyBtn);
group.appendChild(label);
group.appendChild(wrapper);
return group;
}
updateValues() {
const url = shareOptions.url;
const embed = shareOptions.embedCode;
const urlInput = this.el().querySelector('#share-url-input');
const embedInput = this.el().querySelector('#share-embed-input');
if (urlInput) urlInput.value = url;
if (embedInput) embedInput.value = embed;
const socials = this.el().querySelectorAll('.social-btn');
socials.forEach(btn => {
const id = btn.dataset.socialId;
let href = '#';
if (id === 'fbFeed') href = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`;
else if (id === 'tw') href = `https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(shareOptions.title)}`;
else if (id === 'reddit') href = `https://www.reddit.com/submit?url=${encodeURIComponent(url)}&title=${encodeURIComponent(shareOptions.title)}`;
else if (id === 'email') href = `mailto:?subject=${encodeURIComponent(shareOptions.title)}&body=${encodeURIComponent(url)}`;
btn.href = href;
});
}
open() {
// Ensure values are updated after the modal is fully opened and content is in DOM
this.one('modalopen', () => this.updateValues());
super.open();
}
}
videojs.registerComponent('ShareDialog', ShareDialog);
var options = { var options = {
liveui: true, liveui: true,
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0], playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0],
@ -23,16 +521,17 @@ var options = {
'Spacer', 'Spacer',
'captionsButton', 'captionsButton',
'audioTrackButton', 'audioTrackButton',
'qualitySelector', 'QualityMultiSelector',
'QualityMenuButton',
'playbackRateMenuButton', 'playbackRateMenuButton',
'ShareButton',
'fullscreenToggle' 'fullscreenToggle'
] ]
}, },
html5: { html5: {
preloadTextTracks: false, preloadTextTracks: false,
vhs: { vhs: {
overrideNative: true overrideNative: true,
experimentalUseMMS: true
} }
} }
}; };
@ -224,10 +723,14 @@ function isMobile() {
if (isMobile()) { if (isMobile()) {
player.mobileUi({ touchControls: { seekSeconds: 5 * player.playbackRate() } }); player.mobileUi({ touchControls: { seekSeconds: 5 * player.playbackRate() } });
var buttons = ['playToggle', 'volumePanel', 'captionsButton']; var buttons = ['playToggle', 'volumePanel', 'captionsButton', 'ShareButton'];
if (!video_data.params.listen && video_data.params.quality === 'dash') buttons.push('audioTrackButton'); if (!video_data.params.listen && video_data.params.quality === 'dash') {
if (video_data.params.listen || video_data.params.quality !== 'dash') buttons.push('qualitySelector'); buttons.push('audioTrackButton');
buttons.push('QualityMultiSelector');
} else if (video_data.params.listen || video_data.params.quality !== 'dash') {
buttons.push('QualityMultiSelector');
}
// Create new control bar object for operation buttons // Create new control bar object for operation buttons
const ControlBar = videojs.getComponent('controlBar'); const ControlBar = videojs.getComponent('controlBar');
@ -248,18 +751,6 @@ if (isMobile()) {
// Playback menu doesn't work when it's initialized outside of the primary control bar // Playback menu doesn't work when it's initialized outside of the primary control bar
var playback_element = document.getElementsByClassName('vjs-playback-rate')[0]; var playback_element = document.getElementsByClassName('vjs-playback-rate')[0];
operations_bar_element.append(playback_element); operations_bar_element.append(playback_element);
// The share and http source selector element can't be fetched till the players ready.
player.one('playing', function () {
var share_element = document.getElementsByClassName('vjs-share-control')[0];
operations_bar_element.append(share_element);
if (!video_data.params.listen && video_data.params.quality === 'dash') {
var http_source_selector = document.getElementsByClassName('vjs-quality-menu-button vjs-menu-button')[0];
operations_bar_element.append(http_source_selector);
}
});
} }
// Enable VR video support // Enable VR video support
@ -421,34 +912,45 @@ if (video_data.params.autoplay) {
} }
if (!video_data.params.listen && video_data.params.quality === 'dash') { if (!video_data.params.listen && video_data.params.quality === 'dash') {
player.ready(function () {
player.on('loadedmetadata', function () {
const levels = Array.from(player.qualityLevels());
if (!levels.length) return;
if (video_data.params.quality_dash !== 'auto') { let targetLevel = null;
player.ready(function () {
player.on('loadedmetadata', function () { switch (video_data.params.quality_dash) {
const qualityLevels = Array.from(player.qualityLevels()).sort(function (a, b) {return a.height - b.height;}); case 'auto':
let targetQualityLevel; break;
switch (video_data.params.quality_dash) { case 'best':
case 'best': targetLevel = levels.reduce((a, b) => (a.height > b.height ? a : b), levels[0]);
targetQualityLevel = qualityLevels.length - 1; break;
break; case 'worst':
case 'worst': targetLevel = levels.reduce((a, b) => (a.height < b.height ? a : b), levels[0]);
targetQualityLevel = 0; break;
break; default: {
default: const targetHeight = parseInt(video_data.params.quality_dash);
const targetHeight = parseInt(video_data.params.quality_dash); targetLevel = levels
for (let i = 0; i < qualityLevels.length; i++) { .filter(level => level.height <= targetHeight)
if (qualityLevels[i].height <= targetHeight) .sort((a, b) => b.height - a.height)[0];
targetQualityLevel = i; if (!targetLevel) {
else targetLevel = levels.sort((a, b) => b.height - a.height)[0];
break; }
}
} }
player.qualityMenu({ }
defaultResolution: qualityLevels[targetQualityLevel].height
}); const selector = player._qualityMultiSelector;
}); if (selector) {
if (targetLevel) {
const qualityLevels = player.qualityLevels();
const targetIndex = levels.indexOf(targetLevel);
selector.setQualityIndex(targetIndex);
} else {
selector.setAuto();
}
}
}); });
} });
} }
player.vttThumbnails({ player.vttThumbnails({
@ -796,9 +1298,6 @@ addEventListener('keydown', function (e) {
player.on('DOMMouseScroll', mouseScroll); player.on('DOMMouseScroll', mouseScroll);
}()); }());
// Since videojs-share can sometimes be blocked, we defer it until last
if (player.shareMenu) player.shareMenu();
// show the preferred caption by default // show the preferred caption by default
if (player_data.preferred_caption_found) { if (player_data.preferred_caption_found) {
player.ready(function () { player.ready(function () {
@ -813,12 +1312,24 @@ if (player_data.preferred_caption_found) {
// Safari audio double duration fix // Safari audio double duration fix
if (navigator.vendor === 'Apple Computer, Inc.' && video_data.params.listen) { if (navigator.vendor === 'Apple Computer, Inc.' && video_data.params.listen) {
function onTimeUpdate() {
if (
player.remainingTime() < player.duration() / 2 &&
player.remainingTime() >= 2
) {
player.currentTime(player.duration() - 1);
}
}
player.on('loadedmetadata', function () { player.on('loadedmetadata', function () {
player.on('timeupdate', function () { var type = (player.currentType() || '').toLowerCase();
if (player.remainingTime() < player.duration() / 2 && player.remainingTime() >= 2) {
player.currentTime(player.duration() - 1); // Always remove any previous listener to avoid duplicates
} player.off('timeupdate', onTimeUpdate);
});
// Only attach if the current source contains mp4a
if (type.indexOf('mp4a') !== -1) {
player.on('timeupdate', onTimeUpdate);
}
}); });
} }
@ -857,3 +1368,27 @@ addEventListener('DOMContentLoaded', function () {
changeInstanceLink.href = addCurrentTimeToURL(changeInstanceLink.href); changeInstanceLink.href = addCurrentTimeToURL(changeInstanceLink.href);
}); });
}); });
// Restore old playback rate button behavior (click to cycle rates)
player.ready(() => {
const playbackRateButton = player.controlBar.getChild('playbackRateMenuButton');
if (playbackRateButton) {
// Remove default click handler by overriding onClick
playbackRateButton.handleClick = function() {
const rates = this.playbackRates();
const currentRate = this.player().playbackRate();
// Find next rate, wrap to first if at end
let newRate = rates[0];
for (let i = 0; i < rates.length; i++) {
if (rates[i] > currentRate) {
newRate = rates[i];
break;
}
}
this.player().playbackRate(newRate);
};
}
});