web player now supports ASS subtitles
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user