start and stop recording

This commit is contained in:
jojo aquino 2025-03-26 21:40:30 +00:00
parent e388c18e03
commit 3129d90318
6 changed files with 210 additions and 117 deletions

View File

@ -31,6 +31,7 @@ class VideoCall {
this.onCreateLocalVideoStream = options.onCreateLocalVideoStream; this.onCreateLocalVideoStream = options.onCreateLocalVideoStream;
this.remoteParticipantStateChanged = options.remoteParticipantStateChanged; this.remoteParticipantStateChanged = options.remoteParticipantStateChanged;
this.remoteVideoIsAvailableChanged = options.remoteVideoIsAvailableChanged; this.remoteVideoIsAvailableChanged = options.remoteVideoIsAvailableChanged;
this.onGetServerCallID = options.onGetServerCallID;
const tokenCredential = new AzureCommunicationTokenCredential(userAccessToken); const tokenCredential = new AzureCommunicationTokenCredential(userAccessToken);
@ -58,15 +59,10 @@ class VideoCall {
const localVideoStream = await this.createLocalVideoStream(); const localVideoStream = await this.createLocalVideoStream();
const videoOptions = localVideoStream ? { localVideoStreams: [localVideoStream] } : undefined; 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 => { this.callAgent.on('callsUpdated', e => {
e.added.forEach((addedCall) => { e.added.forEach((addedCall) => {
addedCall.on('stateChanged', () => { addedCall.on('stateChanged', () => {
if (addedCall.state === 'Connected') { if (addedCall.state === 'Connected') {
debugger;
addedCall.info.getServerCallId().then(result => { addedCall.info.getServerCallId().then(result => {
this.onGetServerCallID?.(result); this.onGetServerCallID?.(result);
}).catch(err => { }).catch(err => {
@ -76,6 +72,10 @@ class VideoCall {
}); });
}); });
}); });
const roomCallLocator = { roomId: roomId };
let call = this.callAgent.join(roomCallLocator, { videoOptions });
this.subscribeToCall(call);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }

View File

@ -14,13 +14,15 @@ namespace EnotaryoPH.Web.Common.Services
private readonly CommunicationIdentityClient _communicationIdentityClient; private readonly CommunicationIdentityClient _communicationIdentityClient;
private readonly RoomsClient _roomsClient; private readonly RoomsClient _roomsClient;
private readonly CallAutomationClient _callAutomationClient; 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; _dbContext = dbContext;
_communicationIdentityClient = communicationIdentityClient; _communicationIdentityClient = communicationIdentityClient;
_roomsClient = roomsClient; _roomsClient = roomsClient;
_callAutomationClient = callAutomationClient; _callAutomationClient = callAutomationClient;
_configuration = configuration;
} }
public async Task<Guid> GetOrCreateScheduleIDAsync(Guid transaction_UID) public async Task<Guid> GetOrCreateScheduleIDAsync(Guid transaction_UID)
@ -105,19 +107,20 @@ namespace EnotaryoPH.Web.Common.Services
_dbContext.Update(schedule); _dbContext.Update(schedule);
} }
_dbContext.SaveChanges(); _dbContext.SaveChanges();
schedule_UID = schedule.LawyerVideoConferenceSchedule_UID;
} }
} }
return schedule_UID; return schedule_UID;
} }
public async Task StartRecordingAsync(Guid transaction_UID) public async Task StartRecordingAsync(Guid transaction_UID, string serverCallID)
{ {
var transactionEntity = _dbContext.Transactions var transactionEntity = _dbContext.Transactions
.FirstOrDefault(t => t.Transaction_UID == transaction_UID) ?? throw new ArgumentException("Transaction not found."); .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."); 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."); throw new ArgumentException("ServerCallID is not set for this transaction.");
} }
@ -127,8 +130,18 @@ namespace EnotaryoPH.Web.Common.Services
return; return;
} }
if (!string.IsNullOrEmpty(serverCallID))
{
existingSchedule.ServerCallID = serverCallID;
}
CallLocator callLocator = new ServerCallLocator(existingSchedule.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<string>("UriRecordingBloblContainer") ?? string.Empty;
var recordingResult = await _callAutomationClient
.GetCallRecording().StartAsync(new StartRecordingOptions(callLocator)
{
RecordingStorage = RecordingStorage.CreateAzureBlobContainerRecordingStorage(new Uri(uri))
});
existingSchedule.RecordingID = recordingResult.Value.RecordingId; existingSchedule.RecordingID = recordingResult.Value.RecordingId;
_dbContext.Update(existingSchedule); _dbContext.Update(existingSchedule);
_dbContext.SaveChanges(); _dbContext.SaveChanges();
@ -150,7 +163,6 @@ namespace EnotaryoPH.Web.Common.Services
throw new ArgumentException("Recording ID is not set for this transaction."); throw new ArgumentException("Recording ID is not set for this transaction.");
} }
CallLocator callLocator = new ServerCallLocator(existingSchedule.ServerCallID);
await _callAutomationClient.GetCallRecording().StopAsync(existingSchedule.RecordingID); await _callAutomationClient.GetCallRecording().StopAsync(existingSchedule.RecordingID);
} }

View File

@ -4,7 +4,7 @@
{ {
Task<Guid> GetOrCreateScheduleIDAsync(Guid transaction_UID); Task<Guid> GetOrCreateScheduleIDAsync(Guid transaction_UID);
Task StartRecordingAsync(Guid transaction_UID); Task StartRecordingAsync(Guid transaction_UID, string serverCallID);
Task StopRecordingAsync(Guid transaction_UID); Task StopRecordingAsync(Guid transaction_UID);
} }

View File

@ -2,6 +2,7 @@
@using System.Text.Json @using System.Text.Json
@model EnotaryoPH.Web.Pages.Participant.VideoCall.RoomModel @model EnotaryoPH.Web.Pages.Participant.VideoCall.RoomModel
@{ @{
Layout = "_Blank";
} }
@section Head { @section Head {
@ -64,7 +65,19 @@
} }
<div class="container-fluid py-3" id="videoGrid-container"> <div class="container-fluid py-3" id="videoGrid-container">
<div class="row g-2" id="videoGrid"> <div class="d-flex mb-1">
<div>
<span>32:04</span>
</div>
<div class="flex-fill"></div>
<div>
<a href="#" id="ViewDocument" class="btn btn-sm btn-secondary">View Document</a>
<a href="#" id="Approve" class="btn btn-sm btn-success">Approve</a>
<a href="#" id="Reject" class="btn btn-sm btn-danger">Reject</a>
</div>
</div>
<div class="row g-2" id="VideoGrid">
</div> </div>
</div> </div>
@ -74,100 +87,17 @@
</div> </div>
</template> </template>
<input type="hidden" value="@JsonSerializer.Serialize(Model.Participants)" /> <input type="hidden" id="Participants" value="@(System.Web.HttpUtility.JavaScriptStringEncode(JsonSerializer.Serialize(Model.Participants)).Replace("\\", ""))" />
<input type="hidden" id="CommunicationUserToken" value="@Model.CommunicationUserToken" />
<input type="hidden" id="CommunicationUserId" value="@Model.CommunicationUserId" />
<input type="hidden" id="CommunicationRoomId" value="@Model.CommunicationRoomId" />
<form method="post" asp-page-handler="StartRecording">
<input type="hidden" asp-for="ServerCallID" />
<input type="hidden" asp-for="Transaction_UID" />
</form>
@section Scripts { @section Scripts {
<script type="text/javascript" src="/dist/_jfa.js"></script> <script type="text/javascript" src="/dist/_jfa.js"></script>
<script> <script src="~/Pages/Participant/VideoCall/Room.cshtml.js" asp-append-version="true"></script>
let participants = JSON.parse('@Html.Raw(JsonSerializer.Serialize(Model.Participants))');
function updateGrid() {
const videoGrid = document.getElementById('videoGrid');
videoGrid.innerHTML = '';
participants.forEach((participant, index) => {
const col = document.createElement('div');
col.className = 'participant-col';
var tmpl = document.getElementById("templateVideo").cloneNode(true).content;
let 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);
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'
document.getElementById('videoGrid-container').className = `${fluid} py-3`;
}
async function initVideoCall() {
let userAccessToken = '@Model.CommunicationUserToken';
const videoCall = jfa.communication.videocall;
let options = {
onCreateLocalVideoStream: function(el) {
if (el) {
el.style['transform'] = '';
let notary = participants.find(p => p.RoomUserID == '@Model.CommunicationUserId');
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(e) {
debugger;
let url = jfa.utilities.routing.getCurrentURLWithHandler("StartRecording");
debugger;
url.searchParams.append("ServerCallID", e.serverCallId || "");
jfa.utilities.request.post(url, {})
.then(resp => {
if (resp.ok === true) {
jfa.page.reload();
}
})
.catch(err => console.error(err));
}
};
await videoCall.init(userAccessToken, options);
videoCall.joinRoom('@Model.CommunicationRoomId');
}
if (document.readyState !== 'loading') {
init();
} else {
document.addEventListener('DOMContentLoaded', init);
}
async function init() {
updateGrid();
window.addEventListener('resize', updateGrid);
await initVideoCall();
}
</script>
} }

View File

@ -7,6 +7,7 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
{ {
public class RoomModel : PageModel public class RoomModel : PageModel
{ {
private const int VideoConferenceExpirationInHours = 2;
private readonly IConferenceSheduleService _conferenceSheduleService; private readonly IConferenceSheduleService _conferenceSheduleService;
private readonly ICurrentUserService _currentUserService; private readonly ICurrentUserService _currentUserService;
private readonly NotaryoDBContext _dbContext; private readonly NotaryoDBContext _dbContext;
@ -22,10 +23,7 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
public async Task<IActionResult> OnGetAsync() public async Task<IActionResult> OnGetAsync()
{ {
_Transaction = _dbContext.Transactions LoadTransaction();
.Include(t => t.TransactionSignatories)
.Include(t => t.Principal)
.FirstOrDefault(t => t.Transaction_UID == Transaction_UID);
if (_Transaction == null) if (_Transaction == null)
{ {
return NotFound(); return NotFound();
@ -48,12 +46,20 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
return Redirect("/"); return Redirect("/");
} }
_LawyerVideoConferenceSchedule = _dbContext.LawyerVideoConferenceSchedules LoadVideoConferenceSchedule(schedule_UID);
.Include(sched => sched.LawyerVideoConferenceParticipants)
.ThenInclude(p => p.Participant) if ((DateTime.UtcNow - _LawyerVideoConferenceSchedule.MeetingDate).TotalHours > VideoConferenceExpirationInHours)
.Include(sched => sched.Lawyer) {
.ThenInclude(l => l.User) if (!_LawyerVideoConferenceSchedule.Status.IsInList(VideoConferenceStatus.Abandoned, VideoConferenceStatus.Completed))
.FirstOrDefault(sched => sched.LawyerVideoConferenceSchedule_UID == schedule_UID); {
_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) CommunicationUserToken = currentUser.Role == nameof(UserType.Notary)
? _LawyerVideoConferenceSchedule.MeetingRoomTokenID ? _LawyerVideoConferenceSchedule.MeetingRoomTokenID
@ -66,7 +72,33 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
return Page(); 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; } public string CommunicationRoomId { get; private set; }
@ -101,7 +133,7 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
} }
[BindProperty(SupportsGet = true)] [BindProperty(SupportsGet = true)]
public string ServerCallingID { get; set; } public string ServerCallID { get; set; }
[BindProperty(SupportsGet = true)] [BindProperty(SupportsGet = true)]
public Guid Transaction_UID { get; set; } public Guid Transaction_UID { get; set; }

View File

@ -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();
})();