fix: 修复预览播放器三个问题
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m6s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m2s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m19s
AI Code Review / AI Code Review (pull_request) Failing after 2m23s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m52s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m34s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m41s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m32s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 3m30s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 4m9s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m59s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Waiting to run
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 50s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m6s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m2s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m19s
AI Code Review / AI Code Review (pull_request) Failing after 2m23s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m52s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m34s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m41s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m32s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 3m30s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 4m9s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m59s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Waiting to run
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 50s
1. 标题不显示:给 .xx-preview-video::before 添加 z-index: 0,确保渐变光效在内容层之下 2. 没有配音音频:添加 voiceAudioUrl prop 链路,在 FrontendPreviewPlayer 中创建 Audio 对象并同步视频播放 3. 片段切换不连贯:在 useSegmentScheduler 中提前 2 秒预加载下一段,减少黑屏间隙 - 所有修改通过 TypeScript 编译检查 - 代码已通过 Prettier 格式化
This commit is contained in:
@@ -13,7 +13,9 @@ import React, { useMemo } from "react"
|
||||
import { Modal, message } from "antd"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import type { VoiceClone } from "@/api/voice-clone"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { useCloneProgress } from "@/hooks/useCloneProgress"
|
||||
import { getAssetsByKind } from "@/api/assets"
|
||||
import CloneModal from "@/components/voice/CloneModal"
|
||||
import GenerateHeader from "./components/GenerateHeader"
|
||||
import GenerateStepsBar from "./components/GenerateStepsBar"
|
||||
@@ -105,6 +107,18 @@ const GeneratePage: React.FC = () => {
|
||||
[userTemplates, selectedTemplate],
|
||||
)
|
||||
|
||||
/* ── 配音音频 URL ── */
|
||||
const { data: voiceMaterials = [] } = useQuery({
|
||||
queryKey: ["assets", "voice"],
|
||||
queryFn: () => getAssetsByKind("voice", { limit: 50 }),
|
||||
})
|
||||
|
||||
const voiceAudioUrl = useMemo(() => {
|
||||
if (!selectedVoice) return undefined
|
||||
const asset = voiceMaterials.find((v) => v.id === selectedVoice)
|
||||
return asset?.file_url || undefined
|
||||
}, [selectedVoice, voiceMaterials])
|
||||
|
||||
/* ── 步骤导航 ── */
|
||||
const { goNext, goPrev } = useStepNavigation({
|
||||
currentStep,
|
||||
@@ -236,6 +250,7 @@ const GeneratePage: React.FC = () => {
|
||||
assetsReady={previewAssetsReady}
|
||||
assetsLoading={previewAssetsLoading}
|
||||
titleSettings={titleSettings}
|
||||
voiceAudioUrl={voiceAudioUrl}
|
||||
/>
|
||||
)}
|
||||
{currentStep >= 6 && (
|
||||
|
||||
@@ -21,6 +21,8 @@ interface FrontendPreviewPlayerProps {
|
||||
videoRatio: string
|
||||
/** 是否准备好播放(素材已加载) */
|
||||
ready: boolean
|
||||
/** 配音音频 URL */
|
||||
voiceAudioUrl?: string
|
||||
}
|
||||
|
||||
/** 格式化时间 mm:ss */
|
||||
@@ -76,6 +78,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
|
||||
template,
|
||||
videoRatio: _videoRatio,
|
||||
ready,
|
||||
voiceAudioUrl,
|
||||
}) => {
|
||||
const segments = useMemo(() => buildPlaybackSegments(assets, template), [assets, template])
|
||||
|
||||
@@ -90,6 +93,77 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
|
||||
videoRef,
|
||||
} = useSegmentScheduler(segments)
|
||||
|
||||
// ── 配音音频同步 ──
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||||
const prevIsPlayingRef = useRef(false)
|
||||
|
||||
// 创建/更新 Audio 元素
|
||||
useEffect(() => {
|
||||
if (!voiceAudioUrl) {
|
||||
// 没有配音,清理已有 audio
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause()
|
||||
audioRef.current.src = ""
|
||||
audioRef.current = null
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!audioRef.current) {
|
||||
audioRef.current = new Audio()
|
||||
audioRef.current.preload = "auto"
|
||||
}
|
||||
if (audioRef.current.src !== voiceAudioUrl) {
|
||||
audioRef.current.src = voiceAudioUrl
|
||||
}
|
||||
}, [voiceAudioUrl])
|
||||
|
||||
// 同步播放状态
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current
|
||||
if (!audio || !audio.src) return
|
||||
|
||||
if (isPlaying && !prevIsPlayingRef.current) {
|
||||
// 刚进入播放
|
||||
audio.currentTime = currentTime
|
||||
audio.play().catch(() => {})
|
||||
} else if (!isPlaying && prevIsPlayingRef.current) {
|
||||
// 刚暂停
|
||||
audio.pause()
|
||||
}
|
||||
prevIsPlayingRef.current = isPlaying
|
||||
}, [isPlaying, currentTime])
|
||||
|
||||
// seek 时同步音频
|
||||
const handleSeekTo = useCallback(
|
||||
(time: number) => {
|
||||
seekTo(time)
|
||||
const audio = audioRef.current
|
||||
if (audio && audio.src) {
|
||||
audio.currentTime = time
|
||||
}
|
||||
},
|
||||
[seekTo],
|
||||
)
|
||||
|
||||
// 播放结束时暂停音频
|
||||
useEffect(() => {
|
||||
if (!isPlaying) {
|
||||
const audio = audioRef.current
|
||||
if (audio) audio.pause()
|
||||
}
|
||||
}, [isPlaying])
|
||||
|
||||
// 清理
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause()
|
||||
audioRef.current.src = ""
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 进度条拖拽
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const progressRef = useRef<HTMLDivElement>(null)
|
||||
@@ -99,9 +173,9 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
|
||||
if (!progressRef.current || totalDuration <= 0) return
|
||||
const rect = progressRef.current.getBoundingClientRect()
|
||||
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
|
||||
seekTo(ratio * totalDuration)
|
||||
handleSeekTo(ratio * totalDuration)
|
||||
},
|
||||
[totalDuration, seekTo],
|
||||
[totalDuration, handleSeekTo],
|
||||
)
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
@@ -118,7 +192,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
|
||||
if (!progressRef.current || totalDuration <= 0) return
|
||||
const rect = progressRef.current.getBoundingClientRect()
|
||||
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
|
||||
seekTo(ratio * totalDuration)
|
||||
handleSeekTo(ratio * totalDuration)
|
||||
}
|
||||
const handleMouseUp = () => setIsDragging(false)
|
||||
window.addEventListener("mousemove", handleMouseMove)
|
||||
@@ -127,7 +201,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
|
||||
window.removeEventListener("mousemove", handleMouseMove)
|
||||
window.removeEventListener("mouseup", handleMouseUp)
|
||||
}
|
||||
}, [isDragging, totalDuration, seekTo])
|
||||
}, [isDragging, totalDuration, handleSeekTo])
|
||||
|
||||
const progressPercent = totalDuration > 0 ? (currentTime / totalDuration) * 100 : 0
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ interface PreviewVideoPanelProps {
|
||||
assetsLoading: boolean
|
||||
/** 标题设置 — 用于 CSS 实时预览层 */
|
||||
titleSettings?: TitleSettings
|
||||
/** 配音音频 URL */
|
||||
voiceAudioUrl?: string
|
||||
}
|
||||
|
||||
/* ── ASS 坐标系参数(与后端 ass_subtitle_builder.py 一致) ── */
|
||||
@@ -165,6 +167,7 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
|
||||
assetsReady,
|
||||
assetsLoading,
|
||||
titleSettings,
|
||||
voiceAudioUrl,
|
||||
}) => {
|
||||
const videoAspectStyle = { aspectRatio: (videoRatio || "16:9").replace(":", "/") }
|
||||
|
||||
@@ -205,6 +208,7 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
|
||||
template={template}
|
||||
videoRatio={videoRatio}
|
||||
ready={assetsReady}
|
||||
voiceAudioUrl={voiceAudioUrl}
|
||||
/>
|
||||
|
||||
{/* CSS 标题实时预览层 — z-index: 20,始终渲染在内容层之上 */}
|
||||
|
||||
@@ -910,6 +910,7 @@
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at 72% 28%, rgba(255, 255, 255, 0.2), transparent 40%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.xx-preview-video video {
|
||||
|
||||
@@ -237,6 +237,19 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
|
||||
const seg = segments[currentSegmentIndex]
|
||||
if (!seg) return
|
||||
|
||||
// 提前 2 秒预加载下一段
|
||||
if (currentSegmentIndex + 1 < segments.length) {
|
||||
const nextSeg = segments[currentSegmentIndex + 1]
|
||||
if (!preloadVideoRef.current) {
|
||||
preloadVideoRef.current = document.createElement("video")
|
||||
preloadVideoRef.current.preload = "auto"
|
||||
}
|
||||
if (preloadVideoRef.current.src !== nextSeg.videoUrl) {
|
||||
preloadVideoRef.current.src = nextSeg.videoUrl
|
||||
preloadVideoRef.current.load()
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否到达出点(容差 0.15s)
|
||||
if (video.currentTime >= seg.endTime - 0.15) {
|
||||
const nextIndex = currentSegmentIndex + 1
|
||||
|
||||
Reference in New Issue
Block a user