From dd5466f3da30830d652bf764683b534f6f9d186f Mon Sep 17 00:00:00 2001 From: aviadlevy Date: Fri, 3 Jul 2026 15:02:13 +0300 Subject: [PATCH 1/2] fix: apply audio/subtitle selection after mpv parses tracks At playback start setSubtitleTrack/setAudioTrack ran before media_kit finished parsing the file's tracks (open() returns before the track-list observer fires), so the positional remap resolved to null and the selection was silently skipped. mpv then kept its own default pick, which is wrong when a file has multiple default tracks. Wait for the wanted embedded track to appear on the tracks stream before mapping. The wait only fires when the track is not yet present, is race-free, and is capped at 5s, so already-loaded and external cases are unchanged. Fixes #1031 --- lib/wrappers/players/lib_mpv.dart | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/wrappers/players/lib_mpv.dart b/lib/wrappers/players/lib_mpv.dart index e17a8386a..14d47b81d 100644 --- a/lib/wrappers/players/lib_mpv.dart +++ b/lib/wrappers/players/lib_mpv.dart @@ -426,6 +426,15 @@ class LibMPV extends BasePlayer { @override Future seek(Duration position) async => _player?.seek(position); + // mpv parses tracks asynchronously after open(); at playback start the list is still empty, so a + // positional lookup would miss and leave mpv on its own default pick. Wait (capped) for [index]. + Future _awaitTrack(int index, int Function(mpv.Tracks) count) async { + if (_player == null || index < 0 || count(_player!.state.tracks) > index + 2) return; + await _player!.stream.tracks + .firstWhere((tracks) => count(tracks) > index + 2) + .timeout(const Duration(seconds: 5), onTimeout: () => _player!.state.tracks); + } + @override Future setAudioTrack(AudioStreamModel? model, PlaybackModel playbackModel) async { final wantedAudioStream = model ?? playbackModel.defaultAudioStream; @@ -433,9 +442,10 @@ class LibMPV extends BasePlayer { if (wantedAudioStream.index == AudioStreamModel.no().index) { await _player?.setAudioTrack(mpv.AudioTrack.no()); } else { + final index = (playbackModel.audioStreams?.indexOf(wantedAudioStream) ?? -1) - 1; + await _awaitTrack(index, (tracks) => tracks.audio.length); final internalTracks = audioTracks.getRange(2, audioTracks.length).toList(); - final audioTrack = - internalTracks.elementAtOrNull((playbackModel.audioStreams?.indexOf(wantedAudioStream) ?? -1) - 1); + final audioTrack = internalTracks.elementAtOrNull(index); if (audioTrack != null) { await _player?.setAudioTrack(audioTrack); } @@ -455,9 +465,10 @@ class LibMPV extends BasePlayer { return -1; } _currentSubtitleCodec = wantedSubtitle.codec; + final index = playbackModel.subStreams?.sublist(1).indexWhere((element) => element.id == wantedSubtitle.id) ?? -1; + if (!wantedSubtitle.isExternal) await _awaitTrack(index, (tracks) => tracks.subtitle.length); final internalTrack = subTracks.getRange(2, subTracks.length).toList(); - final index = playbackModel.subStreams?.sublist(1).indexWhere((element) => element.id == wantedSubtitle.id); - final subTrack = internalTrack.elementAtOrNull(index ?? -1); + final subTrack = internalTrack.elementAtOrNull(index); if (wantedSubtitle.isExternal && wantedSubtitle.url != null && subTrack == null) { await _player?.setSubtitleTrack(mpv.SubtitleTrack.uri(wantedSubtitle.url!)); } else if (subTrack != null) { From 10090bc73a775aa545939cb5b487dc8266a77f8d Mon Sep 17 00:00:00 2001 From: aviadlevy Date: Sun, 9 Aug 2026 19:07:01 +0300 Subject: [PATCH 2/2] fix: avoid null assertion in the mpv track-wait timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The onTimeout callback in _awaitTrack ran up to 5 seconds after the await suspended, and dereferenced _player with a bang. By then the player can be null (dispose() nulls it, and the audio-queue path swaps it), so the timeout path threw a TypeError instead of returning the current tracks — turning the graceful-degradation branch into the one that breaks track selection. Hoists _player into a local that flow-promotes to non-nullable, so both the stream subscription and the timeout callback capture a non-null instance and neither needs an assertion. --- lib/wrappers/players/lib_mpv.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/wrappers/players/lib_mpv.dart b/lib/wrappers/players/lib_mpv.dart index 14d47b81d..99304542a 100644 --- a/lib/wrappers/players/lib_mpv.dart +++ b/lib/wrappers/players/lib_mpv.dart @@ -429,10 +429,11 @@ class LibMPV extends BasePlayer { // mpv parses tracks asynchronously after open(); at playback start the list is still empty, so a // positional lookup would miss and leave mpv on its own default pick. Wait (capped) for [index]. Future _awaitTrack(int index, int Function(mpv.Tracks) count) async { - if (_player == null || index < 0 || count(_player!.state.tracks) > index + 2) return; - await _player!.stream.tracks + final player = _player; + if (player == null || index < 0 || count(player.state.tracks) > index + 2) return; + await player.stream.tracks .firstWhere((tracks) => count(tracks) > index + 2) - .timeout(const Duration(seconds: 5), onTimeout: () => _player!.state.tracks); + .timeout(const Duration(seconds: 5), onTimeout: () => player.state.tracks); } @override