import type { Api } from "@jellyfin/sdk"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { useCallback } from "react"; import CastContext, { CastState, MediaStreamType, PlayServicesState, useCastState, useRemoteMediaClient, } from "react-native-google-cast"; import { getAudioContentType } from "@/utils/jellyfin/audio/getAudioContentType"; import { getAudioStreamUrl } from "@/utils/jellyfin/audio/getAudioStreamUrl"; interface UseMusicCastOptions { api: Api | null; userId: string | undefined; } interface CastQueueOptions { queue: BaseItemDto[]; startIndex: number; } /** * Hook for casting music to Chromecast with full queue support */ export const useMusicCast = ({ api, userId }: UseMusicCastOptions) => { const client = useRemoteMediaClient(); const castState = useCastState(); const isConnected = castState === CastState.CONNECTED; /** * Get album art URL for a track */ const getAlbumArtUrl = useCallback( (track: BaseItemDto): string | undefined => { if (!api) return undefined; const albumId = track.AlbumId || track.ParentId; if (albumId) { return `${api.basePath}/Items/${albumId}/Images/Primary?maxHeight=600&maxWidth=600`; } return `${api.basePath}/Items/${track.Id}/Images/Primary?maxHeight=600&maxWidth=600`; }, [api], ); /** * Cast a queue of tracks to Chromecast * Uses native queue support for seamless track transitions */ const castQueue = useCallback( async ({ queue, startIndex }: CastQueueOptions): Promise => { if (!client || !api || !userId) { console.warn("Cannot cast: missing client, api, or userId"); return false; } try { // Check Play Services state (Android) const state = await CastContext.getPlayServicesState(); if (state && state !== PlayServicesState.SUCCESS) { CastContext.showPlayServicesErrorDialog(state); return false; } // Build queue items - limit to 100 tracks due to Cast SDK message size limit const queueToSend = queue.slice(0, 100); const queueItems = await Promise.all( queueToSend.map(async (track) => { const streamResult = await getAudioStreamUrl( api, userId, track.Id!, ); if (!streamResult) { throw new Error( `Failed to get stream URL for track: ${track.Name}`, ); } const contentType = getAudioContentType( streamResult.mediaSource?.Container, ); // Calculate stream duration in seconds from runtime ticks const streamDurationSeconds = track.RunTimeTicks ? track.RunTimeTicks / 10000000 : undefined; return { mediaInfo: { contentId: track.Id, contentUrl: streamResult.url, contentType, streamType: MediaStreamType.BUFFERED, streamDuration: streamDurationSeconds, metadata: { type: "musicTrack" as const, title: track.Name || "Unknown Track", artist: track.AlbumArtist || track.Artists?.join(", ") || "", albumName: track.Album || "", images: getAlbumArtUrl(track) ? [{ url: getAlbumArtUrl(track)! }] : [], }, }, autoplay: true, preloadTime: 10, // Preload 10 seconds before track ends }; }), ); // Load media with queue await client.loadMedia({ queueData: { items: queueItems, startIndex: Math.min(startIndex, queueItems.length - 1), }, }); // Show expanded controls CastContext.showExpandedControls(); return true; } catch (error) { console.error("Failed to cast music queue:", error); return false; } }, [client, api, userId, getAlbumArtUrl], ); /** * Cast a single track to Chromecast */ const castTrack = useCallback( async (track: BaseItemDto): Promise => { return castQueue({ queue: [track], startIndex: 0 }); }, [castQueue], ); /** * Stop casting and disconnect */ const stopCasting = useCallback(async () => { if (client) { await client.stop(); } }, [client]); return { client, isConnected, castState, castQueue, castTrack, stopCasting, }; };