From dc05668ccc13ba1400ac7717482e90494f8e55f0 Mon Sep 17 00:00:00 2001 From: Jolie <412895109@qq.com> Date: Tue, 18 Nov 2025 22:38:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(rtc):=20=E9=9B=86=E6=88=90=E5=A3=B0?= =?UTF-8?q?=E7=BD=91RTC=E5=92=8CRTM=E5=8A=9F=E8=83=BD=E4=BB=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=AE=9E=E6=97=B6=E9=9F=B3=E8=A7=86=E9=A2=91=E9=80=9A?= =?UTF-8?q?=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增获取声网RTC和RTM Token的接口与实现 - 更新RTC管理器以支持频道创建及加入流程 - 实现RTM管理器初始化、登录及频道订阅功能 - 添加agora_token_generator依赖用于生成RTM令牌 - 修改RTC初始化逻辑以适配直播场景并启用视频预览- 在用户加入频道成功后自动订阅RTM频道并发送加入房间消息 -优化网络请求代码结构,新增RTC相关API和服务端交互逻辑 --- lib/controller/discover/room_controller.dart | 13 +- lib/main.dart | 4 +- lib/model/rtc/rtc_channel_data.dart | 4 + lib/network/api_urls.dart | 2 + lib/network/rtc_api.dart | 8 + lib/network/rtc_api.g.dart | 62 +++++++ lib/pages/main/main_page.dart | 7 + lib/rtc/rtc_manager.dart | 22 ++- lib/rtc/rtm_manager.dart | 163 ++++++------------- pubspec.lock | 16 ++ pubspec.yaml | 1 + 11 files changed, 175 insertions(+), 127 deletions(-) diff --git a/lib/controller/discover/room_controller.dart b/lib/controller/discover/room_controller.dart index 552d480..f1eb85f 100644 --- a/lib/controller/discover/room_controller.dart +++ b/lib/controller/discover/room_controller.dart @@ -1,8 +1,10 @@ +import 'package:agora_token_generator/agora_token_generator.dart'; import 'package:dating_touchme_app/model/rtc/rtc_channel_data.dart'; import 'package:dating_touchme_app/network/network_service.dart'; import 'package:dating_touchme_app/rtc/rtc_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; import '../../pages/discover/live_room_page.dart'; @@ -27,10 +29,9 @@ class RoomController extends GetxController { isLoading.value = true; final response = await _networkService.rtcApi.createRtcChannel(); final base = response.data; - if (base.isSuccess && base.data != null) { rtcChannel.value = base.data; - await _joinRtcChannel(base.data!); + await _joinRtcChannel(base.data!.token, base.data!.channelId, base.data!.uid); } else { final message = base.message.isNotEmpty ? base.message : '创建频道失败'; SmartDialog.showToast(message); @@ -41,13 +42,13 @@ class RoomController extends GetxController { isLoading.value = false; } } - Future _joinRtcChannel(RtcChannelData data) async { + Future _joinRtcChannel(String token, String channelName, int uid) async { try { await RTCManager.instance.joinChannel( - token: data.token, - channelId: data.channelId, + token: token, + channelId: channelName, + uid: uid ); - Get.to(() => const LiveRoomPage(id: 0)); } catch (e) { SmartDialog.showToast('加入频道失败:$e'); } diff --git a/lib/main.dart b/lib/main.dart index ef1113c..a89fe82 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,8 +22,8 @@ void main() async { // 设置环境配置 - 根据是否为release模式 EnvConfig.setEnvironment(Environment.dev); - RTCManager.instance.initialize(appId: '4c2ea9dcb4c5440593a418df0fdd512d'); - IMManager.instance.initialize('1165251016193374#demo'); + await RTCManager.instance.initialize(appId: '4c2ea9dcb4c5440593a418df0fdd512d'); + await IMManager.instance.initialize('1165251016193374#demo'); // 初始化全局依赖 final networkService = NetworkService(); Get.put(networkService); diff --git a/lib/model/rtc/rtc_channel_data.dart b/lib/model/rtc/rtc_channel_data.dart index 05c65e0..b5795c4 100644 --- a/lib/model/rtc/rtc_channel_data.dart +++ b/lib/model/rtc/rtc_channel_data.dart @@ -2,16 +2,19 @@ class RtcChannelData { final String channelId; final String token; + final int uid; RtcChannelData({ required this.channelId, required this.token, + required this.uid, }); factory RtcChannelData.fromJson(Map json) { return RtcChannelData( channelId: json['channelId']?.toString() ?? '', token: json['token']?.toString() ?? '', + uid: json['uid'] ?? 0, ); } @@ -19,6 +22,7 @@ class RtcChannelData { return { 'channelId': channelId, 'token': token, + 'uid': uid, }; } diff --git a/lib/network/api_urls.dart b/lib/network/api_urls.dart index 9d5a814..ea4e69b 100644 --- a/lib/network/api_urls.dart +++ b/lib/network/api_urls.dart @@ -23,8 +23,10 @@ class ApiUrls { static const String getMaritalStatusList = 'dating-agency-service/user/get/marital/status/list'; static const String getPropertyList = 'dating-agency-service/user/get/property/permits'; static const String getOccupationList = 'dating-agency-service/user/get/occupation/list'; + static const String getSwRtcToken = 'dating-agency-chat-audio/user/get/sw/rtc/token'; static const String createRtcChannel = 'dating-agency-chat-audio/user/create/rtc-channel'; static const String editOwnMarriageInformation = 'dating-agency-service/user/edit/own-marriage-information'; + static const String getSwRtmToken = 'dating-agency-chat-audio/user/get/sw/rtm/token'; //首页相关接口 static const String getMarriageList = 'dating-agency-service/user/page/dongwo/marriage-information'; diff --git a/lib/network/rtc_api.dart b/lib/network/rtc_api.dart index c298e3c..7e4c4e8 100644 --- a/lib/network/rtc_api.dart +++ b/lib/network/rtc_api.dart @@ -12,6 +12,14 @@ abstract class RtcApi { factory RtcApi(Dio dio) = _RtcApi; /// 创建实时音视频频道 + @GET(ApiUrls.getSwRtcToken) + Future>> getSwRtcToken(); + + /// 获取声网 RTM Token + @GET(ApiUrls.getSwRtmToken) + Future>> getSwRtmToken(); + + /// 创建实时音视频频道(返回字符串) @POST(ApiUrls.createRtcChannel) Future>> createRtcChannel(); } diff --git a/lib/network/rtc_api.g.dart b/lib/network/rtc_api.g.dart index 528db3a..2cbdf56 100644 --- a/lib/network/rtc_api.g.dart +++ b/lib/network/rtc_api.g.dart @@ -19,6 +19,68 @@ class _RtcApi implements RtcApi { final ParseErrorLogger? errorLogger; + @override + Future>> getSwRtcToken() async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType>>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'dating-agency-chat-audio/user/get/sw/rtc/token', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late BaseResponse _value; + try { + _value = BaseResponse.fromJson( + _result.data!, + (json) => RtcChannelData.fromJson(json as Map), + ); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + + @override + Future>> getSwRtmToken() async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType>>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'dating-agency-chat-audio/user/get/sw/rtm/token', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late BaseResponse _value; + try { + _value = BaseResponse.fromJson( + _result.data!, + (json) => RtcChannelData.fromJson(json as Map), + ); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + @override Future>> createRtcChannel() async { final _extra = {}; diff --git a/lib/pages/main/main_page.dart b/lib/pages/main/main_page.dart index ef427f8..174e049 100644 --- a/lib/pages/main/main_page.dart +++ b/lib/pages/main/main_page.dart @@ -1,6 +1,7 @@ import 'package:dating_touchme_app/pages/main/tabbar/main_tab_bar.dart'; import 'package:dating_touchme_app/pages/message/message_page.dart'; import 'package:dating_touchme_app/pages/mine/mine_page.dart'; +import 'package:dating_touchme_app/rtc/rtm_manager.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; @@ -47,6 +48,7 @@ class _MainPageState extends State { Get.put(RouteGuardService()); // 检查token并调用获取婚姻信息详情的方法 checkTokenAndFetchMarriageInfo(); + initRTM(); } // 检查token并获取婚姻信息详情 @@ -58,6 +60,11 @@ class _MainPageState extends State { await userController.getBaseUserInfo(userId, true); } } + + initRTM() async { + String? userId = storage.read('userId'); + await RTMManager.instance.initialize(appId: '4c2ea9dcb4c5440593a418df0fdd512d', userId: userId ?? ''); + } @override Widget build(BuildContext context) { diff --git a/lib/rtc/rtc_manager.dart b/lib/rtc/rtc_manager.dart index c89abe9..6f70add 100644 --- a/lib/rtc/rtc_manager.dart +++ b/lib/rtc/rtc_manager.dart @@ -1,4 +1,10 @@ +import 'dart:convert'; + import 'package:agora_rtc_engine/agora_rtc_engine.dart'; +import 'package:dating_touchme_app/rtc/rtm_manager.dart'; +import 'package:get/get.dart'; + +import '../pages/discover/live_room_page.dart'; /// RTC 管理器,负责管理声网音视频通话功能 class RTCManager { @@ -57,7 +63,7 @@ class RTCManager { Future initialize({ required String appId, ChannelProfileType channelProfile = - ChannelProfileType.channelProfileCommunication, + ChannelProfileType.channelProfileLiveBroadcasting, }) async { try { if (_isInitialized && _engine != null) { @@ -72,7 +78,9 @@ class RTCManager { await _engine!.initialize( RtcEngineContext(appId: appId, channelProfile: channelProfile), ); - + await _engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); + await _engine?.enableVideo(); + await _engine?.startPreview(); // 注册事件处理器 _registerEventHandlers(); @@ -91,10 +99,18 @@ class RTCManager { _engine!.registerEventHandler( RtcEngineEventHandler( - onJoinChannelSuccess: (RtcConnection connection, int elapsed) { + onJoinChannelSuccess: (RtcConnection connection, int elapsed) async{ _isInChannel = 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); } diff --git a/lib/rtc/rtm_manager.dart b/lib/rtc/rtm_manager.dart index d7c159f..99a463b 100644 --- a/lib/rtc/rtm_manager.dart +++ b/lib/rtc/rtm_manager.dart @@ -1,4 +1,12 @@ import 'package:agora_rtm/agora_rtm.dart'; +import 'package:agora_token_generator/agora_token_generator.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; + +import '../model/rtc/rtc_channel_data.dart'; +import '../network/network_service.dart'; +import '../network/rtc_api.dart'; /// 声网 RTM 管理器,聚合常用的客户端与 StreamChannel 操作 class RTMManager { @@ -13,6 +21,7 @@ class RTMManager { bool _isLoggedIn = false; String? _currentAppId; String? _currentUserId; + late NetworkService _networkService; final Map _streamChannels = {}; @@ -52,39 +61,73 @@ class RTMManager { _currentUserId == userId) { return true; } - + _networkService = Get.find(); await dispose(); final (status, client) = await RTM(appId, userId, config: config); + print('RTM初始化成功'); + if (status.error) { onOperationError?.call(status); return false; } - _client = client; _currentAppId = appId; _currentUserId = userId; _isInitialized = true; _registerClientListeners(); + final response = await _networkService.rtcApi.getSwRtmToken(); + // 处理响应 + if (response.data.isSuccess) { + await login(response.data.data?.token ?? ''); + } else { + SmartDialog.showToast(response.data.message); + } return true; } /// 登录 RTM Future login(String token) async { + print('RTM TOKEN:$token'); _ensureInitialized(); - final (status, _) = await _client!.login(token); + GetStorage storage = GetStorage(); + String userId = storage.read('userId') ?? ''; + print('RTM userId:$userId'); + String tokens = RtmTokenBuilder.buildToken( + appId: '4c2ea9dcb4c5440593a418df0fdd512d', + appCertificate: '16f34b45181a4fae8acdb1a28762fcfa', + userId: userId, + tokenExpireSeconds: 3600, + ); + print('RTM TOKEN:$tokens'); + final (status, _) = await _client!.login(tokens); final ok = _handleStatus(status); + print(ok ? 'RTM 登录成功' : 'RTM 登录失败'); if (ok) { _isLoggedIn = true; } return ok; } + /// 创建频道 + Future subscribe(String channelName) async { + _ensureInitialized(); + final (status, _) = await _client!.subscribe(channelName); + print(!status.error ? 'RTM 订阅成功' : 'RTM 订阅失败'); + return _handleStatus(status); + } + + /// 取消订阅频道 + Future unsubscribe(String channelName) async { + _ensureInitialized(); + final (status, _) = await _client!.unsubscribe(channelName); + return _handleStatus(status); + } + /// 登出 RTM Future logout() async { if (!_isInitialized || _client == null || !_isLoggedIn) return; - await leaveAllStreamChannels(); final (status, _) = await _client!.logout(); _handleStatus(status); _isLoggedIn = false; @@ -97,13 +140,6 @@ class RTMManager { return _handleStatus(status); } - /// 设置底层参数(JSON) - Future setParameters(String paramsJson) async { - _ensureInitialized(); - final status = await _client!.setParameters(paramsJson); - return _handleStatus(status); - } - /// 发布频道文本消息 Future publishChannelMessage({ required String channelName, @@ -123,99 +159,8 @@ class RTMManager { return _handleStatus(status); } - /// 创建/加入 StreamChannel - Future joinStreamChannel( - String channelName, { - String? token, - bool withMetadata = false, - bool withPresence = true, - bool withLock = false, - bool beQuiet = false, - }) async { - _ensureInitialized(); - - StreamChannel? channel = _streamChannels[channelName]; - if (channel == null) { - final (createStatus, createdChannel) = await _client!.createStreamChannel( - channelName, - ); - if (!_handleStatus(createStatus) || createdChannel == null) { - return false; - } - channel = createdChannel; - _streamChannels[channelName] = channel; - } - - final (status, _) = await channel.join( - token: token, - withMetadata: withMetadata, - withPresence: withPresence, - withLock: withLock, - beQuiet: beQuiet, - ); - return _handleStatus(status); - } - - /// 离开 StreamChannel - Future leaveStreamChannel(String channelName) async { - final channel = _streamChannels[channelName]; - if (channel == null) return; - - final (status, _) = await channel.leave(); - _handleStatus(status); - await channel.release(); - _streamChannels.remove(channelName); - } - - /// 离开全部 StreamChannel - Future leaveAllStreamChannels() async { - final names = _streamChannels.keys.toList(); - for (final name in names) { - await leaveStreamChannel(name); - } - } - - /// 加入主题 - Future joinTopic({ - required String channelName, - required String topic, - RtmMessageQos qos = RtmMessageQos.unordered, - RtmMessagePriority priority = RtmMessagePriority.normal, - String meta = '', - bool syncWithMedia = false, - }) async { - final channel = await _requireChannel(channelName); - final (status, _) = await channel.joinTopic( - topic, - qos: qos, - priority: priority, - meta: meta, - syncWithMedia: syncWithMedia, - ); - return _handleStatus(status); - } - - /// 发送主题消息 - Future publishTopicMessage({ - required String channelName, - required String topic, - required String message, - int sendTs = 0, - String? customType, - }) async { - final channel = await _requireChannel(channelName); - final (status, _) = await channel.publishTextMessage( - topic, - message, - sendTs: sendTs, - customType: customType, - ); - return _handleStatus(status); - } - /// 释放 RTM Client Future dispose() async { - await leaveAllStreamChannels(); if (_client != null && _isLoggedIn) { final (status, _) = await _client!.logout(); _handleStatus(status); @@ -248,20 +193,6 @@ class RTMManager { ); } - Future _requireChannel(String channelName) async { - if (!_streamChannels.containsKey(channelName)) { - final ok = await joinStreamChannel(channelName); - if (!ok) { - throw Exception('加入 StreamChannel 失败:$channelName'); - } - } - final channel = _streamChannels[channelName]; - if (channel == null) { - throw Exception('StreamChannel 不存在:$channelName'); - } - return channel; - } - bool _handleStatus(RtmStatus status) { if (status.error) { onOperationError?.call(status); diff --git a/pubspec.lock b/pubspec.lock index 65f7f97..1886d6d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.2.5" + agora_token_generator: + dependency: "direct main" + description: + name: agora_token_generator + sha256: eeb53d753430b6d6227b05ace89655cd7990b3137a236937825699d528377904 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" analyzer: dependency: transitive description: @@ -1116,6 +1124,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.0" pool: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c20394b..a0b33f7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,6 +70,7 @@ dependencies: agora_rtc_engine: ^6.5.3 pull_to_refresh: ^2.0.0 agora_rtm: ^2.2.5 + agora_token_generator: ^1.0.0 location_plugin: path: location_plugin