feat(tv-page): add subtitle track toggle functionality
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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'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>
|
||||||
|
|||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user