0.2.72 - Magic button, Next up fixes
This commit is contained in:
@@ -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 20–200 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 20–200 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
|
||||
|
||||
Reference in New Issue
Block a user