Files
streamyfin/hooks/useSegmentSkipper.ts
Uruk 2c27186e22 Fix: Improves Chromecast casting experience
Fixes several issues and enhances the Chromecast casting experience:

- Prevents errors when loading media by slimming down the customData payload to avoid exceeding message size limits.
- Improves logic for selecting custom data from media status.
- Fixes an issue with subtitle track selection.
- Recommends stereo audio tracks for better Chromecast compatibility.
- Improves volume control and mute synchronization between the app and the Chromecast device.
- Adds error handling for `loadMedia` in `PlayButton`.
- Fixes image caching issue for season posters in mini player.
- Implements cleanup for scroll retry timeout in episode list.
- Ensures segment skipping functions are asynchronous.
- Resets `hasReportedStartRef` after stopping casting.
- Prevents seeking past the end of Outro segments.
- Reports playback progress more accurately by also taking player state changes into account.
2026-02-09 21:43:33 +01:00

114 lines
3.2 KiB
TypeScript

import { useCallback, useEffect, useRef } from "react";
import { MediaTimeSegment } from "@/providers/Downloads/types";
import { useSettings } from "@/utils/atoms/settings";
import { useHaptic } from "./useHaptic";
type SegmentType = "Intro" | "Outro" | "Recap" | "Commercial" | "Preview";
interface UseSegmentSkipperProps {
segments: MediaTimeSegment[];
segmentType: SegmentType;
currentTime: number;
totalDuration?: number;
seek: (time: number) => void;
isPaused: boolean;
}
interface UseSegmentSkipperReturn {
currentSegment: MediaTimeSegment | null;
skipSegment: (notifyOrUseHaptics?: boolean) => void;
}
/**
* Generic hook to handle all media segment types (intro, outro, recap, commercial, preview)
* Supports three modes: 'none' (disabled), 'ask' (show button), 'auto' (auto-skip)
*/
export const useSegmentSkipper = ({
segments,
segmentType,
currentTime,
totalDuration,
seek,
isPaused,
}: UseSegmentSkipperProps): UseSegmentSkipperReturn => {
const { settings } = useSettings();
const haptic = useHaptic();
const autoSkipTriggeredRef = useRef<string | null>(null);
// Get skip mode based on segment type
const skipMode = (() => {
switch (segmentType) {
case "Intro":
return settings.skipIntro;
case "Outro":
return settings.skipOutro;
case "Recap":
return settings.skipRecap;
case "Commercial":
return settings.skipCommercial;
case "Preview":
return settings.skipPreview;
default:
return "none";
}
})();
// Find current segment
const currentSegment =
segments.find(
(segment) =>
currentTime >= segment.startTime && currentTime < segment.endTime,
) || null;
// Skip function with optional haptic feedback
const skipSegment = useCallback(
(notifyOrUseHaptics = true) => {
if (!currentSegment || skipMode === "none") return;
// For Outro segments, prevent seeking past the end
if (
segmentType === "Outro" &&
totalDuration != null &&
Number.isFinite(totalDuration)
) {
const seekTime = Math.min(currentSegment.endTime, totalDuration);
seek(seekTime);
} else {
seek(currentSegment.endTime);
}
// Only trigger haptic feedback if explicitly requested (manual skip)
if (notifyOrUseHaptics) {
haptic();
}
},
[currentSegment, segmentType, totalDuration, seek, haptic, skipMode],
);
// Auto-skip logic when mode is 'auto'
useEffect(() => {
if (skipMode !== "auto" || isPaused) {
return;
}
// Track segment identity to avoid re-triggering on pause/unpause
const segmentId = currentSegment
? `${currentSegment.startTime}-${currentSegment.endTime}`
: null;
if (currentSegment && autoSkipTriggeredRef.current !== segmentId) {
autoSkipTriggeredRef.current = segmentId;
skipSegment(false); // Don't trigger haptics for auto-skip
}
if (!currentSegment) {
autoSkipTriggeredRef.current = null;
}
}, [currentSegment, skipMode, isPaused, skipSegment]);
// Return null segment if skip mode is 'none'
return {
currentSegment: skipMode === "none" ? null : currentSegment,
skipSegment,
};
};