это мой код server.js:
// --- CRITICAL FIX 1: Use HTTPS ---
const https = require('https');
const fs = require('fs');
const express = require('express');
const { Server } = require('socket.io');
// --- CRITICAL FIX 2: Load Your Certificate Files (Updated to use .key and .crt) ---
// IMPORTANT: Ensure these four files (cert.key, cert.crt, ca.key, ca.crt)
// are located in the same directory as this server.js file.
try {
const options = {
// Use the server's private key
key: fs.readFileSync('cert.key'),
// Use the server's certificate
cert: fs.readFileSync('cert.crt'),
// NOTE: For local testing, you might need ca.crt as ca to complete the chain
// ca: fs.readFileSync('ca.crt'),
};
const app = express();
const PORT = 3000; // Using 3000, but running with HTTPS
// Use https.createServer with the options
const server = https.createServer(options, app);
const io = new Server(server, {
cors: { origin: "*" }, // Allow all origins for local testing
});
// ================== ROOM MANAGEMENT (Simplified) ==================
// Maps: roomId -> [socketIds]
const rooms = {};
// Maps: socketId -> roomId
const socketToRoom = {};
io.on('connection', (socket) => {
console.log(`
// ------------------- Room Joining / Creation -------------------
socket.on('joinRoom', (roomId) => {
// Leave any room they might already be in (cleanup)
const currentRoom = socketToRoom[socket.id];
if (currentRoom) {
socket.leave(currentRoom);
delete socketToRoom[socket.id];
// Notify the other user in the old room
socket.to(currentRoom).emit('userLeft', socket.id);
}
// Join the new room
socket.join(roomId);
socketToRoom[socket.id] = roomId;
// Add socket to the room list
if (!rooms[roomId]) {
rooms[roomId] = [];
}
rooms[roomId].push(socket.id);
console.log(`User ${socket.id} joined room ${roomId}`);
// Notify others in the room about the new user
const usersInRoom = rooms[roomId].filter(id => id !== socket.id);
// 1. Tell the new user who is already there (if any)
socket.emit('roomUsers', usersInRoom);
// 2. Tell existing users a new user joined
socket.to(roomId).emit('userJoined', socket.id);
});
// ------------------- Signaling Relay -------------------
// A standard way to relay WebRTC messages (SDP offers/answers and ICE candidates)
socket.on('signalingMessage', (data) => {
// Data should contain { to: targetSocketId, message: sdpOrIceCandidate }
const roomId = socketToRoom[socket.id];
if (!roomId) return;
// Relay the message to the specified target socket ID
// We use the `to()` method to send a private message
if (data.to) {
socket.to(data.to).emit('signalingMessage', {
from: socket.id,
message: data.message
});
} else {
// If no specific 'to' is defined, broadcast to all others in the room
socket.to(roomId).emit('signalingMessage', {
from: socket.id,
message: data.message
});
}
console.log(`Relaying ${data.message.type || 'ICE'} from ${socket.id} to ${data.to || 'all in room'}`);
});
// ------------------- Disconnect Cleanup -------------------
socket.on('disconnect', () => {
console.log(`
const roomId = socketToRoom[socket.id];
if (roomId && rooms[roomId]) {
// Remove the socket from the room's list
rooms[roomId] = rooms[roomId].filter(id => id !== socket.id);
// If the room is now empty, clean it up
if (rooms[roomId].length === 0) {
delete rooms[roomId];
} else {
// Notify remaining users that this user left
socket.to(roomId).emit('userLeft', socket.id);
}
}
delete socketToRoom[socket.id];
});
});
server.listen(PORT, () => {
console.log(`
console.log('Ensure your Flutter app uses this secure URL!');
});
} catch (e) {
console.error("
console.error("Please ensure 'cert.key' and 'cert.crt' are in the same directory as server.js.");
console.error("Details:", e.message);
}
а это мой файл webrtc.dart:
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:socket_io_client/socket_io_client.dart' as IO;
// --- CRITICAL FIX 3: Update the signaling server URL to HTTPS ---
// Must match the protocol and port of the Node.js server.
const String kServerUrl = 'https://localhost:3000';
// Configuration for STUN (and optional TURN) servers
final Map _iceServers = {
'iceServers': [
// Free Google STUN servers for NAT traversal
{'url': 'stun:stun.l.google.com:19302'},
{'url': 'stun:stun1.l.google.com:19302'},
],
};
// Configuration for media stream constraints (video resolution and audio)
final Map _mediaConstraints = {
'audio': true,
'video': {
'mandatory': {'minWidth': '640', 'minHeight': '480', 'minFrameRate': '30'},
'facingMode': 'user', // Use the front camera
},
};
// Extends ChangeNotifier to allow UI updates via Provider
class WebRTCService with ChangeNotifier {
late IO.Socket socket;
// Maps peer ID to its RTCPeerConnection instance (supports multiple peers)
final Map _peerConnections = {};
MediaStream? _localStream;
String? _roomId; // Private field
// FIX 1: Add a public getter for the room ID
String? get roomId => _roomId;
// Renderers
final RTCVideoRenderer localRenderer = RTCVideoRenderer();
final ValueNotifier remoteRenderersNotifier =
ValueNotifier({});
// Status flags
bool isMicEnabled = true;
bool isCameraEnabled = true;
// Constructor and Initialization
WebRTCService() {
_initRenderers();
}
Future _initRenderers() async {
await localRenderer.initialize();
}
// CRITICAL FIX 4: Add Socket Initialization here
void initSocket() {
try {
socket = IO.io(
kServerUrl,
IO.OptionBuilder()
.setTransports(['websocket'])
.disableAutoConnect()
// CRITICAL FIX 6: Add extraHeaders to bypass SSL verification
// for Flutter WebRTC's underlying HTTP client only in local dev.
// This is necessary because of the self-signed certificate.
.setExtraHeaders({
'sec-websocket-protocol': 'websocket',
// This is what the socket.io-client library does internally
'Host': 'localhost:3000',
})
// IMPORTANT: For Flutter Desktop/Mobile, the following would
// be required, but for Flutter Web, the browser context must be
// configured via the .transportOptions property.
// Since we cannot directly access the underlying HTTP client
// in Flutter Web, we rely on the browser's trust mechanism
// or use .setTransports and .setExtraHeaders as a workaround.
// However, the most robust fix for the HandshakeException
// in Dart is often forcing the SSL bypass at the client level.
// Let's use the `extraHeaders` approach first. If it fails,
// we must use the `badCertificateCallback` (only on mobile/desktop,
// which is impossible on Flutter Web).
// Let's try the `transportOptions` for a more explicit Web fix.
.setTransportOptions({
'extraHeaders': {'Host': 'localhost:3000'},
'rejectUnauthorized':
false, // This is the key setting for bypassing trust, but mainly for mobile/desktop.
})
.build(),
);
_addSocketListeners();
socket.connect();
print('Attempting to connect to signaling server at $kServerUrl');
} catch (e) {
print('Socket connection error: $e');
}
}
void _addSocketListeners() {
// FIX 7: Use onConnectError for debugging connection handshake failures
socket.onConnectError((err) => print('
socket.onConnect((_) => print('
socket.onDisconnect((_) => print('
socket.onError((err) => print('
// 1. New user receives list of existing users. Create PC and OFFER to them.
socket.on('roomUsers', (users) {
print('Existing users in room: $users');
for (var userId in users) {
// isOfferer = true (We initiate the connection by sending an Offer)
_createPeerConnection(userId, true);
}
});
// 2. Existing user receives ID of a new user. Create PC and wait for OFFER.
socket.on('userJoined', (userId) {
print('New user joined: $userId. Preparing to Answer.');
// isOfferer = false (They will send the Offer; we prepare to Answer)
_createPeerConnection(userId, false);
});
// 3. Cleanup when a user leaves
socket.on('userLeft', (userId) {
print('User left: $userId. Cleaning up PC.');
_removePeerConnection(userId);
});
// 4. Handle signaling messages (SDP/ICE)
socket.on('signalingMessage', (data) {
final from = data['from'];
final message = data['message'];
if (_peerConnections.containsKey(from)) {
_handleSignalingMessage(from, message);
} else {
print('Received signaling message for unknown peer: $from');
}
});
}
// Step 1: Get local media (camera and mic)
Future _getLocalMedia() async {
try {
_localStream = await navigator.mediaDevices.getUserMedia(
_mediaConstraints,
);
localRenderer.srcObject = _localStream;
print('
} catch (e) {
print('
}
}
// Step 2: Join the room and start the signaling handshake
Future joinRoom(String roomId) async {
_roomId = roomId;
if (_localStream == null) {
await _getLocalMedia();
}
// CRITICAL FIX 5: Use the 'joinRoom' event on the new server
socket.emit('joinRoom', roomId);
notifyListeners();
}
// Step 3: Create RTCPeerConnection and start negotiation
Future _createPeerConnection(String peerId, bool isOfferer) async {
// 3a. Create the peer connection
final pc = await createPeerConnection(_iceServers);
_peerConnections[peerId] = pc;
// 3b. Add local tracks to the peer connection
_localStream?.getTracks().forEach((track) {
pc.addTrack(track, _localStream!);
});
// 3c. Setup remote video renderer
final remoteRenderer = RTCVideoRenderer();
await remoteRenderer.initialize();
remoteRenderersNotifier.value[peerId] = remoteRenderer;
remoteRenderersNotifier.notifyListeners();
// 3d. Listen for ICE candidates
pc.onIceCandidate = (candidate) {
if (candidate != null) {
// Send the candidate to the other peer via the signaling server
socket.emit('signalingMessage', {
'to': peerId,
'message': {
'type': 'candidate',
'sdpMid': candidate.sdpMid,
'sdpMLineIndex': candidate.sdpMLineIndex,
'candidate': candidate.candidate,
},
});
}
};
// 3e. Listen for remote media track addition
pc.onTrack = (event) {
if (event.streams.isNotEmpty && event.track.kind == 'video') {
// Attach the remote stream to the corresponding renderer
remoteRenderer.srcObject = event.streams[0];
remoteRenderersNotifier.notifyListeners();
print('
}
};
// 3f. If we are the offerer, create and send the Offer SDP
if (isOfferer) {
final offer = await pc.createOffer();
await pc.setLocalDescription(offer);
socket.emit('signalingMessage', {
'to': peerId,
'message': offer.toMap(), // Send SDP type: 'offer'
});
print('
}
}
// Step 4: Handle incoming signaling messages (SDP or ICE)
Future _handleSignalingMessage(
String peerId,
Map message,
) async {
final pc = _peerConnections[peerId];
if (pc == null) return;
final type = message['type'];
if (type == 'offer') {
// 4a. Receive Offer: set remote desc, create Answer, set local desc, send Answer
await pc.setRemoteDescription(
RTCSessionDescription(message['sdp'], type),
);
final answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
socket.emit('signalingMessage', {
'to': peerId,
'message': answer.toMap(), // Send SDP type: 'answer'
});
print('
} else if (type == 'answer') {
// 4b. Receive Answer: set remote desc
await pc.setRemoteDescription(
RTCSessionDescription(message['sdp'], type),
);
print('
} else if (type == 'candidate') {
// 4c. Receive ICE Candidate: add it
final candidate = RTCIceCandidate(
message['candidate'],
message['sdpMid'],
message['sdpMLineIndex'],
);
await pc.addCandidate(candidate);
print('
}
}
// Cleanup one peer connection
void _removePeerConnection(String peerId) {
_peerConnections[peerId]?.close();
_peerConnections.remove(peerId);
// Dispose and remove the remote renderer
remoteRenderersNotifier.value[peerId]?.dispose();
remoteRenderersNotifier.value.remove(peerId);
remoteRenderersNotifier.notifyListeners();
}
// ----------------- Hangup / toggles -----------------
void hangup() {
try {
// 1. Close all peer connections
_peerConnections.forEach((key, pc) => pc.close());
_peerConnections.clear();
// 2. Stop local tracks and streams
_localStream?.getTracks().forEach((t) => t.stop());
_localStream = null;
localRenderer.srcObject = null;
// 3. Clean up remote renderers
remoteRenderersNotifier.value.forEach(
(key, renderer) => renderer.dispose(),
);
remoteRenderersNotifier.value = {};
// 4. Notify server of leave (optional, but good practice)
if (_roomId != null) {
socket.emit('leaveRoom', _roomId);
_roomId = null;
}
// 5. Disconnect socket
socket.disconnect();
print('
notifyListeners();
} catch (e) {
print('
}
}
void dispose() {
hangup();
localRenderer.dispose();
super.dispose();
}
void toggleMic() {
if (_localStream == null) return;
isMicEnabled = !isMicEnabled;
for (var t in _localStream!.getAudioTracks()) {
t.enabled = isMicEnabled;
}
print('🎙 Mic: $isMicEnabled');
notifyListeners();
}
void toggleCam() {
if (_localStream == null) return;
isCameraEnabled = !isCameraEnabled;
for (var t in _localStream!.getVideoTracks()) {
t.enabled = isCameraEnabled;
}
print('
notifyListeners();
}
}
it is showing me this error in my frontend:
DebugService: Error serving requestsError: Unsupported operation: Cannot send Null
Подробнее здесь: https://stackoverflow.com/questions/798 ... ssues-rela
Мобильная версия