复用官方 OpenIMLive 信令与房间,换票改走账号服务 /api/rtc_token;入口仅音频,占线/超时/重复回调与销毁释放由公司层防护。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
209 lines
5.9 KiB
Dart
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;
|
|
}
|
|
}
|