diff --git a/apps/web/src/pages/generate/components/FrontendPreviewPlayer.tsx b/apps/web/src/pages/generate/components/FrontendPreviewPlayer.tsx index 022a77544..b70f2d9fb 100644 --- a/apps/web/src/pages/generate/components/FrontendPreviewPlayer.tsx +++ b/apps/web/src/pages/generate/components/FrontendPreviewPlayer.tsx @@ -134,6 +134,14 @@ const FrontendPreviewPlayer: React.FC = ({ prevIsPlayingRef.current = isPlaying }, [isPlaying, currentTime]) + // 片段切换时同步音频 — 将 audio.currentTime 对齐到视频全局时间 + useEffect(() => { + const audio = audioRef.current + if (!audio || !audio.src || !isPlaying) return + // 用视频当前的全局时间对齐音频 + audio.currentTime = currentTime + }, [currentSegmentIndex]) + // seek 时同步音频 const handleSeekTo = useCallback( (time: number) => { diff --git a/apps/web/src/pages/generate/components/PreviewVideoPanel.tsx b/apps/web/src/pages/generate/components/PreviewVideoPanel.tsx index d4677e80f..65331cfd4 100644 --- a/apps/web/src/pages/generate/components/PreviewVideoPanel.tsx +++ b/apps/web/src/pages/generate/components/PreviewVideoPanel.tsx @@ -11,7 +11,7 @@ * FrontendPreviewPlayer 的内容通过 absolute 定位填充容器 * TitleOverlay 通过 absolute 定位 + z-index: 30 覆盖在最上层 */ -import React, { useMemo } from "react" +import React, { useMemo, useRef, useState, useEffect } from "react" import { LoadingOutlined } from "@ant-design/icons" import type { AssetItem } from "@/api/assets" import type { EditingTemplate } from "@/api/editing-planner" @@ -79,12 +79,16 @@ function getPositionStyle(position: string): React.CSSProperties { * 构建 CSS 标题层的样式 * 所有渲染参数与后端 FFmpeg ASS 字幕一致 */ -function buildTitleStyle(settings: TitleSettings): React.CSSProperties { - const fontSizePercent = (Math.min(settings.size, 36) / ASS_VIDEO_HEIGHT) * 100 +function buildTitleStyle(settings: TitleSettings, containerHeight: number): React.CSSProperties { + // 用 px 计算 fontSize,不再依赖父元素 font-size 的百分比 + const fontSizePx = + containerHeight > 0 + ? (Math.min(settings.size, 36) / ASS_VIDEO_HEIGHT) * containerHeight + : (Math.min(settings.size, 36) / ASS_VIDEO_HEIGHT) * 400 // fallback const base: React.CSSProperties = { fontFamily: settings.font || "思源黑体", - fontSize: `${fontSizePercent}%`, + fontSize: `${fontSizePx}px`, color: settings.color || "#ffffff", fontWeight: settings.bold ? 700 : 400, fontStyle: settings.italic ? "italic" : "normal", @@ -113,15 +117,34 @@ function buildTitleStyle(settings: TitleSettings): React.CSSProperties { * z-index: 20(在视频 z-index:1 和控制条 z-index:10 之上) */ const TitleOverlay: React.FC<{ titleSettings: TitleSettings }> = ({ titleSettings }) => { + const containerRef = useRef(null) + const [containerHeight, setContainerHeight] = useState(400) // fallback + + // ResizeObserver 获取容器实际高度 + useEffect(() => { + const el = containerRef.current + if (!el) return + const ro = new ResizeObserver((entries) => { + for (const entry of entries) { + const h = entry.contentRect.height + if (h > 0) setContainerHeight(h) + } + }) + ro.observe(el) + // 初始化也读一次 + const rect = el.getBoundingClientRect() + if (rect.height > 0) setContainerHeight(rect.height) + return () => ro.disconnect() + }, []) + const positionStyle = useMemo( () => getPositionStyle(titleSettings.position), [titleSettings.position], ) - // eslint-disable-next-line react-hooks/exhaustive-deps const titleStyle = useMemo( - () => buildTitleStyle(titleSettings), - // eslint-disable-next-line react-hooks/exhaustive-deps + () => buildTitleStyle(titleSettings, containerHeight), [ + containerHeight, titleSettings.font, titleSettings.size, titleSettings.color, @@ -132,11 +155,11 @@ const TitleOverlay: React.FC<{ titleSettings: TitleSettings }> = ({ titleSetting ], ) - // 标题为空时显示占位文本 const displayTitle = titleSettings.title?.trim() || "标题预览" return (
= 1(已有 metadata),说明加载已完成 + // ✅ 关键修复:如果 readyState >= 2(HAVE_CURRENT_DATA),说明已有帧数据 // 直接设置 currentTime 并 resolve,不等待事件 - if (video.readyState >= 1) { + if (video.readyState >= 2) { video.currentTime = localTime srcLoadedRef.current = true cleanup()