video call wip
This commit is contained in:
parent
2e31bfdb03
commit
4b5cfa6516
37
EnotaryoPH/EnotaryoPH.Data/Constants/NotaryoEvent.cs
Normal file
37
EnotaryoPH/EnotaryoPH.Data/Constants/NotaryoEvent.cs
Normal file
@ -0,0 +1,37 @@
|
||||
namespace EnotaryoPH.Data.Constants
|
||||
{
|
||||
public enum NotaryoEvent
|
||||
{
|
||||
Unknown = 0,
|
||||
PrincipalRegistered = 1,
|
||||
SignatoryRegistered = 2,
|
||||
WitnessRegistered = 3,
|
||||
LawyerRegistered = 4,
|
||||
LawyerFingerprintScanned = 5,
|
||||
|
||||
IdentificationDocumentUploaded = 10,
|
||||
|
||||
SelfiePassed = 15,
|
||||
SelfieFailed = 16,
|
||||
|
||||
DocumentUploaded = 20,
|
||||
|
||||
LawyerSelected = 30,
|
||||
|
||||
TransactionSubmitted = 40,
|
||||
TransactionApproved = 41,
|
||||
TransactionRejected = 42,
|
||||
|
||||
VideoConferenceStarted = 50,
|
||||
VideoRecordingStarted = 51,
|
||||
VideoRecordingStopped = 52,
|
||||
|
||||
SignatoryApproved = 61,
|
||||
WitnessApproved = 62,
|
||||
|
||||
PaymentReceived = 70,
|
||||
PaymentFailed = 71,
|
||||
|
||||
TransactionCompleted = 100
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
EmailSent = 1,
|
||||
Registered = 2,
|
||||
FaceMatch = 3,
|
||||
Completed = 10
|
||||
Approved = 4,
|
||||
Rejected = 5,
|
||||
}
|
||||
}
|
@ -7,6 +7,8 @@
|
||||
DocumentUploaded = 2,
|
||||
Submitted = 3,
|
||||
Accepted = 4,
|
||||
Approved = 5,
|
||||
Rejected = 6,
|
||||
Completed = 100
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ namespace EnotaryoPH.Data.Entities
|
||||
public int EventLogID { get; set; }
|
||||
|
||||
[Column("StreamID")]
|
||||
public int StreamID { get; set; }
|
||||
public string StreamID { get; set; }
|
||||
|
||||
[Column("LogType")]
|
||||
public string LogType { get; set; }
|
||||
|
@ -43,6 +43,9 @@ namespace EnotaryoPH.Data.Entities
|
||||
[Column("Status")]
|
||||
public string? Status { get; set; }
|
||||
|
||||
[ForeignKey("TransactionID")]
|
||||
public Transaction Transaction { get; set; }
|
||||
|
||||
[Column("TransactionID")]
|
||||
public int TransactionID { get; set; }
|
||||
|
||||
|
@ -29,6 +29,8 @@ namespace EnotaryoPH.Data.Entities
|
||||
[Column("PrincipalID")]
|
||||
public int PrincipalID { get; set; }
|
||||
|
||||
public LawyerVideoConferenceSchedule Schedule { get; set; }
|
||||
|
||||
[Column("Status")]
|
||||
public string Status { get; set; }
|
||||
|
||||
|
@ -5,49 +5,52 @@ namespace EnotaryoPH.Data.Entities
|
||||
[Table("Users")]
|
||||
public class User
|
||||
{
|
||||
[Column("UserID")]
|
||||
public int UserID { get; set; }
|
||||
[Column("BirthDate")]
|
||||
public DateTime BirthDate { get; set; }
|
||||
|
||||
[Column("CreatedOn")]
|
||||
public DateTime? CreatedOn { get; set; }
|
||||
|
||||
[Column("Email")]
|
||||
public string Email { get; set; }
|
||||
|
||||
public List<EventLog> EventLogs { get; set; }
|
||||
|
||||
[Column("Firstname")]
|
||||
public string? Firstname { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public string Fullname => $"{Firstname} {Lastname}".Trim();
|
||||
|
||||
public List<IdentificationDocument> IdentificationDocuments { get; set; }
|
||||
|
||||
[Column("Lastname")]
|
||||
public string? Lastname { get; set; }
|
||||
|
||||
public List<LawyerVideoConferenceParticipant> LawyerVideoConferenceParticipants { get; set; }
|
||||
|
||||
[Column("Middlename")]
|
||||
public string? Middlename { get; set; }
|
||||
|
||||
[Column("PasswordHash")]
|
||||
public string PasswordHash { get; set; }
|
||||
|
||||
[Column("PhoneNumber")]
|
||||
public string? PhoneNumber { get; set; }
|
||||
|
||||
[Column("Firstname")]
|
||||
public string? Firstname { get; set; }
|
||||
|
||||
[Column("Lastname")]
|
||||
public string? Lastname { get; set; }
|
||||
|
||||
[Column("CreatedOn")]
|
||||
public DateTime? CreatedOn { get; set; }
|
||||
|
||||
[Column("User_UID")]
|
||||
public Guid? User_UID { get; set; }
|
||||
[Column("Prefix")]
|
||||
public string? Prefix { get; set; }
|
||||
|
||||
[Column("Role")]
|
||||
public string? Role { get; set; }
|
||||
|
||||
[Column("BirthDate")]
|
||||
public DateTime BirthDate { get; set; }
|
||||
|
||||
[Column("Middlename")]
|
||||
public string? Middlename { get; set; }
|
||||
|
||||
[Column("Suffix")]
|
||||
public string? Suffix { get; set; }
|
||||
|
||||
[Column("Prefix")]
|
||||
public string? Prefix { get; set; }
|
||||
[Column("User_UID")]
|
||||
public Guid? User_UID { get; set; }
|
||||
|
||||
public List<EventLog> EventLogs { get; set; }
|
||||
|
||||
public List<IdentificationDocument> IdentificationDocuments { get; set; }
|
||||
|
||||
public List<LawyerVideoConferenceParticipant> LawyerVideoConferenceParticipants { get; set; }
|
||||
[Column("UserID")]
|
||||
public int UserID { get; set; }
|
||||
}
|
||||
}
|
@ -1,222 +1,301 @@
|
||||
import { AzureCommunicationTokenCredential } from '@azure/communication-common';
|
||||
import { AzureCommunicationTokenCredential, createIdentifierFromRawId } 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.isLocalVideoStartedChangedCallback = null;
|
||||
//this.localVideoStreamsUpdatedCallback = null;
|
||||
//this.onCreateLocalVideoStreamCallback = null;
|
||||
this.onGetServerCallIDCallback = null;
|
||||
//this.remoteParticipantStateChangedCallback = null;
|
||||
//this.remoteParticipantsUpdatedCallback = null;
|
||||
//this.remoteVideoIsAvailableChangedCallback = null;
|
||||
this.stateChangedCallback = null;
|
||||
this.remoteParticipantsUpdated = null;
|
||||
this.isLocalVideoStartedChanged = null;
|
||||
this.localVideoStreamsUpdated = null;
|
||||
this.idChanged = null;
|
||||
this.onCreateLocalVideoStream = null;
|
||||
this.remoteParticipantStateChanged = null;
|
||||
this.remoteVideoIsAvailableChanged = null;
|
||||
this.onGetServerCallID = null;
|
||||
this.callEndedCallback = null;
|
||||
this.participantsJoinedCallback = null;
|
||||
this.callsUpdatedCallback = null;
|
||||
|
||||
this.callAdapter = null;
|
||||
this.videoContainer = null;
|
||||
this.displayName = null;
|
||||
this.roomID = null;
|
||||
this.token = null;
|
||||
this.userID = null;
|
||||
|
||||
this.onFetchParticipantMenuItemsCallback = null;
|
||||
this.onFetchCustomButtonPropsCallbacks = [];
|
||||
this.onAddParticipantCallback = null;
|
||||
}
|
||||
|
||||
async init(userAccessToken, options) {
|
||||
async init(options) {
|
||||
let self = this;
|
||||
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;
|
||||
this.onGetServerCallID = options.onGetServerCallID;
|
||||
/*this.remoteParticipantsUpdatedCallback = options.remoteParticipantsUpdated;*/
|
||||
/*this.isLocalVideoStartedChangedCallback = options.isLocalVideoStartedChanged;*/
|
||||
/*this.localVideoStreamsUpdatedCallback = options.localVideoStreamsUpdated;*/
|
||||
this.idChangedCallback = options.idChanged;
|
||||
//this.onCreateLocalVideoStreamCallback = options.onCreateLocalVideoStream;
|
||||
//this.remoteParticipantStateChangedCallback = options.remoteParticipantStateChanged;
|
||||
//this.remoteVideoIsAvailableChangedCallback = options.remoteVideoIsAvailableChanged;
|
||||
this.onGetServerCallIDCallback = options.onGetServerCallIDCallback;
|
||||
this.callEndedCallback = options.callEndedCallback;
|
||||
this.participantsJoinedCallback = options.participantsJoinedCallback;
|
||||
this.onFetchParticipantMenuItemsCallback = options.onFetchParticipantMenuItemsCallback;
|
||||
this.onFetchCustomButtonPropsCallbacks = options.onFetchCustomButtonPropsCallbacks || [];
|
||||
this.onAddParticipantCallback = options.onAddParticipantCallback;
|
||||
|
||||
const tokenCredential = new AzureCommunicationTokenCredential(userAccessToken);
|
||||
this.callAdapter = options.callAdapter;
|
||||
this.videoContainer = options.videoContainer;
|
||||
this.displayName = options.displayName;
|
||||
this.roomID = options.roomID;
|
||||
this.token = options.token;
|
||||
this.userID = options.userID;
|
||||
|
||||
this.callClient = new CallClient();
|
||||
this.callAgent = await this.callClient.createCallAgent(tokenCredential);
|
||||
this.serverCallId;
|
||||
|
||||
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]);
|
||||
}
|
||||
const callControls = {
|
||||
// Hide all default buttons
|
||||
cameraButton: true,
|
||||
endCallButton: false,
|
||||
microphoneButton: false,
|
||||
participantsButton: true,
|
||||
screenShareButton: false,
|
||||
devicesButton: false,
|
||||
moreButton: false,
|
||||
raiseHandButton: false,
|
||||
reactionButton: false,
|
||||
dtmfDialerButton: false,
|
||||
holdButton: false,
|
||||
peopleButton: false,
|
||||
exitSpotlightButton: false,
|
||||
captionsButton: false,
|
||||
galleryControlsButton: false,
|
||||
teamsMeetingPhoneCallButton: false,
|
||||
displayType: 'compact',
|
||||
|
||||
stopLocalVideo() {
|
||||
if (this.call) {
|
||||
this.call.stopVideo(this.localVideoStream);
|
||||
}
|
||||
}
|
||||
// Hide the entire control bar if needed
|
||||
onFetchCustomButtonProps: this.onFetchCustomButtonPropsCallbacks
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
this.callAgent.on('callsUpdated', e => {
|
||||
e.added.forEach((addedCall) => {
|
||||
addedCall.on('stateChanged', () => {
|
||||
if (addedCall.state === 'Connected') {
|
||||
addedCall.info.getServerCallId().then(result => {
|
||||
this.onGetServerCallID?.(result);
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const roomCallLocator = { roomId: roomId };
|
||||
let call = this.callAgent.join(roomCallLocator, { videoOptions });
|
||||
this.subscribeToCall(call);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||
const callCompositeProps = {
|
||||
callControls: callControls,
|
||||
formFactor: isMobile ? 'mobile' : 'desktop',
|
||||
onFetchParticipantMenuItems: this.onFetchParticipantMenuItemsCallback,
|
||||
options: {
|
||||
callControls: callControls
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
const adapterArgs = {
|
||||
userId: createIdentifierFromRawId(this.userID),
|
||||
//credential: new AzureCommunicationTokenCredential(this.token),
|
||||
token: this.token,
|
||||
displayName: this.displayName,
|
||||
locator: { roomId: this.roomID },
|
||||
callAdapterOptions: {},
|
||||
callCompositeOptions: callCompositeProps
|
||||
};
|
||||
|
||||
// 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);
|
||||
this.callAdapter = await callComposite.loadCallComposite(
|
||||
adapterArgs,
|
||||
this.videoContainer, // container element,
|
||||
callCompositeProps
|
||||
);
|
||||
|
||||
e.added.forEach(remoteParticipant => {
|
||||
this.subscribeToRemoteParticipant(remoteParticipant)
|
||||
//this.callAdapter.callAgent.on("callsUpdated", function (e) {
|
||||
// e.added.forEach((addedCall) => {
|
||||
// addedCall.on('stateChanged', (state) => this.stateChanged(addedCall));
|
||||
// });
|
||||
//});
|
||||
|
||||
this.callAdapter.on("callIdChanged", function (e) {
|
||||
});
|
||||
|
||||
this.callAdapter.onStateChange(state => {
|
||||
if (state.call?.info && !this.serverCallId) {
|
||||
state.call.info.getServerCallId().then(result => {
|
||||
this.serverCallId = result;
|
||||
this.onGetServerCallIDCallback?.(result);
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
this.callAdapter.on("callEnded", function (e) {
|
||||
self.callEndedCallback?.(e);
|
||||
});
|
||||
|
||||
this.callAdapter.on("participantsJoined", function (e, f) {
|
||||
self.participantsJoinedCallback?.(e, f);
|
||||
});
|
||||
|
||||
this.callAdapter.on("onAddParticipant", function (e, f) {
|
||||
self.onAddParticipantCallback?.(e, f);
|
||||
});
|
||||
|
||||
//CallEnded
|
||||
|
||||
return this.callAdapter;
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
//stopLocalVideo() {
|
||||
// if (this.call) {
|
||||
// this.call.stopVideo(this.localVideoStream);
|
||||
// }
|
||||
//}
|
||||
|
||||
async joinRoom() {
|
||||
await this.callAdapter?.joinCall({
|
||||
microphoneOn: true,
|
||||
cameraOn: true
|
||||
});
|
||||
}
|
||||
|
||||
async stopCall(forEveryone = false) {
|
||||
await this.callAdapter?.leaveCall(forEveryone);
|
||||
}
|
||||
|
||||
//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);
|
||||
}
|
||||
}
|
||||
//createIdentifierFromRawId(rawId) {
|
||||
// return createIdentifierFromRawId(rawId);
|
||||
//}
|
||||
|
||||
//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;
|
@ -0,0 +1,39 @@
|
||||
namespace EnotaryoPH.Web.Common.Exceptions
|
||||
{
|
||||
public class NoDataException : Exception
|
||||
{
|
||||
public NoDataException(string typeName, object id) : base($"No data found for {typeName} with id = '{id}'.")
|
||||
{
|
||||
TypeName = typeName;
|
||||
ID = id;
|
||||
Key = string.Empty;
|
||||
}
|
||||
|
||||
public NoDataException(string typeName, object id, string key) : base($"No data found for {typeName} with {key} = '{id}'.")
|
||||
{
|
||||
TypeName = typeName;
|
||||
ID = id;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
public static void ThrowIfNull(object data, string typeName, object id)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
throw new NoDataException(typeName, id);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ThrowIfNull(object data, string typeName, object id, string key)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
throw new NoDataException(typeName, id, key);
|
||||
}
|
||||
}
|
||||
|
||||
public object ID { get; private set; }
|
||||
public string Key { get; private set; }
|
||||
public string TypeName { get; private set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
namespace EnotaryoPH.Web.Common.Extensions
|
||||
{
|
||||
public static class IEnumerableExtensions
|
||||
{
|
||||
public static IEnumerable<T> ToSafeEnumerable<T>(this IEnumerable<T> enumerable) => enumerable ?? Enumerable.Empty<T>();
|
||||
|
||||
public static List<T> ToSafeList<T>(this IEnumerable<T> enumerable) => enumerable?.ToList() ?? [];
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
namespace EnotaryoPH.Web.Common.Extensions
|
||||
{
|
||||
public static class ObjectExtensions
|
||||
{
|
||||
public static int ToInteger(this object obj)
|
||||
{
|
||||
int.TryParse(obj.ToString(), out var result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
private const char Plus = '+';
|
||||
private const char Slash = '/';
|
||||
|
||||
public static string DefaultIfEmpty(this string s, string defaultValue) => !string.IsNullOrWhiteSpace(s) ? s : (defaultValue ?? string.Empty);
|
||||
public static string DefaultIfEmpty(this string s, string defaultValue) => !string.IsNullOrWhiteSpace(s) ? s : defaultValue ?? string.Empty;
|
||||
|
||||
public static bool IsInList(this string s, params string[] list) => list.Contains(s, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
|
@ -0,0 +1,85 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Azure.Communication.CallAutomation;
|
||||
using Azure.Storage.Queues;
|
||||
using Coravel.Invocable;
|
||||
using EnotaryoPH.Data;
|
||||
using EnotaryoPH.Data.Entities;
|
||||
using EnotaryoPH.Web.Common.Jobs.Models;
|
||||
|
||||
namespace EnotaryoPH.Web.Common.Jobs
|
||||
{
|
||||
public class CheckRecordingAvailabilityInvocable : IInvocable
|
||||
{
|
||||
private readonly QueueClient _queueClient;
|
||||
private readonly CallAutomationClient _callAutomationClient;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public CheckRecordingAvailabilityInvocable(QueueClient queueClient, CallAutomationClient callAutomationClient, IConfiguration configuration, IServiceScopeFactory serviceScopeFactory)
|
||||
{
|
||||
_queueClient = queueClient;
|
||||
_callAutomationClient = callAutomationClient;
|
||||
_configuration = configuration;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
public async Task Invoke()
|
||||
{
|
||||
var message = await _queueClient.ReceiveMessageAsync();
|
||||
if (message.Value == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var base64EncodedData = message.Value.Body.ToString();
|
||||
var base64EncodedBytes = Convert.FromBase64String(base64EncodedData);
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
var json = Encoding.UTF8.GetString(base64EncodedBytes);
|
||||
var model = JsonSerializer.Deserialize<RecordingFileStatusUpdatedModel>(json, options);
|
||||
if (model?.Data?.RecordingStorageInfo?.RecordingChunks?.Count > 0)
|
||||
{
|
||||
var dbContext = _serviceScopeFactory.CreateScope().ServiceProvider.GetRequiredService<NotaryoDBContext>();
|
||||
model.Data.RecordingStorageInfo.RecordingChunks.ForEach(async chunk =>
|
||||
{
|
||||
var path = _configuration.GetValue<string>("VideoRecordingsLocation");
|
||||
if (!Path.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
var fileName = Path.Combine(path, $"{chunk.DocumentId}-{chunk.Index}.mp4");
|
||||
if (File.Exists(fileName))
|
||||
{
|
||||
File.Move(fileName, fileName.Replace(".mp4", $"_{DateTime.UtcNow.ToString("yyyy-MM-dd-HH-mm-dd")}.mp4"));
|
||||
}
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
await _callAutomationClient
|
||||
.GetCallRecording().DownloadToAsync(new Uri(chunk.ContentLocation), fileName);
|
||||
|
||||
var schedule = dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.RecordingID == model.Data.RecordingId);
|
||||
if (schedule != null)
|
||||
{
|
||||
if (schedule.VideoRecording == null)
|
||||
{
|
||||
schedule.VideoRecording = new VideoRecording
|
||||
{
|
||||
CreatedOn = DateTime.UtcNow,
|
||||
LocationType = nameof(VideoRecordingLocationType.LocalFolder),
|
||||
VideoConferenceScheduleID = schedule.LawyerVideoConferenceScheduleID,
|
||||
VideoRecording_UID = Guid.CreateVersion7(DateTime.UtcNow)
|
||||
};
|
||||
}
|
||||
schedule.VideoRecording.Path = fileName;
|
||||
schedule.VideoRecording.Metadata = JsonSerializer.Serialize(json);
|
||||
dbContext.UpdateOrCreate(schedule);
|
||||
dbContext.SaveChanges();
|
||||
}
|
||||
});
|
||||
}
|
||||
await _queueClient.DeleteMessageAsync(message.Value.MessageId, message.Value.PopReceipt);
|
||||
}
|
||||
}
|
||||
}
|
12
EnotaryoPH/EnotaryoPH.Web/Common/Jobs/Models/Data.cs
Normal file
12
EnotaryoPH/EnotaryoPH.Web/Common/Jobs/Models/Data.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace EnotaryoPH.Web.Common.Jobs.Models
|
||||
{
|
||||
public class Data
|
||||
{
|
||||
public int RecordingDurationMs { get; set; }
|
||||
public string RecordingId { get; set; }
|
||||
public DateTime RecordingStartTime { get; set; }
|
||||
public RecordingStorageInfo RecordingStorageInfo { get; set; }
|
||||
public string SessionEndReason { get; set; }
|
||||
public string StorageType { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
namespace EnotaryoPH.Web.Common.Jobs.Models
|
||||
{
|
||||
public class RecordingChunk
|
||||
{
|
||||
public string ContentLocation { get; set; }
|
||||
public string DeleteLocation { get; set; }
|
||||
public string DocumentId { get; set; }
|
||||
public string EndReason { get; set; }
|
||||
public int Index { get; set; }
|
||||
public string MetadataLocation { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
namespace EnotaryoPH.Web.Common.Jobs.Models
|
||||
{
|
||||
public class RecordingFileStatusUpdatedModel
|
||||
{
|
||||
public Data Data { get; set; }
|
||||
public string DataVersion { get; set; }
|
||||
public DateTime EventTime { get; set; }
|
||||
public string EventType { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string MetadataVersion { get; set; }
|
||||
public string Subject { get; set; }
|
||||
public string Topic { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace EnotaryoPH.Web.Common.Jobs.Models
|
||||
{
|
||||
public class RecordingStorageInfo
|
||||
{
|
||||
public List<RecordingChunk> RecordingChunks { get; set; }
|
||||
}
|
||||
}
|
@ -1,175 +0,0 @@
|
||||
using Azure;
|
||||
using Azure.Communication;
|
||||
using Azure.Communication.CallAutomation;
|
||||
using Azure.Communication.Identity;
|
||||
using Azure.Communication.Rooms;
|
||||
using EnotaryoPH.Data;
|
||||
using EnotaryoPH.Data.Entities;
|
||||
|
||||
namespace EnotaryoPH.Web.Common.Services
|
||||
{
|
||||
public class ConferenceSheduleService : IConferenceSheduleService
|
||||
{
|
||||
private readonly NotaryoDBContext _dbContext;
|
||||
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, IConfiguration configuration)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_communicationIdentityClient = communicationIdentityClient;
|
||||
_roomsClient = roomsClient;
|
||||
_callAutomationClient = callAutomationClient;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public async Task<Guid> GetOrCreateScheduleIDAsync(Guid transaction_UID)
|
||||
{
|
||||
if (transaction_UID == Guid.Empty)
|
||||
{
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
var transactionEntity = _dbContext.Transactions
|
||||
.Include(t => t.TransactionSignatories)
|
||||
.FirstOrDefault(t => t.Transaction_UID == transaction_UID);
|
||||
if (transactionEntity == null)
|
||||
{
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
var existingSchedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID);
|
||||
if (existingSchedule != null)
|
||||
{
|
||||
return existingSchedule.LawyerVideoConferenceSchedule_UID;
|
||||
}
|
||||
|
||||
var schedule_UID = Guid.Empty;
|
||||
var isReadyForVideoCall = transactionEntity.TransactionSignatories.All(signatory => signatory.Status == nameof(SignatoryStatus.FaceMatch));
|
||||
var isAcceptedByLawyer = transactionEntity.Status == nameof(TransactionState.Accepted) && transactionEntity.LawyerID > 0;
|
||||
if (isReadyForVideoCall && isAcceptedByLawyer)
|
||||
{
|
||||
var schedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID);
|
||||
if (schedule == null)
|
||||
{
|
||||
var participants = transactionEntity.TransactionSignatories.ConvertAll(signatory => new LawyerVideoConferenceParticipant
|
||||
{
|
||||
CreatedOn = DateTime.UtcNow,
|
||||
Status = nameof(VideoConferenceStatus.New),
|
||||
LawyerVideoConferenceParticipant_UID = Guid.CreateVersion7(DateTime.UtcNow),
|
||||
ParticipantID = signatory.UserID,
|
||||
});
|
||||
|
||||
participants.Add(new LawyerVideoConferenceParticipant
|
||||
{
|
||||
CreatedOn = DateTime.UtcNow,
|
||||
Status = nameof(VideoConferenceStatus.New),
|
||||
LawyerVideoConferenceParticipant_UID = Guid.CreateVersion7(DateTime.UtcNow),
|
||||
ParticipantID = transactionEntity.PrincipalID,
|
||||
});
|
||||
|
||||
schedule = new LawyerVideoConferenceSchedule
|
||||
{
|
||||
LawyerVideoConferenceSchedule_UID = Guid.CreateVersion7(DateTime.UtcNow),
|
||||
CreatedOn = DateTime.UtcNow,
|
||||
LawyerID = transactionEntity.LawyerID.GetValueOrDefault(),
|
||||
TransactionID = transactionEntity.TransactionID,
|
||||
MeetingDate = DateTime.UtcNow,
|
||||
Status = nameof(VideoConferenceStatus.New),
|
||||
};
|
||||
|
||||
var roomParticipants = new List<RoomParticipant>();
|
||||
foreach (var participant in participants)
|
||||
{
|
||||
var attendee = await _communicationIdentityClient.CreateUserAsync();
|
||||
participant.MeetingRoomTokenID = await GetTokenResponseAsync(attendee);
|
||||
participant.MeetingRoomUserID = attendee.Value.Id;
|
||||
roomParticipants.Add(new RoomParticipant(attendee) { Role = ParticipantRole.Attendee });
|
||||
}
|
||||
|
||||
var presenter = await _communicationIdentityClient.CreateUserAsync();
|
||||
schedule.MeetingRoomTokenID = await GetTokenResponseAsync(presenter);
|
||||
schedule.MeetingRoomUserID = presenter.Value.Id;
|
||||
roomParticipants.Add(new RoomParticipant(presenter) { Role = ParticipantRole.Presenter });
|
||||
|
||||
CommunicationRoom room = await _roomsClient.CreateRoomAsync(DateTime.Now, DateTime.Now.AddHours(2), roomParticipants);
|
||||
schedule.MeetingRoomID = room.Id;
|
||||
schedule.LawyerVideoConferenceParticipants = participants.ToList();
|
||||
|
||||
if (schedule.LawyerVideoConferenceScheduleID == 0)
|
||||
{
|
||||
_dbContext.Add(schedule);
|
||||
}
|
||||
else
|
||||
{
|
||||
_dbContext.Update(schedule);
|
||||
}
|
||||
_dbContext.SaveChanges();
|
||||
schedule_UID = schedule.LawyerVideoConferenceSchedule_UID;
|
||||
}
|
||||
}
|
||||
|
||||
return schedule_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) && string.IsNullOrEmpty(serverCallID))
|
||||
{
|
||||
throw new ArgumentException("ServerCallID is not set for this transaction.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(existingSchedule.RecordingID))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(serverCallID))
|
||||
{
|
||||
existingSchedule.ServerCallID = serverCallID;
|
||||
}
|
||||
|
||||
CallLocator callLocator = new ServerCallLocator(existingSchedule.ServerCallID);
|
||||
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;
|
||||
_dbContext.Update(existingSchedule);
|
||||
_dbContext.SaveChanges();
|
||||
}
|
||||
|
||||
public async Task StopRecordingAsync(Guid transaction_UID)
|
||||
{
|
||||
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))
|
||||
{
|
||||
throw new ArgumentException("ServerCallID is not set for this transaction.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(existingSchedule.RecordingID))
|
||||
{
|
||||
throw new ArgumentException("Recording ID is not set for this transaction.");
|
||||
}
|
||||
|
||||
await _callAutomationClient.GetCallRecording().StopAsync(existingSchedule.RecordingID);
|
||||
}
|
||||
|
||||
private async Task<string> GetTokenResponseAsync(Response<CommunicationUserIdentifier> user)
|
||||
{
|
||||
var tokenResponse = await _communicationIdentityClient.GetTokenAsync(user, new[] { CommunicationTokenScope.VoIP });
|
||||
return tokenResponse.Value.Token;
|
||||
}
|
||||
}
|
||||
}
|
63
EnotaryoPH/EnotaryoPH.Web/Common/Services/EventService.cs
Normal file
63
EnotaryoPH/EnotaryoPH.Web/Common/Services/EventService.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using System.Text.Json;
|
||||
using EnotaryoPH.Data;
|
||||
using EnotaryoPH.Data.Entities;
|
||||
|
||||
namespace EnotaryoPH.Web.Common.Services
|
||||
{
|
||||
public class EventService : IEventService
|
||||
{
|
||||
private readonly NotaryoDBContext _dBContext;
|
||||
private readonly INotificationService _notificationService;
|
||||
|
||||
public EventService(NotaryoDBContext dBContext, INotificationService notificationService)
|
||||
{
|
||||
_dBContext = dBContext;
|
||||
_notificationService = notificationService;
|
||||
}
|
||||
|
||||
public Task LogAsync(NotaryoEvent notaryoEvent, object entityId) => LogAsync(notaryoEvent, entityId, null);
|
||||
|
||||
public async Task LogAsync(NotaryoEvent notaryoEvent, List<object> entityIds, object payLoad)
|
||||
{
|
||||
foreach (var entityId in entityIds)
|
||||
{
|
||||
await LogAsync(notaryoEvent, entityId, payLoad);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LogAsync(NotaryoEvent notaryoEvent, object entityId, object payLoad)
|
||||
{
|
||||
var logItem = new EventLog
|
||||
{
|
||||
Description = $"Event: {notaryoEvent}, Entity: {entityId}",
|
||||
EventLog_UID = Guid.CreateVersion7(DateTime.UtcNow),
|
||||
LogDate = DateTime.UtcNow,
|
||||
LogType = notaryoEvent.ToString(),
|
||||
Payload = payLoad != null ? JsonSerializer.Serialize(payLoad) : null,
|
||||
StreamID = entityId.ToString(),
|
||||
};
|
||||
if (notaryoEvent == NotaryoEvent.TransactionApproved)
|
||||
{
|
||||
var transaction = _dBContext.Transactions.AsNoTracking()
|
||||
.Include(t => t.TransactionSignatories)
|
||||
.ThenInclude(ts => ts.User)
|
||||
.Include(t => t.Lawyer)
|
||||
.ThenInclude(l => l.User)
|
||||
.Include(t => t.Principal)
|
||||
.Include(t => t.TransactionDocument)
|
||||
.FirstOrDefault(t => t.TransactionID == entityId.ToInteger());
|
||||
logItem.Description = $"Transaction {entityId} has been approved.";
|
||||
if (transaction != null)
|
||||
{
|
||||
var message = $"The document {transaction.TransactionDocument.DocumentType} has been approved.";
|
||||
var allUsers = transaction.TransactionSignatories.ConvertAll(ts => ts.User.Email);
|
||||
allUsers.Add(transaction.Lawyer.User.Email);
|
||||
allUsers.Add(transaction.Principal.Email);
|
||||
await _notificationService.NotifyUsersAsync(message, allUsers.ToArray());
|
||||
}
|
||||
}
|
||||
_dBContext.Add(logItem);
|
||||
_dBContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
namespace EnotaryoPH.Web.Common.Services
|
||||
{
|
||||
public interface IConferenceSheduleService
|
||||
{
|
||||
Task<Guid> GetOrCreateScheduleIDAsync(Guid transaction_UID);
|
||||
|
||||
Task StartRecordingAsync(Guid transaction_UID, string serverCallID);
|
||||
|
||||
Task StopRecordingAsync(Guid transaction_UID);
|
||||
}
|
||||
}
|
11
EnotaryoPH/EnotaryoPH.Web/Common/Services/IEventService.cs
Normal file
11
EnotaryoPH/EnotaryoPH.Web/Common/Services/IEventService.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace EnotaryoPH.Web.Common.Services
|
||||
{
|
||||
public interface IEventService
|
||||
{
|
||||
Task LogAsync(NotaryoEvent notaryoEvent, List<object> entityIds, object payLoad);
|
||||
|
||||
Task LogAsync(NotaryoEvent notaryoEvent, object entityId);
|
||||
|
||||
Task LogAsync(NotaryoEvent notaryoEvent, object entityId, object payLoad);
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
|
||||
namespace EnotaryoPH.Web.Common.Services
|
||||
{
|
||||
public interface INotificationService
|
||||
{
|
||||
Task NotifyAllAsync(string message);
|
||||
Task NotifyUserAsync(string message, string userID);
|
||||
Task NotifyUsersAsync(string message, params string[] userIDs);
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
namespace EnotaryoPH.Web.Common.Services
|
||||
{
|
||||
public interface IVideoConferenceService
|
||||
{
|
||||
Task ApproveTransactionAsync(Guid transaction_UID);
|
||||
|
||||
bool CanStart(Guid transaction_UID);
|
||||
|
||||
Guid GetUIDByTransactionUID(Guid transaction_UID);
|
||||
|
||||
bool HasExpired(Guid transaction_UID);
|
||||
|
||||
Task<Guid> StartAsync(Guid transaction_UID);
|
||||
|
||||
Task StartRecordingAsync(Guid transaction_UID, string serverCallID);
|
||||
}
|
||||
}
|
@ -3,12 +3,16 @@ using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace EnotaryoPH.Web.Common.Services
|
||||
{
|
||||
public class NotificationService
|
||||
public class NotificationService : INotificationService
|
||||
{
|
||||
private readonly IHubContext<NotificationHub> _hubContext;
|
||||
|
||||
public NotificationService(IHubContext<NotificationHub> hubContext) => _hubContext = hubContext;
|
||||
|
||||
public async Task NotifyUserAsync(string user_UID, string message) => await _hubContext.Clients.All.SendAsync("ReceiveUserNotification", user_UID, message);
|
||||
public async Task NotifyAllAsync(string message) => await _hubContext.Clients.All.SendAsync("ReceiveUserNotification", message);
|
||||
|
||||
public async Task NotifyUserAsync(string message, string userID) => await NotifyUsersAsync(message, userID);
|
||||
|
||||
public async Task NotifyUsersAsync(string message, params string[] userIDs) => await _hubContext.Clients.Users(userIDs).SendAsync("ReceiveUserNotification", message);
|
||||
}
|
||||
}
|
@ -0,0 +1,256 @@
|
||||
using Azure;
|
||||
using Azure.Communication;
|
||||
using Azure.Communication.CallAutomation;
|
||||
using Azure.Communication.Identity;
|
||||
using Azure.Communication.Rooms;
|
||||
using EnotaryoPH.Data;
|
||||
using EnotaryoPH.Data.Entities;
|
||||
|
||||
namespace EnotaryoPH.Web.Common.Services
|
||||
{
|
||||
public class VideoConferenceService : IVideoConferenceService
|
||||
{
|
||||
private const int VideoConferenceExpirationInHours = 2;
|
||||
|
||||
private readonly CallAutomationClient _callAutomationClient;
|
||||
private readonly CommunicationIdentityClient _communicationIdentityClient;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly NotaryoDBContext _dbContext;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly RoomsClient _roomsClient;
|
||||
|
||||
public VideoConferenceService(NotaryoDBContext dbContext, CommunicationIdentityClient communicationIdentityClient, RoomsClient roomsClient, CallAutomationClient callAutomationClient, IConfiguration configuration, IEventService eventService)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_communicationIdentityClient = communicationIdentityClient;
|
||||
_roomsClient = roomsClient;
|
||||
_callAutomationClient = callAutomationClient;
|
||||
_configuration = configuration;
|
||||
_eventService = eventService;
|
||||
}
|
||||
|
||||
public async Task ApproveTransactionAsync(Guid transaction_UID)
|
||||
{
|
||||
var transaction = _dbContext.Transactions
|
||||
.Include(t => t.TransactionSignatories)
|
||||
.ThenInclude(ts => ts.User)
|
||||
.Include(t => t.Schedule)
|
||||
.Include(t => t.Lawyer)
|
||||
.ThenInclude(l => l.User)
|
||||
.Include(t => t.Principal)
|
||||
.FirstOrDefault(t => t.Transaction_UID == transaction_UID);
|
||||
NoDataException.ThrowIfNull(transaction, nameof(Transaction), transaction_UID);
|
||||
transaction.Status = nameof(TransactionState.Approved);
|
||||
transaction.TransactionSignatories.ForEach(ts => ts.Status = nameof(SignatoryStatus.Approved));
|
||||
transaction.Schedule.Status = nameof(VideoConferenceStatus.Completed);
|
||||
_dbContext.Update(transaction);
|
||||
_dbContext.SaveChanges();
|
||||
|
||||
await Task.WhenAll(
|
||||
_eventService.LogAsync(NotaryoEvent.TransactionApproved, transaction_UID),
|
||||
StopRecordingAsync(transaction.Schedule)
|
||||
);
|
||||
}
|
||||
|
||||
public bool CanStart(Guid transaction_UID)
|
||||
{
|
||||
if (transaction_UID == Guid.Empty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var transactionEntity = _dbContext.Transactions
|
||||
.AsNoTracking()
|
||||
.Include(t => t.TransactionSignatories)
|
||||
.FirstOrDefault(t => t.Transaction_UID == transaction_UID);
|
||||
if (transactionEntity == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var isReadyForVideoCall = transactionEntity.TransactionSignatories.All(signatory => signatory.Status == nameof(SignatoryStatus.FaceMatch));
|
||||
var isAcceptedByLawyer = transactionEntity.Status == nameof(TransactionState.Accepted) && transactionEntity.LawyerID > 0;
|
||||
return isReadyForVideoCall && isAcceptedByLawyer;
|
||||
}
|
||||
|
||||
public Guid GetUIDByTransactionUID(Guid transaction_UID)
|
||||
{
|
||||
var transactionEntity = _dbContext.Transactions
|
||||
.AsNoTracking()
|
||||
.FirstOrDefault(t => t.Transaction_UID == transaction_UID);
|
||||
if (transactionEntity == null)
|
||||
{
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
var schedule = _dbContext.LawyerVideoConferenceSchedules.AsNoTracking().FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID);
|
||||
return schedule == null ? Guid.Empty : schedule.LawyerVideoConferenceSchedule_UID;
|
||||
}
|
||||
|
||||
public bool HasExpired(Guid transaction_UID)
|
||||
{
|
||||
var transactionEntity = _dbContext.Transactions
|
||||
.Include(t => t.TransactionSignatories)
|
||||
.FirstOrDefault(t => t.Transaction_UID == transaction_UID);
|
||||
if (transactionEntity == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var schedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID);
|
||||
if (schedule == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (schedule.Status == nameof(VideoConferenceStatus.Expired))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if ((DateTime.UtcNow - schedule.MeetingDate).TotalHours > VideoConferenceExpirationInHours)
|
||||
{
|
||||
if (!schedule.Status?.IsInList(VideoConferenceStatus.Abandoned, VideoConferenceStatus.Completed) ?? false)
|
||||
{
|
||||
schedule.Status = nameof(VideoConferenceStatus.Expired);
|
||||
_dbContext.Update(schedule);
|
||||
_dbContext.SaveChanges();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<Guid> StartAsync(Guid transaction_UID)
|
||||
{
|
||||
if (!CanStart(transaction_UID))
|
||||
{
|
||||
throw new ArgumentException("Transaction is not ready for video conference.");
|
||||
}
|
||||
var schedule = GetOrCreateNewSchedule(transaction_UID);
|
||||
if (schedule.Status == nameof(VideoConferenceStatus.New))
|
||||
{
|
||||
await CreateMeetingRoomAsync(schedule);
|
||||
_dbContext.UpdateOrCreate(schedule);
|
||||
_dbContext.SaveChanges();
|
||||
}
|
||||
return schedule.LawyerVideoConferenceSchedule_UID;
|
||||
}
|
||||
|
||||
public async Task StartRecordingAsync(Guid transaction_UID, string serverCallID)
|
||||
{
|
||||
var transactionEntity = _dbContext.Transactions
|
||||
.Include(t => t.TransactionSignatories)
|
||||
.Single(t => t.Transaction_UID == transaction_UID);
|
||||
NoDataException.ThrowIfNull(transactionEntity, nameof(Transaction), transaction_UID);
|
||||
|
||||
var schedule = _dbContext.LawyerVideoConferenceSchedules
|
||||
.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID);
|
||||
NoDataException.ThrowIfNull(schedule, nameof(LawyerVideoConferenceSchedule), transactionEntity.TransactionID, FullName.Of(transactionEntity.TransactionID));
|
||||
|
||||
if (string.IsNullOrEmpty(schedule.RecordingID))
|
||||
{
|
||||
schedule.RecordingID = await StartRecordingAsync(serverCallID);
|
||||
schedule.ServerCallID = serverCallID;
|
||||
_dbContext.Update(schedule);
|
||||
_dbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateMeetingRoomAsync(LawyerVideoConferenceSchedule schedule)
|
||||
{
|
||||
var roomParticipants = new List<RoomParticipant>();
|
||||
foreach (var participant in schedule.LawyerVideoConferenceParticipants.ToSafeList())
|
||||
{
|
||||
var attendee = await _communicationIdentityClient.CreateUserAsync();
|
||||
participant.MeetingRoomTokenID = await GetTokenResponseAsync(attendee);
|
||||
participant.MeetingRoomUserID = attendee.Value.Id;
|
||||
roomParticipants.Add(new RoomParticipant(attendee) { Role = ParticipantRole.Attendee });
|
||||
}
|
||||
|
||||
var presenter = await _communicationIdentityClient.CreateUserAsync();
|
||||
schedule.MeetingRoomTokenID = await GetTokenResponseAsync(presenter);
|
||||
schedule.MeetingRoomUserID = presenter.Value.Id;
|
||||
roomParticipants.Add(new RoomParticipant(presenter) { Role = ParticipantRole.Presenter });
|
||||
|
||||
CommunicationRoom room = await _roomsClient.CreateRoomAsync(DateTime.Now, DateTime.Now.AddHours(2), roomParticipants);
|
||||
schedule.MeetingRoomID = room.Id;
|
||||
schedule.Status = nameof(VideoConferenceStatus.InProgress);
|
||||
}
|
||||
|
||||
private LawyerVideoConferenceSchedule GetOrCreateNewSchedule(Guid transaction_UID)
|
||||
{
|
||||
var transactionEntity = _dbContext.Transactions
|
||||
.Include(t => t.TransactionSignatories)
|
||||
.Single(t => t.Transaction_UID == transaction_UID);
|
||||
|
||||
var schedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID);
|
||||
if (schedule != null)
|
||||
{
|
||||
return schedule;
|
||||
}
|
||||
schedule = new LawyerVideoConferenceSchedule
|
||||
{
|
||||
LawyerVideoConferenceSchedule_UID = Guid.CreateVersion7(DateTime.UtcNow),
|
||||
CreatedOn = DateTime.UtcNow,
|
||||
LawyerID = transactionEntity.LawyerID.GetValueOrDefault(),
|
||||
TransactionID = transactionEntity.TransactionID,
|
||||
Status = nameof(VideoConferenceStatus.New)
|
||||
};
|
||||
|
||||
var participants = transactionEntity.TransactionSignatories.ConvertAll(signatory => new LawyerVideoConferenceParticipant
|
||||
{
|
||||
CreatedOn = DateTime.UtcNow,
|
||||
Status = nameof(VideoConferenceStatus.New),
|
||||
LawyerVideoConferenceParticipant_UID = Guid.CreateVersion7(DateTime.UtcNow),
|
||||
ParticipantID = signatory.UserID,
|
||||
});
|
||||
participants.Add(new LawyerVideoConferenceParticipant
|
||||
{
|
||||
CreatedOn = DateTime.UtcNow,
|
||||
Status = nameof(VideoConferenceStatus.New),
|
||||
LawyerVideoConferenceParticipant_UID = Guid.CreateVersion7(DateTime.UtcNow),
|
||||
ParticipantID = transactionEntity.PrincipalID,
|
||||
});
|
||||
|
||||
schedule.MeetingDate = DateTime.UtcNow;
|
||||
schedule.LawyerVideoConferenceParticipants = participants.ToList();
|
||||
return schedule;
|
||||
}
|
||||
|
||||
private async Task<string> GetTokenResponseAsync(Response<CommunicationUserIdentifier> user)
|
||||
{
|
||||
var tokenResponse = await _communicationIdentityClient.GetTokenAsync(user, new[] { CommunicationTokenScope.VoIP });
|
||||
return tokenResponse.Value.Token;
|
||||
}
|
||||
|
||||
private async Task<string> StartRecordingAsync(string serverCallID)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(serverCallID);
|
||||
CallLocator callLocator = new ServerCallLocator(serverCallID);
|
||||
var uri = _configuration.GetValue<string>("UriRecordingBlobContainer") ?? string.Empty;
|
||||
var recordingResult = await _callAutomationClient
|
||||
.GetCallRecording().StartAsync(new StartRecordingOptions(callLocator)
|
||||
{
|
||||
RecordingContent = RecordingContent.AudioVideo,
|
||||
RecordingStorage = RecordingStorage.CreateAzureBlobContainerRecordingStorage(new Uri(uri)),
|
||||
RecordingFormat = RecordingFormat.Mp4
|
||||
});
|
||||
return recordingResult.Value.RecordingId;
|
||||
}
|
||||
|
||||
private async Task StopRecordingAsync(LawyerVideoConferenceSchedule schedule)
|
||||
{
|
||||
if (string.IsNullOrEmpty(schedule.ServerCallID))
|
||||
{
|
||||
Console.WriteLine("ServerCallID is not set for this transaction.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(schedule.RecordingID))
|
||||
{
|
||||
Console.WriteLine("Recording ID is not set for this transaction.");
|
||||
return;
|
||||
}
|
||||
|
||||
await _callAutomationClient.GetCallRecording().StopAsync(schedule.RecordingID);
|
||||
}
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@
|
||||
<PackageReference Include="Azure.Communication.CallAutomation" Version="1.3.0" />
|
||||
<PackageReference Include="Azure.Communication.Identity" Version="1.3.1" />
|
||||
<PackageReference Include="Azure.Communication.Rooms" Version="1.1.1" />
|
||||
<PackageReference Include="Azure.Storage.Queues" Version="12.22.0" />
|
||||
<PackageReference Include="CompreFace.NET.Sdk" Version="1.0.2" />
|
||||
<PackageReference Include="Coravel" Version="6.0.2" />
|
||||
<PackageReference Include="Coravel.Mailer" Version="7.1.0" />
|
||||
|
@ -8,3 +8,19 @@
|
||||
<h1 class="display-4">Welcome</h1>
|
||||
<p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
<button onclick="onClickMe">
|
||||
click me
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function onClickMe() {
|
||||
alert('yoloooo');
|
||||
}
|
||||
</script>
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
@ -7,12 +6,14 @@ namespace EnotaryoPH.Web.Pages
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
private readonly NotificationService _notificationService;
|
||||
private readonly INotificationService _notificationService;
|
||||
private readonly IEventService _eventService;
|
||||
|
||||
public IndexModel(ICurrentUserService currentUserService, NotificationService notificationService)
|
||||
public IndexModel(ICurrentUserService currentUserService, INotificationService notificationService, IEventService eventService)
|
||||
{
|
||||
_currentUserService = currentUserService;
|
||||
_notificationService = notificationService;
|
||||
_eventService = eventService;
|
||||
}
|
||||
|
||||
public IActionResult OnGet()
|
||||
@ -34,5 +35,14 @@ namespace EnotaryoPH.Web.Pages
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
var message = new { Message = "the quick brown fox jumps over the lazy dog." };
|
||||
|
||||
await _eventService.LogAsync(NotaryoEvent.TransactionApproved, new Guid("0195dfd2-9048-77f1-9d9d-974828cb3e68"));
|
||||
//await _notificationService.NotifyUserAsync("admin@enotaryo.ph", JsonSerializer.Serialize(message));
|
||||
return Redirect("/");
|
||||
}
|
||||
}
|
||||
}
|
@ -9,15 +9,15 @@ namespace EnotaryoPH.Web.Pages.Notary.TransactionStatus
|
||||
{
|
||||
private const string STATUS_READY = "Ready";
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
private readonly IConferenceSheduleService _conferenceSheduleService;
|
||||
private readonly NotaryoDBContext _notaryoDBContext;
|
||||
private readonly IVideoConferenceService _videoConferenceService;
|
||||
private Transaction? _transaction;
|
||||
|
||||
public IndexModel(NotaryoDBContext notaryoDBContext, ICurrentUserService currentUserService, IConferenceSheduleService conferenceSheduleService)
|
||||
public IndexModel(NotaryoDBContext notaryoDBContext, ICurrentUserService currentUserService, IVideoConferenceService videoConferenceService)
|
||||
{
|
||||
_notaryoDBContext = notaryoDBContext;
|
||||
_currentUserService = currentUserService;
|
||||
_conferenceSheduleService = conferenceSheduleService;
|
||||
_videoConferenceService = videoConferenceService;
|
||||
}
|
||||
|
||||
public IActionResult OnGet()
|
||||
@ -81,16 +81,12 @@ namespace EnotaryoPH.Web.Pages.Notary.TransactionStatus
|
||||
_transaction.Status = nameof(TransactionState.Accepted);
|
||||
_transaction.LawyerID = lawyer.LawyerID;
|
||||
_notaryoDBContext.Update(_transaction);
|
||||
_notaryoDBContext.SaveChanges();
|
||||
|
||||
var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID);
|
||||
if (schedule_UID != Guid.Empty)
|
||||
{
|
||||
return _transaction.TransactionSignatories.TrueForAll(sig => sig.Status == nameof(SignatoryStatus.FaceMatch))
|
||||
? Redirect($"/Participant/VideoCall/Room/{Transaction_UID}")
|
||||
: Redirect($"/Participant/VideoCall/Waiting/{Transaction_UID}");
|
||||
}
|
||||
|
||||
return Redirect("/Notary/Dashboard");
|
||||
var canStart = _videoConferenceService.CanStart(Transaction_UID);
|
||||
return canStart
|
||||
? Redirect($"/Participant/VideoCall/Room/{Transaction_UID}")
|
||||
: Redirect($"/Participant/VideoCall/Waiting/{Transaction_UID}");
|
||||
}
|
||||
|
||||
private string ChangeStatusLabel(string status) => status switch { nameof(SignatoryStatus.FaceMatch) => STATUS_READY, _ => status };
|
||||
|
@ -6,98 +6,305 @@
|
||||
}
|
||||
|
||||
@section Head {
|
||||
<style>
|
||||
.video-container {
|
||||
position: relative;
|
||||
padding-bottom: 56.25%; /* 16:9 aspect ratio */
|
||||
background: #000;
|
||||
border: 2px solid #444;
|
||||
<style>
|
||||
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.modal-dialog-right {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: auto;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 350px; /* Adjust the width as needed */
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.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 */
|
||||
}
|
||||
.modal-content-right {
|
||||
border-radius: 0;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.video-element {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.list-group {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
.list-group-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
/* background-color: #f9f9f9; */
|
||||
/* box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); */
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
/* transform: translateX(-50%); */
|
||||
background: rgba(0,0,0,0.8);
|
||||
padding: 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: #007bff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.participant-col {
|
||||
transition: all 0.3s ease;
|
||||
|
||||
display:flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
.modal-header.draggable {
|
||||
cursor: move;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
<div class="container-fluid py-3" id="videoGrid-container">
|
||||
<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>
|
||||
|
||||
<template id="templateVideo">
|
||||
<div class="video-container bg-light">
|
||||
<div class="participant-name">Participant Name</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="vh-100" id="videoGrid-container"></div>
|
||||
|
||||
<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" />
|
||||
<input type="hidden" asp-for="CommunicationUserToken" />
|
||||
<input type="hidden" asp-for="CommunicationUserId" />
|
||||
<input type="hidden" asp-for="CommunicationRoomId" />
|
||||
<input type="hidden" asp-for="DisplayName" />
|
||||
<input type="hidden" asp-for="ParticipantType" />
|
||||
|
||||
<form method="post" asp-page-handler="StartRecording">
|
||||
<input type="hidden" asp-for="ServerCallID" />
|
||||
<input type="hidden" asp-for="Transaction_UID" />
|
||||
</form>
|
||||
|
||||
|
||||
<div class="modal fade right" id="RightSidebarModal" tabindex="-1" role="dialog" aria-labelledby="rightSidebarModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-right" role="document">
|
||||
<div class="modal-content modal-content-right">
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="rightSidebarModalLabel">Participants</h5>
|
||||
<button type="button" class="btn-close" aria-label="Close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="modal-body">
|
||||
<ul class="list-group" id="ParticipantListGroup">
|
||||
<li class="list-group-item">
|
||||
<div class="avatar">JD</div>
|
||||
John Doe
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<div class="avatar">SM</div>
|
||||
Sarah Miller
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<div class="avatar">AM</div>
|
||||
Alex Martin
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<div class="avatar">CW</div>
|
||||
Chris Wilson
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template id="TemplateParticipantItem">
|
||||
<li class="list-group-item participant-item" data-participant-uid="">
|
||||
<div class="avatar participant-avatar">JD</div>
|
||||
<span class="participant-name">John Doe</span>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
|
||||
<div class="modal fade " id="DraggableModal" data-bs-backdrop="false">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header draggable">
|
||||
<h4 class="modal-title">Signatory Name</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="modal-body">
|
||||
|
||||
<!-- Nav tabs -->
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="tab" href="#image1">Image 1</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#image2">Image 2</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Tab panes -->
|
||||
<div class="tab-content">
|
||||
<div id="image1" class="container tab-pane active">
|
||||
<br>
|
||||
<img src="https://placehold.co/600x400?text=Hello" alt="Image 1" class="img-fluid">
|
||||
</div>
|
||||
<div id="image2" class="container tab-pane fade">
|
||||
<br>
|
||||
<img src="https://placehold.co/600x400?text=World" alt="Image 2" class="img-fluid">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="modal-footer justify-content-start">
|
||||
<div class="flex-fill">
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Approve</button>
|
||||
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">Reject</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* <template id="TemplateSidePane">
|
||||
<div data-is-focusable="false" aria-modal="true" data-ui-id="SidePaneSignatories" class="ms-Stack css-366">
|
||||
<div class="ms-Stack css-367">
|
||||
<div role="heading" aria-label="People" aria-level="2" class="ms-StackItem css-368">Signatories</div>
|
||||
<div class="ms-StackItem css-157">
|
||||
<button type="button" class="ms-Button ms-Button--commandBar root-369" aria-label="Close" data-is-focusable="true">
|
||||
<span class="ms-Button-flexContainer flexContainer-193" data-automationid="splitbuttonprimary">
|
||||
<i data-icon-name="cancel" aria-hidden="true" class="ms-Icon root-89 css-196 ms-Button-icon icon-370" style="font-family: FabricMDL2Icons;"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-StackItem css-373">
|
||||
<div class="ms-Stack css-374">
|
||||
<div class="ms-StackItem css-375">
|
||||
<div class="ms-Stack css-252">
|
||||
<div data-ui-id="people-pane-content" class="ms-Stack css-376">
|
||||
<div class="ms-Stack css-162">
|
||||
<div aria-live="assertive" role="status" aria-atomic="true" class="ms-Stack css-241" />
|
||||
</div>
|
||||
<div class="ms-StackItem css-377">
|
||||
<div class="ms-Stack css-378">
|
||||
<div class="ms-Stack css-379">
|
||||
<div aria-label="In this call {numberOfPeople}" id="id__427" class="ms-StackItem css-380">
|
||||
<h2 id="SignatoryCount">In this call (2)</h2>
|
||||
</div>
|
||||
<div class="ms-StackItem css-157">
|
||||
<button type="button" data-ui-id="people-pane-header-more-button" class="ms-Button ms-Button--default ms-Button--hasMenu root-381" aria-label="More" data-is-focusable="true" aria-expanded="false" aria-haspopup="true">
|
||||
<span class="ms-Button-flexContainer flexContainer-193" data-automationid="splitbuttonprimary">
|
||||
<i data-icon-name="PeoplePaneMoreButton" aria-hidden="true" class="ms-Icon root-89 ms-Button-icon icon-382">
|
||||
<svg fill="currentColor" class="___12fm75w f1w7gpdv fez10in fg4l7m0" aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.75 10a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0Zm5 0a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0ZM15 11.75a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Z" fill="currentColor" />
|
||||
</svg>
|
||||
</i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-Stack css-384">
|
||||
<div id="SignatoryList" data-ui-id="participant-list" class="ms-Stack css-386" role="menu">
|
||||
<div role="menuitem" id="id__436" aria-label="principal3@jfaquinojr.com, , Muted, , , , " aria-labelledby="id__427 id__436" aria-expanded="true" aria-disabled="true" aria-controls="id__437" data-is-focusable="false" data-ui-id="participant-item" class="css-392">
|
||||
<div class="ms-Stack css-394">
|
||||
<div data-ui-id="chat-composite-participant-custom-avatar" class="ms-Persona ms-Persona--size32 root-396">
|
||||
<div role="presentation" class="ms-Persona-coin ms-Persona--size32 coin-261">
|
||||
<div role="presentation" class="ms-Persona-imageArea imageArea-399">
|
||||
<div class="ms-Persona-initials initials-402" aria-hidden="true">
|
||||
<span>P</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 0.5rem; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;">
|
||||
<div class="ms-TooltipHost root-404" role="none">
|
||||
<span aria-labelledby="text-tooltip428" class="css-405">principal3@jfaquinojr.com</span>
|
||||
<div hidden="" id="text-tooltip428" style="position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0px; border: 0px; overflow: hidden; white-space: nowrap;">principal3@jfaquinojr.com</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-Stack css-406">
|
||||
<div class="ms-Stack css-407">
|
||||
<i data-icon-name="ParticipantItemMicOff" role="img" aria-label="Muted" class="root-408">
|
||||
<svg fill="currentColor" class="___12fm75w f1w7gpdv fez10in fg4l7m0" aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 5v4.88l.9.9A3 3 0 0 0 13 10V5a3 3 0 0 0-6-.12l1 1V5a2 2 0 1 1 4 0ZM7 7.7 2.15 2.86a.5.5 0 1 1 .7-.7l15 15a.5.5 0 0 1-.7.7l-3.63-3.62a5.48 5.48 0 0 1-3.02 1.25v2.02a.5.5 0 0 1-1 0v-2.02a5.5 5.5 0 0 1-5-5.48.5.5 0 0 1 1 0 4.5 4.5 0 0 0 7.3 3.52l-1.06-1.07A3 3 0 0 1 7 10V7.7Zm4.02 4.02L8 8.71V10a2 2 0 0 0 3.02 1.72Zm3.78.96-.74-.74c.28-.59.44-1.25.44-1.94a.5.5 0 0 1 1 0c0 .97-.25 1.89-.7 2.68Z" fill="currentColor" />
|
||||
</svg>
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="menuitem" id="id__442" aria-label="Notary One, , , , , , " aria-labelledby="id__427 id__442" aria-expanded="false" aria-disabled="true" aria-controls="id__443" data-is-focusable="false" data-ui-id="participant-item" class="css-392">
|
||||
<div class="ms-Stack css-394">
|
||||
<div data-ui-id="chat-composite-participant-custom-avatar" class="ms-Persona ms-Persona--size32 root-396">
|
||||
<div role="presentation" class="ms-Persona-coin ms-Persona--size32 coin-261">
|
||||
<div role="presentation" class="ms-Persona-imageArea imageArea-399">
|
||||
<div class="ms-Persona-initials initials-409" aria-hidden="true">
|
||||
<span>NO</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 0.5rem; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;">
|
||||
<div class="ms-TooltipHost root-404" role="none">
|
||||
<span aria-labelledby="text-tooltip428" class="css-405">Notary One</span>
|
||||
<div hidden="" id="text-tooltip428" style="position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0px; border: 0px; overflow: hidden; white-space: nowrap;">Notary One</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="css-410">(you)</span>
|
||||
<div class="ms-Stack css-406" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="TemplateSignatoryItem">
|
||||
<div role="menuitem" class="css-392">
|
||||
<div class="ms-Stack css-394">
|
||||
<div class="ms-Persona ms-Persona--size32 root-396">
|
||||
<div role="presentation" class="ms-Persona-coin ms-Persona--size32 coin-261">
|
||||
<div role="presentation" class="ms-Persona-imageArea imageArea-399">
|
||||
<div class="ms-Persona-initials initials-402" aria-hidden="true">
|
||||
<span class="signatory-initials">X</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 0.5rem; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;">
|
||||
<div class="ms-TooltipHost root-404" role="none">
|
||||
<span aria-labelledby="text-tooltip428" class="css-405 signatory-name">principal3@jfaquinojr.com</span>
|
||||
<div hidden="" class="signatory-tooltip" style="position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0px; border: 0px; overflow: hidden; white-space: nowrap;">principal3@jfaquinojr.com</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-Stack css-406">
|
||||
<div class="ms-Stack css-407">
|
||||
<i data-icon-name="ParticipantItemMicOff" role="img" aria-label="Muted" class="root-408">
|
||||
<svg fill="currentColor" class="___12fm75w f1w7gpdv fez10in fg4l7m0" aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 5v4.88l.9.9A3 3 0 0 0 13 10V5a3 3 0 0 0-6-.12l1 1V5a2 2 0 1 1 4 0ZM7 7.7 2.15 2.86a.5.5 0 1 1 .7-.7l15 15a.5.5 0 0 1-.7.7l-3.63-3.62a5.48 5.48 0 0 1-3.02 1.25v2.02a.5.5 0 0 1-1 0v-2.02a5.5 5.5 0 0 1-5-5.48.5.5 0 0 1 1 0 4.5 4.5 0 0 0 7.3 3.52l-1.06-1.07A3 3 0 0 1 7 10V7.7Zm4.02 4.02L8 8.71V10a2 2 0 0 0 3.02 1.72Zm3.78.96-.74-.74c.28-.59.44-1.25.44-1.94a.5.5 0 0 1 1 0c0 .97-.25 1.89-.7 2.68Z" fill="currentColor" />
|
||||
</svg>
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
*@
|
||||
@section Scripts {
|
||||
<script type="text/javascript" src="/dist/_jfa.js"></script>
|
||||
<script type="text/javascript" src="/lib/azure-communication-service/callComposite.js"></script>
|
||||
<script src="~/Pages/Participant/VideoCall/Room.cshtml.js" asp-append-version="true"></script>
|
||||
}
|
||||
|
@ -7,18 +7,17 @@ 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;
|
||||
private LawyerVideoConferenceSchedule _LawyerVideoConferenceSchedule;
|
||||
private readonly IVideoConferenceService _videoConferenceService;
|
||||
private Transaction _Transaction;
|
||||
|
||||
public RoomModel(ICurrentUserService currentUserService, NotaryoDBContext dbContext, IConferenceSheduleService conferenceSheduleService)
|
||||
public RoomModel(ICurrentUserService currentUserService, NotaryoDBContext dbContext,
|
||||
IVideoConferenceService videoConferenceService)
|
||||
{
|
||||
_currentUserService = currentUserService;
|
||||
_dbContext = dbContext;
|
||||
_conferenceSheduleService = conferenceSheduleService;
|
||||
_videoConferenceService = videoConferenceService;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
@ -30,8 +29,8 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
|
||||
}
|
||||
|
||||
var currentUser = _dbContext.Users.Single(u => u.User_UID == _currentUserService.GetUser_UID());
|
||||
var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID);
|
||||
if (schedule_UID == Guid.Empty)
|
||||
var canStart = _videoConferenceService.CanStart(Transaction_UID);
|
||||
if (!canStart)
|
||||
{
|
||||
if (_Transaction.PrincipalID == currentUser.UserID)
|
||||
{
|
||||
@ -46,59 +45,54 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
|
||||
return Redirect("/");
|
||||
}
|
||||
|
||||
LoadVideoConferenceSchedule(schedule_UID);
|
||||
|
||||
if ((DateTime.UtcNow - _LawyerVideoConferenceSchedule.MeetingDate).TotalHours > VideoConferenceExpirationInHours)
|
||||
if (_videoConferenceService.HasExpired(Transaction_UID))
|
||||
{
|
||||
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("/");
|
||||
}
|
||||
|
||||
var schedule_UID = await _videoConferenceService.StartAsync(Transaction_UID);
|
||||
|
||||
CommunicationUserToken = currentUser.Role == nameof(UserType.Notary)
|
||||
? _LawyerVideoConferenceSchedule.MeetingRoomTokenID
|
||||
: _LawyerVideoConferenceSchedule.LawyerVideoConferenceParticipants.First(u => u.ParticipantID == currentUser.UserID).MeetingRoomTokenID;
|
||||
CommunicationRoomId = _LawyerVideoConferenceSchedule.MeetingRoomID;
|
||||
? _Transaction.Schedule.MeetingRoomTokenID
|
||||
: GetParticipant(currentUser).MeetingRoomTokenID;
|
||||
CommunicationRoomId = _Transaction.Schedule.MeetingRoomID;
|
||||
CommunicationUserId = currentUser.Role == nameof(UserType.Notary)
|
||||
? _LawyerVideoConferenceSchedule.MeetingRoomUserID
|
||||
: _LawyerVideoConferenceSchedule.LawyerVideoConferenceParticipants.First(u => u.ParticipantID == currentUser.UserID).MeetingRoomUserID;
|
||||
? _Transaction.Schedule.MeetingRoomUserID
|
||||
: GetParticipant(currentUser).MeetingRoomUserID;
|
||||
DisplayName = currentUser.Role == nameof(UserType.Notary)
|
||||
? _Transaction.Lawyer.User.Fullname
|
||||
: GetParticipant(currentUser).Participant.Fullname.DefaultIfEmpty(currentUser.Email);
|
||||
ParticipantType = currentUser.Role == nameof(UserType.Notary)
|
||||
? nameof(UserType.Notary)
|
||||
: GetParticipant(currentUser).Participant.Role;
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task OnPostStartRecordingAsync()
|
||||
private LawyerVideoConferenceParticipant GetParticipant(User currentUser) => _Transaction.Schedule.LawyerVideoConferenceParticipants.First(u => u.ParticipantID == currentUser.UserID);
|
||||
|
||||
public async Task<JsonResult> OnPostApproveAsync()
|
||||
{
|
||||
LoadTransaction();
|
||||
var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID);
|
||||
LoadVideoConferenceSchedule(schedule_UID);
|
||||
await _conferenceSheduleService.StartRecordingAsync(Transaction_UID, ServerCallID);
|
||||
await _videoConferenceService.ApproveTransactionAsync(Transaction_UID);
|
||||
return new JsonResult(true);
|
||||
}
|
||||
|
||||
public async Task OnPostStopRecordingAsync()
|
||||
public async Task<JsonResult> OnPostStartRecordingAsync()
|
||||
{
|
||||
LoadTransaction();
|
||||
var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID);
|
||||
LoadVideoConferenceSchedule(schedule_UID);
|
||||
await _conferenceSheduleService.StopRecordingAsync(Transaction_UID);
|
||||
await _videoConferenceService.StartRecordingAsync(Transaction_UID, ServerCallID);
|
||||
return new JsonResult(true);
|
||||
}
|
||||
|
||||
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);
|
||||
.Include(t => t.TransactionSignatories)
|
||||
.Include(t => t.Lawyer)
|
||||
.ThenInclude(l => l.User)
|
||||
.Include(t => t.Principal)
|
||||
.Include(t => t.Schedule)
|
||||
.ThenInclude(sch => sch.LawyerVideoConferenceParticipants)
|
||||
.FirstOrDefault(t => t.Transaction_UID == Transaction_UID);
|
||||
|
||||
public string CommunicationRoomId { get; private set; }
|
||||
|
||||
@ -106,25 +100,33 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
|
||||
|
||||
public string CommunicationUserToken { get; private set; }
|
||||
|
||||
public string DisplayName { get; private set; }
|
||||
|
||||
public string ParticipantType { get; set; }
|
||||
|
||||
public List<RoomParticipantViewModel> Participants
|
||||
{
|
||||
get
|
||||
{
|
||||
var signatoryTypes = _Transaction.TransactionSignatories.Where(t => t.UserID > 0).ToDictionary(k => k.UserID, v => v.Type);
|
||||
var participants = _LawyerVideoConferenceSchedule.LawyerVideoConferenceParticipants.ConvertAll(p => new RoomParticipantViewModel
|
||||
{
|
||||
Id = p.LawyerVideoConferenceParticipant_UID.ToString(),
|
||||
DisplayName = $"{p.Participant.Firstname} {p.Participant.Lastname}".Trim().DefaultIfEmpty(p.Participant.Email),
|
||||
RoomUserID = p.MeetingRoomUserID,
|
||||
Type = signatoryTypes.GetValueOrDefault(p.ParticipantID, nameof(UserType.Principal))
|
||||
});
|
||||
var signatoryTypes = _Transaction.TransactionSignatories.Where(t => t.UserID > 0)
|
||||
.ToDictionary(k => k.UserID, v => v.Type);
|
||||
var participants = _Transaction.Schedule.LawyerVideoConferenceParticipants.ConvertAll(p =>
|
||||
new RoomParticipantViewModel
|
||||
{
|
||||
Id = p.LawyerVideoConferenceParticipant_UID.ToString(),
|
||||
DisplayName =
|
||||
$"{p.Participant.Firstname} {p.Participant.Lastname}".Trim()
|
||||
.DefaultIfEmpty(p.Participant.Email),
|
||||
RoomUserID = p.MeetingRoomUserID,
|
||||
Type = signatoryTypes.GetValueOrDefault(p.ParticipantID, nameof(UserType.Principal))
|
||||
});
|
||||
|
||||
var host = _LawyerVideoConferenceSchedule.Lawyer.User;
|
||||
var host = _Transaction.Schedule.Lawyer.User;
|
||||
participants.Add(new RoomParticipantViewModel
|
||||
{
|
||||
DisplayName = $"{host.Firstname} {host.Lastname}".Trim().DefaultIfEmpty(host.Email),
|
||||
Id = Guid.Empty.ToString(),
|
||||
RoomUserID = _LawyerVideoConferenceSchedule.MeetingRoomUserID,
|
||||
RoomUserID = _Transaction.Schedule.MeetingRoomUserID,
|
||||
Type = nameof(UserType.Notary)
|
||||
});
|
||||
|
||||
@ -132,10 +134,8 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
|
||||
}
|
||||
}
|
||||
|
||||
[BindProperty(SupportsGet = true)]
|
||||
public string ServerCallID { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public string ServerCallID { get; set; }
|
||||
|
||||
[BindProperty(SupportsGet = true)]
|
||||
public Guid Transaction_UID { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public Guid Transaction_UID { get; set; }
|
||||
}
|
||||
}
|
@ -5,114 +5,291 @@
|
||||
control_communicationRoomId = document.getElementById("CommunicationRoomId"),
|
||||
control_communicationUserId = document.getElementById("CommunicationUserId"),
|
||||
control_communicationUserToken = document.getElementById("CommunicationUserToken"),
|
||||
control_displayName = document.getElementById("DisplayName"),
|
||||
control_participants = document.getElementById("Participants"),
|
||||
control_reject = document.getElementById("Reject"),
|
||||
control_templateVideo = document.getElementById("templateVideo"),
|
||||
control_videoGrid = document.getElementById("VideoGrid"),
|
||||
control_templateSidePane = document.getElementById("TemplateSidePane"),
|
||||
control_templateParticipantItem = document.getElementById("TemplateParticipantItem"),
|
||||
control_draggableModal = document.getElementById("DraggableModal"),
|
||||
control_rightSidebarModal = document.getElementById("RightSidebarModal"),
|
||||
/*control_videoGrid = document.getElementById("VideoGrid"),*/
|
||||
control_videoGridContainer = document.getElementById("videoGrid-container"),
|
||||
control_viewDocument = document.getElementById("ViewDocument"),
|
||||
control_serverCallIID = document.getElementById("ServerCallID"),
|
||||
control_participantType = document.getElementById("ParticipantType"),
|
||||
control_participantListGroup = document.getElementById("ParticipantListGroup"),
|
||||
x = 1;
|
||||
|
||||
let participants = JSON.parse(control_participants.value);
|
||||
|
||||
async function _initVideoCall() {
|
||||
let userAccessToken = control_communicationUserToken.value;
|
||||
let self = this;
|
||||
let customButtons = [];
|
||||
if (control_participantType.value == 'Notary') {
|
||||
customButtons = [
|
||||
function (args) {
|
||||
return {
|
||||
placement: 'secondary',
|
||||
// Icon registered by the composites.
|
||||
iconName: 'OpenAttachment',
|
||||
strings: {
|
||||
label: 'View Document',
|
||||
tooltipContent: 'View the document.'
|
||||
},
|
||||
onItemClick: function () {
|
||||
alert('Document Modal goes here.');
|
||||
}
|
||||
};
|
||||
},
|
||||
function (args) {
|
||||
return {
|
||||
placement: 'secondary',
|
||||
// Icon registered by the composites.
|
||||
iconName: 'ControlButtonParticipantsContextualMenuItem',
|
||||
strings: {
|
||||
label: 'View Participants',
|
||||
tooltipContent: 'View the participants.'
|
||||
},
|
||||
onItemClick: function () {
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(control_rightSidebarModal);
|
||||
modal.show();
|
||||
}
|
||||
};
|
||||
},
|
||||
function (args, b, c) {
|
||||
return {
|
||||
placement: 'primary',
|
||||
// Icon registered by the composites.
|
||||
iconName: 'EditBoxSubmit',
|
||||
strings: {
|
||||
label: 'Approve',
|
||||
tooltipContent: 'Approve the Transaction'
|
||||
},
|
||||
onItemClick: approveTransaction
|
||||
};
|
||||
},
|
||||
function (args, b, c) {
|
||||
return {
|
||||
placement: 'primary',
|
||||
// Icon registered by the composites.
|
||||
iconName: 'EditBoxCancel',
|
||||
strings: {
|
||||
label: 'Reject',
|
||||
tooltipContent: 'Reject the Transaction'
|
||||
},
|
||||
onItemClick: function () {
|
||||
alert('Rejected!');
|
||||
}
|
||||
};
|
||||
}
|
||||
];
|
||||
} else {
|
||||
customButtons = [
|
||||
function (args) {
|
||||
return {
|
||||
placement: 'primary',
|
||||
// Icon registered by the composites.
|
||||
iconName: 'OpenAttachment',
|
||||
strings: {
|
||||
label: 'View Document',
|
||||
tooltipContent: 'View the document.'
|
||||
},
|
||||
onItemClick: function () {
|
||||
alert('Document Modal goes here.');
|
||||
}
|
||||
};
|
||||
},
|
||||
function (args) {
|
||||
return {
|
||||
placement: 'secondary',
|
||||
// Icon registered by the composites.
|
||||
iconName: 'OpenAttachment',
|
||||
strings: {
|
||||
label: 'View Document',
|
||||
tooltipContent: 'View the document.'
|
||||
},
|
||||
onItemClick: function () {
|
||||
alert('Document Modal goes here.');
|
||||
}
|
||||
};
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
//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);
|
||||
// }
|
||||
//},
|
||||
onGetServerCallIDCallback: 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));
|
||||
//debugger;
|
||||
},
|
||||
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);
|
||||
}
|
||||
stateChangedCallback: function (state) {
|
||||
//console.log(state);
|
||||
},
|
||||
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));
|
||||
}
|
||||
onFetchParticipantMenuItemsCallback: function (participantUserId, userId, defaultMenuItems) {
|
||||
const participantList = document.querySelector('[data-ui-id="people-pane-content"]');
|
||||
if (participantList) {
|
||||
participantList.innerHTML = '';
|
||||
console.log(participants);
|
||||
}
|
||||
defaultMenuItems = [];
|
||||
},
|
||||
onFetchCustomButtonPropsCallbacks: customButtons,
|
||||
onAddParticipantCallback: function (e, f) {
|
||||
//debugger;
|
||||
console.log(e, f);
|
||||
},
|
||||
participantsJoinedCallback: function (e, f) {
|
||||
//debugger;
|
||||
console.log(e, f);
|
||||
},
|
||||
callEndedCallback: function (a, b, c) {
|
||||
},
|
||||
token: control_communicationUserToken.value,
|
||||
roomID: control_communicationRoomId.value,
|
||||
displayName: control_displayName.value,
|
||||
userID: control_communicationUserId.value,
|
||||
videoContainer: control_videoGridContainer
|
||||
};
|
||||
await videoCall.init(userAccessToken, options);
|
||||
videoCall.joinRoom(control_communicationRoomId.value);
|
||||
await videoCall.init(options);
|
||||
await videoCall.joinRoom();
|
||||
}
|
||||
|
||||
function _updateGrid() {
|
||||
control_videoGrid.innerHTML = '';
|
||||
function approveTransaction(e) {
|
||||
jfa.communication.videocall.stopCall(true);
|
||||
let url = jfa.utilities.routing.getCurrentURLWithHandler("Approve");
|
||||
jfa.utilities.request.post(url, {})
|
||||
.then(resp => {
|
||||
if (resp.ok === true) {
|
||||
debugger;
|
||||
}
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
}
|
||||
|
||||
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');
|
||||
//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 _createParticipantListItems() {
|
||||
control_participantListGroup.innerHTML = '';
|
||||
participants.forEach(participant => {
|
||||
if (participant.Type === 'Notary') {
|
||||
return;
|
||||
}
|
||||
let participantName = tmpl.querySelector(".participant-name")
|
||||
if (participantName) {
|
||||
participantName.textContent = participant.DisplayName;
|
||||
}
|
||||
col.appendChild(tmpl);
|
||||
control_videoGrid.appendChild(col);
|
||||
const initials = participant.DisplayName.split(' ').map(n => n.charAt(0)).join('');
|
||||
let tmpl = control_templateParticipantItem.cloneNode(true).content;
|
||||
tmpl.querySelector('.participant-item').dataset.participantUid = participant.RoomUserID;
|
||||
tmpl.querySelector('.participant-avatar').textContent = initials;
|
||||
tmpl.querySelector('.participant-name').textContent = participant.DisplayName;
|
||||
control_participantListGroup.appendChild(tmpl);
|
||||
});
|
||||
|
||||
// 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_participantListGroup.addEventListener('click', function (event) {
|
||||
let target = event.target?.closest('.list-group-item');
|
||||
if (target) {
|
||||
|
||||
const sidebarModal = bootstrap.Modal.getOrCreateInstance(control_rightSidebarModal);
|
||||
sidebarModal.hide();
|
||||
|
||||
const draggableModal = bootstrap.Modal.getOrCreateInstance(control_draggableModal);
|
||||
draggableModal.show();
|
||||
}
|
||||
});
|
||||
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');
|
||||
|
||||
control_draggableModal.addEventListener('shown.bs.modal', function (e) {
|
||||
makeDraggable(control_draggableModal);
|
||||
});
|
||||
}
|
||||
|
||||
function makeDraggable(modal) {
|
||||
let isDragging = false;
|
||||
let offsetX, offsetY;
|
||||
|
||||
const header = modal.querySelector('.modal-header');
|
||||
|
||||
header.addEventListener('mousedown', (e) => {
|
||||
isDragging = true;
|
||||
offsetX = e.clientX - modal.offsetLeft;
|
||||
offsetY = e.clientY - modal.offsetTop;
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isDragging) return;
|
||||
modal.style.left = `${e.clientX - offsetX}px`;
|
||||
modal.style.top = `${e.clientY - offsetY}px`;
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
isDragging = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function _init() {
|
||||
_bindEvents();
|
||||
_updateGrid();
|
||||
//_updateGrid();
|
||||
await _initVideoCall();
|
||||
debugger;
|
||||
_createParticipantListItems();
|
||||
}
|
||||
|
||||
await _init();
|
||||
|
@ -9,32 +9,31 @@ namespace EnotaryoPH.Web.Pages.Participant.VideoCall
|
||||
{
|
||||
private readonly NotaryoDBContext _dbContext;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
private readonly IConferenceSheduleService _conferenceSheduleService;
|
||||
private readonly IVideoConferenceService _videoConferenceService;
|
||||
private Transaction _transactionEntity;
|
||||
|
||||
public WaitingModel(NotaryoDBContext notaryoDBContext, ICurrentUserService currentUserService, IConferenceSheduleService conferenceSheduleService)
|
||||
public WaitingModel(NotaryoDBContext notaryoDBContext, ICurrentUserService currentUserService, IVideoConferenceService videoConferenceService)
|
||||
{
|
||||
_dbContext = notaryoDBContext;
|
||||
_currentUserService = currentUserService;
|
||||
_conferenceSheduleService = conferenceSheduleService;
|
||||
_videoConferenceService = videoConferenceService;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
_transactionEntity = _dbContext.Transactions
|
||||
.Include(t => t.TransactionSignatories)
|
||||
.ThenInclude(ts => ts.User)
|
||||
.Include(t => t.Lawyer)
|
||||
.ThenInclude(l => l.User)
|
||||
.Include(t => t.Principal)
|
||||
.FirstOrDefault(t => t.Transaction_UID == Transaction_UID);
|
||||
.Include(t => t.TransactionSignatories)
|
||||
.ThenInclude(ts => ts.User)
|
||||
.Include(t => t.Lawyer)
|
||||
.ThenInclude(l => l.User)
|
||||
.Include(t => t.Principal)
|
||||
.FirstOrDefault(t => t.Transaction_UID == Transaction_UID);
|
||||
if (_transactionEntity == null || _transactionEntity.Status.IsInList(TransactionState.Completed))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var schedule_UID = await _conferenceSheduleService.GetOrCreateScheduleIDAsync(Transaction_UID);
|
||||
if (schedule_UID != Guid.Empty)
|
||||
if (_videoConferenceService.CanStart(Transaction_UID))
|
||||
{
|
||||
return Redirect($"/Participant/VideoCall/Room/{Transaction_UID}");
|
||||
}
|
||||
|
@ -71,7 +71,8 @@
|
||||
.withUrl("/notificationHub")
|
||||
.build();
|
||||
|
||||
connection.on("ReceiveUserNotification", (user_UID, message) => {
|
||||
connection.on("ReceiveUserNotification", (userID, message) => {
|
||||
alert(message);
|
||||
receiveUserNotificationCallback?.(message);
|
||||
});
|
||||
|
||||
|
@ -2,6 +2,7 @@ using System.Security.Principal;
|
||||
using Azure.Communication.CallAutomation;
|
||||
using Azure.Communication.Identity;
|
||||
using Azure.Communication.Rooms;
|
||||
using Azure.Storage.Queues;
|
||||
using Coravel;
|
||||
using EnotaryoPH.Data;
|
||||
using EnotaryoPH.Web.Common.Hubs;
|
||||
@ -35,7 +36,7 @@ namespace EnotaryoPH.Web
|
||||
razorBuilder.AddRazorRuntimeCompilation();
|
||||
#endif
|
||||
builder.Services.AddSignalR();
|
||||
builder.Services.AddSingleton<NotificationService>();
|
||||
builder.Services.AddSingleton<INotificationService, NotificationService>();
|
||||
|
||||
var config = builder.Configuration;
|
||||
builder.Services.AddTransient<IConfiguration, ConfigurationManager>(provider => config);
|
||||
@ -60,9 +61,12 @@ namespace EnotaryoPH.Web
|
||||
return new CompreFaceClient(host, port);
|
||||
});
|
||||
builder.Services.AddQueue();
|
||||
builder.Services.AddScheduler();
|
||||
builder.Services.AddMailer(config);
|
||||
builder.Services.AddTransient<SignatoryInvitationInvocable>();
|
||||
builder.Services.AddTransient<IConferenceSheduleService, ConferenceSheduleService>();
|
||||
builder.Services.AddTransient<CheckRecordingAvailabilityInvocable>();
|
||||
builder.Services.AddTransient<IVideoConferenceService, VideoConferenceService>();
|
||||
builder.Services.AddTransient<IEventService, EventService>();
|
||||
builder.Services.AddScoped<RoomsClient>(provider =>
|
||||
{
|
||||
var connectionString = config.GetConnectionString("AzureCommunication");
|
||||
@ -84,9 +88,20 @@ namespace EnotaryoPH.Web
|
||||
? throw new InvalidConfigurationException("AzureCommunication", string.Empty)
|
||||
: new CallAutomationClient(connectionString);
|
||||
});
|
||||
builder.Services.AddScoped<QueueClient>(provider =>
|
||||
{
|
||||
var connectionString = config.GetConnectionString("AzureStorage");
|
||||
return string.IsNullOrEmpty(connectionString)
|
||||
? throw new InvalidConfigurationException("AzureStorage", string.Empty)
|
||||
: new QueueClient(connectionString, "recording-ready");
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.Services.UseScheduler(scheduler => scheduler
|
||||
.Schedule<CheckRecordingAvailabilityInvocable>()
|
||||
.EveryTenSeconds());
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
|
@ -9,8 +9,8 @@
|
||||
"context": "browser",
|
||||
"outputFormat": "global",
|
||||
"scopeHoist": false,
|
||||
"optimize": true,
|
||||
"sourceMap": false
|
||||
"optimize": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"browserslist": "> 0.5%",
|
||||
|
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user