From 0910be88ef01e4b0e0ac012028990d8453f23ce2 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 19 Oct 2025 18:22:03 +0800 Subject: [PATCH] :bug: Bug fixes of webrtc --- lib/pods/chat/call.dart | 85 ++++++++++++++++++--- lib/pods/chat/webrtc_manager.dart | 14 +++- lib/widgets/chat/call_participant_tile.dart | 59 +++++++++++--- 3 files changed, 136 insertions(+), 22 deletions(-) diff --git a/lib/pods/chat/call.dart b/lib/pods/chat/call.dart index 94a07357..f7703587 100644 --- a/lib/pods/chat/call.dart +++ b/lib/pods/chat/call.dart @@ -120,14 +120,16 @@ class CallNotifier extends _$CallNotifier { // Add local participant immediately when WebRTC is initialized final userinfo = ref.watch(userInfoProvider); - _addLocalParticipant(userinfo.value!); + if (userinfo.value != null) { + _addLocalParticipant(userinfo.value!); + } } void _addLocalParticipant(SnAccount userinfo) { if (_webrtcManager == null) return; // Remove any existing local participant first - _participants.removeWhere((p) => p.participant.name == 'You'); + _participants.removeWhere((p) => p.participant.identity == userinfo.id); // Add local participant (current user) final localParticipant = CallParticipantLive( @@ -154,12 +156,16 @@ class CallNotifier extends _$CallNotifier { final webrtcParticipants = _webrtcManager!.participants; - // Get the local participant (should be the first one) - final localParticipant = - _participants.isNotEmpty && _participants[0].participant.name == 'You' + // Always ensure local participant exists + final existingLocalParticipant = + _participants.isNotEmpty && + _participants[0].remoteParticipant.id == _webrtcManager!.roomId ? _participants[0] : null; + final localParticipant = + existingLocalParticipant ?? _createLocalParticipant(); + // Add remote participants final remoteParticipants = webrtcParticipants.map((p) { @@ -179,14 +185,63 @@ class CallNotifier extends _$CallNotifier { }).toList(); // Combine local participant with remote participants - _participants = - localParticipant != null - ? [localParticipant, ...remoteParticipants] - : remoteParticipants; + _participants = [localParticipant, ...remoteParticipants]; state = state.copyWith(); } + CallParticipantLive _createLocalParticipant() { + return CallParticipantLive( + participant: CallParticipant( + identity: _webrtcManager!.roomId, // Use roomId as local identity + name: 'You', + accountId: '', + account: null, + joinedAt: DateTime.now(), + ), + remoteParticipant: WebRTCParticipant( + id: _webrtcManager!.roomId, + name: 'You', + userinfo: SnAccount( + id: '', + name: '', + nick: '', + language: '', + isSuperuser: false, + automatedId: null, + profile: SnAccountProfile( + id: '', + firstName: '', + middleName: '', + lastName: '', + bio: '', + gender: '', + pronouns: '', + location: '', + timeZone: '', + links: [], + experience: 0, + level: 0, + socialCredits: 0, + socialCreditsLevel: 0, + levelingProgress: 0, + picture: null, + background: null, + verification: null, + usernameColor: null, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + deletedAt: null, + ), + perkSubscription: null, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + deletedAt: null, + ), + )..remoteStream = _webrtcManager!.localStream, // Access local stream + ); + } + Future joinRoom(String roomId) async { if (_roomId == roomId && _webrtcManager != null) { talker.info('[Call] Call skipped. Already connected to this room'); @@ -258,12 +313,24 @@ class CallNotifier extends _$CallNotifier { final target = !state.isMicrophoneEnabled; state = state.copyWith(isMicrophoneEnabled: target); await _webrtcManager?.toggleMicrophone(target); + + // Update local participant's audio state + if (_participants.isNotEmpty) { + _participants[0].remoteParticipant.isAudioEnabled = target; + state = state.copyWith(); // Trigger UI update + } } Future toggleCamera() async { final target = !state.isCameraEnabled; state = state.copyWith(isCameraEnabled: target); await _webrtcManager?.toggleCamera(target); + + // Update local participant's video state + if (_participants.isNotEmpty) { + _participants[0].remoteParticipant.isVideoEnabled = target; + state = state.copyWith(); // Trigger UI update + } } Future toggleScreenShare(BuildContext context) async { diff --git a/lib/pods/chat/webrtc_manager.dart b/lib/pods/chat/webrtc_manager.dart index 6513be5c..c416a2c0 100644 --- a/lib/pods/chat/webrtc_manager.dart +++ b/lib/pods/chat/webrtc_manager.dart @@ -55,7 +55,7 @@ class WebRTCManager { try { _localStream = await navigator.mediaDevices.getUserMedia({ 'audio': true, - 'video': false, + 'video': true, }); talker.info('[WebRTC] Local stream initialized'); } catch (e) { @@ -263,6 +263,12 @@ class WebRTCManager { track.enabled = enabled; } } + + // Update audio enabled state for all participants (they share the same local stream) + for (final participant in _participants.values) { + participant.isAudioEnabled = enabled; + _participantController.add(participant); + } } Future toggleCamera(bool enabled) async { @@ -271,6 +277,12 @@ class WebRTCManager { track.enabled = enabled; }); } + + // Update video enabled state for all participants (they share the same local stream) + for (final participant in _participants.values) { + participant.isVideoEnabled = enabled; + _participantController.add(participant); + } } List get participants => _participants.values.toList(); diff --git a/lib/widgets/chat/call_participant_tile.dart b/lib/widgets/chat/call_participant_tile.dart index 70e66974..f71afd61 100644 --- a/lib/widgets/chat/call_participant_tile.dart +++ b/lib/widgets/chat/call_participant_tile.dart @@ -81,30 +81,65 @@ class SpeakingRippleAvatar extends HookConsumerWidget { } } -class CallParticipantTile extends HookConsumerWidget { +class CallParticipantTile extends StatefulWidget { final CallParticipantLive live; const CallParticipantTile({super.key, required this.live}); @override - Widget build(BuildContext context, WidgetRef ref) { - if (live.hasVideo && live.remoteParticipant.remoteStream != null) { + State createState() => _CallParticipantTileState(); +} + +class _CallParticipantTileState extends State { + RTCVideoRenderer? _renderer; + + @override + void initState() { + super.initState(); + _initRenderer(); + } + + @override + void didUpdateWidget(CallParticipantTile oldWidget) { + super.didUpdateWidget(oldWidget); + // Update renderer source when the stream changes + if (_renderer != null && + widget.live.remoteParticipant.remoteStream != + oldWidget.live.remoteParticipant.remoteStream) { + _renderer!.srcObject = widget.live.remoteParticipant.remoteStream; + } + } + + Future _initRenderer() async { + _renderer = RTCVideoRenderer(); + await _renderer!.initialize(); + _renderer!.srcObject = widget.live.remoteParticipant.remoteStream; + if (mounted) { + setState(() {}); + } + } + + @override + void dispose() { + _renderer?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.live.hasVideo && + widget.live.remoteParticipant.remoteStream != null && + _renderer != null) { return Stack( fit: StackFit.loose, children: [ - AspectRatio( - aspectRatio: 16 / 9, - child: RTCVideoView( - RTCVideoRenderer() - ..srcObject = live.remoteParticipant.remoteStream, - ), - ), + AspectRatio(aspectRatio: 16 / 9, child: RTCVideoView(_renderer!)), Positioned( left: 8, right: 8, bottom: 8, child: Text( - '@${live.participant.name}', + '@${widget.live.participant.name}', textAlign: TextAlign.center, style: const TextStyle( fontSize: 14, @@ -123,7 +158,7 @@ class CallParticipantTile extends HookConsumerWidget { ], ); } else { - return SpeakingRippleAvatar(size: 84, live: live); + return SpeakingRippleAvatar(size: 84, live: widget.live); } } }