feat(tv-page): add subtitle track toggle functionality

This commit is contained in:
2026-03-16 02:42:24 +01:00
parent abcf872d2d
commit b35054f23e
3 changed files with 177 additions and 14 deletions

View File

@@ -395,7 +395,7 @@ impl IMediaProvider for JellyfinMediaProvider {
impl JellyfinMediaProvider { impl JellyfinMediaProvider {
fn hls_url(&self, item_id: &MediaItemId, bitrate: u32) -> String { fn hls_url(&self, item_id: &MediaItemId, bitrate: u32) -> String {
format!( format!(
"{}/Videos/{}/master.m3u8?videoCodec=h264&audioCodec=aac&VideoBitRate={}&mediaSourceId={}&api_key={}", "{}/Videos/{}/master.m3u8?videoCodec=h264&audioCodec=aac&VideoBitRate={}&mediaSourceId={}&SubtitleMethod=Hls&subtitleCodec=vtt&api_key={}",
self.config.base_url, self.config.base_url,
item_id.as_ref(), item_id.as_ref(),
bitrate, bitrate,

View File

@@ -131,7 +131,10 @@ const TOC = [
{ id: "recycle-policy", label: "Recycle policy" }, { id: "recycle-policy", label: "Recycle policy" },
{ id: "import-export", label: "Import & export" }, { id: "import-export", label: "Import & export" },
{ id: "iptv", label: "IPTV export" }, { id: "iptv", label: "IPTV export" },
{ id: "channel-password", label: "Channel passwords" }, { id: "access-control", label: "Access control" },
{ id: "channel-logo", label: "Channel logo" },
{ id: "webhooks", label: "Webhooks" },
{ id: "admin", label: "Admin panel" },
{ id: "tv-page", label: "Watching TV" }, { id: "tv-page", label: "Watching TV" },
{ id: "troubleshooting", label: "Troubleshooting" }, { id: "troubleshooting", label: "Troubleshooting" },
]; ];
@@ -545,6 +548,16 @@ Authorization: Bearer <token>
files instead of hls.js. files instead of hls.js.
</P> </P>
<H3>Transcode settings</H3>
<P>
When transcoding is available (<Code>TRANSCODE_DIR</Code> is set),
a gear icon appears in the Dashboard header. Click it to open the{" "}
<strong className="text-zinc-300">Transcode Settings</strong>{" "}
dialog, where you can adjust the cache cleanup TTL how long
transcoded segment files are kept before the hourly cleanup removes
them.
</P>
<H3>Filter support</H3> <H3>Filter support</H3>
<Note> <Note>
When the local files provider is active, the series picker and genre When the local files provider is active, the series picker and genre
@@ -588,10 +601,12 @@ Authorization: Bearer <token>
content, and starts broadcasting immediately. content, and starts broadcasting immediately.
</P> </P>
<Note> <Note>
Schedules are valid for 48 hours. K-TV does not regenerate them Schedules are valid for 48 hours. If the channel&apos;s{" "}
automatically return to the Dashboard and click{" "} <strong className="text-zinc-300">Auto-schedule</strong> toggle is
<strong className="text-zinc-300">Generate</strong> whenever you enabled (in the edit sheet), the server regenerates the schedule
want a fresh lineup. automatically when it expires. Otherwise, return to the Dashboard
and click <strong className="text-zinc-300">Generate</strong>{" "}
whenever you want a fresh lineup.
</Note> </Note>
</Section> </Section>
@@ -696,6 +711,16 @@ Authorization: Bearer <token>
"string[]", "string[]",
"Jellyfin library / folder IDs. Find the ID in the Jellyfin URL when browsing a library. Leave empty to search all libraries.", "Jellyfin library / folder IDs. Find the ID in the Jellyfin URL when browsing a library. Leave empty to search all libraries.",
], ],
[
<Code key="sn">series_names</Code>,
"string[]",
"Only include episodes from the listed TV series (OR-combined). Jellyfin only.",
],
[
<Code key="st">search_term</Code>,
"string",
"Free-text search passed to the provider.",
],
]} ]}
/> />
<Note> <Note>
@@ -924,19 +949,40 @@ Output only valid JSON matching this structure:
</Section> </Section>
{/* ---------------------------------------------------------------- */} {/* ---------------------------------------------------------------- */}
<Section id="channel-password"> <Section id="access-control">
<H2>Channel passwords</H2> <H2>Access control</H2>
<P> <P>
Individual channels can be protected with an optional password. When Each channel has an <Code>access_mode</Code> field that controls who
set, TV viewers are prompted to enter the password before the stream can watch it. Set it in the edit sheet.
plays. Channels without a password are always public.
</P> </P>
<Table
head={["Mode", "Description"]}
rows={[
[
<Code key="pub">public</Code>,
"Anyone can watch. This is the default.",
],
[
<Code key="pp">password_protected</Code>,
"Viewers must enter a password before the stream plays.",
],
[
<Code key="ar">account_required</Code>,
"Viewers must be logged in to any K-TV account.",
],
[
<Code key="oo">owner_only</Code>,
"Only the channel owner can watch.",
],
]}
/>
<H3>Setting a password</H3> <H3>Setting a password</H3>
<P> <P>
Enter a password in the <strong className="text-zinc-300">Password</strong>{" "} When <Code>access_mode</Code> is{" "}
field when creating a channel or editing it in the Dashboard. Leave <Code>password_protected</Code>, enter a value in the{" "}
the field blank to remove an existing password. <strong className="text-zinc-300">Password</strong> field in the
edit sheet. Leave the field blank to remove an existing password.
</P> </P>
<Warn> <Warn>
@@ -947,6 +993,88 @@ Output only valid JSON matching this structure:
</Warn> </Warn>
</Section> </Section>
{/* ---------------------------------------------------------------- */}
<Section id="channel-logo">
<H2>Channel logo</H2>
<P>
A logo can be shown as a watermark overlay in the TV player. Set
these fields in the channel edit sheet:
</P>
<Table
head={["Field", "Description"]}
rows={[
[
"Logo",
"URL or inline SVG markup. The image is rendered as a semi-transparent overlay on the video.",
],
[
"Logo position",
<>
Corner where the logo appears.{" "}
<Code>top_right</Code> (default) /{" "}
<Code>top_left</Code> /{" "}
<Code>bottom_left</Code> /{" "}
<Code>bottom_right</Code>.
</>,
],
[
"Logo opacity",
"A value from 0.0 (invisible) to 1.0 (fully opaque). Controls how prominent the watermark is.",
],
]}
/>
</Section>
{/* ---------------------------------------------------------------- */}
<Section id="webhooks">
<H2>Webhooks</H2>
<P>
Channels can fire an HTTP POST to a URL of your choice when domain
events occur (e.g. a schedule is generated). Configure webhooks in
the edit sheet under the{" "}
<strong className="text-zinc-300">Webhook</strong> section.
</P>
<H3>Preset formats</H3>
<Table
head={["Preset", "Description"]}
rows={[
["Default", "Simple JSON with event name, timestamp, and data."],
["Discord", "Formatted Discord embed message via a webhook URL."],
["Slack", "Slack Block Kit message via an incoming webhook URL."],
["Custom", "Write your own Handlebars template (see below)."],
]}
/>
<H3>Template variables</H3>
<P>
Custom templates use Handlebars syntax. Available variables:
</P>
<Table
head={["Variable", "Description"]}
rows={[
[<Code key="ev">{"{{event}}"}</Code>, "Event name, e.g. schedule_generated."],
[<Code key="ts">{"{{timestamp}}"}</Code>, "ISO 8601 timestamp of the event."],
[<Code key="ti">{"{{data.item.title}}"}</Code>, "Title of the affected media item (where applicable)."],
]}
/>
<H3>Extra headers</H3>
<P>
Use <Code>webhook_headers</Code> to add custom HTTP headers to
every delivery for example{" "}
<Code>Authorization: Bearer </Code> for endpoints that require
authentication.
</P>
<H3>Poll interval</H3>
<P>
<Code>webhook_poll_interval_secs</Code> controls how often the
backend checks for pending webhook deliveries. Lower values mean
faster delivery but more database reads.
</P>
</Section>
{/* ---------------------------------------------------------------- */} {/* ---------------------------------------------------------------- */}
<Section id="tv-page"> <Section id="tv-page">
<H2>Watching TV</H2> <H2>Watching TV</H2>
@@ -1019,6 +1147,32 @@ Output only valid JSON matching this structure:
</P> </P>
</Section> </Section>
{/* ---------------------------------------------------------------- */}
<Section id="admin">
<H2>Admin panel</H2>
<P>
The <Code>/admin</Code> route is available to any logged-in user.
It provides two live views into the running server:
</P>
<Table
head={["Tab", "Description"]}
rows={[
[
"Server logs",
"Live stream of backend log lines via SSE. Each entry shows the log level, target module, message, and timestamp.",
],
[
"Activity log",
"Last 50 in-app events such as schedule generations, channel creates/updates, and other domain actions.",
],
]}
/>
<Note>
Access requires a valid login session. Unauthenticated visitors are
redirected to the login page.
</Note>
</Section>
{/* ---------------------------------------------------------------- */} {/* ---------------------------------------------------------------- */}
<Section id="troubleshooting"> <Section id="troubleshooting">
<H2>Troubleshooting</H2> <H2>Troubleshooting</H2>

View File

@@ -429,6 +429,14 @@ function TvPageContent() {
case "M": case "M":
toggleMute(); toggleMute();
break; break;
case "c":
case "C":
if (subtitleTracks.length > 0) {
setActiveSubtitleTrack((prev) =>
prev === -1 ? subtitleTracks[0].id : -1,
);
}
break;
default: { default: {
if (e.key >= "0" && e.key <= "9") { if (e.key >= "0" && e.key <= "9") {
setChannelInput((prev) => { setChannelInput((prev) => {
@@ -464,6 +472,7 @@ function TvPageContent() {
channelCount, channelCount,
switchChannel, switchChannel,
resetIdle, resetIdle,
subtitleTracks,
]); ]);
// ------------------------------------------------------------------ // ------------------------------------------------------------------