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.
528 lines
16 KiB
528 lines
16 KiB
import 'dart:convert';
|
|
|
|
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
|
import 'package:dating_touchme_app/rtc/rtm_manager.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:get/get.dart';
|
|
|
|
import '../controller/discover/room_controller.dart';
|
|
import '../pages/discover/live_room_page.dart';
|
|
|
|
/// RTC 管理器,负责管理声网音视频通话功能
|
|
class RTCManager {
|
|
/// 频道加入状态通知,用于UI监听
|
|
final ValueNotifier<bool> channelJoinedNotifier = ValueNotifier<bool>(false);
|
|
final ValueNotifier<List<int>> remoteUsersNotifier = ValueNotifier<List<int>>(
|
|
<int>[],
|
|
);
|
|
RtcEngine? get engine => _engine;
|
|
bool get isInChannel => _isInChannel;
|
|
int? get currentUid => _currentUid;
|
|
|
|
// 单例模式
|
|
static final RTCManager _instance = RTCManager._internal();
|
|
factory RTCManager() => _instance;
|
|
// 静态getter用于instance访问
|
|
static RTCManager get instance => _instance;
|
|
|
|
RtcEngine? _engine;
|
|
bool _isInitialized = false;
|
|
bool _isInChannel = false;
|
|
String? _currentChannelId;
|
|
int? _currentUid;
|
|
ClientRoleType _clientRole = ClientRoleType.clientRoleBroadcaster;
|
|
final List<int> _remoteUserIds = <int>[];
|
|
// 事件回调
|
|
Function(RtcConnection connection, int elapsed)? onJoinChannelSuccess;
|
|
Function(RtcConnection connection, int remoteUid, int elapsed)? onUserJoined;
|
|
Function(
|
|
RtcConnection connection,
|
|
int remoteUid,
|
|
UserOfflineReasonType reason,
|
|
)?
|
|
onUserOffline;
|
|
Function(RtcConnection connection, RtcStats stats)? onLeaveChannel;
|
|
Function(
|
|
RtcConnection connection,
|
|
ConnectionStateType state,
|
|
ConnectionChangedReasonType reason,
|
|
)?
|
|
onConnectionStateChanged;
|
|
Function(int uid, UserInfo userInfo)? onUserInfoUpdated;
|
|
Function(RtcConnection connection, int uid, int elapsed)?
|
|
onFirstRemoteVideoDecoded;
|
|
Function(
|
|
RtcConnection connection,
|
|
VideoSourceType sourceType,
|
|
int uid,
|
|
int width,
|
|
int height,
|
|
int rotation,
|
|
)?
|
|
onVideoSizeChanged;
|
|
Function(RtcConnection connection, int uid, bool muted)? onUserMuteAudio;
|
|
Function(RtcConnection connection, int uid, bool muted)? onUserMuteVideo;
|
|
Function(RtcConnection connection)? onConnectionLost;
|
|
Function(RtcConnection connection, int code, String msg)? onError;
|
|
|
|
RTCManager._internal() {
|
|
print('RTCManager instance created');
|
|
}
|
|
|
|
/// 初始化 RTC Engine
|
|
/// [appId] 声网 App ID
|
|
/// [channelProfile] 频道场景类型,默认为通信模式
|
|
Future<bool> initialize({
|
|
required String appId,
|
|
ChannelProfileType channelProfile =
|
|
ChannelProfileType.channelProfileLiveBroadcasting,
|
|
}) async {
|
|
try {
|
|
if (_isInitialized && _engine != null) {
|
|
print('RTC Engine already initialized');
|
|
return true;
|
|
}
|
|
|
|
// 创建 RTC Engine
|
|
_engine = createAgoraRtcEngine();
|
|
|
|
// 初始化 RTC Engine
|
|
await _engine!.initialize(
|
|
RtcEngineContext(appId: appId, channelProfile: channelProfile),
|
|
);
|
|
await _engine?.setClientRole(role: _clientRole);
|
|
await _engine?.enableVideo();
|
|
// await _engine?.startPreview();
|
|
// 注册事件处理器
|
|
_registerEventHandlers();
|
|
|
|
_isInitialized = true;
|
|
print('RTC Engine initialized successfully');
|
|
return true;
|
|
} catch (e) {
|
|
print('Failed to initialize RTC Engine: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 注册事件处理器
|
|
void _registerEventHandlers() {
|
|
if (_engine == null) return;
|
|
|
|
_engine!.registerEventHandler(
|
|
RtcEngineEventHandler(
|
|
onJoinChannelSuccess: (RtcConnection connection, int elapsed) async {
|
|
_isInChannel = true;
|
|
_remoteUserIds.clear();
|
|
remoteUsersNotifier.value = const [];
|
|
channelJoinedNotifier.value = true;
|
|
_currentChannelId = connection.channelId;
|
|
print('加入频道成功,频道名:${connection.channelId},耗时:${elapsed}ms');
|
|
if (connection.localUid == _currentUid) {
|
|
|
|
await RTMManager.instance.subscribe(_currentChannelId ?? '');
|
|
await RTMManager.instance.publishChannelMessage(
|
|
channelName: _currentChannelId ?? '',
|
|
message: json.encode({'type': 'join_room', 'uid': _currentUid}),
|
|
);
|
|
Get.to(() => const LiveRoomPage(id: 0));
|
|
}
|
|
if (onJoinChannelSuccess != null) {
|
|
onJoinChannelSuccess!(connection, elapsed);
|
|
}
|
|
},
|
|
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
|
|
print('用户加入,UID:$remoteUid');
|
|
_handleRemoteUserJoined(remoteUid);
|
|
if (onUserJoined != null) {
|
|
onUserJoined!(connection, remoteUid, elapsed);
|
|
}
|
|
},
|
|
onUserOffline:
|
|
(
|
|
RtcConnection connection,
|
|
int remoteUid,
|
|
UserOfflineReasonType reason,
|
|
) {
|
|
print('用户离开,UID:$remoteUid,原因:$reason');
|
|
_handleRemoteUserOffline(remoteUid);
|
|
if (onUserOffline != null) {
|
|
onUserOffline!(connection, remoteUid, reason);
|
|
}
|
|
},
|
|
onLeaveChannel: (RtcConnection connection, RtcStats stats) {
|
|
_isInChannel = false;
|
|
_remoteUserIds.clear();
|
|
remoteUsersNotifier.value = const [];
|
|
channelJoinedNotifier.value = false;
|
|
_currentChannelId = null;
|
|
print('离开频道,统计信息:${stats.duration}秒');
|
|
if (onLeaveChannel != null) {
|
|
onLeaveChannel!(connection, stats);
|
|
}
|
|
},
|
|
onConnectionStateChanged:
|
|
(
|
|
RtcConnection connection,
|
|
ConnectionStateType state,
|
|
ConnectionChangedReasonType reason,
|
|
) {
|
|
print('连接状态改变:$state,原因:$reason');
|
|
if (onConnectionStateChanged != null) {
|
|
onConnectionStateChanged!(connection, state, reason);
|
|
}
|
|
},
|
|
onUserInfoUpdated: (int uid, UserInfo userInfo) {
|
|
print('用户信息更新,UID:$uid');
|
|
if (onUserInfoUpdated != null) {
|
|
onUserInfoUpdated!(uid, userInfo);
|
|
}
|
|
},
|
|
onFirstRemoteVideoDecoded:
|
|
(
|
|
RtcConnection connection,
|
|
int uid,
|
|
int width,
|
|
int height,
|
|
int elapsed,
|
|
) {
|
|
print('首次远程视频解码,UID:$uid,分辨率:${width}x${height}');
|
|
if (onFirstRemoteVideoDecoded != null) {
|
|
onFirstRemoteVideoDecoded!(connection, uid, elapsed);
|
|
}
|
|
},
|
|
onVideoSizeChanged:
|
|
(
|
|
RtcConnection connection,
|
|
VideoSourceType sourceType,
|
|
int uid,
|
|
int width,
|
|
int height,
|
|
int rotation,
|
|
) {
|
|
print('视频尺寸改变,UID:$uid,分辨率:${width}x${height}');
|
|
if (onVideoSizeChanged != null) {
|
|
onVideoSizeChanged!(
|
|
connection,
|
|
sourceType,
|
|
uid,
|
|
width,
|
|
height,
|
|
rotation,
|
|
);
|
|
}
|
|
},
|
|
onUserMuteAudio: (RtcConnection connection, int uid, bool muted) {
|
|
print('用户静音状态改变,UID:$uid,静音:$muted');
|
|
if (onUserMuteAudio != null) {
|
|
onUserMuteAudio!(connection, uid, muted);
|
|
}
|
|
},
|
|
onUserMuteVideo: (RtcConnection connection, int uid, bool muted) {
|
|
print('用户视频状态改变,UID:$uid,关闭:$muted');
|
|
if (onUserMuteVideo != null) {
|
|
onUserMuteVideo!(connection, uid, muted);
|
|
}
|
|
},
|
|
onConnectionLost: (RtcConnection connection) {
|
|
print('连接丢失');
|
|
if (onConnectionLost != null) {
|
|
onConnectionLost!(connection);
|
|
}
|
|
},
|
|
onError: (ErrorCodeType err, String msg) {
|
|
print('RTC Engine 错误:$err,消息:$msg');
|
|
if (onError != null) {
|
|
onError!(
|
|
RtcConnection(
|
|
channelId: _currentChannelId ?? '',
|
|
localUid: _currentUid ?? 0,
|
|
),
|
|
err.value(),
|
|
msg,
|
|
);
|
|
}
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 启用视频
|
|
Future<void> enableVideo() async {
|
|
if (_engine == null) {
|
|
throw Exception('RTC Engine not initialized');
|
|
}
|
|
await _engine!.enableVideo();
|
|
print('视频已启用');
|
|
}
|
|
|
|
/// 禁用视频
|
|
Future<void> disableVideo() async {
|
|
if (_engine == null) {
|
|
throw Exception('RTC Engine not initialized');
|
|
}
|
|
await _engine!.disableVideo();
|
|
print('视频已禁用');
|
|
}
|
|
|
|
/// 启用音频
|
|
Future<void> enableAudio() async {
|
|
if (_engine == null) {
|
|
throw Exception('RTC Engine not initialized');
|
|
}
|
|
await _engine!.enableAudio();
|
|
print('音频已启用');
|
|
}
|
|
|
|
/// 禁用音频
|
|
Future<void> disableAudio() async {
|
|
if (_engine == null) {
|
|
throw Exception('RTC Engine not initialized');
|
|
}
|
|
await _engine!.disableAudio();
|
|
print('音频已禁用');
|
|
}
|
|
|
|
/// 开启本地视频预览
|
|
Future<void> startPreview() async {
|
|
if (_engine == null) {
|
|
throw Exception('RTC Engine not initialized');
|
|
}
|
|
await _engine!.startPreview();
|
|
print('本地视频预览已开启');
|
|
}
|
|
|
|
/// 停止本地视频预览
|
|
Future<void> stopPreview() async {
|
|
if (_engine == null) {
|
|
throw Exception('RTC Engine not initialized');
|
|
}
|
|
await _engine!.stopPreview();
|
|
print('本地视频预览已停止');
|
|
}
|
|
|
|
/// 设置本地视频视图
|
|
/// [viewId] 视图ID
|
|
/// [mirrorMode] 镜像模式
|
|
Future<void> setupLocalVideo({
|
|
required int viewId,
|
|
VideoSourceType sourceType = VideoSourceType.videoSourceCameraPrimary,
|
|
VideoMirrorModeType mirrorMode = VideoMirrorModeType.videoMirrorModeAuto,
|
|
}) async {
|
|
if (_engine == null) {
|
|
throw Exception('RTC Engine not initialized');
|
|
}
|
|
await _engine!.setupLocalVideo(
|
|
VideoCanvas(view: viewId, sourceType: sourceType, mirrorMode: mirrorMode),
|
|
);
|
|
print('本地视频视图已设置,viewId:$viewId');
|
|
}
|
|
|
|
/// 设置远程视频视图
|
|
/// [uid] 远程用户ID
|
|
/// [viewId] 视图ID
|
|
Future<void> setupRemoteVideo({
|
|
required int uid,
|
|
required int viewId,
|
|
VideoSourceType sourceType = VideoSourceType.videoSourceCameraPrimary,
|
|
VideoMirrorModeType mirrorMode =
|
|
VideoMirrorModeType.videoMirrorModeDisabled,
|
|
}) async {
|
|
if (_engine == null) {
|
|
throw Exception('RTC Engine not initialized');
|
|
}
|
|
await _engine!.setupRemoteVideo(
|
|
VideoCanvas(
|
|
uid: uid,
|
|
view: viewId,
|
|
sourceType: sourceType,
|
|
mirrorMode: mirrorMode,
|
|
),
|
|
);
|
|
print('远程视频视图已设置,UID:$uid,viewId:$viewId');
|
|
}
|
|
|
|
/// 加入频道
|
|
/// [token] 频道令牌(可选,如果频道未开启鉴权则可以为空字符串)
|
|
/// [channelId] 频道ID
|
|
/// [uid] 用户ID(0表示自动分配)
|
|
/// [options] 频道媒体选项
|
|
Future<void> joinChannel({
|
|
String? token,
|
|
required String channelId,
|
|
int uid = 0,
|
|
ClientRoleType role = ClientRoleType.clientRoleBroadcaster,
|
|
}) async {
|
|
if (_engine == null) {
|
|
throw Exception('RTC Engine not initialized');
|
|
}
|
|
if (_isInChannel) {
|
|
print('已经在频道中,先离开当前频道');
|
|
await leaveChannel();
|
|
}
|
|
await setClientRole(role: role);
|
|
_currentUid = uid;
|
|
if (role == ClientRoleType.clientRoleBroadcaster) {
|
|
await _engine?.startPreview();
|
|
}
|
|
await _engine!.joinChannel(
|
|
token: token ?? '',
|
|
channelId: channelId,
|
|
uid: uid,
|
|
options: ChannelMediaOptions(
|
|
channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
|
|
clientRoleType: role,
|
|
autoSubscribeAudio: true,
|
|
autoSubscribeVideo: true,
|
|
publishCameraTrack: true,
|
|
publishMicrophoneTrack: true,
|
|
),
|
|
);
|
|
print('正在加入频道:$channelId,UID:$uid');
|
|
}
|
|
|
|
/// 离开频道
|
|
Future<void> leaveChannel() async {
|
|
if (_engine == null) {
|
|
throw Exception('RTC Engine not initialized');
|
|
}
|
|
if (!_isInChannel) {
|
|
print('当前不在频道中');
|
|
return;
|
|
}
|
|
await RTMManager.instance.unsubscribe(_currentChannelId ?? '');
|
|
await _engine!.leaveChannel();
|
|
_currentUid = null;
|
|
print('已离开频道');
|
|
}
|
|
|
|
/// 切换摄像头
|
|
Future<void> switchCamera() async {
|
|
if (_engine == null) {
|
|
throw Exception('RTC Engine not initialized');
|
|
}
|
|
await _engine!.switchCamera();
|
|
print('摄像头已切换');
|
|
}
|
|
|
|
/// 静音/取消静音本地音频
|
|
/// [muted] true表示静音,false表示取消静音
|
|
Future<void> muteLocalAudio(bool muted) async {
|
|
if (_engine == null) {
|
|
throw Exception('RTC Engine not initialized');
|
|
}
|
|
await _engine!.muteLocalAudioStream(muted);
|
|
print('本地音频${muted ? "已静音" : "已取消静音"}');
|
|
}
|
|
|
|
/// 开启/关闭本地视频
|
|
/// [enabled] true表示开启,false表示关闭
|
|
Future<void> muteLocalVideo(bool enabled) async {
|
|
if (_engine == null) {
|
|
throw Exception('RTC Engine not initialized');
|
|
}
|
|
await _engine!.muteLocalVideoStream(!enabled);
|
|
print('本地视频${enabled ? "已开启" : "已关闭"}');
|
|
}
|
|
|
|
/// 设置客户端角色(仅用于直播场景)
|
|
/// [role] 客户端角色:主播或观众
|
|
Future<void> setClientRole({
|
|
required ClientRoleType role,
|
|
ClientRoleOptions? options,
|
|
}) async {
|
|
if (_engine == null) {
|
|
throw Exception('RTC Engine not initialized');
|
|
}
|
|
_clientRole = role;
|
|
await _engine!.setClientRole(role: role, options: options);
|
|
print('客户端角色已设置为:$role');
|
|
}
|
|
|
|
/// 获取当前频道ID
|
|
String? get currentChannelId => _currentChannelId;
|
|
|
|
ClientRoleType get clientRole => _clientRole;
|
|
|
|
List<int> get remoteUserIds => List<int>.unmodifiable(_remoteUserIds);
|
|
|
|
/// 释放资源
|
|
Future<void> dispose() async {
|
|
try {
|
|
if (_isInChannel) {
|
|
await leaveChannel();
|
|
}
|
|
if (_engine != null) {
|
|
await _engine!.release();
|
|
_engine = null;
|
|
}
|
|
_remoteUserIds.clear();
|
|
remoteUsersNotifier.value = const [];
|
|
_isInitialized = false;
|
|
_isInChannel = false;
|
|
_currentChannelId = null;
|
|
_currentUid = null;
|
|
channelJoinedNotifier.value = false;
|
|
print('RTC Engine disposed');
|
|
} catch (e) {
|
|
print('Failed to dispose RTC Engine: $e');
|
|
}
|
|
}
|
|
|
|
void _handleRemoteUserJoined(int remoteUid) {
|
|
print('用户已加入频道:$remoteUid');
|
|
if (_remoteUserIds.contains(remoteUid)) return;
|
|
_remoteUserIds.add(remoteUid);
|
|
remoteUsersNotifier.value = List<int>.unmodifiable(_remoteUserIds);
|
|
}
|
|
|
|
void _handleRemoteUserOffline(int remoteUid) {
|
|
final removed = _remoteUserIds.remove(remoteUid);
|
|
if (!removed) return;
|
|
remoteUsersNotifier.value = List<int>.unmodifiable(_remoteUserIds);
|
|
}
|
|
|
|
/// 发布视频
|
|
Future<void> publishVideo(CurrentRole role) async {
|
|
await _engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
|
|
await _engine?.muteLocalAudioStream(false);
|
|
await _engine?.muteLocalVideoStream(false);
|
|
await RTMManager.instance.publishChannelMessage(
|
|
channelName: _currentChannelId ?? '',
|
|
message: json.encode({
|
|
'type': 'join_chat',
|
|
'uid': _currentUid,
|
|
'role': role == CurrentRole.maleAudience ? 'male_audience' : 'female_audience'
|
|
}),
|
|
);
|
|
}
|
|
|
|
/// 发布音频
|
|
Future<void> publishAudio() async {
|
|
await _engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
|
|
await _engine?.muteLocalAudioStream(false);
|
|
await _engine?.muteLocalVideoStream(true);
|
|
await RTMManager.instance.publishChannelMessage(
|
|
channelName: _currentChannelId ?? '',
|
|
message: json.encode({
|
|
'type': 'join_chat',
|
|
'uid': _currentUid,
|
|
'role': 'audience'
|
|
}),
|
|
);
|
|
}
|
|
|
|
/// 取消发布视频
|
|
Future<void> unpublish() async {
|
|
await _engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
|
|
await _engine?.muteLocalAudioStream(true);
|
|
await _engine?.muteLocalVideoStream(true);
|
|
await RTMManager.instance.publishChannelMessage(
|
|
channelName: _currentChannelId ?? '',
|
|
message: json.encode({
|
|
'type': 'leave_chat',
|
|
'uid': _currentUid,
|
|
}),
|
|
);
|
|
}
|
|
}
|