Skip to content

Commit b167306

Browse files
committed
feat(player): implement HLS session progress polling with visual feedback
- Add waitForSessionReady to poll session creation progress before playback - Integrate useSessionProgress hook for reusable progress tracking logic - Display deterministic progress bar with percentage and stage information during session initialization - Add SessionService.getSessionProgress API with HTTP 425 handling for in-progress state - Update backend submodule to v64aaf0b with HLS session progress support - Remove excessive debug logging in SubtitleOverlay to reduce console noise Changes: PlayerPage: - Add waitForSessionReady state and sessionProgress state for UI rendering - Implement polling loop with 2s interval to fetch session progress until ready - Display progress bar with stage text and percentage during session creation - Handle HTTP 425 (session not ready) gracefully in progress polling - Fallback to default playlist URL if progress response URL parsing fails useSessionProgress Hook: - Provide reusable session progress polling with configurable interval (default 2s) - Auto-stop polling when session is ready or error occurs - Expose startPolling, stopPolling, reset methods for lifecycle control - Implement progress threshold (5%) to reduce log noise SessionService: - Add getSessionProgress(sessionId) API to fetch session creation progress - Define SessionProgressResponse and AudioProgressInfo interfaces - Treat HTTP 425 as normal in-progress state (not error) This enhancement provides real-time visual feedback during HLS session initialization, improving user experience by showing progress instead of generic "loading..." spinner. The polling mechanism ensures playback starts only after the session is fully ready, preventing premature playlist access.
1 parent 35cf4ce commit b167306

6 files changed

Lines changed: 440 additions & 43 deletions

File tree

src/renderer/src/pages/player/PlayerPage.tsx

Lines changed: 186 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import db from '@renderer/databases'
44
import {
55
CodecCompatibilityChecker,
66
type ExtendedErrorType,
7+
SessionError,
78
SessionService,
89
VideoLibraryService
910
} from '@renderer/services'
@@ -95,6 +96,12 @@ function PlayerPage() {
9596
originalPath?: string
9697
} | null>(null)
9798
const [showMediaServerPrompt, setShowMediaServerPrompt] = useState(false)
99+
const [waitingForSessionReady, setWaitingForSessionReady] = useState(false)
100+
const [sessionProgress, setSessionProgress] = useState<{
101+
percent: number
102+
stage: string
103+
status: string
104+
} | null>(null)
98105
// const { pokeInteraction } = usePlayerUI()
99106

100107
// 保存转码会话 ID 用于清理
@@ -103,8 +110,64 @@ function PlayerPage() {
103110
// 加载视频数据
104111
useEffect(() => {
105112
let cancelled = false
113+
const pollIntervalMs = 2000
114+
115+
const waitForSessionReady = async (sessionId: string) => {
116+
while (!cancelled) {
117+
try {
118+
const progress = await SessionService.getSessionProgress(sessionId)
119+
if (cancelled) {
120+
break
121+
}
122+
123+
setSessionProgress((prev) => {
124+
const stage = progress.progress_stage?.trim() || prev?.stage || '处理中...'
125+
const rawPercent =
126+
typeof progress.progress_percent === 'number'
127+
? progress.progress_percent
128+
: Number(progress.progress_percent)
129+
const percent = Number.isFinite(rawPercent) ? rawPercent : (prev?.percent ?? 0)
130+
return {
131+
percent,
132+
stage,
133+
status: progress.status
134+
}
135+
})
136+
137+
if (progress.is_ready) {
138+
setSessionProgress((prev) => ({
139+
percent: 100,
140+
stage: progress.progress_stage?.trim() || prev?.stage || '就绪',
141+
status: progress.status
142+
}))
143+
return progress
144+
}
145+
} catch (progressError) {
146+
if (
147+
progressError instanceof SessionError &&
148+
progressError.statusCode &&
149+
progressError.statusCode === 425
150+
) {
151+
// 会话尚未返回进度,等待下一轮
152+
} else {
153+
throw progressError
154+
}
155+
}
156+
157+
if (cancelled) {
158+
break
159+
}
160+
161+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs))
162+
}
163+
164+
throw new Error('会话进度轮询已取消')
165+
}
166+
106167
const loadData = async () => {
107168
setLoading(true)
169+
setWaitingForSessionReady(false)
170+
setSessionProgress(null)
108171
if (!videoId) {
109172
setError('无效的视频 ID')
110173
setVideoData(null)
@@ -189,36 +252,80 @@ function PlayerPage() {
189252

190253
// 保存会话 ID 用于后续清理
191254
sessionIdRef.current = sessionResult.session_id
192-
193-
// 构建完整的播放列表 URL
194-
const playListUrl = await SessionService.getPlaylistUrl(sessionResult.session_id)
195-
196-
// 更新转码信息和播放源
197-
usePlayerStore.getState().updateTranscodeInfo({
198-
hlsSrc: playListUrl,
199-
windowId: 0, // 会话模式不再使用 windowId
200-
assetHash: sessionResult.asset_hash,
201-
profileHash: sessionResult.profile_hash,
202-
cached: false, // 会话模式由后端管理缓存
203-
sessionId: sessionResult.session_id,
204-
endTime: Date.now()
255+
setWaitingForSessionReady(true)
256+
setSessionProgress({
257+
percent: 0,
258+
stage: '正在创建转码会话...',
259+
status: 'initializing'
205260
})
206261

207-
// 切换到 HLS 播放模式
208-
usePlayerStore.getState().switchToHlsSource(playListUrl, {
209-
windowId: 0,
210-
assetHash: sessionResult.asset_hash,
211-
profileHash: sessionResult.profile_hash,
212-
cached: false,
213-
sessionId: sessionResult.session_id
214-
})
215-
216-
finalSrc = playListUrl
217-
218-
logger.info('预转码流程完成,使用 HLS 播放源', {
219-
originalSrc: fileUrl,
220-
hlsSrc: finalSrc
221-
})
262+
try {
263+
const readyProgress = await waitForSessionReady(sessionResult.session_id)
264+
265+
if (cancelled) {
266+
return
267+
}
268+
269+
const fallbackPlaylistUrl = await SessionService.getPlaylistUrl(
270+
sessionResult.session_id
271+
)
272+
let playListUrl = fallbackPlaylistUrl
273+
274+
if (readyProgress.playlist_url) {
275+
try {
276+
playListUrl = new URL(
277+
readyProgress.playlist_url,
278+
fallbackPlaylistUrl
279+
).toString()
280+
} catch (urlError) {
281+
logger.warn('解析会话进度返回的播放列表 URL 失败,将使用默认值', {
282+
sessionId: sessionResult.session_id,
283+
playlistUrl: readyProgress.playlist_url,
284+
error: urlError instanceof Error ? urlError.message : String(urlError)
285+
})
286+
playListUrl = fallbackPlaylistUrl
287+
}
288+
}
289+
290+
// 更新转码信息和播放源
291+
usePlayerStore.getState().updateTranscodeInfo({
292+
hlsSrc: playListUrl,
293+
windowId: 0, // 会话模式不再使用 windowId
294+
assetHash: sessionResult.asset_hash,
295+
profileHash: sessionResult.profile_hash,
296+
cached: false, // 会话模式由后端管理缓存
297+
sessionId: sessionResult.session_id,
298+
endTime: Date.now()
299+
})
300+
301+
// 切换到 HLS 播放模式
302+
usePlayerStore.getState().switchToHlsSource(playListUrl, {
303+
windowId: 0,
304+
assetHash: sessionResult.asset_hash,
305+
profileHash: sessionResult.profile_hash,
306+
cached: false,
307+
sessionId: sessionResult.session_id
308+
})
309+
310+
finalSrc = playListUrl
311+
312+
logger.info('预转码流程完成,使用 HLS 播放源', {
313+
originalSrc: fileUrl,
314+
hlsSrc: finalSrc
315+
})
316+
} catch (progressError) {
317+
if (!cancelled) {
318+
const message =
319+
progressError instanceof Error ? progressError.message : '获取会话进度失败'
320+
logger.error('会话进度轮询失败,转码流程终止', {
321+
error: message,
322+
sessionId: sessionResult.session_id
323+
})
324+
setError(message || '获取会话进度失败')
325+
usePlayerStore.getState().setTranscodeStatus('failed')
326+
}
327+
return
328+
}
222329
}
223330
} catch (checkError) {
224331
logger.error('检查 Media Server 状态失败,显示推荐安装弹窗', {
@@ -270,7 +377,10 @@ function PlayerPage() {
270377
logger.error(`加载视频数据失败: ${err}`)
271378
setError(err instanceof Error ? err.message : '加载失败')
272379
} finally {
273-
if (!cancelled) setLoading(false)
380+
if (!cancelled) {
381+
setWaitingForSessionReady(false)
382+
setLoading(false)
383+
}
274384
}
275385
}
276386

@@ -415,13 +525,29 @@ function PlayerPage() {
415525
}, [handleToggleFullscreen])
416526

417527
if (loading) {
528+
const progressPercent = Math.max(0, Math.min(100, Math.round(sessionProgress?.percent ?? 0)))
529+
418530
return (
419531
<Container>
420532
<LoadingContainer>
421-
<LoadingText>加载中...</LoadingText>
422-
<LoadingBarContainer>
423-
<LoadingBarProgress />
424-
</LoadingBarContainer>
533+
{waitingForSessionReady ? (
534+
<>
535+
<ProgressStageText>
536+
{sessionProgress?.stage || '正在创建转码会话...'}
537+
</ProgressStageText>
538+
<DeterminateBarTrack>
539+
<DeterminateBarFill $percent={progressPercent} />
540+
</DeterminateBarTrack>
541+
<ProgressPercentText>{progressPercent}%</ProgressPercentText>
542+
</>
543+
) : (
544+
<>
545+
<LoadingText>加载中...</LoadingText>
546+
<LoadingBarContainer>
547+
<LoadingBarProgress />
548+
</LoadingBarContainer>
549+
</>
550+
)}
425551
</LoadingContainer>
426552
</Container>
427553
)
@@ -609,6 +735,33 @@ const LoadingBarProgress = styled.div`
609735
}
610736
`
611737

738+
const DeterminateBarTrack = styled.div`
739+
width: 240px;
740+
height: 4px;
741+
background: var(--ant-color-fill-quaternary, rgba(255, 255, 255, 0.08));
742+
border-radius: 2px;
743+
overflow: hidden;
744+
`
745+
746+
const DeterminateBarFill = styled.div<{ $percent: number }>`
747+
height: 100%;
748+
width: ${({ $percent }) => `${$percent}%`};
749+
background: var(--ant-color-primary, #1677ff);
750+
border-radius: 2px;
751+
transition: width 0.4s ease;
752+
`
753+
754+
const ProgressStageText = styled.div`
755+
font-size: 16px;
756+
color: var(--color-text-1, #ddd);
757+
text-align: center;
758+
`
759+
760+
const ProgressPercentText = styled.div`
761+
font-size: 14px;
762+
color: var(--color-text-2, #bbb);
763+
`
764+
612765
const ErrorContainer = styled.div`
613766
display: flex;
614767
flex-direction: column;

src/renderer/src/pages/player/components/SubtitleOverlay.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -423,15 +423,6 @@ export const SubtitleOverlay = memo(function SubtitleOverlay({
423423
return null
424424
}
425425

426-
logger.debug('渲染 SubtitleOverlay', {
427-
displayMode,
428-
position,
429-
size,
430-
isDragging,
431-
isResizing,
432-
showBoundaries
433-
})
434-
435426
return (
436427
<OverlayContainer
437428
ref={overlayRef}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { usePlayerEngine } from './usePlayerEngine'
2+
export { useSessionProgress } from './useSessionProgress'
23
export { useSubtitleEngine } from './useSubtitleEngine'
34
export { useSubtitleOverlay } from './useSubtitleOverlay'
45
export { useSubtitleOverlayUI } from './useSubtitleOverlayUI'

0 commit comments

Comments
 (0)