接了一个国外的项目,项目采用网易云im + 网易云信令+声网rtm遇到的一些问题
这个项目只对接口,给的工期是两周,延了工期,问题还是比较多的
- 需要全局监听rtm信息,收到监听内容,引起视频通话
- 网易云给的文档太烂,所有的类型推策只能文档一点点推
- 声网的rtm配置网易云的信令,坑太多,比如声网接收的字段是number,网易云给的字段是string等一系列报错问题
- im普通的对接,体验太差,采用倒叙分页解决此问题
- im的上传图片上传过程无显示,需要做上传图片的百分比显示
解决 im普通的对接,体验太差,采用倒叙分页解决此问题和图片上传百分比显示
js
//im
NIMMessageListOption option = NIMMessageListOption(
conversationId: widget.conversationId ?? '',
direction: NIMQueryDirection.desc, //倒叙
limit: limit,
anchorMessage: _anchorMessage,
// endTime: endTime,
);
js
//图片
// 采用模拟发送数据,根据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) {
// u83b7u53d6u6587u4ef6u4fe1u606f
final File imageFile = File(pickedFile.path);
final String fileName = pickedFile.name;
// u83b7u53d6u56feu7247u5c3au5bf8
final decodedImage =
await decodeImageFromList(imageFile.readAsBytesSync());
final int width = decodedImage.width;
final int height = decodedImage.height;
// u521bu5efau4e34u65f6u6d88u606f
final 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,单页面引入
js
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
}
});
}
@override
void 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 events
print('Offline event: $event');
});
// Listen for multi-client events
NimCore.instance.signallingService.onMultiClientEvent.listen((event) {
// Handle multi-client events
print('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 button
ElevatedButton(
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 button
ElevatedButton(
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 无效,尝试使用 _lastContext
if (_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 available
final 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');
}
}
}
待续