docs(player): close three of the four gaps with a real device
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 2m26s
CI / e2e (pull_request) Successful in 9m5s

A Light Phone III (Android 14, SDK 34, arm64, Chrome 113 at 424x439)
was attached after the PR was opened, so what it listed as
unverifiable was checked rather than left as a caveat.

SystemOwnsVolume answers true on the device -- the build tag, the
constant, the field and the generated binding, end to end, which is the
one thing a source sweep only approximates and which nothing else here
compiles at all.  The control is absent in both mount points on the
real engine, and the transport measures 143px, exactly what the
desktop-headless "after" predicted.  A stored volume of 37 survives a
session that demonstrably rewrote the row.

The duck is the one that stays open, and now for a stated reason rather
than for want of hardware: the foreground service omits
setWillPauseWhenDucked from Oreo, so the framework attenuates us itself
and never sends AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -- the device logs
`requestAudioFocus() ... flags=0x0` saying so.  minSdk is 21, so that
path is live code on Android 5.0 to 7.1 and unreachable above it.
Asking for a modern phone will not test it.
This commit is contained in:
2026-08-21 01:34:44 -04:00
parent b43172a60c
commit f26b44db08
2 changed files with 68 additions and 1 deletions
+58
View File
@@ -4649,3 +4649,61 @@ against a real audio-focus change. The first is a source sweep
(`TestPlatformVolumeOwnershipIsDeclaredOncePerPlatform`), the second is
`TestSystemVolumeStillDucks` against the arithmetic. Neither is a
device, and no device was attached.
### The device answered three of the four (measured 2026-08-21, TLP301 / Android 14 / SDK 34 / arm64, Chrome 113 at 424x439)
A Light Phone III was attached after the PR was opened, so what that PR
listed as unverifiable was re-checked rather than left as a caveat.
**The whole chain resolves on the device.** `__yj.call("player.Player.
SystemOwnsVolume", [])` answers `true` — build tag, `platformOwnsVolume`,
`Player.systemVolume` and the generated binding, end to end. That is the
one thing the source sweep only approximates, and it took a real arm64
device because nothing else here compiles the `android` file at all.
(`GOOS=android GOARCH=arm64 CGO_ENABLED=1 go build ./backend/...` with
the NDK's clang compiles it in ~40 s and is worth running first; it
catches a type error but not a wrong constant.)
**The control is absent in both mount points**, on the real engine:
`.bottom-bar volume-control` is `hidden` with an empty shadow root, and
so is `now-playing-view`'s. Measured on the device, transport **143px**,
which is the figure the desktop-headless "after" predicted exactly. The
art is 75px there rather than 68 because the fixture's `.names` block is
one line shorter, not because anything differs.
**Nothing persists a level nobody chose, and this is the measurement
that took some care.** The default (50) surviving proves nothing, since
50 is also what a fresh row holds. So: force-stop, pull `yj.db`, set
`player_state.volume = 37`, push it back through
`run-as … dd` (a `cp` from `/sdcard` is refused — the app sandbox
cannot read it), relaunch, and drive a queue change to make the row be
rewritten. Reading it back **the WAL has to be pulled with it** — the
main file still showed the old `last_track_path` and reads as a write
that never happened. With `yj.db-wal` beside it: `last_track_path` is
the new track, so `saveState` ran, and `volume` is still **37**.
**The duck cannot be verified on this device, and now for a stated
reason rather than for want of hardware.** `WailsForegroundService`
builds its `AudioFocusRequest` without `setWillPauseWhenDucked` on
API >= 26, so the framework attenuates the stream itself and never
delivers `AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK`. The device's own log
says so: `MediaFocusControl: requestAudioFocus() … AA=USAGE_MEDIA/
CONTENT_TYPE_MUSIC … req=1 flags=0x0` — no
`AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS`. **`minSdk` is 21**, so the
Go-side duck is not dead code; it is reachable on Android 5.0 to 7.1
and on nothing newer. Any future "verify ducking on a device" needs one
of those, and asking for a modern phone will not do it.
Two smaller things from the same session.
**A fresh install downloads the real catalog, and it is 209px of the
screen while it does.** `YJ_CORE_INDEX_URL` is stubbed in
`dev-headless.sh` and in CI but is real on a device, so the first
measurement taken was of a screen with `job-band` on it and the art at
**0px**. That is not a defect and not #172 — it is the environment.
`explore.Service.StopIndexBuild` and a relaunch is the clean state.
**The first-run wizard does not dismiss when a library appears by a
route other than its own** (#175) — it was still up, full-screen and
intercepting pointer events, after `AddLibrary` succeeded through the
binding, and was gone after a relaunch. Filed.
+10 -1
View File
@@ -1763,7 +1763,8 @@ nothing else. That is `mediacontrols`' split with
decided against `Player.systemVolume`, a field a test sets either way.
The frontend's absent branch is therefore testable in the component
tier with a stubbed binding, and the constant itself is covered by a
source sweep rather than by a device.
source sweep plus, once, a real arm64 device answering `true` — which
is the only tier that compiles the `android` file at all.
**Mute goes with it, because it is a level of zero by another name**
and because with no control rendered it is the one state on such a
@@ -1782,6 +1783,14 @@ offset arithmetic exactly as it was. It is the only thing that may move
the output on such a platform, and it is the one volume-shaped path
that is not refused.
One thing to know before anyone offers to test it on a phone: **the
duck is unreachable above API 25.** `WailsForegroundService` builds its
`AudioFocusRequest` without `setWillPauseWhenDucked` from Oreo, so the
framework attenuates us itself and never sends
`AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK` — a device confirms it by logging
`requestAudioFocus() … flags=0x0`. `minSdk` is 21, so this is live code
rather than dead, on Android 5.0 to 7.1 and nowhere else.
**And below 600px that bar carries three controls, not five** (#59).
Shuffle, repeat and the queue button leave it; what is left is art,
title/artist, favourite, and prev/play/next. `player-controls` is one