From 3129d90318b0ca6f0a3dcd61ba7649f4b821df44 Mon Sep 17 00:00:00 2001 From: jojo aquino Date: Wed, 26 Mar 2025 21:40:30 +0000 Subject: [PATCH] start and stop recording --- .../js/Communication/VideoCall/_VideoCall.js | 10 +- .../Services/ConferenceSheduleService.cs | 22 +++- .../Services/IConferenceSheduleService.cs | 2 +- .../Pages/Participant/VideoCall/Room.cshtml | 118 ++++------------- .../Participant/VideoCall/Room.cshtml.cs | 56 +++++++-- .../Participant/VideoCall/Room.cshtml.js | 119 ++++++++++++++++++ 6 files changed, 210 insertions(+), 117 deletions(-) create mode 100644 EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml.js diff --git a/EnotaryoPH/EnotaryoPH.Web/Assets/js/Communication/VideoCall/_VideoCall.js b/EnotaryoPH/EnotaryoPH.Web/Assets/js/Communication/VideoCall/_VideoCall.js index c9d5901..087f6b1 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Assets/js/Communication/VideoCall/_VideoCall.js +++ b/EnotaryoPH/EnotaryoPH.Web/Assets/js/Communication/VideoCall/_VideoCall.js @@ -31,6 +31,7 @@ class VideoCall { this.onCreateLocalVideoStream = options.onCreateLocalVideoStream; this.remoteParticipantStateChanged = options.remoteParticipantStateChanged; this.remoteVideoIsAvailableChanged = options.remoteVideoIsAvailableChanged; + this.onGetServerCallID = options.onGetServerCallID; const tokenCredential = new AzureCommunicationTokenCredential(userAccessToken); @@ -58,15 +59,10 @@ class VideoCall { const localVideoStream = await this.createLocalVideoStream(); const videoOptions = localVideoStream ? { localVideoStreams: [localVideoStream] } : undefined; - const roomCallLocator = { roomId: roomId }; - let call = this.callAgent.join(roomCallLocator, { videoOptions }); - this.subscribeToCall(call); - this.callAgent.on('callsUpdated', e => { e.added.forEach((addedCall) => { addedCall.on('stateChanged', () => { if (addedCall.state === 'Connected') { - debugger; addedCall.info.getServerCallId().then(result => { this.onGetServerCallID?.(result); }).catch(err => { @@ -76,6 +72,10 @@ class VideoCall { }); }); }); + + const roomCallLocator = { roomId: roomId }; + let call = this.callAgent.join(roomCallLocator, { videoOptions }); + this.subscribeToCall(call); } catch (error) { console.error(error); } diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Services/ConferenceSheduleService.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Services/ConferenceSheduleService.cs index c848c5c..ba412f1 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Common/Services/ConferenceSheduleService.cs +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Services/ConferenceSheduleService.cs @@ -14,13 +14,15 @@ namespace EnotaryoPH.Web.Common.Services private readonly CommunicationIdentityClient _communicationIdentityClient; private readonly RoomsClient _roomsClient; private readonly CallAutomationClient _callAutomationClient; + private readonly IConfiguration _configuration; - public ConferenceSheduleService(NotaryoDBContext dbContext, CommunicationIdentityClient communicationIdentityClient, RoomsClient roomsClient, CallAutomationClient callAutomationClient) + public ConferenceSheduleService(NotaryoDBContext dbContext, CommunicationIdentityClient communicationIdentityClient, RoomsClient roomsClient, CallAutomationClient callAutomationClient, IConfiguration configuration) { _dbContext = dbContext; _communicationIdentityClient = communicationIdentityClient; _roomsClient = roomsClient; _callAutomationClient = callAutomationClient; + _configuration = configuration; } public async Task GetOrCreateScheduleIDAsync(Guid transaction_UID) @@ -105,19 +107,20 @@ namespace EnotaryoPH.Web.Common.Services _dbContext.Update(schedule); } _dbContext.SaveChanges(); + schedule_UID = schedule.LawyerVideoConferenceSchedule_UID; } } return schedule_UID; } - public async Task StartRecordingAsync(Guid transaction_UID) + public async Task StartRecordingAsync(Guid transaction_UID, string serverCallID) { var transactionEntity = _dbContext.Transactions .FirstOrDefault(t => t.Transaction_UID == transaction_UID) ?? throw new ArgumentException("Transaction not found."); var existingSchedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID) ?? throw new ArgumentException("Schedule not found."); - if (string.IsNullOrEmpty(existingSchedule.ServerCallID)) + if (string.IsNullOrEmpty(existingSchedule.ServerCallID) && string.IsNullOrEmpty(serverCallID)) { throw new ArgumentException("ServerCallID is not set for this transaction."); } @@ -127,8 +130,18 @@ namespace EnotaryoPH.Web.Common.Services return; } + if (!string.IsNullOrEmpty(serverCallID)) + { + existingSchedule.ServerCallID = serverCallID; + } + CallLocator callLocator = new ServerCallLocator(existingSchedule.ServerCallID); - var recordingResult = await _callAutomationClient.GetCallRecording().StartAsync(new StartRecordingOptions(callLocator) { RecordingStorage = RecordingStorage.CreateAzureBlobContainerRecordingStorage(new Uri("")) }); + var uri = _configuration.GetValue("UriRecordingBloblContainer") ?? string.Empty; + var recordingResult = await _callAutomationClient + .GetCallRecording().StartAsync(new StartRecordingOptions(callLocator) + { + RecordingStorage = RecordingStorage.CreateAzureBlobContainerRecordingStorage(new Uri(uri)) + }); existingSchedule.RecordingID = recordingResult.Value.RecordingId; _dbContext.Update(existingSchedule); _dbContext.SaveChanges(); @@ -150,7 +163,6 @@ namespace EnotaryoPH.Web.Common.Services throw new ArgumentException("Recording ID is not set for this transaction."); } - CallLocator callLocator = new ServerCallLocator(existingSchedule.ServerCallID); await _callAutomationClient.GetCallRecording().StopAsync(existingSchedule.RecordingID); } diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Services/IConferenceSheduleService.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Services/IConferenceSheduleService.cs index ab3753c..ec03b12 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Common/Services/IConferenceSheduleService.cs +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Services/IConferenceSheduleService.cs @@ -4,7 +4,7 @@ { Task GetOrCreateScheduleIDAsync(Guid transaction_UID); - Task StartRecordingAsync(Guid transaction_UID); + Task StartRecordingAsync(Guid transaction_UID, string serverCallID); Task StopRecordingAsync(Guid transaction_UID); } diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml b/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml index 5ac0d51..28c507c 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml @@ -2,6 +2,7 @@ @using System.Text.Json @model EnotaryoPH.Web.Pages.Participant.VideoCall.RoomModel @{ + Layout = "_Blank"; } @section Head { @@ -64,7 +65,19 @@ }
-
+
+
+ 32:04 +
+
+ + +
+
@@ -74,100 +87,17 @@
- + + + + + +
+ + +
@section Scripts { - + } diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml.cs b/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml.cs index d61ac1d..d3d319a 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml.cs +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml.cs @@ -7,6 +7,7 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall { public class RoomModel : PageModel { + private const int VideoConferenceExpirationInHours = 2; private readonly IConferenceSheduleService _conferenceSheduleService; private readonly ICurrentUserService _currentUserService; private readonly NotaryoDBContext _dbContext; @@ -22,10 +23,7 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall public async Task OnGetAsync() { - _Transaction = _dbContext.Transactions - .Include(t => t.TransactionSignatories) - .Include(t => t.Principal) - .FirstOrDefault(t => t.Transaction_UID == Transaction_UID); + LoadTransaction(); if (_Transaction == null) { return NotFound(); @@ -48,12 +46,20 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall return Redirect("/"); } - _LawyerVideoConferenceSchedule = _dbContext.LawyerVideoConferenceSchedules - .Include(sched => sched.LawyerVideoConferenceParticipants) - .ThenInclude(p => p.Participant) - .Include(sched => sched.Lawyer) - .ThenInclude(l => l.User) - .FirstOrDefault(sched => sched.LawyerVideoConferenceSchedule_UID == schedule_UID); + LoadVideoConferenceSchedule(schedule_UID); + + if ((DateTime.UtcNow - _LawyerVideoConferenceSchedule.MeetingDate).TotalHours > VideoConferenceExpirationInHours) + { + if (!_LawyerVideoConferenceSchedule.Status.IsInList(VideoConferenceStatus.Abandoned, VideoConferenceStatus.Completed)) + { + _LawyerVideoConferenceSchedule.Status = nameof(VideoConferenceStatus.Expired); + _dbContext.Update(_LawyerVideoConferenceSchedule); + _dbContext.SaveChanges(); + } + + TempData["Warning"] = "The video conference has expired."; + return Redirect("/"); + } CommunicationUserToken = currentUser.Role == nameof(UserType.Notary) ? _LawyerVideoConferenceSchedule.MeetingRoomTokenID @@ -66,7 +72,33 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall return Page(); } - public async Task OnPostStartRecordingAsync() => await _conferenceSheduleService.StartRecordingAsync(Transaction_UID); + public async Task OnPostStartRecordingAsync() + { + LoadTransaction(); + var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID); + LoadVideoConferenceSchedule(schedule_UID); + await _conferenceSheduleService.StartRecordingAsync(Transaction_UID, ServerCallID); + } + + public async Task OnPostStopRecordingAsync() + { + LoadTransaction(); + var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID); + LoadVideoConferenceSchedule(schedule_UID); + await _conferenceSheduleService.StopRecordingAsync(Transaction_UID); + } + + private void LoadTransaction() => _Transaction = _dbContext.Transactions + .Include(t => t.TransactionSignatories) + .Include(t => t.Principal) + .FirstOrDefault(t => t.Transaction_UID == Transaction_UID); + + private void LoadVideoConferenceSchedule(Guid schedule_UID) => _LawyerVideoConferenceSchedule = _dbContext.LawyerVideoConferenceSchedules + .Include(sched => sched.LawyerVideoConferenceParticipants) + .ThenInclude(p => p.Participant) + .Include(sched => sched.Lawyer) + .ThenInclude(l => l.User) + .FirstOrDefault(sched => sched.LawyerVideoConferenceSchedule_UID == schedule_UID); public string CommunicationRoomId { get; private set; } @@ -101,7 +133,7 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall } [BindProperty(SupportsGet = true)] - public string ServerCallingID { get; set; } + public string ServerCallID { get; set; } [BindProperty(SupportsGet = true)] public Guid Transaction_UID { get; set; } diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml.js b/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml.js new file mode 100644 index 0000000..79ec01f --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Participant/VideoCall/Room.cshtml.js @@ -0,0 +1,119 @@ +"use strict"; +(async function () { + let + control_approve = document.getElementById("Approve"), + control_communicationRoomId = document.getElementById("CommunicationRoomId"), + control_communicationUserId = document.getElementById("CommunicationUserId"), + control_communicationUserToken = document.getElementById("CommunicationUserToken"), + control_participants = document.getElementById("Participants"), + control_reject = document.getElementById("Reject"), + control_templateVideo = document.getElementById("templateVideo"), + control_videoGrid = document.getElementById("VideoGrid"), + control_videoGridContainer = document.getElementById("videoGrid-container"), + control_viewDocument = document.getElementById("ViewDocument"), + control_serverCallIID = document.getElementById("ServerCallID"), + x = 1; + + let participants = JSON.parse(control_participants.value); + + async function _initVideoCall() { + let userAccessToken = control_communicationUserToken.value; + const videoCall = jfa.communication.videocall; + let options = { + onCreateLocalVideoStream: function (el) { + if (el) { + el.style['transform'] = ''; + let notary = participants.find(p => p.RoomUserID == control_communicationUserId.value); + document.getElementById(notary.Id).appendChild(el); + } + }, + remoteVideoIsAvailableChanged: function (e) { + let participant = participants.find(p => p.RoomUserID == e.participantId); + if (participant) { + e.el.querySelector('video').style['object-fit'] = 'cover'; + document.getElementById(participant.Id).appendChild(e.el); + } + }, + onGetServerCallID: function (serverCallId) { + let url = jfa.utilities.routing.getCurrentURLWithHandler("StartRecording"); + url.searchParams.append("ServerCallID", serverCallId); + jfa.utilities.request.post(url, {}) + .then(resp => { + if (resp.ok === true) { + console.log("started recording"); + } + }) + .catch(err => console.error(err)); + } + }; + await videoCall.init(userAccessToken, options); + videoCall.joinRoom(control_communicationRoomId.value); + } + + function _updateGrid() { + control_videoGrid.innerHTML = ''; + + participants.forEach((participant, index) => { + const col = document.createElement('div'); + col.className = 'participant-col'; + const tmpl = control_templateVideo.cloneNode(true).content; + const vidcontainer = tmpl.querySelector(".video-container") + if (vidcontainer) { + vidcontainer.id = participant.Id; + vidcontainer.classList.add(participant.Id); + vidcontainer.classList.add(participant.Type == 'Notary' ? 'local-video-container' : 'remote-video-container'); + } + let participantName = tmpl.querySelector(".participant-name") + if (participantName) { + participantName.textContent = participant.DisplayName; + } + col.appendChild(tmpl); + control_videoGrid.appendChild(col); + }); + + // Dynamically adjust grid columns based on participant count + const count = participants.length; + const cols = count <= 2 ? 'col-12 col-sm-12 offset-md-1 col-md-10 offset-lg-0 col-lg-6' : + count <= 4 ? 'col-6 col-sm-6 col-md-6 col-lg-6 col-xl-6' : + count <= 8 ? 'col-6 col-md-6 col-lg-4 col-xl-4' : + count <= 9 ? 'col-4' : + 'col-6 col-sm-4 col-lg-3'; + + document.querySelectorAll('.participant-col').forEach(el => { + el.className = `participant-col ${cols}`; + }); + + const fluid = count <= 2 ? 'container-fluid' : + count <= 8 ? 'container-xxl' : + 'container-xl' + control_videoGridContainer.className = `${fluid} py-3`; + } + + function _bindEvents() { + window.addEventListener('resize', _updateGrid); + control_viewDocument.addEventListener("click", function () { + alert('not yet implemented'); + }); + control_approve.addEventListener("click", function () { + let url = jfa.utilities.routing.getCurrentURLWithHandler("StopRecording"); + jfa.utilities.request.post(url, {}) + .then(resp => { + if (resp.ok === true) { + console.log("stopped recording"); + } + }) + .catch(err => console.error(err)); + }); + control_reject.addEventListener("click", function () { + alert('not yet implemented'); + }); + } + + async function _init() { + _bindEvents(); + _updateGrid(); + await _initVideoCall(); + } + + await _init(); +})(); \ No newline at end of file