Skip to content

feat(player): opt-in stream information caption with playlist name - #210

Open
bilipp wants to merge 1 commit into
mainfrom
feat/channel-info-overlay
Open

feat(player): opt-in stream information caption with playlist name#210
bilipp wants to merge 1 commit into
mainfrom
feat/channel-info-overlay

Conversation

@bilipp

@bilipp bilipp commented Sep 1, 2026

Copy link
Copy Markdown
Owner

What

Brings an in-player stream information caption to iOS, iPadOS, macOS and visionOS, shows the owning playlist's name in it, and adds a Simple / Advanced detail-level preference.

Until now the only information surface in the app was the tvOS player chrome — TVPlayerControlsOverlay.titleBlock and the Info panel, every file behind them wrapped in #if os(tvOS). The four non-tvOS engine overlays rendered only title, subtitle and a LIVE dot. So this builds one shared leaf rather than flipping a gate.

Simple

Example · NEWS

Advanced

Example · NEWS · 1080p · H264 · 24 fps · KSPlayer

Settings (Player section, free, no paywall)

Stream Information
  Show Stream Information          [OFF]
  Detail Level                  Simple ⌄
  Shows the playlist, category, and what's on now.

Design decisions

  • Rides the existing controls and inherits their auto-hide. No new button, panel or gesture — the tap-to-toggle, the scrubber drag, the interactive fullScreenCover dismiss and macOS .onContinuousHover already compete on that surface. (tvOS's "show it briefly on channel change" has no iOS equivalent: switchLiveChannel / LiveChannelNavigator exist only inside #if os(tvOS).)
  • Off by default, free for everyone.
  • One shared leaf mounted from all four engine overlays, the way PlayerEpisodeOverlays already is — never four inline edits to four near-identical title blocks. tvOS and non-tvOS both derive from the same PlayerInfoSnapshot, so they cannot drift.
  • tvOS defaults to Advanced, so its caption is unchanged apart from the added playlist name. It gains the same Detail Level picker in its player settings pane.
  • The playlist is the one that owns the item, recovered from the content id's UUID prefix with a single predicated fetchLimit = 1 fetch. Not the globally selected playlist — that would be actively wrong for cross-playlist search, the Other Sources rail, Downloads, Continue Watching and lume://play deep links. No ?? playlists.first fallback: a miss collapses the row.
  • No schema change, and no new field on PlayableMedia — it is Codable, is the macOS WindowGroup value and is JSON-persisted by PlaybackResumeStore for the Live Activity's lume://resume.

Two pre-existing bugs fixed on the way

  • KSPlayer, the default engine, had no videoInfo off tvOS. Its only producer lived in the tvOS-gated KSTVPlaybackEngine.refreshVideoInfo(). Lifted verbatim into an ungated KSVideoInfo that both platforms now call — otherwise the Advanced readout would be permanently blank for most users.
  • PlayerVideoInfo.captionParts hardcoded "… fps" and String(format: "%.2f") — untranslated, and a period decimal separator that is wrong in de/fr/es/pt/it. Now localized and locale-aware, with the English rendering unchanged.

Graceful degradation

Elements with no value collapse rather than emitting a dangling separator — the common case, since AVPlayer reports fps: 0, codec: nil (and an active AirPlay route silently forces AVPlayer), and a live channel with no matched XMLTV has no programme. Covered by tests.

Performance

Preferences are device-local UserDefaults read directly, with @AppStorage only in the leaf — hoisting it into an engine view or FullScreenPlayerView would re-render the whole player tree on every toggle. The per-stream resolve hangs off the same enabled gate, so a viewer with the caption off pays for no fetch at all. EPG comes from ChannelEPGLoader off-main, never a @Query<EPGListing>.

Verification

Builds iOS ✅ · iPadOS ✅ · tvOS ✅ · macOS ✅ · visionOS ⚠️ not built
Tests 1004/1004 pass (30 new)
Lint vendored swiftformat 0/31 need formatting; swiftlint --strict exit 0
Localization 12 new keys, all 9 locales, check-translations.swift → OK

Also verified by hand on an iPhone 17 Pro simulator against the example server: both detail levels render, the playlist name is correct, and a channel with no EPG collapses cleanly.

Reviewer notes

  • visionOS is unbuilt — the platform is not installed on the machine this was developed on. The #if !os(tvOS) gate ships it there and no visionOS-specific API is involved, so the risk is layout rather than compilation, but it has not been compiled.
  • Advanced truncates on iPhone portrait. The caption is one line at .caption with minimumScaleFactor(0.7), riding the control column above the transport; with every Advanced element the engine name gets clipped. Fine in landscape and on iPad/Mac. Worth deciding separately whether Advanced should drop the engine name on compact widths.
  • Dynamic Type is clamped at accessibility1 — above that the caption crowds the title and scrubber off the video. VoiceOver still reads the full expanded label ("24 frames per second", not "24 f p s") as a single element regardless of rendered size.
  • .gitignore gains a bare ExampleData line; the existing ExampleData/ does not match the symlink a worktree needs.

Adds an in-player stream-information caption to iOS, iPadOS, macOS and
visionOS, and puts the owning playlist's name into it. Until now the only
information surface in the app was the tvOS player chrome — every file
behind it is wrapped in `#if os(tvOS)` — so this builds one shared leaf
rather than flipping a gate.

The caption rides the existing controls overlay and inherits its auto-hide:
no new button, panel or gesture competing with the tap-to-toggle, the
scrubber drag and the interactive dismiss already on the video surface.
It is off by default and free.

A two-level Simple/Advanced preset decides how much it spells out. Simple
shows the playlist, category and what's on now; Advanced adds quality,
codec, frame rate and the active engine. tvOS defaults to Advanced, so its
caption is unchanged apart from the added playlist name, and it gains the
same picker in its player settings pane.

The playlist is the one that *owns* the playing item, recovered from the
content id's UUID prefix with a single predicated fetch — not the globally
selected playlist, which would be wrong for cross-playlist search,
Downloads, Continue Watching and deep links. No schema change, and no new
field on the Codable `PlayableMedia` that macOS window restore and the
Live Activity's `lume://resume` persist.

Two pre-existing bugs fixed on the way:

- KSPlayer, the default engine, had no `videoInfo` off tvOS — its only
  producer lived inside the tvOS-gated `KSTVPlaybackEngine`. Lifted verbatim
  into an ungated `KSVideoInfo` that both platforms call, so the Advanced
  readout is not permanently blank for most users.
- `PlayerVideoInfo.captionParts` hardcoded `"… fps"` and a `%.2f` period
  decimal separator, which is untranslated and wrong in de/fr/es/pt/it.

Elements with no value collapse rather than emitting a dangling separator —
the common case, since AVPlayer reports no codec or frame rate and a live
channel with no matched XMLTV has no programme.

Preferences are device-local `UserDefaults` read directly, with `@AppStorage`
only in the leaf; hoisting it would re-render the whole player tree on every
toggle. The per-stream resolve hangs off the same enabled gate, so a viewer
with the caption off pays for no fetch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant