Files
MeBox/web/src/utils/skipSegments.ts
T
truewhile 9ccf60427a 优化
2026-10-22 15:14:00 +08:00

98 lines
3.8 KiB
TypeScript

import type { PlaybackSegment, PlaybackSegmentKind } from '../types/playback'
/** 落在源时间轴上的可跳过区间(秒)。 */
export interface SkipSegment {
kind: PlaybackSegmentKind
startSec: number
endSec: number
}
/** 当前应当在浮层上提示的跳过动作。 */
export interface SkipPrompt extends SkipSegment {
label: string
}
/** 片头/回顾统一叫「跳过片头」;片尾在有下一集时直接叫「下一集」。 */
export function skipLabel(kind: PlaybackSegmentKind, hasNextEpisode: boolean): string {
if (kind === 'credits' || kind === 'preview') {
return hasNextEpisode ? '下一集' : '跳过片尾'
}
return '跳过片头'
}
/**
* 自动跳过后的提示文案。自动跳转很容易被当成「进度条自己动了」,所以必须告诉
* 用户发生了什么,并给一个撤销入口。
*/
export function skippedNoticeText(kind: PlaybackSegmentKind): string {
if (kind === 'credits' || kind === 'preview') return '已跳过片尾'
if (kind === 'recap') return '已跳过回顾'
return '已跳过片头'
}
/**
* 把服务端返回的片段换算成源时间轴上的秒区间。
*
* end_ms === 0 是「一直到片尾」的哨兵值,只有片尾类(credits / preview)会这么
* 返回,用媒体总时长补齐。片头 / 回顾出现 0 一律丢弃:那通常意味着数据源根本没
* 给出结束点(TheIntroDB 用 end_ms: 0 表示「这段不存在」),补成总时长会让整集
* 都落在「片头」区间里,一开播就弹跳过按钮。时长未知(0 / 非有限值)同样丢弃,
* 否则会算出一个永远命中的区间,浮层上的按钮就再也点不掉了。
*/
export function toSkipSegments(
segments: PlaybackSegment[] | null | undefined,
durationSec: number,
): SkipSegment[] {
if (!segments || segments.length === 0) return []
const out: SkipSegment[] = []
for (const segment of segments) {
const startSec = Math.max(0, segment.start_ms / 1000)
const openEnded = segment.kind === 'credits' || segment.kind === 'preview'
if (segment.end_ms > 0) {
const endSec = segment.end_ms / 1000
if (!Number.isFinite(endSec) || endSec <= startSec) continue
out.push({ kind: segment.kind, startSec, endSec })
continue
}
if (!openEnded) continue
if (!Number.isFinite(durationSec) || durationSec <= startSec) continue
out.push({ kind: segment.kind, startSec, endSec: durationSec })
}
return out.sort((a, b) => a.startSec - b.startSec)
}
export interface ResolveActiveSkipOptions {
hasNextEpisode?: boolean
/** 本次播放内已经跳过 / 被用户撤销过的类型,不再提示。 */
excludedKinds?: PlaybackSegmentKind[]
/** 允许提前一点点出现按钮,避免恰好卡在时间轴上时迟到。 */
toleranceSec?: number
}
/**
* 找出当前播放位置命中的可跳过区间。同一位置有多个区间时取最早开始的那个。
*
* 注意 currentSec 必须是「源时间轴」上的绝对秒数:HLS 转码时 <video>.currentTime
* 是相对转码起点的,调用方要用 streamOffset 加回来,否则转码场景整段判断都会错位。
*/
export function resolveActiveSkip(
currentSec: number,
segments: SkipSegment[],
options: ResolveActiveSkipOptions = {},
): SkipPrompt | null {
const { hasNextEpisode = false, excludedKinds = [], toleranceSec = 0.25 } = options
if (!Number.isFinite(currentSec)) return null
for (const segment of segments) {
if (excludedKinds.includes(segment.kind)) continue
if (currentSec < segment.startSec - toleranceSec) continue
if (currentSec >= segment.endSec) continue
return {
kind: segment.kind,
startSec: segment.startSec,
endSec: segment.endSec,
label: skipLabel(segment.kind, hasNextEpisode),
}
}
return null
}