569 lines
17 KiB
Vue
569 lines
17 KiB
Vue
<script setup>
|
|
|
|
import axios from '@axios';
|
|
import {
|
|
LocalParticipant,
|
|
Room,
|
|
RoomEvent,
|
|
Track,
|
|
VideoPresets,
|
|
facingModeFromLocalTrack
|
|
} from 'livekit-client';
|
|
import { defineEmits, defineProps, nextTick, ref } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
const props = defineProps({
|
|
token: {
|
|
type: String,
|
|
required: true,
|
|
}, recording: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
height: {
|
|
type: String,
|
|
required: false,
|
|
default: ''
|
|
}
|
|
})
|
|
const emit = defineEmits([
|
|
'update:token',
|
|
'update:transcriptData'
|
|
])
|
|
let room = null
|
|
const url = "wss://plugnmeet.codelfi.com"
|
|
const participants = ref([])
|
|
const cameras = ref([])
|
|
const cameraDefault = ref()
|
|
const microphones = ref([])
|
|
const microphoneDefault = ref()
|
|
const speakers = ref([])
|
|
const speakerDefault = ref()
|
|
const callStart = ref(false)
|
|
const userType = ref(localStorage.getItem('user_role'))
|
|
const recording = ref(false)
|
|
const isMobile = ref(window.innerWidth <= 768);
|
|
const transcriptData = ref('')
|
|
let rtCon = null
|
|
const appointmentId = localStorage.getItem('patient_appiontment_id')
|
|
const strData = JSON.stringify({ some: "data" })
|
|
const decoder = new TextDecoder()
|
|
|
|
|
|
|
|
const checkMobile = () => {
|
|
isMobile.value = window.innerWidth <= 768; // Adjust the breakpoint as needed
|
|
};
|
|
onMounted(async () => {
|
|
window.addEventListener('resize', checkMobile);
|
|
document.addEventListener('mousemove', handleMouseMove)
|
|
connect();
|
|
// if (userType.value == 'agent')
|
|
// getAppointmentDetails();
|
|
});
|
|
onUnmounted(() => {
|
|
window.removeEventListener('resize', checkMobile);
|
|
document.removeEventListener('mousemove', handleMouseMove)
|
|
|
|
});
|
|
|
|
|
|
const x = ref(0)
|
|
const y = ref(0)
|
|
const dragging = ref(false)
|
|
|
|
const rect = ref(null)
|
|
|
|
const style = computed(() => {
|
|
return {
|
|
transform: `translate(${x.value}px, ${y.value}px)`
|
|
}
|
|
})
|
|
|
|
|
|
const handleMouseDown = e => {
|
|
rect.value = e.currentTarget.getBoundingClientRect()
|
|
dragging.value = true
|
|
}
|
|
|
|
const handleMouseMove = e => {
|
|
if (!dragging.value) return
|
|
x.value = Math.min(Math.max(0, e.clientX - rect.value.left), window.innerWidth - rect.value.width) - 25
|
|
y.value = Math.min(Math.max(0, e.clientY - rect.value.top), window.innerHeight - rect.value.height) - 25
|
|
}
|
|
|
|
const connect = async () => {
|
|
|
|
room = new Room({
|
|
// automatically manage subscribed video quality
|
|
adaptiveStream: true,
|
|
|
|
dynacast: true,
|
|
|
|
// default capture settings
|
|
videoCaptureDefaults: {
|
|
resolution: VideoPresets.h720.resolution,
|
|
},
|
|
});
|
|
|
|
// pre-warm connection, this can be called as early as your page is loaded
|
|
room.prepareConnection(url, props.token);
|
|
console.log(props.token)
|
|
|
|
// set up event listeners
|
|
room
|
|
.on(RoomEvent.TrackSubscribed, (track, publication, participant) => renderParticipant(participant))
|
|
.on(RoomEvent.TrackUnsubscribed, (track, publication, participant) => renderParticipant(participant, true))
|
|
// .on(RoomEvent.ActiveSpeakersChanged, (speakers) => console.log("handleTrackSubscribed"))
|
|
.on(RoomEvent.Disconnected, () => {
|
|
renderParticipant(room.localParticipant, true)
|
|
room.participants.forEach((p) => {
|
|
renderParticipant(p, true);
|
|
})
|
|
room = null
|
|
})
|
|
.on(RoomEvent.Reconnecting, () => appendLog('Reconnecting to room'))
|
|
.on(RoomEvent.Reconnected, async () => {
|
|
appendLog(
|
|
'Successfully reconnected. server',
|
|
await room.engine.getConnectedServerAddress(),
|
|
);
|
|
})
|
|
.on(RoomEvent.ParticipantConnected, async (participant) => {
|
|
console.log('ParticipantConnected')
|
|
callStart.value = true
|
|
|
|
})
|
|
.on(RoomEvent.DataReceived, async (payload, participant, kind) => {
|
|
const strData = decoder.decode(payload)
|
|
transcriptData.value = strData
|
|
emit('update:transcriptData', transcriptData.value);
|
|
console.log('strData', strData)
|
|
})
|
|
.on(RoomEvent.ParticipantDisconnected, (participant) => {
|
|
renderParticipant(participant, true);
|
|
console.log('part length ', room.participants.size)
|
|
setTimeout(() => {
|
|
if (room.participants.size == 0) {
|
|
console.log('disconnect')
|
|
room.disconnect();
|
|
callStart.value = false
|
|
|
|
// if (userType.value == 'agent')
|
|
// router.push('/queue-users');
|
|
|
|
if (userType.value == 'patient') {
|
|
localStorage.setItem('call-end-message', 'Password Sent to Email Address')
|
|
router.push('/overview');
|
|
}
|
|
}
|
|
|
|
}, 1000)
|
|
|
|
|
|
|
|
})
|
|
.on(RoomEvent.MediaDevicesChanged, async () => {
|
|
console.log('device changed')
|
|
})
|
|
.on(RoomEvent.LocalTrackUnpublished, () => renderParticipant(room.localParticipant))
|
|
.on(RoomEvent.LocalTrackPublished, () => renderParticipant(room.localParticipant));
|
|
|
|
// connect to room
|
|
await room.connect(url, props.token);
|
|
console.log("connected to room", room.name);
|
|
// publish local camera and mic tracks
|
|
await room.localParticipant.enableCameraAndMicrophone();
|
|
callStart.value = true
|
|
// connectSpeach();
|
|
|
|
if (props.recording == true) {
|
|
|
|
await axios.post('/agent/api/start-record/' + localStorage.getItem('patient_appiontment_id'))
|
|
.then(response => {
|
|
console.log('start rec', response.data.message)
|
|
if (response.data.message == 'Success')
|
|
recording.value = response.data.message
|
|
|
|
});
|
|
|
|
}
|
|
room.participants.forEach((participant) => {
|
|
renderParticipant(participant);
|
|
});
|
|
renderParticipant(room.localParticipant);
|
|
|
|
const videoDevices = await Room.getLocalDevices('videoinput');
|
|
const audioDevices = await Room.getLocalDevices('audioinput');
|
|
const audioOutputs = await Room.getLocalDevices('audiooutput');
|
|
callStart.value = true
|
|
let selectedVideoDevice = videoDevices[0];
|
|
let selectedMicDevice = audioDevices[0];
|
|
let selectedSpeakerDevice = audioOutputs[0];
|
|
|
|
cameras.value = videoDevices;
|
|
microphones.value = audioDevices;
|
|
speakers.value = audioOutputs;
|
|
|
|
console.log('camera ', cameras.value)
|
|
console.log('mic ', microphones.value)
|
|
console.log('speaker ', speakers.value)
|
|
// console.log('Devices ',videoDevices,audioDevices,audioOutputs)
|
|
if (videoDevices.length > 0) {
|
|
for (const videoDevice of videoDevices) {
|
|
|
|
if (videoDevice.deviceId == 'default') {
|
|
selectedVideoDevice = videoDevice
|
|
break;
|
|
}
|
|
}
|
|
cameraDefault.value = selectedVideoDevice.deviceId
|
|
console.log('default video', cameraDefault.value)
|
|
await room.switchActiveDevice('videoinput', cameraDefault.value);
|
|
}
|
|
if (audioDevices.length > 0) {
|
|
for (const audioDevice of audioDevices) {
|
|
|
|
if (audioDevice.deviceId == 'default') {
|
|
selectedMicDevice = audioDevice
|
|
break;
|
|
}
|
|
}
|
|
microphoneDefault.value = selectedMicDevice.deviceId
|
|
console.log('default mic', microphoneDefault.value)
|
|
await room.switchActiveDevice('audioinput', microphoneDefault.value);
|
|
}
|
|
if (audioOutputs.length > 0) {
|
|
for (const audioOutput of audioOutputs) {
|
|
|
|
if (audioOutput.deviceId == 'default') {
|
|
selectedSpeakerDevice = audioOutput
|
|
break;
|
|
}
|
|
}
|
|
speakerDefault.value = selectedSpeakerDevice.deviceId
|
|
console.log('default audio', speakerDefault.value)
|
|
await room.switchActiveDevice('audiooutput', speakerDefault.value);
|
|
}
|
|
|
|
|
|
return room;
|
|
}
|
|
|
|
const renderParticipant = async (participant, remove = false) => {
|
|
let _participant = participants.value.find(part => part.id == participant.identity)
|
|
if (!_participant) {
|
|
_participant = {
|
|
id: participant.identity,
|
|
type: (participant instanceof LocalParticipant) ? "local" : "remote",
|
|
video: null,
|
|
audio: null,
|
|
};
|
|
participants.value.push(_participant)
|
|
|
|
}
|
|
|
|
if (remove) {
|
|
participants.value.splice(participants.value.indexOf(_participant), 1)
|
|
return;
|
|
}
|
|
|
|
const cameraPub = participant.getTrack(Track.Source.Camera);
|
|
const micPub = participant.getTrack(Track.Source.Microphone);
|
|
|
|
await nextTick()
|
|
// if ((participant instanceof LocalParticipant) && micPub?.track.getProcessor()?.name != "local-processor") {
|
|
// micPub.track.setProcessor(localProcessor);
|
|
// }
|
|
// if (micPub?.getProcessor()?.name != "remote-processor" && !(participant instanceof LocalParticipant)) {
|
|
// micPub.setProcessor(remoteProcessor);
|
|
// }
|
|
window.r = room
|
|
_participant.video = document.getElementById("video-" + _participant.id)
|
|
_participant.audio = document.getElementById("audio-" + _participant.id)
|
|
console.log('video ', _participant.video)
|
|
|
|
|
|
|
|
|
|
const cameraEnabled = cameraPub && cameraPub.isSubscribed && !cameraPub.isMuted;
|
|
if (cameraEnabled) {
|
|
|
|
cameraPub?.videoTrack?.attach(_participant.video);
|
|
} else {
|
|
if (cameraPub?.videoTrack) {
|
|
// detach manually whenever possible
|
|
cameraPub.videoTrack?.detach(_participant.video);
|
|
} else if (_participant.video) {
|
|
|
|
_participant.video.src = '';
|
|
_participant.video.srcObject = null;
|
|
}
|
|
}
|
|
|
|
|
|
const micEnabled = micPub && micPub.isSubscribed && !micPub.isMuted;
|
|
if (micEnabled) {
|
|
if (!(participant instanceof LocalParticipant)) {
|
|
|
|
|
|
|
|
if (!rtCon) {
|
|
micPub?.audioTrack?.attach(_participant.audio);
|
|
|
|
console.log('audioTrack', _participant.audio)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
const endCall = async () => {
|
|
room.disconnect()
|
|
callStart.value = false
|
|
|
|
if (userType.value == 'agent')
|
|
router.push('/provider/queue-users');
|
|
|
|
if (userType.value == 'patient') {
|
|
localStorage.setItem('call-end-message', 'Password Sent to Email Address')
|
|
router.push('/doctor-appiontments');
|
|
}
|
|
}
|
|
|
|
const getAppointmentDetails = async () => {
|
|
axios.post('/agent/api/appointment-detail/' + appointmentId)
|
|
.then(r => {
|
|
let appointmentAnalytics = r.data.agent_appointment.analytics
|
|
transcriptData.value = appointmentAnalytics
|
|
console.log('appointmentAnalytics ', transcriptData.value)
|
|
emit('update:transcriptData', transcriptData.value);
|
|
}).catch(error => {
|
|
console.log("Error", error);
|
|
});
|
|
|
|
}
|
|
|
|
const flipCamera = () => {
|
|
const videoPub = room?.localParticipant.getTrack(Track, Source.Camera);
|
|
if (!videoPub) {
|
|
return;
|
|
}
|
|
const track = videoPub.track;
|
|
const facingMode = track && facingModeFromLocalTrack(track);
|
|
|
|
const options = {
|
|
resolution: VideoPresets.h720.resolution,
|
|
facingMode: facingMode?.facingMode === 'environment' ? 'user' : 'environment',
|
|
}
|
|
videoPub.videoTrack?.restartTrack(options)
|
|
}
|
|
|
|
const selectCamera = async (deviceId) => {
|
|
await room.switchActiveDevice('videoinput', deviceId);
|
|
cameraDefault.value = deviceId
|
|
console.log('camera selected ', deviceId);
|
|
}
|
|
|
|
const selectMicrophone = async (deviceId) => {
|
|
await room.switchActiveDevice('audioinput', deviceId);
|
|
microphoneDefault.value = deviceId
|
|
console.log('mic selected ', deviceId);
|
|
}
|
|
|
|
const selectSpeaker = async (deviceId) => {
|
|
await room.switchActiveDevice('audiooutput', deviceId);
|
|
speakerDefault.value = deviceId
|
|
console.log('speaker selected ', deviceId);
|
|
}
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
<VCard class="text-center text-sm-start rounded-0">
|
|
<div v-if="audioDevices">{{ audioDevices }}</div>
|
|
<div class="livekit-container" @mouseup="dragging = false" @mouseleave="dragging = false"
|
|
:style="`height:` + height">
|
|
<div class="recording-badge" v-if="recording">Recording</div>
|
|
<!-- <v-card class="draggable-card" width="150px" height="150px" color="light" @mousedown=""></v-card> -->
|
|
|
|
<template v-for="(participant, index) in participants" :key="index" :value="index">
|
|
<div v-if="participant.type == 'local'" :style="style" @mousedown="handleMouseDown" class="box">
|
|
<video class="small-video" ref="participant.video" :id="`video-` + participant.id"></video>
|
|
<audio ref="participant.audio" :id="`audio-` + participant.id"></audio>
|
|
</div>
|
|
<div class="remote-box" v-if="participant.type == 'remote'">
|
|
<video class="large-video" ref="participant.video" :id="`video-` + participant.id"></video>
|
|
<audio ref="participant.audio" :id="`audio-` + participant.id"></audio>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<VRow style="background-color: black;" v-if="callStart">
|
|
<VCol cols="12" md="12">
|
|
<div class="text-center mb-2">
|
|
|
|
<v-btn-toggle v-if="!isMobile" v-model="toggle" divided rounded="xl">
|
|
<v-row no-gutters>
|
|
<v-btn class="btn btn-secondary waves-effect waves-light" color="white" icon="mdi-microphone"
|
|
style="background-color: white;color: black;"></v-btn>
|
|
|
|
<v-menu :location="top">
|
|
<template v-slot:activator="{ props }">
|
|
<v-btn color="white" v-bind="props" icon="mdi-chevron-up" title="Devices">
|
|
</v-btn>
|
|
</template>
|
|
<v-list>
|
|
<v-list-item>
|
|
<v-list-item-title>
|
|
|
|
<v-menu :location="top">
|
|
<template v-slot:activator="{ props }">
|
|
<v-btn color="white" v-bind="props">Camera
|
|
<VIcon icon="mdi-chevron-up"></VIcon>
|
|
</v-btn>
|
|
</template>
|
|
<v-list>
|
|
<v-list-item v-for="(camera, index) in cameras" :key="index" :value="camera.deviceId"
|
|
@click="selectCamera(camera.deviceId)">
|
|
<v-list-item-title>
|
|
<VIcon color="green" icon="mdi-check" v-if="cameraDefault == camera.deviceId"></VIcon>
|
|
{{ camera.label }}
|
|
</v-list-item-title>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-menu>
|
|
</v-list-item-title>
|
|
<v-list-item-title>
|
|
|
|
<v-menu :location="top">
|
|
<template v-slot:activator="{ props }">
|
|
<v-btn color="white" v-bind="props">Microphone
|
|
<VIcon icon="mdi-chevron-up"></VIcon>
|
|
</v-btn>
|
|
</template>
|
|
<v-list>
|
|
<v-list-item v-for="(microphone, index) in microphones" :key="index"
|
|
:value="microphone.deviceId" @click="selectMicrophone(microphone.deviceId)">
|
|
<v-list-item-title>
|
|
<VIcon color="green" icon="mdi-check" v-if="microphoneDefault == microphone.deviceId">
|
|
</VIcon>
|
|
{{ microphone.label }}
|
|
</v-list-item-title>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-menu>
|
|
</v-list-item-title>
|
|
<v-list-item-title>
|
|
|
|
<v-menu :location="top">
|
|
<template v-slot:activator="{ props }">
|
|
<v-btn color="white" v-bind="props">Speaker
|
|
<VIcon icon="mdi-chevron-up"></VIcon>
|
|
</v-btn>
|
|
</template>
|
|
<v-list>
|
|
<v-list-item v-for="(speaker, index) in speakers" :key="index" :value="speaker.deviceId"
|
|
@click="selectSpeaker(speaker.deviceId)">
|
|
<v-list-item-title>
|
|
<VIcon color="green" icon="mdi-check" v-if="speakerDefault == speaker.deviceId"></VIcon>
|
|
{{ speaker.label }}
|
|
</v-list-item-title>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-menu>
|
|
</v-list-item-title>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-menu>
|
|
<v-btn class="btn btn-secondary waves-effect waves-light" color="white" icon="mdi-video"
|
|
style="background-color: white;color: black;"></v-btn>
|
|
</v-row>
|
|
</v-btn-toggle>
|
|
|
|
<v-btn v-if="isMobile" class="btn btn-secondary waves-effect waves-light" color="white" icon="mdi-camera-flip"
|
|
style="background-color: white;color: black;" @clicK="flipCamera"></v-btn>
|
|
|
|
<v-btn rounded="xl" class="btn btn-danger waves-effect waves-light ml-2" color="red" icon="mdi-phone-hangup"
|
|
@click="endCall()"></v-btn>
|
|
</div>
|
|
</VCol>
|
|
</VRow>
|
|
<!-- <div id="jaas-container" >
|
|
</div> -->
|
|
<!-- Show the iframe when countdown reaches 1 -->
|
|
<!-- <iframe v-if="showIframe" ref="myIframe" :src="responseUrl" width="100%" height="400" style="border:0"></iframe>
|
|
<div v-else>
|
|
Loading iframe...
|
|
</div> -->
|
|
</VCard>
|
|
</template>
|
|
<style scoped>
|
|
.recording-badge {
|
|
text-align: right;
|
|
color: white;
|
|
margin-right: 10px;
|
|
background: red;
|
|
min-width: 10%;
|
|
float: right;
|
|
border-radius: 25px;
|
|
padding-right: 3px;
|
|
z-index: 10;
|
|
position: relative;
|
|
}
|
|
|
|
.small-screen {
|
|
background-color: white;
|
|
min-height: 150px;
|
|
max-width: 24%;
|
|
}
|
|
|
|
.livekit-container {
|
|
min-height: 500px;
|
|
background-color: black;
|
|
position: relative;
|
|
}
|
|
|
|
.draggable-card {
|
|
cursor: move;
|
|
position: relative;
|
|
}
|
|
|
|
.box {
|
|
position: absolute;
|
|
cursor: move;
|
|
width: 150px;
|
|
height: 150px;
|
|
background: #313131;
|
|
border-radius: 10px;
|
|
margin-left: 10px;
|
|
z-index: 2;
|
|
}
|
|
|
|
.large-video {
|
|
height: 100%;
|
|
width: 100%;
|
|
}
|
|
|
|
.small-video {
|
|
max-width: 100%;
|
|
height: 100%;
|
|
max-height: 100%;
|
|
width: auto;
|
|
z-index: 9;
|
|
}
|
|
|
|
.remote-box {
|
|
position: absolute;
|
|
z-index: 1;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
</style>
|