Files
streamyfin/hooks/useMusicCast.ts
Fredrik Burmester 399eb0a351
Some checks failed
🏗️ Build Apps / 🤖 Build Android APK (Phone) (push) Has been cancelled
🏗️ Build Apps / 🤖 Build Android APK (TV) (push) Has been cancelled
🏗️ Build Apps / 🍎 Build iOS IPA (Phone) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
fix(chromecast): add missing MediaInfo fields
2026-01-10 21:32:09 +01:00

162 lines
4.6 KiB
TypeScript

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<boolean> => {
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<boolean> => {
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,
};
};