Files
tongxunruanjian/mobile-next/openim_live/lib/src/pages/single/room.dart
T
3832e080c4 feat(mobile-next): 迁移 Android 一对一语音通话薄适配与入口
复用官方 OpenIMLive 信令与房间,换票改走账号服务 /api/rtc_token;入口仅音频,占线/超时/重复回调与销毁释放由公司层防护。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-20 07:58:24 +08:00

209 lines
5.9 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:openim_common/openim_common.dart';
import '../../live_client.dart';
import 'widgets/call_state.dart';
import 'widgets/participant.dart';
class SingleRoomView extends SignalView {
const SingleRoomView({
super.key,
required super.callType,
required super.initState,
required super.userID,
required super.callEventSubject,
required super.autoPickup,
super.roomID,
super.onClose,
super.onBindRoomID,
super.onBusyLine,
super.onDial,
super.onStartCalling,
super.onTapCancel,
super.onTapHangup,
super.onTapPickup,
super.onTapReject,
super.onWaitingAccept,
super.onSyncUserInfo,
super.onError,
super.onRoomDisconnected,
});
@override
SignalState<SingleRoomView> createState() => _SingleRoomViewState();
}
class _SingleRoomViewState extends SignalState<SingleRoomView> {
EventsListener<RoomEvent>? _listener;
Room? _room;
@override
void 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;
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 {
_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,
),
),
),
);
_listener = _room?.createListener();
await _room?.connect(
url,
token,
connectOptions: const ConnectOptions(autoSubscribe: true),
);
if (!mounted) return;
_room?.addListener(_onRoomDidUpdate);
if (null != _listener) _setUpListeners();
if (null != _room) roomDidUpdateSubject.add(_room!);
_sortParticipants();
if (CallState.call == callState || CallState.connecting == callState) {
widget.onWaitingAccept?.call();
}
WidgetsBindingCompatible.instance?.addPostFrameCallback((_) {
_publish();
});
} catch (error, stackTrace) {
widget.onError?.call(error, stackTrace);
}
}
void _setUpListeners() => _listener!
..on<RoomDisconnectedEvent>((event) async {
Logger.print('Room disconnected: reason => ${event.reason}');
WidgetsBindingCompatible.instance?.addPostFrameCallback((_) {
widget.onRoomDisconnected?.call();
widget.onClose?.call();
});
})
..on<RoomAttemptReconnectEvent>((event) {
IMViews.showToast('通话重连中');
})
..on<RoomReconnectedEvent>((event) {
IMViews.showToast('通话已恢复');
})
..on<RoomRecordingStatusChanged>((event) {})
..on<LocalTrackPublishedEvent>((_) => _sortParticipants())
..on<LocalTrackUnpublishedEvent>((_) => _sortParticipants())
..on<ParticipantConnectedEvent>((_) => onParticipantConnected())
..on<ParticipantDisconnectedEvent>((_) => onParticipantDisconnected())
..on<DataReceivedEvent>((event) {
String decoded = 'Failed to decode';
try {
decoded = utf8.decode(event.data);
} catch (_) {
Logger.print('Failed to decode: $_');
}
});
void _publish() async {
// video will fail when running in ios simulator
try {
final enabled = widget.callType == CallType.video;
await _room?.localParticipant?.setCameraEnabled(enabled);
} catch (error, stackTrace) {
Logger.print('could not publish video: $error $stackTrace');
}
try {
await _room?.localParticipant?.setMicrophoneEnabled(enabledMicrophone);
} catch (error, stackTrace) {
Logger.print('could not publish audio: $error $stackTrace');
}
}
void _onRoomDidUpdate() {
_sortParticipants();
if (null != _room) roomDidUpdateSubject.add(_room!);
}
void _sortParticipants() {
if (null == _room) return;
final localParticipant = _room!.localParticipant;
if (null != localParticipant) {
VideoTrack? videoTrack;
for (var t in localParticipant.videoTrackPublications) {
if (!t.isScreenShare) {
videoTrack = t.track;
break;
}
}
localParticipantTrack = ParticipantTrack(
participant: localParticipant,
videoTrack: videoTrack,
isScreenShare: false,
);
}
final participant = _room!.remoteParticipants.values.firstOrNull;
if (null != participant) {
VideoTrack? videoTrack;
for (var t in participant.videoTrackPublications) {
if (!t.isScreenShare) {
videoTrack = t.track;
break;
}
}
remoteParticipantTrack = ParticipantTrack(
participant: participant,
videoTrack: videoTrack,
isScreenShare: false,
);
}
if (null != remoteParticipantTrack) {
onParticipantConnected();
}
setState(() {});
}
@override
bool existParticipants() {
return _room?.remoteParticipants.isNotEmpty == true;
}
}