feat(mobile-next): 导入官方 openim-flutter-demo 固定基线 3.8.3-patch.3 并完成双构建
- 上游: github.com/OpenIMSDK/openim-flutter-demo tag 3.8.3-patch.3 commit b3dfdb1e8aaeaaf6f0793e10cadd20d5c184a31f - 并行目录 mobile-next/ 原样导入(含 LICENSE),不覆盖现网 mobile/ - 锁定 Flutter 3.24.5 / Dart 3.5.4 / JDK 17 / Gradle 7.6.3 / AGP 7.3.1 - 实测 Android debug 与 release 构建均成功(见 docs/mobile-next-baseline-import.md) - 本提交可单独回退:git revert 1db1229(重写前) Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../openim_live.dart';
|
||||
|
||||
/// 信令
|
||||
mixin OpenIMLive {
|
||||
final signalingSubject = PublishSubject<CallEvent>();
|
||||
|
||||
void invitationCancelled(SignalingInfo info) {
|
||||
signalingSubject.add(CallEvent(CallState.beCanceled, info));
|
||||
}
|
||||
|
||||
void inviteeAccepted(SignalingInfo info) {
|
||||
signalingSubject.add(CallEvent(CallState.beAccepted, info));
|
||||
}
|
||||
|
||||
void inviteeRejected(SignalingInfo info) {
|
||||
signalingSubject.add(CallEvent(CallState.beRejected, info));
|
||||
}
|
||||
|
||||
void receiveNewInvitation(SignalingInfo info) {
|
||||
signalingSubject.add(CallEvent(CallState.beCalled, info));
|
||||
}
|
||||
|
||||
void beHangup(SignalingInfo info) {
|
||||
signalingSubject.add(CallEvent(CallState.beHangup, info));
|
||||
}
|
||||
|
||||
final backgroundSubject = PublishSubject<bool>();
|
||||
|
||||
final insertSignalingMessageSubject = PublishSubject<CallEvent>();
|
||||
|
||||
Function(SignalingMessageEvent)? onSignalingMessage;
|
||||
final roomParticipantDisconnectedSubject = PublishSubject<RoomCallingInfo>();
|
||||
final roomParticipantConnectedSubject = PublishSubject<RoomCallingInfo>();
|
||||
|
||||
bool _isRunningBackground = false;
|
||||
|
||||
CallEvent? _beCalledEvent;
|
||||
|
||||
bool _autoPickup = false;
|
||||
|
||||
final _ring = 'assets/audio/live_ring.wav';
|
||||
final _audioPlayer = AudioPlayer(
|
||||
// Handle audio_session events ourselves for the purpose of this demo.
|
||||
handleInterruptions: false,
|
||||
// androidApplyAudioAttributes: false,
|
||||
// handleAudioSessionActivation: false,
|
||||
);
|
||||
|
||||
bool get isBusy => OpenIMLiveClient().isBusy;
|
||||
|
||||
onCloseLive() {
|
||||
signalingSubject.close();
|
||||
backgroundSubject.close();
|
||||
roomParticipantDisconnectedSubject.close();
|
||||
roomParticipantConnectedSubject.close();
|
||||
_stopSound();
|
||||
}
|
||||
|
||||
onInitLive() async {
|
||||
_signalingListener();
|
||||
_insertSignalingMessageListener();
|
||||
backgroundSubject.listen((background) {
|
||||
_isRunningBackground = background;
|
||||
if (!_isRunningBackground) {
|
||||
if (_beCalledEvent != null) {
|
||||
signalingSubject.add(_beCalledEvent!);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
roomParticipantDisconnectedSubject.listen((info) {
|
||||
if (null == info.participant || info.participant!.length == 1) {
|
||||
OpenIMLiveClient().closeByRoomID(info.invitation!.roomID!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Stream<CallEvent> get _stream => signalingSubject.stream /*.where((event) => LiveClient.dispatchSignaling(event))*/;
|
||||
|
||||
_signalingListener() => _stream.listen(
|
||||
(event) async {
|
||||
_beCalledEvent = null;
|
||||
if (event.state == CallState.beCalled) {
|
||||
_playSound();
|
||||
final mediaType = event.data.invitation!.mediaType;
|
||||
final sessionType = event.data.invitation!.sessionType;
|
||||
final callType = mediaType == 'audio' ? CallType.audio : CallType.video;
|
||||
final callObj = sessionType == ConversationType.single ? CallObj.single : CallObj.group;
|
||||
|
||||
if (Platform.isAndroid && _isRunningBackground) {
|
||||
_beCalledEvent = event;
|
||||
if (await Permissions.checkSystemAlertWindow()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
_beCalledEvent = null;
|
||||
OpenIMLiveClient().start(
|
||||
Get.overlayContext!,
|
||||
callEventSubject: signalingSubject,
|
||||
roomID: event.data.invitation!.roomID!,
|
||||
inviteeUserIDList: event.data.invitation!.inviteeUserIDList!,
|
||||
inviterUserID: event.data.invitation!.inviterUserID!,
|
||||
groupID: event.data.invitation!.groupID,
|
||||
callType: callType,
|
||||
callObj: callObj,
|
||||
initState: CallState.beCalled,
|
||||
onSyncUserInfo: onSyncUserInfo,
|
||||
onSyncGroupInfo: onSyncGroupInfo,
|
||||
onSyncGroupMemberInfo: onSyncGroupMemberInfo,
|
||||
autoPickup: _autoPickup,
|
||||
onTapPickup: () => onTapPickup(
|
||||
event.data..userID = OpenIM.iMManager.userID,
|
||||
),
|
||||
onTapReject: () => onTapReject(
|
||||
event.data..userID = OpenIM.iMManager.userID,
|
||||
),
|
||||
onTapHangup: (duration, isPositive) => onTapHangup(
|
||||
event.data..userID = OpenIM.iMManager.userID,
|
||||
duration,
|
||||
isPositive,
|
||||
),
|
||||
onError: onError,
|
||||
onRoomDisconnected: () => onRoomDisconnected(event.data),
|
||||
);
|
||||
} else if (event.state == CallState.beRejected) {
|
||||
insertSignalingMessageSubject.add(event);
|
||||
_stopSound();
|
||||
} else if (event.state == CallState.beHangup) {
|
||||
_stopSound();
|
||||
} else if (event.state == CallState.beCanceled) {
|
||||
insertSignalingMessageSubject.add(event);
|
||||
_stopSound();
|
||||
} else if (event.state == CallState.beAccepted) {
|
||||
_stopSound();
|
||||
} else if (event.state == CallState.otherReject || event.state == CallState.otherAccepted) {
|
||||
_stopSound();
|
||||
} else if (event.state == CallState.timeout) {
|
||||
insertSignalingMessageSubject.add(event);
|
||||
|
||||
_stopSound();
|
||||
final sessionType = event.data.invitation!.sessionType;
|
||||
|
||||
if (sessionType == 1) {
|
||||
onTimeoutCancelled(event.data);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
_insertSignalingMessageListener() {
|
||||
insertSignalingMessageSubject.listen((value) {
|
||||
_insertMessage(
|
||||
state: value.state,
|
||||
signalingInfo: value.data,
|
||||
duration: value.fields ?? 0,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
call({
|
||||
required CallObj callObj,
|
||||
required CallType callType,
|
||||
CallState callState = CallState.call,
|
||||
String? roomID,
|
||||
String? inviterUserID,
|
||||
required List<String> inviteeUserIDList,
|
||||
String? groupID,
|
||||
SignalingCertificate? credentials,
|
||||
}) async {
|
||||
final mediaType = callType == CallType.audio ? 'audio' : 'video';
|
||||
final sessionType = callObj == CallObj.single ? 1 : 3;
|
||||
inviterUserID ??= OpenIM.iMManager.userID;
|
||||
|
||||
final signal = SignalingInfo(
|
||||
userID: inviterUserID,
|
||||
invitation: InvitationInfo(
|
||||
inviterUserID: inviterUserID,
|
||||
inviteeUserIDList: inviteeUserIDList,
|
||||
roomID: roomID ?? groupID ?? const Uuid().v4(),
|
||||
timeout: 30,
|
||||
mediaType: mediaType,
|
||||
sessionType: sessionType,
|
||||
platformID: IMUtils.getPlatform(),
|
||||
groupID: groupID,
|
||||
),
|
||||
);
|
||||
|
||||
OpenIMLiveClient().start(
|
||||
Get.overlayContext!,
|
||||
callEventSubject: signalingSubject,
|
||||
inviterUserID: inviterUserID,
|
||||
groupID: groupID,
|
||||
inviteeUserIDList: inviteeUserIDList,
|
||||
callObj: callObj,
|
||||
callType: callType,
|
||||
initState: callState,
|
||||
onDialSingle: () => onDialSingle(signal),
|
||||
onJoinGroup: () => Future.value(credentials!),
|
||||
onTapCancel: () => onTapCancel(signal),
|
||||
onTapHangup: (duration, isPositive) => onTapHangup(
|
||||
signal,
|
||||
duration,
|
||||
isPositive,
|
||||
),
|
||||
onSyncUserInfo: onSyncUserInfo,
|
||||
onSyncGroupInfo: onSyncGroupInfo,
|
||||
onSyncGroupMemberInfo: onSyncGroupMemberInfo,
|
||||
onWaitingAccept: () {
|
||||
if (callObj == CallObj.single) _playSound();
|
||||
},
|
||||
onBusyLine: onBusyLine,
|
||||
onStartCalling: () {
|
||||
_stopSound();
|
||||
},
|
||||
onError: onError,
|
||||
onRoomDisconnected: () => onRoomDisconnected(signal),
|
||||
onClose: _stopSound,
|
||||
);
|
||||
}
|
||||
|
||||
onError(error, stack) {
|
||||
Logger.print('onError=====> $error $stack');
|
||||
OpenIMLiveClient().close();
|
||||
_stopSound();
|
||||
if (error is PlatformException) {
|
||||
if (int.parse(error.code) == SDKErrorCode.hasBeenBlocked) {
|
||||
IMViews.showToast(StrRes.callFail);
|
||||
return;
|
||||
}
|
||||
}
|
||||
IMViews.showToast(StrRes.networkError);
|
||||
}
|
||||
|
||||
onRoomDisconnected(SignalingInfo signalingInfo) {}
|
||||
|
||||
Future<SignalingCertificate> onDialSingle(SignalingInfo signaling) async {
|
||||
final data = {'customType': CustomMessageType.callingInvite, 'data': signaling.invitation!.toJson()};
|
||||
final message = await OpenIM.iMManager.messageManager
|
||||
.createCustomMessage(data: jsonEncode(data), extension: '', description: '');
|
||||
OpenIM.iMManager.messageManager.sendMessage(
|
||||
message: message,
|
||||
offlinePushInfo: OfflinePushInfo(),
|
||||
userID: signaling.invitation!.inviteeUserIDList!.first,
|
||||
isOnlineOnly: true);
|
||||
final certificate = await Apis.getTokenForRTC(signaling.invitation!.roomID!, OpenIM.iMManager.userID);
|
||||
|
||||
return certificate;
|
||||
}
|
||||
|
||||
Future<SignalingCertificate> onTapPickup(SignalingInfo signaling) async {
|
||||
_beCalledEvent = null; // ios bug
|
||||
_autoPickup = false;
|
||||
_stopSound();
|
||||
final data = {'customType': CustomMessageType.callingAccept, 'data': signaling.invitation!.toJson()};
|
||||
final message = await OpenIM.iMManager.messageManager
|
||||
.createCustomMessage(data: jsonEncode(data), extension: '', description: '');
|
||||
OpenIM.iMManager.messageManager.sendMessage(
|
||||
message: message,
|
||||
offlinePushInfo: OfflinePushInfo(),
|
||||
userID: signaling.invitation!.inviterUserID,
|
||||
isOnlineOnly: true);
|
||||
final certificate = await Apis.getTokenForRTC(signaling.invitation!.roomID!, OpenIM.iMManager.userID);
|
||||
|
||||
return certificate;
|
||||
}
|
||||
|
||||
onTapReject(SignalingInfo signaling) async {
|
||||
_stopSound();
|
||||
insertSignalingMessageSubject.add(CallEvent(CallState.reject, signaling));
|
||||
|
||||
final data = {'customType': CustomMessageType.callingReject, 'data': signaling.invitation!.toJson()};
|
||||
final message = await OpenIM.iMManager.messageManager
|
||||
.createCustomMessage(data: jsonEncode(data), extension: '', description: '');
|
||||
final recvUserID = signaling.invitation!.inviterUserID == OpenIM.iMManager.userID
|
||||
? signaling.invitation!.inviteeUserIDList!.first
|
||||
: signaling.invitation!.inviterUserID;
|
||||
return OpenIM.iMManager.messageManager
|
||||
.sendMessage(message: message, offlinePushInfo: OfflinePushInfo(), userID: recvUserID, isOnlineOnly: true);
|
||||
}
|
||||
|
||||
onTapCancel(SignalingInfo signaling) async {
|
||||
_stopSound();
|
||||
insertSignalingMessageSubject.add(CallEvent(CallState.cancel, signaling));
|
||||
|
||||
final data = {'customType': CustomMessageType.callingCancel, 'data': signaling.invitation!.toJson()};
|
||||
final message = await OpenIM.iMManager.messageManager
|
||||
.createCustomMessage(data: jsonEncode(data), extension: '', description: '');
|
||||
final recvUserID = signaling.invitation!.inviterUserID == OpenIM.iMManager.userID
|
||||
? signaling.invitation!.inviteeUserIDList!.first
|
||||
: signaling.invitation!.inviterUserID;
|
||||
OpenIM.iMManager.messageManager
|
||||
.sendMessage(message: message, offlinePushInfo: OfflinePushInfo(), userID: recvUserID, isOnlineOnly: true);
|
||||
return true;
|
||||
}
|
||||
|
||||
onTimeoutCancelled(SignalingInfo signaling) async {
|
||||
final data = {'customType': CustomMessageType.callingCancel, 'data': signaling.invitation!.toJson()};
|
||||
final message = await OpenIM.iMManager.messageManager
|
||||
.createCustomMessage(data: jsonEncode(data), extension: '', description: '');
|
||||
|
||||
OpenIM.iMManager.messageManager.sendMessage(
|
||||
message: message,
|
||||
offlinePushInfo: OfflinePushInfo(),
|
||||
userID: signaling.invitation!.inviterUserID,
|
||||
isOnlineOnly: true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
onTapHangup(SignalingInfo signaling, int duration, bool isPositive) async {
|
||||
if (isPositive) {
|
||||
final data = {'customType': CustomMessageType.callingHungup, 'data': signaling.invitation!.toJson()};
|
||||
final message = await OpenIM.iMManager.messageManager
|
||||
.createCustomMessage(data: jsonEncode(data), extension: '', description: '');
|
||||
final recvUserID = signaling.invitation!.inviterUserID == OpenIM.iMManager.userID
|
||||
? signaling.invitation!.inviteeUserIDList!.first
|
||||
: signaling.invitation!.inviterUserID;
|
||||
OpenIM.iMManager.messageManager
|
||||
.sendMessage(message: message, offlinePushInfo: OfflinePushInfo(), userID: recvUserID, isOnlineOnly: true);
|
||||
}
|
||||
_stopSound();
|
||||
|
||||
insertSignalingMessageSubject.add(CallEvent(
|
||||
CallState.hangup,
|
||||
signaling,
|
||||
fields: duration,
|
||||
));
|
||||
}
|
||||
|
||||
onBusyLine() {
|
||||
_stopSound();
|
||||
IMViews.showToast(StrRes.busyVideoCallHint);
|
||||
}
|
||||
|
||||
onJoin() {}
|
||||
|
||||
Future<UserInfo?> onSyncUserInfo(userID) async {
|
||||
var list = await OpenIM.iMManager.userManager.getUsersInfo(
|
||||
userIDList: [userID],
|
||||
);
|
||||
|
||||
return list.firstOrNull?.simpleUserInfo;
|
||||
}
|
||||
|
||||
Future<GroupInfo?> onSyncGroupInfo(groupID) async {
|
||||
var list = await OpenIM.iMManager.groupManager.getGroupsInfo(
|
||||
groupIDList: [groupID],
|
||||
);
|
||||
return list.firstOrNull;
|
||||
}
|
||||
|
||||
Future<List<GroupMembersInfo>> onSyncGroupMemberInfo(groupID, userIDList) async {
|
||||
var list = await OpenIM.iMManager.groupManager.getGroupMembersInfo(
|
||||
groupID: groupID,
|
||||
userIDList: userIDList,
|
||||
);
|
||||
return list;
|
||||
}
|
||||
|
||||
void _playSound() async {
|
||||
if (!_audioPlayer.playerState.playing) {
|
||||
_audioPlayer.setAsset(_ring, package: 'openim_common');
|
||||
_audioPlayer.setLoopMode(LoopMode.one);
|
||||
_audioPlayer.setVolume(1.0);
|
||||
_audioPlayer.play();
|
||||
}
|
||||
}
|
||||
|
||||
void _stopSound() async {
|
||||
if (_audioPlayer.playerState.playing) {
|
||||
_audioPlayer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
void _insertMessage({
|
||||
required CallState state,
|
||||
required SignalingInfo signalingInfo,
|
||||
int duration = 0,
|
||||
}) async {
|
||||
(() async {
|
||||
var invitation = signalingInfo.invitation;
|
||||
var mediaType = invitation!.mediaType;
|
||||
var inviterUserID = invitation.inviterUserID;
|
||||
var inviteeUserID = invitation.inviteeUserIDList!.first;
|
||||
var groupID = invitation.groupID;
|
||||
Logger.print(
|
||||
'end calling and insert message state:${state.name}, mediaType:$mediaType, inviterUserID:$inviterUserID, inviteeUserID:$inviteeUserID, groupID:$groupID, duration:$duration',
|
||||
functionName: '_insertMessage');
|
||||
var message = await OpenIM.iMManager.messageManager.createCallMessage(
|
||||
state: state.name,
|
||||
type: mediaType!,
|
||||
duration: duration,
|
||||
);
|
||||
|
||||
String? receiverID;
|
||||
if (inviterUserID != OpenIM.iMManager.userID) {
|
||||
receiverID = inviterUserID;
|
||||
} else {
|
||||
receiverID = inviteeUserID;
|
||||
}
|
||||
|
||||
var msg = await OpenIM.iMManager.messageManager.insertSingleMessageToLocalStorage(
|
||||
receiverID: inviteeUserID,
|
||||
senderID: inviterUserID,
|
||||
message: message
|
||||
..status = 2
|
||||
..isRead = true,
|
||||
);
|
||||
|
||||
onSignalingMessage?.call(SignalingMessageEvent(msg, 1, receiverID, null));
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
class SignalingMessageEvent {
|
||||
Message message;
|
||||
String? userID;
|
||||
String? groupID;
|
||||
int sessionType;
|
||||
|
||||
SignalingMessageEvent(
|
||||
this.message,
|
||||
this.sessionType,
|
||||
this.userID,
|
||||
this.groupID,
|
||||
);
|
||||
|
||||
bool get isSingleChat => sessionType == ConversationType.single;
|
||||
|
||||
bool get isGroupChat => sessionType == ConversationType.group || sessionType == ConversationType.superGroup;
|
||||
}
|
||||
|
||||
extension MessageMangerExt on MessageManager {
|
||||
Future<Message> createCallMessage({
|
||||
required String type,
|
||||
required String state,
|
||||
int? duration,
|
||||
}) =>
|
||||
createCustomMessage(
|
||||
data: json.encode({
|
||||
"customType": CustomMessageType.call,
|
||||
"data": {
|
||||
'duration': duration,
|
||||
'state': state,
|
||||
'type': type,
|
||||
},
|
||||
}),
|
||||
extension: '',
|
||||
description: '',
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user