feat(mobile-next): 迁移 Android 一对一语音通话薄适配与入口
复用官方 OpenIMLive 信令与房间,换票改走账号服务 /api/rtc_token;入口仅音频,占线/超时/重复回调与销毁释放由公司层防护。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
2a817e77d8
commit
3832e080c4
@@ -134,6 +134,11 @@ mixin OpenIMLive {
|
||||
),
|
||||
onError: onError,
|
||||
onRoomDisconnected: () => onRoomDisconnected(event.data),
|
||||
onStartCalling: onLiveConnected,
|
||||
onClose: () {
|
||||
_stopSound();
|
||||
onLiveSessionClosed();
|
||||
},
|
||||
);
|
||||
} else if (event.state == CallState.beRejected) {
|
||||
insertSignalingMessageSubject.add(event);
|
||||
@@ -224,10 +229,14 @@ mixin OpenIMLive {
|
||||
onBusyLine: onBusyLine,
|
||||
onStartCalling: () {
|
||||
_stopSound();
|
||||
onLiveConnected();
|
||||
},
|
||||
onError: onError,
|
||||
onRoomDisconnected: () => onRoomDisconnected(signal),
|
||||
onClose: _stopSound,
|
||||
onClose: () {
|
||||
_stopSound();
|
||||
onLiveSessionClosed();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -255,11 +264,20 @@ mixin OpenIMLive {
|
||||
offlinePushInfo: OfflinePushInfo(),
|
||||
userID: signaling.invitation!.inviteeUserIDList!.first,
|
||||
isOnlineOnly: true);
|
||||
final certificate = await Apis.getTokenForRTC(signaling.invitation!.roomID!, OpenIM.iMManager.userID);
|
||||
final certificate = await getRtcCertificate(signaling.invitation!.roomID!, OpenIM.iMManager.userID);
|
||||
|
||||
return certificate;
|
||||
}
|
||||
|
||||
/// 公司层可覆盖:走账号服务 `/api/rtc_token`,默认仍走官方 chat 换票。
|
||||
Future<SignalingCertificate> getRtcCertificate(String roomID, String userID) {
|
||||
return Apis.getTokenForRTC(roomID, userID);
|
||||
}
|
||||
|
||||
void onLiveSessionClosed() {}
|
||||
|
||||
void onLiveConnected() {}
|
||||
|
||||
Future<SignalingCertificate> onTapPickup(SignalingInfo signaling) async {
|
||||
_beCalledEvent = null; // ios bug
|
||||
_autoPickup = false;
|
||||
@@ -272,7 +290,7 @@ mixin OpenIMLive {
|
||||
offlinePushInfo: OfflinePushInfo(),
|
||||
userID: signaling.invitation!.inviterUserID,
|
||||
isOnlineOnly: true);
|
||||
final certificate = await Apis.getTokenForRTC(signaling.invitation!.roomID!, OpenIM.iMManager.userID);
|
||||
final certificate = await getRtcCertificate(signaling.invitation!.roomID!, OpenIM.iMManager.userID);
|
||||
|
||||
return certificate;
|
||||
}
|
||||
|
||||
@@ -43,50 +43,59 @@ class _SingleRoomViewState extends SignalState<SingleRoomView> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// always dispose listener
|
||||
(() async {
|
||||
_room?.removeListener(_onRoomDidUpdate);
|
||||
await _listener?.dispose();
|
||||
await _room?.remoteParticipants.values.firstOrNull?.dispose();
|
||||
await _room?.localParticipant?.dispose();
|
||||
await _room?.disconnect();
|
||||
await _room?.dispose();
|
||||
})();
|
||||
final room = _room;
|
||||
final listener = _listener;
|
||||
_room = null;
|
||||
_listener = null;
|
||||
room?.removeListener(_onRoomDidUpdate);
|
||||
() async {
|
||||
await listener?.dispose();
|
||||
await room?.remoteParticipants.values.firstOrNull?.dispose();
|
||||
await room?.localParticipant?.dispose();
|
||||
await room?.disconnect();
|
||||
await room?.dispose();
|
||||
}();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> connect() async {
|
||||
final url = certificate.liveURL!;
|
||||
final token = certificate.token!;
|
||||
final url = certificate.liveURL;
|
||||
final token = certificate.token;
|
||||
if (url == null || url.isEmpty || token == null || token.isEmpty) {
|
||||
widget.onError?.call(StateError('missing livekit credential'), StackTrace.current);
|
||||
widget.onClose?.call();
|
||||
return;
|
||||
}
|
||||
final busyLineUsers = certificate.busyLineUserIDList ?? [];
|
||||
if (busyLineUsers.isNotEmpty) {
|
||||
widget.onBusyLine?.call();
|
||||
widget.onClose?.call();
|
||||
return;
|
||||
}
|
||||
// Try to connect to a room
|
||||
// This will throw an Exception if it fails for any reason.
|
||||
try {
|
||||
//create new room
|
||||
_room = Room();
|
||||
_room = Room(
|
||||
roomOptions: const RoomOptions(
|
||||
dynacast: true,
|
||||
adaptiveStream: true,
|
||||
defaultCameraCaptureOptions: CameraCaptureOptions(params: VideoParametersPresets.h720_169),
|
||||
defaultVideoPublishOptions: VideoPublishOptions(
|
||||
simulcast: true,
|
||||
videoCodec: 'VP9',
|
||||
videoEncoding: VideoEncoding(
|
||||
maxBitrate: 5 * 1000 * 1000,
|
||||
maxFramerate: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Create a Listener before connecting
|
||||
_listener = _room?.createListener();
|
||||
// Try to connect to the room
|
||||
// This will throw an Exception if it fails for any reason.
|
||||
await _room?.connect(url, token,
|
||||
roomOptions: RoomOptions(
|
||||
dynacast: true,
|
||||
adaptiveStream: true,
|
||||
defaultCameraCaptureOptions: const CameraCaptureOptions(params: VideoParametersPresets.h720_169),
|
||||
defaultVideoPublishOptions: VideoPublishOptions(
|
||||
simulcast: true,
|
||||
videoCodec: 'VP9',
|
||||
videoEncoding: const VideoEncoding(
|
||||
maxBitrate: 5 * 1000 * 1000,
|
||||
maxFramerate: 15,
|
||||
))));
|
||||
await _room?.connect(
|
||||
url,
|
||||
token,
|
||||
connectOptions: const ConnectOptions(autoSubscribe: true),
|
||||
);
|
||||
if (!mounted) return;
|
||||
_room?.addListener(_onRoomDidUpdate);
|
||||
if (null != _listener) _setUpListeners();
|
||||
@@ -111,6 +120,12 @@ class _SingleRoomViewState extends SignalState<SingleRoomView> {
|
||||
widget.onClose?.call();
|
||||
});
|
||||
})
|
||||
..on<RoomAttemptReconnectEvent>((event) {
|
||||
IMViews.showToast('通话重连中');
|
||||
})
|
||||
..on<RoomReconnectedEvent>((event) {
|
||||
IMViews.showToast('通话已恢复');
|
||||
})
|
||||
..on<RoomRecordingStatusChanged>((event) {})
|
||||
..on<LocalTrackPublishedEvent>((_) => _sortParticipants())
|
||||
..on<LocalTrackUnpublishedEvent>((_) => _sortParticipants())
|
||||
|
||||
@@ -70,6 +70,7 @@ abstract class SignalState<T extends SignalView> extends State<T> {
|
||||
int duration = 0;
|
||||
bool enabledMicrophone = true;
|
||||
bool enabledSpeaker = true;
|
||||
Timer? _inviteTimeout;
|
||||
|
||||
ParticipantTrack? remoteParticipantTrack;
|
||||
ParticipantTrack? localParticipantTrack;
|
||||
@@ -82,11 +83,14 @@ abstract class SignalState<T extends SignalView> extends State<T> {
|
||||
widget.onSyncUserInfo?.call(widget.userID).then(_onUpdateUserInfo);
|
||||
onDail();
|
||||
autoPickup();
|
||||
_armInviteTimeout();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_inviteTimeout?.cancel();
|
||||
_inviteTimeout = null;
|
||||
callStateSubject.close();
|
||||
callEventSub?.cancel();
|
||||
super.dispose();
|
||||
@@ -116,14 +120,17 @@ abstract class SignalState<T extends SignalView> extends State<T> {
|
||||
}
|
||||
|
||||
if (event.state == CallState.beRejected || event.state == CallState.beCanceled) {
|
||||
_cancelInviteTimeout();
|
||||
widget.onClose?.call();
|
||||
} else if (event.state == CallState.otherReject || event.state == CallState.otherAccepted) {
|
||||
if (existParticipants()) {
|
||||
return;
|
||||
}
|
||||
_cancelInviteTimeout();
|
||||
widget.onClose?.call();
|
||||
IMViews.showToast(sprintf(StrRes.otherCallHandle, [event.state == CallState.otherReject ? StrRes.rejectCall : StrRes.accept]));
|
||||
} else if (event.state == CallState.timeout) {
|
||||
_cancelInviteTimeout();
|
||||
widget.onClose?.call();
|
||||
} else if (event.state == CallState.beAccepted) {
|
||||
// 邀请对象比发起对象提前进入房间
|
||||
@@ -134,6 +141,7 @@ abstract class SignalState<T extends SignalView> extends State<T> {
|
||||
}
|
||||
|
||||
onParticipantConnected() {
|
||||
_cancelInviteTimeout();
|
||||
callStateSubject.add(CallState.calling);
|
||||
widget.onStartCalling?.call();
|
||||
}
|
||||
@@ -145,10 +153,13 @@ abstract class SignalState<T extends SignalView> extends State<T> {
|
||||
/// 发起者在对方为进入房间都是 等待状态
|
||||
onDail() async {
|
||||
if (widget.initState == CallState.call) {
|
||||
// callStateSubject.add(CallState.connecting);
|
||||
certificate = await widget.onDial!.call();
|
||||
widget.onBindRoomID?.call(roomID = certificate.roomID!);
|
||||
await connect();
|
||||
try {
|
||||
certificate = await widget.onDial!.call();
|
||||
widget.onBindRoomID?.call(roomID = certificate.roomID!);
|
||||
await connect();
|
||||
} catch (error, stackTrace) {
|
||||
widget.onError?.call(error, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,25 +171,33 @@ abstract class SignalState<T extends SignalView> extends State<T> {
|
||||
|
||||
onTapPickup() async {
|
||||
Logger.print('------------onTapPickup---------连接中--------');
|
||||
callStateSubject.add(CallState.connecting);
|
||||
certificate = await widget.onTapPickup!.call();
|
||||
widget.onBindRoomID?.call(roomID = certificate.roomID!);
|
||||
await connect();
|
||||
callStateSubject.add(CallState.calling);
|
||||
widget.onStartCalling?.call();
|
||||
Logger.print('------------onTapPickup---------连接成功--------');
|
||||
try {
|
||||
callStateSubject.add(CallState.connecting);
|
||||
certificate = await widget.onTapPickup!.call();
|
||||
widget.onBindRoomID?.call(roomID = certificate.roomID!);
|
||||
await connect();
|
||||
_cancelInviteTimeout();
|
||||
callStateSubject.add(CallState.calling);
|
||||
widget.onStartCalling?.call();
|
||||
Logger.print('------------onTapPickup---------连接成功--------');
|
||||
} catch (error, stackTrace) {
|
||||
widget.onError?.call(error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
/// [isPositive] 人为挂断行为
|
||||
onTapHangup(bool isPositive) async {
|
||||
_cancelInviteTimeout();
|
||||
await widget.onTapHangup?.call(duration, isPositive).whenComplete(() => /*isPositive ? {} : */ widget.onClose?.call());
|
||||
}
|
||||
|
||||
onTapCancel() async {
|
||||
_cancelInviteTimeout();
|
||||
await widget.onTapCancel?.call().whenComplete(() => widget.onClose?.call());
|
||||
}
|
||||
|
||||
onTapReject() async {
|
||||
_cancelInviteTimeout();
|
||||
await widget.onTapReject?.call().whenComplete(() => widget.onClose?.call());
|
||||
}
|
||||
|
||||
@@ -227,6 +246,27 @@ abstract class SignalState<T extends SignalView> extends State<T> {
|
||||
|
||||
bool smallScreenIsRemote = true;
|
||||
|
||||
void _armInviteTimeout() {
|
||||
if (widget.initState != CallState.call && widget.initState != CallState.beCalled) {
|
||||
return;
|
||||
}
|
||||
_inviteTimeout?.cancel();
|
||||
_inviteTimeout = Timer(const Duration(seconds: 30), () async {
|
||||
if (!mounted) return;
|
||||
if (callState == CallState.calling) return;
|
||||
if (widget.initState == CallState.call) {
|
||||
await onTapCancel();
|
||||
} else {
|
||||
await onTapReject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _cancelInviteTimeout() {
|
||||
_inviteTimeout?.cancel();
|
||||
_inviteTimeout = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Stack(
|
||||
children: [
|
||||
|
||||
@@ -4,24 +4,15 @@ import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:openim_live/src/widgets/live_button.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
|
||||
import '../../../live_client.dart';
|
||||
import '../../../widgets/loading_view.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:openim_live/src/widgets/live_button.dart';
|
||||
import 'package:openim_live/src/widgets/loading_view.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
|
||||
import '../../../live_client.dart';
|
||||
|
||||
class ControlsView extends StatefulWidget {
|
||||
const ControlsView({
|
||||
Key? key,
|
||||
@@ -82,8 +73,8 @@ class _ControlsViewState extends State<ControlsView> {
|
||||
/// 默认启用麦克风
|
||||
bool _enabledMicrophone = true;
|
||||
|
||||
/// 默认开启扬声器
|
||||
bool _enabledSpeaker = true;
|
||||
/// 语音默认听筒,视频默认扬声器
|
||||
late bool _enabledSpeaker;
|
||||
|
||||
final _lockAudio = Lock();
|
||||
final _lockSpeaker = Lock();
|
||||
@@ -100,6 +91,7 @@ class _ControlsViewState extends State<ControlsView> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_enabledSpeaker = widget.callType != CallType.audio;
|
||||
_onChangedCallState(widget.initState);
|
||||
_callStateChangedSub = widget.callStateStream.listen(_onChangedCallState);
|
||||
_roomDidUpdateSub = widget.roomDidUpdateStream.listen(_roomDidUpdate);
|
||||
@@ -116,6 +108,7 @@ class _ControlsViewState extends State<ControlsView> {
|
||||
_participant = room.localParticipant;
|
||||
_participant?.addListener(_onChange);
|
||||
}
|
||||
Hardware.instance.setSpeakerphoneOn(_enabledSpeaker);
|
||||
}
|
||||
|
||||
_onChangedCallState(CallState state) {
|
||||
|
||||
Reference in New Issue
Block a user