Compare commits
3 Commits
43d767bc03
...
0c459bf7e3
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c459bf7e3 | |||
| a2576abee0 | |||
| f4b28c3fa2 |
@@ -145,6 +145,7 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
id: _webrtcManager!.roomId,
|
id: _webrtcManager!.roomId,
|
||||||
name: userinfo.nick,
|
name: userinfo.nick,
|
||||||
userinfo: userinfo,
|
userinfo: userinfo,
|
||||||
|
isLocal: true,
|
||||||
)..remoteStream = _webrtcManager!.localStream, // Access local stream
|
)..remoteStream = _webrtcManager!.localStream, // Access local stream
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'call.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$callNotifierHash() => r'91e546c8711d1b46740ad592cbe481173b227e7b';
|
String _$callNotifierHash() => r'4015d326388553c46859fe537e84d2c9da4236c9';
|
||||||
|
|
||||||
/// See also [CallNotifier].
|
/// See also [CallNotifier].
|
||||||
@ProviderFor(CallNotifier)
|
@ProviderFor(CallNotifier)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
import 'package:island/models/account.dart';
|
import 'package:island/models/account.dart';
|
||||||
import 'package:island/pods/chat/webrtc_signaling.dart';
|
import 'package:island/pods/chat/webrtc_signaling.dart';
|
||||||
|
import 'package:island/pods/userinfo.dart';
|
||||||
import 'package:island/talker.dart';
|
import 'package:island/talker.dart';
|
||||||
|
|
||||||
class WebRTCParticipant {
|
class WebRTCParticipant {
|
||||||
@@ -11,14 +12,19 @@ class WebRTCParticipant {
|
|||||||
final SnAccount userinfo;
|
final SnAccount userinfo;
|
||||||
RTCPeerConnection? peerConnection;
|
RTCPeerConnection? peerConnection;
|
||||||
MediaStream? remoteStream;
|
MediaStream? remoteStream;
|
||||||
|
List<RTCIceCandidate> remoteCandidates = [];
|
||||||
bool isAudioEnabled = true;
|
bool isAudioEnabled = true;
|
||||||
bool isVideoEnabled = false;
|
bool isVideoEnabled = false;
|
||||||
bool isConnected = false;
|
bool isConnected = false;
|
||||||
|
bool isLocal = false;
|
||||||
|
|
||||||
WebRTCParticipant({
|
WebRTCParticipant({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.userinfo,
|
required this.userinfo,
|
||||||
|
this.isAudioEnabled = true,
|
||||||
|
this.isVideoEnabled = false,
|
||||||
|
this.isLocal = false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +52,10 @@ class WebRTCManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initialize(Ref ref) async {
|
Future<void> initialize(Ref ref) async {
|
||||||
|
final user = ref.watch(userInfoProvider).value!;
|
||||||
|
_signaling.userId = user.id;
|
||||||
|
_signaling.userName = user.name;
|
||||||
|
_signaling.user = user;
|
||||||
await _initializeLocalStream();
|
await _initializeLocalStream();
|
||||||
_setupSignalingListeners();
|
_setupSignalingListeners();
|
||||||
await _signaling.connect(ref);
|
await _signaling.connect(ref);
|
||||||
@@ -58,6 +68,19 @@ class WebRTCManager {
|
|||||||
'video': true,
|
'video': true,
|
||||||
});
|
});
|
||||||
talker.info('[WebRTC] Local stream initialized');
|
talker.info('[WebRTC] Local stream initialized');
|
||||||
|
|
||||||
|
// Add local participant
|
||||||
|
bool videoEnabled = _localStream!.getVideoTracks().isNotEmpty;
|
||||||
|
WebRTCParticipant localParticipant = WebRTCParticipant(
|
||||||
|
id: _signaling.userId,
|
||||||
|
name: _signaling.userName,
|
||||||
|
userinfo: _signaling.user,
|
||||||
|
isLocal: true,
|
||||||
|
isAudioEnabled: true,
|
||||||
|
isVideoEnabled: videoEnabled,
|
||||||
|
);
|
||||||
|
_participants[_signaling.userId] = localParticipant;
|
||||||
|
_participantController.add(localParticipant);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
talker.error('[WebRTC] Failed to initialize local stream: $e');
|
talker.error('[WebRTC] Failed to initialize local stream: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
@@ -154,6 +177,7 @@ class WebRTCManager {
|
|||||||
|
|
||||||
final peerConnection = await createPeerConnection(configuration);
|
final peerConnection = await createPeerConnection(configuration);
|
||||||
_peerConnections[participantId] = peerConnection;
|
_peerConnections[participantId] = peerConnection;
|
||||||
|
_participants[participantId]!.peerConnection = peerConnection;
|
||||||
|
|
||||||
if (_localStream != null) {
|
if (_localStream != null) {
|
||||||
for (final track in _localStream!.getTracks()) {
|
for (final track in _localStream!.getTracks()) {
|
||||||
@@ -231,6 +255,15 @@ class WebRTCManager {
|
|||||||
await peerConnection.setLocalDescription(answer);
|
await peerConnection.setLocalDescription(answer);
|
||||||
// CHANGED: Send answer to the specific participant
|
// CHANGED: Send answer to the specific participant
|
||||||
_signaling.sendAnswer(participantId, answer);
|
_signaling.sendAnswer(participantId, answer);
|
||||||
|
|
||||||
|
// Process any queued ICE candidates
|
||||||
|
final participant = _participants[participantId];
|
||||||
|
if (participant != null) {
|
||||||
|
for (final candidate in participant.remoteCandidates) {
|
||||||
|
await peerConnection.addCandidate(candidate);
|
||||||
|
}
|
||||||
|
participant.remoteCandidates.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleAnswer(String from, Map<String, dynamic> data) async {
|
Future<void> _handleAnswer(String from, Map<String, dynamic> data) async {
|
||||||
@@ -241,6 +274,15 @@ class WebRTCManager {
|
|||||||
final peerConnection = _peerConnections[participantId];
|
final peerConnection = _peerConnections[participantId];
|
||||||
if (peerConnection != null) {
|
if (peerConnection != null) {
|
||||||
await peerConnection.setRemoteDescription(answer);
|
await peerConnection.setRemoteDescription(answer);
|
||||||
|
|
||||||
|
// Process any queued ICE candidates
|
||||||
|
final participant = _participants[participantId];
|
||||||
|
if (participant != null) {
|
||||||
|
for (final candidate in participant.remoteCandidates) {
|
||||||
|
await peerConnection.addCandidate(candidate);
|
||||||
|
}
|
||||||
|
participant.remoteCandidates.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,11 +297,14 @@ class WebRTCManager {
|
|||||||
data['sdpMLineIndex'],
|
data['sdpMLineIndex'],
|
||||||
);
|
);
|
||||||
|
|
||||||
final peerConnection = _peerConnections[participantId];
|
final participant = _participants[participantId];
|
||||||
if (peerConnection != null) {
|
if (participant != null) {
|
||||||
// It's possible for candidates to arrive before the remote description is set.
|
final pc = participant.peerConnection;
|
||||||
// A robust implementation might queue them, but for now, we'll just add them.
|
if (pc != null) {
|
||||||
await peerConnection.addCandidate(candidate);
|
await pc.addCandidate(candidate);
|
||||||
|
} else {
|
||||||
|
participant.remoteCandidates.add(candidate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,11 +316,7 @@ class WebRTCManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update audio enabled state for all participants (they share the same local stream)
|
_participants[_signaling.userId]?.isAudioEnabled = enabled;
|
||||||
for (final participant in _participants.values) {
|
|
||||||
participant.isAudioEnabled = enabled;
|
|
||||||
_participantController.add(participant);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleCamera(bool enabled) async {
|
Future<void> toggleCamera(bool enabled) async {
|
||||||
@@ -285,11 +326,7 @@ class WebRTCManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update video enabled state for all participants (they share the same local stream)
|
_participants[_signaling.userId]?.isVideoEnabled = enabled;
|
||||||
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();
|
||||||
@@ -300,6 +337,7 @@ class WebRTCManager {
|
|||||||
pc.close();
|
pc.close();
|
||||||
}
|
}
|
||||||
_peerConnections.clear();
|
_peerConnections.clear();
|
||||||
|
_participants.values.forEach((p) => p.remoteCandidates.clear());
|
||||||
_participants.clear();
|
_participants.clear();
|
||||||
_localStream?.dispose();
|
_localStream?.dispose();
|
||||||
_participantController.close();
|
_participantController.close();
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import 'package:island/models/account.dart';
|
|||||||
import 'package:island/models/chat.dart';
|
import 'package:island/models/chat.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/pods/userinfo.dart';
|
|
||||||
import 'package:island/pods/websocket.dart';
|
import 'package:island/pods/websocket.dart';
|
||||||
import 'package:web_socket_channel/io.dart';
|
import 'package:web_socket_channel/io.dart';
|
||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
@@ -51,12 +50,13 @@ class WebRTCSignaling {
|
|||||||
final String roomId;
|
final String roomId;
|
||||||
late final String userId;
|
late final String userId;
|
||||||
late final String userName;
|
late final String userName;
|
||||||
late final SnAccount user;
|
late SnAccount user;
|
||||||
final StreamController<SignalingMessage> _messageController =
|
final StreamController<SignalingMessage> _messageController =
|
||||||
StreamController<SignalingMessage>.broadcast();
|
StreamController<SignalingMessage>.broadcast();
|
||||||
final StreamController<WebRTCWelcomeMessage> _welcomeController =
|
final StreamController<WebRTCWelcomeMessage> _welcomeController =
|
||||||
StreamController<WebRTCWelcomeMessage>.broadcast();
|
StreamController<WebRTCWelcomeMessage>.broadcast();
|
||||||
WebSocketChannel? _channel;
|
WebSocketChannel? _channel;
|
||||||
|
Timer? _heartbeatTimer;
|
||||||
|
|
||||||
Stream<SignalingMessage> get messages => _messageController.stream;
|
Stream<SignalingMessage> get messages => _messageController.stream;
|
||||||
Stream<WebRTCWelcomeMessage> get welcomeMessages => _welcomeController.stream;
|
Stream<WebRTCWelcomeMessage> get welcomeMessages => _welcomeController.stream;
|
||||||
@@ -64,13 +64,9 @@ class WebRTCSignaling {
|
|||||||
WebRTCSignaling({required this.roomId});
|
WebRTCSignaling({required this.roomId});
|
||||||
|
|
||||||
Future<void> connect(Ref ref) async {
|
Future<void> connect(Ref ref) async {
|
||||||
user = ref.watch(userInfoProvider).value!;
|
|
||||||
final baseUrl = ref.watch(serverUrlProvider);
|
final baseUrl = ref.watch(serverUrlProvider);
|
||||||
final token = await getToken(ref.watch(tokenProvider));
|
final token = await getToken(ref.watch(tokenProvider));
|
||||||
|
|
||||||
userId = user.id;
|
|
||||||
userName = user.name;
|
|
||||||
|
|
||||||
final url = '$baseUrl/sphere/chat/realtime/$roomId'.replaceFirst(
|
final url = '$baseUrl/sphere/chat/realtime/$roomId'.replaceFirst(
|
||||||
'http',
|
'http',
|
||||||
'ws',
|
'ws',
|
||||||
@@ -88,6 +84,9 @@ class WebRTCSignaling {
|
|||||||
}
|
}
|
||||||
await _channel!.ready;
|
await _channel!.ready;
|
||||||
|
|
||||||
|
// Start heartbeat timer
|
||||||
|
_heartbeatTimer = Timer.periodic(const Duration(seconds: 30), (timer) => _sendHeartbeat());
|
||||||
|
|
||||||
_channel!.stream.listen(
|
_channel!.stream.listen(
|
||||||
(data) {
|
(data) {
|
||||||
final dataStr =
|
final dataStr =
|
||||||
@@ -196,7 +195,15 @@ class WebRTCSignaling {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _sendHeartbeat() {
|
||||||
|
if (_channel == null) return;
|
||||||
|
talker.info('[WebRTC Signaling] Sending heartbeat');
|
||||||
|
final packet = WebSocketPacket(type: 'heartbeat', data: null);
|
||||||
|
_channel!.sink.add(jsonEncode(packet.toJson()));
|
||||||
|
}
|
||||||
|
|
||||||
void disconnect() {
|
void disconnect() {
|
||||||
|
_heartbeatTimer?.cancel();
|
||||||
_channel?.sink.close();
|
_channel?.sink.close();
|
||||||
_messageController.close();
|
_messageController.close();
|
||||||
_welcomeController.close();
|
_welcomeController.close();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
; ==================================================
|
; ==================================================
|
||||||
#define AppVersion "3.2.0"
|
#define AppVersion "3.3.0"
|
||||||
#define BuildNumber "134"
|
#define BuildNumber "136"
|
||||||
; ==================================================
|
; ==================================================
|
||||||
|
|
||||||
#define FullVersion AppVersion + "." + BuildNumber
|
#define FullVersion AppVersion + "." + BuildNumber
|
||||||
|
|||||||
Reference in New Issue
Block a user