frontend: align types to backend, playout HLS, rm Jellyfin proxy (#12)
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { ScheduleSlot } from "@/app/(main)/tv/components";
|
||||
import type { ScheduledSlotResponse } from "@/lib/types";
|
||||
|
||||
@@ -99,63 +98,17 @@ export function findNextSlot(
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// useStreamUrl — resolves the 307 stream redirect via a Next.js API route
|
||||
// useStreamUrl — HLS playlist from the Playout Service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolves the live stream URL for a channel, starting at the correct
|
||||
* broadcast offset so refresh doesn't replay from the beginning.
|
||||
*
|
||||
* The backend's GET /channels/:id/stream endpoint returns a 307 redirect to
|
||||
* the Jellyfin stream URL. Since browsers can't read redirect Location headers
|
||||
* from fetch(), we proxy through /api/stream/[channelId] (a Next.js route that
|
||||
* runs server-side) and return the final URL as JSON.
|
||||
*
|
||||
* slotId is included in the query key so the URL is re-fetched automatically
|
||||
* when the current item changes (the next scheduled item starts playing).
|
||||
* Within the same slot, the URL stays stable — no mid-item restarts.
|
||||
*
|
||||
* Returns null when the channel is in a gap (no-signal / 204).
|
||||
*/
|
||||
/**
|
||||
* Resolves the stream URL for the current slot, with StartTimeTicks set so
|
||||
* Jellyfin begins transcoding at the correct broadcast offset.
|
||||
*
|
||||
* slotId is in the query key: the URL refetches when the item changes (new
|
||||
* slot), but stays stable while the same slot is playing — no mid-item
|
||||
* restarts. offsetSecs is captured once when the query first runs for a
|
||||
* given slot, so 30-second broadcast refetches don't disturb playback.
|
||||
*/
|
||||
export function useStreamUrl(
|
||||
channelId: string | undefined,
|
||||
token: string | null,
|
||||
slotId: string | undefined,
|
||||
channelPassword?: string,
|
||||
blockPassword?: string,
|
||||
bitrateBps?: number,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: ["stream-url", channelId, slotId, channelPassword, blockPassword, bitrateBps],
|
||||
queryFn: async (): Promise<string | null> => {
|
||||
const params = new URLSearchParams();
|
||||
if (token) params.set("token", token);
|
||||
if (channelPassword) params.set("channel_password", channelPassword);
|
||||
if (blockPassword) params.set("block_password", blockPassword);
|
||||
if (quality) params.set("quality", quality);
|
||||
const res = await fetch(`/api/stream/${channelId}?${params}`, {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (res.status === 204) return null;
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
const msg = body?.error ?? `Stream resolve failed: ${res.status}`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
const { url } = (await res.json()) as { url: string };
|
||||
return bitrateBps ? `${url}&VideoBitRate=${bitrateBps}` : url;
|
||||
},
|
||||
enabled: !!channelId && !!slotId,
|
||||
staleTime: Infinity,
|
||||
retry: false,
|
||||
});
|
||||
const PLAYOUT_URL =
|
||||
process.env.NEXT_PUBLIC_PLAYOUT_URL ?? "http://localhost:9090";
|
||||
|
||||
export function useStreamUrl(channelId: string | undefined) {
|
||||
if (!channelId) return { data: null, isLoading: false, error: null };
|
||||
return {
|
||||
data: `${PLAYOUT_URL}/playout/${channelId}/playlist.m3u8`,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user