🐛 Bug fixes of webrtc

This commit is contained in:
2025-10-19 18:22:03 +08:00
parent e96b1fd9d4
commit 0910be88ef
3 changed files with 136 additions and 22 deletions

View File

@@ -120,14 +120,16 @@ class CallNotifier extends _$CallNotifier {
// Add local participant immediately when WebRTC is initialized // Add local participant immediately when WebRTC is initialized
final userinfo = ref.watch(userInfoProvider); final userinfo = ref.watch(userInfoProvider);
_addLocalParticipant(userinfo.value!); if (userinfo.value != null) {
_addLocalParticipant(userinfo.value!);
}
} }
void _addLocalParticipant(SnAccount userinfo) { void _addLocalParticipant(SnAccount userinfo) {
if (_webrtcManager == null) return; if (_webrtcManager == null) return;
// Remove any existing local participant first // 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) // Add local participant (current user)
final localParticipant = CallParticipantLive( final localParticipant = CallParticipantLive(
@@ -154,12 +156,16 @@ class CallNotifier extends _$CallNotifier {
final webrtcParticipants = _webrtcManager!.participants; final webrtcParticipants = _webrtcManager!.participants;
// Get the local participant (should be the first one) // Always ensure local participant exists
final localParticipant = final existingLocalParticipant =
_participants.isNotEmpty && _participants[0].participant.name == 'You' _participants.isNotEmpty &&
_participants[0].remoteParticipant.id == _webrtcManager!.roomId
? _participants[0] ? _participants[0]
: null; : null;
final localParticipant =
existingLocalParticipant ?? _createLocalParticipant();
// Add remote participants // Add remote participants
final remoteParticipants = final remoteParticipants =
webrtcParticipants.map((p) { webrtcParticipants.map((p) {
@@ -179,14 +185,63 @@ class CallNotifier extends _$CallNotifier {
}).toList(); }).toList();
// Combine local participant with remote participants // Combine local participant with remote participants
_participants = _participants = [localParticipant, ...remoteParticipants];
localParticipant != null
? [localParticipant, ...remoteParticipants]
: remoteParticipants;
state = state.copyWith(); 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<void> joinRoom(String roomId) async { Future<void> joinRoom(String roomId) async {
if (_roomId == roomId && _webrtcManager != null) { if (_roomId == roomId && _webrtcManager != null) {
talker.info('[Call] Call skipped. Already connected to this room'); talker.info('[Call] Call skipped. Already connected to this room');
@@ -258,12 +313,24 @@ class CallNotifier extends _$CallNotifier {
final target = !state.isMicrophoneEnabled; final target = !state.isMicrophoneEnabled;
state = state.copyWith(isMicrophoneEnabled: target); state = state.copyWith(isMicrophoneEnabled: target);
await _webrtcManager?.toggleMicrophone(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<void> toggleCamera() async { Future<void> toggleCamera() async {
final target = !state.isCameraEnabled; final target = !state.isCameraEnabled;
state = state.copyWith(isCameraEnabled: target); state = state.copyWith(isCameraEnabled: target);
await _webrtcManager?.toggleCamera(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<void> toggleScreenShare(BuildContext context) async { Future<void> toggleScreenShare(BuildContext context) async {

View File

@@ -55,7 +55,7 @@ class WebRTCManager {
try { try {
_localStream = await navigator.mediaDevices.getUserMedia({ _localStream = await navigator.mediaDevices.getUserMedia({
'audio': true, 'audio': true,
'video': false, 'video': true,
}); });
talker.info('[WebRTC] Local stream initialized'); talker.info('[WebRTC] Local stream initialized');
} catch (e) { } catch (e) {
@@ -263,6 +263,12 @@ class WebRTCManager {
track.enabled = enabled; 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<void> toggleCamera(bool enabled) async { Future<void> toggleCamera(bool enabled) async {
@@ -271,6 +277,12 @@ class WebRTCManager {
track.enabled = enabled; 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<WebRTCParticipant> get participants => _participants.values.toList(); List<WebRTCParticipant> get participants => _participants.values.toList();

View File

@@ -81,30 +81,65 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
} }
} }
class CallParticipantTile extends HookConsumerWidget { class CallParticipantTile extends StatefulWidget {
final CallParticipantLive live; final CallParticipantLive live;
const CallParticipantTile({super.key, required this.live}); const CallParticipantTile({super.key, required this.live});
@override @override
Widget build(BuildContext context, WidgetRef ref) { State<CallParticipantTile> createState() => _CallParticipantTileState();
if (live.hasVideo && live.remoteParticipant.remoteStream != null) { }
class _CallParticipantTileState extends State<CallParticipantTile> {
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<void> _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( return Stack(
fit: StackFit.loose, fit: StackFit.loose,
children: [ children: [
AspectRatio( AspectRatio(aspectRatio: 16 / 9, child: RTCVideoView(_renderer!)),
aspectRatio: 16 / 9,
child: RTCVideoView(
RTCVideoRenderer()
..srcObject = live.remoteParticipant.remoteStream,
),
),
Positioned( Positioned(
left: 8, left: 8,
right: 8, right: 8,
bottom: 8, bottom: 8,
child: Text( child: Text(
'@${live.participant.name}', '@${widget.live.participant.name}',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
@@ -123,7 +158,7 @@ class CallParticipantTile extends HookConsumerWidget {
], ],
); );
} else { } else {
return SpeakingRippleAvatar(size: 84, live: live); return SpeakingRippleAvatar(size: 84, live: widget.live);
} }
} }
} }