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:
编码工程师
2026-08-20 07:58:24 +08:00
co-authored by Cursor multica-agent
parent 2a817e77d8
commit 3832e080c4
19 changed files with 597 additions and 81 deletions
@@ -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())