mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-21 08:14:42 +01:00
feat: open subtitles
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import TrackPlayer, {
|
||||
Event,
|
||||
type PlaybackActiveTrackChangedEvent,
|
||||
State,
|
||||
useActiveTrack,
|
||||
usePlaybackState,
|
||||
useProgress,
|
||||
} from "react-native-track-player";
|
||||
import {
|
||||
audioStorageEvents,
|
||||
deleteTrack,
|
||||
@@ -7,34 +14,7 @@ import {
|
||||
} from "@/providers/AudioStorage";
|
||||
import { useMusicPlayer } from "@/providers/MusicPlayerProvider";
|
||||
|
||||
// TrackPlayer is not available on tvOS - wrap in try-catch in case native module isn't linked
|
||||
let TrackPlayerModule: typeof import("react-native-track-player") | null = null;
|
||||
if (!Platform.isTV) {
|
||||
try {
|
||||
TrackPlayerModule = require("react-native-track-player");
|
||||
} catch (e) {
|
||||
console.warn("TrackPlayer not available:", e);
|
||||
}
|
||||
}
|
||||
|
||||
type PlaybackActiveTrackChangedEvent = {
|
||||
track?: { id?: string; url?: string };
|
||||
lastTrack?: { id?: string };
|
||||
};
|
||||
|
||||
type Track = { id?: string; url?: string; [key: string]: unknown };
|
||||
type PlaybackErrorEvent = { code?: string; message?: string };
|
||||
|
||||
// Stub component for tvOS or when TrackPlayer is not available
|
||||
const StubMusicPlaybackEngine: React.FC = () => null;
|
||||
|
||||
// Full implementation for non-TV platforms when TrackPlayer is available
|
||||
const MobileMusicPlaybackEngine: React.FC = () => {
|
||||
// These are guaranteed to exist since we only use this component when TrackPlayerModule is available
|
||||
const TrackPlayer = TrackPlayerModule!.default;
|
||||
const { Event, State, useActiveTrack, usePlaybackState, useProgress } =
|
||||
TrackPlayerModule!;
|
||||
|
||||
export const MusicPlaybackEngine: React.FC = () => {
|
||||
const { position, duration } = useProgress(1000);
|
||||
const playbackState = usePlaybackState();
|
||||
const activeTrack = useActiveTrack();
|
||||
@@ -68,7 +48,7 @@ const MobileMusicPlaybackEngine: React.FC = () => {
|
||||
useEffect(() => {
|
||||
const isPlaying = playbackState.state === State.Playing;
|
||||
setIsPlaying(isPlaying);
|
||||
}, [playbackState.state, setIsPlaying, State.Playing]);
|
||||
}, [playbackState.state, setIsPlaying]);
|
||||
|
||||
// Sync active track changes
|
||||
useEffect(() => {
|
||||
@@ -91,63 +71,59 @@ const MobileMusicPlaybackEngine: React.FC = () => {
|
||||
// Listen for track changes (native -> JS)
|
||||
// This triggers look-ahead caching, checks for cached versions, and handles track end
|
||||
useEffect(() => {
|
||||
const subscription = TrackPlayer.addEventListener(
|
||||
Event.PlaybackActiveTrackChanged,
|
||||
async (event: PlaybackActiveTrackChangedEvent) => {
|
||||
// Trigger look-ahead caching when a new track starts playing
|
||||
if (event.track) {
|
||||
triggerLookahead();
|
||||
const subscription =
|
||||
TrackPlayer.addEventListener<PlaybackActiveTrackChangedEvent>(
|
||||
Event.PlaybackActiveTrackChanged,
|
||||
async (event) => {
|
||||
// Trigger look-ahead caching when a new track starts playing
|
||||
if (event.track) {
|
||||
triggerLookahead();
|
||||
|
||||
// Check if there's a cached version we should use instead
|
||||
const trackId = event.track.id;
|
||||
const currentUrl = event.track.url as string;
|
||||
// Check if there's a cached version we should use instead
|
||||
const trackId = event.track.id;
|
||||
const currentUrl = event.track.url as string;
|
||||
|
||||
// Only check if currently using a remote URL
|
||||
if (trackId && currentUrl && !currentUrl.startsWith("file://")) {
|
||||
const cachedPath = getLocalPath(trackId);
|
||||
if (cachedPath) {
|
||||
console.log(
|
||||
`[AudioCache] Switching to cached version for ${trackId}`,
|
||||
);
|
||||
try {
|
||||
// Load the cached version, preserving position if any
|
||||
const currentIndex = await TrackPlayer.getActiveTrackIndex();
|
||||
if (currentIndex !== undefined && currentIndex >= 0) {
|
||||
const queue = await TrackPlayer.getQueue();
|
||||
const track = queue[currentIndex];
|
||||
// Remove and re-add with cached URL
|
||||
await TrackPlayer.remove(currentIndex);
|
||||
await TrackPlayer.add(
|
||||
{ ...track, url: cachedPath },
|
||||
currentIndex,
|
||||
);
|
||||
await TrackPlayer.skip(currentIndex);
|
||||
await TrackPlayer.play();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[AudioCache] Failed to switch to cached version:",
|
||||
error,
|
||||
// Only check if currently using a remote URL
|
||||
if (trackId && currentUrl && !currentUrl.startsWith("file://")) {
|
||||
const cachedPath = getLocalPath(trackId);
|
||||
if (cachedPath) {
|
||||
console.log(
|
||||
`[AudioCache] Switching to cached version for ${trackId}`,
|
||||
);
|
||||
try {
|
||||
// Load the cached version, preserving position if any
|
||||
const currentIndex = await TrackPlayer.getActiveTrackIndex();
|
||||
if (currentIndex !== undefined && currentIndex >= 0) {
|
||||
const queue = await TrackPlayer.getQueue();
|
||||
const track = queue[currentIndex];
|
||||
// Remove and re-add with cached URL
|
||||
await TrackPlayer.remove(currentIndex);
|
||||
await TrackPlayer.add(
|
||||
{ ...track, url: cachedPath },
|
||||
currentIndex,
|
||||
);
|
||||
await TrackPlayer.skip(currentIndex);
|
||||
await TrackPlayer.play();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[AudioCache] Failed to switch to cached version:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no next track and the previous track ended, call onTrackEnd
|
||||
if (event.lastTrack && !event.track) {
|
||||
onTrackEnd();
|
||||
}
|
||||
},
|
||||
);
|
||||
// If there's no next track and the previous track ended, call onTrackEnd
|
||||
if (event.lastTrack && !event.track) {
|
||||
onTrackEnd();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return () => subscription.remove();
|
||||
}, [
|
||||
Event.PlaybackActiveTrackChanged,
|
||||
TrackPlayer,
|
||||
onTrackEnd,
|
||||
triggerLookahead,
|
||||
]);
|
||||
}, [onTrackEnd, triggerLookahead]);
|
||||
|
||||
// Listen for audio cache download completion and update queue URLs
|
||||
useEffect(() => {
|
||||
@@ -165,7 +141,7 @@ const MobileMusicPlaybackEngine: React.FC = () => {
|
||||
const currentIndex = await TrackPlayer.getActiveTrackIndex();
|
||||
|
||||
// Find the track in the queue
|
||||
const trackIndex = queue.findIndex((t: Track) => t.id === itemId);
|
||||
const trackIndex = queue.findIndex((t) => t.id === itemId);
|
||||
|
||||
// Only update if track is in queue and not currently playing
|
||||
if (trackIndex >= 0 && trackIndex !== currentIndex) {
|
||||
@@ -194,13 +170,13 @@ const MobileMusicPlaybackEngine: React.FC = () => {
|
||||
return () => {
|
||||
audioStorageEvents.off("complete", onComplete);
|
||||
};
|
||||
}, [TrackPlayer]);
|
||||
}, []);
|
||||
|
||||
// Listen for playback errors (corrupted cache files)
|
||||
useEffect(() => {
|
||||
const subscription = TrackPlayer.addEventListener(
|
||||
Event.PlaybackError,
|
||||
async (event: PlaybackErrorEvent) => {
|
||||
async (event) => {
|
||||
const activeTrack = await TrackPlayer.getActiveTrack();
|
||||
if (!activeTrack?.url) return;
|
||||
|
||||
@@ -239,14 +215,8 @@ const MobileMusicPlaybackEngine: React.FC = () => {
|
||||
);
|
||||
|
||||
return () => subscription.remove();
|
||||
}, [Event.PlaybackError, TrackPlayer]);
|
||||
}, []);
|
||||
|
||||
// No visual component needed - TrackPlayer is headless
|
||||
return null;
|
||||
};
|
||||
|
||||
// Export the appropriate component based on platform and module availability
|
||||
export const MusicPlaybackEngine: React.FC =
|
||||
Platform.isTV || !TrackPlayerModule
|
||||
? StubMusicPlaybackEngine
|
||||
: MobileMusicPlaybackEngine;
|
||||
|
||||
Reference in New Issue
Block a user