Updated subtitle detection logic, added closed caption button

This commit is contained in:
Trevor Mears
2026-01-06 20:58:57 -08:00
parent 955d68e899
commit 28f16980e8
3 changed files with 153 additions and 21 deletions
+17 -6
View File
@@ -136,12 +136,7 @@
<h2 class="watch-title-overlay" id="player-channel-name"></h2>
</div>
<!-- Center: Large Play Button -->
<button class="watch-center-play" id="player-center-play">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M8 5v14l11-7z" />
</svg>
</button>
<!-- Loading Spinner -->
<div class="watch-loading" id="player-loading">
@@ -178,6 +173,22 @@
</button>
<input type="range" class="watch-volume" id="player-volume" min="0" max="100" value="80">
<!-- Captions Button -->
<div class="watch-captions-wrapper">
<button class="watch-btn" id="player-captions-btn" title="Captions">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path
d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1c0 .55-.45 1-1 1H7c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1c0 .55-.45 1-1 1h-3c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1z" />
</svg>
</button>
<div class="watch-captions-menu hidden" id="player-captions-menu">
<div class="captions-menu-title">Subtitles</div>
<div class="captions-menu-list" id="player-captions-list">
<button class="captions-option active" data-index="-1">Off</button>
</div>
</div>
</div>
<button class="watch-btn" id="btn-fullscreen" title="Fullscreen">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
+125 -15
View File
@@ -147,7 +147,6 @@ class VideoPlayer {
initCustomControls() {
// Elements
this.controlsOverlay = document.getElementById('player-controls-overlay');
this.centerPlayBtn = document.getElementById('player-center-play');
this.loadingSpinner = document.getElementById('player-loading');
const btnPlay = document.getElementById('btn-play');
@@ -163,7 +162,6 @@ class VideoPlayer {
// Initial State: Hide all overlay elements until content is loaded
this.loadingSpinner?.classList.remove('show');
this.centerPlayBtn?.classList.remove('show');
this.controlsOverlay?.classList.add('hidden');
// Play/Pause toggle
@@ -180,18 +178,10 @@ class VideoPlayer {
togglePlay();
});
this.centerPlayBtn?.addEventListener('click', (e) => {
e.stopPropagation();
togglePlay();
});
// Update play/pause UI
const updatePlayUI = () => {
const isPaused = this.video.paused;
// Center button
this.centerPlayBtn?.classList.toggle('show', isPaused);
// Bottom bar button
const iconPlay = btnPlay?.querySelector('.icon-play');
const iconPause = btnPlay?.querySelector('.icon-pause');
@@ -208,14 +198,10 @@ class VideoPlayer {
// Loading spinner
this.video.addEventListener('waiting', () => {
this.loadingSpinner?.classList.add('show');
this.centerPlayBtn?.classList.remove('show');
});
this.video.addEventListener('canplay', () => {
this.loadingSpinner?.classList.remove('show');
if (this.video.paused) {
this.centerPlayBtn?.classList.add('show');
}
});
// Mute/Volume
@@ -255,6 +241,26 @@ class VideoPlayer {
this.video.addEventListener('volumechange', updateVolumeUI);
// Captions
this.captionsBtn = document.getElementById('player-captions-btn');
this.captionsMenu = document.getElementById('player-captions-menu');
this.captionsList = document.getElementById('player-captions-list');
this.captionsMenuOpen = false;
this.captionsBtn?.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleCaptionsMenu();
});
// Close captions menu when clicking outside
document.addEventListener('click', (e) => {
if (this.captionsMenuOpen &&
!this.captionsMenu.contains(e.target) &&
!this.captionsBtn.contains(e.target)) {
this.closeCaptionsMenu();
}
});
// Fullscreen
btnFullscreen?.addEventListener('click', (e) => {
e.stopPropagation();
@@ -321,6 +327,100 @@ class VideoPlayer {
/**
* Toggle captions menu visibility
*/
toggleCaptionsMenu() {
if (!this.captionsMenu) return;
this.captionsMenuOpen = !this.captionsMenuOpen;
if (this.captionsMenuOpen) {
this.updateCaptionsTracks();
this.captionsMenu.classList.remove('hidden');
} else {
this.captionsMenu.classList.add('hidden');
}
}
/**
* Close captions menu
*/
closeCaptionsMenu() {
if (!this.captionsMenu) return;
this.captionsMenuOpen = false;
this.captionsMenu.classList.add('hidden');
}
/**
* Update available caption tracks in the menu
*/
updateCaptionsTracks() {
if (!this.captionsList) return;
// Clear existing list (keep only Off option)
this.captionsList.innerHTML = '<button class="captions-option" data-index="-1">Off</button>';
// Add tracks
if (this.video.textTracks && this.video.textTracks.length > 0) {
let hasActiveTrack = false;
for (let i = 0; i < this.video.textTracks.length; i++) {
const track = this.video.textTracks[i];
const btn = document.createElement('button');
btn.className = 'captions-option';
btn.textContent = track.label || `Track ${i + 1} (${track.language || 'unknown'})`;
btn.dataset.index = i;
if (track.mode === 'showing') {
btn.classList.add('active');
// Add checkmark
btn.innerHTML += ' <span style="float: right;">✓</span>';
hasActiveTrack = true;
}
btn.onclick = (e) => {
e.stopPropagation();
this.selectCaptionTrack(i);
};
this.captionsList.appendChild(btn);
}
// Handle "Off" button state
const offBtn = this.captionsList.querySelector('[data-index="-1"]');
if (offBtn) {
if (!hasActiveTrack) {
offBtn.classList.add('active');
offBtn.innerHTML += ' <span style="float: right;">✓</span>';
}
offBtn.onclick = (e) => {
e.stopPropagation();
this.selectCaptionTrack(-1);
};
}
}
}
/**
* Select a caption track
*/
selectCaptionTrack(index) {
if (!this.video.textTracks) return;
// Turn off all tracks
for (let i = 0; i < this.video.textTracks.length; i++) {
this.video.textTracks[i].mode = 'hidden'; // or 'disabled'
}
// Turn on selected track
if (index >= 0 && index < this.video.textTracks.length) {
this.video.textTracks[index].mode = 'showing';
}
this.closeCaptionsMenu();
}
init() {
// Apply default/remembered volume
const volume = this.settings.rememberVolume ? this.settings.lastVolume : this.settings.defaultVolume;
@@ -469,6 +569,17 @@ class VideoPlayer {
}
});
// Listen for subtitle track updates
this.hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, (event, data) => {
console.log('Subtitle tracks updated:', data.subtitleTracks);
// Wait a moment for native text tracks to populate
setTimeout(() => this.updateCaptionsTracks(), 100);
});
this.hls.on(Hls.Events.SUBTITLE_TRACK_SWITCH, (event, data) => {
console.log('Subtitle track switched:', data);
});
this.hls.on(Hls.Events.MANIFEST_PARSED, () => {
this.video.play().catch(e => console.log('Autoplay prevented:', e));
});
@@ -837,7 +948,6 @@ class VideoPlayer {
this.overlay.classList.remove('hidden'); // Show "Select a channel"
this.controlsOverlay?.classList.add('hidden'); // Hide controls
this.loadingSpinner?.classList.remove('show');
this.centerPlayBtn?.classList.remove('show');
this.nowPlaying.classList.add('hidden');
}
+11
View File
@@ -268,6 +268,17 @@ class WatchPage {
this.hls.loadSource(finalUrl);
this.hls.attachMedia(this.video);
// Listen for subtitle track updates
this.hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, (event, data) => {
console.log('[WatchPage] Subtitle tracks updated:', data.subtitleTracks);
// Wait a moment for native text tracks to populate
setTimeout(() => this.updateCaptionsTracks(), 100);
});
this.hls.on(Hls.Events.SUBTITLE_TRACK_SWITCH, (event, data) => {
console.log('[WatchPage] Subtitle track switched:', data);
});
this.hls.on(Hls.Events.MANIFEST_PARSED, () => {
this.video.play().catch(e => {
if (e.name !== 'AbortError') console.error('[WatchPage] Autoplay error:', e);