diff --git a/android/app/src/main/cpp/pstr_mpv.cpp b/android/app/src/main/cpp/pstr_mpv.cpp index dffa0ba..ff1d808 100644 --- a/android/app/src/main/cpp/pstr_mpv.cpp +++ b/android/app/src/main/cpp/pstr_mpv.cpp @@ -323,8 +323,11 @@ class Player { // 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 // 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"); pending_start_.store(std::max(0.0, start)); // Retire the outgoing file's position, duration and end flag *before* diff --git a/android/app/src/main/java/io/narl/protonstream/playback/NativeMpvHost.kt b/android/app/src/main/java/io/narl/protonstream/playback/NativeMpvHost.kt index 8815ca8..7fa0fd4 100644 --- a/android/app/src/main/java/io/narl/protonstream/playback/NativeMpvHost.kt +++ b/android/app/src/main/java/io/narl/protonstream/playback/NativeMpvHost.kt @@ -322,7 +322,17 @@ class NativeMpvHost private constructor(private val nativeHandle: Long) : Libmpv fun selectTrack(track: MpvTrack?) { val audio = track?.type == "audio" withOpenHandle { nativeSelectTrack(it, audio, track?.id ?: -1) } - 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. + 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() + } } /** @@ -558,6 +568,9 @@ class NativeMpvHost private constructor(private val nativeHandle: Long) : Libmpv private const val CACHED_UNTIL = 13 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 { System.loadLibrary("pstr_android") System.loadLibrary("pstr_mpv") diff --git a/docs/BUGS.md b/docs/BUGS.md index 8f7607d..938168d 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -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 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 -repository root; Phases 1 to 5 of it have landed except for the tail of B41 and -one row of Phase 5's table — everything from B14 to B39 is below in **Fixed**, -and B41 states which of its items are done. +repository root; Phases 1 to 5 of it have landed except for one row of Phase +5's table — everything from B14 to B39 is below in **Fixed**, and B41's items +have all landed. ### B41 — assorted correctness and hygiene defects -Individually small, grouped so they are not lost. The items marked **Landed** -came with Phases 4 and 5; the rest are still open, which is why this entry is. -What remains is one item: the asynchronous track-list read-back. +Individually small, grouped so they are not lost. Every item has now landed; +the entry stays here, with B42, until a device run confirms them. -- `NativeMpvHost.kt:131-135` reads the track list back immediately after an - asynchronous property set, so the selection tick lags by up to a second; - `alang`/`slang` are applied only at load (`pstr_mpv.cpp:225-227`), so a - language change takes effect one episode late. +- **Landed.** `NativeMpvHost.selectTrack` read the track list back straight + after setting `aid`/`sid`, before mpv had made the switch, so the tick in the + track chooser stayed on the old track until the next poll, up to a second + 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 `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 diff --git a/ios/ProtonStream/Player/MpvPlayer.swift b/ios/ProtonStream/Player/MpvPlayer.swift index 1fce6c8..bf68f4b 100644 --- a/ios/ProtonStream/Player/MpvPlayer.swift +++ b/ios/ProtonStream/Player/MpvPlayer.swift @@ -32,7 +32,7 @@ struct MpvTrack: Identifiable, Hashable { let type: String let language: String let title: String - let selected: Bool + var selected: Bool /// The container marks it as the default choice. var isDefault = false /// 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. func select(type: String, track: MpvTrack?) { 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