Browse Source

feat(live): 实现直播观看功能并优化RTC管理

- 新增观众端加入直播间逻辑,支持主播和观众两种角色
- 优化RTC管理器,增加远程用户列表监听和通知机制
- 调整直播页面UI,完善主播展示区域和连麦用户显示
- 更新网络请求,支持通过频道ID获取RTC Token
- 完善登录流程,修复用户ID存储逻辑
- 增加网络安全配置文件,提升应用安全性
- 扩展Android支持的ABI架构,提高兼容性
- 优化登录页面布局和交互细节
ios
Jolie 4 months ago
parent
commit
0b981bc13b
14 changed files with 293 additions and 182 deletions
  1. 3
      android/app/build.gradle.kts
  2. 1
      android/app/src/main/AndroidManifest.xml
  3. 13
      android/app/src/main/res/xml/network_security_config.xml
  4. 29
      lib/controller/discover/room_controller.dart
  5. 2
      lib/controller/mine/login_controller.dart
  6. 4
      lib/main.dart
  7. 4
      lib/network/rtc_api.dart
  8. 6
      lib/network/rtc_api.g.dart
  9. 18
      lib/pages/discover/discover_page.dart
  10. 2
      lib/pages/discover/live_room_page.dart
  11. 116
      lib/pages/mine/login_page.dart
  12. 79
      lib/rtc/rtc_manager.dart
  13. 14
      lib/rtc/rtm_manager.dart
  14. 184
      lib/widget/live/live_room_anchor_showcase.dart

3
android/app/build.gradle.kts

@ -55,6 +55,9 @@ android {
ndk {
abiFilters.clear()
abiFilters += "arm64-v8a"
abiFilters += "armeabi-v7a"
abiFilters += "x86"
abiFilters += "x86_64"
// 或者:abiFilters.add("arm64-v8a")
}

1
android/app/src/main/AndroidManifest.xml

@ -8,6 +8,7 @@
<application
android:label="动我"
android:name="${applicationName}"
android:networkSecurityConfig="@xml/network_security_config">
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"

13
android/app/src/main/res/xml/network_security_config.xml

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<!-- 信任系统默认CA -->
<certificates src="system" />
</trust-anchors>
<!-- 强制TLS 1.2+ -->
<tls-config>
<min-tls-version>TLSv1.2</min-tls-version>
</tls-config>
</base-config>
</network-security-config>

29
lib/controller/discover/room_controller.dart

@ -1,3 +1,5 @@
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:agora_token_generator/agora_token_generator.dart';
import 'package:dating_touchme_app/model/live/live_chat_message.dart';
import 'package:dating_touchme_app/model/rtc/rtc_channel_data.dart';
import 'package:dating_touchme_app/network/network_service.dart';
@ -77,16 +79,15 @@ class RoomController extends GetxController {
/// RTC
Future<void> createRtcChannel() async {
if (isLoading.value) return;
final granted = await _ensureRtcPermissions();
if (!granted) return;
try {
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!.token, base.data!.channelId, base.data!.uid, ClientRoleType.clientRoleBroadcaster);
await _joinRtcChannel(base.data!.token, base.data!.channelId, base.data!.uid);
} else {
final message = base.message.isNotEmpty ? base.message : '创建频道失败';
@ -94,20 +95,34 @@ class RoomController extends GetxController {
}
} catch (e) {
SmartDialog.showToast('创建频道异常:$e');
} finally {
isLoading.value = false;
}
}
Future<void> joinChannel(String channelName) async {
try {
final response = await _networkService.rtcApi.getSwRtcToken(channelName);
final base = response.data;
if (base.isSuccess && base.data != null) {
rtcChannel.value = base.data;
await _joinRtcChannel(base.data!.token, channelName, base.data!.uid, ClientRoleType.clientRoleAudience);
}
} catch (e) {
SmartDialog.showToast('加入频道异常:$e');
}
}
Future<void> _joinRtcChannel(
String token,
String channelName,
int uid,
ClientRoleType roleType
) async {
try {
await RTCManager.instance.joinChannel(
token: token,
channelId: channelName,
uid: uid,
role: roleType,
);
} catch (e) {
SmartDialog.showToast('加入频道失败:$e');
@ -117,7 +132,7 @@ class RoomController extends GetxController {
///
Future<void> sendChatMessage(String content) async {
final channelName = rtcChannel.value?.channelId ?? RTCManager.instance.currentChannelId;
final result = await _messageService.sendMessage(
content: content,
channelName: channelName,
@ -152,8 +167,8 @@ class RoomController extends GetxController {
return false;
}
Future<void> disposeRtcResources() async {
await RTCManager.instance.dispose();
Future<void> leaveChannel() async {
await RTCManager.instance.leaveChannel();
}
}

2
lib/controller/mine/login_controller.dart

@ -115,7 +115,7 @@ class LoginController extends GetxController {
GlobalData().userId = result.userId;
GlobalData().qnToken = result.token;
await storage.write('token', result.token);
// await storage.write('userId', result.userId);
await storage.write('userId', result.userId);
//
await _handleUserInfoRetrieval(result.userId);

4
lib/main.dart

@ -84,10 +84,10 @@ class MyApp extends StatelessWidget {
// token是否为空
final storage = GetStorage();
final userId = storage.read<String>('userId');
final token = storage.read<String>('token');
// token不为空token为空
if (userId != null && userId.isNotEmpty) {
if (token != null && token.isNotEmpty) {
return MainPage();
} else {
return LoginPage();

4
lib/network/rtc_api.dart

@ -13,7 +13,9 @@ abstract class RtcApi {
///
@GET(ApiUrls.getSwRtcToken)
Future<HttpResponse<BaseResponse<RtcChannelData>>> getSwRtcToken();
Future<HttpResponse<BaseResponse<RtcChannelData>>> getSwRtcToken(
@Query('channelId') String channelId,
);
/// RTM Token
@GET(ApiUrls.getSwRtmToken)

6
lib/network/rtc_api.g.dart

@ -20,9 +20,11 @@ class _RtcApi implements RtcApi {
final ParseErrorLogger? errorLogger;
@override
Future<HttpResponse<BaseResponse<RtcChannelData>>> getSwRtcToken() async {
Future<HttpResponse<BaseResponse<RtcChannelData>>> getSwRtcToken(
String channelId,
) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final queryParameters = <String, dynamic>{r'channelId': channelId};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<HttpResponse<BaseResponse<RtcChannelData>>>(

18
lib/pages/discover/discover_page.dart

@ -149,11 +149,25 @@ class LiveItem extends StatefulWidget {
}
class _LiveItemState extends State<LiveItem> {
late final RoomController roomController;
@override
void initState() {
super.initState();
if (Get.isRegistered<RoomController>()) {
roomController = Get.find<RoomController>();
} else {
roomController = Get.put(RoomController());
}
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
Get.to(() => LiveRoomPage(id: 0));
onTap: () async{
// Get.to(() => LiveRoomPage(id: 0));
await roomController.joinChannel('1189028638616588288');
},
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(10.w)),

2
lib/pages/discover/live_room_page.dart

@ -75,7 +75,7 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
@override
void dispose() {
_roomController.disposeRtcResources();
_roomController.leaveChannel();
_messageController.dispose();
super.dispose();
}

116
lib/pages/mine/login_page.dart

@ -7,7 +7,7 @@ import 'package:dating_touchme_app/controller/mine/login_controller.dart';
class LoginPage extends StatelessWidget {
LoginPage({super.key});
//
final agreeTerms = Rx<bool>(false);
@ -20,13 +20,16 @@ class LoginPage extends StatelessWidget {
resizeToAvoidBottomInset: false,
body: Stack(
children: [
Image.asset(
Assets.imagesLoginBg,
width: 1.sw,
height: 1.sh,
),
Container(
width: double.infinity,
height: 1.sh,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(Assets.imagesLoginBg),
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
),
padding: EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
@ -35,10 +38,7 @@ class LoginPage extends StatelessWidget {
Center(
child: Column(
children: [
Image.asset(
Assets.imagesLoginLogo,
height: 60,
),
Image.asset(Assets.imagesLoginLogo, height: 60),
const SizedBox(height: 10),
const Text(
'心动就动我 幸福马上行动',
@ -70,7 +70,10 @@ class LoginPage extends StatelessWidget {
decoration: const InputDecoration(
hintText: '请输入你的手机号',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 15, vertical: 14),
contentPadding: EdgeInsets.symmetric(
horizontal: 15,
vertical: 14,
),
counterText: '',
),
keyboardType: TextInputType.phone,
@ -101,7 +104,10 @@ class LoginPage extends StatelessWidget {
decoration: const InputDecoration(
hintText: '请输入验证码',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 15, vertical: 14),
contentPadding: EdgeInsets.symmetric(
horizontal: 15,
vertical: 14,
),
counterText: '',
),
keyboardType: TextInputType.number,
@ -114,18 +120,25 @@ class LoginPage extends StatelessWidget {
),
//
GestureDetector(
onTap: controller.isSendingCode.value || controller.countdownSeconds.value > 0
onTap:
controller.isSendingCode.value ||
controller.countdownSeconds.value > 0
? null
: controller.getVerificationCode,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 14),
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 14,
),
child: Text(
controller.countdownSeconds.value > 0
? '${controller.countdownSeconds.value}秒后重试'
: '获取验证码',
style: TextStyle(
fontSize: 14,
color: (controller.isSendingCode.value || controller.countdownSeconds.value > 0)
color:
(controller.isSendingCode.value ||
controller.countdownSeconds.value > 0)
? Colors.grey.shade400
: const Color.fromRGBO(74, 99, 235, 1),
),
@ -141,16 +154,19 @@ class LoginPage extends StatelessWidget {
//
Row(
children: [
Obx(() => Checkbox(
value: agreeTerms.value,
onChanged: (value) {
agreeTerms.value = value ?? false;
},
activeColor: Colors.grey,
side: const BorderSide(color: Colors.grey),
shape: const CircleBorder(),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
)),
Obx(
() => Checkbox(
value: agreeTerms.value,
onChanged: (value) {
agreeTerms.value = value ?? false;
},
activeColor: Colors.grey,
side: const BorderSide(color: Colors.grey),
shape: const CircleBorder(),
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
),
),
const Text(
'我已阅读并同意',
style: TextStyle(
@ -199,18 +215,20 @@ class LoginPage extends StatelessWidget {
onPressed: controller.isLoggingIn.value
? null
: () {
//
if (!agreeTerms.value) {
SmartDialog.showToast('请阅读并同意用户协议和隐私政策');
return;
}
//
if (!agreeTerms.value) {
SmartDialog.showToast('请阅读并同意用户协议和隐私政策');
return;
}
//
controller.login();
},
//
controller.login();
},
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 50),
backgroundColor: agreeTerms.value ? const Color.fromRGBO(74, 99, 235, 1) : Colors.grey.shade300,
backgroundColor: agreeTerms.value
? const Color.fromRGBO(74, 99, 235, 1)
: Colors.grey.shade300,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
@ -218,26 +236,26 @@ class LoginPage extends StatelessWidget {
),
child: controller.isLoggingIn.value
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Text(
'注册并登录',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
'注册并登录',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
],
),
)
),
],
),
);

79
lib/rtc/rtc_manager.dart

@ -11,6 +11,9 @@ import '../pages/discover/live_room_page.dart';
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;
@ -26,8 +29,8 @@ class RTCManager {
bool _isInChannel = false;
String? _currentChannelId;
int? _currentUid;
int? _streamId;
ClientRoleType _clientRole = ClientRoleType.clientRoleBroadcaster;
final List<int> _remoteUserIds = <int>[];
//
Function(RtcConnection connection, int elapsed)? onJoinChannelSuccess;
Function(RtcConnection connection, int remoteUid, int elapsed)? onUserJoined;
@ -86,9 +89,9 @@ class RTCManager {
await _engine!.initialize(
RtcEngineContext(appId: appId, channelProfile: channelProfile),
);
await _engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
await _engine?.setClientRole(role: _clientRole);
await _engine?.enableVideo();
await _engine?.startPreview();
// await _engine?.startPreview();
//
_registerEventHandlers();
@ -107,16 +110,19 @@ class RTCManager {
_engine!.registerEventHandler(
RtcEngineEventHandler(
onJoinChannelSuccess: (RtcConnection connection, int elapsed) async{
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){
if (connection.localUid == _currentUid) {
await RTMManager.instance.subscribe(_currentChannelId ?? '');
await RTMManager.instance.publishChannelMessage(
channelName: _currentChannelId ?? '',
message: json.encode({'type': 'join_room', 'uid': _currentUid})
channelName: _currentChannelId ?? '',
message: json.encode({'type': 'join_room', 'uid': _currentUid}),
);
Get.to(() => const LiveRoomPage(id: 0));
}
@ -126,13 +132,11 @@ class RTCManager {
},
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
print('用户加入,UID:$remoteUid');
_handleRemoteUserJoined(remoteUid);
if (onUserJoined != null) {
onUserJoined!(connection, remoteUid, elapsed);
}
},
onStreamMessage: (RtcConnection connection, int remoteUid, int streamId, Uint8List data, int length, int sentTs){
print('收到消息,UID:$remoteUid,流ID:$streamId,数据:${utf8.decode(data)}');
},
onUserOffline:
(
RtcConnection connection,
@ -140,12 +144,15 @@ class RTCManager {
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}');
@ -343,7 +350,7 @@ class RTCManager {
String? token,
required String channelId,
int uid = 0,
ChannelMediaOptions? options,
ClientRoleType role = ClientRoleType.clientRoleBroadcaster,
}) async {
if (_engine == null) {
throw Exception('RTC Engine not initialized');
@ -352,15 +359,24 @@ class RTCManager {
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: options ?? const ChannelMediaOptions(),
options: ChannelMediaOptions(
channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
clientRoleType: role,
autoSubscribeAudio: true,
autoSubscribeVideo: true,
publishCameraTrack: true,
publishMicrophoneTrack: true,
),
);
_streamId = await _engine?.createDataStream(DataStreamConfig(syncWithAudio: false, ordered: false));
print('正在加入频道:$channelId,UID:$uid');
}
@ -373,7 +389,7 @@ class RTCManager {
print('当前不在频道中');
return;
}
await RTMManager.instance.unsubscribe(_currentChannelId ?? '');
await _engine!.leaveChannel();
_currentUid = null;
print('已离开频道');
@ -417,24 +433,18 @@ class RTCManager {
if (_engine == null) {
throw Exception('RTC Engine not initialized');
}
_clientRole = role;
await _engine!.setClientRole(role: role, options: options);
print('客户端角色已设置为:$role');
}
///
Future<void> sendMessage(String message) async {
Uint8List data = utf8.encode(message);
await _engine!.sendStreamMessage(
streamId: _streamId ?? 0,
data: data,
length: data.length,
);
print('已发送消息:$message');
}
/// ID
String? get currentChannelId => _currentChannelId;
ClientRoleType get clientRole => _clientRole;
List<int> get remoteUserIds => List<int>.unmodifiable(_remoteUserIds);
///
Future<void> dispose() async {
try {
@ -445,6 +455,8 @@ class RTCManager {
await _engine!.release();
_engine = null;
}
_remoteUserIds.clear();
remoteUsersNotifier.value = const [];
_isInitialized = false;
_isInChannel = false;
_currentChannelId = null;
@ -455,4 +467,17 @@ class RTCManager {
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);
}
}

14
lib/rtc/rtm_manager.dart

@ -74,13 +74,13 @@ class RTMManager {
_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);
}
// final response = await _networkService.rtcApi.getSwRtmToken();
// //
// if (response.data.isSuccess) {
await login('007eJxTYIhoZ/m/gMf0gdWv1PV2R6I/Lpry06W77sOR2BDuFv89JeEKDCbJRqmJlinJSSbJpiYmBqaWxokmhhYpaQZpKSmmhkYpE9/IZgrwMTDYOB4rZ2RgYmAEQhCfkcEcAPTmHg4=');
// } else {
// SmartDialog.showToast(response.data.message);
// }
return true;
}

184
lib/widget/live/live_room_anchor_showcase.dart

@ -19,114 +19,132 @@ class _LiveRoomAnchorShowcaseState extends State<LiveRoomAnchorShowcase> {
return ValueListenableBuilder<bool>(
valueListenable: _rtcManager.channelJoinedNotifier,
builder: (context, joined, _) {
return Column(
children: [
Stack(
return ValueListenableBuilder<List<int>>(
valueListenable: _rtcManager.remoteUsersNotifier,
builder: (context, remoteUids, __) {
final int? remoteUid = remoteUids.isNotEmpty
? remoteUids.first
: null;
return Column(
children: [
_buildAnchorVideo(joined),
Positioned(
top: 5.w,
left: 5.w,
child: Container(
width: 42.w,
height: 13.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(13.w)),
color: const Color.fromRGBO(142, 20, 186, 1),
),
child: Center(
child: Text(
"主持人",
style: TextStyle(fontSize: 9.w, color: Colors.white),
Stack(
children: [
_buildAnchorVideo(joined, remoteUid),
Positioned(
top: 5.w,
left: 5.w,
child: Container(
width: 42.w,
height: 13.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(13.w)),
color: const Color.fromRGBO(142, 20, 186, 1),
),
child: Center(
child: Text(
"主持人",
style: TextStyle(
fontSize: 9.w,
color: Colors.white,
),
),
),
),
),
),
),
Positioned(
top: 5.w,
right: 5.w,
child: Container(
width: 20.w,
height: 20.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(20.w)),
color: const Color.fromRGBO(0, 0, 0, .3),
),
child: Center(
child: Image.asset(
Assets.imagesGiftIcon,
width: 19.w,
height: 19.w,
Positioned(
top: 5.w,
right: 5.w,
child: Container(
width: 20.w,
height: 20.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(20.w)),
color: const Color.fromRGBO(0, 0, 0, .3),
),
child: Center(
child: Image.asset(
Assets.imagesGiftIcon,
width: 19.w,
height: 19.w,
),
),
),
),
),
),
Positioned(
bottom: 5.w,
right: 5.w,
child: Container(
width: 47.w,
height: 20.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(20.w)),
color: Colors.white,
),
child: Center(
child: Text(
"加好友",
style: TextStyle(
fontSize: 11.w,
color: const Color.fromRGBO(117, 98, 249, 1),
Positioned(
bottom: 5.w,
right: 5.w,
child: Container(
width: 47.w,
height: 20.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(20.w)),
color: Colors.white,
),
child: Center(
child: Text(
"加好友",
style: TextStyle(
fontSize: 11.w,
color: const Color.fromRGBO(117, 98, 249, 1),
),
),
),
),
),
),
),
],
),
SizedBox(height: 5.w),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildSideAnchorCard(
isLeft: true,
micIcon: Assets.imagesMicClose,
],
),
_buildSideAnchorCard(
isLeft: false,
micIcon: Assets.imagesMicOpen,
SizedBox(height: 5.w),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildSideAnchorCard(
isLeft: true,
micIcon: Assets.imagesMicClose,
),
_buildSideAnchorCard(
isLeft: false,
micIcon: Assets.imagesMicOpen,
),
],
),
],
),
],
);
},
);
},
);
}
Widget _buildAnchorVideo(bool joined) {
Widget _buildAnchorVideo(bool joined, int? remoteUid) {
final engine = _rtcManager.engine;
if (!joined || engine == null) {
return _buildWaitingPlaceholder();
}
final localUid = _rtcManager.currentUid ?? 0;
print('joined: $joined');
ClientRoleType role = _rtcManager.clientRole;
return ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(9.w)),
child: SizedBox(
width: 177.w,
height: 175.w,
child: AgoraVideoView(
controller: VideoViewController(
rtcEngine: engine,
canvas: VideoCanvas(
uid: 0,
),
),
onAgoraVideoViewCreated: (viewId){
engine.startPreview();
},
),
child: role == ClientRoleType.clientRoleBroadcaster
? AgoraVideoView(
controller: VideoViewController(
rtcEngine: engine,
canvas: const VideoCanvas(uid: 0),
),
)
: (_rtcManager.currentChannelId == null
? _buildWaitingPlaceholder()
: AgoraVideoView(
controller: VideoViewController.remote(
rtcEngine: engine,
canvas: VideoCanvas(uid: _rtcManager.remoteUserIds.first),
connection: RtcConnection(
channelId: _rtcManager.currentChannelId!,
),
),
)),
),
);
}

Loading…
Cancel
Save