Browse Source

feat(rtc): 集成声网RTC和RTM功能以支持实时音视频通话

- 新增获取声网RTC和RTM Token的接口与实现
- 更新RTC管理器以支持频道创建及加入流程
- 实现RTM管理器初始化、登录及频道订阅功能
- 添加agora_token_generator依赖用于生成RTM令牌
- 修改RTC初始化逻辑以适配直播场景并启用视频预览- 在用户加入频道成功后自动订阅RTM频道并发送加入房间消息
-优化网络请求代码结构,新增RTC相关API和服务端交互逻辑
ios
Jolie 4 months ago
parent
commit
dc05668ccc
11 changed files with 175 additions and 127 deletions
  1. 13
      lib/controller/discover/room_controller.dart
  2. 4
      lib/main.dart
  3. 4
      lib/model/rtc/rtc_channel_data.dart
  4. 2
      lib/network/api_urls.dart
  5. 8
      lib/network/rtc_api.dart
  6. 62
      lib/network/rtc_api.g.dart
  7. 7
      lib/pages/main/main_page.dart
  8. 22
      lib/rtc/rtc_manager.dart
  9. 163
      lib/rtc/rtm_manager.dart
  10. 16
      pubspec.lock
  11. 1
      pubspec.yaml

13
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<void> _joinRtcChannel(RtcChannelData data) async {
Future<void> _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');
}

4
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);

4
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<String, dynamic> 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,
};
}

2
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';

8
lib/network/rtc_api.dart

@ -12,6 +12,14 @@ abstract class RtcApi {
factory RtcApi(Dio dio) = _RtcApi;
///
@GET(ApiUrls.getSwRtcToken)
Future<HttpResponse<BaseResponse<RtcChannelData>>> getSwRtcToken();
/// RTM Token
@GET(ApiUrls.getSwRtmToken)
Future<HttpResponse<BaseResponse<RtcChannelData>>> getSwRtmToken();
///
@POST(ApiUrls.createRtcChannel)
Future<HttpResponse<BaseResponse<RtcChannelData>>> createRtcChannel();
}

62
lib/network/rtc_api.g.dart

@ -19,6 +19,68 @@ class _RtcApi implements RtcApi {
final ParseErrorLogger? errorLogger;
@override
Future<HttpResponse<BaseResponse<RtcChannelData>>> getSwRtcToken() async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<HttpResponse<BaseResponse<RtcChannelData>>>(
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<Map<String, dynamic>>(_options);
late BaseResponse<RtcChannelData> _value;
try {
_value = BaseResponse<RtcChannelData>.fromJson(
_result.data!,
(json) => RtcChannelData.fromJson(json as Map<String, dynamic>),
);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
final httpResponse = HttpResponse(_value, _result);
return httpResponse;
}
@override
Future<HttpResponse<BaseResponse<RtcChannelData>>> getSwRtmToken() async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<HttpResponse<BaseResponse<RtcChannelData>>>(
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<Map<String, dynamic>>(_options);
late BaseResponse<RtcChannelData> _value;
try {
_value = BaseResponse<RtcChannelData>.fromJson(
_result.data!,
(json) => RtcChannelData.fromJson(json as Map<String, dynamic>),
);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
final httpResponse = HttpResponse(_value, _result);
return httpResponse;
}
@override
Future<HttpResponse<BaseResponse<RtcChannelData>>> createRtcChannel() async {
final _extra = <String, dynamic>{};

7
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<MainPage> {
Get.put(RouteGuardService());
// token并调用获取婚姻信息详情的方法
checkTokenAndFetchMarriageInfo();
initRTM();
}
// token并获取婚姻信息详情
@ -58,6 +60,11 @@ class _MainPageState extends State<MainPage> {
await userController.getBaseUserInfo(userId, true);
}
}
initRTM() async {
String? userId = storage.read<String>('userId');
await RTMManager.instance.initialize(appId: '4c2ea9dcb4c5440593a418df0fdd512d', userId: userId ?? '');
}
@override
Widget build(BuildContext context) {

22
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<bool> 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);
}

163
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<String, StreamChannel> _streamChannels = {};
@ -52,39 +61,73 @@ class RTMManager {
_currentUserId == userId) {
return true;
}
_networkService = Get.find<NetworkService>();
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<bool> 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<bool> subscribe(String channelName) async {
_ensureInitialized();
final (status, _) = await _client!.subscribe(channelName);
print(!status.error ? 'RTM 订阅成功' : 'RTM 订阅失败');
return _handleStatus(status);
}
///
Future<bool> unsubscribe(String channelName) async {
_ensureInitialized();
final (status, _) = await _client!.unsubscribe(channelName);
return _handleStatus(status);
}
/// RTM
Future<void> 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<bool> setParameters(String paramsJson) async {
_ensureInitialized();
final status = await _client!.setParameters(paramsJson);
return _handleStatus(status);
}
///
Future<bool> publishChannelMessage({
required String channelName,
@ -123,99 +159,8 @@ class RTMManager {
return _handleStatus(status);
}
/// / StreamChannel
Future<bool> 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<void> 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<void> leaveAllStreamChannels() async {
final names = _streamChannels.keys.toList();
for (final name in names) {
await leaveStreamChannel(name);
}
}
///
Future<bool> 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<bool> 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<void> dispose() async {
await leaveAllStreamChannels();
if (_client != null && _isLoggedIn) {
final (status, _) = await _client!.logout();
_handleStatus(status);
@ -248,20 +193,6 @@ class RTMManager {
);
}
Future<StreamChannel> _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);

16
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:

1
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

Loading…
Cancel
Save