voice chat indicators
This commit is contained in:
parent
6dc145a2c8
commit
02495c9e43
4 changed files with 125 additions and 17 deletions
|
|
@ -1098,6 +1098,12 @@ public class Requests
|
||||||
await Room.Shared.VoiceLeave(id, serializedBody.string2)
|
await Room.Shared.VoiceLeave(id, serializedBody.string2)
|
||||||
, password));
|
, password));
|
||||||
break;
|
break;
|
||||||
|
case "room/voice/state":
|
||||||
|
await context.Response.WriteAsync(
|
||||||
|
Encryption.Encryption.EncryptForClient(
|
||||||
|
await Room.Shared.VoiceState(id, serializedBody.string2)
|
||||||
|
, password));
|
||||||
|
break;
|
||||||
case "room/signal":
|
case "room/signal":
|
||||||
await context.Response.WriteAsync(
|
await context.Response.WriteAsync(
|
||||||
Encryption.Encryption.EncryptForClient(
|
Encryption.Encryption.EncryptForClient(
|
||||||
|
|
|
||||||
|
|
@ -531,6 +531,23 @@ public class Utils
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Dopisuje "call":true do wpisu JSON listy, jeśli w pokoju trwa aktywna rozmowa
|
||||||
|
//głosowa (ActiveCalls). Pozwala klientowi od razu po starcie pokazać wskaźnik.
|
||||||
|
public static string AppendCallStateToEntry(string entryJson, string roomId)
|
||||||
|
{
|
||||||
|
bool active = false;
|
||||||
|
if (ActiveCalls.TryGetValue(roomId, out var callList))
|
||||||
|
{
|
||||||
|
//Odczyt Count pod lockiem - ta sama lista jest mutowana pod lockiem
|
||||||
|
//w VoiceJoin/VoiceLeave/disconnect cleanup
|
||||||
|
lock (callList) { active = callList.Count > 0; }
|
||||||
|
}
|
||||||
|
if (!active) return entryJson;
|
||||||
|
string trimmed = entryJson.TrimEnd();
|
||||||
|
if (!trimmed.EndsWith("}")) return entryJson;
|
||||||
|
return $"{trimmed.Substring(0, trimmed.Length - 1)},\"call\":true}}";
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task<string> GetUserDms(string id)
|
public static async Task<string> GetUserDms(string id)
|
||||||
{
|
{
|
||||||
string path = $"{ACCOUNTS_DATA_DIR}/{id}/dms";
|
string path = $"{ACCOUNTS_DATA_DIR}/{id}/dms";
|
||||||
|
|
@ -546,14 +563,16 @@ public class Utils
|
||||||
{
|
{
|
||||||
string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{dmFiles[i]}"));
|
string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{dmFiles[i]}"));
|
||||||
if (string.IsNullOrWhiteSpace(content)) continue;
|
if (string.IsNullOrWhiteSpace(content)) continue;
|
||||||
|
|
||||||
|
content = AppendCallStateToEntry(content, dmFiles[i]);
|
||||||
|
|
||||||
if (!first) dmsBuilder.Append(',');
|
if (!first) dmsBuilder.Append(',');
|
||||||
dmsBuilder.Append($"\"{dmFiles[i]}\":");
|
dmsBuilder.Append($"\"{dmFiles[i]}\":");
|
||||||
dmsBuilder.Append(content);
|
dmsBuilder.Append(content);
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
dmsBuilder.Append("}");
|
dmsBuilder.Append("}");
|
||||||
|
|
||||||
return $"{{{dmsBuilder.ToString()}}}";
|
return $"{{{dmsBuilder.ToString()}}}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -170,14 +170,16 @@ public class Groups
|
||||||
{
|
{
|
||||||
string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{groupFiles[i]}"));
|
string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{groupFiles[i]}"));
|
||||||
if (string.IsNullOrWhiteSpace(content)) continue;
|
if (string.IsNullOrWhiteSpace(content)) continue;
|
||||||
|
|
||||||
|
content = Account.Utils.AppendCallStateToEntry(content, groupFiles[i]);
|
||||||
|
|
||||||
if (!first) groupsBuilder.Append(',');
|
if (!first) groupsBuilder.Append(',');
|
||||||
groupsBuilder.Append($"\"{groupFiles[i]}\":");
|
groupsBuilder.Append($"\"{groupFiles[i]}\":");
|
||||||
groupsBuilder.Append(content);
|
groupsBuilder.Append(content);
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
groupsBuilder.Append("}");
|
groupsBuilder.Append("}");
|
||||||
|
|
||||||
return $"{{{groupsBuilder.ToString()}}}";
|
return $"{{{groupsBuilder.ToString()}}}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -655,6 +655,44 @@ public class Shared
|
||||||
Timeout = TimeSpan.FromSeconds(2)
|
Timeout = TimeSpan.FromSeconds(2)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ===== FEDERACJA GŁOSU - PLAN (celowo niezaimplementowany) =====
|
||||||
|
//
|
||||||
|
// Zasady hostowania rozmów między serwerami:
|
||||||
|
// 1) GROUPS - rozmowa głosowa jest zawsze hostowana przez serwer DOMOWY
|
||||||
|
// grupy (domena z groupId, np. "123;example.com" -> example.com).
|
||||||
|
// Powód: członkowie, klucze pokoju i ActiveCalls żyją na serwerze grupy,
|
||||||
|
// tam najprościej utrzymać jeden spójny stan i rozsyłać call_update
|
||||||
|
// (także do użytkowników zdalnych, przez API federacyjne).
|
||||||
|
//
|
||||||
|
// 2) DMS - gdy uczestnicy DM są z dwóch różnych domen, rozmowę hostuje
|
||||||
|
// serwer domowy OSOBY, KTÓRA ROZPOCZĘŁA rozmowę (pierwszy uczestnik).
|
||||||
|
// Powód: initiator i tak wykonuje room/voice/join u siebie; druga strona
|
||||||
|
// dostaje call_update/sygnalizację i dołącza do pokoju na hoście.
|
||||||
|
// Determinacja "kto zaczął": pierwszy wpis w liście uczestników danej
|
||||||
|
// rozmowy; po stronie implementacji trzeba tę informację przekazać
|
||||||
|
// w protokole federacyjnym (np. w żądaniu forwardu join).
|
||||||
|
//
|
||||||
|
// Do zrobienia przy implementacji:
|
||||||
|
// - GetVoiceHostDomain() ma zwracać domenę hosta (groups: domena z roomId;
|
||||||
|
// dms: domena inicjatora gdy obie domeny różne),
|
||||||
|
// - VoiceJoin/VoiceState: gdy hostDomain != DOMAIN -> forward żądania do
|
||||||
|
// room/voice/join / room/voice/state na serwerze domowym hosta i zwrot
|
||||||
|
// jego odpowiedzi (proxy), zamiast lokalnej alokacji ticketu,
|
||||||
|
// - klucz ActiveCalls dla pokoi zdalnych kwalifikować domeną
|
||||||
|
// (np. "{roomId}@{hostDomain}"), żeby nie kolidował z lokalnymi,
|
||||||
|
// - call_update rozsyłać również do członków zdalnych (Sender API),
|
||||||
|
// a disconnect cleanup powiadamiał hosta o odejściu swojego usera,
|
||||||
|
// - sygnalizacja (room/signal): sprawdzić trasowanie signal: do zdalnych.
|
||||||
|
public static string GetVoiceHostDomain(string roomType, string roomId)
|
||||||
|
{
|
||||||
|
// TODO(federation):
|
||||||
|
// - groups -> domena z części roomId po ';',
|
||||||
|
// - dms -> rozparsować dmId ("idA;domA_idB;domB"); jeśli domA != domB,
|
||||||
|
// hostem jest domena inicjatora (wymaga informacji o inicjatorze
|
||||||
|
// przekazanej przez protokół federacyjny).
|
||||||
|
return DOMAIN;
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task<string> VoiceJoin(string id, string body)
|
public static async Task<string> VoiceJoin(string id, string body)
|
||||||
{
|
{
|
||||||
Universal2String serializedBody = JsonSerializer.Deserialize(
|
Universal2String serializedBody = JsonSerializer.Deserialize(
|
||||||
|
|
@ -666,18 +704,43 @@ public class Shared
|
||||||
|
|
||||||
if (!await IsMemberOfRoom(id, roomType, roomId)) return "error:forbidden";
|
if (!await IsMemberOfRoom(id, roomType, roomId)) return "error:forbidden";
|
||||||
|
|
||||||
|
//Federacja (patrz plan wyżej): dla pokoju zdalnego to żądanie powinno
|
||||||
|
//zostać sforwardowane do serwera hostującego rozmowę. Na razie każda
|
||||||
|
//rozmowa jest hostowana lokalnie.
|
||||||
|
string voiceHostDomain = GetVoiceHostDomain(roomType, roomId);
|
||||||
|
|
||||||
|
bool added = false;
|
||||||
var callList = ActiveCalls.GetOrAdd(roomId, _ => new List<string>());
|
var callList = ActiveCalls.GetOrAdd(roomId, _ => new List<string>());
|
||||||
lock (callList)
|
lock (callList)
|
||||||
{
|
{
|
||||||
if (!callList.Contains(id))
|
if (!callList.Contains(id))
|
||||||
{
|
{
|
||||||
callList.Add(id);
|
callList.Add(id);
|
||||||
|
added = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
string callState;
|
//call_update rozsyłamy TYLKO gdy skład się zmienił - ponowne wejście
|
||||||
lock (callList) { callState = JsonSerializer.Serialize(callList, AppJsonSerializerContext.Default.ListString); }
|
//(np. reconnect) nie generuje zbędnych broadcastów do wszystkich
|
||||||
|
//członków, co przy dużych grupach oszczędza WS-owe pakiety i CPU
|
||||||
|
if (added)
|
||||||
|
{
|
||||||
|
string callState;
|
||||||
|
lock (callList) { callState = JsonSerializer.Serialize(callList, AppJsonSerializerContext.Default.ListString); }
|
||||||
|
|
||||||
|
string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members");
|
||||||
|
foreach(string memberWD in members)
|
||||||
|
{
|
||||||
|
if (Account.Utils.IsUserLocal(memberWD, out string _))
|
||||||
|
{
|
||||||
|
string memberId = Account.Utils.GetValidIdOrZero(memberWD);
|
||||||
|
_ = Utils.Utils.SendWsMessage(memberId, $"call_update:{roomId}:{callState}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Wybór serwera głosowego: najmniej obciążony z lokalnej listy VOICE_IPS.
|
||||||
|
//Federacja (plan): gdy voiceHostDomain != DOMAIN, tutaj należy użyć
|
||||||
|
//adresu serwera głosowego domeny hosta zamiast lokalnej listy.
|
||||||
string bestIp = "";
|
string bestIp = "";
|
||||||
long bestLoad = long.MaxValue;
|
long bestLoad = long.MaxValue;
|
||||||
|
|
||||||
|
|
@ -728,15 +791,6 @@ public class Shared
|
||||||
return "error:voice.not.configured"; // 48 random bytes colliding - practically impossible
|
return "error:voice.not.configured"; // 48 random bytes colliding - practically impossible
|
||||||
}
|
}
|
||||||
|
|
||||||
string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members");
|
|
||||||
foreach(string memberWD in members)
|
|
||||||
{
|
|
||||||
if (Account.Utils.IsUserLocal(memberWD, out string _))
|
|
||||||
{
|
|
||||||
string memberId = Account.Utils.GetValidIdOrZero(memberWD);
|
|
||||||
_ = Utils.Utils.SendWsMessage(memberId, $"call_update:{roomId}:{callState}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $"success:{bestIp}|{publicId}|{connKey}|{proof}";
|
return $"success:{bestIp}|{publicId}|{connKey}|{proof}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -930,6 +984,33 @@ public class Shared
|
||||||
return "success";
|
return "success";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Zwraca listę uczestników aktywnej rozmowy głosowej w pokoju (ID z domeną).
|
||||||
|
//Używane przez klienta do pokazania wskaźników "na rozmowie" po otwarciu pokoju.
|
||||||
|
public static async Task<string> VoiceState(string id, string body)
|
||||||
|
{
|
||||||
|
Universal2String serializedBody = JsonSerializer.Deserialize(
|
||||||
|
body,
|
||||||
|
AppJsonSerializerContext.Default.Universal2String
|
||||||
|
);
|
||||||
|
string roomType = serializedBody.string1;
|
||||||
|
string roomId = serializedBody.string2;
|
||||||
|
|
||||||
|
if (!await IsMemberOfRoom(id, roomType, roomId)) return "error:forbidden";
|
||||||
|
|
||||||
|
List<string> participants = new List<string>();
|
||||||
|
if (ActiveCalls.TryGetValue(roomId, out var callList))
|
||||||
|
{
|
||||||
|
lock (callList)
|
||||||
|
{
|
||||||
|
foreach (string memberId in callList)
|
||||||
|
{
|
||||||
|
participants.Add($"{memberId};{DOMAIN}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return JsonSerializer.Serialize(participants, AppJsonSerializerContext.Default.ListString);
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task<string> RoomSignal(string id, string body)
|
public static async Task<string> RoomSignal(string id, string body)
|
||||||
{
|
{
|
||||||
Universal3String serializedBody = JsonSerializer.Deserialize(
|
Universal3String serializedBody = JsonSerializer.Deserialize(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue