Flutter Audioplayers ^0.20.1 安卓音频seek后从头播放问题
问题根因
安卓端seek后从头播放是版本兼容+写法问题共同导致的:
- audioplayers 0.20.1版本在安卓端默认会为网络音频优先匹配
LOW_LATENCY低延迟播放模式,这个模式是为按键音、提示音等短音频设计的,本身不支持进度跳转,调用seek会直接重置播放器回到0秒位置。iOS端没有该模式限制,所以表现正常。 - 原有写法在Slider的
onChanged回调中实时触发seek,拖动进度条时1秒内会触发十几次甚至几十次seek请求,安卓原生MediaPlayer对高频连续seek的兼容性很差,会直接重置播放状态。 - 额外逻辑bug:暂停后再次点击播放按钮时,重复调用
play(url)重新加载音频,本身也会导致从头播放;且Slider的max值硬编码为120,和实际音频时长不匹配,会触发进度越界异常。
修复步骤
- 初始化播放器时强制指定使用支持seek的标准媒体播放模式
- 新增拖动临时值存储拖动中的进度,拖动过程中只更新滑块UI,等拖动结束后仅执行1次seek操作,避免高频调用
- 修正播放/暂停逻辑,首次播放调用
play加载音频,暂停后恢复调用resume,不要重复加载音源 - 将Slider的max值改为动态获取的音频总时长,处理音频未加载完成的边界情况
修正后完整代码
import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/material.dart'; class PlayAudio extends StatefulWidget { const PlayAudio({Key? key}) : super(key: key); @override State<PlayAudio> createState() => _PlayAudioState(); } class _PlayAudioState extends State<PlayAudio> { bool isPlaying = false; bool isAudioLoaded = false; Duration duration = const Duration(); Duration position = const Duration(); // 存储拖动中的进度值,避免拖动时滑块被实时进度回调冲跳 double? dragProgress; final AudioPlayer audioPlayer = AudioPlayer(); final String audioUrl = "https://www.learningcontainer.com/wp-content/uploads/2020/02/Kalimba.mp3"; @override void initState() { super.initState(); // 强制指定使用支持seek的标准媒体播放模式,修复安卓低延迟模式不支持跳转的问题 audioPlayer.setPlayerMode(PlayerMode.MEDIA_PLAYER); audioPlayer.onPlayerStateChanged.listen((event) { setState(() { isPlaying = event == PlayerState.PLAYING; }); }); audioPlayer.onDurationChanged.listen((event) { setState(() { duration = event; }); }); audioPlayer.onAudioPositionChanged.listen((event) { // 拖动过程中不更新实时进度,避免滑块跳变 if (dragProgress == null) { setState(() { position = event; }); } }); // 播放完成后重置加载状态 audioPlayer.onPlayerCompletion.listen((event) { setState(() { isPlaying = false; isAudioLoaded = false; position = Duration.zero; }); }); } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ InkWell( onTap: () async { if (isPlaying) { await audioPlayer.pause(); } else { if (isAudioLoaded) { // 已加载过音频,暂停后直接恢复 await audioPlayer.resume(); } else { // 首次播放加载音源 await audioPlayer.play(audioUrl); isAudioLoaded = true; } } }, child: Container( color: Colors.red, child: const Padding( padding: EdgeInsets.all(8.0), child: Text("play Audio"), ), ), ), Slider.adaptive( min: 0.0, // 拖动时显示临时进度,否则显示真实播放进度 value: dragProgress ?? position.inSeconds.toDouble(), // 音频未加载完成时max设为1,避免滑块初始化报错 max: duration.inSeconds.toDouble() == 0 ? 1.0 : duration.inSeconds.toDouble(), // 拖动过程中只更新UI,不触发seek onChanged: (value) { setState(() { dragProgress = value; }); }, // 拖动结束后只执行一次seek onChangeEnd: (value) async { final targetPosition = Duration(seconds: value.toInt()); await audioPlayer.seek(targetPosition); // seek完成后清空临时进度,恢复实时进度更新 setState(() { dragProgress = null; position = targetPosition; }); }), Text(formatTime(position)), Text(formatTime(duration - position)), ], ), ), ); } String formatTime(Duration duration) { String twoDigits(int n) => n.toString().padLeft(2, "0"); final hours = twoDigits(duration.inHours); final minutes = twoDigits(duration.inMinutes.remainder(60)); final seconds = twoDigits(duration.inSeconds.remainder(60)); return [ if (duration.inHours > 0) hours, minutes, seconds ].join(":"); } @override void dispose() { audioPlayer.dispose(); super.dispose(); } }
额外说明
如果后续升级audioplayers到1.x以上版本,注意API变动:play方法需要传入UrlSource包装的音源,onAudioPositionChanged改名为onPositionChanged,其余seek逻辑可以保持不变。
内容的提问来源于stack exchange,提问作者Akhil
相关产品推荐
相关产品推荐

