emby支持高级字幕
This commit is contained in:
+149
-20
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user