fix: 修复预览播放器视频无法播放和标题不显示 #1423

Closed
xiaoxia wants to merge 2 commits from fix/preview-player-playback-and-title into develop
3 changed files with 90 additions and 48 deletions
@@ -52,9 +52,16 @@ function buildPlaybackSegments(
const startTime = 0
const endTime = Math.min(startTime + segDuration, assetDuration)
const videoUrl = asset.file_url || asset.storage_key
// 调试日志:打印每个片段的 videoUrl
console.log(
`[buildPlaybackSegments] 片段 ${i}: assetId=${asset.id}, videoUrl=${videoUrl}, file_url=${asset.file_url}, storage_key=${asset.storage_key}`,
)
segments.push({
assetId: asset.id,
videoUrl: asset.file_url || asset.storage_key,
videoUrl,
startTime,
endTime,
order: i,
@@ -103,6 +103,7 @@ function buildTitleStyle(settings: TitleSettings): React.CSSProperties {
/**
* CSS 标题预览覆盖层
* 始终渲染:有标题显示标题,无标题显示占位文本"标题预览"
*/
const TitleOverlay: React.FC<{ titleSettings: TitleSettings }> = ({ titleSettings }) => {
const positionStyle = useMemo(
@@ -124,7 +125,8 @@ const TitleOverlay: React.FC<{ titleSettings: TitleSettings }> = ({ titleSetting
],
)
if (!titleSettings.title?.trim()) return null
// 标题为空时显示占位文本
const displayTitle = titleSettings.title?.trim() || "标题预览"
return (
<div
@@ -143,7 +145,7 @@ const TitleOverlay: React.FC<{ titleSettings: TitleSettings }> = ({ titleSetting
position: "absolute",
}}
>
{titleSettings.title}
{displayTitle}
</div>
</div>
)
@@ -160,7 +162,6 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
titleSettings,
}) => {
const videoAspectStyle = { aspectRatio: (videoRatio || "16:9").replace(":", "/") }
const hasTitle = !!titleSettings?.title?.trim()
return (
<div className="xx-generate-preview">
@@ -200,8 +201,9 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
)}
{/* CSS 标题实时预览层 — 与 FFmpeg ASS 渲染坐标对齐
始终渲染在内容层之上(z-index: 30),只要 titleSettings 有标题文字就显示 */}
{hasTitle && <TitleOverlay titleSettings={titleSettings!} />}
始终渲染在内容层之上(z-index: 30
有标题显示标题,无标题显示"标题预览"占位文本 */}
{titleSettings && <TitleOverlay titleSettings={titleSettings} />}
</div>
{/* 素材信息 */}
@@ -109,34 +109,69 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
? currentTime - (timelineStarts[currentSegmentIndex] || 0) + currentSegment.startTime
: 0
/** 切换到指定片段 */
/** 切换到指定片段,返回 Promise 在视频可播放后 resolve */
const switchToSegment = useCallback(
(index: number, seekToLocalTime?: number) => {
const video = videoRef.current
if (!video || index >= segments.length) return
const seg = segments[index]
video.src = seg.videoUrl
const localTime = seekToLocalTime ?? seg.startTime
// 等待 src 设置后再设置 currentTime
const onLoaded = () => {
video.currentTime = localTime
video.removeEventListener("loadedmetadata", onLoaded)
}
video.addEventListener("loadedmetadata", onLoaded)
setCurrentSegmentIndex(index)
// 预加载下一段
if (index + 1 < segments.length) {
const nextSeg = segments[index + 1]
if (!preloadVideoRef.current) {
preloadVideoRef.current = document.createElement("video")
preloadVideoRef.current.preload = "auto"
(index: number, seekToLocalTime?: number): Promise<void> => {
return new Promise((resolve) => {
const video = videoRef.current
if (!video || index >= segments.length) {
resolve()
return
}
preloadVideoRef.current.src = nextSeg.videoUrl
}
const seg = segments[index]
const videoUrl = seg.videoUrl
// 调试日志:检查 videoUrl 是否有效
if (!videoUrl || videoUrl === "") {
console.error(
`[useSegmentScheduler] 片段 ${index} 的 videoUrl 为空!assetId: ${seg.assetId}`,
)
resolve()
return
}
// 检查是否是完整的 HTTP URL
if (!videoUrl.startsWith("http://") && !videoUrl.startsWith("https://")) {
console.warn(
`[useSegmentScheduler] 片段 ${index} 的 videoUrl 不是完整 URL: ${videoUrl},可能导致加载失败`,
)
}
video.src = videoUrl
const localTime = seekToLocalTime ?? seg.startTime
// 等待 src 设置后再设置 currentTime 并 resolve
const onLoaded = () => {
video.currentTime = localTime
video.removeEventListener("loadedmetadata", onLoaded)
resolve()
}
video.addEventListener("loadedmetadata", onLoaded)
// 监听加载错误
const onError = () => {
console.error(
`[useSegmentScheduler] 视频加载失败: ${videoUrl}`,
video.error?.message || "unknown error",
)
video.removeEventListener("error", onError)
resolve() // 即使失败也 resolve,让调用方继续处理
}
video.addEventListener("error", onError)
setCurrentSegmentIndex(index)
// 预加载下一段
if (index + 1 < segments.length) {
const nextSeg = segments[index + 1]
if (!preloadVideoRef.current) {
preloadVideoRef.current = document.createElement("video")
preloadVideoRef.current.preload = "auto"
}
preloadVideoRef.current.src = nextSeg.videoUrl
}
})
},
[segments],
)
@@ -156,7 +191,7 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
if (video.currentTime >= seg.endTime - 0.15) {
const nextIndex = currentSegmentIndex + 1
if (nextIndex < segments.length) {
// 切到下一段
// 切到下一段(不等待,直接继续 tick
switchToSegment(nextIndex)
const accumulatedTime =
(timelineStarts[currentSegmentIndex] || 0) + (seg.endTime - seg.startTime)
@@ -186,18 +221,21 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
setIsEnded(false)
// 如果还没设置 src(首次播放),先加载第一段
// 如果还没设置 src(首次播放),先加载第一段并等待
if (!video.src || video.src === "") {
switchToSegment(0)
console.log("[useSegmentScheduler] 首次播放,加载片段 0...")
await switchToSegment(0)
// switchToSegment 会等待 loadedmetadata
}
try {
await video.play()
console.log("[useSegmentScheduler] 播放开始")
setIsPlaying(true)
rafRef.current = requestAnimationFrame(tick)
} catch {
} catch (err) {
// 自动播放可能被浏览器阻止,忽略
console.warn("[useSegmentScheduler] auto-play blocked by browser")
console.warn("[useSegmentScheduler] 播放失败:", err)
}
}, [canPlay, switchToSegment, tick])
@@ -217,18 +255,15 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
if (isEnded) {
// 播放结束后再次播放,从头开始
setIsEnded(false)
switchToSegment(0, segments[0]?.startTime)
const video = videoRef.current
if (video) {
const onSeeked = () => {
switchToSegment(0, segments[0]?.startTime).then(() => {
const video = videoRef.current
if (video) {
video.play().catch(() => {})
setIsPlaying(true)
setCurrentTime(0)
rafRef.current = requestAnimationFrame(tick)
video.removeEventListener("seeked", onSeeked)
}
video.addEventListener("seeked", onSeeked)
}
})
} else {
play()
}
@@ -237,7 +272,7 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
/** 跳转到指定全局时间 */
const seekTo = useCallback(
(time: number) => {
async (time: number) => {
const video = videoRef.current
if (!video || !canPlay) return
@@ -248,9 +283,7 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
// 如果片段变了,需要切换 src
if (index !== currentSegmentIndex) {
switchToSegment(index, localTime)
// switchToSegment 会设置 src 并在 loadedmetadata 后设置 currentTime
// 所以这里不需要再设置
await switchToSegment(index, localTime)
} else {
video.currentTime = localTime
}