fix(android,ios): the track tick moves when a track is chosen, and no file inherits another's languages

Choosing an audio or subtitle track read the track list back before mpv
had made the switch, so the tick stayed on the old track until the next
poll. The tick now moves on the choice, and the list is read again
300 ms later to confirm it.

On Android, `load` set `alang` and `slang` only when they were not
empty. mpv keeps both across `loadfile`, so a file with no language
preference took the previous file's. Both are now set on every load.
This closes the last open item of B41.
This commit is contained in:
nvrl committed 2026-10-31 03:26:39 +01:00
1 parent 973a8f2749
commit 3fe4900337
4 files changed
+48 -14

No files matched your search

+5 -2
View File
@@ -323,8 +323,11 @@ class Player {
// needs no window, and a load that cannot get one plays audio anyway — // needs no window, and a load that cannot get one plays audio anyway —
// waiting for a surface here is what made background playback fail after // waiting for a surface here is what made background playback fail after
// a five-second stall. // a five-second stall.
if (!audio.empty()) set_string("alang", audio); //
if (!subtitle.empty()) set_string("slang", subtitle); // Set even when empty: mpv keeps both across `loadfile`, so a file
// with no language preference used to inherit the last one's.
set_string("alang", audio);
set_string("slang", subtitle);
set_string("sid", subtitles ? "auto" : "no"); set_string("sid", subtitles ? "auto" : "no");
pending_start_.store(std::max(0.0, start)); pending_start_.store(std::max(0.0, start));
// Retire the outgoing file's position, duration and end flag *before* // Retire the outgoing file's position, duration and end flag *before*
@@ -322,8 +322,18 @@ class NativeMpvHost private constructor(private val nativeHandle: Long) : Libmpv
fun selectTrack(track: MpvTrack?) { fun selectTrack(track: MpvTrack?) {
val audio = track?.type == "audio" val audio = track?.type == "audio"
withOpenHandle { nativeSelectTrack(it, audio, track?.id ?: -1) } withOpenHandle { nativeSelectTrack(it, audio, track?.id ?: -1) }
// mpv makes the switch on its own thread and lists it a moment later,
// so a track list read back now still ticks the old track. The tick
// moves here, and a read shortly after confirms it.
val type = if (audio) "audio" else "sub"
mutableTracks.value = mutableTracks.value.map {
if (it.type == type) it.copy(selected = it.id == track?.id) else it
}
scope.launch(Dispatchers.Default) {
delay(TRACK_SETTLE_MS)
refreshTracks() refreshTracks()
} }
}
/** /**
* Explicit user stop: stop media and retire the foreground service. * Explicit user stop: stop media and retire the foreground service.
@@ -558,6 +568,9 @@ class NativeMpvHost private constructor(private val nativeHandle: Long) : Libmpv
private const val CACHED_UNTIL = 13 private const val CACHED_UNTIL = 13
private const val LOADED = 14 private const val LOADED = 14
/** How long after a track switch mpv's track list is read again. */
private const val TRACK_SETTLE_MS = 300L
init { init {
System.loadLibrary("pstr_android") System.loadLibrary("pstr_android")
System.loadLibrary("pstr_mpv") System.loadLibrary("pstr_mpv")
+17 -10
View File
@@ -12,20 +12,27 @@ All of B14–B33 came out of a static review of the Android client
device** — there was no Android acceptance harness to reproduce them with, which device** — there was no Android acceptance harness to reproduce them with, which
is itself B42. Each states the reasoning that identifies it; treat the is itself B42. Each states the reasoning that identifies it; treat the
reproduction step as part of the fix. The remediation order is `plan.md` at the reproduction step as part of the fix. The remediation order is `plan.md` at the
repository root; Phases 1 to 5 of it have landed except for the tail of B41 and repository root; Phases 1 to 5 of it have landed except for one row of Phase
one row of Phase 5's table — everything from B14 to B39 is below in **Fixed**, 5's table — everything from B14 to B39 is below in **Fixed**, and B41's items
and B41 states which of its items are done. have all landed.
### B41 — assorted correctness and hygiene defects ### B41 — assorted correctness and hygiene defects
Individually small, grouped so they are not lost. The items marked **Landed** Individually small, grouped so they are not lost. Every item has now landed;
came with Phases 4 and 5; the rest are still open, which is why this entry is. the entry stays here, with B42, until a device run confirms them.
What remains is one item: the asynchronous track-list read-back.
- `NativeMpvHost.kt:131-135` reads the track list back immediately after an - **Landed.** `NativeMpvHost.selectTrack` read the track list back straight
asynchronous property set, so the selection tick lags by up to a second; after setting `aid`/`sid`, before mpv had made the switch, so the tick in the
`alang`/`slang` are applied only at load (`pstr_mpv.cpp:225-227`), so a track chooser stayed on the old track until the next poll, up to a second
language change takes effect one episode late. later. The tick now moves when the track is chosen, and the list is read again
300 ms after. The second half of the report, a language change landing one
episode late, did not hold up: the choice is saved from the chosen track, and
the next episode loads with it. What was wrong nearby is that `load` set
`alang` and `slang` only when they were not empty, and mpv keeps both across
`loadfile`, so a file with no language preference took the previous file's.
Both are set on every load now. The iOS client had the same early read in
`MpvPlayer.select` and takes the same fix; it already set both languages on
every load.
- **Landed.** `NativeMpvHost.kt` polled on `Dispatchers.Main.immediate`, and - **Landed.** `NativeMpvHost.kt` polled on `Dispatchers.Main.immediate`, and
`tracks_json` issues five `mpv_get_property` calls per track — 51 synchronous `tracks_json` issues five `mpv_get_property` calls per track — 51 synchronous
property reads on the UI thread for a 10-track file, contending on the core property reads on the UI thread for a 10-track file, contending on the core
+13 -2
View File
@@ -32,7 +32,7 @@ struct MpvTrack: Identifiable, Hashable {
let type: String let type: String
let language: String let language: String
let title: String let title: String
let selected: Bool var selected: Bool
/// The container marks it as the default choice. /// The container marks it as the default choice.
var isDefault = false var isDefault = false
/// The container marks it as forced: on-screen text only, by convention. /// The container marks it as forced: on-screen text only, by convention.
@@ -303,7 +303,18 @@ final class MpvPlayer {
/// A track, or none: `nil` turns subtitles off. /// A track, or none: `nil` turns subtitles off.
func select(type: String, track: MpvTrack?) { func select(type: String, track: MpvTrack?) {
setString(type == "audio" ? "aid" : "sid", track.map { String($0.id) } ?? "no") setString(type == "audio" ? "aid" : "sid", track.map { String($0.id) } ?? "no")
refreshTracks() // mpv makes the switch on its own thread and lists it a moment later,
// so a track list read back now still ticks the old track. The tick
// moves here, and a read shortly after confirms it.
tracks = tracks.map { listed in
var listed = listed
if listed.type == type { listed.selected = listed.id == track?.id }
return listed
}
Task { [weak self] in
try? await Task.sleep(for: .milliseconds(300))
self?.refreshTracks()
}
} }
/// Showing no picture while backgrounded keeps the audio playing: iOS /// Showing no picture while backgrounded keeps the audio playing: iOS