Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ddde4c650 |
@@ -2,6 +2,9 @@
|
||||
* 前端预览播放器
|
||||
* 用原生 <video> 标签按时间线播放素材片段
|
||||
* 替代后端 FFmpeg 渲染预览,实现真正的实时预览
|
||||
*
|
||||
* 注意:本组件不创建 .xx-preview-video 容器(由父组件 PreviewVideoPanel 提供)
|
||||
* 避免嵌套 .xx-preview-video 导致 CSS 冲突
|
||||
*/
|
||||
import React, { useMemo, useCallback, useState, useRef, useEffect } from "react"
|
||||
import { PlayCircleOutlined, PauseCircleOutlined, SoundOutlined } from "@ant-design/icons"
|
||||
@@ -41,9 +44,7 @@ function buildPlaybackSegments(
|
||||
const segments: PlaybackSegment[] = []
|
||||
|
||||
assets.forEach((asset, i) => {
|
||||
// 获取素材时长(从 metadata 或顶层字段)
|
||||
const assetDuration = asset.duration || asset.metadata?.duration || 30
|
||||
// 模板片段的时长约束
|
||||
const tplSeg = templateSegments[i] || templateSegments[templateSegments.length - 1]
|
||||
const segDuration = tplSeg
|
||||
? Math.min(tplSeg.duration_max, Math.max(tplSeg.duration_min, assetDuration))
|
||||
@@ -54,9 +55,8 @@ function buildPlaybackSegments(
|
||||
|
||||
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}`,
|
||||
`[buildPlaybackSegments] 片段 ${i}: assetId=${asset.id}, videoUrl=${videoUrl?.substring(0, 80)}, file_url=${!!asset.file_url}`,
|
||||
)
|
||||
|
||||
segments.push({
|
||||
@@ -77,7 +77,6 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
|
||||
videoRatio,
|
||||
ready,
|
||||
}) => {
|
||||
// 构建播放片段
|
||||
const segments = useMemo(() => buildPlaybackSegments(assets, template), [assets, template])
|
||||
|
||||
const {
|
||||
@@ -131,13 +130,23 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
|
||||
}
|
||||
}, [isDragging, totalDuration, seekTo])
|
||||
|
||||
const videoAspectStyle = { aspectRatio: (videoRatio || "16:9").replace(":", "/") }
|
||||
const progressPercent = totalDuration > 0 ? (currentTime / totalDuration) * 100 : 0
|
||||
|
||||
// 未就绪状态
|
||||
if (!ready || !assets.length) {
|
||||
return (
|
||||
<div className="xx-preview-empty">
|
||||
<div
|
||||
className="xx-preview-empty"
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<SoundOutlined style={{ fontSize: 48, color: "var(--text-tertiary)", marginBottom: 12 }} />
|
||||
<p className="xx-preview-empty-title">准备预览素材...</p>
|
||||
<p className="xx-preview-empty-desc">加载素材后即可预览播放</p>
|
||||
@@ -148,7 +157,18 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
|
||||
// 无播放片段
|
||||
if (!canPlay) {
|
||||
return (
|
||||
<div className="xx-preview-empty">
|
||||
<div
|
||||
className="xx-preview-empty"
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<PlayCircleOutlined
|
||||
style={{ fontSize: 48, color: "var(--text-tertiary)", marginBottom: 12 }}
|
||||
/>
|
||||
@@ -159,76 +179,84 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="xx-frontend-preview-player">
|
||||
{/* 视频区域 */}
|
||||
<div className="xx-preview-video" style={{ ...videoAspectStyle, position: "relative" }}>
|
||||
<video
|
||||
ref={videoRef}
|
||||
preload="auto"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "contain",
|
||||
background: "#000",
|
||||
}}
|
||||
playsInline
|
||||
/>
|
||||
<>
|
||||
{/* ✅ 不再创建 .xx-preview-video 容器,直接渲染 video + 覆盖层 */}
|
||||
{/* video 元素 — 由父组件 .xx-preview-video 容器提供定位和尺寸 */}
|
||||
<video
|
||||
ref={videoRef}
|
||||
preload="auto"
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "contain",
|
||||
background: "#000",
|
||||
zIndex: 1,
|
||||
}}
|
||||
playsInline
|
||||
/>
|
||||
|
||||
{/* 播放/暂停按钮覆盖 */}
|
||||
{!isPlaying && (
|
||||
<button
|
||||
className="xx-preview-play-btn"
|
||||
onClick={togglePlayPause}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
background: "rgba(0,0,0,0.5)",
|
||||
border: "none",
|
||||
borderRadius: "50%",
|
||||
width: 56,
|
||||
height: 56,
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "#fff",
|
||||
fontSize: 28,
|
||||
zIndex: 10,
|
||||
transition: "opacity 0.2s",
|
||||
}}
|
||||
>
|
||||
{isEnded ? <PlayCircleOutlined /> : <PlayCircleOutlined />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 当前片段指示器 */}
|
||||
<div
|
||||
{/* 播放按钮覆盖层 */}
|
||||
{!isPlaying && (
|
||||
<button
|
||||
className="xx-preview-play-btn"
|
||||
onClick={togglePlayPause}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
left: 8,
|
||||
background: "rgba(0,0,0,0.6)",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
background: "rgba(0,0,0,0.5)",
|
||||
border: "none",
|
||||
borderRadius: "50%",
|
||||
width: 56,
|
||||
height: 56,
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "#fff",
|
||||
fontSize: 11,
|
||||
padding: "2px 8px",
|
||||
borderRadius: 4,
|
||||
fontSize: 28,
|
||||
zIndex: 10,
|
||||
transition: "opacity 0.2s",
|
||||
}}
|
||||
>
|
||||
片段 {currentSegmentIndex + 1}/{segments.length}
|
||||
</div>
|
||||
<PlayCircleOutlined />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 片段指示器 */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
left: 8,
|
||||
background: "rgba(0,0,0,0.6)",
|
||||
color: "#fff",
|
||||
fontSize: 11,
|
||||
padding: "2px 8px",
|
||||
borderRadius: 4,
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
片段 {currentSegmentIndex + 1}/{segments.length}
|
||||
</div>
|
||||
|
||||
{/* 控制条 */}
|
||||
{/* 控制条 — 绝对定位在底部 */}
|
||||
<div
|
||||
className="xx-preview-controls"
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
padding: "8px 0",
|
||||
padding: "8px 12px",
|
||||
background: "linear-gradient(transparent, rgba(0,0,0,0.6))",
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
{/* 播放/暂停 */}
|
||||
@@ -237,7 +265,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: "var(--text-primary, #fff)",
|
||||
color: "#fff",
|
||||
fontSize: 18,
|
||||
cursor: "pointer",
|
||||
padding: 4,
|
||||
@@ -252,7 +280,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
|
||||
<span
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "var(--text-secondary, #999)",
|
||||
color: "rgba(255,255,255,0.8)",
|
||||
minWidth: 80,
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
}}
|
||||
@@ -282,7 +310,6 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
|
||||
transition: isDragging ? "none" : "width 0.1s linear",
|
||||
}}
|
||||
/>
|
||||
{/* 进度指示点 */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
@@ -300,7 +327,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
* 架构改造:完全去除后端 FFmpeg 预览依赖
|
||||
* - 使用 FrontendPreviewPlayer 直接播放素材片段
|
||||
* - TitleOverlay CSS 层实时响应标题样式变化
|
||||
*
|
||||
* 布局:本组件提供 .xx-preview-video 容器(position: relative + overflow: hidden)
|
||||
* FrontendPreviewPlayer 的内容通过 absolute 定位填充容器
|
||||
* TitleOverlay 通过 absolute 定位 + z-index: 30 覆盖在最上层
|
||||
*/
|
||||
import React, { useMemo } from "react"
|
||||
import { LoadingOutlined } from "@ant-design/icons"
|
||||
@@ -104,6 +108,7 @@ function buildTitleStyle(settings: TitleSettings): React.CSSProperties {
|
||||
/**
|
||||
* CSS 标题预览覆盖层
|
||||
* 始终渲染:有标题显示标题,无标题显示占位文本"标题预览"
|
||||
* z-index: 20(在视频 z-index:1 和控制条 z-index:10 之上)
|
||||
*/
|
||||
const TitleOverlay: React.FC<{ titleSettings: TitleSettings }> = ({ titleSettings }) => {
|
||||
const positionStyle = useMemo(
|
||||
@@ -130,12 +135,12 @@ const TitleOverlay: React.FC<{ titleSettings: TitleSettings }> = ({ titleSetting
|
||||
|
||||
return (
|
||||
<div
|
||||
className="xx-preview-title-overlay"
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
zIndex: 30,
|
||||
zIndex: 20,
|
||||
pointerEvents: "none",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -170,10 +175,11 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
|
||||
{assetsReady && assets.length > 0 && <span className="xx-preview-badge">实时预览</span>}
|
||||
</div>
|
||||
|
||||
{/* 预览容器 — 标题叠加层始终渲染在容器顶层,不受加载状态影响 */}
|
||||
{/* ✅ 预览容器 — 唯一的 .xx-preview-video 容器
|
||||
内部所有内容(视频、控制条、标题叠加层)通过 absolute 定位填充 */}
|
||||
<div className="xx-preview-video" style={{ ...videoAspectStyle, position: "relative" }}>
|
||||
{/* 内容层:加载中 or 前端播放器 */}
|
||||
{assetsLoading ? (
|
||||
{/* 加载中状态 */}
|
||||
{assetsLoading && (
|
||||
<div
|
||||
className="xx-preview-loading-center"
|
||||
style={{
|
||||
@@ -183,7 +189,7 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 1,
|
||||
zIndex: 5,
|
||||
}}
|
||||
>
|
||||
<LoadingOutlined style={{ fontSize: 36, color: "#fff" }} spin />
|
||||
@@ -191,18 +197,17 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
|
||||
加载素材中...
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<FrontendPreviewPlayer
|
||||
assets={assets}
|
||||
template={template}
|
||||
videoRatio={videoRatio}
|
||||
ready={assetsReady}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* CSS 标题实时预览层 — 与 FFmpeg ASS 渲染坐标对齐
|
||||
始终渲染在内容层之上(z-index: 30)
|
||||
有标题显示标题,无标题显示"标题预览"占位文本 */}
|
||||
{/* 前端播放器(视频 + 控制条 + 播放按钮)*/}
|
||||
<FrontendPreviewPlayer
|
||||
assets={assets}
|
||||
template={template}
|
||||
videoRatio={videoRatio}
|
||||
ready={assetsReady}
|
||||
/>
|
||||
|
||||
{/* CSS 标题实时预览层 — z-index: 20,始终渲染在内容层之上 */}
|
||||
{titleSettings && <TitleOverlay titleSettings={titleSettings} />}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -91,6 +91,10 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
|
||||
const rafRef = useRef<number>(0)
|
||||
const isSeekingRef = useRef(false)
|
||||
|
||||
// ✅ 关键修复:用 ref 标记是否已加载过片段(替代检查 video.src)
|
||||
// video.src = "" 在浏览器中会被解析为页面 URL,导致 "video.src === ''" 永远为 false
|
||||
const srcLoadedRef = useRef(false)
|
||||
|
||||
// 预加载用的隐藏 video 元素
|
||||
const preloadVideoRef = useRef<HTMLVideoElement | null>(null)
|
||||
|
||||
@@ -122,7 +126,6 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
|
||||
const seg = segments[index]
|
||||
const videoUrl = seg.videoUrl
|
||||
|
||||
// 调试日志:检查 videoUrl 是否有效
|
||||
if (!videoUrl || videoUrl === "") {
|
||||
console.error(
|
||||
`[useSegmentScheduler] 片段 ${index} 的 videoUrl 为空!assetId: ${seg.assetId}`,
|
||||
@@ -133,33 +136,80 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
|
||||
|
||||
// 检查是否是完整的 HTTP URL
|
||||
if (!videoUrl.startsWith("http://") && !videoUrl.startsWith("https://")) {
|
||||
console.warn(
|
||||
`[useSegmentScheduler] 片段 ${index} 的 videoUrl 不是完整 URL: ${videoUrl},可能导致加载失败`,
|
||||
)
|
||||
console.warn(`[useSegmentScheduler] 片段 ${index} 的 videoUrl 不是完整 URL: ${videoUrl}`)
|
||||
}
|
||||
|
||||
video.src = videoUrl
|
||||
|
||||
const localTime = seekToLocalTime ?? seg.startTime
|
||||
// 等待 src 设置后再设置 currentTime 并 resolve
|
||||
|
||||
// ✅ 关键修复:清除旧事件监听,防止重复绑定
|
||||
const cleanup = () => {
|
||||
video.removeEventListener("loadedmetadata", onLoaded)
|
||||
video.removeEventListener("error", onError)
|
||||
video.removeEventListener("canplay", onCanPlay)
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
|
||||
const onLoaded = () => {
|
||||
video.currentTime = localTime
|
||||
video.removeEventListener("loadedmetadata", onLoaded)
|
||||
srcLoadedRef.current = true
|
||||
cleanup()
|
||||
console.log(
|
||||
`[useSegmentScheduler] 片段 ${index} loadedmetadata, src=${videoUrl.substring(0, 80)}`,
|
||||
)
|
||||
resolve()
|
||||
}
|
||||
|
||||
const onCanPlay = () => {
|
||||
video.currentTime = localTime
|
||||
srcLoadedRef.current = true
|
||||
cleanup()
|
||||
console.log(
|
||||
`[useSegmentScheduler] 片段 ${index} canplay, src=${videoUrl.substring(0, 80)}`,
|
||||
)
|
||||
resolve()
|
||||
}
|
||||
video.addEventListener("loadedmetadata", onLoaded)
|
||||
|
||||
// 监听加载错误
|
||||
const onError = () => {
|
||||
console.error(
|
||||
`[useSegmentScheduler] 视频加载失败: ${videoUrl}`,
|
||||
video.error?.message || "unknown error",
|
||||
video.error?.message || `code=${video.error?.code}`,
|
||||
)
|
||||
video.removeEventListener("error", onError)
|
||||
resolve() // 即使失败也 resolve,让调用方继续处理
|
||||
srcLoadedRef.current = true // 标记为已尝试,避免死循环
|
||||
cleanup()
|
||||
resolve()
|
||||
}
|
||||
|
||||
// ✅ 关键修复:10秒超时防止 Promise 永远不 resolve
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.warn(
|
||||
`[useSegmentScheduler] 片段 ${index} 加载超时 (10s), readyState=${video.readyState}`,
|
||||
)
|
||||
cleanup()
|
||||
resolve()
|
||||
}, 10000)
|
||||
|
||||
// 先绑定事件,再设置 src(避免错过已触发的loadedmetadata)
|
||||
video.addEventListener("loadedmetadata", onLoaded)
|
||||
video.addEventListener("canplay", onCanPlay)
|
||||
video.addEventListener("error", onError)
|
||||
|
||||
// 设置 src 并显式调用 load()
|
||||
video.src = videoUrl
|
||||
video.load()
|
||||
|
||||
// ✅ 关键修复:如果 readyState >= 1(已有 metadata),说明加载已完成
|
||||
// 直接设置 currentTime 并 resolve,不等待事件
|
||||
if (video.readyState >= 1) {
|
||||
video.currentTime = localTime
|
||||
srcLoadedRef.current = true
|
||||
cleanup()
|
||||
console.log(
|
||||
`[useSegmentScheduler] 片段 ${index} 已缓存(readyState=${video.readyState}), 直接播放`,
|
||||
)
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
setCurrentSegmentIndex(index)
|
||||
|
||||
// 预加载下一段
|
||||
@@ -191,13 +241,11 @@ 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)
|
||||
setCurrentTime(accumulatedTime)
|
||||
} else {
|
||||
// 播放结束
|
||||
video.pause()
|
||||
setIsPlaying(false)
|
||||
setIsEnded(true)
|
||||
@@ -205,7 +253,6 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 更新全局时间
|
||||
const globalTime =
|
||||
(timelineStarts[currentSegmentIndex] || 0) + (video.currentTime - seg.startTime)
|
||||
setCurrentTime(Math.max(0, Math.min(globalTime, totalDuration)))
|
||||
@@ -217,25 +264,41 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
|
||||
/** 播放 */
|
||||
const play = useCallback(async () => {
|
||||
const video = videoRef.current
|
||||
if (!video || !canPlay) return
|
||||
if (!video || !canPlay) {
|
||||
console.log("[useSegmentScheduler] play: video=", !!video, "canPlay=", canPlay)
|
||||
return
|
||||
}
|
||||
|
||||
setIsEnded(false)
|
||||
|
||||
// 如果还没设置 src(首次播放),先加载第一段并等待
|
||||
if (!video.src || video.src === "") {
|
||||
// ✅ 关键修复:用 ref 标记代替 video.src 检查
|
||||
if (!srcLoadedRef.current) {
|
||||
console.log("[useSegmentScheduler] 首次播放,加载片段 0...")
|
||||
await switchToSegment(0)
|
||||
// switchToSegment 会等待 loadedmetadata
|
||||
}
|
||||
|
||||
try {
|
||||
await video.play()
|
||||
console.log("[useSegmentScheduler] 播放开始")
|
||||
const playPromise = video.play()
|
||||
if (playPromise !== undefined) {
|
||||
await playPromise
|
||||
}
|
||||
console.log("[useSegmentScheduler] 播放开始, src=", video.src?.substring(0, 80))
|
||||
setIsPlaying(true)
|
||||
rafRef.current = requestAnimationFrame(tick)
|
||||
} catch (err) {
|
||||
// 自动播放可能被浏览器阻止,忽略
|
||||
console.warn("[useSegmentScheduler] 播放失败:", err)
|
||||
console.warn("[useSegmentScheduler] 播放失败:", err, "src=", video.src?.substring(0, 80))
|
||||
// 如果播放失败,尝试重新加载片段
|
||||
if (!srcLoadedRef.current) {
|
||||
srcLoadedRef.current = false
|
||||
await switchToSegment(0)
|
||||
try {
|
||||
await video.play()
|
||||
setIsPlaying(true)
|
||||
rafRef.current = requestAnimationFrame(tick)
|
||||
} catch (retryErr) {
|
||||
console.error("[useSegmentScheduler] 重试播放仍然失败:", retryErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [canPlay, switchToSegment, tick])
|
||||
|
||||
@@ -255,10 +318,11 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
|
||||
if (isEnded) {
|
||||
// 播放结束后再次播放,从头开始
|
||||
setIsEnded(false)
|
||||
srcLoadedRef.current = false // ✅ 重置标记,强制重新加载
|
||||
switchToSegment(0, segments[0]?.startTime).then(() => {
|
||||
const video = videoRef.current
|
||||
if (video) {
|
||||
video.play().catch(() => {})
|
||||
video.play().catch((e) => console.warn("[useSegmentScheduler] restart play failed:", e))
|
||||
setIsPlaying(true)
|
||||
setCurrentTime(0)
|
||||
rafRef.current = requestAnimationFrame(tick)
|
||||
@@ -281,7 +345,6 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
|
||||
|
||||
isSeekingRef.current = true
|
||||
|
||||
// 如果片段变了,需要切换 src
|
||||
if (index !== currentSegmentIndex) {
|
||||
await switchToSegment(index, localTime)
|
||||
} else {
|
||||
@@ -292,7 +355,6 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
|
||||
setCurrentSegmentIndex(index)
|
||||
setIsEnded(false)
|
||||
|
||||
// 延迟恢复 tick 检测
|
||||
setTimeout(() => {
|
||||
isSeekingRef.current = false
|
||||
}, 200)
|
||||
@@ -318,10 +380,12 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
|
||||
setCurrentTime(0)
|
||||
setCurrentSegmentIndex(0)
|
||||
setIsEnded(false)
|
||||
srcLoadedRef.current = false // ✅ 重置标记
|
||||
const video = videoRef.current
|
||||
if (video) {
|
||||
video.pause()
|
||||
video.src = ""
|
||||
video.removeAttribute("src")
|
||||
video.load()
|
||||
}
|
||||
}, [segments])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user