🐛 Bug fixes of webrtc
This commit is contained in:
@@ -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);
|
||||||
|
if (userinfo.value != null) {
|
||||||
_addLocalParticipant(userinfo.value!);
|
_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 {
|
||||||
|
@@ -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();
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user