fix(player): tell the native backend's caller what it actually did

Two defects found by running on an Android tablet, both invisible on the
desktop, and both the same mistake: a rule written for the webview applied to a
backend that is not one.

The quality picker froze on the first stream. `StreamQualityResponse::Native`
carried only a position, so nothing replaced the selection the UI holds after a
native quality change. The picker derives the rung in force from that
selection's rendition, and a transcode always has a rendition — so the fallback
that would have used the requested value was never reached. The stream changed
and the menu did not. The native variant now carries the `StreamSelection` the
backend opened, like the HTML5 variant already did.

This was invisible on the desktop because the webview path replaces the
selection as a side effect of reloading its element. It looked correct there for
a reason that does not generalise.

A quality change restarted playback from zero. The resume position came from
`videoElement.currentTime`, which the frontend cannot supply on a native backend
— there is no `<video>` element, so it correctly sends null and the backend
substituted 0. Reading it from the DOM at all inverts the rule that the player
is the authority on playback state; the fallback now asks the controller where
it is. Captured before the negotiation round-trip, so it resumes a few hundred
milliseconds behind rather than ahead, which is the right direction to err.

Also from the tablet, and NOT fixed here because it changes playback behaviour
and deserves its own change: `audio_forces_transcode` judges against
`WEBVIEW_AUDIO_CODECS` on every platform, and `video_audio_codecs` narrows the
advertised direct-play audio set to that same webview list. On Android the
decoder is ExoPlayer. The tablet reports dts among its platform codecs, has it
stripped from the profile, and then has the webview rule force a transcode for
it. That is the third instance of a decode capability tied to the wrong
renderer, and it is what DR-233 exists to collapse — evidence now, not a design
preference.

It also corrects the record on this branch's headline number. The measured 85%
direct-play rate used a hypothetical Android profile including ac3/eac3; this
tablet's MediaCodecList reports neither, so eac3 content — about a third of the
sampled library — correctly transcodes here. 85% was the ceiling of a profile
the app does not send, on hardware that could not use it. The negotiation and
the contract are sound; the figure was not a measurement of what ships.
This commit is contained in:
2026-08-22 13:45:03 +02:00
parent 84cf31b929
commit 4f6cf22419
4 changed files with 66 additions and 12 deletions
+12 -5
View File
@@ -2415,11 +2415,18 @@
mediaSourceId ?? null,
selectedAudioTrackIndex,
);
// The HTML5 path reloads through the adapter, which already set the new
// selection via the bridge. The native path reloads inside Rust and
// returns nothing, so record what was asked for as the ceiling in force.
if (!negotiated) {
defaultQuality = quality;
// Adopt whatever the backend says it opened. The HTML5 path has already
// set this via the adapter bridge, so this is a no-op there; the native
// path reloads inside Rust and this is the only thing that updates the UI.
//
// Assigning it is what keeps the picker honest: `selectedQuality` reads
// the selection's rendition, and a transcode always has one — so without
// this the menu stayed on the first stream's rung while the stream itself
// changed underneath.
//
// TRACES: UR-074, UR-079 | DR-225, DR-226
if (negotiated) {
currentSelection = negotiated;
}
if (videoElement && !videoElement.paused) {
startTimeUpdates();