using RelayShared.Rtc; using SurrealDb.Net; namespace RelayServer.Services.Rtc; public sealed class RtcCallService { private readonly SurrealDbClient _db; public RtcCallService(SurrealDbClient db) { _db = db; } /// /// Checks whether the specified channel currently has an active RTC call. /// /// The channel to inspect. /// /// True if the channel has an active call; otherwise, false. /// public async Task HasActiveCallAsync(string channelId) { var activeCalls = await _db.Select("rtc_active_calls"); return activeCalls.Any(x => x.ChannelId == channelId && x.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(); } /// /// Creates or updates the current SDP offer for a user in the specified channel. /// If no active call exists for the channel, a new active call is created. /// Otherwise, the existing active call timestamp is refreshed. /// /// The channel the offer belongs to. /// The user creating the offer. /// The RtcSession Type. /// The SDP offer payload. public async Task WriteOfferAsync(string channelId, string username, RtcSessionDescription sessionDescription) { var activeCalls = await _db.Select("rtc_active_calls"); var activeCall = activeCalls.FirstOrDefault(x => x.ChannelId == channelId && x.IsActive); if (activeCall is null) { await _db.Create("rtc_active_calls", new DBActiveCall { ChannelId = channelId, OfferUser = username, Offer = new RtcSessionDescription { Type = sessionDescription.Type, Sdp = sessionDescription.Sdp }, Answer = null, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, IsActive = true }); return; } activeCall.OfferUser = username; activeCall.Offer = new RtcSessionDescription { Type = sessionDescription.Type, Sdp = sessionDescription.Sdp }; activeCall.UpdatedAt = DateTime.UtcNow; await _db.Merge(activeCall); } /// /// Gets the current offer stored on the active call for the specified channel. /// /// The channel whose offer should be retrieved. /// /// The current offer for the active call, or null if no active call or offer exists. /// public async Task GetOfferAsync(string channelId) { var activeCall = await GetActiveCallAsync(channelId); return activeCall?.Offer; } /// /// Writes a new SDP answer for the specified channel and refreshes the active call timestamp /// when a matching active call exists. /// /// The channel the answer belongs to. /// The original offer owner. /// The SDP and type answer payload. public async Task WriteAnswerAsync(string channelId, RtcSessionDescription sessionDescription) { var activeCalls = await _db.Select("rtc_active_calls"); var activeCall = activeCalls.FirstOrDefault(x => x.ChannelId == channelId && x.IsActive); if (activeCall is null) return; activeCall.Answer = new RtcSessionDescription { Type = sessionDescription.Type, Sdp = sessionDescription.Sdp }; activeCall.UpdatedAt = DateTime.UtcNow; await _db.Merge(activeCall); } /// /// Gets all answers stored for the specified channel in creation order. /// /// The channel whose answers should be retrieved. /// /// A list of answers for the channel ordered from oldest to newest. /// public async Task> GetAnswersAsync(string channelId) { var activeCall = await GetActiveCallAsync(channelId); if (activeCall?.Answer is null) return []; return [activeCall.Answer]; } /// /// Gets the most recent answer stored for the specified channel. /// /// The channel whose latest answer should be retrieved. /// /// The newest answer for the channel, or null if no answer exists. /// public async Task GetLatestAnswerAsync(string channelId) { var activeCall = await GetActiveCallAsync(channelId); return activeCall?.Answer; } /// /// Writes a new ICE candidate entry for the specified channel and user. /// /// The channel the ICE candidate belongs to. /// The user who produced the ICE candidate. /// The ICE candidate string. /// The SDP media identifier for the candidate, if any. /// The SDP media line index for the candidate, if any. /// /// The signaling direction the candidate belongs to, such as offer or answer. /// public async Task WriteIceCandidateAsync( string channelId, string username, string candidate, string? sdpMid, int? sdpMLineIndex/*, string direction*/) { await _db.Create("rtc_ice_candidates", new DBIceCandidate { ChannelId = channelId, Username = username, Candidate = candidate, SdpMid = sdpMid, SdpMLineIndex = sdpMLineIndex, // Direction = direction, CreatedAt = DateTime.UtcNow }); } /// /// Gets all ICE candidates stored for the specified channel in creation order. /// /// The channel whose ICE candidates should be retrieved. /// /// A list of ICE candidates for the channel ordered from oldest to newest. /// 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(); } /// /// Gets ICE candidates for the specified channel that were created by other users /// and match the requested signaling direction. /// /// The channel whose ICE candidates should be retrieved. /// The user to exclude from the results. /// The signaling direction to match. /// /// A list of matching ICE candidates ordered from oldest to newest. /// public async Task> GetIceCandidatesForOthersAsync(string channelId, string username, string direction) { var candidates = await _db.Select("rtc_ice_candidates"); return candidates .Where(x => x.ChannelId == channelId && x.Username != username /*&& x.Direction == direction*/) .OrderBy(x => x.CreatedAt) .ToList(); } /// /// Leaves the active call for the specified channel. In the current implementation, /// the call is only marked inactive when the offer user leaves. /// /// The channel whose call should be left. /// The user leaving the call. public async Task LeaveCallAsync(string channelId, string username) { var activeCalls = await _db.Select("rtc_active_calls"); var activeCall = activeCalls.FirstOrDefault(x => x.ChannelId == channelId && x.IsActive); if (activeCall is null) return; if (activeCall.OfferUser == username) { activeCall.IsActive = false; activeCall.UpdatedAt = DateTime.UtcNow; await _db.Merge(activeCall); } } /// /// Gets all active call records that currently contain an offer. /// /// /// A list of active calls with offers, ordered from newest to oldest. /// public async Task> GetOffersAsync() { var activeCalls = await _db.Select("rtc_active_calls"); return activeCalls .Where(x => x.Offer is not null) .OrderByDescending(x => x.UpdatedAt) .ToList(); } }