0.2.72 - Magic button, Next up fixes

This commit is contained in:
ponzischeme89
2026-08-17 11:41:36 +12:00
parent 36cb1324fd
commit 12b27c77c3
2655 changed files with 5435 additions and 254 deletions
+128 -6
View File
@@ -653,6 +653,19 @@ Ten-second progress reports and per-keystroke searches are DEBUG on purpose.
the admin rail — bump it with a meaningful server change; nothing else identifies which
tree a container was deployed from.
**Emby's own version rides the reachability probe**, and the television prints it beside the
gateway's on Settings → About: `0.1.50 (4.10.0.21)`. `emby.Client.Ping` already asks
`/System/Info/Public`, which carries `Version`, so reading it costs nothing — a second call
per probe to learn a string that moves a few times a year would be the wrong trade. Three
things to preserve: the last known version is **kept through an outage**, because About is
exactly the page somebody opens when the server has stopped answering and a version that
blanked itself would replace a fact that is still true with nothing; `version` is **omitted
rather than sent empty**, so a client can tell "no probe has answered yet" from "Emby
answered without one"; and `gatewayVersionLabel` (pure, tested) prints the gateway's version
alone in every unknown case rather than empty brackets, which read as a fault. A gateway that
is not answering prints "Not connected" with no brackets at all — it cannot vouch for what
Emby is running either.
**Admin interface** is `server/internal/api/admin/` — a shell, a stylesheet, a shared
runtime and one fragment per page, all embedded and composed by `admin_console.go` at
start-up into finished bytes per URL. No build step and no CDN: a strict no-dependency
@@ -1335,9 +1348,37 @@ and the app's own startup ~365 ms. Within that, the largest single term is **col
warm connection to Emby** — the same file, same seek, was 4177 ms on the first playback of
a session and 2004 ms on the second. Artwork and API traffic go to the *gateway* host, so
the pool has nothing open to *Emby* when the first playback starts, and that first resume
pays DNS, TCP, TLS and Emby's file open. Pre-warming that connection is the open
opportunity. Two things that look like causes and are not: the subtitle auto-selection
costs 20200 ms, not seconds, and the seek itself is about 900 ms.
pays DNS, TCP, TLS and Emby's file open. Two things that look like causes and are not: the
subtitle auto-selection costs 20200 ms, not seconds, and the seek itself is about 900 ms.
**`StreamWarmer` is what closes that gap** (`data/remote/StreamWarmer.kt`). It opens a
connection to the machine video comes from before anybody asks for video, which is only
worth anything because `HttpStack` shares one connection pool: a connection opened by an
unrelated HEAD request is the connection ExoPlayer picks up later. Nothing is handed over
but the address. Four things to preserve:
- **It warms the host, never the title.** The obvious version asks for the first byte of the
film, which would also warm Emby's file cache — and would put a delivery for a title
nobody watched into somebody's server history. This app already refuses to warm
`PlaybackInfo` on focus for that reason. Any answer establishes the connection, so the
request is a bare HEAD of the origin and a 404 or a 405 is as good as a 200.
- **The address outlives the process.** On the gateway path a resolved stream URL is the only
thing that ever names Emby, so a television that has just started has no idea where the
video lives. Remembering it is what moves the saving to the *first* playback after a cold
start, which is exactly the one that was slowest. It is kept in its own small
`SharedPreferences` file rather than in the settings DataStore, which rewrites and fsyncs
everything it holds on every edit.
- **It is warmed from two places and needs both.** `MembyApp.onCreate` covers the cold start,
and `HomeViewModel.warmDetailPage` covers a viewer who has been browsing for a while —
focus settling on a playable card is the best warning of a Play press this app gets, and
the three-minute interval is shorter than the pool's five-minute keep-alive so that press
meets a live connection rather than one the pool has just evicted.
- **Failure is silent and costs nothing.** It is a performance hint: a server asleep, an
address that has moved, or no network at all each leave playback exactly as slow as it was
before. A failed warm clears the interval so the next attempt is not held off for minutes.
`originOf` is the pure half and is unit-tested — a warm aimed at the wrong host is worse
than none, since it opens a connection nothing will use and leaves the one that matters
cold.
**Playback position has one ordered exit path.** Ten-second progress updates, pause/seek
updates and the final Stop all pass through `EmbyRepository`'s `playbackReportMutex`, so a
@@ -1427,6 +1468,69 @@ inside the running player instead of relaunching the activity, so `itemId`/`play
/`stopReported` must all be reset together or the outgoing episode is never reported
stopped; and a movie simply resolves to null, which is why nothing special-cases item type.
**One next-item pipeline.** `ui/player/NextUpPipeline.kt` owns the answer to "what plays
after this?", and everything that wants to know reads it from there: the manual Next Episode
button, the next-up banner, the credits pane, the countdown and the ended frame. Before it,
each of those read one `nextEpisode` field which was populated only when
`Settings.autoPlayNextEpisode` was on — so with the setting off the field was permanently
null and the credits pane, the banner and the completion handler were code nothing could
reach. Things to preserve:
- **Resolution and automatic advance are two decisions, not one.** `NextUpResolver` resolves
unconditionally for episodic content and `shouldAutoAdvance` is the *only* place the
viewer's setting is read. Somebody who has turned automatic advance off has said they want
to press something, not that they want to be returned to the launcher and made to find the
next episode by hand. Putting that setting back on the lookup restores the original defect.
- **Every trigger goes through `startNextEpisode`**, which claims `advanceRequested`
synchronously. `advancing` cannot do that job: it is not set until the coroutine that
re-negotiates the stream returns, and the credits marker, a 250 ms progress tick and a
button press all land well inside that window — which is how one press used to be able to
produce two advances.
- **The stream is re-negotiated before it is used, never after it fails.** The URL and play
session a lookup returns were obtained when the *current* episode started, which on a
full-length episode is long enough ago for Emby to have expired the session — the
intermittent failure to establish the stream on an advance. `NextUpResolver.playable`
re-fetches past `STREAM_FRESHNESS_MS`, and `warm()` does it ahead of the credits so the
press itself waits on nothing. The metadata is not re-fetched with it: the banner and the
button draw from the stale copy immediately, because only the stream goes off.
- **The resolver is single-flight and keyed on the subject.** Repeated callers join the
outstanding request rather than starting another, and `begin` discards the outgoing
episode's answer so it can never be offered against the incoming one. `resolvedAt` is
nullable rather than a zero sentinel, because `elapsedRealtime` is time since boot and is
legitimately near zero on a television that has just been switched on.
- **The two optional transport controls are removed, not disabled.** `player_next_episode`
and `player_magic` in `memby_player_controls.xml` are `gone` until they mean something: a
television is driven by a D-pad, and a greyed button is a stop on the way to the one the
viewer wanted. Their rules are the pure `shouldOfferNextEpisodeButton` /
`shouldOfferMagicButton`, unit-tested in `NextUpPipelineTest`.
**Magic is "put something on and don't ask me what".** `POST /v1/magic` →
`recommend.MagicPick`, which was written in 0.2.68 and had no route, no client and no way of
being pressed until now. It is the movie player's control and Next Episode is the episode
player's: the picker is films only (`onlyMovies`), on the reasoning that the button plays
something immediately and a series is a question about which episode — and beside an explicit
Next Episode action, a second "play something else" control is two answers to one question.
Things to preserve:
- **The exclusions are the client's.** The film playing now and the last `MAGIC_MEMORY` picks
travel with the request, because it is the player that knows what it has already put in
front of somebody. That is why it is a POST: a query string that lengthens with every press
is one something in the middle eventually truncates, and the failure would be silent
repetition.
- **It is never cached.** The whole point is that pressing it twice gives two answers.
- **Every way it can fail is the same answer to the viewer.** The direct path has nobody to
ask, a gateway predating the route answers 404, and a household with nothing unseen left
answers 404 too — all three withdraw the button rather than showing an error over somebody's
film. Hence `magicAvailable` starting at `ServerConfig.isGateway` and switching off on the
first unanswerable press.
- **The pick is announced before the picture changes.** A button that silently replaces the
programme is one nobody presses twice.
- **A film is a new subject, not the next step of this one**, so it launches through the
ordinary `PlaybackRequest` intent rather than through `playNext`: a fresh session, a fresh
pre-roll decision, exactly as pressing Play on its detail page gives. Nothing is negotiated
before the viewer has been shown what was chosen, the same rule that keeps `PlaybackInfo`
off the focus-warming path.
The optional **next-episode recap or preview** extends that same auto-advance path. Once the
next episode is known, the television searches YouTube in the background using series name,
episode code and title, ranks official previews and recaps above reactions, reviews and
@@ -1663,9 +1767,12 @@ needs no change. Things to preserve:
while the pane is up and the countdown moves into it — that countdown only appears inside
the last minute, where it was always the banner's job. The pane also rides the banner's
250 ms tick and its `nextEpisode` guard, which is exactly the right gate: a film and the
last episode of a season both correctly get nothing. It therefore inherits auto-play's
switch, since nothing resolves a next episode when that is off — deliberate, because this
*is* the auto-advance experience.
last episode of a season both correctly get nothing. It does **not** inherit auto-play's
switch — see "One next-item pipeline" below. It used to, because nothing resolved a next
episode when that was off, which meant this pane and the banner were unreachable code on
every set whose viewer preferred to press something. The pane still appears and its Play
still works; only the countdown, which promises a transition that will happen by itself,
is conditional on the setting.
- **The transform is a scale, never a reparent.** The pre-roll moves the `PlayerView` between
parents; doing that mid-playback tears the SurfaceView down and flashes black over
somebody's credits. `CREDITS_VIDEO_SCALE`/`CREDITS_VIDEO_SHIFT_X` are public so
@@ -1683,6 +1790,21 @@ needs no change. Things to preserve:
- Screenshots are `EndCreditsScreenshotTest` → `build/screenshots/end-credits/`, over a
deliberately bright frame: there is no scrim between the credits and the panel.
**`StartupTrace` times the two waits either side of playback** — opening the app, and
opening a page — because "the launcher feels slow" is no more actionable than "playback is
slow" was, and `PlaybackTrace` already answers the second. Two shapes, because there are two
kinds of question: **launch milestones** (`home_visible`, `first_row_visible`,
`home_interactive`) are cumulative from process start, in the way playback's marks are
cumulative from the Play press, and are recorded once — a relaunch is a new process, and an
activity Android recreated behind somebody is not a second launch; **spans** are repeatable,
which is the shape a detail page needs, since a viewer opens many in a session and each is
its own wait. It is debug-only, like `PerformanceMonitor`, and every entry point returns
before allocating on a release build — a television has no log anybody reads, so in
production these would be pure cost. The milestones are recorded from `LaunchedEffect`s and
never from a composable body: measuring the launcher must never be a reason the launcher
recomposes. Read them with `adb logcat -s MembyStartup`, and the playback half with
`MembyPlayback`.
**Performance instrumentation.** `PerformanceMonitor` (JankStats) is debug-only and logs to
tag `EmbyClientPerf`. `benchmark/` is a `com.android.test` macrobenchmark module targeting
the release variants the `androidx.baselineprofile` plugin generates, so its numbers are