Files
tongxunruanjian/mobile-next/openim_live/lib/src/pages/single/room.dart
T
编码工程师andmultica-agent 8046ec7483 feat(mobile-next): 导入官方 openim-flutter-demo 固定基线 3.8.3-patch.3 并完成双构建
- 上游: 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>
2026-08-19 15:24:33 +08:00

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;
}
}