Реализация звонков на сервере #15
@@ -19,7 +19,11 @@ public enum NetworkSignalType {
|
|||||||
/**
|
/**
|
||||||
* Сигнал для завершения звонка, указывает на то, что звонок завершен и участники должны прекратить обмен данными
|
* Сигнал для завершения звонка, указывает на то, что звонок завершен и участники должны прекратить обмен данными
|
||||||
*/
|
*/
|
||||||
END_CALL(3);
|
END_CALL(3),
|
||||||
|
/**
|
||||||
|
* Создание комнаты
|
||||||
|
*/
|
||||||
|
CREATE_ROOM(4);
|
||||||
|
|
||||||
private final int code;
|
private final int code;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package im.rosetta.packet.runtime;
|
||||||
|
|
||||||
|
public enum NetworkWebRTCType {
|
||||||
|
/**
|
||||||
|
* Оффер
|
||||||
|
*/
|
||||||
|
OFFER(0),
|
||||||
|
/**
|
||||||
|
* Ответ на оффер
|
||||||
|
*/
|
||||||
|
ANSWER(1),
|
||||||
|
/**
|
||||||
|
* ICE кандидат
|
||||||
|
*/
|
||||||
|
ICE_CANDIDATE(2);
|
||||||
|
|
||||||
|
private final int code;
|
||||||
|
|
||||||
|
NetworkWebRTCType(int code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NetworkWebRTCType fromCode(int code) {
|
||||||
|
for (NetworkWebRTCType type : NetworkWebRTCType.values()) {
|
||||||
|
if (type.code == code) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unknown NetworkWebRTCType code: " + code);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,9 +3,18 @@ package im.rosetta.service.services;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import im.rosetta.client.ClientManager;
|
||||||
import im.rosetta.logger.Logger;
|
import im.rosetta.logger.Logger;
|
||||||
import im.rosetta.logger.enums.Color;
|
import im.rosetta.logger.enums.Color;
|
||||||
|
import im.rosetta.packet.Packet27WebRTC;
|
||||||
|
import im.rosetta.packet.runtime.NetworkWebRTCType;
|
||||||
|
import io.g365sfu.Room;
|
||||||
import io.g365sfu.SFU;
|
import io.g365sfu.SFU;
|
||||||
|
import io.g365sfu.webrtc.ICECandidate;
|
||||||
|
import io.g365sfu.webrtc.RTCIceServer;
|
||||||
|
import io.g365sfu.webrtc.SDPAnswer;
|
||||||
|
import io.g365sfu.webrtc.SDPOffer;
|
||||||
|
import io.orprotocol.ProtocolException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Это сервис который взаимодействуют с SFU серверами для организации звонков между пользователями.
|
* Это сервис который взаимодействуют с SFU серверами для организации звонков между пользователями.
|
||||||
@@ -14,9 +23,44 @@ public class ForwardUnitService {
|
|||||||
|
|
||||||
private Logger logger;
|
private Logger logger;
|
||||||
private Set<SFU> sfuConnections = new HashSet<>();
|
private Set<SFU> sfuConnections = new HashSet<>();
|
||||||
|
private ClientManager clientManager;
|
||||||
|
private Set<RTCIceServer> turnServers = new HashSet<>();
|
||||||
|
|
||||||
public ForwardUnitService(Logger logger) {
|
public ForwardUnitService(Logger logger, ClientManager clientManager) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
|
this.clientManager = clientManager;
|
||||||
|
this.readAllTurnServers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Читаем все TURN сервера из переменной окружения и сохраняем их для дальнейшего
|
||||||
|
* использования при организации звонков через SFU серверы.
|
||||||
|
*/
|
||||||
|
private void readAllTurnServers() {
|
||||||
|
String turnServersEnv = System.getenv("TURN_SERVERS");
|
||||||
|
if(turnServersEnv == null || turnServersEnv.isEmpty()) {
|
||||||
|
this.logger.info(Color.YELLOW + "No TURN servers configured, skipping TURN servers boot");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String[] turnServers = turnServersEnv.split(",");
|
||||||
|
for(String turnServer : turnServers) {
|
||||||
|
String[] parts = turnServer.split("@");
|
||||||
|
if(parts.length != 2) {
|
||||||
|
this.logger.error(Color.RED + "Invalid TURN server configuration: " + turnServer);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String address = parts[0];
|
||||||
|
String credentialsPart = parts[1];
|
||||||
|
String[] credentialsParts = credentialsPart.split(":");
|
||||||
|
if(credentialsParts.length != 2) {
|
||||||
|
this.logger.error(Color.RED + "Invalid TURN server credentials configuration: " + credentialsPart);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String username = credentialsParts[0];
|
||||||
|
String credential = credentialsParts[1];
|
||||||
|
RTCIceServer iceServer = new RTCIceServer(address, username, credential);
|
||||||
|
this.turnServers.add(iceServer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,6 +89,22 @@ public class ForwardUnitService {
|
|||||||
try {
|
try {
|
||||||
SFU connection = new SFU(address, secretKey);
|
SFU connection = new SFU(address, secretKey);
|
||||||
connection.connect();
|
connection.connect();
|
||||||
|
connection.setIceConsumer(arg0 -> {
|
||||||
|
try {
|
||||||
|
onIceCandidate(arg0);
|
||||||
|
} catch (ProtocolException e) {
|
||||||
|
this.logger.error(Color.RED + "Failed to retranslate ICE-candidate from SFU server: " + address + ", error: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connection.setAnswerConsumer(arg0 -> {
|
||||||
|
try{
|
||||||
|
onSdpAnswer(arg0);
|
||||||
|
}catch(ProtocolException e){
|
||||||
|
this.logger.error(Color.RED + "Failed to retranslate SDP answer from SFU server: " + address + ", error: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
this.sfuConnections.add(connection);
|
this.sfuConnections.add(connection);
|
||||||
this.logger.info(Color.GREEN + "Successfully connected to SFU server: " + address);
|
this.logger.info(Color.GREEN + "Successfully connected to SFU server: " + address);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -52,5 +112,94 @@ public class ForwardUnitService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onSdpAnswer(SDPAnswer sdpAnswer) throws ProtocolException {
|
||||||
|
String participantId = sdpAnswer.getParticipantId();
|
||||||
|
Packet27WebRTC packet = new Packet27WebRTC();
|
||||||
|
packet.setSdpOrCandidate(sdpAnswer.getSdp());
|
||||||
|
packet.setType(NetworkWebRTCType.ANSWER);
|
||||||
|
this.clientManager.sendPacketToAuthorizedPK(participantId, packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполняется когда сервер SFU отправляет ICE-кандидата для одного из участников комнаты.
|
||||||
|
* @param iceCandidate объект ICECandidate,
|
||||||
|
* который содержит информацию о комнате, участнике и самом кандидате,
|
||||||
|
* которая необходима для правильной маршрутизации данных между участниками звонка через сервер SFU.
|
||||||
|
* @throws ProtocolException
|
||||||
|
*/
|
||||||
|
public void onIceCandidate(ICECandidate iceCandidate) throws ProtocolException {
|
||||||
|
String publicKey = iceCandidate.getParticipantId();
|
||||||
|
Packet27WebRTC packet = new Packet27WebRTC();
|
||||||
|
packet.setSdpOrCandidate(iceCandidate.getCandidate());
|
||||||
|
packet.setType(NetworkWebRTCType.ICE_CANDIDATE);
|
||||||
|
this.clientManager.sendPacketToAuthorizedPK(publicKey, packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполняется когда сервер SFU отправляет SDP оффер для одного из участников комнаты.
|
||||||
|
* @param sdpOffer объект SDPOffer,
|
||||||
|
* который содержит информацию о комнате, участнике и самом оффере,
|
||||||
|
* которая необходима для правильной маршрутизации данных между участниками звонка через сервер SFU.
|
||||||
|
* @throws ProtocolException
|
||||||
|
*/
|
||||||
|
public void onSdpOffer(SDPOffer sdpOffer) throws ProtocolException {
|
||||||
|
String participantId = sdpOffer.getParticipantId();
|
||||||
|
Packet27WebRTC packet = new Packet27WebRTC();
|
||||||
|
packet.setSdpOrCandidate(sdpOffer.getSdp());
|
||||||
|
packet.setType(NetworkWebRTCType.OFFER);
|
||||||
|
this.clientManager.sendPacketToAuthorizedPK(participantId, packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает комнату в которой сейчас находится пользователь
|
||||||
|
* @param participantId идентификатор пользователя на сервере SFU
|
||||||
|
* @return комната Room если найдена, иначе null
|
||||||
|
*/
|
||||||
|
public Room getRoomByParticipantId(String participantId) {
|
||||||
|
for(SFU sfu : this.sfuConnections){
|
||||||
|
Room room = sfu.getRoomByParticipantId(participantId);
|
||||||
|
if(room != null){
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Автоматически выбирает сервер для создания комнаты, и создает в нем комнату
|
||||||
|
* @return комната
|
||||||
|
*/
|
||||||
|
public Room createRoom() {
|
||||||
|
if(this.sfuConnections.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
SFU selectedSfu = null;
|
||||||
|
int minRooms = Integer.MAX_VALUE;
|
||||||
|
for(SFU sfu : this.sfuConnections) {
|
||||||
|
int roomsCount = sfu.getRoomsCount();
|
||||||
|
if(roomsCount < minRooms) {
|
||||||
|
minRooms = roomsCount;
|
||||||
|
selectedSfu = sfu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(selectedSfu != null) {
|
||||||
|
try{
|
||||||
|
return selectedSfu.createRoom();
|
||||||
|
}catch(Exception e){
|
||||||
|
this.logger.error(Color.RED + "Failed to create room on SFU server: " + selectedSfu.getServerAddress() + ", error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить список TURN серверов, которые могут быть использованы для обмена кандидатами между участниками звонка через сервер SFU.
|
||||||
|
* @return список серверов для RTC
|
||||||
|
*/
|
||||||
|
public Set<RTCIceServer> getTurnServers() {
|
||||||
|
return turnServers;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user