Compare commits

..

24 Commits

Author SHA1 Message Date
d4125846ec Merge pull request 'Видеосообщения (кружочки)' (#19) from dev into main
All checks were successful
Build rosetta-wss / build (push) Successful in 1m57s
Reviewed-on: #19
2026-04-11 15:56:05 +00:00
RoyceDa
f87198c054 Видеосообщения (кружочки) 2026-04-10 17:36:31 +02:00
RoyceDa
58fe3c409d Новый тип вложений - голосовое сообщение
All checks were successful
Build rosetta-wss / build (push) Successful in 1m51s
2026-04-10 17:28:32 +02:00
RoyceDa
bdc44f36f0 Push READ теперь с пустым уведомлением для точного пробуждения
All checks were successful
Build rosetta-wss / build (push) Successful in 1m57s
2026-04-08 22:11:52 +02:00
RoyceDa
145aaf8288 Исправление кика с сервера при гонке с закрытием RTC на SFU
All checks were successful
Build rosetta-wss / build (push) Successful in 1m31s
2026-04-07 15:24:45 +02:00
RoyceDa
ddcd08aeae END_CALL для остальных устройств принявшего звонок пользователя 2026-04-07 15:16:00 +02:00
RoyceDa
435d6fefa8 Защита от подмены src в пакетах 2026-04-07 15:10:27 +02:00
RoyceDa
986cd765d8 Дополнительные комментарии 2026-04-07 14:56:56 +02:00
RoyceDa
a9c4612a72 Исправление недохода END_CALL к вызываемому пользователю если он авторизован 2026-04-07 14:55:39 +02:00
RoyceDa
bc0a64f450 Исправление зависших сессий на сервере при преждевременном RTC close 2026-04-07 14:43:32 +02:00
faaffd86d0 Обновить src/main/java/im/rosetta/service/dispatch/push/dispatchers/VoIPApns.java
All checks were successful
Build rosetta-wss / build (push) Successful in 1m45s
2026-04-05 21:03:41 +00:00
b1d3416684 Обновить src/main/java/im/rosetta/service/dispatch/push/dispatchers/VoIPApns.java
All checks were successful
Build rosetta-wss / build (push) Successful in 2m14s
2026-04-05 14:46:34 +00:00
RoyceDa
7eacaa6298 Расширены коды ошибок
All checks were successful
Build rosetta-wss / build (push) Successful in 3m42s
2026-04-04 18:59:08 +02:00
RoyceDa
76a007ff42 Фикс киков с сервера
All checks were successful
Build rosetta-wss / build (push) Successful in 3m30s
2026-04-04 18:49:39 +02:00
RoyceDa
20dd5933d9 Merge branch 'dev' into main
All checks were successful
Build rosetta-wss / build (push) Successful in 4m14s
2026-04-04 18:35:51 +02:00
RoyceDa
939a4d55f4 Обновление протокола для неавторизованных звонков 2026-04-04 18:31:05 +02:00
RoyceDa
c59687564e Merge branch 'main' of https://git.rosetta.im/Rosetta/rosetta-wss into main
All checks were successful
Build rosetta-wss / build (push) Successful in 1m45s
2026-04-02 18:29:39 +02:00
RoyceDa
9b8d8cd863 Merge branch 'dev' into main 2026-04-02 18:29:36 +02:00
RoyceDa
3c6b2e0e71 Обновление протокола звонков 2026-04-02 18:11:24 +02:00
398b869e59 Методы в SignalPeer без авторизации 2026-04-02 00:31:18 +02:00
69c6bc63b3 DEVELOPMENT_APNS_HOST
All checks were successful
Build rosetta-wss / build (push) Successful in 1m43s
2026-04-01 14:51:05 +00:00
RoyceDa
aa6361c253 Merge branch 'main' of https://git.rosetta.im/Rosetta/rosetta-wss into main
All checks were successful
Build rosetta-wss / build (push) Successful in 1m46s
2026-04-01 16:08:18 +02:00
RoyceDa
b07f76ba1e Добавлены call-pushes 2026-04-01 16:06:35 +02:00
d4448a629b Удалить serviceAccount.json
Some checks failed
Build rosetta-wss / build (push) Has been cancelled
2026-04-01 13:01:03 +00:00
22 changed files with 733 additions and 144 deletions

4
.env
View File

@@ -14,6 +14,10 @@ SDU_SERVERS=http://10.211.55.2:7777
#Firebase Credentials
FIREBASE_CREDENTIALS_PATH=serviceAccount.json
#Apple APNS
APNS_KEY_PATH=voip.p12
APNS_P12_PASSWORD=rosetta1
IOS_BUNDLE_ID=com.rosetta.dev
#Каждые сколько дней будет очищаться буфер (максимальная дистанция синхронизации сообщений)
BUFFER_CLEANUP_DAYS=7

3
.gitignore vendored
View File

@@ -34,6 +34,9 @@ target
.settings
.project
.classpath
serviceAccount.json
build/*.p12
*.p12
build/.env
build/.env*

View File

@@ -46,6 +46,12 @@
<artifactId>jakarta.persistence-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>com.eatthepath</groupId>
<artifactId>pushy</artifactId>
<version>0.15.4</version>
</dependency>
</dependencies>
<build>

View File

@@ -1,13 +0,0 @@
{
"type": "service_account",
"project_id": "rosetta-messanger-dev",
"private_key_id": "69822675ff0a49a8ce0bd147dbab3ef432963485",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC/iYhrgWfi/ecP\nIs7Y0u7MueE/leU0QZF3V6hIcs1Iq1/CSUYcszv+YXuKItfwi0pYtntPmnCtdKnv\nFgZZN/9I0/AjFPlggqEXBH223FjsAfoNTk8a0PXyF8oOG0u2pN34t20nxXFSYars\n/6yJMKOBdaJ+L51dlBeCj40UFm3zQSVpejRkspWmT5Qz7kWh2e8yj6ukPyXEfrrD\n8jmM6uQeKwzlmfjD8YbL7R2m+lE1hzkoFCo0PrqMQYIIpAMT9n9meCk/LrQMIUnA\n+zICsTffFwbFzmJ2A022po3ObL4dUYgqGrPs5DqjxwTF4mI0g2dkPvoxpUKkVzC7\nHzzOYTwRAgMBAAECggEAGIacDSa1rpdpfK2DLrjFHY+YXAPYehpX7fVaVeXmtcKa\nppkAXSYcQq9T2jUqUSGyOZgq7l9Yb0rhhFJd1L3QDBDLY9/zK3GpU/ZeF0oG1DhP\n6bVDJCwUBNXD/eNujKrMG5AimOBLlEtvpMemXa4jOa1eSxR+F4tM1AndUZr/pZFT\nIdPBrId3WemkcWH2b1R4RLVu8zqci1x+nWvL4Gt8O/gDpDzlmV2Disvpf300a5wU\ngIKcOI2H6vouOwL9ltMPTBXdpDIC64Q8zvK3973d3Fqx2Mt2mO4ZgylFpk2nmAm6\n3wknrkJDE+Z/H7XmdwpMhRmmyku3vuqsDwmy7BEGGwKBgQD03oOJ6eb9kG6JX830\nRH/Vhh3NlpVEzuHzRA8WznsX5WaeyMF9NWo4Lh9ifBxkRAQKftEC0CR5GZp3/895\nQuoMMbrrklWWkVRwCZWYQBquRXNHbTPj6ztvHaZZrw8HhemZWtOF89T5f8Kn4LcD\nFwR8kIqXeLxeTbfc2hQXZvP7vwKBgQDIPmhM2Td/Ni4Mbe3wGmOyS0RweK5qcKBF\n32N1rCr8smrD1irg+w9Qrmz2cX3udtVJDRLz4ctSmiEPs+UrzuyG6yTbdv7bh8AN\nsqHOdc4GRqGsB/jfXagVjRMSeObQk5fR03tiPLP15J+lpoEsjwUzw7L3Ioacaq62\nqSi87br8LwKBgQDu/uMJz4bJg5evcxeUSusuH5mlGE0WfIniIlJL4zoXR6qSXcUk\nDOdgb/vn5tTbM9tx1vbvNPH0VH4Ek2QPqbTANCWJWSk6LRxpwaEFmcOwxk5Or5IO\n6X/34suDCy6zHAu0xwZe3m7HGeCGc/iMBoI1heoPDyNjM5257AviD3UhBwKBgCWS\nmC17QI+NEfzhD5lSykwlFVVpP4jXUytpLBdjU7mQnLncULVgRlJkOCvRxchd4c1Q\nN7MtNeJs6zEwFxsuO3FhY8wOOunkQeQQFY5QynShAiruYANBZo2Mp/x6VQzj9MO5\nQ9h9/WJxIIeLg4dh2p8I5Ga8wrdMyTWa7frtPH2fAoGALUkccsu4Mcws55Xy1aHH\nNETI6CRQpaU4v6jNhuuY4g6jp5ScaqkpuuzHOYorpmdC3180YXzPn2pwKeYWa0BA\nTjR8z603bCpdRfpic1UvYi96jwAcNiJTAGGVwmyYopIFBdHphfGS3hWZlYfonVps\n+k87j3WGwOiUGqoKSGJi1cw=\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-fbsvc@rosetta-messanger-dev.iam.gserviceaccount.com",
"client_id": "115421173243098464717",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40rosetta-messanger-dev.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

View File

@@ -1,5 +1,6 @@
package im.rosetta;
import im.rosetta.calls.CallManager;
import im.rosetta.client.ClientManager;
import im.rosetta.client.OnlineManager;
import im.rosetta.event.EventManager;
@@ -82,6 +83,7 @@ public class Boot {
private OnlineManager onlineManager;
private BufferCleanupService bufferCleanupService;
private ForwardUnitService forwardUnitService;
private CallManager callManager;
/**
* Конструктор по умолчанию, использует порт 3000 для сервера
@@ -104,7 +106,8 @@ public class Boot {
port,
30
), packetManager, this.serverAdapter);
this.clientManager = new ClientManager(server);
this.clientManager = new ClientManager(this.server);
this.callManager = new CallManager(this.clientManager);
/**
* Каждые сколько дней будет очищаться буфер (это влияет на синхронизацию сообщений, так
* как при синхронизации клиент запрашивает пакеты из буфера за последние 7 дней, если этот параметр будет меньше,
@@ -113,7 +116,7 @@ public class Boot {
int cleanupEveryDays = System.getenv("BUFFER_CLEANUP_DAYS") != null ?
Integer.parseInt(System.getenv("BUFFER_CLEANUP_DAYS")) : 7;
this.bufferCleanupService = new BufferCleanupService(cleanupEveryDays, this.logger);
this.forwardUnitService = new ForwardUnitService(this.logger, this.clientManager);
this.forwardUnitService = new ForwardUnitService(this.logger, this.clientManager, this.callManager);
}
/**
@@ -228,8 +231,8 @@ public class Boot {
this.packetManager.registerExecutor(22, new Executor22GroupBan());
this.packetManager.registerExecutor(24, new Executor24DeviceResolve(this.clientManager, this.eventManager, this.packetManager));
this.packetManager.registerExecutor(25, new Executor25Sync(this.packetManager));
this.packetManager.registerExecutor(26, new Executor26SignalPeer(this.clientManager, this.forwardUnitService));
this.packetManager.registerExecutor(27, new Executor27WebRTC(this.forwardUnitService));
this.packetManager.registerExecutor(26, new Executor26SignalPeer(this.clientManager, this.forwardUnitService, this.callManager));
this.packetManager.registerExecutor(27, new Executor27WebRTC(this.callManager));
this.packetManager.registerExecutor(28, new Executor28IceServers(this.forwardUnitService));
}

View File

@@ -27,7 +27,11 @@ public enum Failures implements BaseFailures {
/**
* Слишком много подписок на онлайн статусы
*/
TOO_MANY_ONLINE_SUBSCRIPTIONS(3010);
TOO_MANY_ONLINE_SUBSCRIPTIONS(3010),
/**
* Нет сессии звонка
*/
NO_CALL_SESSION(3011);
private final int code;

View File

@@ -0,0 +1,119 @@
package im.rosetta.calls;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import im.rosetta.client.ClientManager;
import im.rosetta.packet.Packet26SignalPeer;
import im.rosetta.packet.runtime.NetworkSignalType;
import io.g365sfu.Room;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
import io.orprotocol.packet.Packet;
public class CallManager {
private List<CallSession> callSessions = new ArrayList<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private static final long RINGING_TIMEOUT = 30 * 1000;
private ClientManager clientManager;
public CallManager(ClientManager clientManager) {
this.clientManager = clientManager;
scheduler.scheduleAtFixedRate(this::cleanupCallSessions, 0, 1, TimeUnit.SECONDS);
}
public void cleanupCallSessions() {
/**
* Такая конструкция нужна для избежания ConcurrentModificationException,
* так как мы не можем удалять элементы из списка, по которому проходим в цикле,
* поэтому мы сначала собираем сессии звонков, которые нужно удалить, а потом
* удаляем их из основного списка
*/
List<CallSession> sessionsToRemove = new ArrayList<>();
for (CallSession session : this.callSessions) {
if (session.shouldRemove()) {
/**
* Отправляем всем в сессии что звонок завершился, так как он устарел, и удаляем сессию из списка активных сессий
*/
Packet26SignalPeer rtout = new Packet26SignalPeer();
rtout.setSignalType(NetworkSignalType.RINGING_TIMEOUT);
Packet26SignalPeer endCallPacket = new Packet26SignalPeer();
endCallPacket.setSignalType(NetworkSignalType.END_CALL);
endCallPacket.setJoinToken(session.getJoinToken());
endCallPacket.setCallId(session.getCallId());
try {
session.sendPacket(rtout, null);
this.sendPacketToRinging(session, endCallPacket);
} catch (ProtocolException e) {
e.printStackTrace();
}
sessionsToRemove.add(session);
}
}
for (CallSession session : sessionsToRemove) {
this.callSessions.remove(session);
}
}
public CallSession createCall(String callId, String joinToken) {
CallSession session = new CallSession(callId, joinToken, RINGING_TIMEOUT);
this.callSessions.add(session);
return session;
}
public CallSession getCallSession(String callId, String joinToken) {
for (CallSession session : this.callSessions) {
if (session.getCallId().equals(callId) && session.getJoinToken().equals(joinToken)) {
return session;
}
}
return null;
}
public CallSession getCallSession(Room room) {
for (CallSession session : this.callSessions) {
if (session.getRoom() != null && session.getRoom().equals(room)) {
return session;
}
}
return null;
}
public boolean isBusy(String publicKey) {
for (CallSession session : this.callSessions) {
if (session.clients.containsKey(publicKey)) {
return true;
}
if(session.ringing.containsKey(publicKey)) {
return true;
}
}
return false;
}
public void sendPacketToRinging(CallSession session, Packet packet) throws ProtocolException {
for (String publicKey : session.ringing.keySet()) {
this.clientManager.sendPacketToAuthorizedPK(publicKey, packet);
}
}
public CallSession getCallSession(Client client) {
for (CallSession session : this.callSessions) {
if (session.clients.containsValue(client)) {
return session;
}
}
return null;
}
public void removeSession(CallSession session) {
this.callSessions.remove(session);
}
}

View File

@@ -0,0 +1,131 @@
package im.rosetta.calls;
import java.util.HashMap;
import io.g365sfu.Room;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
import io.orprotocol.packet.Packet;
public class CallSession {
public String callId;
public String joinToken;
public Long createdAt;
public Long ringingTimeout;
/**
* Клиенты которым сейчас идет дозвон (публичные ключи)
* Клиенты в этом списке не могут принимать другие звонки, так как они уже заняты дозвоном,
* но они еще не в звонке, так как не приняли звонок
*
* Клиенты удаляются из этого списка, когда они принимают звонок или отклоняют его,
* тогда они либо переходят в звонок, либо становятся свободными для других звонков
* pk -> время начала дозвона (timestamp в миллисекундах)
*/
public HashMap<String, Long> ringing;
/**
* Клиенты, которые уже приняли звонок и находятся в звонке (публичные ключи) и их сокеты
* pk -> client
*/
public HashMap<String, Client> clients;
/**
* Если звонок активен у него появляется комната, иначе комната null
*/
public Room room;
public CallSession(String callId, String joinToken, Long ringingTimeout) {
this.callId = callId;
this.joinToken = joinToken;
this.clients = new HashMap<>();
this.ringing = new HashMap<>();
this.createdAt = System.currentTimeMillis();
this.ringingTimeout = ringingTimeout;
}
public void joinCall(String publicKey, Client client) {
if(this.ringing.containsKey(publicKey)) {
this.ringing.remove(publicKey);
}
this.clients.put(publicKey, client);
}
/**
* Проверяет, может ли этот публичный ключ выполнять какие-либо действия в рамках этой сессии звонка,
* чтобы не допустить выполнение действий от посторонних публичных ключей, которые не участвуют в звонке
* @param publicKey Публичный ключ для проверки
* @return true, если этот публичный ключ может выполнять действия в рамках этой сессии звонка, false иначе
*/
public boolean isValidSource(String publicKey) {
return this.ringing.containsKey(publicKey) || this.clients.containsKey(publicKey);
}
/**
* Получаем публичный ключ клиента по его сокету, чтобы понимать, кто отправляет сигналы в рамках звонка
* @param client Сокет клиента, для которого нужно получить публичный ключ
* @return Публичный ключ клиента, или null если клиент не найден в сессии звонка
*/
public String getPublicKey(Client client) {
for (String publicKey : this.clients.keySet()) {
if (this.clients.get(publicKey).equals(client)) {
return publicKey;
}
}
return null;
}
public String getCallId() {
return this.callId;
}
public String getJoinToken() {
return this.joinToken;
}
public void leaveCall(String publicKey) {
if(this.clients.containsKey(publicKey)) {
this.clients.remove(publicKey);
}
if(this.ringing.containsKey(publicKey)) {
this.ringing.remove(publicKey);
}
}
public void addRinging(String publicKey) {
this.ringing.put(publicKey, System.currentTimeMillis());
}
public void setRoom(Room room) {
this.room = room;
}
public Room getRoom() {
return this.room;
}
/**
* Отправляем пакет всем участникам звонка, кроме исключенного клиента (обычно отправителя)
* @param packet Пакет для отправки
* @param excludeClient Клиент, которому не нужно отправлять пакет (обычно отправитель)
* @throws ProtocolException Если произошла ошибка при отправке пакета клиенту
*/
public void sendPacket(Packet packet, Client excludeClient) throws ProtocolException {
for (Client client : this.clients.values()) {
if (!client.equals(excludeClient)) {
client.send(packet);
}
}
}
/**
* Проверяем, нужно ли удалять сессию звонка.
* Сессию звонка нужно удалять, если в ней меньше 2 клиентов и при этом нет клиентов в состоянии дозвона,
* или если сессия была создана более 1 минуты назад, так как это значит, что клиенты не ответили на звонок и он устарел
* @return true, если сессию звонка нужно удалять, false иначе
*/
public boolean shouldRemove() {
return this.clients.size() <= 1 && (this.ringing.size() == 0 || System.currentTimeMillis() - this.createdAt > this.ringingTimeout);
}
}

View File

@@ -56,9 +56,10 @@ public class ClientManager {
* Отправить пакет ВСЕМ АВТОРИЗОВАННЫМ клиентам с публичным ключом publicKey
* @param publicKey публичный ключ получателя
* @param packet пакет для отправки
* @param exclude клиент, который не должен получать этот пакет, может быть null
* @throws ProtocolException если произошла ошибка при отправке пакета клиенту
*/
public void sendPacketToAuthorizedPK(String publicKey, Packet packet) throws ProtocolException {
public void sendPacketToAuthorizedPK(String publicKey, Packet packet, Client exclude) throws ProtocolException {
Set<Client> clients = this.clientIndexer.getClients(ECIAuthentificate.class, "publicKey", publicKey);
if(clients == null){
/**
@@ -77,12 +78,55 @@ public class ClientManager {
continue;
}
/**
* Отправляем пакет каждому клиенту с таким публичным ключом (то есть всем его авторизованным сессиям/устройствам)
* Отправляем пакет каждому клиенту с таким публичным ключом (то есть всем его авторизованным сессиям/устройствам),
* исключая клиента exclude
*/
if(exclude != null && client.equals(exclude)){
/**
* Этот клиент является исключением, он не должен получать этот пакет
*/
continue;
}
client.send(packet);
}
}
/**
* Отправить пакет ВСЕМ АВТОРИЗОВАННЫМ клиентам с публичным ключом publicKey
* @param publicKey публичный ключ получателя
* @param packet пакет для отправки
* @throws ProtocolException если произошла ошибка при отправке пакета клиенту
*/
public void sendPacketToAuthorizedPK(String publicKey, Packet packet) throws ProtocolException {
this.sendPacketToAuthorizedPK(publicKey, packet, null);
}
/**
* Отправить пакет всем клиентам с публичными ключами из списка publicKeys
* @param publicKeys список публичных ключей получателей
* @param packet пакет для отправки
* @param exclude клиент, который не должен получать этот пакет, может быть null
* @throws ProtocolException если произошла ошибка при отправке пакета клиенту
*/
public void sendPacketToAuthorizedPK(List<String> publicKeys, Packet packet, Client exclude) throws ProtocolException {
for(String publicKey : publicKeys){
this.sendPacketToAuthorizedPK(publicKey, packet, exclude);
}
}
/**
* Отправить пакет всем клиентам с публичными ключами из списка publicKeys
* @param publicKeys список публичных ключей получателей
* @param packet пакет для отправки
* @throws ProtocolException если произошла ошибка при отправке пакета клиенту
*/
public void sendPacketToAuthorizedPK(List<String> publicKeys, Packet packet) throws ProtocolException {
for(String publicKey : publicKeys){
this.sendPacketToAuthorizedPK(publicKey, packet, null);
}
}
/**
* Отправить пакет всем клиентам с публичными ключом как у client, кроме клиента client, который является отправителем и не должен получать этот пакет
* @param client клиент
@@ -111,18 +155,6 @@ public class ClientManager {
}
}
/**
* Отправить пакет всем клиентам с публичными ключами из списка publicKeys
* @param publicKeys список публичных ключей получателей
* @param packet пакет для отправки
* @throws ProtocolException если произошла ошибка при отправке пакета клиенту
*/
public void sendPacketToAuthorizedPK(List<String> publicKeys, Packet packet) throws ProtocolException {
for(String publicKey : publicKeys){
this.sendPacketToAuthorizedPK(publicKey, packet);
}
}
/**
* Получить список клиентов по публичному ключу (get PublicKey clients), могут быть неавторизованные клиенты
* @param publicKey публичный ключ клиента

View File

@@ -1,10 +1,19 @@
package im.rosetta.executors;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import im.rosetta.Failures;
import im.rosetta.calls.CallManager;
import im.rosetta.calls.CallSession;
import im.rosetta.client.ClientManager;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.packet.Packet26SignalPeer;
import im.rosetta.packet.runtime.NetworkSignalType;
import im.rosetta.service.dispatch.push.PushNotifyDispatcher;
import im.rosetta.service.dispatch.runtime.PushType;
import im.rosetta.service.services.ForwardUnitService;
import io.g365sfu.Room;
import io.orprotocol.ProtocolException;
@@ -18,62 +27,196 @@ public class Executor26SignalPeer extends PacketExecutor<Packet26SignalPeer> {
private ClientManager clientManager;
private ForwardUnitService fus;
private PushNotifyDispatcher pushNotifyDispatcher = new PushNotifyDispatcher();
/**
* Сигналы, которые может выполнять только авторизованный пользователь,
* все сигналы, которые не входят в этот перечень, будут доступны для
* исполнения без авторизации.
*/
private Set<NetworkSignalType> authentificatedTypes = new HashSet<>(){{
add(NetworkSignalType.CALL);
}};
public Executor26SignalPeer(ClientManager clientManager, ForwardUnitService fus) {
/**
* Менеджер звонков, который реализует весь необхоимый функционал для управления звонками,
* например проверку занятости пользователя, и тд
*/
private CallManager callManager;
public Executor26SignalPeer(ClientManager clientManager, ForwardUnitService fus, CallManager callManager) {
this.clientManager = clientManager;
this.fus = fus;
this.callManager = callManager;
}
@Override
public void onPacketReceived(Packet26SignalPeer packet, Client client) throws Exception, ProtocolException {
String src = packet.getSrc();
String dst = packet.getDst();
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if (eciAuthentificate == null || !eciAuthentificate.hasAuthorized()) {
NetworkSignalType type = packet.getSignalType();
if ((eciAuthentificate == null || !eciAuthentificate.hasAuthorized())
&& this.authentificatedTypes.contains(type)) {
/**
* Если клиент не авторизован, то мы не будем обрабатывать его сигналы на анициализацию звонка
* Если клиент не авторизован, то мы не будем обрабатывать его сигналы на инициализацию звонка и создание комнаты
* и просто отключим его от сервера.
*/
client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED);
return;
}
NetworkSignalType type = packet.getSignalType();
if(src != null && !src.equals(eciAuthentificate.getPublicKey()) && authentificatedTypes.contains(type)) {
/**
* Если src в пакете не совпадает с авторизованным PK клиента, то это может означать, что клиент пытается
* отправить сигнал от другого пользователя, отключаем его от сервера.
*/
client.disconnect(Failures.DATA_MISSMATCH);
return;
}
if(type == NetworkSignalType.CALL) {
/**
* Инициируется звонок от src к dst, проверяем, что dst не занят другим звонком, если занят, то отправляем сигнал END_CALL_BECAUSE_BUSY обратно src
*/
Room room = this.fus.getRoomByParticipantId(packet.getDst());
if(room != null) {
if(this.callManager.isBusy(dst) || this.callManager.isBusy(src)) {
/**
* Получатель сигнала уже находится в другой комнате, значит он занят другим звонком, отправляем сигнал END_CALL_BECAUSE_BUSY обратно src
*/
Packet26SignalPeer responsePacket = new Packet26SignalPeer();
responsePacket.setSignalType(NetworkSignalType.END_CALL_BECAUSE_BUSY);
this.clientManager.sendPacketToAuthorizedPK(packet.getSrc(), responsePacket);
client.send(responsePacket);
return;
}
}
if(type == NetworkSignalType.CREATE_ROOM){
/**
* Создается комната для звонка
* Генерируем CallID и JoinToken
*/
Room room = this.fus.createRoom();
room.addParticipant(packet.getSrc());
room.addParticipant(packet.getDst());
packet.setRoomId(room.getRoomId());
String callId = UUID.randomUUID().toString();
String joinToken = UUID.randomUUID().toString();
packet.setJoinToken(joinToken);
packet.setCallId(callId);
/**
* Результат создания комнаты транслируем обоим участникам, чтобы они могли начать обмен WebRTC SDP, и тд
* Создаем сессию звонка и добавляем в нее звонящего
*/
this.clientManager.sendPacketToAuthorizedPK(packet.getSrc(), packet);
this.clientManager.sendPacketToAuthorizedPK(packet.getDst(), packet);
CallSession session = this.callManager.createCall(callId, joinToken);
session.joinCall(src, client);
/**
* Добавляем dst в ringing, чтобы пометить, что ему поступает звонок
*/
session.addRinging(dst);
/**
* Получатель сигнала не занят, отправляем ему пуш уведомление о входящем звонке и сигнал CALL для инициализации звонка
*/
pushNotifyDispatcher.sendPush(dst, new HashMap<>(){{
put("type", PushType.CALL);
put("dialog", src);
put("callId", callId);
put("joinToken", joinToken);
}});
/**
* Отправляем сигнал CALL всем авторизованным устройствам вызываемого абонента
*/
this.clientManager.sendPacketToAuthorizedPK(dst, packet);
return;
}
/**
* TODO: Проверка на существование получателя
*/
this.clientManager.sendPacketToAuthorizedPK(packet.getDst(), packet);
/**
* TODO: Высокоприоритетный пуш для сигналов звонков, чтобы мобильные устройства могли показать
* интерфейс входящего звонка, даже если приложение находится в фоне
*/
if(type == NetworkSignalType.ACCEPT){
String callId = packet.getCallId();
String joinToken = packet.getJoinToken();
CallSession session = this.callManager.getCallSession(callId, joinToken);
if(session == null) {
/**
* Сессии звонка нет
*/
client.disconnect(Failures.NO_CALL_SESSION);
return;
}
if(!session.isValidSource(src)) {
/**
* Клиент не состоит в сессии звонка, отключаем его от сервера, так как он отправляет некорректные данные
*/
client.disconnect(Failures.DATA_MISSMATCH);
return;
}
Room room = this.fus.createRoom();
session.setRoom(room);
session.joinCall(src, client);
for(String participant : session.clients.keySet()) {
room.addParticipant(participant);
}
session.sendPacket(packet, client);
/**
* Сбрасываем вызов на всех остальных устройствах пользователя, который принимает звонок,
* чтобы он не смог принять или отклонить звонок с другого устройства
*/
Packet26SignalPeer endCallOtherDevices = new Packet26SignalPeer();
endCallOtherDevices.setSignalType(NetworkSignalType.END_CALL);
this.clientManager.sendPacketToAuthorizedPK(src, endCallOtherDevices, client);
return;
}
if(type == NetworkSignalType.KEY_EXCHANGE){
/**
* Ретранслируем ключи в рамках сессии
*/
CallSession session = this.callManager.getCallSession(client);
if(session == null) {
/**
* Сессии звонка нет
*/
client.disconnect(Failures.NO_CALL_SESSION);
return;
}
/**
* Обмениваемся ключами в рамках сессии, ретранслируя их всем участникам сессии, кроме отправителя
*/
session.sendPacket(packet, client);
return;
}
if(type == NetworkSignalType.END_CALL) {
/**
* Ретранслируем сигнал окончания звонка всем участникам сессии, кроме отправителя, и удаляем сессию
*/
/**
* Сначала получаем сессию по сокету отправителя пакета, если не находим, то пробуем найти сессию по callId и joinToken из пакета, если не находим,
* то отключаем клиента от сервера, так как он отправляет некорректные данные
*/
CallSession session = this.callManager.getCallSession(client);
if(session == null) {
String callId = packet.getCallId();
String joinToken = packet.getJoinToken();
session = this.callManager.getCallSession(callId, joinToken);
}
if(session == null) {
/**
* Сессии звонка нет, скорее всего она была удалена при обрыве RTC Peer Connection,
* при срабатывании RTCPeerConnection::close на клиенте раньше, чем клиент отправил сигнал END_CALL
*/
return;
}
/**
* Отправляем сигнал окончания звонка всем участникам сессии, кроме отправителя
*/
session.sendPacket(packet, client);
/**
* Отправляем пакет вызываемым (ringing) пользователям (которые еще не в сессии)
*/
this.callManager.sendPacketToRinging(session, packet);
/**
* Удаляем сессию из активных сессий звонков
*/
this.callManager.removeSession(session);
return;
}
if(type == NetworkSignalType.ACTIVE) {
/**
* Клиент сообщил, что прошел стадию обмена ключами и звонок активен
*/
CallSession session = this.callManager.getCallSession(client);
if(session == null) {
/**
* Сессии звонка нет
*/
client.disconnect(Failures.NO_CALL_SESSION);
return;
}
session.sendPacket(packet, null);
}
}
}

View File

@@ -1,10 +1,10 @@
package im.rosetta.executors;
import im.rosetta.Failures;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.calls.CallManager;
import im.rosetta.calls.CallSession;
import im.rosetta.packet.Packet27WebRTC;
import im.rosetta.packet.runtime.NetworkWebRTCType;
import im.rosetta.service.services.ForwardUnitService;
import io.g365sfu.Room;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
@@ -12,35 +12,42 @@ import io.orprotocol.packet.PacketExecutor;
public class Executor27WebRTC extends PacketExecutor<Packet27WebRTC> {
private ForwardUnitService fus;
private CallManager callManager;
public Executor27WebRTC(ForwardUnitService fus) {
this.fus = fus;
public Executor27WebRTC(CallManager callManager) {
this.callManager = callManager;
}
@Override
public void onPacketReceived(Packet27WebRTC packet, Client client) throws Exception, ProtocolException {
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()) {
/**
* Получаем, в какой сессии находится этот сокет
*/
CallSession session = this.callManager.getCallSession(client);
if(session == null) {
/**
* Если клиент не авторизован, то мы не будем обрабатывать его сигналы на инициализацию звонка
* и просто отключим его от сервера.
* Если сессия не найдена, то мы не будем обрабатывать сигналы для звонка
*/
client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED);
return;
}
String publicKey = eciAuthentificate.getPublicKey();
/**
* Так как в комнатах Participants это публичные ключи пользователей, то мы можем
* найти комнату, в которой находится пользователь, по его публичному ключу
*/
Room room = this.fus.getRoomByParticipantId(publicKey);
Room room = session.getRoom();
if(room == null) {
/**
* Если комната не найдена, то мы не будем обрабатывать сигналы для звонка
* и просто отключим клиента от сервера.
* Звонок еще не активен, а значит комнаты еще нет. Нельзя обменяться WebRTC сигналами пока комнаты еще нет.
*/
return;
}
/**
* Получаем публичный ключ, которым представился клиент, в рамках сессии звонка.
* Мы не делаем это через ECIAuthentificate, так как в рамках звонка клиент может не быть авторизован, но при этом он уже находится в сессии звонка, и мы можем идентифицировать его по публичному ключу,
* который он указал при присоединении к звонку используя joinToken.
* Так что, несмотря на то, что клиент может быть не авторизован, мы все равно можем достоверно знать его публичный ключ
*/
String publicKey = session.getPublicKey(client);
if(publicKey == null) {
/**
* Избыточная проверка, так как если клиент находится в сессии, то он должен быть в списке клиентов сессии,
* но на всякий случай проверим это, чтобы избежать возможных ошибок
*/
client.disconnect(Failures.DATA_MISSMATCH);
return;

View File

@@ -2,8 +2,6 @@ package im.rosetta.executors;
import java.util.ArrayList;
import im.rosetta.Failures;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.packet.Packet28IceServers;
import im.rosetta.service.services.ForwardUnitService;
import io.orprotocol.ProtocolException;
@@ -20,15 +18,6 @@ public class Executor28IceServers extends PacketExecutor<Packet28IceServers> {
@Override
public void onPacketReceived(Packet28IceServers packet, Client client) throws Exception, ProtocolException {
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()) {
/**
* Если клиент не авторизован, то мы не будем обрабатывать его запрос на получение ICE серверов
* и просто отключим его от сервера.
*/
client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED);
return;
}
/**
* Берем TURN сервера и отправляем их клиенту
*/

View File

@@ -29,16 +29,18 @@ public class Packet26SignalPeer extends Packet {
* Тип сигнала
*/
private NetworkSignalType signalType;
/**
* Идентификатор комнаты, в которой происходит звонок, заполняется если тип сигнала CREATE_ROOM, иначе null
* callId и joinToken нужны для того, чтобы идентифицировать сессию звонка. Так как roomId это только ID комнаты на sfu
*/
private String roomId;
private String callId;
private String joinToken;
@Override
public void read(Stream stream) {
this.signalType = NetworkSignalType.fromCode(stream.readInt8());
if(this.signalType == NetworkSignalType.END_CALL_BECAUSE_BUSY || this.signalType == NetworkSignalType.END_CALL_BECAUSE_PEER_DISCONNECTED) {
if(this.signalType == NetworkSignalType.END_CALL_BECAUSE_BUSY
|| this.signalType == NetworkSignalType.END_CALL_BECAUSE_PEER_DISCONNECTED
|| this.signalType == NetworkSignalType.RINGING_TIMEOUT) {
return;
}
this.src = stream.readString();
@@ -46,8 +48,9 @@ public class Packet26SignalPeer extends Packet {
if (signalType == NetworkSignalType.KEY_EXCHANGE) {
this.sharedPublic = stream.readString();
}
if(signalType == NetworkSignalType.CREATE_ROOM) {
this.roomId = stream.readString();
if(signalType == NetworkSignalType.CALL || signalType == NetworkSignalType.ACCEPT || signalType == NetworkSignalType.END_CALL) {
this.callId = stream.readString();
this.joinToken = stream.readString();
}
}
@@ -56,7 +59,9 @@ public class Packet26SignalPeer extends Packet {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeInt8(this.signalType.getCode());
if(this.signalType == NetworkSignalType.END_CALL_BECAUSE_BUSY || this.signalType == NetworkSignalType.END_CALL_BECAUSE_PEER_DISCONNECTED) {
if(this.signalType == NetworkSignalType.END_CALL_BECAUSE_BUSY
|| this.signalType == NetworkSignalType.END_CALL_BECAUSE_PEER_DISCONNECTED
|| this.signalType == NetworkSignalType.RINGING_TIMEOUT) {
return stream;
}
stream.writeString(this.src);
@@ -64,8 +69,9 @@ public class Packet26SignalPeer extends Packet {
if (signalType == NetworkSignalType.KEY_EXCHANGE) {
stream.writeString(this.sharedPublic);
}
if(signalType == NetworkSignalType.CREATE_ROOM) {
stream.writeString(this.roomId);
if(signalType == NetworkSignalType.CALL || signalType == NetworkSignalType.ACCEPT || signalType == NetworkSignalType.END_CALL) {
stream.writeString(this.callId);
stream.writeString(this.joinToken);
}
return stream;
}
@@ -134,20 +140,35 @@ public class Packet26SignalPeer extends Packet {
public void setSignalType(NetworkSignalType signalType) {
this.signalType = signalType;
}
/**
* Получить идентификатор сессии звонка, если тип сигнала CALL или ACCEPT
* @return идентификатор сессии звонка, если тип сигнала CALL или ACCEPT, иначе null
*/
public String getCallId() {
return callId;
}
/**
* Получить идентификатор созданной комнаты, если тип сигнала CREATE_ROOM
* @return идентификатор комнаты, если тип сигнала CREATE_ROOM, иначе null
* Установить идентификатор сессии звонка, если тип сигнала CALL или ACCEPT
* @param callId идентификатор сессии звонка, если тип сигнала CALL или ACCEPT
*/
public String getRoomId() {
return roomId;
public void setCallId(String callId) {
this.callId = callId;
}
/**
* Установить идентификатор комнаты, в которой происходит звонок, если тип сигнала CREATE_ROOM
* @param roomId идентификатор комнаты, если тип сигнала CREATE_ROOM
/**
* Получить токен для присоединения к сессии звонка, если тип сигнала CALL или ACCEPT
* @return токен для присоединения к сессии звонка, если тип сигнала CALL или ACCEPT, иначе null
*/
public void setRoomId(String roomId) {
this.roomId = roomId;
public String getJoinToken() {
return joinToken;
}
/**
* Установить токен для присоединения к сессии звонка, если тип сигнала CALL или ACCEPT
* @param joinToken токен для присоединения к сессии звонка, если тип сигнала CALL или ACCEPT
*/
public void setJoinToken(String joinToken) {
this.joinToken = joinToken;
}
}

View File

@@ -60,6 +60,4 @@ public class Packet27WebRTC extends Packet {
public void setType(NetworkWebRTCType type) {
this.type = type;
}
}

View File

@@ -8,7 +8,9 @@ public enum AttachmentType {
MESSAGES(1),
FILE(2),
AVATAR(3),
CALL(4);
CALL(4),
VOICE(5),
VIDEO_CIRCLE(6);
private final int code;

View File

@@ -21,9 +21,9 @@ public enum NetworkSignalType {
*/
END_CALL(3),
/**
* Создание комнаты
* Активная стадия звонка, значит комната уже создана на SFU
*/
CREATE_ROOM(4),
ACTIVE(4),
/**
* Обрыв связи с пиром
*/
@@ -31,7 +31,16 @@ public enum NetworkSignalType {
/**
* Не удалось дозвониться - пользователь занят другим звонком
*/
END_CALL_BECAUSE_BUSY(6);
END_CALL_BECAUSE_BUSY(6),
/**
* Принятие звонка
*/
ACCEPT(7),
/**
* Таймаут на этапе дозвона, если пользователь не ответил на звонок в течение определенного времени, то звонок считается неуспешным и вызывающей
* стороне отправляется этот сигнал, а сессия звонка удаляется, так как она уже не актуальна
*/
RINGING_TIMEOUT(8);
private final int code;

View File

@@ -61,9 +61,10 @@ public class FCM extends Pusher {
* Тихий тип уведомления для очистки отправленных уведомлений на устройстве,
* не должен отображаться пользователю, поэтому не задаем звук и ставим contentAvailable для iOS и high priority для Android
*/
apnsConfig.setAps(Aps.builder().setContentAvailable(true).setSound("default").build());
apnsConfig.setAps(Aps.builder().setContentAvailable(true).setMutableContent(true).build());
androidConfig.setPriority(AndroidConfig.Priority.HIGH);
messageBuilder.setApnsConfig(apnsConfig.build());
messageBuilder.setNotification(Notification.builder().setTitle("").setBody("").build());
messageBuilder.setAndroidConfig(androidConfig.build());
break;
case PushType.PERSONAL_MESSAGE:
@@ -82,8 +83,7 @@ public class FCM extends Pusher {
break;
case PushType.CALL:
/**
* Звонок для андроид используем high priority, чтобы уведомление доставлялось даже если устройство в режиме Doze,
* для iOS используем VoIP уведомление, которое доставляется даже если приложение убито
* Это только для Android, для iOS используется VoIP APNs с отдельным сертификатом
*/
androidConfig.setPriority(AndroidConfig.Priority.HIGH);
messageBuilder.setAndroidConfig(androidConfig.build());
@@ -98,7 +98,7 @@ public class FCM extends Pusher {
Message message = this.buildMessage(token, data);
FirebaseMessaging.getInstance().send(message);
}catch(Exception e){
e.printStackTrace();
}
}

View File

@@ -1,15 +1,96 @@
package im.rosetta.service.dispatch.push.dispatchers;
import java.io.File;
import java.util.HashMap;
import java.util.concurrent.ExecutionException;
import com.eatthepath.pushy.apns.ApnsClient;
import com.eatthepath.pushy.apns.ApnsClientBuilder;
import com.eatthepath.pushy.apns.PushNotificationResponse;
import com.eatthepath.pushy.apns.util.SimpleApnsPushNotification;
import com.eatthepath.pushy.apns.util.TokenUtil;
import im.rosetta.service.dispatch.push.Pusher;
import im.rosetta.service.dispatch.runtime.PushType;
public class VoIPApns extends Pusher {
private ApnsClient client;
private String topic;
public VoIPApns(){
this.initializeApns();
}
private void initializeApns() {
try {
String p12Path = System.getenv("APNS_KEY_PATH");
String p12Password = System.getenv("APNS_P12_PASSWORD");
String bundleId = System.getenv("IOS_BUNDLE_ID");
if (p12Path == null || bundleId == null) {
throw new IllegalStateException("APNS_P12_PATH and IOS_BUNDLE_ID must be set");
}
this.topic = bundleId + ".voip";
this.client = new ApnsClientBuilder()
.setApnsServer(ApnsClientBuilder.PRODUCTION_APNS_HOST)
.setClientCredentials(new File(p12Path), p12Password)
.build();
} catch (Exception e) {
throw new RuntimeException("Failed to init VoIP APNs client", e);
}
}
@Override
public void sendPush(String token, HashMap<String, String> data) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'sendPush'");
if(data.get("type") != PushType.CALL) {
/**
* Для VoIP APNs отправляем уведомления только о входящих звонках
*/
return;
}
try {
String normalizedToken = TokenUtil.sanitizeTokenString(token);
String payload = """
{
"aps": { "content-available": 1 },
"type": "CALL",
"callId": "%s",
"from": "%s",
"joinToken": "%s"
}
""".formatted(
escape(data.getOrDefault("callId", "")),
escape(data.getOrDefault("dialog", "")),
escape(data.getOrDefault("joinToken", ""))
);
SimpleApnsPushNotification push = new SimpleApnsPushNotification(
normalizedToken,
topic,
payload,
null, // invalidation time
com.eatthepath.pushy.apns.DeliveryPriority.IMMEDIATE,
com.eatthepath.pushy.apns.PushType.VOIP // apns-push-type: voip
);
PushNotificationResponse<SimpleApnsPushNotification> response = client.sendNotification(push).get();
if (!response.isAccepted()) {
System.err.println("VoIP push rejected: " + response.getRejectionReason());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
private String escape(String v) {
return v == null ? "" : v.replace("\\", "\\\\").replace("\"", "\\\"");
}
}

View File

@@ -1,5 +1,6 @@
package im.rosetta.service.services;
import java.util.HashSet;
import java.util.List;
import im.rosetta.client.tags.ECIAuthentificate;
@@ -82,4 +83,18 @@ public class DeviceService extends Service<DeviceRepository> {
return device.getSyncTime();
}
/**
* Получить публичные ключи пользователей, которые связаны с данным deviceId
* @param deviceId ID устройства
* @return набор публичных ключей пользователей, которые связаны с данным deviceId
*/
public HashSet<String> getPublicKeysByDeviceId(String deviceId) {
List<Device> devices = this.getRepository().findAllByField("deviceId", deviceId);
HashSet<String> publicKeys = new HashSet<>();
for(Device device : devices) {
publicKeys.add(device.getPublicKey());
}
return publicKeys;
}
}

View File

@@ -7,6 +7,8 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import im.rosetta.calls.CallManager;
import im.rosetta.calls.CallSession;
import im.rosetta.client.ClientManager;
import im.rosetta.logger.Logger;
import im.rosetta.logger.enums.Color;
@@ -33,10 +35,12 @@ public class ForwardUnitService {
private Set<SFU> sfuConnections = ConcurrentHashMap.newKeySet();
private ClientManager clientManager;
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private CallManager callManager;
public ForwardUnitService(Logger logger, ClientManager clientManager) {
public ForwardUnitService(Logger logger, ClientManager clientManager, CallManager callManager) {
this.logger = logger;
this.clientManager = clientManager;
this.callManager = callManager;
this.sfuConnectionsSheduler();
}
@@ -135,25 +139,33 @@ public class ForwardUnitService {
public void onPeerDisconnected(DisconnectedPeer disconnectedPeer) throws ProtocolException {
Room room = disconnectedPeer.getRoom();
if(disconnectedPeer.getReason() != DisconnectReason.FAILED){
CallSession callSession = this.callManager.getCallSession(room);
callSession.leaveCall(disconnectedPeer.getPeerId());
if(disconnectedPeer.getReason() == DisconnectReason.FAILED){
/**
* Если у нас произошло штатное отключение, а не в результате обрыва связи - то не нужно отправлять
* оппонентам пакеты о том, что участник отключился в результате обрыва связи.
* Произошло нештатное отключение клиента от сервера SFU, например, из-за сбоя сети
*/
return;
}
for(String peerId : room.getParticipants()) {
/**
* Уведомляем все пиры, что соединение с пиром было потеряно
*/
if(room.getParticipants().size() == 1) {
/**
* Звонок был завершен, так как в комнате остался только один участник, который не может продолжать звонок в одиночку.
*/
if(callSession.shouldRemove()){
Packet26SignalPeer packet = new Packet26SignalPeer();
packet.setSignalType(NetworkSignalType.END_CALL_BECAUSE_PEER_DISCONNECTED);
this.clientManager.sendPacketToAuthorizedPK(peerId, packet);
callSession.sendPacket(packet, null);
this.callManager.removeSession(callSession);
}
return;
}
if(disconnectedPeer.getReason() == DisconnectReason.CLOSED){
/**
* Клиент намеренно покинул звонок, например, отключился от SFU сервера, так как завершил звонок или вышел из комнаты
* (например если клиент отрабатывает выход из звонка по кнопке END не правильно)
*/
if(callSession.shouldRemove()){
Packet26SignalPeer packet = new Packet26SignalPeer();
packet.setSignalType(NetworkSignalType.END_CALL);
callSession.sendPacket(packet, null);
this.callManager.removeSession(callSession);
}
return;
}
}
@@ -255,5 +267,19 @@ public class ForwardUnitService {
return iceServers;
}
/**
* Получить комнату по ее ID, который используется на сервере SFU для идентификации комнаты.
* @param roomId ID комнаты на сервере SFU
* @return комната Room если найдена, иначе null
*/
public Room getRoomById(String roomId) {
for(SFU sfu : this.sfuConnections) {
Room room = sfu.getRoom(roomId);
if(room != null) {
return room;
}
}
return null;
}
}

View File

@@ -157,4 +157,12 @@ public class Room {
buffer.flip();
this.sfu.getConnection().send(buffer);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Room room = (Room) obj;
return roomId.equals(room.roomId);
}
}

View File

@@ -239,17 +239,18 @@ public class Client {
* @param client клиент
* @return true если это один и тот же клиент, false если нет
*/
public boolean equals(Client client) {
if(client == null){
return false;
}
if(!(client instanceof Client)){
return false;
}
if(!client.getClientId().equals(this.clientId)){
return false;
}
return true;
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Client client = (Client) obj;
return this.clientId != null && this.clientId.equals(client.clientId);
}
@Override
public int hashCode() {
return this.clientId == null ? 0 : this.clientId.hashCode();
}
}