fix: 修复预览播放器视频无法播放和标题不显示 #1423
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user