Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 61 additions & 26 deletions lib/plugin/pl_player/controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -924,35 +924,47 @@ class PlPlayerController with BlockConfigMixin {
if (onlyPlayAudio.value) {
video = audio;
} else {
extras['audio-files'] =
'"${Platform.isWindows ? audio.replaceAll(';', r'\;') : audio.replaceAll(':', r'\:')}"';
final escapedAudio = Platform.isWindows
? audio.replaceAll(';', r'\;')
: audio.replaceAll(':', r'\:');
// Android 上旧版 libmpv(如部分 Android TV armeabi-v7a 设备)不支持 loadfile 的 options 参数,
// 传入 extras 会导致整个 loadfile 命令失败(视频和音频都无法加载)。
// 因此 Android 上不使用 extras 传递 audio-files,改为通过 change-list 命令方式(pre-open + post-open 双重保障)。
// 注意:change-list 对 audio-files 属性值的 URL 中的冒号同样需要转义(旧版 mpv 会将冒号解析为 key:value 分隔符)
if (!Platform.isAndroid) {
extras['audio-files'] = '"$escapedAudio"';
}
await player.command(['change-list', 'audio-files', 'clr', '']);
await player.command(['change-list', 'audio-files', 'set', escapedAudio]);
Comment on lines +927 to +938
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change-list audio-files is executed before player.open() on all platforms (including when a previous media is still loaded/paused). This mutates the player's current state and can have side effects (e.g., applying external audio to the currently loaded item) while it’s redundant on non-Android where extras['audio-files'] is still used. Consider limiting the pre-open change-list path to Android only, and/or moving the change-list calls to post-open() where you already re-apply them for Android.

Copilot uses AI. Check for mistakes.
}
if (kDebugMode || Platform.isAndroid) {
String audioNormalization = AudioNormalization.getParamFromConfig(
Pref.audioNormalization,
String audioNormalization = AudioNormalization.getParamFromConfig(
Pref.audioNormalization,
);
if (volume != null && volume.isNotEmpty) {
audioNormalization = audioNormalization.replaceFirstMapped(
loudnormRegExp,
(i) =>
'loudnorm=${volume.format(
Map.fromEntries(
i.group(1)!.split(':').map((item) {
final parts = item.split('=');
return MapEntry(parts[0].toLowerCase(), num.parse(parts[1]));
}),
),
)}',
);
if (volume != null && volume.isNotEmpty) {
audioNormalization = audioNormalization.replaceFirstMapped(
loudnormRegExp,
(i) =>
'loudnorm=${volume.format(
Map.fromEntries(
i.group(1)!.split(':').map((item) {
final parts = item.split('=');
return MapEntry(parts[0].toLowerCase(), num.parse(parts[1]));
}),
),
)}',
);
} else {
audioNormalization = audioNormalization.replaceFirst(
loudnormRegExp,
AudioNormalization.getParamFromConfig(Pref.fallbackNormalization),
);
}
if (audioNormalization.isNotEmpty) {
} else {
audioNormalization = audioNormalization.replaceFirst(
loudnormRegExp,
AudioNormalization.getParamFromConfig(Pref.fallbackNormalization),
);
}
if (audioNormalization.isNotEmpty) {
// Android 上不使用 extras 传递 lavfi-complex,避免旧版 mpv 的 loadfile options 参数导致整体失败
if (!Platform.isAndroid) {
extras['lavfi-complex'] = '"[aid1] $audioNormalization [ao]"';
}
await player.command(['set', 'lavfi-complex', '[aid1] $audioNormalization [ao]']);
}
Comment on lines +962 to 968
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lavfi-complex is now set via player.command(['set', ...]) before player.open(). This changes the player’s state for the currently loaded (paused) media and is also redundant on non-Android where the same option is still passed via extras. To avoid unintended cross-media effects, consider: (1) using extras only on non-Android (no pre-open set), and (2) applying lavfi-complex via command only on Android (ideally post-open() if loadfile replace resets it similarly to audio-files).

Copilot uses AI. Check for mistakes.
}

Expand All @@ -964,17 +976,40 @@ class PlPlayerController with BlockConfigMixin {
),
play: false,
);

// 旧版 libmpv(如部分 armeabi-v7a Android TV 设备)的 loadfile replace 命令会清除 audio-files 属性,
// 因此在 player.open() 之后再次通过 change-list 重新设置,确保旧版 mpv 也能正确加载独立音频流。
// 注意:URL 中的冒号需要转义(旧版 mpv change-list 会将未转义的冒号解析为 key:value 分隔符)
if (Platform.isAndroid) {
if (dataSource.audioSource case final audio? when (audio.isNotEmpty && !onlyPlayAudio.value)) {
final ea = audio.replaceAll(':', r'\:');
await player.command(['change-list', 'audio-files', 'clr', '']);
await player.command(['change-list', 'audio-files', 'set', ea]);
}
Comment on lines +983 to +988
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Android post-open() re-apply logic for audio-files is duplicated here and in refreshPlayer(). Consider extracting a small helper (e.g., _reapplyExternalAudioFilesIfNeeded(player)) to keep the escaping and conditions consistent and reduce the chance of future divergence.

Copilot uses AI. Check for mistakes.
}
}

Future<void>? refreshPlayer() {
if (dataSource is FileSource) {
return null;
}
if (_videoPlayerController?.current.isNotEmpty ?? false) {
return _videoPlayerController!.open(
final future = _videoPlayerController!.open(
_videoPlayerController!.current.last.copyWith(start: position),
play: true,
);
// Android 上需要在 open() 后重新设置 audio-files(旧版 mpv loadfile replace 会清除该属性)
// URL 中的冒号需要转义(旧版 mpv change-list 会将未转义的冒号解析为 key:value 分隔符)
if (Platform.isAndroid) {
if (dataSource.audioSource case final audio? when (audio.isNotEmpty && !onlyPlayAudio.value)) {
final ea = audio.replaceAll(':', r'\:');
return future.then((_) async {
await _videoPlayerController?.command(['change-list', 'audio-files', 'clr', '']);
await _videoPlayerController?.command(['change-list', 'audio-files', 'set', ea]);
});
}
}
return future;
}
return null;
}
Expand Down