当前位置: 首页 > news >正文

【flutter】flutter网易云信令 + im + 声网rtm从0实现通话视频文字聊天的踩坑

接了一个国外的项目,项目采用网易云im + 网易云信令+声网rtm遇到的一些问题

这个项目只对接口,给的工期是两周,延了工期,问题还是比较多的

  1. 需要全局监听rtm信息,收到监听内容,引起视频通话
  2. 网易云给的文档太烂,所有的类型推策只能文档一点点推
  3. 声网的rtm配置网易云的信令,坑太多,比如声网接收的字段是number,网易云给的字段是string等一系列报错问题
  4. im普通的对接,体验太差,采用倒叙分页解决此问题
  5. im的上传图片上传过程无显示,需要做上传图片的百分比显示

解决 im普通的对接,体验太差,采用倒叙分页解决此问题和图片上传百分比显示

//imNIMMessageListOption option = NIMMessageListOption(conversationId: widget.conversationId ?? '',direction: NIMQueryDirection.desc, //倒叙limit: limit,anchorMessage: _anchorMessage,// endTime: endTime,);

//图片
// 采用模拟发送数据,根据im提供的NimCore.instance.messageService.sendMessage ,得到是否成功,来显示状态Future<void> _pickImage() async {try {_logI('Picking image from gallery');final XFile? pickedFile = await _imagePicker.pickImage(source: ImageSource.gallery,imageQuality: 80,);if (pickedFile != null) {// u83b7u53d6u6587u4ef6u4fe1u606ffinal File imageFile = File(pickedFile.path);final String fileName = pickedFile.name;// u83b7u53d6u56feu7247u5c3au5bf8final decodedImage =await decodeImageFromList(imageFile.readAsBytesSync());final int width = decodedImage.width;final int height = decodedImage.height;// u521bu5efau4e34u65f6u6d88u606ffinal tempMessage = UnifiedMessage.createTempImage(pickedFile.path);setState(() {_messages.insert(0, tempMessage);});_scrollToBottom();// u5f00u59cbu4e0au4f20_sendImageMessage(tempMessage, pickedFile.path, fileName, width, height);}} catch (e) {_logI('Error picking image: $e');ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to pick image: $e')),);}}

全局监听rtm信息回调

建立 call_manager.dart,单页面引入

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:nim_core_v2/nim_core.dart';
import 'package:yunxin_alog/yunxin_alog.dart';
import '../screens/video_call_screen.dart';
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import '../utils/event_bus.dart';
import '../utils/toast_util.dart';var listener;class CallManager {static final CallManager _instance = CallManager._internal();factory CallManager() => _instance;CallManager._internal();RtcEngine? _engine; // 添加 engine 变量final AudioPlayer _audioPlayer = AudioPlayer();Timer? _ringtoneTimer;BuildContext? _lastContext;bool _isShowingCallDialog = false;void initialize(BuildContext context) {_lastContext = context;_setupSignallingListeners();}void updateContext(BuildContext context) {_lastContext = context;}// 添加设置 engine 的方法void setEngine(RtcEngine engine) {_engine = engine;}// 修改现有的代码void _handleCallHangup(event) {print('关闭信令频道房间成功${event.channelInfo!.channelId} 目前一样');NimCore.instance.signallingService.closeRoom(event.channelInfo!.channelId!, true, null).then((result) async {_isShowingCallDialog = false;EventBusUtil().eventBus.fire(VideoCallEvent(VideoCallEvent.LEAVE_CHANNEL));if (result.isSuccess) {if (_engine != null) {await _engine!.leaveChannel();await _engine!.release();}// Success handling} else {// Error handling}});}@overridevoid dispose() {print('dispose');if (listener != null) {listener.cancel();listener = null;}}// 添加一个方法来检查并清理监听器void cleanup() {print('cleanup');if (listener != null) {listener.cancel();listener = null;}_ringtoneTimer?.cancel();_ringtoneTimer = null;_audioPlayer.stop();_isShowingCallDialog = false;}// NIMSignallingEventTypeUnknown	0	未知
// NIMSignallingEventTypeClose	1	关闭信令频道房间
// NIMSignallingEventTypeJoin	2	加入信令频道房间
// NIMSignallingEventTypeInvite	3	邀请加入信令频道房间
// NIMSignallingEventTypeCancelInvite	4	取消邀请加入信令频道房间
// NIMSignallingEventTypeReject	5	拒绝入房的邀请
// NIMSignallingEventTypeAccept	6	接受入房的邀请
// NIMSignallingEventTypeLeave	7	离开信令频道房间
// NIMSignallingEventTypeControl	8	自定义控制命令void _setupSignallingListeners() {// Listen for online events (when the app is active)listener = NimCore.instance.signallingService.onOnlineEvent.listen((NIMSignallingEvent event) {print("事件监听开始${event.toJson()}");_handleSignallingEvent(event);});// Listen for offline events (when the app is in background)NimCore.instance.signallingService.onOfflineEvent.listen((event) {// Handle offline eventsprint('Offline event: $event');});// Listen for multi-client eventsNimCore.instance.signallingService.onMultiClientEvent.listen((event) {// Handle multi-client eventsprint('Multi-client event: $event');});Alog.i(tag: 'CallManager', content: 'Signalling listeners setup complete');}void _handleSignallingEvent(NIMSignallingEvent event) {if (event.channelInfo != null &&event.eventType ==NIMSignallingEventType.NIMSignallingEventTypeInvite) {// 3_showIncomingCallDialog(event.channelInfo, event.requestId ?? '');}if (event.channelInfo != null &&event.eventType == NIMSignallingEventType.NIMSignallingEventTypeClose) {//1_handleCallHangup(event);}if (event.channelInfo != null &&event.eventType == NIMSignallingEventType.NIMSignallingEventTypeJoin) {EventBusUtil().eventBus.fire(VideoCallEvent(VideoCallEvent.USER_JOINED));}if (event.channelInfo != null &&event.eventType ==NIMSignallingEventType.NIMSignallingEventTypeReject) {ToastUtil.showDanger('user reject');cleanup();}}Future<void> _playRingtone() async {try {await _audioPlayer.play(AssetSource('sounds/incoming_call.mp3'));// Loop the ringtone_ringtoneTimer =Timer.periodic(const Duration(seconds: 3), (timer) async {await _audioPlayer.play(AssetSource('sounds/incoming_call.mp3'));});} catch (e) {Alog.e(tag: 'CallManager', content: 'Error playing ringtone: $e');}}void _stopRingtone() {_ringtoneTimer?.cancel();_ringtoneTimer = null;_audioPlayer.stop();}void _showIncomingCallDialog(NIMSignallingChannelInfo? channelInfo, String requestId) {if (channelInfo == null || _lastContext == null || _isShowingCallDialog) {return;}_isShowingCallDialog = true;_playRingtone();String? channelId = channelInfo.channelId;String? callerName = channelInfo.creatorAccountId;showDialog(context: _lastContext!,barrierDismissible: false,builder: (context) {return AlertDialog(backgroundColor: Colors.black87,title: const Text('Incoming Video Call',style: TextStyle(color: Colors.white),textAlign: TextAlign.center,),content: Column(mainAxisSize: MainAxisSize.min,children: [const CircleAvatar(radius: 40,backgroundColor: Colors.purple,child: Icon(Icons.person, size: 50, color: Colors.white),),const SizedBox(height: 16),Text(callerName ?? 'Unknown Caller',style: const TextStyle(color: Colors.white,fontSize: 18,fontWeight: FontWeight.bold,),),const SizedBox(height: 8),const Text('is calling you...',style: TextStyle(color: Colors.white70),),],),actions: [Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly,children: [// Decline buttonElevatedButton(onPressed: () {_stopRingtone();_isShowingCallDialog = false;Navigator.of(context).pop();// Reject the call - using hangup instead of reject since reject isn't available_rejectCall(channelInfo, requestId, context);},style: ElevatedButton.styleFrom(backgroundColor: Colors.red,shape: const CircleBorder(),padding: const EdgeInsets.all(16),),child: const Icon(Icons.call_end, color: Colors.white),),// Accept buttonElevatedButton(onPressed: () {_stopRingtone();_isShowingCallDialog = false;Navigator.of(context).pop();// Accept the call_acceptCall(channelInfo, requestId, context);},style: ElevatedButton.styleFrom(backgroundColor: Colors.green,shape: const CircleBorder(),padding: const EdgeInsets.all(16),),child: const Icon(Icons.call, color: Colors.white),),],),],);},).then((_) {_stopRingtone();_isShowingCallDialog = false;});}Future<void> _acceptCall(NIMSignallingChannelInfo channelInfo,String requestId, BuildContext context) async {String? channelId = channelInfo.channelId;String? creatorAccountId = channelInfo.creatorAccountId;if (channelId == null || creatorAccountId == null) {return;}final params = NIMSignallingCallSetupParams(channelId: channelId,callerAccountId: creatorAccountId,requestId: requestId,);try {final result = await NimCore.instance.signallingService.callSetup(params);if (result.isSuccess) {// Navigate to the video call screen with incomingCall flag// 检查 context 是否还有效print("1121212${result.toMap()}");final setUpChanelId = result.data?.roomInfo?.channelInfo?.channelId;final setUpCalleeAccountId =result.data?.roomInfo?.channelInfo?.creatorAccountId;// final setUpRemoteUid = result.data?.roomInfo?.members?.first.uid ?? 0;// final setUpRemoteUid = result.data?.roomInfo?.members?.first.uid;final setUpRemoteUid =result.data?.roomInfo?.channelInfo?.creatorAccountId;if (!context.mounted) {Alog.e(tag: 'CallManager', content: 'Context is not mounted anymore');// 如果原始 context 无效,尝试使用 _lastContextif (_lastContext != null && _lastContext!.mounted) {context = _lastContext!;} else {Alog.e(tag: 'CallManager',content: 'No valid context available for navigation');return;}}Navigator.push(context,MaterialPageRoute(builder: (context) => VideoCallScreen(calleeAccountId: channelInfo.creatorAccountId,isIncomingCall: true,setUpChanelId: setUpChanelId,setUpCalleeAccountId: setUpCalleeAccountId,setUpRemoteUid: int.tryParse(setUpRemoteUid!) ?? 0),),);} else {Alog.e(tag: 'CallManager',content: 'Failed to setup video call: ${result.code}');ToastUtil.showDanger('Failed to setup video call');}} catch (e) {ToastUtil.showDanger('Failed to setup video call');Alog.e(tag: 'CallManager', content: 'Error accepting call: $e');}}Future<void> _rejectCall(NIMSignallingChannelInfo channelInfo,String requestId, BuildContext context) async {try {String? channelId = channelInfo.channelId;String? creatorAccountId = channelInfo.creatorAccountId;if (channelId == null || creatorAccountId == null) {return;}final params = NIMSignallingRejectInviteParams(channelId: channelId,inviterAccountId: creatorAccountId,requestId: requestId,);// Close the room since direct reject isn't availablefinal result =await NimCore.instance.signallingService.rejectInvite(params);if (result.isSuccess) {Alog.i(tag: 'CallManager', content: 'Call rejected successfully');} else {Alog.e(tag: 'CallManager',content: 'Failed to reject call: ${result.code}');}} catch (e) {Alog.e(tag: 'CallManager', content: 'Error rejecting call: $e');}}
}

待续

http://www.dtcms.com/a/274183.html

相关文章:

  • 如何蒸馏 设计 中文nl2sql 数据集,进行grpo强化学习 训练qwen3-8b,可以提升大模型nl2sql的能力,支持300行sql生成
  • Redis 分片集群
  • mysql报错服务没有报告任何错误
  • AIGC(生成式AI)试用 33 -- 用AI学AI名词
  • 彻底解决JavaFx在Linux中文无法正常显示的问题(究其根本原因)
  • 剑指offer——数组:数组中重复的数字
  • 博客系统开发全流程解析(前端+后端+数据库)与 AI 协作初体验
  • [大模型问数]实现大模型调用MYSQL(03)【MCP笔记】
  • Prometheus+Grafana部署及企业微信邮件/群消息告警通知配置
  • Shader面试题100道之(81-100)
  • 中学物理模拟实验Python程序集成打包
  • 牛客周赛 Round 99
  • Spring 声明式事务:从原理到实现的完整解析
  • 破解多宠管理难题,端侧AI重新定义宠物智能硬件
  • 《Spring 中上下文传递的那些事儿》Part 10:上下文敏感信息脱敏与审计日志记录
  • ESP32_启动日志分析
  • 【TCP/IP】17. 移动 IP
  • Linux权限的概念
  • 硬件加速(FPGA)
  • 函数指针指针函数 智能指针
  • 通过ETL工具,高效完成达梦数据库数据同步至数仓Oracle的具体实现
  • MDSE模型驱动的软件工程和敏捷开发相结合的案例
  • Django 视图(View)
  • 指令重排序带来的多线程问题与volatile解决方案
  • Linux设备树(dts/dtsi/dtb、设备树概念,设备树解析,驱动匹配)
  • P1204 [USACO1.2] 挤牛奶Milking Cows
  • 如何设置直播间的观看门槛,让直播间安全有效地运行?
  • 云原生周刊:镜像兼容性
  • 假日流量红利:如何用ASO策略抢占季节性下载高峰?
  • 不同质押周期对代币价格稳定性的具体影响及数据支撑