You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,和实际音频时长不匹配,会触发进度越界异常。
修复步骤
  1. 初始化播放器时强制指定使用支持seek的标准媒体播放模式
  2. 新增拖动临时值存储拖动中的进度,拖动过程中只更新滑块UI,等拖动结束后仅执行1次seek操作,避免高频调用
  3. 修正播放/暂停逻辑,首次播放调用play加载音频,暂停后恢复调用resume,不要重复加载音源
  4. 将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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 12:24:22