Files
rosetta-wss/src/main/java/io/g365sfu/SFU.java

305 lines
16 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package io.g365sfu;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import io.g365sfu.exception.SFUException;
import io.g365sfu.exception.SFUHandshakeException;
import io.g365sfu.net.Incoming;
import io.g365sfu.net.Outgoing;
import io.g365sfu.net.SfuSock;
import io.g365sfu.util.StrUtils;
import io.g365sfu.webrtc.ICECandidate;
import io.g365sfu.webrtc.SDPAnswer;
import io.g365sfu.webrtc.SDPOffer;
public class SFU {
private String serverAddress;
private String secretKey;
private SfuSock socket;
private HashMap<String, CompletableFuture<String>> pendingRoomCreations = new HashMap<>();
/**
* Комнаты которые принадлежат этому серверу SFU. Ключом является ID комнаты,
* а значением объект Room, который содержит информацию о комнате, ее участниках и связанном с ней сервере SFU.
*/
private HashMap<String, Room> rooms = new HashMap<>();
/**
* Потребитель для обработки входящих ICE-кандидатов от сервера SFU.
* Этот потребитель будет вызываться при получении сообщения от сервера SFU с кодом 0x04,
* содержащим информацию об ICE-кандидате для одного из участников комнаты.
*/
private Consumer<ICECandidate> onIceCandidate;
/**
* Потребитель для обработки входящих SDP Answer от сервера SFU.
* Этот потребитель будет вызываться при получении сообщения от сервера SFU с кодом 0x05,
* содержащим информацию об SDP Answer для одного из участников комнаты.
*/
private Consumer<SDPAnswer> onSdpAnswer;
private Consumer<SDPOffer> onSdpOffer;
/**
* Конструктор для создания объекта SFU, который будет использоваться для установления соединения с SFU сервером.
* @param serverAddress адрес SFU сервера в формате "host:port", например "sfu.example.com:8080"
* @param secretKey секретный ключ для аутентификации с SFU сервером, который должен быть согласован с настройками сервера.
*/
public SFU(String serverAddress, String secretKey) {
this.serverAddress = serverAddress;
this.secretKey = secretKey;
}
/**
* Установить соединение с SFU сервером и начать обмен рукопожатиями для аутентификации и установления безопасного канала связи.
* @throws URISyntaxException если адрес сервера имеет неправильный формат
* @throws InterruptedException если соединение было прервано во время попытки подключения
* @throws SFUException если не удалось установить соединение с SFU сервером или если соединение было установлено,
* но не открыто после подключения
* @throws TimeoutException не удалось обменяться рукопожатиями с SFU сервером в течение 30 секунд
* @throws ExecutionException если во время обмена рукопожатиями произошла ошибка выполнения
* @throws SFUHandshakeException если обмен рукопожатиями с SFU завершился неудачно (например, плохой ключ)
*/
public void connect() throws URISyntaxException, InterruptedException, SFUException, ExecutionException, TimeoutException, SFUHandshakeException {
this.socket = new SfuSock(this.serverAddress);
this.socket.setMessageConsumer(this::onMessage);
boolean connected = this.socket.connectBlocking(30, TimeUnit.SECONDS);
if(!connected){
throw new SFUException("Failed to connect to SFU server, read time out: " + this.serverAddress);
}
if(!this.socket.isOpen()) {
throw new SFUException("Connection to SFU server at " + this.serverAddress + " is not open");
}
boolean estabilished = this.socket.handshakeExchange(this.secretKey).get(30, TimeUnit.SECONDS);
if(!estabilished) {
throw new SFUHandshakeException("Failed to establish handshake with SFU server at " + this.serverAddress);
}
}
private void onMessage(ByteBuffer message) {
if(message.remaining() < 1) {
System.err.println("Received empty message from SFU server");
return;
}
byte packetId = message.get(0);
if(packetId == Incoming.ROOM_CREATE) {
/**
* Ответ на создание комнаты, который содержит ID созданной комнаты
*/
int roomIdLength = message.getInt();
byte[] roomIdBytes = new byte[roomIdLength];
message.get(roomIdBytes);
String roomId = new String(roomIdBytes).trim();
CompletableFuture<String> future = this.pendingRoomCreations.remove(roomId);
if(future != null) {
future.complete(roomId);
}
return;
}
if(packetId == Incoming.ICE_CANDIDATE) {
/**
* ICE-candidate от сервера SFU для одного из участников комнаты
*/
int roomidLength = message.getInt();
byte[] roomIdBytes = new byte[roomidLength];
message.get(roomIdBytes);
String roomId = new String(roomIdBytes).trim();
int peerIdLength = message.getInt();
byte[] peerIdBytes = new byte[peerIdLength];
message.get(peerIdBytes);
String peerId = new String(peerIdBytes).trim();
int candidateLength = message.getInt();
byte[] candidateBytes = new byte[candidateLength];
message.get(candidateBytes);
String candidate = new String(candidateBytes).trim();
ICECandidate iceCandidate = new ICECandidate(roomId, peerId, candidate);
if(this.onIceCandidate != null) {
this.onIceCandidate.accept(iceCandidate);
}
return;
}
if(packetId == Incoming.SDP_ANSWER) {
/**
* Ответ на Offer от сервера SFU, который содержит SDP Answer
*/
int roomidLength = message.getInt();
byte[] roomIdBytes = new byte[roomidLength];
message.get(roomIdBytes);
String roomId = new String(roomIdBytes).trim();
int peerIdLength = message.getInt();
byte[] peerIdBytes = new byte[peerIdLength];
message.get(peerIdBytes);
String peerId = new String(peerIdBytes).trim();
int sdpAnswerLength = message.getInt();
byte[] sdpAnswerBytes = new byte[sdpAnswerLength];
message.get(sdpAnswerBytes);
String sdpAnswer = new String(sdpAnswerBytes).trim();
SDPAnswer answer = new SDPAnswer(roomId, peerId, sdpAnswer);
if(this.onSdpAnswer != null) {
this.onSdpAnswer.accept(answer);
}
return;
}
if(packetId == Incoming.SDP_OFFER) {
/**
* Offer от сервера SFU для одного из участников комнаты при renegotiation
*/
int roomidLength = message.getInt();
byte[] roomIdBytes = new byte[roomidLength];
message.get(roomIdBytes);
String roomId = new String(roomIdBytes).trim();
int peerIdLength = message.getInt();
byte[] peerIdBytes = new byte[peerIdLength];
message.get(peerIdBytes);
String peerId = new String(peerIdBytes).trim();
int sdpOfferLength = message.getInt();
byte[] sdpOfferBytes = new byte[sdpOfferLength];
message.get(sdpOfferBytes);
String sdpOffer = new String(sdpOfferBytes).trim();
SDPOffer offer = new SDPOffer(roomId, peerId, sdpOffer);
if(this.onSdpOffer != null) {
this.onSdpOffer.accept(offer);
}
return;
}
}
/**
* Получить адрес SFU сервера, к которому установлено соединение
* @return адрес SFU сервера
*/
public String getServerAddress() {
return this.serverAddress;
}
/**
* Получить соединение к SFU серверу, если оно было установлено
* @return объект SfuSock, представляющий соединение к SFU серверу
*/
public SfuSock getConnection() {
return this.socket;
}
/**
* Проверить, установлено ли соединение с SFU сервером и открыто ли оно
* @return true, если соединение установлено и открыто, false в противном случае
*/
public boolean isOpen() {
return this.socket != null && this.socket.isOpen();
}
/**
* Создает комнату на сервере SFU для организации звонков между пользователями. Комната автоматически удаляется
* при выходе последнего участника из нее. Внутри комнаты пользователи могут обмениваться аудио и видео потоками, а сервер SFU
* будет эффективно их пересылать между участниками, минимизируя задержки и оптимизируя использование пропускной способности.
* @throws TimeoutException
* @throws ExecutionException
* @throws InterruptedException
* @internal Этот метод формирует пакет с кодом 0x02,
* за которым следует случайно сгенерированный идентификатор комнаты,
* и отправляет его на сервер SFU.
*/
public Room createRoom() throws InterruptedException, ExecutionException, TimeoutException {
String roomId = StrUtils.randomString(64);
/**
* 1 байт номер пакета, 4 байта длина ID комнаты, N байт ID комнаты
*/
ByteBuffer buffer = ByteBuffer.allocate(1 + 4 + roomId.getBytes().length);
CompletableFuture<String> future = new CompletableFuture<>();
this.pendingRoomCreations.put(roomId, future);
/**
* 0x02 - создание комнаты
*/
buffer.put(Outgoing.ROOM_CREATE);
buffer.putInt(roomId.getBytes().length);
buffer.put(roomId.getBytes());
buffer.flip();
this.socket.send(buffer);
String createdRoomId = future.get(30, TimeUnit.SECONDS);
Room room = new Room(createdRoomId, this, new HashSet<>());
this.rooms.put(createdRoomId, room);
return room;
}
/**
* Получить все комнаты на сервере
* @return комнаты на этом сервере
*/
public HashSet<Room> getRooms() {
return new HashSet<>(this.rooms.values());
}
/**
* Получить комнату по ее идентификатору
* @param roomId идентификатор комнаты
* @return объект Room, представляющий комнату с данным идентификатором, или null, если комната не найдена
*/
public Room getRoom(String roomId) {
return this.rooms.get(roomId);
}
/**
* Получить комнату, в которой участвует пользователь с данным идентификатором
* @param participantId идентификатор пользователя, который является участником комнаты
* @return объект Room, представляющий комнату, в которой участвует пользователь с данным идентификатором, или null, если такой комнаты не найдено
*/
public Room getRoomByParticipantId(String participantId) {
for(Room room : this.rooms.values()) {
if(room.containsParticipant(participantId)) {
return room;
}
}
return null;
}
/**
* Получает количество комнат на этом сервере
* @return возвращает количество комнат на сервере
*/
public int getRoomsCount() {
return this.rooms.size();
}
/**
* Устанавливает потребителя для обработки входящих ICE-кандидатов от сервера SFU.
* @param onIceCandidate потребитель, который будет вызываться при получении сообщения от сервера SFU с кодом 0x04,
* содержащим информацию об ICE-кандидате для одного из участников комнаты.
* Параметром будет объект ICECandidate, который содержит информацию о комнате, участнике и самом
* кандидате, необходимую для правильной маршрутизации данных между участниками звонка через сервер SFU.
*/
public void setIceConsumer(Consumer<ICECandidate> onIceCandidate) {
this.onIceCandidate = onIceCandidate;
}
/**
* Устанавливает потребителя для обработки входящих SDP Answer от сервера SFU.
* @param onSdpAnswer потребитель, который будет вызываться при получении сообщения от сервера SFU с кодом 0x05,
* содержащим информацию об SDP Answer для одного из участников комнаты.
* Параметром будет объект SDPAnswer, который содержит информацию о комнате, участнике и самом SDP Answer,
* необходимую для установления медиа-сессии между участником и сервером SFU.
*/
public void setAnswerConsumer(Consumer<SDPAnswer> onSdpAnswer) {
this.onSdpAnswer = onSdpAnswer;
}
/**
* Устанавливает потребителя для обработки входящих SDP Offer от сервера SFU при renegotiation.
* @param onSdpOffer потребитель, который будет вызываться при получении сообщения от сервера SFU с кодом 0x08,
* содержащим информацию об SDP Offer для одного из участников комнаты при renegotiation.
* Параметром будет объект SDPOffer, который содержит информацию о комнате, участнике и самом SDP Offer,
* необходимую для установления медиа-сессии между участником и сервером SFU.
*/
public void setOfferConsumer(Consumer<SDPOffer> onSdpOffer) {
this.onSdpOffer = onSdpOffer;
}
}