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