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:
+12
-2
@@ -3343,9 +3343,19 @@ export type StreamKind = "audio" | "video" | "subtitle" |
|
||||
*/
|
||||
export type StreamQualityResponse =
|
||||
/**
|
||||
* The native backend was reloaded here; nothing left for the frontend.
|
||||
* The native backend was reloaded here; nothing left for the frontend to
|
||||
* *do* — but it still has to be told what was negotiated.
|
||||
*
|
||||
* This carried only a position at first, which left the picker on Android
|
||||
* pinned to the rendition of the *first* stream: the UI derives the rung in
|
||||
* force from the selection it holds, nothing replaced that selection on the
|
||||
* native path, 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.
|
||||
*
|
||||
* TRACES: UR-074, UR-079 | DR-225, DR-226
|
||||
*/
|
||||
{ strategy: "native"; position: number } |
|
||||
{ strategy: "native"; selection: StreamSelection; position: number } |
|
||||
/**
|
||||
* HTML5 must reload its element with this selection.
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -221,8 +221,9 @@ async function setStreamQuality(
|
||||
await adapter.reloadSource(response.selection, response.position ?? currentPosition ?? 0);
|
||||
return response.selection;
|
||||
}
|
||||
// A native backend reloaded itself; there is no selection on that branch.
|
||||
return null;
|
||||
// The native backend reloaded itself, but still reports what it opened — the
|
||||
// caller needs it to show the rung actually in force.
|
||||
return response.selection ?? null;
|
||||
}
|
||||
|
||||
async function next() {
|
||||
|
||||
Reference in New Issue
Block a user