feat: open subtitles

This commit is contained in:
Fredrik Burmester
2026-01-18 13:20:17 +01:00
parent ebb33854d7
commit 18bc45ea0a
10 changed files with 361 additions and 382 deletions

View File

@@ -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;