From b2f40054fc9e57bfd0995e08740ddd6e396c87dd Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Wed, 11 Mar 2026 21:55:20 +0100 Subject: [PATCH] feat: add subtitle track support to VideoPlayer and integrate with TvPage --- .../app/(main)/tv/components/video-player.tsx | 40 +++++++++++- k-tv-frontend/app/(main)/tv/page.tsx | 63 ++++++++++++++++++- 2 files changed, 100 insertions(+), 3 deletions(-) diff --git a/k-tv-frontend/app/(main)/tv/components/video-player.tsx b/k-tv-frontend/app/(main)/tv/components/video-player.tsx index 71a2695..35d2f61 100644 --- a/k-tv-frontend/app/(main)/tv/components/video-player.tsx +++ b/k-tv-frontend/app/(main)/tv/components/video-player.tsx @@ -1,17 +1,37 @@ import { forwardRef, useEffect, useRef } from "react"; import Hls from "hls.js"; +export interface SubtitleTrack { + id: number; + name: string; + lang?: string; +} + interface VideoPlayerProps { src?: string; poster?: string; className?: string; /** Seconds into the current item to seek on load (broadcast sync). */ initialOffset?: number; + /** Active subtitle track index, or -1 to disable. */ + subtitleTrack?: number; onStreamError?: () => void; + onSubtitleTracksChange?: (tracks: SubtitleTrack[]) => void; } const VideoPlayer = forwardRef( - ({ src, poster, className, initialOffset = 0, onStreamError }, ref) => { + ( + { + src, + poster, + className, + initialOffset = 0, + subtitleTrack = -1, + onStreamError, + onSubtitleTracksChange, + }, + ref, + ) => { const internalRef = useRef(null); const hlsRef = useRef(null); @@ -21,12 +41,20 @@ const VideoPlayer = forwardRef( else if (ref) ref.current = el; }; + // Apply subtitle track changes without tearing down the HLS instance + useEffect(() => { + if (hlsRef.current) { + hlsRef.current.subtitleTrack = subtitleTrack; + } + }, [subtitleTrack]); + useEffect(() => { const video = internalRef.current; if (!video || !src) return; hlsRef.current?.destroy(); hlsRef.current = null; + onSubtitleTracksChange?.([]); const isHls = src.includes(".m3u8"); @@ -41,6 +69,16 @@ const VideoPlayer = forwardRef( video.play().catch(() => {}); }); + hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, (_event, data) => { + onSubtitleTracksChange?.( + data.subtitleTracks.map((t) => ({ + id: t.id, + name: t.name, + lang: t.lang, + })), + ); + }); + hls.on(Hls.Events.ERROR, (_event, data) => { if (data.fatal) onStreamError?.(); }); diff --git a/k-tv-frontend/app/(main)/tv/page.tsx b/k-tv-frontend/app/(main)/tv/page.tsx index fd96ce3..b69c6d0 100644 --- a/k-tv-frontend/app/(main)/tv/page.tsx +++ b/k-tv-frontend/app/(main)/tv/page.tsx @@ -10,6 +10,7 @@ import { UpNextBanner, NoSignal, } from "./components"; +import type { SubtitleTrack } from "./components/video-player"; import { useAuthContext } from "@/context/auth-context"; import { useChannels, useCurrentBroadcast, useEpg } from "@/hooks/use-channels"; import { @@ -53,6 +54,11 @@ export default function TvPage() { // Stream error recovery const [streamError, setStreamError] = useState(false); + + // Subtitles + const [subtitleTracks, setSubtitleTracks] = useState([]); + const [activeSubtitleTrack, setActiveSubtitleTrack] = useState(-1); + const [showSubtitlePicker, setShowSubtitlePicker] = useState(false); const queryClient = useQueryClient(); // Tick for live progress calculation (every 30 s is fine for the progress bar) @@ -73,6 +79,13 @@ export default function TvPage() { setStreamError(false); }, [broadcast?.slot.id]); + // Reset subtitle state when channel or slot changes + useEffect(() => { + setSubtitleTracks([]); + setActiveSubtitleTrack(-1); + setShowSubtitlePicker(false); + }, [channelIdx, broadcast?.slot.id]); + // ------------------------------------------------------------------ // Derived display values // ------------------------------------------------------------------ @@ -225,6 +238,8 @@ export default function TvPage() { src={streamUrl} className="absolute inset-0 h-full w-full" initialOffset={broadcast ? calcOffsetSecs(broadcast.slot.start_at) : 0} + subtitleTrack={activeSubtitleTrack} + onSubtitleTracksChange={setSubtitleTracks} onStreamError={handleStreamError} /> ); @@ -254,8 +269,52 @@ export default function TvPage() { className="pointer-events-none absolute inset-0 z-10 flex flex-col justify-between transition-opacity duration-300" style={{ opacity: showOverlays ? 1 : 0 }} > - {/* Top-right: guide toggle */} -
+ {/* Top-right: subtitle picker + guide toggle */} +
+ {subtitleTracks.length > 0 && ( +
+ + + {showSubtitlePicker && ( +
+ + {subtitleTracks.map((track) => ( + + ))} +
+ )} +
+ )} +