web player now supports ASS subtitles

This commit is contained in:
2026-01-03 14:53:42 +01:00
parent 136914ba4a
commit 3fe39bd6df
22 changed files with 650 additions and 3617 deletions

View File

@@ -15,6 +15,7 @@ const AnimePlayer = (function() {
let _localEntryId = null;
let _totalEpisodes = 0;
let _manualExtensionId = null;
let _activeSubtitleIndex = -1;
let hlsInstance = null;
let subtitleRenderer = null;
@@ -448,48 +449,6 @@ const AnimePlayer = (function() {
if (_rpcActive) sendRPC({ paused: true });
}
function onTimeUpdate() {
if (!els.video) return;
// Update progress bar
const percent = (els.video.currentTime / els.video.duration) * 100;
if (els.progressPlayed) {
els.progressPlayed.style.width = `${percent}%`;
}
if (els.progressHandle) {
els.progressHandle.style.left = `${percent}%`;
}
// Update time display
if (els.timeDisplay) {
const current = formatTime(els.video.currentTime);
const total = formatTime(els.video.duration);
els.timeDisplay.textContent = `${current} / ${total}`;
}
// Update progress for AniList
if (!_progressUpdated && els.video.duration) {
const percentage = els.video.currentTime / els.video.duration;
if (percentage >= 0.8) {
updateProgress();
_progressUpdated = true;
}
}
// Update subtitles - SAFE CHECK
// We only call setCurrentTime if renderer exists AND is not in disposed state
if (subtitleRenderer) {
try {
subtitleRenderer.setCurrentTime(els.video.currentTime);
} catch (e) {
// If the worker is dead or instance is invalid, silence the error
// and potentially nullify the renderer to stop further attempts
console.warn("Subtitle renderer error during timeupdate:", e);
subtitleRenderer = null;
}
}
}
function onProgress() {
if (!els.video || !els.progressBuffer) return;
if (els.video.buffered.length > 0) {
@@ -747,11 +706,27 @@ const AnimePlayer = (function() {
if (hlsInstance) hlsInstance.audioTrack = parseInt(value);
} else if (type === 'subtitle') {
const idx = parseInt(value);
_activeSubtitleIndex = idx; // <--- ACTUALIZAMOS EL ESTADO AQUÍ
// 1. Lógica nativa (para mantener compatibilidad interna)
if (els.video && els.video.textTracks) {
Array.from(els.video.textTracks).forEach((track, i) => {
track.mode = (i === idx) ? 'showing' : 'hidden';
// Si usamos JASSUB, ocultamos la nativa. Si no, mostramos la seleccionada.
track.mode = (subtitleRenderer && idx !== -1) ? 'hidden' : ((i === idx) ? 'showing' : 'hidden');
});
}
// 2. Lógica de JASSUB
if (subtitleRenderer) {
if (idx === -1) {
subtitleRenderer.dispose();
} else {
const sub = _currentSubtitles[idx];
if (sub) {
subtitleRenderer.setTrack(sub.src);
}
}
}
} else if (type === 'speed') {
if (els.video) els.video.playbackRate = parseFloat(value);
}
@@ -762,45 +737,79 @@ const AnimePlayer = (function() {
}
function getActiveSubtitleIndex() {
if (!els.video || !els.video.textTracks) return -1;
for (let i = 0; i < els.video.textTracks.length; i++) {
if (els.video.textTracks[i].mode === 'showing') return i;
}
return -1;
return _activeSubtitleIndex;
}
// Subtitle renderer with libass
async function initSubtitleRenderer() {
if (!window.SubtitlesOctopus || !els.video || !els.subtitlesCanvas) return;
if (!els.video) return;
// Ensure clean slate
// Cleanup previous instance
if (subtitleRenderer) {
try { subtitleRenderer.dispose(); } catch(e) { console.warn(e); }
try {
subtitleRenderer.dispose();
} catch(e) {
console.warn('Error disposing renderer:', e);
}
subtitleRenderer = null;
}
// Find ASS subtitle
const assSubtitle = _currentSubtitles.find(sub =>
sub.src && (sub.src.endsWith('.ass') || sub.label?.toLowerCase().includes('ass'))
(sub.src && sub.src.endsWith('.ass')) ||
(sub.label && sub.label.toLowerCase().includes('ass'))
);
if (!assSubtitle) return;
if (!assSubtitle) {
console.log('No ASS subtitles found in current list');
return;
}
try {
subtitleRenderer = new SubtitlesOctopus({
video: els.video,
canvas: els.subtitlesCanvas,
subUrl: assSubtitle.src,
fonts: [],
workerUrl: '/libs/subtitles-octopus-worker.js',
legacyWorkerUrl: '/libs/subtitles-octopus-worker-legacy.js',
});
console.log('Initializing JASSUB for:', assSubtitle.label);
// Check if JASSUB global is available
if (window.SubtitleRenderer && typeof window.JASSUB !== 'undefined') {
// --- CAMBIO AQUÍ: Pasamos els.subtitlesCanvas ---
subtitleRenderer = new SubtitleRenderer(els.video, els.subtitlesCanvas);
await subtitleRenderer.init(assSubtitle.src);
} else {
console.warn('JASSUB library not loaded.');
}
} catch (e) {
console.error('Subtitle renderer error:', e);
console.error('Subtitle renderer setup error:', e);
subtitleRenderer = null;
}
}
function onTimeUpdate() {
if (!els.video) return;
// Update progress bar
const percent = (els.video.currentTime / els.video.duration) * 100;
if (els.progressPlayed) {
els.progressPlayed.style.width = `${percent}%`;
}
if (els.progressHandle) {
els.progressHandle.style.left = `${percent}%`;
}
// Update time display
if (els.timeDisplay) {
const current = formatTime(els.video.currentTime);
const total = formatTime(els.video.duration);
els.timeDisplay.textContent = `${current} / ${total}`;
}
// Update progress for AniList
if (!_progressUpdated && els.video.duration) {
const percentage = els.video.currentTime / els.video.duration;
if (percentage >= 0.8) {
updateProgress();
_progressUpdated = true;
}
}
}
// Player lifecycle
async function playEpisode(episodeNumber) {
const targetEp = parseInt(episodeNumber);
@@ -1163,62 +1172,56 @@ const AnimePlayer = (function() {
}
function initVideoPlayer(url, type, subtitles = []) {
// Double check cleanup
// 1. CLEANUP FIRST: Destroy subtitle renderer while elements still exist
// This prevents "removeChild" errors because the DOM is still intact
if (subtitleRenderer) {
try {
subtitleRenderer.dispose();
} catch(e) {
console.warn('Renderer dispose error (clean):', e);
}
subtitleRenderer = null;
}
// 2. Destroy HLS instance
if (hlsInstance) {
hlsInstance.destroy();
hlsInstance = null;
}
if (subtitleRenderer) {
try { subtitleRenderer.dispose(); } catch(e) {}
subtitleRenderer = null;
}
const container = document.querySelector('.video-frame');
if (!container) return;
// --- SAFE VIDEO ELEMENT REPLACEMENT ---
// 3. Remove OLD Elements
const oldVideo = container.querySelector('video');
const oldCanvas = container.querySelector('#subtitles-canvas');
if (oldVideo) {
try {
// Remove listeners to stop events from firing during removal
oldVideo.ontimeupdate = null;
oldVideo.onplay = null;
oldVideo.onpause = null;
// Stop playback
oldVideo.pause();
oldVideo.removeAttribute('src');
oldVideo.load(); // Forces media unload
// Remove from DOM
if (oldVideo.parentNode) {
oldVideo.parentNode.removeChild(oldVideo);
} else {
oldVideo.remove();
}
} catch (e) {
console.warn("Error cleaning up old video element:", e);
// Continue anyway, we need to create the new player
}
oldVideo.removeAttribute('src');
oldVideo.load();
oldVideo.remove();
}
if (oldCanvas) {
oldCanvas.remove();
}
// 4. Create NEW Elements
const newVideo = document.createElement('video');
newVideo.id = 'player';
newVideo.crossOrigin = 'anonymous';
newVideo.playsInline = true;
// Insert new video carefully
if (container.firstChild) {
container.insertBefore(newVideo, container.firstChild);
} else {
container.appendChild(newVideo);
}
const newCanvas = document.createElement('canvas');
newCanvas.id = 'subtitles-canvas';
container.appendChild(newCanvas)
container.appendChild(newVideo);
els.video = newVideo;
els.subtitlesCanvas = newCanvas;
// Re-setup control listeners
setupCustomControls();
// 5. Initialize Player (HLS or Native)
if (Hls.isSupported() && type === 'm3u8') {
hlsInstance = new Hls({
enableWorker: true,
@@ -1240,49 +1243,63 @@ const AnimePlayer = (function() {
if (els.loader) els.loader.style.display = 'none';
});
hlsInstance.on(Hls.Events.LEVEL_SWITCHED, () => {
buildSettingsPanel();
});
hlsInstance.on(Hls.Events.LEVEL_SWITCHED, () => buildSettingsPanel());
hlsInstance.on(Hls.Events.AUDIO_TRACK_SWITCHED, () => buildSettingsPanel());
hlsInstance.on(Hls.Events.AUDIO_TRACK_SWITCHED, () => {
buildSettingsPanel();
});
if (els.downloadBtn) {
els.downloadBtn.style.display = 'flex';
}
} else if (els.video.canPlayType('application/vnd.apple.mpegurl') && type === 'm3u8') {
// Native HLS support (Safari)
els.video.src = url;
attachSubtitles(subtitles);
buildSettingsPanel();
els.video.play().catch(() => {});
if(els.loader) els.loader.style.display = 'none';
if (els.downloadBtn) {
els.downloadBtn.style.display = 'flex';
}
} else {
els.video.src = url;
attachSubtitles(subtitles);
buildSettingsPanel();
els.video.play().catch(() => {});
if(els.loader) els.loader.style.display = 'none';
if (els.downloadBtn) {
els.downloadBtn.style.display = 'none';
}
if (els.downloadBtn) els.downloadBtn.style.display = 'flex';
}
// Try to init ASS subtitle renderer
initSubtitleRenderer();
// 6. Init Subtitles with explicit delay
// We use setTimeout instead of requestAnimationFrame to let the Layout Engine catch up
if (type === 'm3u8') {
hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
attachSubtitles(subtitles);
buildSettingsPanel();
if (els.downloadBtn) els.downloadBtn.style.display = 'flex';
// IMPORTANTE: Esperar a loadedmetadata antes de init subtitles
if (els.video.readyState >= 1) {
initSubtitleRenderer();
} else {
els.video.addEventListener('loadedmetadata', () => {
initSubtitleRenderer();
}, { once: true });
}
els.video.play().catch(() => {});
if (els.loader) els.loader.style.display = 'none';
});
} else {
els.video.src = url;
attachSubtitles(subtitles);
buildSettingsPanel();
// Para video directo, esperar metadata
els.video.addEventListener('loadedmetadata', () => {
initSubtitleRenderer();
}, { once: true });
els.video.play().catch(() => {});
if(els.loader) els.loader.style.display = 'none';
if (els.downloadBtn) els.downloadBtn.style.display = 'flex';
}
}
function attachSubtitles(subtitles) {
if (!els.video) return;
// Remove existing tracks
_activeSubtitleIndex = -1;
Array.from(els.video.querySelectorAll('track')).forEach(t => t.remove());
if (subtitles.length === 0) return;
subtitles.forEach((sub, i) => {
const track = document.createElement('track');
track.kind = 'subtitles';
@@ -1293,10 +1310,15 @@ const AnimePlayer = (function() {
els.video.appendChild(track);
});
// Enable first track
setTimeout(() => {
if (els.video.textTracks && els.video.textTracks.length > 0) {
els.video.textTracks[0].mode = 'showing';
_activeSubtitleIndex = 0;
if (!subtitleRenderer) {
els.video.textTracks[0].mode = 'showing';
} else {
els.video.textTracks[0].mode = 'hidden';
}
}
}, 100);
}