优化视频头尾跳过功能

This commit is contained in:
truewhile
2026-09-23 16:07:28 +08:00
parent 44fde6c5a0
commit 34ccf14cda
26 changed files with 1992 additions and 47 deletions
+2 -1
View File
@@ -1,5 +1,5 @@
import { api } from './client'
import type { PlayProfile } from '../types'
import type { PlayProfile, SegmentSource } from '../types'
// Payload accepted by create / update.
export interface PlayProfileInput {
@@ -14,6 +14,7 @@ export interface PlayProfileInput {
preferred_audio_lang?: string
autoplay_next: boolean
skip_intro: boolean
segment_source: SegmentSource
allowed_library_ids: string[]
}
+28 -8
View File
@@ -75,6 +75,13 @@ type PlaybackProgressSession = {
// 自动跳过片头后,「已跳过 · 撤销」提示停留的时长。
const SKIP_NOTICE_MS = 6000
// ffprobe 章节提取在服务端是异步的:pending 为 true 时按这个间隔重试,最多这么
// 多次(远端探测实测 3~4 秒,本地更快;40 秒的预算足以覆盖慢速 CDN 与大文件)。
// 重试期间播放完全不受影响;拿到数据时如果片头已经播过去了,resolveActiveSkip
// 自然不会再提示,不会出现「点一下就跳过头」的按钮。
const SKIP_SEGMENTS_POLL_MS = 4000
const SKIP_SEGMENTS_MAX_POLLS = 10
function normalizePlayerVolume(value: unknown): number {
const parsed = Number(value)
if (!Number.isFinite(parsed)) return 1
@@ -732,6 +739,7 @@ export function PlayerPage() {
useEffect(() => {
if (!mediaId) return
let cancelled = false
let pollTimer: ReturnType<typeof setTimeout> | null = null
setRawSkipSegments([])
setAutoSkipIntro(false)
setActiveSkip(null)
@@ -739,16 +747,28 @@ export function PlayerPage() {
setDismissedSkipKinds([])
setAutoSuppressedKinds([])
// 片段数据与播放来源无关,播放开始后异步补抓即可,绝不挡在起播路径上。
playbackAPI
.segments(mediaId)
.then((res) => {
if (cancelled) return
setAutoSkipIntro(Boolean(res.auto_skip))
setRawSkipSegments(res.segments ?? [])
})
.catch(() => undefined)
//
// 服务端的 ffprobe 章节提取是异步的:pending 为 true 说明这次还没结果,隔几秒
// 再拉一次。提取完成后如果片头还没播完,跳过按钮会自己出现;已经过了片头时间
// 的话 resolveActiveSkip 不会提示,所以不会出现「点一下就跳过头」的按钮。
const load = (attempt: number) => {
playbackAPI
.segments(mediaId)
.then((res) => {
if (cancelled) return
setAutoSkipIntro(Boolean(res.auto_skip))
setRawSkipSegments(res.segments ?? [])
const hasSegments = (res.segments ?? []).length > 0
if (res.pending && !hasSegments && attempt < SKIP_SEGMENTS_MAX_POLLS) {
pollTimer = setTimeout(() => load(attempt + 1), SKIP_SEGMENTS_POLL_MS)
}
})
.catch(() => undefined)
}
load(0)
return () => {
cancelled = true
if (pollTimer) clearTimeout(pollTimer)
}
}, [mediaId])
+1
View File
@@ -37,6 +37,7 @@ export function ProfileFormModal({
preferred_audio_lang: editing?.preferred_audio_lang ?? '',
autoplay_next: editing?.autoplay_next ?? true,
skip_intro: editing?.skip_intro ?? false,
segment_source: editing?.segment_source ?? 'auto',
allowed_library_ids: editing?.allowed_library_ids ?? [],
}))
const [saving, setSaving] = useState(false)
+11
View File
@@ -126,6 +126,17 @@ export function ProfilePreferenceFields({
checked={form.skip_intro}
onChange={(value) => update({ skip_intro: value })}
/>
<Field label="片头片尾数据来源">
<select
className="input-base"
value={form.segment_source}
onChange={(event) => update({ segment_source: event.target.value as PlayProfileInput['segment_source'] })}
>
<option value="auto">自动(优先用文件内嵌章节,没有可用章节时用 TheIntroDB)</option>
<option value="theintrodb">TheIntroDB 社区数据库(依赖刮削出的 TMDb ID)</option>
<option value="ffprobe">ffprobe 本地提取(读取文件内嵌章节,不联网)</option>
</select>
</Field>
</>
)
}
+4
View File
@@ -10,9 +10,13 @@ export interface PlayProfile {
preferred_audio_lang?: string
autoplay_next: boolean
skip_intro: boolean
segment_source?: SegmentSource
allowed_library_ids: string[]
total_watch_time: number
last_active_at?: string
created_at: string
updated_at: string
}
/** 片头/片尾数据来源:auto 优先文件内嵌章节,没有则回落 TheIntroDB。 */
export type SegmentSource = 'auto' | 'theintrodb' | 'ffprobe'
+11
View File
@@ -43,5 +43,16 @@ export interface PlaybackSegmentsResponse {
segments: PlaybackSegment[]
/** 当前生效播放档案的「自动跳过片头」开关。 */
auto_skip: boolean
/**
* true 表示 ffprobe 章节提取还在服务端后台跑:这次可能还没有章节数据,隔几秒
* 再拉一次就能拿到。拿到时若片头还没播完,跳过按钮会自动出现;已经过了片头
* 时间则不会提示。
*/
pending?: boolean
/** 本次实际生效的数据来源。 */
source?: PlaybackSegmentSource
}
/** 片头/片尾数据来源,与后端 play_profiles.segment_source 一致。 */
export type PlaybackSegmentSource = 'auto' | 'theintrodb' | 'ffprobe'