You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
243 lines
6.2 KiB
243 lines
6.2 KiB
import 'dart:async';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:flutter/services.dart';
|
|
|
|
/// 实时语音服务异常
|
|
class RealtimeException implements Exception {
|
|
final String message;
|
|
|
|
RealtimeException(this.message);
|
|
|
|
@override
|
|
String toString() => 'RealtimeException: $message';
|
|
}
|
|
|
|
/// 连接状态
|
|
enum ConnectionStatus {
|
|
disconnected,
|
|
connecting,
|
|
connected,
|
|
error,
|
|
}
|
|
|
|
/// 语音状态
|
|
enum VoiceStatus {
|
|
idle,
|
|
recording,
|
|
processing,
|
|
playing,
|
|
}
|
|
|
|
/// 实时语音事件
|
|
class RealtimeEvent {
|
|
final String type;
|
|
final dynamic data;
|
|
|
|
RealtimeEvent({required this.type, this.data});
|
|
|
|
factory RealtimeEvent.fromMap(Map<String, dynamic> map) {
|
|
return RealtimeEvent(
|
|
type: map['type'] as String,
|
|
data: map['data'],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 实时语音聊天服务
|
|
class RealtimeService {
|
|
static const MethodChannel _channel = MethodChannel('realtime/methods');
|
|
static const EventChannel _eventChannel = EventChannel('realtime/events');
|
|
|
|
StreamController<RealtimeEvent>? _eventStreamController;
|
|
Stream<RealtimeEvent>? _eventStream;
|
|
|
|
/// 获取事件流
|
|
Stream<RealtimeEvent> get eventStream {
|
|
if (_eventStream == null) {
|
|
_eventStreamController = StreamController<RealtimeEvent>.broadcast();
|
|
_eventStream = _eventStreamController!.stream;
|
|
|
|
// 监听原生事件
|
|
_eventChannel.receiveBroadcastStream().listen(
|
|
(dynamic event) {
|
|
if (event is Map<dynamic, dynamic>) {
|
|
final eventMap = Map<String, dynamic>.from(event);
|
|
final realtimeEvent = RealtimeEvent.fromMap(eventMap);
|
|
_eventStreamController!.add(realtimeEvent);
|
|
}
|
|
},
|
|
onError: (error) {
|
|
_eventStreamController!.addError(RealtimeException('事件流错误: $error'));
|
|
},
|
|
);
|
|
}
|
|
|
|
return _eventStream!;
|
|
}
|
|
|
|
/// 初始化实时语音服务
|
|
///
|
|
/// [serverUrl] WebSocket服务器地址
|
|
/// [sampleRate] 采样率,默认16000
|
|
/// [channels] 声道数,默认1(单声道)
|
|
/// [bitsPerSample] 位深,默认16
|
|
Future<bool> initialize({
|
|
required String serverUrl,
|
|
int sampleRate = 16000,
|
|
int channels = 1,
|
|
int bitsPerSample = 16,
|
|
}) async {
|
|
try {
|
|
final result = await _channel.invokeMethod<bool>(
|
|
'initialize',
|
|
{
|
|
'serverUrl': serverUrl,
|
|
'sampleRate': sampleRate,
|
|
'channels': channels,
|
|
'bitsPerSample': bitsPerSample,
|
|
},
|
|
);
|
|
|
|
return result ?? false;
|
|
} catch (e) {
|
|
throw RealtimeException('初始化失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 连接到服务器
|
|
Future<bool> connect() async {
|
|
try {
|
|
final result = await _channel.invokeMethod<bool>('connect');
|
|
return result ?? false;
|
|
} catch (e) {
|
|
throw RealtimeException('连接失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 断开连接
|
|
Future<bool> disconnect() async {
|
|
try {
|
|
final result = await _channel.invokeMethod<bool>('disconnect');
|
|
return result ?? false;
|
|
} catch (e) {
|
|
throw RealtimeException('断开连接失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 开始录音
|
|
Future<bool> startRecording() async {
|
|
try {
|
|
final result = await _channel.invokeMethod<bool>('startRecording');
|
|
return result ?? false;
|
|
} catch (e) {
|
|
throw RealtimeException('开始录音失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 停止录音
|
|
Future<bool> stopRecording() async {
|
|
try {
|
|
final result = await _channel.invokeMethod<bool>('stopRecording');
|
|
return result ?? false;
|
|
} catch (e) {
|
|
throw RealtimeException('停止录音失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 停止播放
|
|
Future<bool> stopPlaying() async {
|
|
try {
|
|
final result = await _channel.invokeMethod<bool>('stopPlaying');
|
|
return result ?? false;
|
|
} catch (e) {
|
|
throw RealtimeException('停止播放失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 获取连接状态
|
|
Future<ConnectionStatus> getConnectionStatus() async {
|
|
try {
|
|
final result = await _channel.invokeMethod<String>('getConnectionStatus');
|
|
switch (result) {
|
|
case 'disconnected':
|
|
return ConnectionStatus.disconnected;
|
|
case 'connecting':
|
|
return ConnectionStatus.connecting;
|
|
case 'connected':
|
|
return ConnectionStatus.connected;
|
|
case 'error':
|
|
return ConnectionStatus.error;
|
|
default:
|
|
return ConnectionStatus.disconnected;
|
|
}
|
|
} catch (e) {
|
|
throw RealtimeException('获取连接状态失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 获取语音状态
|
|
Future<VoiceStatus> getVoiceStatus() async {
|
|
try {
|
|
final result = await _channel.invokeMethod<String>('getVoiceStatus');
|
|
switch (result) {
|
|
case 'idle':
|
|
return VoiceStatus.idle;
|
|
case 'recording':
|
|
return VoiceStatus.recording;
|
|
case 'processing':
|
|
return VoiceStatus.processing;
|
|
case 'playing':
|
|
return VoiceStatus.playing;
|
|
default:
|
|
return VoiceStatus.idle;
|
|
}
|
|
} catch (e) {
|
|
throw RealtimeException('获取语音状态失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 发送文本消息
|
|
Future<bool> sendTextMessage(String message) async {
|
|
try {
|
|
final result = await _channel.invokeMethod<bool>(
|
|
'sendTextMessage',
|
|
{'message': message},
|
|
);
|
|
return result ?? false;
|
|
} catch (e) {
|
|
throw RealtimeException('发送文本消息失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 设置音频参数
|
|
Future<bool> setAudioConfig({
|
|
int? sampleRate,
|
|
int? channels,
|
|
int? bitsPerSample,
|
|
}) async {
|
|
try {
|
|
final params = <String, dynamic>{};
|
|
if (sampleRate != null) params['sampleRate'] = sampleRate;
|
|
if (channels != null) params['channels'] = channels;
|
|
if (bitsPerSample != null) params['bitsPerSample'] = bitsPerSample;
|
|
|
|
final result = await _channel.invokeMethod<bool>('setAudioConfig', params);
|
|
return result ?? false;
|
|
} catch (e) {
|
|
throw RealtimeException('设置音频参数失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 释放资源
|
|
Future<void> dispose() async {
|
|
try {
|
|
await _channel.invokeMethod<void>('dispose');
|
|
_eventStreamController?.close();
|
|
_eventStreamController = null;
|
|
_eventStream = null;
|
|
} catch (e) {
|
|
throw RealtimeException('释放资源失败: $e');
|
|
}
|
|
}
|
|
}
|