Browse Source

feat(chat): 添加视频通话功能

- 添加视频通话页面实现视频通话界面和功能
- 在聊天页面中启用视频通话按钮并实现通话发起逻辑
- 添加通话管理器处理视频通话状态和时长
- 更新资源文件添加视频通话相关图标
- 实现通话消息解析显示视频通话状态
- 添加通话时长计时和用户信息显示功能
master
Jolie 3 months ago
parent
commit
4ad54dc3b3
7 changed files with 500 additions and 19 deletions
  1. BIN
      assets/images/video_call.png
  2. 24
      lib/controller/message/conversation_controller.dart
  3. 4
      lib/generated/assets.dart
  4. 24
      lib/im/im_manager.dart
  5. 26
      lib/pages/message/chat_page.dart
  6. 427
      lib/pages/message/video_call_page.dart
  7. 14
      lib/widget/message/chat_input_bar.dart

BIN
assets/images/video_call.png

Before After
Width: 90  |  Height: 68  |  Size: 910 B

24
lib/controller/message/conversation_controller.dart

@ -1,3 +1,4 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
import '../../im/im_manager.dart';
@ -389,7 +390,28 @@ class ConversationController extends GetxController {
if(message.body.type == MessageType.TXT){
final body = message.body as EMTextMessageBody;
return body.content;
final content = body.content;
// CALL消息
if (content != null && content.startsWith('[CALL:]')) {
try {
final jsonStr = content.substring(7); // '[CALL:]'
final callInfo = jsonDecode(jsonStr) as Map<String, dynamic>;
final callType = callInfo['callType'] as String?;
if (callType == 'video') {
return '[视频通话]';
} else if (callType == 'voice') {
return '[语音通话]';
}
} catch (e) {
//
if (Get.isLogEnable) {
Get.log('⚠️ [ConversationController] 解析CALL消息失败: $e');
}
}
}
return content ?? '';
}else if(message.body.type == MessageType.IMAGE){
return '[图片]';
}else if(message.body.type == MessageType.VOICE){

4
lib/generated/assets.dart

@ -124,6 +124,7 @@ class Assets {
static const String imagesHiIcon = 'assets/images/hi_icon.png';
static const String imagesHomeNol = 'assets/images/home_nol.png';
static const String imagesHomePre = 'assets/images/home_pre.png';
static const String imagesImCoinIcon = 'assets/images/im_coin_icon.png';
static const String imagesInformationBg = 'assets/images/information_bg.png';
static const String imagesLastMsgIcon = 'assets/images/last_msg_icon.png';
static const String imagesLimitTime = 'assets/images/limit_time.png';
@ -201,6 +202,7 @@ class Assets {
static const String imagesUserAvatar = 'assets/images/user_avatar.png';
static const String imagesVerifiedIcon = 'assets/images/verified_icon.png';
static const String imagesVideo = 'assets/images/video.png';
static const String imagesVideoCall = 'assets/images/video_call.png';
static const String imagesVip = 'assets/images/vip.png';
static const String imagesVipBanner = 'assets/images/vip_banner.png';
static const String imagesVipBg = 'assets/images/vip_bg.png';
@ -221,5 +223,5 @@ class Assets {
static const String imagesWallet = 'assets/images/wallet.png';
static const String imagesWechatPay = 'assets/images/wechat_pay.png';
static const String imagesWomenIcon = 'assets/images/women_icon.png';
static const String imagesImCoinIcon = 'assets/images/im_coin_icon.png';
}

24
lib/im/im_manager.dart

@ -1,5 +1,6 @@
import 'dart:io';
import 'dart:async';
import 'dart:convert';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -1547,7 +1548,28 @@ class IMManager {
try {
if (message.body.type == MessageType.TXT) {
final body = message.body as EMTextMessageBody;
return body.content ?? '';
final content = body.content;
// CALL消息
if (content != null && content.startsWith('[CALL:]')) {
try {
final jsonStr = content.substring(7); // '[CALL:]'
final callInfo = jsonDecode(jsonStr) as Map<String, dynamic>;
final callType = callInfo['callType'] as String?;
if (callType == 'video') {
return '[视频通话]';
} else if (callType == 'voice') {
return '[语音通话]';
}
} catch (e) {
//
if (Get.isLogEnable) {
Get.log('⚠️ [IMManager] 解析CALL消息失败: $e');
}
}
}
return content ?? '';
} else if (message.body.type == MessageType.IMAGE) {
return '[图片]';
} else if (message.body.type == MessageType.VOICE) {

26
lib/pages/message/chat_page.dart

@ -4,6 +4,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
import '../../controller/message/call_manager.dart';
import '../../controller/message/chat_controller.dart';
import '../../controller/message/voice_player_manager.dart';
// import '../../controller/message/call_manager.dart'; //
@ -14,6 +15,7 @@ import '../../../widget/message/message_item.dart';
import '../../../widget/message/chat_gift_popup.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'chat_settings_page.dart';
import 'video_call_page.dart';
import '../home/user_information_page.dart';
import '../../../widget/live/live_recharge_popup.dart';
@ -298,15 +300,21 @@ class _ChatPageState extends State<ChatPage> {
// chatController: controller,
// );
// },
//
// onVideoCall: () async {
// //
// await CallManager.instance.initiateCall(
// targetUserId: widget.userId,
// callType: CallType.video,
// chatController: controller,
// );
// },
//
onVideoCall: () async {
//
await CallManager.instance.initiateCall(
targetUserId: widget.userId,
callType: CallType.video,
chatController: controller,
);
//
Get.to(() => VideoCallPage(
targetUserId: widget.userId,
userData: widget.userData ?? controller.userData,
isInitiator: true,
));
},
),
],
),

427
lib/pages/message/video_call_page.dart

@ -0,0 +1,427 @@
import 'dart:async';
import 'dart:ui';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dating_touchme_app/generated/assets.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../controller/message/call_manager.dart';
import '../../controller/message/conversation_controller.dart';
import '../../model/home/marriage_data.dart';
///
class VideoCallPage extends StatefulWidget {
final String targetUserId;
final MarriageData? userData;
final bool isInitiator; //
const VideoCallPage({
super.key,
required this.targetUserId,
this.userData,
this.isInitiator = true,
});
@override
State<VideoCallPage> createState() => _VideoCallPageState();
}
class _VideoCallPageState extends State<VideoCallPage> {
final CallManager _callManager = CallManager.instance;
bool _isMicMuted = false;
bool _isSpeakerOn = false;
Duration _callDuration = Duration.zero;
Timer? _durationTimer;
String? _targetUserName;
String? _targetAvatarUrl;
//
bool _isCallConnected = false;
@override
void initState() {
super.initState();
_initializeCall();
_loadUserInfo();
_initCallStatus();
_startDurationTimer();
// UI样式
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
}
///
void _initCallStatus() {
//
final callSession = _callManager.currentCall.value;
if (callSession != null && _callManager.callDurationSeconds.value > 0) {
//
_isCallConnected = true;
_callDuration = Duration(seconds: _callManager.callDurationSeconds.value);
} else {
//
_isCallConnected = false;
}
}
///
Future<void> _loadUserInfo() async {
// 使 userData
if (widget.userData != null) {
setState(() {
_targetUserName = widget.userData!.nickName.isNotEmpty
? widget.userData!.nickName
: widget.targetUserId;
_targetAvatarUrl = widget.userData!.profilePhoto;
});
return;
}
// userData ConversationController
try {
if (Get.isRegistered<ConversationController>()) {
final conversationController = Get.find<ConversationController>();
//
final cachedUserInfo = conversationController.getCachedUserInfo(widget.targetUserId);
if (cachedUserInfo != null && (cachedUserInfo.nickName != null || cachedUserInfo.avatarUrl != null)) {
setState(() {
_targetUserName = cachedUserInfo.nickName ?? widget.targetUserId;
_targetAvatarUrl = cachedUserInfo.avatarUrl;
});
return;
}
// IM
final userInfo = await conversationController.loadContact(widget.targetUserId);
if (userInfo != null && (userInfo.nickName != null || userInfo.avatarUrl != null)) {
setState(() {
_targetUserName = userInfo.nickName ?? widget.targetUserId;
_targetAvatarUrl = userInfo.avatarUrl;
});
return;
}
}
} catch (e) {
print('⚠️ [VideoCallPage] 加载用户信息失败: $e');
}
// 使
setState(() {
_targetUserName = widget.targetUserId;
_targetAvatarUrl = null;
});
}
@override
void dispose() {
_durationTimer?.cancel();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values);
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
super.dispose();
}
///
Future<void> _initializeCall() async {
try {
// TODO: RTC Engine并加入频道
// await _rtcManager.initialize(appId: 'your_app_id');
// await _rtcManager.joinChannel(token: 'token', channelId: 'channel_id', uid: uid);
} catch (e) {
print('初始化通话失败: $e');
}
}
///
void _startDurationTimer() {
// CallManager
_callManager.currentCall.listen((callSession) {
if (mounted) {
final wasConnected = _isCallConnected;
//
if (callSession != null && _callManager.callDurationSeconds.value > 0) {
_isCallConnected = true;
if (!wasConnected) {
//
_callDuration = Duration(seconds: _callManager.callDurationSeconds.value);
}
} else if (callSession == null) {
_isCallConnected = false;
}
setState(() {});
}
});
//
_callManager.callDurationSeconds.listen((seconds) {
if (mounted && _isCallConnected) {
setState(() {
_callDuration = Duration(seconds: seconds);
});
} else if (mounted && !_isCallConnected && seconds > 0) {
//
setState(() {
_isCallConnected = true;
_callDuration = Duration(seconds: seconds);
});
}
});
// 使
if (!_isCallConnected) {
_durationTimer = Timer.periodic(Duration(seconds: 1), (timer) {
if (mounted) {
final callSession = _callManager.currentCall.value;
final duration = _callManager.callDurationSeconds.value;
// 0
if (callSession != null && duration > 0) {
_isCallConnected = true;
_callDuration = Duration(seconds: duration);
timer.cancel();
setState(() {});
}
}
});
}
}
///
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final minutes = twoDigits(duration.inMinutes.remainder(60));
final seconds = twoDigits(duration.inSeconds.remainder(60));
return '$minutes:$seconds';
}
///
void _toggleMic() {
setState(() {
_isMicMuted = !_isMicMuted;
});
// TODO: RTC Manager切换麦克风
// _rtcManager.enableAudio(!_isMicMuted);
}
///
void _toggleSpeaker() {
setState(() {
_isSpeakerOn = !_isSpeakerOn;
});
// TODO: RTC Manager切换扬声器
// _rtcManager.setEnableSpeakerphone(_isSpeakerOn);
}
///
void _hangUp() async {
try {
// TODO: RTC频道
// await _rtcManager.leaveChannel();
//
await _callManager.endCall(callDuration: _callDuration.inSeconds);
//
Get.back();
} catch (e) {
print('挂断通话失败: $e');
Get.back();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
// /
_buildBackground(),
//
_buildUserInfo(),
//
_buildCallDuration(),
//
_buildControlButtons(),
],
),
);
}
///
Widget _buildBackground() {
return SizedBox(
width: double.infinity,
height: 1.sh,
child: _targetAvatarUrl != null && _targetAvatarUrl!.isNotEmpty
? ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: CachedNetworkImage(
imageUrl: _targetAvatarUrl!,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
errorWidget: (context, url, error) => _buildDefaultBackground(),
),
)
: _buildDefaultBackground(),
);
}
///
Widget _buildDefaultBackground() {
return Container(
color: Colors.black,
);
}
///
Widget _buildUserInfo() {
return Positioned(
top: MediaQuery.of(context).size.height * 0.15,
left: 0,
right: 0,
child: Column(
children: [
//
ClipOval(
child: _targetAvatarUrl != null && _targetAvatarUrl!.isNotEmpty
? CachedNetworkImage(
imageUrl: _targetAvatarUrl!,
width: 120.w,
height: 120.w,
fit: BoxFit.cover,
errorWidget: (context, url, error) => Image.asset(
Assets.imagesUserAvatar,
width: 120.w,
height: 120.w,
fit: BoxFit.cover,
),
)
: Image.asset(
Assets.imagesUserAvatar,
width: 120.w,
height: 120.w,
fit: BoxFit.cover,
),
),
SizedBox(height: 16.h),
//
Text(
_targetUserName ?? widget.targetUserId,
style: TextStyle(
color: Colors.white,
fontSize: 24.sp,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
///
Widget _buildCallDuration() {
return Positioned(
bottom: MediaQuery.of(context).size.height * 0.25,
left: 0,
right: 0,
child: Center(
child: Text(
_isCallConnected ? _formatDuration(_callDuration) : '正在呼叫中',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w500,
),
),
),
);
}
///
Widget _buildControlButtons() {
return Positioned(
bottom: 40.h,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
//
_buildControlButton(
icon: Icons.volume_up,
label: '免提',
isActive: _isSpeakerOn,
onTap: _toggleSpeaker,
),
//
_buildControlButton(
icon: Icons.mic,
label: '麦克风',
isActive: !_isMicMuted,
onTap: _toggleMic,
),
//
_buildControlButton(
icon: Icons.call_end,
label: '挂断',
isActive: true,
onTap: _hangUp,
isHangUp: true,
),
],
),
);
}
///
Widget _buildControlButton({
required IconData icon,
required String label,
required bool isActive,
required VoidCallback onTap,
bool isHangUp = false,
}) {
return GestureDetector(
onTap: onTap,
child: Column(
children: [
Container(
width: 56.w,
height: 56.w,
decoration: BoxDecoration(
color: isHangUp
? Color(0xFFFF3B30)
: (isActive ? Colors.white.withOpacity(0.3) : Colors.white.withOpacity(0.2)),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: Colors.white,
size: 28.w,
),
),
SizedBox(height: 8.h),
Text(
label,
style: TextStyle(
color: Colors.white,
fontSize: 12.sp,
),
),
],
),
);
}
}

14
lib/widget/message/chat_input_bar.dart

@ -300,13 +300,13 @@ class _ChatInputBarState extends State<ChatInputBar> {
// widget.onVoiceCall?.call();
// }),
//
// Image.asset(
// Assets.imagesSendVideoCall,
// width: 24.w,
// height: 24.w,
// ).onTap(() {
// widget.onVideoCall?.call();
// }),
Image.asset(
Assets.imagesVideoCall,
width: 24.w,
height: 24.w,
).onTap(() {
widget.onVideoCall?.call();
}),
//
Image.asset(Assets.imagesGift, width: 24.w, height: 24.w).onTap(() {
widget.onGiftTap?.call();

Loading…
Cancel
Save