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

Flutter中WebSocket对接ElevenLabs音频流播放不流畅问题求助

修复Flutter中ElevenLabs WebSocket音频流播放的跳词与中断问题

问题根源

你的代码存在两个核心问题导致播放异常:

  1. 每次收到音频chunk就新建StreamController并重新调用setAudioSource,强制播放器中断当前播放、加载新源,引发跳词和重启。
  2. MyCustomSource仅返回创建时已缓存的字节数据,后续接收的音频chunk无法被播放器获取,导致播放中断。

修复方案与代码修改

1. 复用StreamController,单次初始化音频源

将StreamController改为全局复用,仅初始化一次音频源,持续向流中添加新的音频chunk:

final socketChannel = IOWebSocketChannel.connect(
    'wss://api.elevenlabs.io/v1/text-to-speech/$voiceId/stream-input?model_id=$model');
final AudioPlayer audioPlayer = AudioPlayer();
// 全局复用的流控制器,广播模式支持多监听
final StreamController<List<int>> audioStreamController = StreamController.broadcast();

final bosMessage = {
  "text": " ",
  "voice_settings": {"stability": 0.5, "similarity_boost": true},
  "xi_api_key": apiKey,
};

// 仅初始化一次音频源
await audioPlayer.setAudioSource(MyCustomSource(audioStreamController.stream));
// 提前启动播放,等待音频chunk到达
await audioPlayer.play();

socketChannel.sink.add(jsonEncode(bosMessage));
socketChannel.stream.listen((message) async {
  final response = jsonDecode(message);
  if (response['audio'] != null) {
    try {
      final audioChunk = response['audio'] as String;
      final uint8List = Uint8List.fromList(base64.decode(audioChunk));
      // 向同一个流控制器添加新的音频chunk
      audioStreamController.add(uint8List);
    } catch (e) {
      print("处理音频chunk出错: $e");
    }
  }
});

2. 改进MyCustomSource的流式播放支持

修改自定义音频源,使其能动态响应播放器的请求,持续提供后续接收的音频数据:

import 'package:just_audio/just_audio.dart';

class MyCustomSource extends StreamAudioSource {
  final Stream<List<int>> byteStream;
  final _buffer = <int>[];
  bool _isStreamCompleted = false;

  MyCustomSource(this.byteStream) {
    // 监听音频流,持续更新缓冲区
    byteStream.listen(
      (data) {
        _buffer.addAll(data);
      },
      onDone: () {
        _isStreamCompleted = true;
      },
      onError: (error) => print("音频流错误: $error"),
    );
  }

  @override
  Future<StreamAudioResponse> request([int? start, int? end]) async {
    start ??= 0;
    end ??= _buffer.length;

    // 如果请求的范围超出当前缓存,等待新数据(流未结束时)
    while (end > _buffer.length && !_isStreamCompleted) {
      await Future.delayed(const Duration(milliseconds: 50));
    }

    // 修正end值到实际可用的缓冲区长度
    end = end.clamp(start, _buffer.length);

    return StreamAudioResponse(
      sourceLength: _isStreamCompleted ? _buffer.length : null, // 流未结束时不返回总长度
      contentLength: end - start,
      offset: start,
      stream: Stream.value(_buffer.sublist(start, end)),
      contentType: 'audio/mpeg',
    );
  }

  @override
  Future<void> dispose() async {
    super.dispose();
  }
}

3. 资源释放优化

在页面销毁时,确保释放所有资源避免内存泄漏:

@override
void dispose() {
  audioStreamController.close();
  audioPlayer.dispose();
  socketChannel.sink.close();
  super.dispose();
}

额外优化建议

  • 添加WebSocket重连逻辑:当连接断开时自动重试,避免播放中断。
  • 缓冲区内存管理:定期清理已播放完成的音频数据,防止缓冲区过大占用内存(需结合播放器的当前播放位置实现)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 17:53:18