mirror of
https://github.com/iv-org/invidious.git
synced 2026-10-25 14:29:16 -05:00
add quality selector, share menu, ios playback fixes
This commit is contained in:
parent
9141550a5a
commit
0164a87b8e
@ -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; }
|
||||||
|
|||||||
@ -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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user