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

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

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
相关文章智能推荐
1
在线播放切歌“声音和信息不一致”:一次从入口补丁到状态链路重构的排查
开发日志记录一次 Flutter 音乐播放器在线切歌错位问题:音频已切到下一首,但封面/歌名滞后,甚至要点暂停才刷新。包含踩坑方案、失败原因和最终稳定修复。
2
重装后收藏/喜欢 歌曲不见了:一次「ID 稳定性 + 数据迁移」的修复记录
开发日志Flutter 本地音乐播放器在重装/升级后出现“收藏还在,但喜欢列表空了”的问题。本文记录从日志定位到根因(songId 不稳定)以及如何通过 canonical path + 迁移把用户数据救回来。
3
播放页默认封面切换闪烁(技术细节版):从资源猜测到动画结构稳定性的完整修复
开发日志技术细节版复盘:记录播放器在“无封面歌曲”场景下切换歌词页时闪烁的问题,包含日志观察、错误方案、最终代码改动与可复用排查模板。
4
Android/Flyme 通知栏“暂停后继续播放无响应”技术复盘:根因、关键代码与最终稳定方案
开发日志一次真实线上故障的技术复盘:Flyme 机型通知栏暂停后无法继续播放。包含时序根因、关键代码改动、日志证据、兼容策略与验证结果。
5
Flutter/iOS 后台自动切歌与通知栏手动“下一首”稳定实现方法
开发日志Flutter/iOS 后台自动切歌与通知栏“下一首”稳定实现方法前段时间在重构播放器内核时,我顺手把一类最烦的 iOS 播放问题彻底收了一遍:后台自动切歌不稳定、锁屏/通知栏点“下一首”偶发失...
随机文章随机推荐

评论区

Profile Image of the Author
Leguan
Hello, I'm Leguan.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:000:00
暂无歌词
分类
标签
站点统计
文章
15
说说
21
分类
3
标签
23
总字数
23,182
运行时长
0
最后活动
0 天前
站点信息
构建平台
Unknown CI
博客版本
Firefly v6.13.3
文章许可
CC BY-NC-SA 4.0

文章目录