1368 字
7 分钟

iOS 本地 FLAC 拖动进度条不准:一次「日志正确但耳朵不对」的排查

2026-01-15
浏览量 加载中...

iOS 本地 FLAC 拖动进度条不准:一次「日志正确但耳朵不对」的排查#

最近在重写一个 Flutter 本地音乐播放器,播放器内核用的是 just_audio + audio_service。本来一切都挺顺的,直到我开始认真测试「拖动播放界面的进度条」:UI 上的进度跳过去了,日志也说 seek 成功了,但耳朵听到的位置明显不对

更离谱的是:哪怕你把 slider 直接拉到 100%,实际也没有播放到结尾,而是停在一个“看起来差不多但就是不对”的位置。

直接拖到结束还在播放

这个问题只在 iOS 的本地 FLAC 上稳定复现(文件放在应用沙盒里,不走系统媒体库)。

现象与复现条件#

  • 平台:iOS(应用沙盒文件)
  • 音频格式:FLAC
  • 表现:
    • seek 日志显示 position 已到目标值
    • 实际听感偏前,拖到结尾仍未到结尾
    • 歌曲时长无异常

日志示例(从 UI slider -> seek -> UI 认为“对齐”):

[SLIDER] onChangeEnd: sliderValue=1.0, duration=247153ms, targetPosition=247153ms, actualPosition=5674ms
[SEEK] Request: target=247153ms, current=5869ms, duration=247153ms
[EFFECTIVE_POS] Aligned! actual=247153ms, pending=247153ms, delta=0ms
[SEEK] After seek(): position=247153ms

你看日志,完全没毛病:目标=247153ms、seek 后 position=247153ms、甚至 UI 的 “effectivePosition” 还打印了 Aligned!。但声音就是没到那儿。

排查路径(以及为什么这些路走不通)#

我一开始的直觉是“时长算错了 / 元数据不准”。毕竟拖到 100% 还不到尾部,很像 duration 出问题。

但很快就排掉了:同一首歌,无论用 on_audio_query 拿的 duration,还是解析 metadata 得到的 duration,显示都正常,且播放从头到尾的总时长也没问题。

接下来我开始怀疑是“UI 算法”或“状态更新延迟”,于是做了两件事:

1)把 slider 侧的 targetPosition 打印清楚#

进度条松手时我会算目标毫秒数并调用 seek(lib/features/player/presentation/screens/player_screen.dart),类似这样:

onChangeEnd: (value) async {
final targetPosition = Duration(
milliseconds: (value * duration.inMilliseconds).toInt(),
);
debugPrint('[SLIDER] onChangeEnd: sliderValue=$value, '
'duration=${duration.inMilliseconds}ms, '
'targetPosition=${targetPosition.inMilliseconds}ms, '
'actualPosition=${state.position.inMilliseconds}ms');
await audioManager?.seek(targetPosition);
}

这一步确认:targetPosition 绝对算对了,而且和 UI 上显示的时间一致。

2)把 AudioManager.seek 的前后状态也打出来#

我在 lib/core/services/audio_manager.dartseek 里加了日志,并且等 ProcessingState.ready

@override
Future<void> seek(Duration position) async {
debugPrint('[SEEK] Request: target=${position.inMilliseconds}ms, '
'current=${_player.position.inMilliseconds}ms, '
'duration=${_player.duration?.inMilliseconds}ms');
await _player.seek(position);
debugPrint('[SEEK] After seek(): position=${_player.position.inMilliseconds}ms');
await _player.processingStateStream
.firstWhere((state) =>
state == ProcessingState.ready ||
state == ProcessingState.completed)
.timeout(const Duration(seconds: 2),
onTimeout: () => ProcessingState.ready);
debugPrint('[SEEK] After ready: position=${_player.position.inMilliseconds}ms, '
'processingState=${_player.processingState}');
}

结果依然很诡异:position 的数值确实跳到了目标位置,ready 也到了,但实际听到的位置还是偏前。

这时候我才意识到:这不是“UI/状态没更新”,而是更底层的东西——iOS 对这个 FLAC 的 seek 本身不精确,而 just_audio 上报的 position 也不代表你耳朵听到的那一帧一定已经对齐。

定位到关键点:iOS 的精确时长/定位选项#

继续往下挖,我去翻了 just_audio 的 Darwin(iOS/macOS)实现。它本质上是用 AVURLAsset 创建 AVPlayerItem。在 Apple 的世界里,“快”和“准”是可以二选一的:默认情况下系统并不一定会用最精确的方式去解析时长与时间轴(尤其是本地文件、尤其是某些格式)。

just_audio 其实提供了一个开关,把它传到 AVURLAssetPreferPreciseDurationAndTimingKey 上:也就是 preferPreciseDurationAndTiming

解决思路是:为 iOS 的 FLAC 文件启用精确时长/时间轴选项

在 just_audio 里,这个选项对应:

DarwinAssetOptions(preferPreciseDurationAndTiming: true)

代码改动#

核心修改在 lib/core/services/audio_manager.dart,我最终做了三件事(这三件事组合起来才稳):

  1. 播放列表构建 AudioSource 时,不再用默认的 AudioSource.uri(...)(它会根据 URI 判断类型,但我需要更明确地控制 options)。
  2. 改用 ProgressiveAudioSource(...),并把 ProgressiveAudioSourceOptions 填进去(只对 iOS/macOS + .flac 启用精确模式)。
  3. 同时把 duration 也传给 source(song.durationAsDuration),作为一个更稳定的兜底(避免某些文件解析 duration 走近似路径)。

关键实现(节选):

AudioSource _buildAudioSource(LocalSongModel song) {
final filePath = song.filePath;
if (filePath != null && filePath.isNotEmpty) {
final file = File(filePath);
if (file.existsSync()) {
return _buildProgressiveSource(
Uri.file(filePath),
song,
isFlac: filePath.toLowerCase().endsWith('.flac'),
);
}
}
final uri = Uri.parse(song.uri);
return _buildProgressiveSource(
uri,
song,
isFlac: uri.path.toLowerCase().endsWith('.flac'),
);
}
AudioSource _buildProgressiveSource(
Uri uri,
LocalSongModel song, {
required bool isFlac,
}) {
final usePreciseTiming =
isFlac && (Platform.isIOS || Platform.isMacOS);
final options = usePreciseTiming
? const ProgressiveAudioSourceOptions(
darwinAssetOptions:
DarwinAssetOptions(preferPreciseDurationAndTiming: true),
)
: null;
return ProgressiveAudioSource(
uri,
tag: song,
duration: song.durationAsDuration,
options: options,
);
}

然后在 setPlaylist 里统一用 _buildAudioSource 生成 playlist 的 children:

final audioSources = songs.map(_buildAudioSource).toList();
await _player.setAudioSource(
ConcatenatingAudioSource(children: audioSources),
initialIndex: _currentIndex,
);

这次修复的关键不是“再等一等”“再对齐一下 position”,而是让 iOS 在解析这个 FLAC 的时间轴时走精确路径。否则你在 Dart 层做再多校验,最终音频解码器/播放器层面还是会“跳到一个差不多的位置”。

结果#

  • FLAC 拖动进度条与实际听感一致
  • 拖到尾部能真正到尾部
  • 日志与听感完全对齐

小结#

这个问题的难点在于:你能拿到的一切“状态”都在告诉你它对了,但最终用户体验仍然是错的
从“检查 duration/元数据”到“怀疑 UI 算法/状态延迟”再到“翻平台实现”,最后才发现真正的开关在 iOS 的 AVURLAsset 上。

如果你遇到类似情况(iOS + FLAC + seek 不准),优先试:

DarwinAssetOptions(preferPreciseDurationAndTiming: true)

最后补一句经验:播放器这种东西,UI 层能做的“看起来正确”很有限;一旦出现「UI 正确但耳朵不对」,多半是平台层(解码/seek/时基)的问题,不要在 Dart 层硬耗太久。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
iOS 本地 FLAC 拖动进度条不准:一次「日志正确但耳朵不对」的排查
https://www.ymxx.net/posts/seedbugfix/
作者
Leguan
发布于
2026-01-15
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Leguan
Hello, I'm Leguan.
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签
站点统计
文章
9
说说
10
分类
3
标签
17
总字数
10,655
运行时长
0
最后活动
0 天前

目录