emby支持高级字幕

This commit is contained in:
mtvpls
2026-06-14 20:33:11 +08:00
parent c45fdded59
commit 7a34950a7e
7 changed files with 494 additions and 50 deletions
+149 -20
View File
@@ -132,6 +132,17 @@ interface CustomSubtitleState {
content?: string;
}
interface SourceSubtitleItem {
label: string;
url: string;
fallbackUrl?: string;
fallbackFormat?: string;
format?: string;
sourceFormat?: string;
codec?: string;
renderMode?: 'native' | 'jassub';
}
interface JassubSubtitleInstance {
setTrack?: (content: string) => void | Promise<void>;
setTrackByUrl?: (url: string) => void | Promise<void>;
@@ -1956,6 +1967,20 @@ function PlayPageClient() {
return ADVANCED_SUBTITLE_FORMATS.has(format.toLowerCase());
};
const getSourceSubtitleFormat = (subtitle?: SourceSubtitleItem | null) => {
return (
subtitle?.format ||
subtitle?.sourceFormat ||
subtitle?.codec ||
''
).toLowerCase();
};
const isAdvancedSourceSubtitle = (subtitle?: SourceSubtitleItem | null) => {
return subtitle?.renderMode === 'jassub' ||
isAdvancedSubtitleFormat(getSourceSubtitleFormat(subtitle));
};
const getJassubSubtitleInstance = (): JassubSubtitleInstance | null => {
return artPlayerRef.current?.plugins?.artplayerPluginJassub?.instance || null;
};
@@ -2004,13 +2029,17 @@ function PlayPageClient() {
};
const ensureJassubSubtitleInstance = async (
initialContent: string
initialTrack: { content?: string; url?: string }
): Promise<{ instance: JassubSubtitleInstance; created: boolean }> => {
const existingInstance = getJassubSubtitleInstance();
if (existingInstance) {
return { instance: existingInstance, created: false };
}
if (!initialTrack.content && !initialTrack.url) {
throw new Error('缺少高级字幕内容');
}
if (!artPlayerRef.current) {
throw new Error('播放器尚未就绪');
}
@@ -2021,7 +2050,9 @@ function PlayPageClient() {
artPlayerRef.current.plugins.add(
artplayerPluginJassub({
subContent: initialContent,
...(initialTrack.content
? { subContent: initialTrack.content }
: { subUrl: initialTrack.url }),
workerUrl: `${JASSUB_ASSET_BASE}/jassub-worker.js`,
wasmUrl: `${JASSUB_ASSET_BASE}/jassub-worker.wasm`,
modernWasmUrl: `${JASSUB_ASSET_BASE}/jassub-worker-modern.wasm`,
@@ -2045,7 +2076,7 @@ function PlayPageClient() {
if (!artPlayerRef.current) return;
artPlayerRef.current.subtitle.show = false;
const { instance, created } = await ensureJassubSubtitleInstance(content);
const { instance, created } = await ensureJassubSubtitleInstance({ content });
// 新建实例时 subContent 已作为初始轨道传入;复用实例时需要显式切轨。
if (!created) {
@@ -2055,6 +2086,63 @@ function PlayPageClient() {
currentSubtitleLabelRef.current = label;
};
const switchAdvancedSubtitleByUrl = async (url: string, label: string) => {
if (!artPlayerRef.current) return;
artPlayerRef.current.subtitle.show = false;
const { instance, created } = await ensureJassubSubtitleInstance({ url });
// 新建实例时 subUrl 已作为初始轨道传入;复用实例时需要显式切轨。
if (!created) {
if (instance.setTrackByUrl) {
await instance.setTrackByUrl(url);
} else {
const response = await fetch(url, {
credentials: 'include',
cache: 'no-store',
});
if (!response.ok) {
throw new Error(`高级字幕加载失败 (${response.status})`);
}
await instance.setTrack?.(await response.text());
}
}
currentSubtitleLabelRef.current = label;
};
const switchSourceSubtitle = async (subtitle: SourceSubtitleItem) => {
if (!subtitle.url) return;
if (isAdvancedSourceSubtitle(subtitle)) {
try {
await switchAdvancedSubtitleByUrl(subtitle.url, subtitle.label);
return;
} catch (error) {
if (!subtitle.fallbackUrl) {
throw error;
}
console.warn('[Subtitle] 高级字幕加载失败,尝试降级为普通字幕:', error);
switchSubtitle(subtitle.fallbackUrl, subtitle.label);
const message = `高级字幕渲染失败,已降级为普通字幕:${subtitle.label}`;
if (artPlayerRef.current) {
artPlayerRef.current.notice.show = message;
}
setToast({
message,
type: 'info',
duration: 5000,
onClose: () => setToast(null),
});
}
return;
}
switchSubtitle(subtitle.url, subtitle.label);
};
const removeSubtitleSetting = () => {
try {
artPlayerRef.current?.setting.remove('subtitle-selector');
@@ -2066,7 +2154,7 @@ function PlayPageClient() {
const updateSubtitleSetting = () => {
if (!artPlayerRef.current) return;
const sourceSubtitles = detailRef.current?.subtitles?.[currentEpisodeIndexRef.current] || [];
const sourceSubtitles = (detailRef.current?.subtitles?.[currentEpisodeIndexRef.current] || []) as SourceSubtitleItem[];
const customSubtitle =
customSubtitleRef.current?.episodeIndex === currentEpisodeIndexRef.current
? customSubtitleRef.current
@@ -2077,12 +2165,19 @@ function PlayPageClient() {
const subtitleOptions = [
{ html: '关闭', action: 'close' },
{ html: '上传本地字幕', action: 'upload' },
...sourceSubtitles.map((sub: any) => ({
html: sub.label,
action: 'switch',
engine: 'native',
url: sub.url,
})),
...sourceSubtitles.map((sub: SourceSubtitleItem) => {
const isAdvanced = isAdvancedSourceSubtitle(sub);
const format = getSourceSubtitleFormat(sub);
return {
html: sub.label,
action: 'switch',
engine: isAdvanced ? 'jassub' : 'native',
url: sub.url,
fallbackUrl: sub.fallbackUrl,
fallbackFormat: sub.fallbackFormat,
format,
};
}),
...(customSubtitle
? [
{
@@ -2115,8 +2210,21 @@ function PlayPageClient() {
return currentSubtitleLabelRef.current;
}
if (item.engine === 'jassub' && item.content) {
void switchAdvancedSubtitle(item.content, item.html).catch((error) => {
if (item.engine === 'jassub') {
const switchPromise = item.content
? switchAdvancedSubtitle(item.content, item.html)
: item.url
? switchSourceSubtitle({
label: item.html,
url: item.url,
fallbackUrl: item.fallbackUrl,
fallbackFormat: item.fallbackFormat,
format: item.format,
renderMode: 'jassub',
})
: Promise.resolve();
void switchPromise.catch((error) => {
console.warn('[Subtitle] 高级字幕切换失败:', error);
setToast({
message: error instanceof Error ? error.message : '高级字幕切换失败',
@@ -4773,11 +4881,16 @@ function PlayPageClient() {
if (!artPlayerRef.current || !detail) return;
revokeCustomSubtitle();
const currentSubtitles = detail.subtitles?.[currentEpisodeIndex] || [];
const currentSubtitles = (detail.subtitles?.[currentEpisodeIndex] || []) as SourceSubtitleItem[];
// 如果有字幕,更新播放器字幕
if (currentSubtitles.length > 0) {
switchSubtitle(currentSubtitles[0].url, currentSubtitles[0].label);
currentSubtitleLabelRef.current = currentSubtitles[0].label;
void switchSourceSubtitle(currentSubtitles[0]).catch((error) => {
console.warn('[Subtitle] 源字幕加载失败:', error);
artPlayerRef.current.subtitle.show = false;
currentSubtitleLabelRef.current = '关闭';
});
} else {
artPlayerRef.current.subtitle.show = false;
currentSubtitleLabelRef.current = '关闭';
@@ -6574,9 +6687,11 @@ function PlayPageClient() {
Artplayer.USE_RAF = true;
// 获取当前集的字幕
const currentSubtitles = detailRef.current?.subtitles?.[currentEpisodeIndex] || [];
const currentSubtitles = (detailRef.current?.subtitles?.[currentEpisodeIndex] || []) as SourceSubtitleItem[];
const defaultSubtitle = currentSubtitles[0];
const shouldUseNativeInitialSubtitle = !!defaultSubtitle && !isAdvancedSourceSubtitle(defaultSubtitle);
const savedSubtitleSize = typeof window !== 'undefined' ? localStorage.getItem('subtitleSize') || '2em' : '2em';
currentSubtitleLabelRef.current = currentSubtitles[0]?.label || '关闭';
currentSubtitleLabelRef.current = defaultSubtitle?.label || '关闭';
artPlayerRef.current = new Artplayer({
container: artRef.current!,
@@ -6598,9 +6713,9 @@ function PlayPageClient() {
aspectRatio: false,
fullscreen: !isIOS, // iOS 禁用原生全屏按钮,避免触发系统播放器
fullscreenWeb: true, // 保留网页全屏按钮(所有平台)
...(currentSubtitles.length > 0 ? {
...(shouldUseNativeInitialSubtitle ? {
subtitle: {
url: currentSubtitles[0].url,
url: defaultSubtitle!.url,
type: 'vtt',
style: {
color: '#fff',
@@ -7715,8 +7830,22 @@ function PlayPageClient() {
applyProgressThumbConfig();
// 添加字幕切换和本地字幕上传功能
updateSubtitleSetting();
// 添加字幕切换和本地字幕上传功能ASS/SSA 需要播放器 ready 后挂载 JASSUB
const readySubtitles = (detailRef.current?.subtitles?.[currentEpisodeIndexRef.current] || []) as SourceSubtitleItem[];
const readyDefaultSubtitle = readySubtitles[0];
if (readyDefaultSubtitle && isAdvancedSourceSubtitle(readyDefaultSubtitle)) {
void switchSourceSubtitle(readyDefaultSubtitle)
.catch((error) => {
console.warn('[Subtitle] 高级字幕自动加载失败:', error);
if (artPlayerRef.current) {
artPlayerRef.current.subtitle.show = false;
}
currentSubtitleLabelRef.current = '关闭';
})
.finally(updateSubtitleSetting);
} else {
updateSubtitleSetting();
}
// 添加字幕大小设置
if (artPlayerRef.current) {