make video conference work

This commit is contained in:
jojo aquino 2025-03-21 21:58:26 +00:00
parent 9ea1a6f3d6
commit 1e89e52b12
26 changed files with 4033 additions and 2158 deletions

5
.gitignore vendored
View File

@ -360,4 +360,7 @@ MigrationBackup/
# Fody - auto-generated XML schema # Fody - auto-generated XML schema
FodyWeavers.xsd FodyWeavers.xsd
/EnotaryoPH/EnotaryoPH.Web/wwwroot/dist/
.parcel*/
/EnotaryoPH/EnotaryoPH.Web/wwwroot/dist

View File

@ -0,0 +1,11 @@
namespace EnotaryoPH.Data.Constants
{
public enum VideoConferenceStatus
{
New = 0,
InProgress = 1,
Expired = 49,
Abandoned = 50,
Completed = 100
}
}

View File

@ -5,23 +5,37 @@ namespace EnotaryoPH.Data.Entities
[Table("Lawyers")] [Table("Lawyers")]
public class Lawyer public class Lawyer
{ {
[Column("LawyerID")] [Column("CommissionExpiration")]
public int LawyerID { get; set; } public DateTime? CommissionExpiration { get; set; }
[Column("Rollnumber")] [Column("CommissionLocation")]
public string? Rollnumber { get; set; } public string? CommissionLocation { get; set; }
[Column("CommissionNumber")]
public string? CommissionNumber { get; set; }
[Column("CreatedOn")]
public DateTime? CreatedOn { get; set; }
[Column("IBPNumber")] [Column("IBPNumber")]
public string? IBPNumber { get; set; } public string? IBPNumber { get; set; }
[Column("Lawyer_UID")]
public Guid? Lawyer_UID { get; set; }
[Column("LawyerID")]
public int LawyerID { get; set; }
public List<LawyerVideoConferenceSchedule> LawyerVideoConferenceSchedules { get; set; }
[Column("MCLEComplianceNumber")] [Column("MCLEComplianceNumber")]
public string? MCLEComplianceNumber { get; set; } public string? MCLEComplianceNumber { get; set; }
[Column("MCLEDate")] [Column("MCLEDate")]
public DateTime? MCLEDate { get; set; } public DateTime? MCLEDate { get; set; }
[Column("PTRNumber")] [Column("OfficeAddress")]
public string? PTRNumber { get; set; } public string? OfficeAddress { get; set; }
[Column("PTRDate")] [Column("PTRDate")]
public DateTime? PTRDate { get; set; } public DateTime? PTRDate { get; set; }
@ -29,31 +43,19 @@ namespace EnotaryoPH.Data.Entities
[Column("PTRlocation")] [Column("PTRlocation")]
public string? PTRlocation { get; set; } public string? PTRlocation { get; set; }
[Column("OfficeAddress")] [Column("PTRNumber")]
public string? OfficeAddress { get; set; } public string? PTRNumber { get; set; }
[Column("UserID")] [Column("Rollnumber")]
public int UserID { get; set; } public string? Rollnumber { get; set; }
[Column("Lawyer_UID")]
public Guid? Lawyer_UID { get; set; }
[Column("CreatedOn")]
public DateTime? CreatedOn { get; set; }
[Column("CommissionNumber")]
public string? CommissionNumber { get; set; }
[Column("CommissionLocation")]
public string? CommissionLocation { get; set; }
[Column("CommissionExpiration")]
public DateTime? CommissionExpiration { get; set; }
[Column("Status")] [Column("Status")]
public string? Status { get; set; } public string? Status { get; set; }
[ForeignKey("UserID")] [ForeignKey("UserID")]
public User User { get; set; } public User User { get; set; }
[Column("UserID")]
public int UserID { get; set; }
} }
} }

View File

@ -5,28 +5,34 @@ namespace EnotaryoPH.Data.Entities
[Table("LawyerVideoConferenceParticipants")] [Table("LawyerVideoConferenceParticipants")]
public class LawyerVideoConferenceParticipant public class LawyerVideoConferenceParticipant
{ {
[Column("LawyerVideoConferenceParticipantID")]
public int LawyerVideoConferenceParticipantID { get; set; }
[Column("LawyerVideoConferenceScheduleID")]
public int LawyerVideoConferenceScheduleID { get; set; }
[Column("ParticipantID")]
public int? ParticipantID { get; set; }
[Column("Status")]
public string? Status { get; set; }
[Column("CreatedOn")] [Column("CreatedOn")]
public DateTime? CreatedOn { get; set; } public DateTime? CreatedOn { get; set; }
[Column("LawyerVideoConferenceParticipant_UID")] [Column("LawyerVideoConferenceParticipant_UID")]
public Guid? LawyerVideoConferenceParticipant_UID { get; set; } public Guid? LawyerVideoConferenceParticipant_UID { get; set; }
[ForeignKey("ParticipantID")] [Column("LawyerVideoConferenceParticipantID")]
public User Participant { get; set; } public int LawyerVideoConferenceParticipantID { get; set; }
[ForeignKey("LawyerVideoConferenceScheduleID")] [ForeignKey("LawyerVideoConferenceScheduleID")]
public LawyerVideoConferenceSchedule LawyerVideoConferenceSchedule { get; set; } public LawyerVideoConferenceSchedule LawyerVideoConferenceSchedule { get; set; }
[Column("LawyerVideoConferenceScheduleID")]
public int LawyerVideoConferenceScheduleID { get; set; }
[Column("MeetingRoomTokenID")]
public string? MeetingRoomTokenID { get; set; }
[Column("MeetingRoomUserID")]
public string? MeetingRoomUserID { get; set; }
[ForeignKey("ParticipantID")]
public User Participant { get; set; }
[Column("ParticipantID")]
public int? ParticipantID { get; set; }
[Column("Status")]
public string? Status { get; set; }
} }
} }

View File

@ -5,27 +5,39 @@ namespace EnotaryoPH.Data.Entities
[Table("LawyerVideoConferenceSchedule")] [Table("LawyerVideoConferenceSchedule")]
public class LawyerVideoConferenceSchedule public class LawyerVideoConferenceSchedule
{ {
[Column("LawyerVideoConferenceScheduleID")] [Column("CreatedOn")]
public int LawyerVideoConferenceScheduleID { get; set; } public DateTime? CreatedOn { get; set; }
[ForeignKey("LawyerID")]
public Lawyer Lawyer { get; set; }
[Column("LawyerID")] [Column("LawyerID")]
public int LawyerID { get; set; } public int LawyerID { get; set; }
[Column("TransactionID")] public List<LawyerVideoConferenceParticipant> LawyerVideoConferenceParticipants { get; set; }
public int TransactionID { get; set; }
[Column("MeetingDate")]
public DateTime MeetingDate { get; set; }
[Column("CreatedOn")]
public DateTime? CreatedOn { get; set; }
[Column("LawyerVideoConferenceSchedule_UID")] [Column("LawyerVideoConferenceSchedule_UID")]
public Guid LawyerVideoConferenceSchedule_UID { get; set; } public Guid LawyerVideoConferenceSchedule_UID { get; set; }
[Column("LawyerVideoConferenceScheduleID")]
public int LawyerVideoConferenceScheduleID { get; set; }
[Column("MeetingDate")]
public DateTime MeetingDate { get; set; }
[Column("MeetingRoomID")]
public string? MeetingRoomID { get; set; }
[Column("MeetingRoomTokenID")]
public string? MeetingRoomTokenID { get; set; }
[Column("MeetingRoomUserID")]
public string? MeetingRoomUserID { get; set; }
[Column("Status")] [Column("Status")]
public string? Status { get; set; } public string? Status { get; set; }
public List<LawyerVideoConferenceParticipant> LawyerVideoConferenceParticipants { get; set; } [Column("TransactionID")]
public int TransactionID { get; set; }
} }
} }

View File

@ -0,0 +1,213 @@
import { AzureCommunicationTokenCredential } from '@azure/communication-common';
import { CallClient, LocalVideoStream, VideoStreamRenderer } from '@azure/communication-calling';
class VideoCall {
constructor() {
this.callClient = null;
this.callAgent = null;
this.deviceManager = null;
this.localVideoStream = null;
this.call = null;
this.videoElement = document.createElement('video');
this.videoElement.setAttribute('autoplay', '');
this.videoElement.setAttribute('muted', '');
this.stateChangedCallback = null;
this.remoteParticipantsUpdated = null;
this.isLocalVideoStartedChanged = null;
this.localVideoStreamsUpdated = null;
this.idChanged = null;
this.onCreateLocalVideoStream = null;
this.remoteParticipantStateChanged = null;
this.remoteVideoIsAvailableChanged = null;
}
async init(userAccessToken, options) {
this.stateChangedCallback = options.stateChangedCallback;
this.remoteParticipantsUpdated = options.remoteParticipantsUpdated;
this.isLocalVideoStartedChanged = options.isLocalVideoStartedChanged;
this.localVideoStreamsUpdated = options.localVideoStreamsUpdated;
this.idChanged = options.idChanged;
this.onCreateLocalVideoStream = options.onCreateLocalVideoStream;
this.remoteParticipantStateChanged = options.remoteParticipantStateChanged;
this.remoteVideoIsAvailableChanged = options.remoteVideoIsAvailableChanged;
const tokenCredential = new AzureCommunicationTokenCredential(userAccessToken);
this.callClient = new CallClient();
this.callAgent = await this.callClient.createCallAgent(tokenCredential);
this.deviceManager = await this.callClient.getDeviceManager();
await this.deviceManager.askDevicePermission({ audio: true, video: true });
const cameras = await this.deviceManager.getCameras();
this.localVideoStream = new LocalVideoStream(cameras[0]);
}
async startLocalVideo() {
if (this.localVideoStream) {
//const localVideoOptions = new VideoStreamOptions();
await this.callAgent.startCall([this.callAgent.identity], { video: [this.localVideoStream] });
}
}
stopLocalVideo() {
if (this.call) {
this.call.stopVideo(this.localVideoStream);
}
}
async joinRoom(roomId) {
// Assuming you have a room ID and call the appropriate ACS API to join a room
// This is just a placeholder, replace with actual logic
console.log('Joining room:', roomId);
if (this.callAgent) {
try {
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);
} catch (error) {
console.error(error);
}
}
}
async subscribeToCall(call) {
try {
call.on('idChanged', () => {
this.idChanged?.(call.id);
});
call.on('stateChanged', async () => {
this.stateChangedCallback?.(call.state);
});
call.on('isLocalVideoStartedChanged', () => {
this.isLocalVideoStartedChanged?.(call.isLocalVideoStarted);
});
call.on('localVideoStreamsUpdated', e => {
this.localVideoStreamsUpdated?.(e);
});
// Subscribe to the call's 'remoteParticipantsUpdated' event to be
// notified when new participants are added to the call or removed from the call.
call.on('remoteParticipantsUpdated', e => {
this.remoteParticipantsUpdated?.(e);
e.added.forEach(remoteParticipant => {
this.subscribeToRemoteParticipant(remoteParticipant)
});
// Unsubscribe from participants that are removed from the call
e.removed.forEach(remoteParticipant => {
console.log('Remote participant removed from the call.');
});
});
call.localVideoStreams.forEach(async (lvs) => {
this.localVideoStream = lvs;
await this.displayLocalVideoStream(lvs);
});
// Inspect the call's current remote participants and subscribe to them.
call.remoteParticipants.forEach(remoteParticipant => {
this.subscribeToRemoteParticipant(remoteParticipant);
});
} catch (error) {
console.error(error);
}
}
subscribeToRemoteParticipant(remoteParticipant) {
try {
// Inspect the initial remoteParticipant.state value.
console.log(`Remote participant state: ${remoteParticipant.state}`);
// Subscribe to remoteParticipant's 'stateChanged' event for value changes.
remoteParticipant.on('stateChanged', () => {
console.log(`Remote participant state changed: ${remoteParticipant.state}`, JSON.stringify(remoteParticipant));
this.remoteParticipantStateChanged?.(remoteParticipant.state);
});
// Inspect the remoteParticipants's current videoStreams and subscribe to them.
remoteParticipant.videoStreams.forEach(remoteVideoStream => {
this.subscribeToRemoteVideoStream(remoteVideoStream);
});
// Subscribe to the remoteParticipant's 'videoStreamsUpdated' event to be
// notified when the remoteParticipant adds new videoStreams and removes video streams.
remoteParticipant.on('videoStreamsUpdated', e => {
// Subscribe to new remote participant's video streams that were added.
e.added.forEach(remoteVideoStream => {
this.subscribeToRemoteVideoStream(remoteVideoStream);
});
// Unsubscribe from remote participant's video streams that were removed.
e.removed.forEach(remoteVideoStream => {
console.log('Remote participant video stream was removed.');
})
});
} catch (error) {
console.error(error);
}
}
async subscribeToRemoteVideoStream(remoteVideoStream) {
let renderer = new VideoStreamRenderer(remoteVideoStream);
let view;
const createView = async () => {
// Create a renderer view for the remote video stream.
view = await renderer.createView();
this.remoteVideoIsAvailableChanged?.({
isAvailable: remoteVideoStream.isAvailable,
participantId: remoteVideoStream.tsParticipantId,
el: view.target
});
}
// Remote participant has switched video on/off
remoteVideoStream.on('isAvailableChanged', async () => {
try {
if (remoteVideoStream.isAvailable) {
await createView();
} else {
view?.dispose();
}
} catch (e) {
console.error(e);
}
});
// Remote participant has video on initially.
if (remoteVideoStream.isAvailable) {
try {
await createView();
} catch (e) {
console.error(e);
}
}
}
async createLocalVideoStream() {
const camera = (await this.deviceManager.getCameras())[0];
if (camera) {
return new LocalVideoStream(camera);
} else {
console.error(`No camera device found on the system`);
}
}
leaveRoom() {
if (this.call) {
this.call.hangUp();
}
}
async displayLocalVideoStream(lvs) {
try {
let localVideoStreamRenderer = new VideoStreamRenderer(lvs);
const view = await localVideoStreamRenderer.createView();
this.onCreateLocalVideoStream?.(view.target);
} catch (error) {
console.error(error);
}
}
}
export default VideoCall;

View File

@ -1,5 +1,4 @@
jfa.components.dialog = (function () { function _confirm({ message, title, callbackyes, callbackno, yesLabel, noLabel }) {
function _confirm({ message, title, callbackyes, callbackno, yesLabel, noLabel }) {
const dialogTemplate = jfa.page.getDialogTemplate().content; const dialogTemplate = jfa.page.getDialogTemplate().content;
let spanTitle = dialogTemplate.querySelector(".modal__title__text"); let spanTitle = dialogTemplate.querySelector(".modal__title__text");
@ -34,7 +33,6 @@
modal.show(); modal.show();
} }
return { export default {
confirm: _confirm confirm: _confirm
}; };
})();

View File

@ -1,5 +1,4 @@
jfa.page = (function () {  function _getAlertContainer() {
function _getAlertContainer() {
return document.getElementById("ContainerAlert"); return document.getElementById("ContainerAlert");
} }
@ -23,12 +22,13 @@
window.location.reload(); window.location.reload();
} }
return { const page = {
getAlertContainer: _getAlertContainer, getAlertContainer: _getAlertContainer,
getAlertTemplate: _getAlertTemplate, getAlertTemplate: _getAlertTemplate,
getAntiForgeryToken: _getAntiForgeryToken, getAntiForgeryToken: _getAntiForgeryToken,
getDialogContainer: _getDialogContainer, getDialogContainer: _getDialogContainer,
getDialogTemplate: _getDialogTemplate, getDialogTemplate: _getDialogTemplate,
reload: reload reload: reload
}; }
})();
export default { page };

View File

@ -1,4 +1,4 @@
jfa.utilities.element = (function () { 
function disable(element) { function disable(element) {
if (!element) { if (!element) {
return; return;
@ -28,10 +28,9 @@
element.hidden = false; element.hidden = false;
} }
return { export default {
disable: disable, disable: disable,
enable: enable, enable: enable,
hide: hide, hide: hide,
show: show show: show
}; };
})();

View File

@ -1,5 +1,4 @@
jfa.utilities.request = (function () { 
async function del(url, data) { async function del(url, data) {
return await _fetch(url, data, "DELETE"); return await _fetch(url, data, "DELETE");
} }
@ -21,8 +20,7 @@
return await _fetch(url, data, "POST"); return await _fetch(url, data, "POST");
} }
return { export default {
delete: del, delete: del,
post: post post: post
}; };
})();

View File

@ -1,5 +1,4 @@
jfa.utilities.routing = (function () { function getCurrentURL() {
function getCurrentURL() {
return new URL(window.location.origin + window.location.pathname); return new URL(window.location.origin + window.location.pathname);
} }
@ -9,8 +8,9 @@
return url; return url;
} }
return { export default {
getCurrentURL: getCurrentURL, getCurrentURL: getCurrentURL,
getCurrentURLWithHandler: getCurrentURLWithHandler getCurrentURLWithHandler: getCurrentURLWithHandler
}; }
})();

View File

@ -1,5 +1,25 @@
"use strict"; "use strict";
var jfa = {
components: {}, import page from "../../Assets/js/Page/_Page.js";
utilities: {} import routing from "../../Assets/js/Utilities/Routing/_Routing.js";
import element from "../../Assets/js/Utilities/Element/_Element.js";
import request from "../../Assets/js/Utilities/Request/_Request.js";
import dialog from "../../Assets/js/Components/Dialog/_Dialog.js";
import videocall from "../../Assets/js/Communication/VideoCall/_VideoCall.js";
const jfa = {
components: {
dialog
},
utilities: {
routing,
element,
request
},
page,
communication: {
videocall: new videocall()
}
}; };
export default { jfa };
window.jfa = jfa;

View File

@ -8,6 +8,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Azure.Communication.Identity" Version="1.3.1" />
<PackageReference Include="Azure.Communication.Rooms" Version="1.1.1" />
<PackageReference Include="CompreFace.NET.Sdk" Version="1.0.2" /> <PackageReference Include="CompreFace.NET.Sdk" Version="1.0.2" />
<PackageReference Include="Coravel" Version="6.0.2" /> <PackageReference Include="Coravel" Version="6.0.2" />
<PackageReference Include="Coravel.Mailer" Version="7.1.0" /> <PackageReference Include="Coravel.Mailer" Version="7.1.0" />
@ -30,5 +32,7 @@
<Folder Include="wwwroot\lib\signalr\" /> <Folder Include="wwwroot\lib\signalr\" />
</ItemGroup> </ItemGroup>
<Target Name="ParcelBuild" BeforeTargets="Build">
<Exec Command="npm run build" />
</Target>
</Project> </Project>

View File

@ -2,3 +2,60 @@
@model EnotaryoPH.Web.Pages.Notary.Dashboard.DashboardModel @model EnotaryoPH.Web.Pages.Notary.Dashboard.DashboardModel
@{ @{
} }
@section Head {
<link href="~/lib/fontawesome-free-6.7.1-web/css/all.min.css" rel="stylesheet" />
}
<section class="my-5">
<div class="container">
<div class="row">
<div class="col-2 col-sm-1 col-md-3 g-0">
<div class="sidemenu">
<div class="align-items-center sidemenu__menuitem"><a class="d-flex flex-grow-1 justify-content-center align-items-center justify-content-md-start p-1 text-decoration-none" href="#available-transactions"><i class="far fa-check-circle fs-4 text-success sidemenu__menuitem__icon" style="padding: 5px;"></i><span class="d-none d-md-inline-block ms-1 sidemenu__menuitem__text">Available</span></a></div>
<div class="align-items-center sidemenu__menuitem"><a class="d-flex flex-grow-1 justify-content-center align-items-center justify-content-md-start p-1 text-decoration-none" href="#incomplete-docs"><i class="far fa-clock fs-4 text-warning sidemenu__menuitem__icon" style="padding: 5px;"></i><span class="d-none d-md-inline-block ms-1 sidemenu__menuitem__text">Incomplete</span></a></div>
<div class="align-items-center sidemenu__menuitem"><a class="d-flex flex-grow-1 justify-content-center align-items-center justify-content-md-start p-1 text-decoration-none" href="#identification-docs"><i class="far fa-address-card fs-4 text-dark sidemenu__menuitem__icon" style="padding: 5px;"></i><span class="d-none d-md-inline-block ms-1 sidemenu__menuitem__text">Identification Docs</span></a></div>
</div>
</div>
<div class="col g-0 mx-2">
<div id="available-transactions" class="row g-0 mb-5">
<div class="col">
<h3>Available Transactions</h3>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Type</th>
<th>Date</th>
<th>Link</th>
</tr>
</thead>
<tbody>
@* @if (Model.CompletedDocuments.Count > 0)
{
@foreach (var item in Model.CompletedDocuments)
{
<tr>
<td>@item.Type</td>
<td>@item.Date.ToShortDateString()</td>
<td><a href="@item.Link">View</a></td>
</tr>
}
}
else
{
<tr>
<td colspan="3">
No records to display.
</td>
</tr>
} *@
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</section>

View File

@ -33,7 +33,7 @@ namespace EnotaryoPH.Web.Pages.Participant.Registration
return NotFound(); return NotFound();
} }
var existingUser = _notaryoDBContext.Users.FirstOrDefault(e => e.Email.ToLower() == signatory.Email.ToLower()); var existingUser = _notaryoDBContext.Users.FirstOrDefault(e => e.Email.Equals(signatory.Email, StringComparison.CurrentCultureIgnoreCase));
if (existingUser != null) if (existingUser != null)
{ {
signatory.Status = nameof(SignatoryStatus.Registered); signatory.Status = nameof(SignatoryStatus.Registered);

View File

@ -1,4 +1,160 @@
@page @page "{Transaction_UID}"
@using System.Text.Json
@model EnotaryoPH.Web.Pages.Participant.VideoCall.RoomModel @model EnotaryoPH.Web.Pages.Participant.VideoCall.RoomModel
@{ @{
} }
@section Head {
<style>
.video-container {
position: relative;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
background: #000;
border: 2px solid #444;
width: 100%;
overflow: hidden;
}
.video-container video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover; /* This will maintain the video's original aspect ratio and crop if necessary */
}
.video-element {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
}
.participant-name {
position: absolute;
bottom: 10px;
left: 10px;
color: white;
background: rgba(0,0,0,0.5);
padding: 2px 8px;
border-radius: 4px;
z-index: 1;
}
.controls {
position: fixed;
bottom: 20px;
left: 50%;
/* transform: translateX(-50%); */
background: rgba(0,0,0,0.8);
padding: 10px;
border-radius: 20px;
}
.participant-col {
transition: all 0.3s ease;
display:flex;
justify-content: center;
align-items: center;
}
</style>
}
<div class="container-fluid py-3" id="videoGrid-container">
<div class="row g-2" id="videoGrid">
</div>
</div>
<template id="templateVideo">
<div class="video-container bg-light">
<div class="participant-name">Participant Name</div>
</div>
</template>
<input type="hidden" value="@JsonSerializer.Serialize(Model.Participants)" />
@section Scripts {
<script type="text/javascript" src="~/dist/js/_Jfa.js"></script>
<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);
}
}
}
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

@ -1,3 +1,9 @@
using Azure;
using Azure.Communication;
using Azure.Communication.Identity;
using Azure.Communication.Rooms;
using EnotaryoPH.Data;
using EnotaryoPH.Data.Entities;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
@ -5,8 +11,138 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
{ {
public class RoomModel : PageModel public class RoomModel : PageModel
{ {
public void OnGet() private const string CONNECTION_STRING = "endpoint=https://comm-enotario.asiapacific.communication.azure.com/;accesskey=yqxq5mRByG8aPwZBiT/KFfx6Rr16RR280dC3GVkGaFVNXGk91sIDy04j5jrZfjDZAvdn0fjtyF0kGJPXMGgfKg==";
private readonly ICurrentUserService _currentUserService;
private readonly NotaryoDBContext _dbContext;
private readonly ISession _session;
private readonly CommunicationUserIdentifier? _user1;
private readonly CommunicationUserIdentifier? _user2;
private CommunicationIdentityClient? _communicationIdentityClient;
private LawyerVideoConferenceSchedule _LawyerVideoConferenceSchedule;
private Transaction _Transaction;
public RoomModel(IHttpContextAccessor httpContextAccessor, ICurrentUserService currentUserService, NotaryoDBContext dbContext)
{ {
_session = httpContextAccessor.HttpContext.Session;
_currentUserService = currentUserService;
_dbContext = dbContext;
}
public async Task<IActionResult> OnGetAsync()
{
_Transaction = _dbContext.Transactions
.Include(t => t.TransactionSignatories)
.FirstOrDefault(t => t.Transaction_UID == Transaction_UID);
if (_Transaction == null)
{
return NotFound();
}
_LawyerVideoConferenceSchedule = _dbContext.LawyerVideoConferenceSchedules
.Include(meeting => meeting.LawyerVideoConferenceParticipants)
.ThenInclude(p => p.Participant)
.Include(meeting => meeting.Lawyer)
.ThenInclude(l => l.User)
.FirstOrDefault(meeting => meeting.TransactionID == _Transaction.TransactionID);
if (ShouldCreateRoom())
{
_LawyerVideoConferenceSchedule ??= new LawyerVideoConferenceSchedule
{
LawyerVideoConferenceSchedule_UID = Guid.CreateVersion7(),
CreatedOn = DateTime.UtcNow,
LawyerID = _Transaction.LawyerID.GetValueOrDefault(),
TransactionID = _Transaction.TransactionID,
};
_LawyerVideoConferenceSchedule.MeetingDate = DateTime.UtcNow;
_LawyerVideoConferenceSchedule.Status = nameof(VideoConferenceStatus.New);
var roomParticipants = new List<RoomParticipant>();
foreach (var participant in _LawyerVideoConferenceSchedule.LawyerVideoConferenceParticipants)
{
var attendee = await CommunicationIdentityClient.CreateUserAsync();
roomParticipants.Add(new RoomParticipant(attendee) { Role = ParticipantRole.Attendee });
participant.MeetingRoomTokenID = await GetTokenResponse(attendee);
participant.MeetingRoomUserID = attendee.Value.Id;
}
var presenter = await CommunicationIdentityClient.CreateUserAsync();
roomParticipants.Add(new RoomParticipant(presenter) { Role = ParticipantRole.Presenter });
_LawyerVideoConferenceSchedule.MeetingRoomTokenID = await GetTokenResponse(presenter);
_LawyerVideoConferenceSchedule.MeetingRoomUserID = presenter.Value.Id;
var roomsClient = new RoomsClient(CONNECTION_STRING);
CommunicationRoom room = await roomsClient.CreateRoomAsync(DateTime.Now, DateTime.Now.AddHours(2), roomParticipants);
_LawyerVideoConferenceSchedule.MeetingRoomID = room.Id;
if (_LawyerVideoConferenceSchedule.LawyerVideoConferenceScheduleID == 0)
{
_dbContext.Add(_LawyerVideoConferenceSchedule);
}
else
{
_dbContext.Update(_LawyerVideoConferenceSchedule);
}
_dbContext.SaveChanges();
}
var currentUser = _dbContext.Users.FirstOrDefault(u => u.User_UID == _currentUserService.GetUser_UID());
CommunicationUserToken = currentUser.Role == nameof(UserType.Notary)
? _LawyerVideoConferenceSchedule.MeetingRoomTokenID
: _LawyerVideoConferenceSchedule.LawyerVideoConferenceParticipants.First(u => u.ParticipantID == currentUser.UserID).MeetingRoomTokenID;
CommunicationRoomId = _LawyerVideoConferenceSchedule.MeetingRoomID;
CommunicationUserId = currentUser.Role == nameof(UserType.Notary)
? _LawyerVideoConferenceSchedule.MeetingRoomUserID
: _LawyerVideoConferenceSchedule.LawyerVideoConferenceParticipants.First(u => u.ParticipantID == currentUser.UserID).MeetingRoomUserID;
return Page();
}
private static string GetHashCode(string stringValue) => stringValue.GetHashCode().ToString("X8");
private async Task<string> GetTokenResponse(Response<CommunicationUserIdentifier> user)
{
var tokenResponse = await CommunicationIdentityClient.GetTokenAsync(user, new[] { CommunicationTokenScope.VoIP });
return tokenResponse.Value.Token;
}
private bool ShouldCreateRoom() => _LawyerVideoConferenceSchedule == null || DateTime.UtcNow.Subtract(_LawyerVideoConferenceSchedule.MeetingDate).TotalHours > 1 || _LawyerVideoConferenceSchedule.LawyerVideoConferenceParticipants.Any(p => string.IsNullOrEmpty(p.MeetingRoomTokenID));
public string CommunicationRoomId { get; private set; }
public string CommunicationUserToken { get; private set; }
public string CommunicationUserId { get; set; }
public List<RoomParticipantViewModel> Participants
{
get
{
var signatoryTypes = _Transaction.TransactionSignatories.Where(t => t.UserID > 0).ToDictionary(k => k.UserID, v => v.Type);
var dic = _LawyerVideoConferenceSchedule.LawyerVideoConferenceParticipants.ConvertAll(p => new RoomParticipantViewModel
{
Id = p.LawyerVideoConferenceParticipant_UID.ToString(),
DisplayName = $"{p.Participant.Firstname} {p.Participant.Lastname}",
RoomUserID = p.MeetingRoomUserID,
Type = signatoryTypes.GetValueOrDefault(p.ParticipantID, nameof(UserType.Principal))
});
var host = _LawyerVideoConferenceSchedule.Lawyer.User;
dic.Add(new RoomParticipantViewModel
{
DisplayName = $"{host.Firstname} {host.Lastname}",
Id = Guid.Empty.ToString(),
RoomUserID = _LawyerVideoConferenceSchedule.MeetingRoomUserID,
Type = nameof(UserType.Notary)
});
return dic;
} }
} }
[BindProperty(SupportsGet = true)]
public Guid Transaction_UID { get; set; }
private CommunicationIdentityClient CommunicationIdentityClient => _communicationIdentityClient ??= new CommunicationIdentityClient(CONNECTION_STRING);
}
} }

View File

@ -0,0 +1,10 @@
namespace EnotaryoPH.Web.Pages.Participant.VideoCall
{
public class RoomParticipantViewModel
{
public string DisplayName { get; set; }
public string Id { get; set; }
public string RoomUserID { get; set; }
public string Type { get; set; }
}
}

View File

@ -35,7 +35,7 @@
</ul> </ul>
@if (User.Identity?.IsAuthenticated ?? false) @if (User.Identity?.IsAuthenticated ?? false)
{ {
<a class="btn btn-primary ms-md-2" role="button" href="/Login?handler=Logout">Logout</a> <a class="btn btn-primary ms-md-2" role="button" href="/Login?handler=Logout" data-bs-toggle="tooltip" data-bss-tooltip data-bs-placement="bottom" title="@User.Identity.Name">Logout</a>
} }
else else
{ {

View File

@ -5,7 +5,7 @@
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": true,
"applicationUrl": "http://localhost:5199", "applicationUrl": "http://0.0.0.0:5199",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }
@ -14,7 +14,7 @@
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": true,
"applicationUrl": "https://localhost:7121;http://localhost:5199", "applicationUrl": "https://0.0.0.0:7121;http://0.0.0.0:5199",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

View File

@ -1,33 +0,0 @@
const gulp = require("gulp"),
concat = require('gulp-concat'),
del = require('del'),
sass = require('gulp-sass')(require('sass')),
rename = require("gulp-rename"),
uglify = require('gulp-uglify');
const bundles = {
jfa: {
outputFileName: "wwwroot/dist/js/jfa.js",
inputFiles: [
"Assets/js/_Jfa.js",
"Assets/js/Page/_Page.js",
"Assets/js/Utilities/Routing/_Routing.js",
"Assets/js/Utilities/Element/_Element.js",
"Assets/js/Utilities/Request/_Request.js",
"Assets/js/Components/Dialog/_Dialog.js"
]
},
};
gulp.task('bundle:jfa', function () {
return gulp.src(bundles.jfa.inputFiles, { base: "." })
.pipe(concat(bundles.jfa.outputFileName))
.pipe(gulp.dest("."))
.pipe(uglify())
.pipe(rename({ suffix: '.min' }))
.pipe(gulp.dest("."));
});
gulp.task('default', gulp.series(
gulp.parallel(['bundle:jfa'])
));

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,30 @@
{ {
"name": "jfa home", "name": "eNotaryo",
"version": "1.0.0", "version": "1.0.0",
"description": "JFA Websites", "description": "Electronic Notarization Platform",
"main": "index.js",
"author": "Jose Aquino Jr", "author": "Jose Aquino Jr",
"license": "MIT", "license": "MIT",
"browser": "wwwroot/dist/jfa.js",
"targets": {
"default": {
"context": "browser",
"outputFormat": "global",
"scopeHoist": false
}
},
"browserslist": "> 0.5%",
"scripts": {
"clean": "rimraf wwwroot/dist",
"build:jfa": "npm run clean && parcel build Assets/js/_Jfa.js --dist-dir wwwroot/dist/js --no-source-maps --no-optimize && parcel build Assets/js/_Jfa.js --dist-dir wwwroot/dist/js2 --no-source-maps",
"build": "npm run build:jfa",
"start": "parcel watch Assets/js/_Jfa.js --dist-dir wwwroot/dist/js --no-source-maps"
},
"devDependencies": { "devDependencies": {
"del": "6.1.1", "parcel": "^2.13.3",
"gulp": "^5.0.0", "rimraf": "^6.0.1"
"gulp-concat": "^2.6.1", },
"gulp-rename": "^2.0.0", "dependencies": {
"gulp-sass": "^5.1.0", "@azure/communication-calling": "^1.33.2",
"gulp-uglify": "^3.0.2", "@azure/communication-common": "^2.3.1"
"merge-stream": "^2.0.0",
"sass": "^1.54.9"
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -7,6 +7,13 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnotaryoPH.Web", "EnotaryoP
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnotaryoPH.Data", "EnotaryoPH.Data\EnotaryoPH.Data.csproj", "{7B06785D-1B94-472B-885C-3C2F8834438B}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnotaryoPH.Data", "EnotaryoPH.Data\EnotaryoPH.Data.csproj", "{7B06785D-1B94-472B-885C-3C2F8834438B}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D9FA3452-4AC7-4628-8531-4C6BC344427B}"
ProjectSection(SolutionItems) = preProject
..\.gitignore = ..\.gitignore
..\azure-pipelines.yml = ..\azure-pipelines.yml
..\README.md = ..\README.md
EndProjectSection
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU