feat: implement HLS streaming support in VideoPlayer and enhance stream URL handling

This commit is contained in:
2026-03-11 20:51:06 +01:00
parent 4789dca679
commit b813594059
7 changed files with 161 additions and 33 deletions

View File

@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { useQueryClient } from "@tanstack/react-query";
import {
VideoPlayer,
ChannelInfo,
@@ -46,6 +47,13 @@ export default function TvPage() {
const [showSchedule, setShowSchedule] = useState(false);
const idleTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
// Video ref — used to resume playback if autoplay was blocked on load
const videoRef = useRef<HTMLVideoElement>(null);
// Stream error recovery
const [streamError, setStreamError] = useState(false);
const queryClient = useQueryClient();
// Tick for live progress calculation (every 30 s is fine for the progress bar)
const [, setTick] = useState(0);
useEffect(() => {
@@ -57,11 +65,12 @@ export default function TvPage() {
const { data: broadcast, isLoading: isLoadingBroadcast } =
useCurrentBroadcast(channel?.id ?? "");
const { data: epgSlots } = useEpg(channel?.id ?? "");
const { data: streamUrl } = useStreamUrl(
channel?.id,
token,
broadcast?.slot.id,
);
const { data: streamUrl } = useStreamUrl(channel?.id, token, broadcast?.slot.id);
// Clear stream error when the slot changes (next item started)
useEffect(() => {
setStreamError(false);
}, [broadcast?.slot.id]);
// ------------------------------------------------------------------
// Derived display values
@@ -87,6 +96,8 @@ export default function TvPage() {
() => setShowOverlays(false),
IDLE_TIMEOUT_MS,
);
// Resume playback if autoplay was blocked (e.g. on page refresh with no prior interaction)
videoRef.current?.play().catch(() => {});
}, []);
useEffect(() => {
@@ -151,6 +162,22 @@ export default function TvPage() {
return () => window.removeEventListener("keydown", handleKey);
}, [nextChannel, prevChannel, toggleSchedule]);
// ------------------------------------------------------------------
// Stream error recovery
// ------------------------------------------------------------------
const handleStreamError = useCallback(() => {
setStreamError(true);
}, []);
const handleRetry = useCallback(() => {
// Bust the cached stream URL so it refetches with the current offset
queryClient.invalidateQueries({
queryKey: ["stream-url", channel?.id, broadcast?.slot.id],
});
setStreamError(false);
}, [queryClient, channel?.id, broadcast?.slot.id]);
// ------------------------------------------------------------------
// Render helpers
// ------------------------------------------------------------------
@@ -178,16 +205,30 @@ export default function TvPage() {
/>
);
}
if (streamError) {
return (
<NoSignal variant="error" message="Stream failed to load.">
<button
onClick={handleRetry}
className="mt-2 rounded-md border border-zinc-700 bg-zinc-800/80 px-4 py-2 text-xs text-zinc-300 transition-colors hover:bg-zinc-700 hover:text-zinc-100"
>
Retry
</button>
</NoSignal>
);
}
if (streamUrl) {
return (
<VideoPlayer
ref={videoRef}
src={streamUrl}
className="absolute inset-0 h-full w-full"
initialOffset={broadcast?.offset_secs}
initialOffset={broadcast?.offset_secs ?? 0}
onStreamError={handleStreamError}
/>
);
}
// Broadcast exists but stream URL resolving — show no-signal until ready
// Broadcast exists but stream URL resolving — show loading until ready
return <NoSignal variant="loading" message="Loading stream…" />;
};