Работа с WEBRTC
Разбираюсь с webrtc и то как он работает.
using Connectify.Db.Model;
using GachiHubBackend.Hubs.Enums;
using GachiHubBackend.Repositories;
using GachiHubBackend.Repositories.Interfaces;
using GachiHubBackend.Services;
using GachiHubBackend.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using SignalRSwaggerGen.Attributes;
namespace GachiHubBackend.Hubs;
[Authorize]
[SignalRHub]
public class RoomHub : Hub
{
private readonly UserRepository _userRepository;
private readonly RoomRepository _roomRepository;
private readonly IRepository<Message> _messageRepository;
private readonly IRoomHubContextService _roomHubContextService;
public RoomHub(UserRepository userRepository, RoomRepository roomRepository, IRepository<Message> messagesRepository)
{
_userRepository = userRepository;
_roomRepository = roomRepository;
_messageRepository = messagesRepository;
_roomHubContextService = new RoomHubContextService(this, _userRepository);
}
public async Task SendOffer(object offer)
{
var currentUser = await _roomHubContextService.GetCurrentUserAsync();
var connectionIds = await _roomHubContextService.GetOtherUsersConnectionIdsInRoomAsync();
await Clients.Clients(connectionIds!).SendAsync(RoomHubEvent.ReceiveOffer.ToString(), new
{
offer,
fromUserId = currentUser!.Id,
});
}
public async Task SendAnswer(object answer, int targetUserId)
{
var currentUser = await _roomHubContextService.GetCurrentUserAsync();
var room = await _roomRepository.GetByIdAsync(currentUser!.Room!.Id);
var targetUser = room!.Users.FirstOrDefault(u => u.Id == targetUserId);
if (targetUser!.ConnectionId != null)
{
await Clients.Clients(targetUser.ConnectionId)
.SendAsync(RoomHubEvent.ReceiveAnswer.ToString(), new
{
answer,
fromUserId = currentUser!.Id,
});
}
}
public async Task SendCandidate(object candidate)
{
var currentUser = await _roomHubContextService.GetCurrentUserAsync();
var connectionIds = await _roomHubContextService.GetOtherUsersConnectionIdsInRoomAsync();
await Clients.Clients(connectionIds!)
.SendAsync(RoomHubEvent.ReceiveCandidate.ToString(), new
{
candidate,
fromUserId = currentUser!.Id,
});
}
public async Task SendMessage(string content)
{
var currentUser = await _roomHubContextService.GetCurrentUserAsync();
var connectionIds = await _roomHubContextService.GetOtherUsersConnectionIdsInRoomAsync();
var message = new Message()
{
Content = content,
Room = currentUser!.Room!,
From = currentUser,
SendAt = DateTime.Now,
};
await _messageRepository.AddAsync(message);
await Clients.Clients(connectionIds!).SendAsync(RoomHubEvent.ReceiveMessage.ToString(), new
{
From = currentUser.Login,
message.Content,
message.SendAt,
});
}
public async Task JoinRoom(int id)
{
var currentUser = await _roomHubContextService.GetCurrentUserAsync();
var room = await _roomRepository.GetByIdAsync(id);
if (room == null)
{
throw new HubException("Room not found");
}
await _roomRepository.AddUserToRoomAsync(room, currentUser!);
var connectionIds = room.Users.Select(u => u.ConnectionId).ToList();
await Clients.Clients(connectionIds!).SendAsync(RoomHubEvent.JoinedRoom.ToString(), currentUser, room);
await Clients.AllExcept(connectionIds!).SendAsync(RoomHubEvent.JoinedToOtherRoom.ToString(), room);
}
public async Task LeaveRoom()
{
var currentUser = await _roomHubContextService.GetCurrentUserAsync();
var room = await _roomRepository.GetByIdAsync(currentUser!.Room!.Id);
if (room == null)
{
throw new HubException("Room not found");
}
var user = room.Users.FirstOrDefault(u => u.ConnectionId == currentUser.ConnectionId);
if (user == null)
{
throw new HubException("User not found");
}
var connectionIds = (await _roomHubContextService.GetOtherUsersConnectionIdsInRoomAsync()).ToList();
user.Room = null;
user.RoomId = null;
await _userRepository.UpdateAsync(user);
await Clients.Clients(connectionIds!).SendAsync(RoomHubEvent.LeavedRoom.ToString(), currentUser, room);
await Clients.AllExcept(connectionIds!).SendAsync(RoomHubEvent.LeavedFromOtherRoom.ToString(), room);
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
var currentUser = await _roomHubContextService.GetCurrentUserAsync();
currentUser!.ConnectionId = null;
currentUser.RoomId = null;
currentUser.Room = null;
var room = await _roomRepository.GetRoomByOwnerIdAsync(currentUser!.Id);
if (room != null)
{
var connectionIds = room.Users.Select(u => u.ConnectionId);
await Clients.Clients(connectionIds!).SendAsync(RoomHubEvent.LeftedRoom.ToString(), currentUser!.Login);
}
await _userRepository.UpdateAsync(currentUser);
await base.OnDisconnectedAsync(exception);
}
public override async Task OnConnectedAsync()
{
var currentUser = await _roomHubContextService.GetCurrentUserAsync();
if (currentUser == null)
{
throw new UnauthorizedAccessException();
}
currentUser.ConnectionId = Context.ConnectionId;
await _userRepository.UpdateAsync(currentUser);
await base.OnConnectedAsync();
}
}
есть вот такой у меня хаб, для сигналинга с помощью SignalR. Суть в том, что я пытался с фронтендером реализовать общение множества пользователей в комнате, но все попытки не увенчались успехом. Ниже пример кода фронта для работы с хабом. Пишем все это дело на реакте.
import IRoom from "../../types/IRoom";
import IUser from "../../types/IUser";
import { connectionApi } from "./connectionApi";
// Define interfaces
export interface PeerConnection {
connection: RTCPeerConnection;
userId: number;
stream?: MediaStream;
}
const iceServers = [
{
urls: import.meta.env.VITE_TURN_SERVER_IP,
username: import.meta.env.VITE_TURN_SERVER_USERNAME,
credential: import.meta.env.VITE_TURN_SERVER_CREDENTIAL,
},
];
export class PeerConnectionService {
peerConnections: PeerConnection[] = [];
currentUser: IUser | null = null;
createNewPeerConnection = async (userId: number) => {
if (userId == this.currentUser?.id) return;
console.log(
`[PeerConnectionService] Creating new peer connection for user ID: ${userId}`
);
const newPeerConnection = new RTCPeerConnection({ iceServers });
console.log(`[PeerConnectionService] ICE servers configured:`, iceServers);
if (userId) {
console.log(`[PeerConnectionService] Requesting user media (audio)`);
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
});
console.log(`[PeerConnectionService] Media stream obtained successfully`);
stream.getTracks().forEach((track) => {
console.log(
`[PeerConnectionService] Adding track to peer connection: ${track.kind}`
);
newPeerConnection.addTrack(track, stream);
});
this.createOffer(newPeerConnection);
console.log(
`[PeerConnectionService] Adding new peer connection to collection for user ID: ${userId}`
);
this.peerConnections = [
...this.peerConnections,
{ connection: newPeerConnection, userId, stream: stream },
];
}
return newPeerConnection;
};
createOffer = async (peerConnection: RTCPeerConnection) => {
console.log(`[PeerConnectionService] Creating offer for peer connection`);
const offer = await peerConnection.createOffer();
console.log(`[PeerConnectionService] Setting local description (offer)`);
await peerConnection.setLocalDescription(offer);
console.log(`[PeerConnectionService] Sending offer via SignalR`);
connectionApi.connection?.invoke("SendOffer", offer);
};
removePeerConnection = (userId: number) => {
console.log(
`[PeerConnectionService] Removing peer connection for user ID: ${userId}`
);
const connectionToRemove = this.peerConnections.find(
(connection) => connection.userId === userId
);
if (connectionToRemove) {
console.log(
`[PeerConnectionService] Closing connection for user ID: ${userId}`
);
connectionToRemove.connection.close();
} else {
console.log(
`[PeerConnectionService] No connection found for user ID: ${userId}`
);
}
this.peerConnections = this.peerConnections.filter(
(connection) => connection.userId !== userId
);
console.log(
`[PeerConnectionService] Remaining connections: ${this.peerConnections.length}`
);
};
getPeerConnectionByUserId = async (userId: number) => {
console.log(
`[PeerConnectionService] Looking for peer connection with user ID: ${userId}`
);
const connection = this.peerConnections.find(
(peer) => peer.userId == userId
);
console.log(
`[PeerConnectionService] Connection ${
connection ? "found" : "not found"
} for user ID: ${userId}`
);
return connection;
};
createPeerConnections = (room: IRoom, currentUser: IUser) => {
this.currentUser = currentUser;
console.log(
`[PeerConnectionService] Creating peer connections for room: ${room.title} (ID: ${room.id})`
);
console.log(
`[PeerConnectionService] Current user: ${currentUser.login} (ID: ${currentUser.id})`
);
console.log(
`[PeerConnectionService] Total users in room: ${room.users.length}`
);
const otherUsers = room.users.filter((user) => user.id != currentUser.id);
console.log(
`[PeerConnectionService] Creating connections for ${otherUsers.length} other users`
);
otherUsers.map((user) => {
console.log(
`[PeerConnectionService] Creating connection for user: ${user.login} (ID: ${user.id})`
);
this.createNewPeerConnection(user.id);
});
};
removeAllPeerConnections = () => {
console.log(
`[PeerConnectionService] Removing all peer connections (total: ${this.peerConnections.length})`
);
this.peerConnections.map((connection: PeerConnection) => {
console.log(
`[PeerConnectionService] Removing connection for user ID: ${connection.userId}`
);
this.removePeerConnection(connection.userId);
});
};
subscribeOnEvents = () => {
console.log(`[PeerConnectionService] Subscribing to SignalR events`);
connectionApi.connection?.on(
"ReceiveOffer",
async (response: {
offer: RTCSessionDescriptionInit;
fromUserId: number;
}) => {
console.log(
`[PeerConnectionService] Received offer from user ID: ${response.fromUserId}`
);
const userConnection = await this.getPeerConnectionByUserId(
response.fromUserId
);
if (userConnection) {
console.log(
`[PeerConnectionService] Setting remote description (offer) from user ID: ${response.fromUserId}`
);
await userConnection.connection.setRemoteDescription(
new RTCSessionDescription(response.offer)
);
console.log(
`[PeerConnectionService] Creating answer for user ID: ${response.fromUserId}`
);
const answer = await userConnection.connection.createAnswer();
console.log(
`[PeerConnectionService] Setting local description (answer)`
);
await userConnection.connection?.setLocalDescription(answer);
console.log(
`[PeerConnectionService] Sending answer to user ID: ${response.fromUserId}`
);
connectionApi.connection?.invoke(
"SendAnswer",
answer,
response.fromUserId
);
} else {
console.log(
`[PeerConnectionService] No connection found for user ID: ${response.fromUserId}, cannot process offer`
);
}
}
);
connectionApi.connection?.on(
"ReceiveAnswer",
async (response: {
answer: RTCSessionDescriptionInit;
fromUserId: number;
}) => {
console.log(
`[PeerConnectionService] Received answer from user ID: ${response.fromUserId}`
);
const userConnection = await this.getPeerConnectionByUserId(
response.fromUserId
);
if (userConnection) {
console.log(
`[PeerConnectionService] Setting remote description (answer) from user ID: ${response.fromUserId}`
);
await userConnection.connection.setRemoteDescription(
new RTCSessionDescription(response.answer)
);
console.log(
`[PeerConnectionService] Setting up ICE candidate handler for user ID: ${response.fromUserId}`
);
userConnection.connection.onicecandidate = (event) => {
if (event.candidate) {
console.log(
`[PeerConnectionService] ICE candidate generated, sending to user ID: ${response.fromUserId}`
);
connectionApi.connection?.invoke(
"SendCandidate",
event.candidate
);
}
};
userConnection.connection.onnegotiationneeded = () => {
console.log("onnegotiationneeded");
if (userConnection.connection.signalingState != "stable") return;
};
console.log(
`[PeerConnectionService] Setting up track handler for user ID: ${response.fromUserId}`
);
userConnection.connection.ontrack = (event) => {
console.log(
`[PeerConnectionService] Track received from user ID: ${response.fromUserId}`
);
const remoteAudio = document.getElementById(
`remoteAudio-${userConnection.userId}`
) as HTMLAudioElement;
if (remoteAudio && event.streams[0]) {
console.log(
`[PeerConnectionService] Setting remote stream to audio element for user ID: ${userConnection.userId}`
);
remoteAudio.srcObject = event.streams[0];
remoteAudio.volume = 100 / 100; // Устанавливаем начальную громкость
} else {
console.log(
`[PeerConnectionService] Could not find audio element or stream for user ID: ${userConnection.userId}`
);
}
};
} else {
console.log(
`[PeerConnectionService] No connection found for user ID: ${response.fromUserId}, cannot process answer`
);
}
}
);
connectionApi.connection?.on(
"ReceiveCandidate",
async (response: { candidate: RTCIceCandidate; fromUserId: number }) => {
console.log(
`[PeerConnectionService] Received ICE candidate from user ID: ${response.fromUserId}`
);
try {
const userConnection = await this.getPeerConnectionByUserId(
response.fromUserId
);
if (userConnection?.connection.remoteDescription) {
console.log(
`[PeerConnectionService] Adding ICE candidate from user ID: ${response.fromUserId}`
);
await userConnection.connection.addIceCandidate(
new RTCIceCandidate(response.candidate)
);
console.log(
`[PeerConnectionService] ICE candidate added successfully`
);
} else {
console.log(
`[PeerConnectionService] Cannot add ICE candidate, no remote description set for user ID: ${response.fromUserId}`
);
}
} catch (error) {
console.error(
`[PeerConnectionService] Error adding ICE candidate from user ID: ${response.fromUserId}:`,
error
);
}
}
);
connectionApi.connection?.on("JoinedRoom", (user: IUser, room: IRoom) => {
console.log(
`[PeerConnectionService] User with ID: ${user.id} Joined to room: ${room.id}`
);
this.createNewPeerConnection(user.id);
});
console.log(
`[PeerConnectionService] All SignalR event handlers registered`
);
};
}
export const peerConnectionService = new PeerConnectionService();
У нас возникают ошибки рода:
[2025-05-01T09:05:18.113Z] Error: A callback for the method 'receiveanswer' threw error 'InvalidStateError: Cannot set remote answer in state stable'.
WebRTC: ICE failed, add a TURN server and see about:webrtc for more details
Объясните, может мы что-то не понимаем?