diff --git a/wgshare/lib/common/models/meeting_room_user.dart b/wgshare/lib/common/models/meeting_room_user.dart index cb810de..5ec6707 100644 --- a/wgshare/lib/common/models/meeting_room_user.dart +++ b/wgshare/lib/common/models/meeting_room_user.dart @@ -36,7 +36,10 @@ class MeetingRoomUser extends Object{ @JsonKey(name: 'isRoomManager') bool isRoomManager; - MeetingRoomUser(this.uid,this.connectId,this.account,this.enableMicr,this.enableCamera,this.screenShareId,this.userName,this.roleId,this.roleName,this.isRoomManager,); + @JsonKey(name: 'volume') + double? volume = 0.0; + + MeetingRoomUser(this.uid,this.connectId,this.account,this.enableMicr,this.enableCamera,this.screenShareId,this.userName,this.roleId,this.roleName,this.isRoomManager,this.volume); factory MeetingRoomUser.fromJson(Map srcJson) => _$MeetingRoomUserFromJson(srcJson); diff --git a/wgshare/lib/pages/metting/meeting_main_logic.dart b/wgshare/lib/pages/metting/meeting_main_logic.dart index ef48ebf..bd55e76 100644 --- a/wgshare/lib/pages/metting/meeting_main_logic.dart +++ b/wgshare/lib/pages/metting/meeting_main_logic.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; import 'package:signalr_core/signalr_core.dart'; import 'package:wgshare/common/store/user_store.dart'; +import 'package:wgshare/utils/count_microphone_volume.dart'; import '../../common/config/request_config.dart'; import '../../common/mixins/request_tool_mixin.dart'; import '../../common/models/common/base_structure_result.dart'; @@ -137,9 +138,10 @@ class MeetingMainLogic extends GetxController with RequestToolMixin{ /// 结束发言 Future doHttpCancelSpeak() async { BaseStructureResult res = await getClient().cancelSpeak(state.meetingRoomInfo.value!.id, state.meetingRoomInfo.value!.roomNum, UserStore.to.userInfoEntity.value!.uid); + setClientRole("观众"); } - /// 设置麦克风是否静音 + /// 设置麦克风是否开启 void setMicrophoneOpen(bool isOpen){ state.isOpenMicrophone.value = isOpen; for(var i = 0; i < state.cacheUsers.value.length; i++){ @@ -148,7 +150,22 @@ class MeetingMainLogic extends GetxController with RequestToolMixin{ } } state.users.value = state.cacheUsers.value; - setEnableLocalAudio(isOpen); + if(isOpen == true){ + setClientRole("主播"); + }else{ + setClientRole("观众"); + } + } + + /// 设置视频是否打开 + void setCameraOpen(bool isOpen){ + state.isOpenCamera.value = isOpen; + if(isOpen == true){ + setEnableLocalVideo(isOpen); + setClientRole("主播"); + }else{ + setClientRole("观众"); + } } /// --------------------------signalR Socket相关 @@ -207,7 +224,7 @@ class MeetingMainLogic extends GetxController with RequestToolMixin{ if(UserStore.to.userInfoEntity.value!.uid == meetingRoomUser.uid){ state.isSpeak.value = true; state.isOpenMicrophone.value = true; - setEnableLocalAudio(true); + setClientRole("主播"); } }else{ debugPrint("wgs输出===:Socket-停止发言:${e?[0]}--${e?[1]}"); @@ -221,7 +238,7 @@ class MeetingMainLogic extends GetxController with RequestToolMixin{ if(UserStore.to.userInfoEntity.value!.uid == meetingRoomUser.uid){ state.isSpeak.value = false; state.isOpenMicrophone.value = false; - setEnableLocalAudio(false); + setClientRole("观众"); } } update(); @@ -268,7 +285,6 @@ class MeetingMainLogic extends GetxController with RequestToolMixin{ } state.users.value = state.cacheUsers.value; state.isOpenMicrophone.value = e?[0]; - setEnableLocalAudio(e?[0]); }); /// 单独用户开闭麦回调 @@ -284,7 +300,6 @@ class MeetingMainLogic extends GetxController with RequestToolMixin{ } } state.isOpenMicrophone.value = true; - setEnableLocalAudio(true); }else{ debugPrint("wgs输出===:Socket-单独用户闭麦"); for(MeetingRoomUser mru in state.cacheUsers.value){ @@ -293,7 +308,6 @@ class MeetingMainLogic extends GetxController with RequestToolMixin{ } } state.isOpenMicrophone.value = false; - setEnableLocalAudio(false); } }); } @@ -340,7 +354,11 @@ class MeetingMainLogic extends GetxController with RequestToolMixin{ )); // 设置默认音频路由为听筒 - state.rctEngine.value?.setDefaultAudioRouteToSpeakerphone(false); + await state.rctEngine.value?.setDefaultAudioRouteToSpeakerphone(false); + // 打开用户音量回调 + await state.rctEngine.value?.enableAudioVolumeIndication(interval: 200, smooth: 3, reportVad: true); + // 启用音频模块 + await state.rctEngine.value?.enableAudio(); joinMeetingToRtc(); @@ -384,6 +402,49 @@ class MeetingMainLogic extends GetxController with RequestToolMixin{ // 音频采集开关回调 onLocalAudioStateChanged: (RtcConnection connection, LocalAudioStreamState state, LocalAudioStreamReason reason){ debugPrint("wgs输出===:RTC-音频采集开关:$state"); + }, + + // 视频采集开关回调 + onRemoteVideoStateChanged: (RtcConnection connection, + int remoteUid, + RemoteVideoState state, + RemoteVideoStateReason reason, + int elapsed){ + debugPrint("wgs输出===:RTC-视频采集开关:$state"); + }, + + // 用户音量提示回调 + onAudioVolumeIndication: ( + RtcConnection connection, + List speakers, + int speakerNumber, + int totalVolume){ + if(speakers.isNotEmpty){ + for(AudioVolumeInfo avi in speakers){ + for(MeetingRoomUser mru in state.cacheUsers.value){ + if(avi.uid == 0){ + debugPrint("wgs输出===:RTC-用户音量提示(自己):${CountMicrophoneVolume.getVolume(avi.volume!)}"); + mru.volume = CountMicrophoneVolume.getVolume(avi.volume!); + state.microphoneVolume.value = CountMicrophoneVolume.getVolume(avi.volume!); + }else{ + if(avi.uid.toString() == mru.uid){ + debugPrint("wgs输出===:RTC-用户音量提示(远端用户):${speakers[0].uid}--${speakers[0].volume}"); + mru.volume = CountMicrophoneVolume.getVolume(avi.volume!); + } + } + } + } + + } + }, + + // 切换用户角色回调 + onClientRoleChanged: ( + RtcConnection connection, + ClientRoleType oldRole, + ClientRoleType newRole, + ClientRoleOptions newRoleOptions){ + debugPrint("wgs输出===:RTC-切换用户角色"); } ), ); @@ -394,21 +455,21 @@ class MeetingMainLogic extends GetxController with RequestToolMixin{ await state.rctEngine.value?.joinChannel( token: state.meetingToken.value, channelId: state.roomNumber.value, - uid: 0, + uid: int.parse(UserStore.to.userInfoEntity.value!.uid), options: const ChannelMediaOptions( // 自动订阅所有视频流 autoSubscribeVideo: true, // 自动订阅所有音频流 autoSubscribeAudio: true, // 发布摄像头采集的视频 - publishCameraTrack: false, + publishCameraTrack: true, // 发布麦克风采集的音频 - publishMicrophoneTrack: false, + publishMicrophoneTrack: true, // 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众) // 这里设置角色为clientRoleBroadcaster(主播) // 主播:可以在频道内发布音视频,同时也可以订阅其他主播发布的音视频 // 观众:可以在频道内订阅音视频,不具备发布音视频权限 - clientRoleType: ClientRoleType.clientRoleBroadcaster), + clientRoleType: ClientRoleType.clientRoleAudience), ); } @@ -420,14 +481,34 @@ class MeetingMainLogic extends GetxController with RequestToolMixin{ await state.rctEngine.value?.release(); } + /// 设置用户角色 + Future setClientRole(String roleStr) async { + if(roleStr == "主播"){ + await state.rctEngine.value?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); + }else{ + await state.rctEngine.value?.setClientRole(role: ClientRoleType.clientRoleAudience); + } + } + /// 设置音频输出路由(没有外接设备时生效) Future setEnableSpeakerphone(int mode) async { state.communicationMode.value = mode; await state.rctEngine.value?.setEnableSpeakerphone(mode == 1 ? false : true); } - /// 设置是否打开本地音频采集 - Future setEnableLocalAudio(bool enabled) async { - await state.rctEngine.value?.enableLocalAudio(enabled); + /// 设置是否打开本地视频采集 + Future setEnableLocalVideo(bool enabled) async { + state.isOpenCamera.value = enabled; + if(enabled == true){ + // 启用视频模块 + await state.rctEngine.value?.enableVideo(); + // 启用本地预览 + await state.rctEngine.value?.startPreview(); + }else{ + // 关闭视频模块 + await state.rctEngine.value?.disableVideo(); + // 关闭本地预览 + await state.rctEngine.value?.stopPreview(); + } } } diff --git a/wgshare/lib/pages/metting/meeting_main_state.dart b/wgshare/lib/pages/metting/meeting_main_state.dart index 074ac0e..9cac268 100644 --- a/wgshare/lib/pages/metting/meeting_main_state.dart +++ b/wgshare/lib/pages/metting/meeting_main_state.dart @@ -55,6 +55,10 @@ class MeetingMainState { late RxBool isSpeak = false.obs; /// 是否打开麦克风 late RxBool isOpenMicrophone = false.obs; + /// 麦克风音量 + late RxDouble microphoneVolume = 0.0.obs; + /// 是否打开摄像头 + late RxBool isOpenCamera = false.obs; /// 聊天数据 late RxList meetingRoomMsgs = RxList([]); diff --git a/wgshare/lib/pages/metting/meeting_main_view.dart b/wgshare/lib/pages/metting/meeting_main_view.dart index 1be7a46..5b54775 100644 --- a/wgshare/lib/pages/metting/meeting_main_view.dart +++ b/wgshare/lib/pages/metting/meeting_main_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:liquid_progress_indicator_v2/liquid_progress_indicator.dart'; import 'package:wgshare/common/store/user_store.dart'; import 'package:wgshare/pages/metting/share/meeting_main_share_view.dart'; import 'package:wgshare/pages/metting/video/meeting_main_video_view.dart'; @@ -9,6 +10,7 @@ import 'package:wgshare/utils/toast_utils.dart'; import '../../utils/color_util.dart'; import '../../utils/cus_behavior.dart'; +import '../../view/view_svg_path.dart'; import 'meeting_main_logic.dart'; import 'meeting_main_state.dart'; import 'voice/meeting_main_voice_view.dart'; @@ -167,19 +169,21 @@ class MeetingMainPage extends StatelessWidget { // 语音 Visibility( - visible: true, + visible: state.pageState.value == 0, child: MeetingMainVoiceComponent(users: state.cacheUsers.value) ), // 视频 Visibility( - visible: false, - child: MeetingMainVideoComponent() + visible: state.pageState.value == 1, + child: null != state.rctEngine.value + ? MeetingMainVideoComponent(rtcEngine: state.rctEngine.value!, channelId: state.roomNumber.value,isOpenCamera: state.isOpenCamera.value) + : Container() ), // 共享屏幕 Visibility( - visible: false, + visible: state.pageState.value == 2, child: MeetingMainShareComponent() ), @@ -247,15 +251,33 @@ class MeetingMainPage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Image.asset( + state.isSpeak.value == false + ? Image.asset( state.isSpeak.value == false ? 'assets/images/meeting_main_sqfy.png' : state.isOpenMicrophone.value == true ? 'assets/images/meeting_main_microphone_default.png' : 'assets/images/meeting_main_sqfy.png', - width: 22.w, - height: 22.h, - ), + width: 20.w, + height: 20.h, + ) + : state.isOpenMicrophone.value == true + ? Container( + width: 20.w, + height: 20.h, + child: LiquidCustomProgressIndicator( + value: state.microphoneVolume.value, + valueColor: const AlwaysStoppedAnimation(ColorUtil.Color_2_177_136), + backgroundColor: ColorUtil.Color_255_255_255, + direction: Axis.vertical, + shapePath: ViewSvgPath.getMicrpphonePath() + ), + ) + : Image.asset( + 'assets/images/meeting_main_sqfy.png', + width: 20.w, + height: 20.h, + ) , SizedBox(height: 4.h), Text( state.isSpeak.value == false ? '申请发言' : state.isOpenMicrophone.value == true ? "手动静音" : "解除静音", @@ -288,13 +310,13 @@ class MeetingMainPage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( - 'assets/images/meeting_main_sp.png', + state.isSpeak.value == true ? state.isOpenCamera.value == true ? 'assets/images/meeting_main_camera_open.png' : 'assets/images/meeting_main_camera_default.png' : 'assets/images/meeting_main_sp.png', width: 22.w, height: 22.h, ), SizedBox(height: 4.h), Text( - '开启视频', + state.isOpenCamera.value == true ? "关闭视频" : "开启视频", style: TextStyle( fontSize: 12.sp, color: ColorUtil.Color_202_202_202), @@ -302,7 +324,13 @@ class MeetingMainPage extends StatelessWidget { ], ), onTap: (){ - ToastUtils.getErrFluttertoast(context: context, msg: '开启视频...'); + if(state.isSpeak.value == true){ + if(state.isOpenCamera.value == true){ + logic.setCameraOpen(false); + }else{ + logic.setCameraOpen(true); + } + } }, ), diff --git a/wgshare/lib/pages/metting/video/meeting_main_video_view.dart b/wgshare/lib/pages/metting/video/meeting_main_video_view.dart index c41f545..a9cba26 100644 --- a/wgshare/lib/pages/metting/video/meeting_main_video_view.dart +++ b/wgshare/lib/pages/metting/video/meeting_main_video_view.dart @@ -1,13 +1,19 @@ +import 'package:agora_rtc_engine/agora_rtc_engine.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:wgshare/common/store/user_store.dart'; import '../../../utils/color_util.dart'; import 'meeting_main_video_logic.dart'; import 'meeting_main_video_state.dart'; class MeetingMainVideoComponent extends StatelessWidget { - MeetingMainVideoComponent({Key? key}) : super(key: key); + MeetingMainVideoComponent({super.key, required this.rtcEngine, required this.channelId, required this.isOpenCamera}); + + final RtcEngine rtcEngine; + final String channelId; + final bool isOpenCamera; final MeetingMainVideoLogic logic = Get.put(MeetingMainVideoLogic()); final MeetingMainVideoState state = Get.find().state; @@ -85,23 +91,28 @@ class MeetingMainVideoComponent extends StatelessWidget { ), ), ), + /// 右上角小窗 Positioned( top: 58, - right: 13, + right: 12, child: Stack( children: [ - Container( - width: 120.w, - height: 150.h, - padding: const EdgeInsets.only(left: 12, right: 12), - decoration: BoxDecoration( - image: DecorationImage( - fit: BoxFit.fill, - image: NetworkImage( - "https://tse1-mm.cn.bing.net/th/id/OIP-C.hdhK40Dw3yN_2mjNQNqFCgAAAA?w=186&h=186&c=7&r=0&o=5&pid=1.7", - ), + Visibility( + visible: isOpenCamera, + child: SizedBox( + width: 120.w, + height: 150.h, + child: Center( + child: isOpenCamera + ? AgoraVideoView( + controller: VideoViewController( + rtcEngine: rtcEngine, + canvas: VideoCanvas(uid: int.parse(UserStore.to.userInfoEntity.value!.uid)), ), ) + : const CircularProgressIndicator(), + ), + ), ), Positioned( left: 4, diff --git a/wgshare/lib/pages/metting/voice/meeting_main_voice_view.dart b/wgshare/lib/pages/metting/voice/meeting_main_voice_view.dart index 436281c..3d817c8 100644 --- a/wgshare/lib/pages/metting/voice/meeting_main_voice_view.dart +++ b/wgshare/lib/pages/metting/voice/meeting_main_voice_view.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:liquid_progress_indicator_v2/liquid_progress_indicator.dart'; import '../../../common/models/meeting_room_user.dart'; import '../../../utils/color_util.dart'; import '../../../utils/cus_behavior.dart'; +import '../../../view/view_svg_path.dart'; import 'meeting_main_voice_logic.dart'; import 'meeting_main_voice_state.dart'; @@ -54,13 +56,21 @@ class MeetingMainVoiceComponent extends StatelessWidget { ), ), SizedBox(height: 6.h), - Row( + users[index].enableMicr == true + ? Row( mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Image.asset( - users[index].enableMicr == true ? 'assets/images/meeting_main_speak1.png' : 'assets/images/meeting_main_microphone_open.png', - width: 22.w, - height: 22.h, + Container( + width: 20.w, + height: 20.h, + child: LiquidCustomProgressIndicator( + value: users[index].volume ?? 0.0, + valueColor: const AlwaysStoppedAnimation(ColorUtil.Color_2_177_136), + backgroundColor: ColorUtil.Color_255_255_255, + direction: Axis.vertical, + shapePath: ViewSvgPath.getMicrpphonePath() + ), ), Text( users[index].userName, @@ -69,6 +79,23 @@ class MeetingMainVoiceComponent extends StatelessWidget { color: ColorUtil.Color_255_255_255), ) ], + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + 'assets/images/meeting_main_microphone_open.png', + width: 20.w, + height: 20.h, + ), + Text( + users[index].userName, + style: TextStyle( + fontSize: 12.sp, + color: ColorUtil.Color_255_255_255), + ) + ], ), ], ); diff --git a/wgshare/lib/utils/count_microphone_volume.dart b/wgshare/lib/utils/count_microphone_volume.dart new file mode 100644 index 0000000..2a5b928 --- /dev/null +++ b/wgshare/lib/utils/count_microphone_volume.dart @@ -0,0 +1,17 @@ + +/// 计算声网SDK返回的用户音量 +class CountMicrophoneVolume { + + static double getVolume(int volume){ + var resultVolume = 0.0; + if(volume == 0){ + resultVolume = 0; + }else if(volume > 0 && volume < 200){ + resultVolume = volume / 200; + }else{ + resultVolume = 1; + } + return resultVolume; + } + +} \ No newline at end of file diff --git a/wgshare/lib/view/view_svg_path.dart b/wgshare/lib/view/view_svg_path.dart new file mode 100644 index 0000000..c2166fd --- /dev/null +++ b/wgshare/lib/view/view_svg_path.dart @@ -0,0 +1,40 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +/// 各类SVG路径 +class ViewSvgPath { + + /// 麦克风 + static Path getMicrpphonePath(){ + Path mPath = Path(); + mPath.moveTo(10.2188, 11.875); + mPath.cubicTo(11.9375, 11.875, 13.3438, 10.4688, 13.3438, 8.75); + mPath.lineTo(13.3438, 5); + mPath.cubicTo(13.3438, 3.28125, 11.9375, 1.875, 10.2188, 1.875); + mPath.cubicTo(8.5, 1.875, 7.09375, 3.28125, 7.09375, 5); + mPath.lineTo(7.09375, 8.75); + mPath.cubicTo(7.09375, 10.4688, 8.5, 11.875, 10.2188, 11.875); + + mPath.moveTo(15.777, 9.61683); + mPath.cubicTo(15.8297, 9.27504, 15.5973, 8.95668, 15.2555, 8.90394); + mPath.cubicTo(14.9137, 8.85316, 14.5954, 9.08558, 14.5426, 9.42543); + mPath.cubicTo(14.2145, 11.5348, 12.3571, 13.1246, 10.2184, 13.1246); + mPath.cubicTo(8.07973, 13.1246, 6.22035, 11.5329, 5.89418, 9.42348); + mPath.cubicTo(5.84144, 9.08168, 5.52113, 8.84926, 5.18129, 8.90199); + mPath.cubicTo(4.83949, 8.95473, 4.60707, 9.27308, 4.6598, 9.61488); + mPath.cubicTo(5.05433, 12.1637, 7.08168, 14.0641, 9.5934, 14.3375); + mPath.lineTo(9.5934, 16.2496); + mPath.lineTo(7.7184, 16.2496); + mPath.cubicTo(7.37269, 16.2496, 7.0934, 16.5289, 7.0934, 16.8746); + mPath.cubicTo(7.0934, 17.2204, 7.37269, 17.4996, 7.7184, 17.4996); + mPath.lineTo(12.7184, 17.4996); + mPath.cubicTo(13.0641, 17.4996, 13.3434, 17.2204, 13.3434, 16.8746); + mPath.cubicTo(13.3434, 16.5289, 13.0641, 16.2496, 12.7184, 16.2496); + mPath.lineTo(10.8434, 16.2496); + mPath.lineTo(10.8434, 14.3375); + mPath.cubicTo(13.3532, 14.0641, 15.3825, 12.1657, 15.777, 9.61683); + mPath.close(); + return mPath; + } +} \ No newline at end of file diff --git a/wgshare/pubspec.lock b/wgshare/pubspec.lock index ee40c95..dce5d0c 100644 --- a/wgshare/pubspec.lock +++ b/wgshare/pubspec.lock @@ -594,6 +594,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.0.0" + liquid_progress_indicator_v2: + dependency: "direct main" + description: + name: liquid_progress_indicator_v2 + sha256: "6bb2c675bab4936864a63ccd503be417e407974e11c62711917a4006bb9288b8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.0" logger: dependency: "direct main" description: diff --git a/wgshare/pubspec.yaml b/wgshare/pubspec.yaml index 0a4152c..6145843 100644 --- a/wgshare/pubspec.yaml +++ b/wgshare/pubspec.yaml @@ -76,6 +76,9 @@ dependencies: # .net socket通信插件 signalr_core: ^1.1.1 + # 水波效果的进度器 + liquid_progress_indicator_v2: ^0.5.0 + dev_dependencies: flutter_test: sdk: flutter