frontend: align types to backend, playout HLS, rm Jellyfin proxy (#12)
This commit is contained in:
@@ -8,7 +8,7 @@ import type {
|
||||
LogoPosition,
|
||||
ProgrammingBlock,
|
||||
MediaFilter,
|
||||
RecyclePolicy,
|
||||
RotationPolicy,
|
||||
Weekday,
|
||||
} from "@/lib/types";
|
||||
import { WEEKDAYS } from "@/lib/types";
|
||||
@@ -51,7 +51,7 @@ export function defaultBlock(startMins = 20 * 60, durationMins = 60): Programmin
|
||||
duration_mins: durationMins,
|
||||
content: { type: "algorithmic", filter: defaultFilter(), strategy: "random" },
|
||||
loop_on_finish: true,
|
||||
ignore_recycle_policy: false,
|
||||
ignore_rotation_policy: false,
|
||||
access_mode: "public",
|
||||
};
|
||||
}
|
||||
@@ -67,7 +67,7 @@ export function useChannelForm(channel: ChannelResponse | null) {
|
||||
const [description, setDescription] = useState("");
|
||||
const [timezone, setTimezone] = useState("UTC");
|
||||
const [dayBlocks, setDayBlocks] = useState<Record<Weekday, ProgrammingBlock[]>>(emptyDayBlocks);
|
||||
const [recyclePolicy, setRecyclePolicy] = useState<RecyclePolicy>({
|
||||
const [rotationPolicy, setRotationPolicy] = useState<RotationPolicy>({
|
||||
cooldown_days: null,
|
||||
cooldown_generations: null,
|
||||
min_available_ratio: 0.1,
|
||||
@@ -96,12 +96,12 @@ export function useChannelForm(channel: ChannelResponse | null) {
|
||||
...emptyDayBlocks(),
|
||||
...channel.schedule_config.day_blocks,
|
||||
});
|
||||
setRecyclePolicy(channel.recycle_policy);
|
||||
setRotationPolicy(channel.rotation_policy);
|
||||
setAutoSchedule(channel.auto_schedule);
|
||||
setAccessMode(channel.access_mode ?? "public");
|
||||
setAccessMode((channel.access_mode as AccessMode) ?? "public");
|
||||
setAccessPassword("");
|
||||
setLogo(channel.logo ?? null);
|
||||
setLogoPosition(channel.logo_position ?? "top_right");
|
||||
setLogoPosition((channel.logo_position as LogoPosition) ?? "top_right");
|
||||
setLogoOpacity(Math.round((channel.logo_opacity ?? 1) * 100));
|
||||
setWebhookUrl(channel.webhook_url ?? "");
|
||||
setWebhookPollInterval(channel.webhook_poll_interval_secs ?? 5);
|
||||
@@ -164,7 +164,7 @@ export function useChannelForm(channel: ChannelResponse | null) {
|
||||
// Blocks (day-keyed)
|
||||
dayBlocks, setDayBlocks,
|
||||
selectedBlockId, setSelectedBlockId,
|
||||
recyclePolicy, setRecyclePolicy,
|
||||
rotationPolicy, setRotationPolicy,
|
||||
addBlock,
|
||||
updateBlock,
|
||||
removeBlock,
|
||||
|
||||
@@ -33,7 +33,7 @@ export function useImportChannel(token: string | null) {
|
||||
WEEKDAYS.map(d => [d, d === 'monday' ? data.blocks : []])
|
||||
) as Record<Weekday, typeof data.blocks>,
|
||||
},
|
||||
recycle_policy: data.recycle_policy,
|
||||
rotation_policy: data.rotation_policy,
|
||||
},
|
||||
token,
|
||||
);
|
||||
|
||||
@@ -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