- 上游: 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>
194 lines
5.7 KiB
Dart
194 lines
5.7 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() {
|
|
// 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();
|
|
})();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Future<void> connect() async {
|
|
final url = certificate.liveURL!;
|
|
final token = certificate.token!;
|
|
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();
|
|
|
|
// 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,
|
|
))));
|
|
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<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;
|
|
}
|
|
}
|