feat: optimize and complete casting system implementation

Performance Optimizations:
- Add progress tracking to skip redundant Jellyfin API calls (< 5s changes)
- Debounce volume changes (300ms) to reduce API load
- Memoize expensive calculations (protocol colors, icons, progress percent)
- Remove dead useChromecastPlayer hook (redundant with useCasting)

Feature Integrations:
- Add ChromecastEpisodeList modal with Episodes button for TV shows
- Add ChromecastDeviceSheet modal accessible via device indicator
- Add ChromecastSettingsMenu modal with settings icon
- Integrate segment detection with Skip Intro/Credits/Recap buttons
- Add next episode countdown UI (30s before end)

AirPlay Support:
- Add comprehensive documentation for AirPlay detection approaches
- Document integration requirements with AVRoutePickerView
- Prepare infrastructure for native module or AVPlayer integration

UI Improvements:
- Make device indicator tappable to open device sheet
- Add settings icon in header
- Show segment skip buttons dynamically based on current playback
- Display next episode countdown with cancel option
- Optimize hook ordering to prevent conditional hook violations

TODOs for future work:
- Fetch actual episode list from Jellyfin API
- Wire media source/audio/subtitle track selectors to player
- Implement episode auto-play logic
- Create native module for AirPlay state detection
- Add RemoteMediaClient to segment skip functions
This commit is contained in:
Uruk
2026-01-19 22:46:12 +01:00
committed by Gauvain
parent 72c050b9a5
commit 6894decdba
3 changed files with 258 additions and 265 deletions

View File

@@ -34,10 +34,18 @@ export const useCasting = (item: BaseItemDto | null) => {
const [state, setState] = useState<CastPlayerState>(DEFAULT_CAST_STATE);
const progressIntervalRef = useRef<NodeJS.Timeout | null>(null);
const controlsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const lastReportedProgressRef = useRef(0);
const volumeDebounceRef = useRef<NodeJS.Timeout | null>(null);
// Detect which protocol is active
const chromecastConnected = castDevice !== null;
const airplayConnected = false; // TODO: Detect AirPlay connection from video player
// TODO: AirPlay detection requires integration with video player's AVRoutePickerView
// The @douglowder/expo-av-route-picker-view package doesn't expose route state
// Options:
// 1. Create native module to detect AVAudioSession.sharedInstance().currentRoute
// 2. Use AVPlayer's isExternalPlaybackActive property
// 3. Listen to AVPlayerItemDidPlayToEndTimeNotification for AirPlay events
const airplayConnected = false;
const activeProtocol: CastProtocol | null = chromecastConnected
? "chromecast"
@@ -94,12 +102,19 @@ export const useCasting = (item: BaseItemDto | null) => {
}
}, [mediaStatus, activeProtocol]);
// Progress reporting to Jellyfin
// Progress reporting to Jellyfin (optimized to skip redundant reports)
useEffect(() => {
if (!isConnected || !item?.Id || !user?.Id || state.progress <= 0) return;
const reportProgress = () => {
const progressSeconds = Math.floor(state.progress / 1000);
// Skip if progress hasn't changed significantly (less than 5 seconds)
if (Math.abs(progressSeconds - lastReportedProgressRef.current) < 5) {
return;
}
lastReportedProgressRef.current = progressSeconds;
const playStateApi = api ? getPlaystateApi(api) : null;
playStateApi
?.reportPlaybackProgress({
@@ -197,15 +212,25 @@ export const useCasting = (item: BaseItemDto | null) => {
setState(DEFAULT_CAST_STATE);
}, [client, api, item?.Id, user?.Id, state.progress, activeProtocol]);
// Volume control
// Volume control (debounced to reduce API calls)
const setVolume = useCallback(
async (volume: number) => {
(volume: number) => {
const clampedVolume = Math.max(0, Math.min(1, volume));
if (activeProtocol === "chromecast") {
await client?.setStreamVolume(clampedVolume);
}
// TODO: AirPlay volume control
// Update UI immediately
setState((prev) => ({ ...prev, volume: clampedVolume }));
// Debounce API call
if (volumeDebounceRef.current) {
clearTimeout(volumeDebounceRef.current);
}
volumeDebounceRef.current = setTimeout(async () => {
if (activeProtocol === "chromecast") {
await client?.setStreamVolume(clampedVolume).catch(console.error);
}
// TODO: AirPlay volume control
}, 300);
},
[client, activeProtocol],
);
@@ -240,6 +265,9 @@ export const useCasting = (item: BaseItemDto | null) => {
if (controlsTimeoutRef.current) {
clearTimeout(controlsTimeoutRef.current);
}
if (volumeDebounceRef.current) {
clearTimeout(volumeDebounceRef.current);
}
};
}, []);