Хотите создать приложение, подобное Google Meet (клон Google Meet), но столкнулись с проблемами, связанными со стеком теJavascript

Форум по Javascript
Ответить
Anonymous
 Хотите создать приложение, подобное Google Meet (клон Google Meet), но столкнулись с проблемами, связанными со стеком те

Сообщение Anonymous »

Привет, мне поручено создать клон Google Meet, но требования к технологическому стеку не являются устоявшимися. они хотят, чтобы база данных была на Postgre SQL, серверная часть была на Javscript (достаточно справедливо), а внешний интерфейс - на флаттере. теперь мне удалось настроить интерфейс с помощью флаттера, и я создал правильный файл сервера для бэкэнда, а также использовал API-интерфейсы webrtc и веб-сокетов (socket.io) для установления соединения, но каким-то образом мой флаттер неправильно соединяется с моим внутренним сервером и не может взаимодействовать с моим внутренним сервером. в том смысле, что когда я впервые запускаю свой сервер, а затем, когда я запускаю свой интерфейс, в моей внутренней консоли я должен видеть запрос от интерфейса flutter, который я не могу видеть. Я застрял в этом вопросе последние 15 дней. также, если кто-нибудь знает, как подключить Postgre SQL к JavaScript, пожалуйста, помогите мне. Мне нужно руководство, и я хочу найти выход из этой ситуации и, по крайней мере, иметь возможность установить правильное соединение один-к-одному с помощью webrts, если не один-ко-многим на данный момент. Пожалуйста, подскажите мне, как мне это сделать.
это мой код 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(`✅ User connected: ${socket.id}`);

// ------------------- 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(`❌ User disconnected: ${socket.id}`);
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(`🔥 Signaling Server running securely on https://localhost:${PORT}`);
console.log('Ensure your Flutter app uses this secure URL!');
});
} catch (e) {
console.error("🔴 ERROR: Failed to load HTTPS certificate files.");
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 Connect Error: $err'));
socket.onConnect((_) => print('✅ Socket Connected!'));
socket.onDisconnect((_) => print('❌ Socket Disconnected!'));
socket.onError((err) => print('⚠️ Socket Error: $err'));

// 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('✅ Local media obtained.');
} catch (e) {
print('⚠️ Error getting local media: $e');
}
}

// 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('📺 Received remote video track from $peerId');
}
};

// 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('➡️ Sent Offer to $peerId');
}
}

// 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('⬅️ Received Offer and Sent Answer to $peerId');
} else if (type == 'answer') {
// 4b. Receive Answer: set remote desc
await pc.setRemoteDescription(
RTCSessionDescription(message['sdp'], type),
);
print('⬅️ Received Answer from $peerId');
} else if (type == 'candidate') {
// 4c. Receive ICE Candidate: add it
final candidate = RTCIceCandidate(
message['candidate'],
message['sdpMid'],
message['sdpMLineIndex'],
);
await pc.addCandidate(candidate);
print('🧊 Added ICE candidate from $peerId');
}
}

// 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('📴 Hangup & socket disconnected');
notifyListeners();
} catch (e) {
print('⚠️ hangup error: $e');
}
}

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('📷 Camera: $isCameraEnabled');
notifyListeners();
}
}

it is showing me this error in my frontend:

DebugService: Error serving requestsError: Unsupported operation: Cannot send Null
⚠️ Socket Error: {msg: websocket error, desc: [object Event], type: TransportError}
❌ Socket Connect Error: {msg: websocket error, desc: [object Event], type: TransportError}


Подробнее здесь: https://stackoverflow.com/questions/798 ... ssues-rela
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Javascript»