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

Flutter移动端用just_audio播放WebSocket流式音频的延迟问题求助

解决WebSocket音频块无间断播放的延迟问题

当前通过WebSocket接收服务器音频块,将其添加到ConcatenatingAudioSource后调用playQueuedAudio循环播放,但块之间存在明显延迟。问题出在手动轮询的实现逻辑上,没有充分利用just_audio本身的队列自动播放能力,以下是规范的优化方案:

现有代码的核心问题

  1. Timer轮询引入延迟:100ms的轮询间隔本身会导致播放衔接的滞后,无法实时响应音频块的到达
  2. 手动控制播放流程:每次播放完手动移除队列元素并重新调用play(),破坏了just_audio内置的队列自动衔接逻辑
  3. 状态管理混乱:_isPlaying的手动维护容易出现竞态,导致播放逻辑冲突

优化实现方案

利用just_audio的ConcatenatingAudioSource自动播放下一个音频源的能力,让播放器状态自动响应队列变化,无需手动轮询控制。

重构后的完整代码

import 'dart:async';
import 'dart:convert';
import 'package:just_audio/just_audio.dart';
import 'package:meddymobile/utils/ws_connection.dart';

class PlayerService {
  final WSConnection _wsConnection;
  final AudioPlayer _audioPlayer = AudioPlayer();
  final ConcatenatingAudioSource _audioChunkPlaylist =
      ConcatenatingAudioSource(children: []);
  StreamSubscription? _playerStateSubscription;

  PlayerService(this._wsConnection) {
    _wsConnection.setHandler('audio_3', _handleAudioMessage);
    _initPlayer();
  }

  void _initPlayer() async {
    await _audioPlayer.setAudioSource(_audioChunkPlaylist);
    // 监听播放器状态,自动处理播放/暂停逻辑
    _playerStateSubscription = _audioPlayer.playerStateStream.listen((state) {
      // 队列播放完毕且无新块时暂停
      if (state.processingState == ProcessingState.completed && _audioChunkPlaylist.length == 0) {
        _audioPlayer.pause();
      }
      // 播放器空闲但队列有内容时,自动启动播放
      else if (state.processingState == ProcessingState.idle && _audioChunkPlaylist.length > 0) {
        _audioPlayer.play();
      }
    });
  }

  void _handleAudioMessage(Map<String, dynamic> message) {
    if (message['audio'] != null) {
      final audioData = base64Decode(message['audio']);
      _audioChunkPlaylist.add(MyAudioSource(audioData));
      
      // 首个音频块到达时,若播放器未播放则立即启动
      if (!_audioPlayer.playing && _audioChunkPlaylist.length == 1) {
        _audioPlayer.play();
      }
    }
  }

  void playQueuedAudio() {
    // 只需确保队列有内容时播放器处于播放状态
    if (_audioChunkPlaylist.length > 0 && !_audioPlayer.playing) {
      _audioPlayer.play();
    }
  }

  Future<void> stopPlayback() async {
    await _audioPlayer.stop();
    await _audioChunkPlaylist.clear();
  }

  Future<void> dispose() async {
    await _playerStateSubscription?.cancel();
    await _audioPlayer.dispose();
  }
}

class MyAudioSource extends StreamAudioSource {
  final List<int> _buffer;

  MyAudioSource(this._buffer);

  @override
  Future<StreamAudioResponse> request([int? start, int? end]) async {
    start ??= 0;
    end ??= _buffer.length;
    return StreamAudioResponse(
      sourceLength: _buffer.length,
      contentLength: end - start,
      offset: start,
      stream: Stream.value(_buffer.sublist(start, end)),
      contentType: 'audio/mp3',
    );
  }
}

关键优化点说明

  • 移除Timer轮询:通过playerStateStream实时监听播放器状态,彻底消除轮询带来的延迟
  • 自动队列衔接:just_audio会自动处理当前音频源播放完成后的下一个源播放,无需手动调用play()和移除队列元素
  • 智能状态管理:根据播放器状态和队列长度自动切换播放/暂停,避免手动维护状态的错误
  • 即时触发播放:首个音频块到达时立即启动播放,后续块自动无缝衔接

额外优化建议

  • 确保所有音频块的编码格式一致(如统一为MP3),避免格式切换导致的解码延迟
  • 可以在接收音频块时提前完成数据解码,减少播放前的等待时间
  • 监控队列长度,若积累过多可调整WebSocket接收策略或音频块大小,避免内存占用过高

内容的提问来源于stack exchange,提问作者Mustafa

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 01:30:09