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;