`
- )
- .join("")}
+ )
+ .join('')}
`;
- this.settingsOverlay.appendChild(this.speedSubmenu);
- }
+ this.settingsOverlay.appendChild(this.speedSubmenu);
+ }
- createQualitySubmenu(qualities, currentValue) {
- this.qualitySubmenu = document.createElement("div");
- this.qualitySubmenu.className = "quality-submenu";
+ createQualitySubmenu(qualities, currentValue) {
+ this.qualitySubmenu = document.createElement('div');
+ this.qualitySubmenu.className = 'quality-submenu';
- const header = `
+ const header = `
`;
- const optionsHtml = qualities
- .map(
- (q) => `
-
+ const optionsHtml = qualities
+ .map(
+ (q) => `
+
${q.displayLabel || q.label}
- ${q.value === currentValue ? '✓' : ""}
+ ${q.value === currentValue ? '✓' : ''}
`
- )
- .join("");
+ )
+ .join('');
- this.qualitySubmenu.innerHTML = header + optionsHtml;
- this.settingsOverlay.appendChild(this.qualitySubmenu);
- }
+ this.qualitySubmenu.innerHTML = header + optionsHtml;
+ this.settingsOverlay.appendChild(this.qualitySubmenu);
+ }
- createSubtitlesSubmenu() {
- this.subtitlesSubmenu = document.createElement("div");
- this.subtitlesSubmenu.className = "subtitles-submenu";
+ createSubtitlesSubmenu() {
+ this.subtitlesSubmenu = document.createElement('div');
+ this.subtitlesSubmenu.className = 'subtitles-submenu';
- // Header
- const header = `
+ // Header
+ const header = `
`;
- this.subtitlesSubmenu.innerHTML = header + '
';
- this.settingsOverlay.appendChild(this.subtitlesSubmenu);
+ this.subtitlesSubmenu.innerHTML = header + '
';
+ this.settingsOverlay.appendChild(this.subtitlesSubmenu);
- // Populate now and on demand
- this.refreshSubtitlesSubmenu();
- }
-
- refreshSubtitlesSubmenu() {
- if (!this.subtitlesSubmenu) return;
- const body = this.subtitlesSubmenu.querySelector('.submenu-body');
- if (!body) return;
- const player = this.player();
- const tracks = player.textTracks();
-
- // Determine active
- let activeLang = null;
- for (let i = 0; i < tracks.length; i++) {
- const t = tracks[i];
- if (t.kind === 'subtitles' && t.mode === 'showing') {
- activeLang = t.language;
- break;
- }
+ // Populate now and on demand
+ this.refreshSubtitlesSubmenu();
}
- // Build items: Off + languages
- const items = [];
- items.push({ label: 'Off', lang: null });
- for (let i = 0; i < tracks.length; i++) {
- const t = tracks[i];
- if (t.kind === 'subtitles') {
- items.push({ label: t.label || t.language || `Track ${i}`, lang: t.language, track: t });
- }
- }
+ refreshSubtitlesSubmenu() {
+ if (!this.subtitlesSubmenu) return;
+ const body = this.subtitlesSubmenu.querySelector('.submenu-body');
+ if (!body) return;
+ const player = this.player();
+ const tracks = player.textTracks();
- body.innerHTML = items.map((it) => `
+ // Determine active
+ let activeLang = null;
+ for (let i = 0; i < tracks.length; i++) {
+ const t = tracks[i];
+ if (t.kind === 'subtitles' && t.mode === 'showing') {
+ activeLang = t.language;
+ break;
+ }
+ }
+
+ // Build items: Off + languages
+ const items = [];
+ items.push({ label: 'Off', lang: null });
+ for (let i = 0; i < tracks.length; i++) {
+ const t = tracks[i];
+ if (t.kind === 'subtitles') {
+ items.push({ label: t.label || t.language || `Track ${i}`, lang: t.language, track: t });
+ }
+ }
+
+ body.innerHTML = items
+ .map(
+ (it) => `
${it.label}
${it.lang === activeLang ? '✓' : ''}
- `).join('');
+ `
+ )
+ .join('');
- // Also update the current subtitle display in main settings
- this.updateCurrentSubtitleDisplay();
- }
+ // Also update the current subtitle display in main settings
+ this.updateCurrentSubtitleDisplay();
+ }
- updateCurrentSubtitleDisplay() {
- try {
- const player = this.player();
- const tracks = player.textTracks();
- let currentSubtitleLabel = "Off";
- let activeTrack = null;
-
- // Find the active subtitle track
- for (let i = 0; i < tracks.length; i++) {
- const t = tracks[i];
- if (t.kind === 'subtitles' && t.mode === 'showing') {
- currentSubtitleLabel = t.label || t.language || "Subtitles";
- activeTrack = t;
- break;
+ updateCurrentSubtitleDisplay() {
+ try {
+ const player = this.player();
+ const tracks = player.textTracks();
+ let currentSubtitleLabel = 'Off';
+ let activeTrack = null;
+
+ // Find the active subtitle track
+ for (let i = 0; i < tracks.length; i++) {
+ const t = tracks[i];
+ if (t.kind === 'subtitles' && t.mode === 'showing') {
+ currentSubtitleLabel = t.label || t.language || 'Subtitles';
+ activeTrack = t;
+ break;
+ }
+ }
+
+ const currentSubtitlesDisplay = this.settingsOverlay.querySelector('.current-subtitles');
+ if (currentSubtitlesDisplay) {
+ const oldValue = currentSubtitlesDisplay.textContent;
+ currentSubtitlesDisplay.textContent = currentSubtitleLabel;
+
+ // Only log if the value actually changed
+ if (oldValue !== currentSubtitleLabel) {
+ console.log(`Updated current subtitle display: "${oldValue}" → "${currentSubtitleLabel}"`);
+ if (activeTrack) {
+ console.log(
+ `Active track details: language="${activeTrack.language}", label="${activeTrack.label}", mode="${activeTrack.mode}"`
+ );
+ }
+ }
+ }
+ } catch (error) {
+ console.error('Error updating current subtitle display:', error);
}
- }
+ }
- const currentSubtitlesDisplay = this.settingsOverlay.querySelector('.current-subtitles');
- if (currentSubtitlesDisplay) {
- const oldValue = currentSubtitlesDisplay.textContent;
- currentSubtitlesDisplay.textContent = currentSubtitleLabel;
-
- // Only log if the value actually changed
- if (oldValue !== currentSubtitleLabel) {
- console.log(`Updated current subtitle display: "${oldValue}" → "${currentSubtitleLabel}"`);
- if (activeTrack) {
- console.log(`Active track details: language="${activeTrack.language}", label="${activeTrack.label}", mode="${activeTrack.mode}"`);
- }
+ // Method to periodically check and update subtitle display
+ startSubtitleSync() {
+ // Update immediately
+ this.updateCurrentSubtitleDisplay();
+
+ // Listen for real-time subtitle changes
+ this.player().on('texttrackchange', () => {
+ console.log('Text track changed - updating subtitle display');
+ this.updateCurrentSubtitleDisplay();
+ // Also refresh the subtitle submenu to show correct selection
+ this.refreshSubtitlesSubmenu();
+ });
+
+ // Set up periodic updates every 2 seconds as backup
+ this.subtitleSyncInterval = setInterval(() => {
+ this.updateCurrentSubtitleDisplay();
+ }, 2000);
+ }
+
+ // Method to stop subtitle sync
+ stopSubtitleSync() {
+ if (this.subtitleSyncInterval) {
+ clearInterval(this.subtitleSyncInterval);
+ this.subtitleSyncInterval = null;
}
- } else {
- console.warn('Could not find .current-subtitles element in settings overlay');
- }
- } catch (error) {
- console.error('Error updating current subtitle display:', error);
- }
- }
-
- // Method to periodically check and update subtitle display
- startSubtitleSync() {
- // Update immediately
- this.updateCurrentSubtitleDisplay();
-
- // Listen for real-time subtitle changes
- this.player().on('texttrackchange', () => {
- console.log('Text track changed - updating subtitle display');
- this.updateCurrentSubtitleDisplay();
- // Also refresh the subtitle submenu to show correct selection
- this.refreshSubtitlesSubmenu();
- });
-
- // Set up periodic updates every 2 seconds as backup
- this.subtitleSyncInterval = setInterval(() => {
- this.updateCurrentSubtitleDisplay();
- }, 2000);
- }
-
- // Method to stop subtitle sync
- stopSubtitleSync() {
- if (this.subtitleSyncInterval) {
- clearInterval(this.subtitleSyncInterval);
- this.subtitleSyncInterval = null;
- }
- }
-
- // Cleanup method
- dispose() {
- this.stopSubtitleSync();
- // Remove event listeners
- document.removeEventListener("click", this.handleClickOutside);
- // Remove text track change listener
- if (this.player()) {
- this.player().off('texttrackchange');
- }
- }
-
- getAvailableQualities() {
- // Priority: provided options -> MEDIA_DATA JSON -> player sources -> default
- const desiredOrder = [
- "auto",
- "144p",
- "240p",
- "360p",
- "480p",
- "720p",
- "1080p",
- ];
-
- if (
- Array.isArray(this.providedQualities) &&
- this.providedQualities.length
- ) {
- return this.sortAndDecorateQualities(
- this.providedQualities,
- desiredOrder
- );
}
- try {
- const md = typeof window !== "undefined" ? window.MEDIA_DATA : null;
- const jsonQualities = md?.data?.qualities;
- if (Array.isArray(jsonQualities) && jsonQualities.length) {
- // Expected format: [{label: '1080p', value: '1080p', src: '...'}]
- const normalized = jsonQualities.map((q) => ({
- label: q.label || q.value || "Auto",
- value: (q.value || q.label || "auto").toString().toLowerCase(),
- src: q.src || q.url || q.href,
- type: q.type || "video/mp4",
- }));
- return this.sortAndDecorateQualities(normalized, desiredOrder);
- }
- } catch (e) {
- // ignore
+ // Cleanup method
+ dispose() {
+ this.stopSubtitleSync();
+ // Remove event listeners
+ document.removeEventListener('click', this.handleClickOutside);
+ // Remove text track change listener
+ if (this.player()) {
+ this.player().off('texttrackchange');
+ }
}
- // Derive from player's current sources
- const sources = this.player().currentSources
- ? this.player().currentSources()
- : this.player().currentSrc();
- if (Array.isArray(sources) && sources.length > 0) {
- const mapped = sources.map((s, idx) => {
- const label =
- s.label ||
- s.res ||
- this.inferLabelFromSrc(s.src) ||
- (idx === 0 ? "Auto" : `Source ${idx + 1}`);
- const value = String(label).toLowerCase();
- return { label, value, src: s.src, type: s.type || "video/mp4" };
- });
- return this.sortAndDecorateQualities(mapped, desiredOrder);
+ getAvailableQualities() {
+ // Priority: provided options -> MEDIA_DATA JSON -> player sources -> default
+ const desiredOrder = ['auto', '144p', '240p', '360p', '480p', '720p', '1080p'];
+
+ if (Array.isArray(this.providedQualities) && this.providedQualities.length) {
+ return this.sortAndDecorateQualities(this.providedQualities, desiredOrder);
+ }
+
+ try {
+ const md = typeof window !== 'undefined' ? window.MEDIA_DATA : null;
+ const jsonQualities = md?.data?.qualities;
+ if (Array.isArray(jsonQualities) && jsonQualities.length) {
+ // Expected format: [{label: '1080p', value: '1080p', src: '...'}]
+ const normalized = jsonQualities.map((q) => ({
+ label: q.label || q.value || 'Auto',
+ value: (q.value || q.label || 'auto').toString().toLowerCase(),
+ src: q.src || q.url || q.href,
+ type: q.type || 'video/mp4',
+ }));
+ return this.sortAndDecorateQualities(normalized, desiredOrder);
+ }
+ } catch (e) {
+ // ignore
+ }
+
+ // Derive from player's current sources
+ const sources = this.player().currentSources ? this.player().currentSources() : this.player().currentSrc();
+ if (Array.isArray(sources) && sources.length > 0) {
+ const mapped = sources.map((s, idx) => {
+ const label =
+ s.label || s.res || this.inferLabelFromSrc(s.src) || (idx === 0 ? 'Auto' : `Source ${idx + 1}`);
+ const value = String(label).toLowerCase();
+ return { label, value, src: s.src, type: s.type || 'video/mp4' };
+ });
+ return this.sortAndDecorateQualities(mapped, desiredOrder);
+ }
+
+ // Default fallback - return empty array if no valid sources found
+ return [];
}
- // Default fallback - return empty array if no valid sources found
- return [];
- }
+ sortAndDecorateQualities(list, desiredOrder) {
+ const orderIndex = (val) => {
+ const i = desiredOrder.indexOf(String(val).toLowerCase());
+ return i === -1 ? 999 : i;
+ };
- sortAndDecorateQualities(list, desiredOrder) {
- const orderIndex = (val) => {
- const i = desiredOrder.indexOf(String(val).toLowerCase());
- return i === -1 ? 999 : i;
- };
-
- // Only include qualities that have actual sources
- const validQualities = list.filter(q => q.src);
-
- const decorated = validQualities
- .map((q) => {
- const val = (q.value || q.label || "").toString().toLowerCase();
- const baseLabel = q.label || q.value || "";
- const is1080 = val === "1080p";
- const displayLabel = is1080
- ? `${baseLabel}
HD`
- : baseLabel;
- return { ...q, value: val, label: baseLabel, displayLabel };
- })
- .sort((a, b) => orderIndex(a.value) - orderIndex(b.value));
+ // Only include qualities that have actual sources
+ const validQualities = list.filter((q) => q.src);
- return decorated;
- }
+ const decorated = validQualities
+ .map((q) => {
+ const val = (q.value || q.label || '').toString().toLowerCase();
+ const baseLabel = q.label || q.value || '';
+ const is1080 = val === '1080p';
+ const displayLabel = is1080 ? `${baseLabel}
HD` : baseLabel;
+ return { ...q, value: val, label: baseLabel, displayLabel };
+ })
+ .sort((a, b) => orderIndex(a.value) - orderIndex(b.value));
- inferLabelFromSrc(src) {
- if (!src) return null;
- // Try to detect typical resolution markers in file name or query string
- const match = /(?:_|\.|\/)\D*(1440p|1080p|720p|480p|360p|240p|144p)/i.exec(
- src
- );
- if (match && match[1]) return match[1].toUpperCase();
- const m2 = /(\b\d{3,4})p\b/i.exec(src);
- if (m2 && m2[1]) return `${m2[1]}p`;
- return null;
- }
-
- positionButton() {
- const controlBar = this.player().getChild("controlBar");
- const fullscreenToggle = controlBar.getChild("fullscreenToggle");
-
- if (this.settingsButton && fullscreenToggle) {
- // Small delay to ensure all buttons are created
- setTimeout(() => {
- const fullscreenIndex = controlBar.children().indexOf(fullscreenToggle);
- controlBar.removeChild(this.settingsButton);
- controlBar.addChild(this.settingsButton, {}, fullscreenIndex + 1);
- console.log("✓ Settings button positioned after fullscreen toggle");
- }, 50);
+ return decorated;
}
- }
- setupEventListeners() {
- // Close button functionality
- const closeButton = this.settingsOverlay.querySelector('.settings-close-btn');
- if (closeButton) {
- const closeFunction = (e) => {
+ inferLabelFromSrc(src) {
+ if (!src) return null;
+ // Try to detect typical resolution markers in file name or query string
+ const match = /(?:_|\.|\/)\D*(1440p|1080p|720p|480p|360p|240p|144p)/i.exec(src);
+ if (match && match[1]) return match[1].toUpperCase();
+ const m2 = /(\b\d{3,4})p\b/i.exec(src);
+ if (m2 && m2[1]) return `${m2[1]}p`;
+ return null;
+ }
+
+ positionButton() {
+ const controlBar = this.player().getChild('controlBar');
+ const fullscreenToggle = controlBar.getChild('fullscreenToggle');
+
+ if (this.settingsButton && fullscreenToggle) {
+ // Small delay to ensure all buttons are created
+ setTimeout(() => {
+ const fullscreenIndex = controlBar.children().indexOf(fullscreenToggle);
+ controlBar.removeChild(this.settingsButton);
+ controlBar.addChild(this.settingsButton, {}, fullscreenIndex + 1);
+ console.log('✓ Settings button positioned after fullscreen toggle');
+ }, 50);
+ }
+ }
+
+ setupEventListeners() {
+ // Close button functionality
+ const closeButton = this.settingsOverlay.querySelector('.settings-close-btn');
+ if (closeButton) {
+ const closeFunction = (e) => {
+ e.stopPropagation();
+ this.settingsOverlay.classList.remove('show');
+ this.settingsOverlay.style.display = 'none';
+ this.speedSubmenu.style.display = 'none';
+ if (this.qualitySubmenu) this.qualitySubmenu.style.display = 'none';
+ if (this.subtitlesSubmenu) this.subtitlesSubmenu.style.display = 'none';
+ const btnEl = this.settingsButton?.el();
+ if (btnEl) {
+ btnEl.classList.remove('settings-clicked');
+ }
+ };
+
+ closeButton.addEventListener('click', closeFunction);
+ closeButton.addEventListener(
+ 'touchend',
+ (e) => {
+ e.preventDefault();
+ closeFunction(e);
+ },
+ { passive: false }
+ );
+ }
+
+ // Settings item clicks
+ this.settingsOverlay.addEventListener('click', (e) => {
+ e.stopPropagation();
+
+ if (e.target.closest('[data-setting="playback-speed"]')) {
+ this.speedSubmenu.style.display = 'flex';
+ this.qualitySubmenu.style.display = 'none';
+ }
+
+ if (e.target.closest('[data-setting="quality"]')) {
+ this.qualitySubmenu.style.display = 'flex';
+ this.speedSubmenu.style.display = 'none';
+ }
+
+ if (e.target.closest('[data-setting="subtitles"]')) {
+ this.refreshSubtitlesSubmenu();
+ this.subtitlesSubmenu.style.display = 'flex';
+ this.speedSubmenu.style.display = 'none';
+ this.qualitySubmenu.style.display = 'none';
+ }
+ });
+
+ // Touch scroll detection for settingsOverlay
+ this.settingsOverlay.addEventListener(
+ 'touchstart',
+ (e) => {
+ this.touchStartY = e.touches[0].clientY;
+ this.isTouchScrolling = false;
+ },
+ { passive: true }
+ );
+ this.settingsOverlay.addEventListener(
+ 'touchmove',
+ (e) => {
+ const dy = Math.abs(e.touches[0].clientY - this.touchStartY);
+ if (dy > 10) this.isTouchScrolling = true;
+ },
+ { passive: true }
+ );
+ // Mobile touch events for settings items (tap vs scroll)
+ this.settingsOverlay.addEventListener(
+ 'touchend',
+ (e) => {
+ e.stopPropagation();
+ if (this.isTouchScrolling) {
+ this.isTouchScrolling = false;
+ return;
+ }
+
+ if (e.target.closest('[data-setting="playback-speed"]')) {
+ e.preventDefault();
+ this.speedSubmenu.style.display = 'flex';
+ this.qualitySubmenu.style.display = 'none';
+ }
+
+ if (e.target.closest('[data-setting="quality"]')) {
+ e.preventDefault();
+ this.qualitySubmenu.style.display = 'flex';
+ this.speedSubmenu.style.display = 'none';
+ }
+
+ if (e.target.closest('[data-setting="subtitles"]')) {
+ e.preventDefault();
+ this.refreshSubtitlesSubmenu();
+ this.subtitlesSubmenu.style.display = 'flex';
+ this.speedSubmenu.style.display = 'none';
+ this.qualitySubmenu.style.display = 'none';
+ }
+ },
+ { passive: false }
+ );
+
+ // Speed submenu header (back button)
+ const speedHeader = this.speedSubmenu.querySelector('.submenu-header');
+ speedHeader.addEventListener('click', () => {
+ this.speedSubmenu.style.display = 'none';
+ });
+ speedHeader.addEventListener(
+ 'touchend',
+ (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.speedSubmenu.style.display = 'none';
+ },
+ { passive: false }
+ );
+
+ // Quality submenu header (back button)
+ const qualityHeader = this.qualitySubmenu.querySelector('.submenu-header');
+ qualityHeader.addEventListener('click', () => {
+ this.qualitySubmenu.style.display = 'none';
+ });
+ qualityHeader.addEventListener(
+ 'touchend',
+ (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.qualitySubmenu.style.display = 'none';
+ },
+ { passive: false }
+ );
+
+ // Subtitles submenu header (back)
+ const subtitlesHeader = this.subtitlesSubmenu.querySelector('.submenu-header');
+ subtitlesHeader.addEventListener('click', () => {
+ this.subtitlesSubmenu.style.display = 'none';
+ });
+ subtitlesHeader.addEventListener(
+ 'touchend',
+ (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.subtitlesSubmenu.style.display = 'none';
+ },
+ { passive: false }
+ );
+
+ // Speed option clicks
+ this.speedSubmenu.addEventListener('click', (e) => {
+ const speedOption = e.target.closest('.speed-option');
+ if (speedOption) {
+ const speed = parseFloat(speedOption.dataset.speed);
+ this.handleSpeedChange(speed, speedOption);
+ }
+ });
+
+ // Touch scroll detection for speed submenu
+ this.speedSubmenu.addEventListener(
+ 'touchstart',
+ (e) => {
+ this.touchStartY = e.touches[0].clientY;
+ this.isTouchScrolling = false;
+ },
+ { passive: true }
+ );
+ this.speedSubmenu.addEventListener(
+ 'touchmove',
+ (e) => {
+ const dy = Math.abs(e.touches[0].clientY - this.touchStartY);
+ if (dy > 10) this.isTouchScrolling = true;
+ },
+ { passive: true }
+ );
+ // Mobile touch events for speed options (tap vs scroll)
+ this.speedSubmenu.addEventListener(
+ 'touchend',
+ (e) => {
+ e.stopPropagation();
+ if (this.isTouchScrolling) {
+ this.isTouchScrolling = false;
+ return;
+ }
+ const speedOption = e.target.closest('.speed-option');
+ if (speedOption) {
+ e.preventDefault();
+ const speed = parseFloat(speedOption.dataset.speed);
+ this.handleSpeedChange(speed, speedOption);
+ }
+ },
+ { passive: false }
+ );
+
+ // Quality option clicks
+ this.qualitySubmenu.addEventListener('click', (e) => {
+ const qualityOption = e.target.closest('.quality-option');
+ if (qualityOption) {
+ const value = qualityOption.dataset.quality;
+ this.handleQualityChange(value, qualityOption);
+ }
+ });
+
+ this.qualitySubmenu.addEventListener(
+ 'touchstart',
+ (e) => {
+ this.touchStartY = e.touches[0].clientY;
+ this.isTouchScrolling = false;
+ },
+ { passive: true }
+ );
+ this.qualitySubmenu.addEventListener(
+ 'touchmove',
+ (e) => {
+ const dy = Math.abs(e.touches[0].clientY - this.touchStartY);
+ if (dy > 10) this.isTouchScrolling = true;
+ },
+ { passive: true }
+ );
+ // Mobile touch events for quality options (tap vs scroll)
+ this.qualitySubmenu.addEventListener(
+ 'touchend',
+ (e) => {
+ e.stopPropagation();
+ if (this.isTouchScrolling) {
+ this.isTouchScrolling = false;
+ return;
+ }
+ const qualityOption = e.target.closest('.quality-option');
+ if (qualityOption) {
+ e.preventDefault();
+ const value = qualityOption.dataset.quality;
+ this.handleQualityChange(value, qualityOption);
+ }
+ },
+ { passive: false }
+ );
+
+ // Subtitle option clicks
+ this.subtitlesSubmenu.addEventListener('click', (e) => {
+ const opt = e.target.closest('.subtitle-option');
+ if (opt) {
+ const lang = opt.dataset.lang || null;
+ this.handleSubtitleChange(lang, opt);
+ }
+ });
+
+ // Touch scroll detection for subtitles submenu
+ this.subtitlesSubmenu.addEventListener(
+ 'touchstart',
+ (e) => {
+ this.touchStartY = e.touches[0].clientY;
+ this.isTouchScrolling = false;
+ },
+ { passive: true }
+ );
+ this.subtitlesSubmenu.addEventListener(
+ 'touchmove',
+ (e) => {
+ const dy = Math.abs(e.touches[0].clientY - this.touchStartY);
+ if (dy > 10) this.isTouchScrolling = true;
+ },
+ { passive: true }
+ );
+ // Mobile touch events for subtitle options (tap vs scroll)
+ this.subtitlesSubmenu.addEventListener(
+ 'touchend',
+ (e) => {
+ e.stopPropagation();
+ if (this.isTouchScrolling) {
+ this.isTouchScrolling = false;
+ return;
+ }
+ const opt = e.target.closest('.subtitle-option');
+ if (opt) {
+ e.preventDefault();
+ const lang = opt.dataset.lang || null;
+ this.handleSubtitleChange(lang, opt);
+ }
+ },
+ { passive: false }
+ );
+
+ // Close menu when clicking outside
+ document.addEventListener('click', this.handleClickOutside);
+
+ // Add hover effects
+ this.settingsOverlay.addEventListener('mouseover', (e) => {
+ const item = e.target.closest('.settings-item, .speed-option');
+ if (item && !item.style.background.includes('0.1')) {
+ item.style.background = 'rgba(255, 255, 255, 0.05)';
+ }
+ });
+
+ this.settingsOverlay.addEventListener('mouseout', (e) => {
+ const item = e.target.closest('.settings-item, .speed-option');
+ if (item && !item.style.background.includes('0.1')) {
+ item.style.background = 'transparent';
+ }
+ });
+
+ // Start subtitle synchronization
+ this.startSubtitleSync();
+ }
+
+ toggleSettings(e) {
e.stopPropagation();
- this.settingsOverlay.classList.remove("show");
- this.settingsOverlay.style.display = "none";
- this.speedSubmenu.style.display = "none";
- if (this.qualitySubmenu) this.qualitySubmenu.style.display = "none";
- if (this.subtitlesSubmenu) this.subtitlesSubmenu.style.display = "none";
+ const isVisible = this.settingsOverlay.classList.contains('show');
+
+ if (isVisible) {
+ this.settingsOverlay.classList.remove('show');
+ this.settingsOverlay.style.display = 'none';
+ } else {
+ this.settingsOverlay.classList.add('show');
+ this.settingsOverlay.style.display = 'block';
+ }
+
+ this.speedSubmenu.style.display = 'none'; // Hide submenu when main menu toggles
+ if (this.qualitySubmenu) this.qualitySubmenu.style.display = 'none';
const btnEl = this.settingsButton?.el();
if (btnEl) {
- btnEl.classList.remove("settings-clicked");
+ if (!isVisible) {
+ btnEl.classList.add('settings-clicked');
+ } else {
+ btnEl.classList.remove('settings-clicked');
+ }
}
- };
-
- closeButton.addEventListener('click', closeFunction);
- closeButton.addEventListener('touchend', (e) => {
- e.preventDefault();
- closeFunction(e);
- }, { passive: false });
}
- // Settings item clicks
- this.settingsOverlay.addEventListener("click", (e) => {
- e.stopPropagation();
+ handleSpeedChange(speed, speedOption) {
+ // Update player speed
+ this.player().playbackRate(speed);
- if (e.target.closest('[data-setting="playback-speed"]')) {
- this.speedSubmenu.style.display = "flex";
- this.qualitySubmenu.style.display = "none";
- }
+ // Save preference
+ this.userPreferences.setPreference('playbackRate', speed);
- if (e.target.closest('[data-setting="quality"]')) {
- this.qualitySubmenu.style.display = "flex";
- this.speedSubmenu.style.display = "none";
- }
+ // Update UI
+ this.speedSubmenu.querySelectorAll('.speed-option').forEach((opt) => {
+ opt.classList.remove('active');
+ opt.style.background = 'transparent';
+ const check = opt.querySelector('.checkmark');
+ if (check) check.remove();
+ });
- if (e.target.closest('[data-setting="subtitles"]')) {
- this.refreshSubtitlesSubmenu();
- this.subtitlesSubmenu.style.display = "flex";
- this.speedSubmenu.style.display = "none";
- this.qualitySubmenu.style.display = "none";
- }
- });
+ speedOption.classList.add('active');
+ speedOption.style.background = 'rgba(255, 255, 255, 0.1)';
+ speedOption.insertAdjacentHTML('beforeend', '
✓');
- // Touch scroll detection for settingsOverlay
- this.settingsOverlay.addEventListener('touchstart', (e) => {
- this.touchStartY = e.touches[0].clientY;
- this.isTouchScrolling = false;
- }, { passive: true });
- this.settingsOverlay.addEventListener('touchmove', (e) => {
- const dy = Math.abs(e.touches[0].clientY - this.touchStartY);
- if (dy > 10) this.isTouchScrolling = true;
- }, { passive: true });
- // Mobile touch events for settings items (tap vs scroll)
- this.settingsOverlay.addEventListener("touchend", (e) => {
- e.stopPropagation();
- if (this.isTouchScrolling) { this.isTouchScrolling = false; return; }
+ // Update main menu display
+ const currentSpeedDisplay = this.settingsOverlay.querySelector('.current-speed');
+ const speedLabel = speed === 1 ? 'Normal' : `${speed}`;
+ currentSpeedDisplay.textContent = speedLabel;
- if (e.target.closest('[data-setting="playback-speed"]')) {
- e.preventDefault();
- this.speedSubmenu.style.display = "flex";
- this.qualitySubmenu.style.display = "none";
- }
+ // Close only the speed submenu (keep overlay open)
+ this.speedSubmenu.style.display = 'none';
- if (e.target.closest('[data-setting="quality"]')) {
- e.preventDefault();
- this.qualitySubmenu.style.display = "flex";
- this.speedSubmenu.style.display = "none";
- }
-
- if (e.target.closest('[data-setting="subtitles"]')) {
- e.preventDefault();
- this.refreshSubtitlesSubmenu();
- this.subtitlesSubmenu.style.display = "flex";
- this.speedSubmenu.style.display = "none";
- this.qualitySubmenu.style.display = "none";
- }
- }, { passive: false });
-
- // Speed submenu header (back button)
- const speedHeader = this.speedSubmenu.querySelector(".submenu-header");
- speedHeader.addEventListener("click", () => {
- this.speedSubmenu.style.display = "none";
- });
- speedHeader.addEventListener("touchend", (e) => {
- e.preventDefault();
- e.stopPropagation();
- this.speedSubmenu.style.display = "none";
- }, { passive: false });
-
- // Quality submenu header (back button)
- const qualityHeader = this.qualitySubmenu.querySelector(".submenu-header");
- qualityHeader.addEventListener("click", () => {
- this.qualitySubmenu.style.display = "none";
- });
- qualityHeader.addEventListener("touchend", (e) => {
- e.preventDefault();
- e.stopPropagation();
- this.qualitySubmenu.style.display = "none";
- }, { passive: false });
-
- // Subtitles submenu header (back)
- const subtitlesHeader = this.subtitlesSubmenu.querySelector(".submenu-header");
- subtitlesHeader.addEventListener("click", () => {
- this.subtitlesSubmenu.style.display = "none";
- });
- subtitlesHeader.addEventListener("touchend", (e) => {
- e.preventDefault();
- e.stopPropagation();
- this.subtitlesSubmenu.style.display = "none";
- }, { passive: false });
-
- // Speed option clicks
- this.speedSubmenu.addEventListener("click", (e) => {
- const speedOption = e.target.closest(".speed-option");
- if (speedOption) {
- const speed = parseFloat(speedOption.dataset.speed);
- this.handleSpeedChange(speed, speedOption);
- }
- });
-
- // Touch scroll detection for speed submenu
- this.speedSubmenu.addEventListener('touchstart', (e) => {
- this.touchStartY = e.touches[0].clientY;
- this.isTouchScrolling = false;
- }, { passive: true });
- this.speedSubmenu.addEventListener('touchmove', (e) => {
- const dy = Math.abs(e.touches[0].clientY - this.touchStartY);
- if (dy > 10) this.isTouchScrolling = true;
- }, { passive: true });
- // Mobile touch events for speed options (tap vs scroll)
- this.speedSubmenu.addEventListener("touchend", (e) => {
- e.stopPropagation();
- if (this.isTouchScrolling) { this.isTouchScrolling = false; return; }
- const speedOption = e.target.closest(".speed-option");
- if (speedOption) {
- e.preventDefault();
- const speed = parseFloat(speedOption.dataset.speed);
- this.handleSpeedChange(speed, speedOption);
- }
- }, { passive: false });
-
- // Quality option clicks
- this.qualitySubmenu.addEventListener("click", (e) => {
- const qualityOption = e.target.closest(".quality-option");
- if (qualityOption) {
- const value = qualityOption.dataset.quality;
- this.handleQualityChange(value, qualityOption);
- }
- });
-
- this.qualitySubmenu.addEventListener('touchstart', (e) => {
- this.touchStartY = e.touches[0].clientY;
- this.isTouchScrolling = false;
- }, { passive: true });
- this.qualitySubmenu.addEventListener('touchmove', (e) => {
- const dy = Math.abs(e.touches[0].clientY - this.touchStartY);
- if (dy > 10) this.isTouchScrolling = true;
- }, { passive: true });
- // Mobile touch events for quality options (tap vs scroll)
- this.qualitySubmenu.addEventListener("touchend", (e) => {
- e.stopPropagation();
- if (this.isTouchScrolling) { this.isTouchScrolling = false; return; }
- const qualityOption = e.target.closest(".quality-option");
- if (qualityOption) {
- e.preventDefault();
- const value = qualityOption.dataset.quality;
- this.handleQualityChange(value, qualityOption);
- }
- }, { passive: false });
-
- // Subtitle option clicks
- this.subtitlesSubmenu.addEventListener('click', (e) => {
- const opt = e.target.closest('.subtitle-option');
- if (opt) {
- const lang = opt.dataset.lang || null;
- this.handleSubtitleChange(lang, opt);
- }
- });
-
- // Touch scroll detection for subtitles submenu
- this.subtitlesSubmenu.addEventListener('touchstart', (e) => {
- this.touchStartY = e.touches[0].clientY;
- this.isTouchScrolling = false;
- }, { passive: true });
- this.subtitlesSubmenu.addEventListener('touchmove', (e) => {
- const dy = Math.abs(e.touches[0].clientY - this.touchStartY);
- if (dy > 10) this.isTouchScrolling = true;
- }, { passive: true });
- // Mobile touch events for subtitle options (tap vs scroll)
- this.subtitlesSubmenu.addEventListener('touchend', (e) => {
- e.stopPropagation();
- if (this.isTouchScrolling) { this.isTouchScrolling = false; return; }
- const opt = e.target.closest('.subtitle-option');
- if (opt) {
- e.preventDefault();
- const lang = opt.dataset.lang || null;
- this.handleSubtitleChange(lang, opt);
- }
- }, { passive: false });
-
- // Close menu when clicking outside
- document.addEventListener("click", this.handleClickOutside);
-
- // Add hover effects
- this.settingsOverlay.addEventListener("mouseover", (e) => {
- const item = e.target.closest(".settings-item, .speed-option");
- if (item && !item.style.background.includes("0.1")) {
- item.style.background = "rgba(255, 255, 255, 0.05)";
- }
- });
-
- this.settingsOverlay.addEventListener("mouseout", (e) => {
- const item = e.target.closest(".settings-item, .speed-option");
- if (item && !item.style.background.includes("0.1")) {
- item.style.background = "transparent";
- }
- });
-
- // Start subtitle synchronization
- this.startSubtitleSync();
- }
-
- toggleSettings(e) {
- e.stopPropagation();
- const isVisible = this.settingsOverlay.classList.contains("show");
-
- if (isVisible) {
- this.settingsOverlay.classList.remove("show");
- this.settingsOverlay.style.display = "none";
- } else {
- this.settingsOverlay.classList.add("show");
- this.settingsOverlay.style.display = "block";
+ console.log('Playback speed preference saved:', speed);
}
-
- this.speedSubmenu.style.display = "none"; // Hide submenu when main menu toggles
- if (this.qualitySubmenu) this.qualitySubmenu.style.display = "none";
- const btnEl = this.settingsButton?.el();
- if (btnEl) {
- if (!isVisible) {
- btnEl.classList.add("settings-clicked");
- } else {
- btnEl.classList.remove("settings-clicked");
- }
- }
- }
- handleSpeedChange(speed, speedOption) {
- // Update player speed
- this.player().playbackRate(speed);
+ handleQualityChange(value, qualityOption) {
+ const qualities = this.getAvailableQualities();
+ const selected = qualities.find((q) => String(q.value) === String(value));
- // Save preference
- this.userPreferences.setPreference("playbackRate", speed);
+ // Save preference
+ this.userPreferences.setQualityPreference(value);
- // Update UI
- this.speedSubmenu.querySelectorAll(".speed-option").forEach((opt) => {
- opt.classList.remove("active");
- opt.style.background = "transparent";
- const check = opt.querySelector(".checkmark");
- if (check) check.remove();
- });
+ // Update UI
+ this.qualitySubmenu.querySelectorAll('.quality-option').forEach((opt) => {
+ opt.classList.remove('active');
+ opt.style.background = 'transparent';
+ const check = opt.querySelector('.checkmark');
+ if (check) check.remove();
+ });
- speedOption.classList.add("active");
- speedOption.style.background = "rgba(255, 255, 255, 0.1)";
- speedOption.insertAdjacentHTML(
- "beforeend",
- '
✓'
- );
+ qualityOption.classList.add('active');
+ qualityOption.style.background = 'rgba(255, 255, 255, 0.1)';
+ qualityOption.insertAdjacentHTML('beforeend', '
✓');
- // Update main menu display
- const currentSpeedDisplay =
- this.settingsOverlay.querySelector(".current-speed");
- const speedLabel = speed === 1 ? "Normal" : `${speed}`;
- currentSpeedDisplay.textContent = speedLabel;
+ // Update main menu display
+ const currentQualityDisplay = this.settingsOverlay.querySelector('.current-quality');
+ currentQualityDisplay.innerHTML = selected?.displayLabel || selected?.label || String(value);
- // Close only the speed submenu (keep overlay open)
- this.speedSubmenu.style.display = "none";
+ // Perform source switch if we have src defined
+ if (selected?.src) {
+ const player = this.player();
+ const wasPaused = player.paused();
+ const currentTime = player.currentTime();
+ const rate = player.playbackRate();
- console.log("Playback speed preference saved:", speed);
- }
-
- handleQualityChange(value, qualityOption) {
- const qualities = this.getAvailableQualities();
- const selected = qualities.find((q) => String(q.value) === String(value));
-
- // Save preference
- this.userPreferences.setQualityPreference(value);
-
- // Update UI
- this.qualitySubmenu.querySelectorAll(".quality-option").forEach((opt) => {
- opt.classList.remove("active");
- opt.style.background = "transparent";
- const check = opt.querySelector(".checkmark");
- if (check) check.remove();
- });
-
- qualityOption.classList.add("active");
- qualityOption.style.background = "rgba(255, 255, 255, 0.1)";
- qualityOption.insertAdjacentHTML(
- "beforeend",
- '
✓'
- );
-
- // Update main menu display
- const currentQualityDisplay =
- this.settingsOverlay.querySelector(".current-quality");
- currentQualityDisplay.innerHTML =
- selected?.displayLabel || selected?.label || String(value);
-
- // Perform source switch if we have src defined
- if (selected?.src) {
- const player = this.player();
- const wasPaused = player.paused();
- const currentTime = player.currentTime();
- const rate = player.playbackRate();
-
- // Capture active subtitle language and existing remote tracks
- let activeSubtitleLang = null;
- try {
- const tt = player.textTracks();
- for (let i = 0; i < tt.length; i++) {
- const t = tt[i];
- if (t.kind === 'subtitles' && t.mode === 'showing') {
- activeSubtitleLang = t.language || t.srclang || null;
- break;
- }
- }
- } catch (e) {}
-
- // Persist active subtitle language so it survives reloads
- if (activeSubtitleLang) {
- this.userPreferences.setPreference('subtitleLanguage', activeSubtitleLang, true);
- // Also mark subtitles as enabled so applySubtitlePreference() runs on load
- this.userPreferences.setPreference('subtitleEnabled', true, true);
- }
-
- // Prefer remoteTextTrackEls (have src reliably)
- const subtitleTracksInfo = [];
- try {
- const els = player.remoteTextTrackEls ? player.remoteTextTrackEls() : [];
- for (let i = 0; i < els.length; i++) {
- const el = els[i];
- // Only keep subtitle tracks
- if ((el.kind || '').toLowerCase() === 'subtitles') {
- subtitleTracksInfo.push({
- kind: 'subtitles',
- src: el.src,
- srclang: el.srclang || (el.track && el.track.language) || '',
- label: el.label || (el.track && el.track.label) || '',
- default: !!el.default
- });
- }
- }
- } catch (e) {}
-
- // Fallback: try TextTracks if no elements found and track.src exists
- if (subtitleTracksInfo.length === 0) {
- try {
- const tt = player.textTracks();
- for (let i = 0; i < tt.length; i++) {
- const t = tt[i];
- if (t.kind === 'subtitles' && t.src) {
- subtitleTracksInfo.push({
- kind: 'subtitles',
- src: t.src,
- srclang: t.language || '',
- label: t.label || '',
- default: false
- });
- }
- }
- } catch (e) {}
- }
-
- player.addClass('vjs-changing-resolution');
- player.isChangingQuality = true; // prevent seek indicator during quality change
- player.src({ src: selected.src, type: selected.type || 'video/mp4' });
-
- if (wasPaused) {
- player.pause();
- }
-
- const finishRestore = () => {
- // Re-add remote tracks
- try {
- subtitleTracksInfo.forEach((trackInfo) => {
- if (trackInfo && trackInfo.src) {
- player.addRemoteTextTrack(trackInfo, false);
- }
- });
- } catch (e) {}
-
- // Restore time and rate
- try { player.playbackRate(rate); } catch (e) {}
- try { if (!isNaN(currentTime)) player.currentTime(currentTime); } catch (e) {}
-
- // Resume state
- if (!wasPaused) {
- player.play().catch(() => {});
- } else {
- player.pause();
- }
-
- // Restore the previously active subtitle language
- setTimeout(() => {
- try {
- const tt2 = player.textTracks();
- let restored = false;
- for (let i = 0; i < tt2.length; i++) {
- const t = tt2[i];
- if (t.kind === 'subtitles') {
- const match = activeSubtitleLang && (t.language === activeSubtitleLang || t.srclang === activeSubtitleLang);
- t.mode = match ? 'showing' : 'disabled';
- if (match) restored = true;
- }
- }
- // If nothing restored but a preference exists, try to apply it
- if (!restored) {
- const pref = this.userPreferences.getPreference('subtitleLanguage');
- if (pref) {
- for (let i = 0; i < tt2.length; i++) {
- const t = tt2[i];
- if (t.kind === 'subtitles' && (t.language === pref || t.srclang === pref)) {
- t.mode = 'showing';
- break;
- }
+ // Capture active subtitle language and existing remote tracks
+ let activeSubtitleLang = null;
+ try {
+ const tt = player.textTracks();
+ for (let i = 0; i < tt.length; i++) {
+ const t = tt[i];
+ if (t.kind === 'subtitles' && t.mode === 'showing') {
+ activeSubtitleLang = t.language || t.srclang || null;
+ break;
+ }
}
- }
+ } catch (e) {}
+
+ // Persist active subtitle language so it survives reloads
+ if (activeSubtitleLang) {
+ this.userPreferences.setPreference('subtitleLanguage', activeSubtitleLang, true);
+ // Also mark subtitles as enabled so applySubtitlePreference() runs on load
+ this.userPreferences.setPreference('subtitleEnabled', true, true);
}
- } catch (e) {}
- // Sync UI
- this.refreshSubtitlesSubmenu();
- this.updateCurrentSubtitleDisplay();
- player.trigger('texttrackchange');
- }, 150);
- // Ensure Subtitles (CC) button remains visible after source switch
- try {
- const controlBar = player.getChild('controlBar');
- const names = ['subtitlesButton','textTrackButton','subsCapsButton'];
- for (const n of names) {
- const btn = controlBar && controlBar.getChild(n);
- if (btn) {
- if (typeof btn.show === 'function') btn.show();
- const el = btn.el && btn.el();
- if (el) { el.style.display = ''; el.style.visibility = ''; }
+ // Prefer remoteTextTrackEls (have src reliably)
+ const subtitleTracksInfo = [];
+ try {
+ const els = player.remoteTextTrackEls ? player.remoteTextTrackEls() : [];
+ for (let i = 0; i < els.length; i++) {
+ const el = els[i];
+ // Only keep subtitle tracks
+ if ((el.kind || '').toLowerCase() === 'subtitles') {
+ subtitleTracksInfo.push({
+ kind: 'subtitles',
+ src: el.src,
+ srclang: el.srclang || (el.track && el.track.language) || '',
+ label: el.label || (el.track && el.track.label) || '',
+ default: !!el.default,
+ });
+ }
+ }
+ } catch (e) {}
+
+ // Fallback: try TextTracks if no elements found and track.src exists
+ if (subtitleTracksInfo.length === 0) {
+ try {
+ const tt = player.textTracks();
+ for (let i = 0; i < tt.length; i++) {
+ const t = tt[i];
+ if (t.kind === 'subtitles' && t.src) {
+ subtitleTracksInfo.push({
+ kind: 'subtitles',
+ src: t.src,
+ srclang: t.language || '',
+ label: t.label || '',
+ default: false,
+ });
+ }
+ }
+ } catch (e) {}
}
- }
- } catch (e) {}
- player.removeClass('vjs-changing-resolution');
- };
+ player.addClass('vjs-changing-resolution');
+ player.isChangingQuality = true; // prevent seek indicator during quality change
+ player.src({ src: selected.src, type: selected.type || 'video/mp4' });
- // Wait for metadata/data to be ready, then restore
- const onLoadedMeta = () => {
- player.off('loadedmetadata', onLoadedMeta);
- // Some browsers need loadeddata to have text track list ready
- player.one('loadeddata', finishRestore);
- };
- player.one('loadedmetadata', onLoadedMeta);
- }
-
- // Close only the quality submenu (keep overlay open)
- if (this.qualitySubmenu) this.qualitySubmenu.style.display = "none";
-
- console.log("Quality preference saved:", value);
- }
-
- handleSubtitleChange(lang, optionEl) {
- const player = this.player();
- const tracks = player.textTracks();
-
- // Update tracks
- for (let i = 0; i < tracks.length; i++) {
- const t = tracks[i];
- if (t.kind === 'subtitles') {
- t.mode = lang && t.language === lang ? 'showing' : 'disabled';
- }
- }
-
- // Save preference via UserPreferences (force set)
- this.userPreferences.setPreference('subtitleLanguage', lang || null, true);
- this.userPreferences.setPreference('subtitleEnabled', !!lang, true); // for iphones
-
- // Update UI selection
- this.subtitlesSubmenu.querySelectorAll('.subtitle-option').forEach((opt) => {
- opt.classList.remove('active');
- opt.style.background = 'transparent';
- const check = opt.querySelector('.checkmark');
- if (check) check.remove();
- });
- optionEl.classList.add('active');
- optionEl.style.background = 'rgba(255, 255, 255, 0.1)';
- optionEl.insertAdjacentHTML('beforeend', '
✓');
-
- // Update label in main settings
- const label = lang ? (optionEl.querySelector('span')?.textContent || lang) : 'Off';
- const currentSubtitlesDisplay = this.settingsOverlay.querySelector('.current-subtitles');
- if (currentSubtitlesDisplay) currentSubtitlesDisplay.textContent = label;
-
- // Close only the subtitles submenu (keep overlay open)
- this.subtitlesSubmenu.style.display = 'none';
- }
-
- restoreSubtitlePreference() {
- const savedLanguage = this.userPreferences.getPreference('subtitleLanguage');
- if (savedLanguage) {
- const tryRestore = (attempt = 1) => {
- try {
- const player = this.player();
- const tracks = player.textTracks();
- const saved = String(savedLanguage || '').toLowerCase();
- // First disable all subtitle tracks
- for (let i = 0; i < tracks.length; i++) {
- const t = tracks[i];
- if (t.kind === 'subtitles') t.mode = 'disabled';
- }
- // Helper for robust language matching (language or srclang; en vs en-US)
- const matches = (t) => {
- const tl = String(t.language || t.srclang || '').toLowerCase();
- if (!tl || !saved) return false;
- return tl === saved || tl.startsWith(saved + '-') || saved.startsWith(tl + '-');
- };
-
- let restored = false;
- for (let i = 0; i < tracks.length; i++) {
- const t = tracks[i];
- if (t.kind === 'subtitles' && matches(t)) {
- t.mode = 'showing';
- restored = true;
- // Persist enabled flag so iOS applies on next load
- try { this.userPreferences.setPreference('subtitleEnabled', true, true); } catch (e) { }
- // Refresh UI
- this.refreshSubtitlesSubmenu();
- this.updateCurrentSubtitleDisplay();
- try { player.trigger('texttrackchange'); } catch (e) { }
- break;
+ if (wasPaused) {
+ player.pause();
}
- }
- if (!restored && attempt < 8) {
- // Retry with incremental delay for iOS where tracks may not be ready
- const delay = 150 * attempt;
- setTimeout(() => tryRestore(attempt + 1), delay);
- }
- } catch (e) {
- if (attempt < 8) setTimeout(() => tryRestore(attempt + 1), 150 * attempt);
+ const finishRestore = () => {
+ // Re-add remote tracks
+ try {
+ subtitleTracksInfo.forEach((trackInfo) => {
+ if (trackInfo && trackInfo.src) {
+ player.addRemoteTextTrack(trackInfo, false);
+ }
+ });
+ } catch (e) {}
+
+ // Restore time and rate
+ try {
+ player.playbackRate(rate);
+ } catch (e) {}
+ try {
+ if (!isNaN(currentTime)) player.currentTime(currentTime);
+ } catch (e) {}
+
+ // Resume state
+ if (!wasPaused) {
+ player.play().catch(() => {});
+ } else {
+ player.pause();
+ }
+
+ // Restore the previously active subtitle language
+ setTimeout(() => {
+ try {
+ const tt2 = player.textTracks();
+ let restored = false;
+ for (let i = 0; i < tt2.length; i++) {
+ const t = tt2[i];
+ if (t.kind === 'subtitles') {
+ const match =
+ activeSubtitleLang &&
+ (t.language === activeSubtitleLang || t.srclang === activeSubtitleLang);
+ t.mode = match ? 'showing' : 'disabled';
+ if (match) restored = true;
+ }
+ }
+ // If nothing restored but a preference exists, try to apply it
+ if (!restored) {
+ const pref = this.userPreferences.getPreference('subtitleLanguage');
+ if (pref) {
+ for (let i = 0; i < tt2.length; i++) {
+ const t = tt2[i];
+ if (t.kind === 'subtitles' && (t.language === pref || t.srclang === pref)) {
+ t.mode = 'showing';
+ break;
+ }
+ }
+ }
+ }
+ } catch (e) {}
+ // Sync UI
+ this.refreshSubtitlesSubmenu();
+ this.updateCurrentSubtitleDisplay();
+ player.trigger('texttrackchange');
+ }, 150);
+
+ // Ensure Subtitles (CC) button remains visible after source switch
+ try {
+ const controlBar = player.getChild('controlBar');
+ const names = ['subtitlesButton', 'textTrackButton', 'subsCapsButton'];
+ for (const n of names) {
+ const btn = controlBar && controlBar.getChild(n);
+ if (btn) {
+ if (typeof btn.show === 'function') btn.show();
+ const el = btn.el && btn.el();
+ if (el) {
+ el.style.display = '';
+ el.style.visibility = '';
+ }
+ }
+ }
+ } catch (e) {}
+
+ player.removeClass('vjs-changing-resolution');
+ };
+
+ // Wait for metadata/data to be ready, then restore
+ const onLoadedMeta = () => {
+ player.off('loadedmetadata', onLoadedMeta);
+ // Some browsers need loadeddata to have text track list ready
+ player.one('loadeddata', finishRestore);
+ };
+ player.one('loadedmetadata', onLoadedMeta);
}
- };
- setTimeout(() => tryRestore(1), 300);
- try {
- const p = this.player();
- const once = (ev) => p.one(ev, () => setTimeout(() => tryRestore(1), 50));
- once('loadedmetadata');
- once('loadeddata');
- once('canplay');
- } catch (e) { }
- }
- }
- handleClickOutside(e) {
- if (
- this.settingsOverlay &&
- this.settingsButton &&
- !this.settingsOverlay.contains(e.target) &&
- !this.settingsButton.el().contains(e.target)
- ) {
- this.settingsOverlay.classList.remove("show");
- this.settingsOverlay.style.display = "none";
- this.speedSubmenu.style.display = "none";
- if (this.qualitySubmenu) this.qualitySubmenu.style.display = "none";
- const btnEl = this.settingsButton?.el();
- if (btnEl) {
- btnEl.classList.remove("settings-clicked");
- }
- }
- }
+ // Close only the quality submenu (keep overlay open)
+ if (this.qualitySubmenu) this.qualitySubmenu.style.display = 'none';
- dispose() {
- // Remove event listeners
- document.removeEventListener("click", this.handleClickOutside);
-
- // Remove DOM elements
- if (this.settingsOverlay) {
- this.settingsOverlay.remove();
+ console.log('Quality preference saved:', value);
}
- super.dispose();
- }
+ handleSubtitleChange(lang, optionEl) {
+ const player = this.player();
+ const tracks = player.textTracks();
+
+ // Update tracks
+ for (let i = 0; i < tracks.length; i++) {
+ const t = tracks[i];
+ if (t.kind === 'subtitles') {
+ t.mode = lang && t.language === lang ? 'showing' : 'disabled';
+ }
+ }
+
+ // Save preference via UserPreferences (force set)
+ this.userPreferences.setPreference('subtitleLanguage', lang || null, true);
+ this.userPreferences.setPreference('subtitleEnabled', !!lang, true); // for iphones
+
+ // Update UI selection
+ this.subtitlesSubmenu.querySelectorAll('.subtitle-option').forEach((opt) => {
+ opt.classList.remove('active');
+ opt.style.background = 'transparent';
+ const check = opt.querySelector('.checkmark');
+ if (check) check.remove();
+ });
+ optionEl.classList.add('active');
+ optionEl.style.background = 'rgba(255, 255, 255, 0.1)';
+ optionEl.insertAdjacentHTML('beforeend', '
✓');
+
+ // Update label in main settings
+ const label = lang ? optionEl.querySelector('span')?.textContent || lang : 'Off';
+ const currentSubtitlesDisplay = this.settingsOverlay.querySelector('.current-subtitles');
+ if (currentSubtitlesDisplay) currentSubtitlesDisplay.textContent = label;
+
+ // Close only the subtitles submenu (keep overlay open)
+ this.subtitlesSubmenu.style.display = 'none';
+ }
+
+ restoreSubtitlePreference() {
+ const savedLanguage = this.userPreferences.getPreference('subtitleLanguage');
+ if (savedLanguage) {
+ const tryRestore = (attempt = 1) => {
+ try {
+ const player = this.player();
+ const tracks = player.textTracks();
+ const saved = String(savedLanguage || '').toLowerCase();
+ // First disable all subtitle tracks
+ for (let i = 0; i < tracks.length; i++) {
+ const t = tracks[i];
+ if (t.kind === 'subtitles') t.mode = 'disabled';
+ }
+ // Helper for robust language matching (language or srclang; en vs en-US)
+ const matches = (t) => {
+ const tl = String(t.language || t.srclang || '').toLowerCase();
+ if (!tl || !saved) return false;
+ return tl === saved || tl.startsWith(saved + '-') || saved.startsWith(tl + '-');
+ };
+
+ let restored = false;
+ for (let i = 0; i < tracks.length; i++) {
+ const t = tracks[i];
+ if (t.kind === 'subtitles' && matches(t)) {
+ t.mode = 'showing';
+ restored = true;
+ // Persist enabled flag so iOS applies on next load
+ try {
+ this.userPreferences.setPreference('subtitleEnabled', true, true);
+ } catch (e) {}
+ // Refresh UI
+ this.refreshSubtitlesSubmenu();
+ this.updateCurrentSubtitleDisplay();
+ try {
+ player.trigger('texttrackchange');
+ } catch (e) {}
+ break;
+ }
+ }
+
+ if (!restored && attempt < 8) {
+ // Retry with incremental delay for iOS where tracks may not be ready
+ const delay = 150 * attempt;
+ setTimeout(() => tryRestore(attempt + 1), delay);
+ }
+ } catch (e) {
+ if (attempt < 8) setTimeout(() => tryRestore(attempt + 1), 150 * attempt);
+ }
+ };
+ setTimeout(() => tryRestore(1), 300);
+ try {
+ const p = this.player();
+ const once = (ev) => p.one(ev, () => setTimeout(() => tryRestore(1), 50));
+ once('loadedmetadata');
+ once('loadeddata');
+ once('canplay');
+ } catch (e) {}
+ }
+ }
+
+ handleClickOutside(e) {
+ if (
+ this.settingsOverlay &&
+ this.settingsButton &&
+ !this.settingsOverlay.contains(e.target) &&
+ !this.settingsButton.el().contains(e.target)
+ ) {
+ this.settingsOverlay.classList.remove('show');
+ this.settingsOverlay.style.display = 'none';
+ this.speedSubmenu.style.display = 'none';
+ if (this.qualitySubmenu) this.qualitySubmenu.style.display = 'none';
+ const btnEl = this.settingsButton?.el();
+ if (btnEl) {
+ btnEl.classList.remove('settings-clicked');
+ }
+ }
+ }
+
+ dispose() {
+ // Remove event listeners
+ document.removeEventListener('click', this.handleClickOutside);
+
+ // Remove DOM elements
+ if (this.settingsOverlay) {
+ this.settingsOverlay.remove();
+ }
+
+ super.dispose();
+ }
}
// Set component name for Video.js
-CustomSettingsMenu.prototype.controlText_ = "Settings Menu";
+CustomSettingsMenu.prototype.controlText_ = 'Settings Menu';
// Register the component with Video.js
-videojs.registerComponent("CustomSettingsMenu", CustomSettingsMenu);
+videojs.registerComponent('CustomSettingsMenu', CustomSettingsMenu);
export default CustomSettingsMenu;