using RelayShared.Rtc; using SurrealDb.Net; namespace RelayServer.Services.Rtc; public sealed class RtcCallService { private readonly SurrealDbClient _db; public RtcCallService(SurrealDbClient db) { _db = db; } public async Task HasActiveCallAsync(string channelId) { var activeCall = await GetActiveCallAsync(channelId); return activeCall is not null && activeCall.IsActive; } public async Task GetActiveCallAsync(string channelId) { var activeCalls = await _db.Select("rtc_active_calls"); return activeCalls .Where(x => x.ChannelId == channelId && x.IsActive) .OrderByDescending(x => x.UpdatedAt) .FirstOrDefault(); } public async Task> GetParticipantsAsync(string channelId) { return RtcChannelPresenceService.GetUsersInChannel(channelId).ToList(); } public async Task WriteOfferAsync(string channelId, string username, string targetUsername, RtcSessionDescription sessionDescription) { var activeCall = await EnsureActiveCallAsync(channelId); var participant = GetOrCreateParticipant(activeCall, username); participant.Offer = CloneSessionDescription(sessionDescription); participant.Answer = null; activeCall.UpdatedAt = DateTime.UtcNow; await SaveActiveCallAsync(activeCall); } public async Task GetOfferAsync(string channelId, string fromUsername, string targetUsername) { var activeCall = await GetActiveCallAsync(channelId); return activeCall?.Participants .FirstOrDefault(x => x.Username.Equals(fromUsername, StringComparison.OrdinalIgnoreCase)) ?.Offer; } public async Task WriteAnswerAsync(string channelId, string username, string targetUsername, RtcSessionDescription sessionDescription) { var activeCall = await EnsureActiveCallAsync(channelId); var participant = GetOrCreateParticipant(activeCall, username); participant.Answer = CloneSessionDescription(sessionDescription); activeCall.UpdatedAt = DateTime.UtcNow; await SaveActiveCallAsync(activeCall); } public async Task GetAnswerAsync(string channelId, string fromUsername, string targetUsername) { var activeCall = await GetActiveCallAsync(channelId); return activeCall?.Participants .FirstOrDefault(x => x.Username.Equals(fromUsername, StringComparison.OrdinalIgnoreCase)) ?.Answer; } public async Task WriteIceCandidateAsync( string channelId, string username, string targetUsername, string candidate, string? sdpMid, int? sdpMLineIndex) { await _db.Create("rtc_ice_candidates", new RtcIceCandidate { ChannelId = channelId, Username = username, TargetUsername = targetUsername, Candidate = candidate, SdpMid = sdpMid, SdpMLineIndex = sdpMLineIndex, CreatedAt = DateTime.UtcNow }); } public async Task> GetIceCandidatesAsync(string channelId) { var candidates = await _db.Select("rtc_ice_candidates"); return candidates .Where(x => x.ChannelId == channelId) .OrderBy(x => x.CreatedAt) .ToList(); } public async Task LeaveCallAsync(string channelId, string username) { var activeCall = await GetActiveCallAsync(channelId); if (activeCall is null) return; activeCall.Participants.RemoveAll(x => x.Username.Equals(username, StringComparison.OrdinalIgnoreCase)); activeCall.IsActive = activeCall.Participants.Count > 0; activeCall.UpdatedAt = DateTime.UtcNow; await SaveActiveCallAsync(activeCall); } public async Task> GetOffersAsync() { var activeCalls = await _db.Select("rtc_active_calls"); return activeCalls .Where(x => x.Participants.Any(p => p.Offer is not null)) .OrderByDescending(x => x.UpdatedAt) .ToList(); } private async Task EnsureActiveCallAsync(string channelId) { var activeCall = await GetActiveCallAsync(channelId); if (activeCall is not null) return activeCall; return await _db.Create("rtc_active_calls", new RtcActiveCall { ChannelId = channelId, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, IsActive = true, Participants = [] }); } private async Task SaveActiveCallAsync(RtcActiveCall activeCall) { if (activeCall.Id is null) { await _db.Create("rtc_active_calls", activeCall); return; } await _db.Merge(activeCall); } private static RtcParticipantState GetOrCreateParticipant(RtcActiveCall activeCall, string username) { var participant = activeCall.Participants .FirstOrDefault(x => x.Username.Equals(username, StringComparison.OrdinalIgnoreCase)); if (participant is not null) return participant; participant = new RtcParticipantState { Username = username }; activeCall.Participants.Add(participant); return participant; } private static RtcSessionDescription CloneSessionDescription(RtcSessionDescription sessionDescription) { return new RtcSessionDescription { Type = sessionDescription.Type, Sdp = sessionDescription.Sdp }; } }