support for subs on local files
This commit is contained in:
@@ -857,27 +857,15 @@ const AnimePlayer = (function() {
|
||||
if (hlsInstance) hlsInstance.audioTrack = parseInt(value);
|
||||
} else if (type === 'subtitle') {
|
||||
const idx = parseInt(value);
|
||||
_activeSubtitleIndex = idx; // <--- ACTUALIZAMOS EL ESTADO AQUÍ
|
||||
_activeSubtitleIndex = idx;
|
||||
|
||||
// 1. Lógica nativa (para mantener compatibilidad interna)
|
||||
if (els.video && els.video.textTracks) {
|
||||
Array.from(els.video.textTracks).forEach((track, i) => {
|
||||
// Si usamos JASSUB, ocultamos la nativa. Si no, mostramos la seleccionada.
|
||||
track.mode = (subtitleRenderer && idx !== -1) ? 'hidden' : ((i === idx) ? 'showing' : 'hidden');
|
||||
track.mode = 'hidden';
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Lógica de JASSUB
|
||||
if (subtitleRenderer) {
|
||||
if (idx === -1) {
|
||||
subtitleRenderer.dispose();
|
||||
} else {
|
||||
const sub = _currentSubtitles[idx];
|
||||
if (sub) {
|
||||
subtitleRenderer.setTrack(sub.src);
|
||||
}
|
||||
}
|
||||
}
|
||||
initSubtitleRenderer();
|
||||
} else if (type === 'speed') {
|
||||
if (els.video) els.video.playbackRate = parseFloat(value);
|
||||
}
|
||||
@@ -1185,20 +1173,6 @@ const AnimePlayer = (function() {
|
||||
if(els.loader) els.loader.style.display = 'flex';
|
||||
}
|
||||
|
||||
async function getLocalEntryId() {
|
||||
if (_localEntryId) return _localEntryId;
|
||||
try {
|
||||
const res = await fetch(`/api/library/anime/${_animeId}`);
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
_localEntryId = data.id;
|
||||
return _localEntryId;
|
||||
} catch (e) {
|
||||
console.error("Error fetching local ID:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStream() {
|
||||
if (!_currentEpisode) return;
|
||||
_progressUpdated = false;
|
||||
@@ -1225,16 +1199,21 @@ const AnimePlayer = (function() {
|
||||
_rawVideoData = null;
|
||||
}
|
||||
|
||||
// En la función loadStream(), cuando detectas local:
|
||||
|
||||
if (currentExt === 'local') {
|
||||
try {
|
||||
setLoading("Fetching Local Unit Data...");
|
||||
|
||||
// 1. Obtener las unidades locales para encontrar el ID del episodio específico
|
||||
// 1. Obtener unidades
|
||||
const unitsRes = await fetch(`/api/library/${_animeId}/units`);
|
||||
if (!unitsRes.ok) throw new Error("Could not fetch local units");
|
||||
|
||||
const unitsData = await unitsRes.json();
|
||||
const targetUnit = unitsData.units ? unitsData.units.find(u => u.number === parseInt(_currentEpisode)) : null;
|
||||
_localEntryId = unitsData.entry_id;
|
||||
|
||||
const targetUnit = unitsData.units ?
|
||||
unitsData.units.find(u => u.number === parseInt(_currentEpisode)) : null;
|
||||
|
||||
if (!targetUnit) {
|
||||
throw new Error(`Episode ${_currentEpisode} not found in local library`);
|
||||
@@ -1242,46 +1221,70 @@ const AnimePlayer = (function() {
|
||||
|
||||
setLoading("Initializing HLS Stream...");
|
||||
|
||||
const manifestRes = await fetch(`/api/library/stream/anime/${unitsData.entry_id}/${targetUnit.number}/manifest`);
|
||||
// 2. Obtener Manifest
|
||||
const manifestRes = await fetch(
|
||||
`/api/library/stream/anime/${unitsData.entry_id}/${targetUnit.number}/manifest`
|
||||
);
|
||||
|
||||
if (!manifestRes.ok) throw new Error("Failed to generate stream manifest");
|
||||
|
||||
const manifestData = await manifestRes.json();
|
||||
|
||||
// DEBUG: Verificar que llega aquí
|
||||
console.log("Manifest Loaded:", manifestData);
|
||||
|
||||
// 3. Chapters
|
||||
if (manifestData.chapters && manifestData.chapters.length > 0) {
|
||||
_skipIntervals = manifestData.chapters.map(c => ({
|
||||
startTime: c.start,
|
||||
endTime: c.end,
|
||||
type: c.title.toLowerCase().includes('op') ? 'op' :
|
||||
c.title.toLowerCase().includes('ed') ? 'ed' : 'chapter'
|
||||
type: (c.title || '').toLowerCase().includes('op') ? 'op' :
|
||||
(c.title || '').toLowerCase().includes('ed') ? 'ed' : 'chapter'
|
||||
}));
|
||||
renderSkipMarkers(_skipIntervals);
|
||||
monitorSkipButton(_skipIntervals);
|
||||
}
|
||||
|
||||
// 4. Mapear Subtítulos WebVTT
|
||||
const subs = (manifestData.subtitles || []).map(s => ({
|
||||
label: s.title || s.language || `Track ${s.index}`,
|
||||
srclang: s.language || 'unk',
|
||||
src: s.url // URL al endpoint de conversión VTT (.vtt)
|
||||
}));
|
||||
// 4. CORRECCIÓN DE SUBTÍTULOS
|
||||
const rawSubs = manifestData.subtitles || [];
|
||||
console.log("Raw Subtitles from JSON:", rawSubs);
|
||||
|
||||
const subs = rawSubs.map((s, index) => {
|
||||
// Limpieza segura del código de idioma (ej. "English" -> "en")
|
||||
let langCode = 'en';
|
||||
if (s.language && typeof s.language === 'string' && s.language.length >= 2) {
|
||||
langCode = s.language.substring(0, 2).toLowerCase();
|
||||
}
|
||||
|
||||
// Título legible (Prioridad: Title > Language > Track #)
|
||||
const label = s.title || s.language || `Track ${index + 1}`;
|
||||
|
||||
return {
|
||||
label: label,
|
||||
srclang: langCode,
|
||||
src: s.url // Usamos la URL directa del JSON local
|
||||
};
|
||||
});
|
||||
|
||||
// ACTUALIZACIÓN CRÍTICA DEL ESTADO GLOBAL
|
||||
_currentSubtitles = subs;
|
||||
console.log("Processed Subtitles:", _currentSubtitles);
|
||||
|
||||
// 5. Guardar referencia para MPV o descargas
|
||||
_rawVideoData = {
|
||||
url: manifestData.masterPlaylist,
|
||||
headers: {}
|
||||
};
|
||||
|
||||
// 6. Cargar en el reproductor (Hls.js gestionará los audios del master.m3u8)
|
||||
// Inicializar video pasando explícitamente los subs procesados
|
||||
initVideoPlayer(manifestData.masterPlaylist, 'm3u8', subs);
|
||||
|
||||
} catch(e) {
|
||||
console.error("Local HLS Error:", e);
|
||||
setLoading("Local Error: " + e.message);
|
||||
|
||||
// Fallback: si falla, intentar cargar desde extensión online
|
||||
// Fallback logic...
|
||||
const localOption = els.extSelect.querySelector('option[value="local"]');
|
||||
if (localOption) localOption.remove();
|
||||
|
||||
const fallbackSource = (_entrySource === 'local') ? 'anilist' : _entrySource;
|
||||
els.extSelect.value = fallbackSource;
|
||||
handleExtensionChange(true);
|
||||
|
||||
@@ -1,60 +1,46 @@
|
||||
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
|
||||
|
||||
if (!this.video || !this.canvas) return;
|
||||
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;
|
||||
@@ -62,7 +48,6 @@ class SubtitleRenderer {
|
||||
this.init(url);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.instance) {
|
||||
try {
|
||||
@@ -74,8 +59,6 @@ class SubtitleRenderer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Simple Renderer remains unchanged for SRT/VTT (Non-ASS)
|
||||
class SimpleSubtitleRenderer {
|
||||
constructor(video, canvas) {
|
||||
this.video = video;
|
||||
@@ -83,11 +66,9 @@ class SimpleSubtitleRenderer {
|
||||
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;
|
||||
@@ -99,19 +80,31 @@ class SimpleSubtitleRenderer {
|
||||
window.addEventListener('resize', updateSize);
|
||||
this.resizeHandler = updateSize;
|
||||
}
|
||||
|
||||
async loadSubtitles(url) {
|
||||
try {
|
||||
const response = await fetch(`/api/proxy?url=${encodeURIComponent(url)}`);
|
||||
let finalUrl = url;
|
||||
const isLocal = url.startsWith('/');
|
||||
const isAlreadyProxied = url.includes('/api/proxy');
|
||||
if (!isLocal && !isAlreadyProxied && (url.startsWith('http:') || url.startsWith('https:'))) {
|
||||
finalUrl = `/api/proxy?url=${encodeURIComponent(url)}`;
|
||||
}
|
||||
console.log('Fetching subtitles from:', finalUrl);
|
||||
const response = await fetch(finalUrl);
|
||||
if (!response.ok) throw new Error(`Status: ${response.status}`);
|
||||
const text = await response.text();
|
||||
this.cues = this.parseSRT(text);
|
||||
} catch (error) {
|
||||
console.error('Failed to load subtitles:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setTrack(url) {
|
||||
this.cues = [];
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.loadSubtitles(url);
|
||||
}
|
||||
parseSRT(srtText) {
|
||||
const blocks = srtText.trim().split('\n\n');
|
||||
const normalizedText = srtText.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
const blocks = normalizedText.trim().split('\n\n');
|
||||
return blocks.map(block => {
|
||||
const lines = block.split('\n');
|
||||
if (lines.length < 3) return null;
|
||||
@@ -119,11 +112,12 @@ class SimpleSubtitleRenderer {
|
||||
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');
|
||||
let text = lines.slice(2).join('\n');
|
||||
text = text.replace(/<[^>]*>/g, '');
|
||||
text = text.replace(/\{[^}]*\}/g, '');
|
||||
return { start, end, text };
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.destroyed) return;
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
@@ -131,7 +125,6 @@ class SimpleSubtitleRenderer {
|
||||
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);
|
||||
@@ -149,13 +142,11 @@ class SimpleSubtitleRenderer {
|
||||
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