web player now supports ASS subtitles
This commit is contained in:
@@ -15,6 +15,7 @@ const AnimePlayer = (function() {
|
|||||||
let _localEntryId = null;
|
let _localEntryId = null;
|
||||||
let _totalEpisodes = 0;
|
let _totalEpisodes = 0;
|
||||||
let _manualExtensionId = null;
|
let _manualExtensionId = null;
|
||||||
|
let _activeSubtitleIndex = -1;
|
||||||
|
|
||||||
let hlsInstance = null;
|
let hlsInstance = null;
|
||||||
let subtitleRenderer = null;
|
let subtitleRenderer = null;
|
||||||
@@ -448,48 +449,6 @@ const AnimePlayer = (function() {
|
|||||||
if (_rpcActive) sendRPC({ paused: true });
|
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() {
|
function onProgress() {
|
||||||
if (!els.video || !els.progressBuffer) return;
|
if (!els.video || !els.progressBuffer) return;
|
||||||
if (els.video.buffered.length > 0) {
|
if (els.video.buffered.length > 0) {
|
||||||
@@ -747,11 +706,27 @@ const AnimePlayer = (function() {
|
|||||||
if (hlsInstance) hlsInstance.audioTrack = parseInt(value);
|
if (hlsInstance) hlsInstance.audioTrack = parseInt(value);
|
||||||
} else if (type === 'subtitle') {
|
} else if (type === 'subtitle') {
|
||||||
const idx = parseInt(value);
|
const idx = parseInt(value);
|
||||||
|
_activeSubtitleIndex = idx; // <--- ACTUALIZAMOS EL ESTADO AQUÍ
|
||||||
|
|
||||||
|
// 1. Lógica nativa (para mantener compatibilidad interna)
|
||||||
if (els.video && els.video.textTracks) {
|
if (els.video && els.video.textTracks) {
|
||||||
Array.from(els.video.textTracks).forEach((track, i) => {
|
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') {
|
} else if (type === 'speed') {
|
||||||
if (els.video) els.video.playbackRate = parseFloat(value);
|
if (els.video) els.video.playbackRate = parseFloat(value);
|
||||||
}
|
}
|
||||||
@@ -762,45 +737,79 @@ const AnimePlayer = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getActiveSubtitleIndex() {
|
function getActiveSubtitleIndex() {
|
||||||
if (!els.video || !els.video.textTracks) return -1;
|
return _activeSubtitleIndex;
|
||||||
for (let i = 0; i < els.video.textTracks.length; i++) {
|
|
||||||
if (els.video.textTracks[i].mode === 'showing') return i;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subtitle renderer with libass
|
|
||||||
async function initSubtitleRenderer() {
|
async function initSubtitleRenderer() {
|
||||||
if (!window.SubtitlesOctopus || !els.video || !els.subtitlesCanvas) return;
|
if (!els.video) return;
|
||||||
|
|
||||||
// Ensure clean slate
|
// Cleanup previous instance
|
||||||
if (subtitleRenderer) {
|
if (subtitleRenderer) {
|
||||||
try { subtitleRenderer.dispose(); } catch(e) { console.warn(e); }
|
try {
|
||||||
|
subtitleRenderer.dispose();
|
||||||
|
} catch(e) {
|
||||||
|
console.warn('Error disposing renderer:', e);
|
||||||
|
}
|
||||||
subtitleRenderer = null;
|
subtitleRenderer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find ASS subtitle
|
// Find ASS subtitle
|
||||||
const assSubtitle = _currentSubtitles.find(sub =>
|
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 {
|
try {
|
||||||
subtitleRenderer = new SubtitlesOctopus({
|
console.log('Initializing JASSUB for:', assSubtitle.label);
|
||||||
video: els.video,
|
|
||||||
canvas: els.subtitlesCanvas,
|
// Check if JASSUB global is available
|
||||||
subUrl: assSubtitle.src,
|
if (window.SubtitleRenderer && typeof window.JASSUB !== 'undefined') {
|
||||||
fonts: [],
|
// --- CAMBIO AQUÍ: Pasamos els.subtitlesCanvas ---
|
||||||
workerUrl: '/libs/subtitles-octopus-worker.js',
|
subtitleRenderer = new SubtitleRenderer(els.video, els.subtitlesCanvas);
|
||||||
legacyWorkerUrl: '/libs/subtitles-octopus-worker-legacy.js',
|
await subtitleRenderer.init(assSubtitle.src);
|
||||||
});
|
} else {
|
||||||
|
console.warn('JASSUB library not loaded.');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Subtitle renderer error:', e);
|
console.error('Subtitle renderer setup error:', e);
|
||||||
subtitleRenderer = null;
|
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
|
// Player lifecycle
|
||||||
async function playEpisode(episodeNumber) {
|
async function playEpisode(episodeNumber) {
|
||||||
const targetEp = parseInt(episodeNumber);
|
const targetEp = parseInt(episodeNumber);
|
||||||
@@ -1163,62 +1172,56 @@ const AnimePlayer = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initVideoPlayer(url, type, subtitles = []) {
|
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) {
|
if (hlsInstance) {
|
||||||
hlsInstance.destroy();
|
hlsInstance.destroy();
|
||||||
hlsInstance = null;
|
hlsInstance = null;
|
||||||
}
|
}
|
||||||
if (subtitleRenderer) {
|
|
||||||
try { subtitleRenderer.dispose(); } catch(e) {}
|
|
||||||
subtitleRenderer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const container = document.querySelector('.video-frame');
|
const container = document.querySelector('.video-frame');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
// --- SAFE VIDEO ELEMENT REPLACEMENT ---
|
// 3. Remove OLD Elements
|
||||||
const oldVideo = container.querySelector('video');
|
const oldVideo = container.querySelector('video');
|
||||||
|
const oldCanvas = container.querySelector('#subtitles-canvas');
|
||||||
|
|
||||||
if (oldVideo) {
|
if (oldVideo) {
|
||||||
try {
|
oldVideo.removeAttribute('src');
|
||||||
// Remove listeners to stop events from firing during removal
|
oldVideo.load();
|
||||||
oldVideo.ontimeupdate = null;
|
oldVideo.remove();
|
||||||
oldVideo.onplay = null;
|
}
|
||||||
oldVideo.onpause = null;
|
if (oldCanvas) {
|
||||||
|
oldCanvas.remove();
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Create NEW Elements
|
||||||
const newVideo = document.createElement('video');
|
const newVideo = document.createElement('video');
|
||||||
newVideo.id = 'player';
|
newVideo.id = 'player';
|
||||||
newVideo.crossOrigin = 'anonymous';
|
newVideo.crossOrigin = 'anonymous';
|
||||||
newVideo.playsInline = true;
|
newVideo.playsInline = true;
|
||||||
|
|
||||||
// Insert new video carefully
|
const newCanvas = document.createElement('canvas');
|
||||||
if (container.firstChild) {
|
newCanvas.id = 'subtitles-canvas';
|
||||||
container.insertBefore(newVideo, container.firstChild);
|
container.appendChild(newCanvas)
|
||||||
} else {
|
container.appendChild(newVideo);
|
||||||
container.appendChild(newVideo);
|
|
||||||
}
|
|
||||||
|
|
||||||
els.video = newVideo;
|
els.video = newVideo;
|
||||||
|
els.subtitlesCanvas = newCanvas;
|
||||||
|
|
||||||
// Re-setup control listeners
|
|
||||||
setupCustomControls();
|
setupCustomControls();
|
||||||
|
|
||||||
|
// 5. Initialize Player (HLS or Native)
|
||||||
if (Hls.isSupported() && type === 'm3u8') {
|
if (Hls.isSupported() && type === 'm3u8') {
|
||||||
hlsInstance = new Hls({
|
hlsInstance = new Hls({
|
||||||
enableWorker: true,
|
enableWorker: true,
|
||||||
@@ -1240,49 +1243,63 @@ const AnimePlayer = (function() {
|
|||||||
if (els.loader) els.loader.style.display = 'none';
|
if (els.loader) els.loader.style.display = 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
hlsInstance.on(Hls.Events.LEVEL_SWITCHED, () => {
|
hlsInstance.on(Hls.Events.LEVEL_SWITCHED, () => buildSettingsPanel());
|
||||||
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 {
|
} else {
|
||||||
els.video.src = url;
|
els.video.src = url;
|
||||||
attachSubtitles(subtitles);
|
attachSubtitles(subtitles);
|
||||||
buildSettingsPanel();
|
buildSettingsPanel();
|
||||||
els.video.play().catch(() => {});
|
els.video.play().catch(() => {});
|
||||||
if(els.loader) els.loader.style.display = 'none';
|
if(els.loader) els.loader.style.display = 'none';
|
||||||
if (els.downloadBtn) {
|
if (els.downloadBtn) els.downloadBtn.style.display = 'flex';
|
||||||
els.downloadBtn.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to init ASS subtitle renderer
|
// 6. Init Subtitles with explicit delay
|
||||||
initSubtitleRenderer();
|
// 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) {
|
function attachSubtitles(subtitles) {
|
||||||
if (!els.video) return;
|
if (!els.video) return;
|
||||||
|
|
||||||
// Remove existing tracks
|
_activeSubtitleIndex = -1;
|
||||||
|
|
||||||
Array.from(els.video.querySelectorAll('track')).forEach(t => t.remove());
|
Array.from(els.video.querySelectorAll('track')).forEach(t => t.remove());
|
||||||
|
|
||||||
|
if (subtitles.length === 0) return;
|
||||||
|
|
||||||
subtitles.forEach((sub, i) => {
|
subtitles.forEach((sub, i) => {
|
||||||
const track = document.createElement('track');
|
const track = document.createElement('track');
|
||||||
track.kind = 'subtitles';
|
track.kind = 'subtitles';
|
||||||
@@ -1293,10 +1310,15 @@ const AnimePlayer = (function() {
|
|||||||
els.video.appendChild(track);
|
els.video.appendChild(track);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enable first track
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (els.video.textTracks && els.video.textTracks.length > 0) {
|
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);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|||||||
161
desktop/src/scripts/anime/subtitle-renderer.js
Normal file
161
desktop/src/scripts/anime/subtitle-renderer.js
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
const BASE_PATH = '/src/scripts/jassub/';
|
||||||
|
|
||||||
|
class SubtitleRenderer {
|
||||||
|
// 1. Aceptamos 'canvas' en el constructor
|
||||||
|
constructor(video, canvas) {
|
||||||
|
this.video = video;
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.instance = null;
|
||||||
|
this.currentUrl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(subtitleUrl) {
|
||||||
|
if (!this.video || !this.canvas) return; // 2. Verificamos canvas
|
||||||
|
|
||||||
|
this.dispose();
|
||||||
|
|
||||||
|
const finalUrl = subtitleUrl.includes('/api/proxy')
|
||||||
|
? subtitleUrl
|
||||||
|
: `/api/proxy?url=${encodeURIComponent(subtitleUrl)}`;
|
||||||
|
|
||||||
|
this.currentUrl = finalUrl;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.instance = new JASSUB({
|
||||||
|
video: this.video,
|
||||||
|
canvas: this.canvas,
|
||||||
|
subUrl: finalUrl,
|
||||||
|
|
||||||
|
workerUrl: `${BASE_PATH}jassub-worker.js`,
|
||||||
|
wasmUrl: `${BASE_PATH}jassub-worker.wasm`,
|
||||||
|
modernWasmUrl: `${BASE_PATH}jassub-worker-modern.wasm`,
|
||||||
|
|
||||||
|
blendMode: 'js',
|
||||||
|
asyncRender: true,
|
||||||
|
onDemand: true,
|
||||||
|
targetFps: 60,
|
||||||
|
debug: false
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('JASSUB initialized for:', finalUrl);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error("JASSUB Init Error:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resize() {
|
||||||
|
if (this.instance && this.instance.resize) {
|
||||||
|
this.instance.resize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTrack(url) {
|
||||||
|
const finalUrl = url.includes('/api/proxy')
|
||||||
|
? url
|
||||||
|
: `/api/proxy?url=${encodeURIComponent(url)}`;
|
||||||
|
|
||||||
|
if (this.instance) {
|
||||||
|
this.instance.setTrackByUrl(finalUrl);
|
||||||
|
this.currentUrl = finalUrl;
|
||||||
|
} else {
|
||||||
|
this.init(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
if (this.instance) {
|
||||||
|
try {
|
||||||
|
this.instance.destroy();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Error destroying JASSUB:", e);
|
||||||
|
}
|
||||||
|
this.instance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple Renderer remains unchanged for SRT/VTT (Non-ASS)
|
||||||
|
class SimpleSubtitleRenderer {
|
||||||
|
constructor(video, canvas) {
|
||||||
|
this.video = video;
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.ctx = canvas.getContext('2d');
|
||||||
|
this.cues = [];
|
||||||
|
this.destroyed = false;
|
||||||
|
|
||||||
|
this.setupCanvas();
|
||||||
|
this.video.addEventListener('timeupdate', () => this.render());
|
||||||
|
}
|
||||||
|
|
||||||
|
setupCanvas() {
|
||||||
|
const updateSize = () => {
|
||||||
|
if (!this.video || !this.canvas) return;
|
||||||
|
const rect = this.video.getBoundingClientRect();
|
||||||
|
this.canvas.width = rect.width;
|
||||||
|
this.canvas.height = rect.height;
|
||||||
|
};
|
||||||
|
updateSize();
|
||||||
|
window.addEventListener('resize', updateSize);
|
||||||
|
this.resizeHandler = updateSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadSubtitles(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/proxy?url=${encodeURIComponent(url)}`);
|
||||||
|
const text = await response.text();
|
||||||
|
this.cues = this.parseSRT(text);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load subtitles:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseSRT(srtText) {
|
||||||
|
const blocks = srtText.trim().split('\n\n');
|
||||||
|
return blocks.map(block => {
|
||||||
|
const lines = block.split('\n');
|
||||||
|
if (lines.length < 3) return null;
|
||||||
|
const timeMatch = lines[1].match(/(\d{2}):(\d{2}):(\d{2}),(\d{3}) --> (\d{2}):(\d{2}):(\d{2}),(\d{3})/);
|
||||||
|
if (!timeMatch) return null;
|
||||||
|
const start = parseInt(timeMatch[1]) * 3600 + parseInt(timeMatch[2]) * 60 + parseInt(timeMatch[3]) + parseInt(timeMatch[4]) / 1000;
|
||||||
|
const end = parseInt(timeMatch[5]) * 3600 + parseInt(timeMatch[6]) * 60 + parseInt(timeMatch[7]) + parseInt(timeMatch[8]) / 1000;
|
||||||
|
const text = lines.slice(2).join('\n');
|
||||||
|
return { start, end, text };
|
||||||
|
}).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.destroyed) return;
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
const currentTime = this.video.currentTime;
|
||||||
|
const cue = this.cues.find(c => currentTime >= c.start && currentTime <= c.end);
|
||||||
|
if (cue) this.drawSubtitle(cue.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSubtitle(text) {
|
||||||
|
const lines = text.split('\n');
|
||||||
|
const fontSize = Math.max(20, this.canvas.height * 0.04);
|
||||||
|
this.ctx.font = `bold ${fontSize}px Arial, sans-serif`;
|
||||||
|
this.ctx.textAlign = 'center';
|
||||||
|
this.ctx.textBaseline = 'bottom';
|
||||||
|
const lineHeight = fontSize * 1.2;
|
||||||
|
const startY = this.canvas.height - 60;
|
||||||
|
lines.reverse().forEach((line, index) => {
|
||||||
|
const y = startY - (index * lineHeight);
|
||||||
|
this.ctx.strokeStyle = 'black';
|
||||||
|
this.ctx.lineWidth = 4;
|
||||||
|
this.ctx.strokeText(line, this.canvas.width / 2, y);
|
||||||
|
this.ctx.fillStyle = 'white';
|
||||||
|
this.ctx.fillText(line, this.canvas.width / 2, y);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.destroyed = true;
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
if (this.resizeHandler) window.removeEventListener('resize', this.resizeHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.SubtitleRenderer = SubtitleRenderer;
|
||||||
|
window.SimpleSubtitleRenderer = SimpleSubtitleRenderer;
|
||||||
BIN
desktop/src/scripts/jassub/default.woff2
Normal file
BIN
desktop/src/scripts/jassub/default.woff2
Normal file
Binary file not shown.
BIN
desktop/src/scripts/jassub/jassub-worker-modern.wasm
Normal file
BIN
desktop/src/scripts/jassub/jassub-worker-modern.wasm
Normal file
Binary file not shown.
11
desktop/src/scripts/jassub/jassub-worker.js
Normal file
11
desktop/src/scripts/jassub/jassub-worker.js
Normal file
File diff suppressed because one or more lines are too long
BIN
desktop/src/scripts/jassub/jassub-worker.wasm
Normal file
BIN
desktop/src/scripts/jassub/jassub-worker.wasm
Normal file
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,10 @@
|
|||||||
<link rel="icon" href="/public/assets/waifuboards.ico" type="image/x-icon" />
|
<link rel="icon" href="/public/assets/waifuboards.ico" type="image/x-icon" />
|
||||||
<title>WaifuBoard</title>
|
<title>WaifuBoard</title>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
||||||
|
<script type="module">
|
||||||
|
import JASSUB from 'https://cdn.jsdelivr.net/npm/jassub@1.8.8/dist/jassub.es.js';
|
||||||
|
window.JASSUB = JASSUB;
|
||||||
|
</script>
|
||||||
<link rel="stylesheet" href="/views/css/globals.css" />
|
<link rel="stylesheet" href="/views/css/globals.css" />
|
||||||
<link rel="stylesheet" href="/views/css/components/anilist-modal.css" />
|
<link rel="stylesheet" href="/views/css/components/anilist-modal.css" />
|
||||||
<link rel="stylesheet" href="/views/css/anime/anime.css" />
|
<link rel="stylesheet" href="/views/css/anime/anime.css" />
|
||||||
@@ -302,6 +305,7 @@
|
|||||||
<script src="/src/scripts/utils/youtube-player-utils.js"></script>
|
<script src="/src/scripts/utils/youtube-player-utils.js"></script>
|
||||||
<script src="/src/scripts/utils/list-modal-manager.js"></script>
|
<script src="/src/scripts/utils/list-modal-manager.js"></script>
|
||||||
<script src="/src/scripts/utils/match-modal.js"></script>
|
<script src="/src/scripts/utils/match-modal.js"></script>
|
||||||
|
<script src="/src/scripts/anime/subtitle-renderer.js"></script>
|
||||||
|
|
||||||
<script src="/src/scripts/anime/player.js"></script>
|
<script src="/src/scripts/anime/player.js"></script>
|
||||||
<script src="/src/scripts/anime/entry.js"></script>
|
<script src="/src/scripts/anime/entry.js"></script>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const AnimePlayer = (function() {
|
|||||||
let _localEntryId = null;
|
let _localEntryId = null;
|
||||||
let _totalEpisodes = 0;
|
let _totalEpisodes = 0;
|
||||||
let _manualExtensionId = null;
|
let _manualExtensionId = null;
|
||||||
|
let _activeSubtitleIndex = -1;
|
||||||
|
|
||||||
let hlsInstance = null;
|
let hlsInstance = null;
|
||||||
let subtitleRenderer = null;
|
let subtitleRenderer = null;
|
||||||
@@ -448,48 +449,6 @@ const AnimePlayer = (function() {
|
|||||||
if (_rpcActive) sendRPC({ paused: true });
|
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() {
|
function onProgress() {
|
||||||
if (!els.video || !els.progressBuffer) return;
|
if (!els.video || !els.progressBuffer) return;
|
||||||
if (els.video.buffered.length > 0) {
|
if (els.video.buffered.length > 0) {
|
||||||
@@ -747,11 +706,27 @@ const AnimePlayer = (function() {
|
|||||||
if (hlsInstance) hlsInstance.audioTrack = parseInt(value);
|
if (hlsInstance) hlsInstance.audioTrack = parseInt(value);
|
||||||
} else if (type === 'subtitle') {
|
} else if (type === 'subtitle') {
|
||||||
const idx = parseInt(value);
|
const idx = parseInt(value);
|
||||||
|
_activeSubtitleIndex = idx; // <--- ACTUALIZAMOS EL ESTADO AQUÍ
|
||||||
|
|
||||||
|
// 1. Lógica nativa (para mantener compatibilidad interna)
|
||||||
if (els.video && els.video.textTracks) {
|
if (els.video && els.video.textTracks) {
|
||||||
Array.from(els.video.textTracks).forEach((track, i) => {
|
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') {
|
} else if (type === 'speed') {
|
||||||
if (els.video) els.video.playbackRate = parseFloat(value);
|
if (els.video) els.video.playbackRate = parseFloat(value);
|
||||||
}
|
}
|
||||||
@@ -762,45 +737,79 @@ const AnimePlayer = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getActiveSubtitleIndex() {
|
function getActiveSubtitleIndex() {
|
||||||
if (!els.video || !els.video.textTracks) return -1;
|
return _activeSubtitleIndex;
|
||||||
for (let i = 0; i < els.video.textTracks.length; i++) {
|
|
||||||
if (els.video.textTracks[i].mode === 'showing') return i;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subtitle renderer with libass
|
|
||||||
async function initSubtitleRenderer() {
|
async function initSubtitleRenderer() {
|
||||||
if (!window.SubtitlesOctopus || !els.video || !els.subtitlesCanvas) return;
|
if (!els.video) return;
|
||||||
|
|
||||||
// Ensure clean slate
|
// Cleanup previous instance
|
||||||
if (subtitleRenderer) {
|
if (subtitleRenderer) {
|
||||||
try { subtitleRenderer.dispose(); } catch(e) { console.warn(e); }
|
try {
|
||||||
|
subtitleRenderer.dispose();
|
||||||
|
} catch(e) {
|
||||||
|
console.warn('Error disposing renderer:', e);
|
||||||
|
}
|
||||||
subtitleRenderer = null;
|
subtitleRenderer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find ASS subtitle
|
// Find ASS subtitle
|
||||||
const assSubtitle = _currentSubtitles.find(sub =>
|
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 {
|
try {
|
||||||
subtitleRenderer = new SubtitlesOctopus({
|
console.log('Initializing JASSUB for:', assSubtitle.label);
|
||||||
video: els.video,
|
|
||||||
canvas: els.subtitlesCanvas,
|
// Check if JASSUB global is available
|
||||||
subUrl: assSubtitle.src,
|
if (window.SubtitleRenderer && typeof window.JASSUB !== 'undefined') {
|
||||||
fonts: [],
|
// --- CAMBIO AQUÍ: Pasamos els.subtitlesCanvas ---
|
||||||
workerUrl: '/libs/subtitles-octopus-worker.js',
|
subtitleRenderer = new SubtitleRenderer(els.video, els.subtitlesCanvas);
|
||||||
legacyWorkerUrl: '/libs/subtitles-octopus-worker-legacy.js',
|
await subtitleRenderer.init(assSubtitle.src);
|
||||||
});
|
} else {
|
||||||
|
console.warn('JASSUB library not loaded.');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Subtitle renderer error:', e);
|
console.error('Subtitle renderer setup error:', e);
|
||||||
subtitleRenderer = null;
|
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
|
// Player lifecycle
|
||||||
async function playEpisode(episodeNumber) {
|
async function playEpisode(episodeNumber) {
|
||||||
const targetEp = parseInt(episodeNumber);
|
const targetEp = parseInt(episodeNumber);
|
||||||
@@ -1163,62 +1172,56 @@ const AnimePlayer = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initVideoPlayer(url, type, subtitles = []) {
|
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) {
|
if (hlsInstance) {
|
||||||
hlsInstance.destroy();
|
hlsInstance.destroy();
|
||||||
hlsInstance = null;
|
hlsInstance = null;
|
||||||
}
|
}
|
||||||
if (subtitleRenderer) {
|
|
||||||
try { subtitleRenderer.dispose(); } catch(e) {}
|
|
||||||
subtitleRenderer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const container = document.querySelector('.video-frame');
|
const container = document.querySelector('.video-frame');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
// --- SAFE VIDEO ELEMENT REPLACEMENT ---
|
// 3. Remove OLD Elements
|
||||||
const oldVideo = container.querySelector('video');
|
const oldVideo = container.querySelector('video');
|
||||||
|
const oldCanvas = container.querySelector('#subtitles-canvas');
|
||||||
|
|
||||||
if (oldVideo) {
|
if (oldVideo) {
|
||||||
try {
|
oldVideo.removeAttribute('src');
|
||||||
// Remove listeners to stop events from firing during removal
|
oldVideo.load();
|
||||||
oldVideo.ontimeupdate = null;
|
oldVideo.remove();
|
||||||
oldVideo.onplay = null;
|
}
|
||||||
oldVideo.onpause = null;
|
if (oldCanvas) {
|
||||||
|
oldCanvas.remove();
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Create NEW Elements
|
||||||
const newVideo = document.createElement('video');
|
const newVideo = document.createElement('video');
|
||||||
newVideo.id = 'player';
|
newVideo.id = 'player';
|
||||||
newVideo.crossOrigin = 'anonymous';
|
newVideo.crossOrigin = 'anonymous';
|
||||||
newVideo.playsInline = true;
|
newVideo.playsInline = true;
|
||||||
|
|
||||||
// Insert new video carefully
|
const newCanvas = document.createElement('canvas');
|
||||||
if (container.firstChild) {
|
newCanvas.id = 'subtitles-canvas';
|
||||||
container.insertBefore(newVideo, container.firstChild);
|
container.appendChild(newCanvas)
|
||||||
} else {
|
container.appendChild(newVideo);
|
||||||
container.appendChild(newVideo);
|
|
||||||
}
|
|
||||||
|
|
||||||
els.video = newVideo;
|
els.video = newVideo;
|
||||||
|
els.subtitlesCanvas = newCanvas;
|
||||||
|
|
||||||
// Re-setup control listeners
|
|
||||||
setupCustomControls();
|
setupCustomControls();
|
||||||
|
|
||||||
|
// 5. Initialize Player (HLS or Native)
|
||||||
if (Hls.isSupported() && type === 'm3u8') {
|
if (Hls.isSupported() && type === 'm3u8') {
|
||||||
hlsInstance = new Hls({
|
hlsInstance = new Hls({
|
||||||
enableWorker: true,
|
enableWorker: true,
|
||||||
@@ -1240,49 +1243,63 @@ const AnimePlayer = (function() {
|
|||||||
if (els.loader) els.loader.style.display = 'none';
|
if (els.loader) els.loader.style.display = 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
hlsInstance.on(Hls.Events.LEVEL_SWITCHED, () => {
|
hlsInstance.on(Hls.Events.LEVEL_SWITCHED, () => buildSettingsPanel());
|
||||||
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 {
|
} else {
|
||||||
els.video.src = url;
|
els.video.src = url;
|
||||||
attachSubtitles(subtitles);
|
attachSubtitles(subtitles);
|
||||||
buildSettingsPanel();
|
buildSettingsPanel();
|
||||||
els.video.play().catch(() => {});
|
els.video.play().catch(() => {});
|
||||||
if(els.loader) els.loader.style.display = 'none';
|
if(els.loader) els.loader.style.display = 'none';
|
||||||
if (els.downloadBtn) {
|
if (els.downloadBtn) els.downloadBtn.style.display = 'flex';
|
||||||
els.downloadBtn.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to init ASS subtitle renderer
|
// 6. Init Subtitles with explicit delay
|
||||||
initSubtitleRenderer();
|
// 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) {
|
function attachSubtitles(subtitles) {
|
||||||
if (!els.video) return;
|
if (!els.video) return;
|
||||||
|
|
||||||
// Remove existing tracks
|
_activeSubtitleIndex = -1;
|
||||||
|
|
||||||
Array.from(els.video.querySelectorAll('track')).forEach(t => t.remove());
|
Array.from(els.video.querySelectorAll('track')).forEach(t => t.remove());
|
||||||
|
|
||||||
|
if (subtitles.length === 0) return;
|
||||||
|
|
||||||
subtitles.forEach((sub, i) => {
|
subtitles.forEach((sub, i) => {
|
||||||
const track = document.createElement('track');
|
const track = document.createElement('track');
|
||||||
track.kind = 'subtitles';
|
track.kind = 'subtitles';
|
||||||
@@ -1293,10 +1310,15 @@ const AnimePlayer = (function() {
|
|||||||
els.video.appendChild(track);
|
els.video.appendChild(track);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enable first track
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (els.video.textTracks && els.video.textTracks.length > 0) {
|
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);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|||||||
161
docker/src/scripts/anime/subtitle-renderer.js
Normal file
161
docker/src/scripts/anime/subtitle-renderer.js
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
const BASE_PATH = '/src/scripts/jassub/';
|
||||||
|
|
||||||
|
class SubtitleRenderer {
|
||||||
|
// 1. Aceptamos 'canvas' en el constructor
|
||||||
|
constructor(video, canvas) {
|
||||||
|
this.video = video;
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.instance = null;
|
||||||
|
this.currentUrl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(subtitleUrl) {
|
||||||
|
if (!this.video || !this.canvas) return; // 2. Verificamos canvas
|
||||||
|
|
||||||
|
this.dispose();
|
||||||
|
|
||||||
|
const finalUrl = subtitleUrl.includes('/api/proxy')
|
||||||
|
? subtitleUrl
|
||||||
|
: `/api/proxy?url=${encodeURIComponent(subtitleUrl)}`;
|
||||||
|
|
||||||
|
this.currentUrl = finalUrl;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.instance = new JASSUB({
|
||||||
|
video: this.video,
|
||||||
|
canvas: this.canvas,
|
||||||
|
subUrl: finalUrl,
|
||||||
|
|
||||||
|
workerUrl: `${BASE_PATH}jassub-worker.js`,
|
||||||
|
wasmUrl: `${BASE_PATH}jassub-worker.wasm`,
|
||||||
|
modernWasmUrl: `${BASE_PATH}jassub-worker-modern.wasm`,
|
||||||
|
|
||||||
|
blendMode: 'js',
|
||||||
|
asyncRender: true,
|
||||||
|
onDemand: true,
|
||||||
|
targetFps: 60,
|
||||||
|
debug: false
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('JASSUB initialized for:', finalUrl);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error("JASSUB Init Error:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resize() {
|
||||||
|
if (this.instance && this.instance.resize) {
|
||||||
|
this.instance.resize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTrack(url) {
|
||||||
|
const finalUrl = url.includes('/api/proxy')
|
||||||
|
? url
|
||||||
|
: `/api/proxy?url=${encodeURIComponent(url)}`;
|
||||||
|
|
||||||
|
if (this.instance) {
|
||||||
|
this.instance.setTrackByUrl(finalUrl);
|
||||||
|
this.currentUrl = finalUrl;
|
||||||
|
} else {
|
||||||
|
this.init(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
if (this.instance) {
|
||||||
|
try {
|
||||||
|
this.instance.destroy();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Error destroying JASSUB:", e);
|
||||||
|
}
|
||||||
|
this.instance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple Renderer remains unchanged for SRT/VTT (Non-ASS)
|
||||||
|
class SimpleSubtitleRenderer {
|
||||||
|
constructor(video, canvas) {
|
||||||
|
this.video = video;
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.ctx = canvas.getContext('2d');
|
||||||
|
this.cues = [];
|
||||||
|
this.destroyed = false;
|
||||||
|
|
||||||
|
this.setupCanvas();
|
||||||
|
this.video.addEventListener('timeupdate', () => this.render());
|
||||||
|
}
|
||||||
|
|
||||||
|
setupCanvas() {
|
||||||
|
const updateSize = () => {
|
||||||
|
if (!this.video || !this.canvas) return;
|
||||||
|
const rect = this.video.getBoundingClientRect();
|
||||||
|
this.canvas.width = rect.width;
|
||||||
|
this.canvas.height = rect.height;
|
||||||
|
};
|
||||||
|
updateSize();
|
||||||
|
window.addEventListener('resize', updateSize);
|
||||||
|
this.resizeHandler = updateSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadSubtitles(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/proxy?url=${encodeURIComponent(url)}`);
|
||||||
|
const text = await response.text();
|
||||||
|
this.cues = this.parseSRT(text);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load subtitles:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseSRT(srtText) {
|
||||||
|
const blocks = srtText.trim().split('\n\n');
|
||||||
|
return blocks.map(block => {
|
||||||
|
const lines = block.split('\n');
|
||||||
|
if (lines.length < 3) return null;
|
||||||
|
const timeMatch = lines[1].match(/(\d{2}):(\d{2}):(\d{2}),(\d{3}) --> (\d{2}):(\d{2}):(\d{2}),(\d{3})/);
|
||||||
|
if (!timeMatch) return null;
|
||||||
|
const start = parseInt(timeMatch[1]) * 3600 + parseInt(timeMatch[2]) * 60 + parseInt(timeMatch[3]) + parseInt(timeMatch[4]) / 1000;
|
||||||
|
const end = parseInt(timeMatch[5]) * 3600 + parseInt(timeMatch[6]) * 60 + parseInt(timeMatch[7]) + parseInt(timeMatch[8]) / 1000;
|
||||||
|
const text = lines.slice(2).join('\n');
|
||||||
|
return { start, end, text };
|
||||||
|
}).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.destroyed) return;
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
const currentTime = this.video.currentTime;
|
||||||
|
const cue = this.cues.find(c => currentTime >= c.start && currentTime <= c.end);
|
||||||
|
if (cue) this.drawSubtitle(cue.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSubtitle(text) {
|
||||||
|
const lines = text.split('\n');
|
||||||
|
const fontSize = Math.max(20, this.canvas.height * 0.04);
|
||||||
|
this.ctx.font = `bold ${fontSize}px Arial, sans-serif`;
|
||||||
|
this.ctx.textAlign = 'center';
|
||||||
|
this.ctx.textBaseline = 'bottom';
|
||||||
|
const lineHeight = fontSize * 1.2;
|
||||||
|
const startY = this.canvas.height - 60;
|
||||||
|
lines.reverse().forEach((line, index) => {
|
||||||
|
const y = startY - (index * lineHeight);
|
||||||
|
this.ctx.strokeStyle = 'black';
|
||||||
|
this.ctx.lineWidth = 4;
|
||||||
|
this.ctx.strokeText(line, this.canvas.width / 2, y);
|
||||||
|
this.ctx.fillStyle = 'white';
|
||||||
|
this.ctx.fillText(line, this.canvas.width / 2, y);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.destroyed = true;
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
if (this.resizeHandler) window.removeEventListener('resize', this.resizeHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.SubtitleRenderer = SubtitleRenderer;
|
||||||
|
window.SimpleSubtitleRenderer = SimpleSubtitleRenderer;
|
||||||
BIN
docker/src/scripts/jassub/default.woff2
Normal file
BIN
docker/src/scripts/jassub/default.woff2
Normal file
Binary file not shown.
BIN
docker/src/scripts/jassub/jassub-worker-modern.wasm
Normal file
BIN
docker/src/scripts/jassub/jassub-worker-modern.wasm
Normal file
Binary file not shown.
11
docker/src/scripts/jassub/jassub-worker.js
Normal file
11
docker/src/scripts/jassub/jassub-worker.js
Normal file
File diff suppressed because one or more lines are too long
BIN
docker/src/scripts/jassub/jassub-worker.wasm
Normal file
BIN
docker/src/scripts/jassub/jassub-worker.wasm
Normal file
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,10 @@
|
|||||||
<link rel="icon" href="/public/assets/waifuboards.ico" type="image/x-icon" />
|
<link rel="icon" href="/public/assets/waifuboards.ico" type="image/x-icon" />
|
||||||
<title>WaifuBoard</title>
|
<title>WaifuBoard</title>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
||||||
<script src="/src/scripts/libass/subtitles-octopus.js"></script>
|
<script type="module">
|
||||||
<link rel="stylesheet" href="/views/css/globals.css" />
|
import JASSUB from 'https://cdn.jsdelivr.net/npm/jassub@1.8.8/dist/jassub.es.js';
|
||||||
|
window.JASSUB = JASSUB;
|
||||||
|
</script> <link rel="stylesheet" href="/views/css/globals.css" />
|
||||||
<link rel="stylesheet" href="/views/css/components/anilist-modal.css" />
|
<link rel="stylesheet" href="/views/css/components/anilist-modal.css" />
|
||||||
<link rel="stylesheet" href="/views/css/anime/anime.css" />
|
<link rel="stylesheet" href="/views/css/anime/anime.css" />
|
||||||
<link rel="stylesheet" href="/views/css/anime/player.css" />
|
<link rel="stylesheet" href="/views/css/anime/player.css" />
|
||||||
@@ -282,6 +284,7 @@
|
|||||||
<script src="/src/scripts/utils/youtube-player-utils.js"></script>
|
<script src="/src/scripts/utils/youtube-player-utils.js"></script>
|
||||||
<script src="/src/scripts/utils/list-modal-manager.js"></script>
|
<script src="/src/scripts/utils/list-modal-manager.js"></script>
|
||||||
<script src="/src/scripts/utils/match-modal.js"></script>
|
<script src="/src/scripts/utils/match-modal.js"></script>
|
||||||
|
<script src="/src/scripts/anime/subtitle-renderer.js"></script>
|
||||||
|
|
||||||
<script src="/src/scripts/anime/player.js"></script>
|
<script src="/src/scripts/anime/player.js"></script>
|
||||||
<script src="/src/scripts/anime/entry.js"></script>
|
<script src="/src/scripts/anime/entry.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user