Updated subtitle detection logic, added closed caption button
This commit is contained in:
+17
-6
@@ -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" />
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user