mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-19 12:20:26 +01:00
Replace the duplicated per-platform segment-skip logic with a shared useMediaSegments hook: per-type skippers, overlap priority (Commercial > Recap > Intro > Preview > Outro) and a single auto-skip driver so both platforms behave identically. - One auto-skip effect on the priority-resolved active segment, so overlapping auto segments can't fire competing seeks. - Sub-second precision (stop flooring currentTime to whole seconds). - Gate auto-skip on !isBuffering plus a short arm delay so it never seeks a not-yet-seekable transcoded stream at a 0:00 intro. - Dedup guard survives the transient null when a transcoded stream bounces the reported position, instead of looping seeks.
105 lines
2.9 KiB
TypeScript
105 lines
2.9 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
import { MediaTimeSegment } from "@/providers/Downloads/types";
|
|
import { SegmentSkipMode, useSettings } from "@/utils/atoms/settings";
|
|
import { useHaptic } from "./useHaptic";
|
|
|
|
export type SegmentType =
|
|
| "Intro"
|
|
| "Outro"
|
|
| "Recap"
|
|
| "Commercial"
|
|
| "Preview";
|
|
|
|
const SEGMENT_TO_SETTING: Record<
|
|
SegmentType,
|
|
"skipIntro" | "skipOutro" | "skipRecap" | "skipCommercial" | "skipPreview"
|
|
> = {
|
|
Intro: "skipIntro",
|
|
Outro: "skipOutro",
|
|
Recap: "skipRecap",
|
|
Commercial: "skipCommercial",
|
|
Preview: "skipPreview",
|
|
};
|
|
|
|
interface UseSegmentSkipperProps {
|
|
segments: MediaTimeSegment[];
|
|
segmentType: SegmentType;
|
|
currentTime: number;
|
|
totalDuration?: number;
|
|
seek: (time: number) => void;
|
|
}
|
|
|
|
interface UseSegmentSkipperReturn {
|
|
currentSegment: MediaTimeSegment | null;
|
|
skipSegment: (useHaptics?: boolean) => void;
|
|
skipMode: SegmentSkipMode;
|
|
}
|
|
|
|
/**
|
|
* Generic hook for a single media segment type (intro, outro, recap, commercial, preview).
|
|
* Reports the segment currently under the playhead, its skip mode, and a skip action.
|
|
* Auto-skip is NOT performed here: the consumer drives it from the priority-resolved
|
|
* active segment so overlapping segments can't trigger competing seeks.
|
|
*/
|
|
export const useSegmentSkipper = ({
|
|
segments,
|
|
segmentType,
|
|
currentTime,
|
|
totalDuration,
|
|
seek,
|
|
}: UseSegmentSkipperProps): UseSegmentSkipperReturn => {
|
|
const { settings } = useSettings();
|
|
const haptic = useHaptic();
|
|
|
|
const skipMode: SegmentSkipMode =
|
|
settings?.[SEGMENT_TO_SETTING[segmentType]] ?? "none";
|
|
|
|
const currentSegment = useMemo(
|
|
() =>
|
|
segments.find(
|
|
(s) => currentTime >= s.startTime && currentTime < s.endTime,
|
|
) ?? null,
|
|
[segments, currentTime],
|
|
);
|
|
|
|
// Refs keep skipSegment's identity stable across seek/haptic changes
|
|
// (haptic is unstable when disabled), so the consumer's auto-skip effect
|
|
// doesn't re-fire spuriously.
|
|
const seekRef = useRef(seek);
|
|
const hapticRef = useRef(haptic);
|
|
useEffect(() => {
|
|
seekRef.current = seek;
|
|
hapticRef.current = haptic;
|
|
});
|
|
|
|
const skipSegment = useCallback(
|
|
(useHaptics = true) => {
|
|
if (!currentSegment || skipMode === "none") return;
|
|
|
|
// Outro endTime sometimes exceeds the actual file duration. Keep a 2s
|
|
// buffer so the player's natural end-of-video flow (next-episode
|
|
// countdown, etc.) still fires instead of stalling at the exact end.
|
|
let target = currentSegment.endTime;
|
|
if (
|
|
segmentType === "Outro" &&
|
|
totalDuration != null &&
|
|
Number.isFinite(totalDuration) &&
|
|
target >= totalDuration
|
|
) {
|
|
target = Math.max(0, totalDuration - 2);
|
|
}
|
|
|
|
seekRef.current(target);
|
|
|
|
if (useHaptics) hapticRef.current();
|
|
},
|
|
[currentSegment, segmentType, totalDuration, skipMode],
|
|
);
|
|
|
|
return {
|
|
currentSegment: skipMode === "none" ? null : currentSegment,
|
|
skipSegment,
|
|
skipMode,
|
|
};
|
|
};
|