|
@@ -0,0 +1,762 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
|
|
+<head>
|
|
|
+ <meta charset="utf-8">
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
|
+ <link href="https://fonts.googleapis.com/css?family=Nunito:200,600,700" rel="stylesheet">
|
|
|
+ <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
|
|
|
+ <link href="/css/app.css" rel="stylesheet">
|
|
|
+ <link rel="stylesheet" href="/fontawesome-free/css/all.min.css">
|
|
|
+ <link href="/css/meeting.css" rel="stylesheet">
|
|
|
+ <link href="/css/style.css" rel="stylesheet">
|
|
|
+ <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
|
|
|
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.0/sockjs.min.js"
|
|
|
+ integrity="sha512-5yJ548VSnLflcRxWNqVWYeQZnby8D8fJTmYRLyvs445j1XmzR8cnWi85lcHx3CUEeAX+GrK3TqTfzOO6LKDpdw=="
|
|
|
+ crossorigin="anonymous"></script>
|
|
|
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"
|
|
|
+ integrity="sha512-iKDtgDyTHjAitUDdLljGhenhPwrbBfqTKWO1mkhSFH3A7blITC9MhYon6SjnMhp4o0rADGw9yAC6EW4t5a4K3g=="
|
|
|
+ crossorigin="anonymous"></script>
|
|
|
+ <script defer src=//download.agora.io/sdk/web/AgoraRTC_N-4.1.0.js></script>
|
|
|
+ <title>Scholar: Meet</title>
|
|
|
+</head>
|
|
|
+
|
|
|
+<body class="p-0 m-0">
|
|
|
+
|
|
|
+ <div id="proCallComponent" v-cloak>
|
|
|
+
|
|
|
+ <div class="d-flex px-2 border-bottom mb-2 {{ $client ? '' : 'justify-content-center' }}">
|
|
|
+ @if($client)
|
|
|
+ <div class="py-2 font-weight-normal mcp-theme-1 d-inline-flex align-items-center flex-grow-1">
|
|
|
+ <i class="fa fa-user-injured small mr-2"></i>
|
|
|
+ <a href="#" onclick="return window.top.openInLHS('/patients/view/{{$client->uid}}')">
|
|
|
+ <span class="font-weight-bold">{{ $client->displayName() }}</span>
|
|
|
+ </a>
|
|
|
+ <span class="text-secondary ml-3">{{ $pro->displayName() }}</span>
|
|
|
+ <span class="text-secondary ml-3">{{ $session->uid }}</span>
|
|
|
+ <span class="text-secondary ml-3">{{ date('H:i:s') }}</span>
|
|
|
+ </div>
|
|
|
+ @endif
|
|
|
+
|
|
|
+ <div class="py-2 d-flex align-items-center">
|
|
|
+ <a href="#" v-if="ringer" v-on:click.prevent="toggleRinger()"
|
|
|
+ class="font-weight-bold btn btn-sm btn-success">
|
|
|
+ Ringer
|
|
|
+ <i class="ml-1 fa fa-volume-up"></i>
|
|
|
+ </a>
|
|
|
+ <a href="#" v-if="!ringer" v-on:click.prevent="toggleRinger()"
|
|
|
+ class="font-weight-bold btn btn-sm btn-warning">
|
|
|
+ Ringer
|
|
|
+ <i class="ml-1 fa fa-volume-mute"></i>
|
|
|
+ </a>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ @if($client)
|
|
|
+ <div class="">
|
|
|
+ <div class="main-view mx-auto">
|
|
|
+ <div id="self-view" class="full-view"
|
|
|
+ :data-self="mainViewParticipant.self"
|
|
|
+ :data-uid="mainViewParticipant.uid"
|
|
|
+ :data-name="mainViewParticipant.name"
|
|
|
+ :data-type="mainViewParticipant.type"></div>
|
|
|
+ <div class="thumbs">
|
|
|
+ <div v-if="mainViewParticipant.uid !== myMediaServiceIdentifier"
|
|
|
+ :id="'remote-view-' + myMediaServiceIdentifier"
|
|
|
+ :data-self="true"
|
|
|
+ :data-uid="myMediaServiceIdentifier"
|
|
|
+ :data-name="'You'"
|
|
|
+ :data-type="'PRO'" {{-- TODO: change in FE4 --}}
|
|
|
+ :data-audio="myMedia && myMedia.isMicrophoneOn ? 'on' : 'off'"
|
|
|
+ v-on:click.prevent="showInCenterView(true, myMediaServiceIdentifier, 'You', 'PRO')"
|
|
|
+ class="remote-view thumb-view c-pointer">
|
|
|
+ <i v-show="!myMedia || !myMedia.isMicrophoneOn" class="fa fa-microphone-slash muted"></i>
|
|
|
+ </div>
|
|
|
+ <div v-for="participant in otherParticipants"
|
|
|
+ v-if="mainViewParticipant.uid !== (+participant.mediaServiceIdentifier)"
|
|
|
+ :id="'remote-view-' + participant.mediaServiceIdentifier"
|
|
|
+ :data-self="false"
|
|
|
+ :data-uid="participant.mediaServiceIdentifier"
|
|
|
+ :data-name="participant.displayName"
|
|
|
+ :data-type="participant.participantType"
|
|
|
+ :data-audio="participant.media && participant.media.isMicrophoneOn ? 'on' : 'off'"
|
|
|
+ v-on:click.prevent="showInCenterView(false, participant.mediaServiceIdentifier, participant.displayName, participant.participantType)"
|
|
|
+ class="remote-view thumb-view c-pointer">
|
|
|
+ <i v-show="!participant.media || !participant.media.isMicrophoneOn" class="fa fa-microphone-slash muted"></i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="call-actions d-flex align-items-center">
|
|
|
+ <button class="btn btn-danger rounded-circle"
|
|
|
+ title="Leave Call"
|
|
|
+ v-on:click.prevent="leaveClientRoom()">
|
|
|
+ <i class="fa fa-phone"></i>
|
|
|
+ </button>
|
|
|
+ <button v-if="myMedia.isCameraOn" class="btn btn-default bg-light border rounded-circle"
|
|
|
+ title="Stop Camera"
|
|
|
+ v-on:click.prevent="myCameraIsOff()">
|
|
|
+ <i class="fa fa-video"></i>
|
|
|
+ </button>
|
|
|
+ <button v-if="!myMedia.isCameraOn" class="btn btn-secondary rounded-circle"
|
|
|
+ title="Start Camera"
|
|
|
+ v-on:click.prevent="myCameraIsOn()">
|
|
|
+ <i class="fa fa-video-slash"></i>
|
|
|
+ </button>
|
|
|
+ <button v-if="myMedia.isMicrophoneOn" class="btn btn-default bg-light border rounded-circle"
|
|
|
+ title="Stop Microphone"
|
|
|
+ v-on:click.prevent="myMicrophoneIsOff()">
|
|
|
+ <i class="fa fa-microphone"></i>
|
|
|
+ </button>
|
|
|
+ <button v-if="!myMedia.isMicrophoneOn" class="btn btn-secondary rounded-circle"
|
|
|
+ title="Start Microphone"
|
|
|
+ v-on:click.prevent="myMicrophoneIsOn()">
|
|
|
+ <i class="fa fa-microphone-slash"></i>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ @endif
|
|
|
+
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="border-top patient-queue mcp-theme-1" id="queueComponent" v-cloak>
|
|
|
+ <div class="bg-secondary text-white font-weight-bold text-center py-1" v-if="items.length > 0">
|
|
|
+ @{{ items.length }} patient@{{ items.length > 1 ? 's' : '' }} in the queue
|
|
|
+ </div>
|
|
|
+ <div class="bg-secondary text-white font-weight-bold text-center py-1" v-if="items.length === 0">
|
|
|
+ No patients in the queue
|
|
|
+ </div>
|
|
|
+ <div v-if="items && items.length" class="d-flex align-items-center my-1">
|
|
|
+ <div v-for="item in items">
|
|
|
+ <div class="queue-item border border-primary rounded mx-1" :title="item.name">
|
|
|
+ <div class="patient-avatar mb-1 text-dark">@{{ item.initials }}</div>
|
|
|
+ <div class="font-weight-bold small text-nowrap text-ellipsis">@{{ item.name }}</div>
|
|
|
+ </div>
|
|
|
+ <button class="btn btn-sm btn-primary mt-1 text-white font-weight-bold py-0 mx-auto d-block"
|
|
|
+ v-on:click.prevent="claim(item.clientUid)">Claim</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ (function () {
|
|
|
+ window.proCallComponent = new Vue({
|
|
|
+ el: '#proCallComponent',
|
|
|
+ delimiters: ['@{{', '}}'],
|
|
|
+ data: {
|
|
|
+
|
|
|
+ // model - declare up-front to make reactive - override with server data on mount
|
|
|
+ amIInAMeeting: false,
|
|
|
+ meetingType: '', // PRO/CLIENT,
|
|
|
+ inMeetingForClientUid: '',
|
|
|
+ inMeetingForClient: {
|
|
|
+ clientMediaServiceRoomIdentifier: '',
|
|
|
+ uid: '',
|
|
|
+ displayName: '',
|
|
|
+ dob: '',
|
|
|
+ },
|
|
|
+ myMediaServiceToken: '',
|
|
|
+ myMediaServiceIdentifier: '',
|
|
|
+ myMedia: {
|
|
|
+ isCameraAcquired: false,
|
|
|
+ isCameraOn: false,
|
|
|
+ isMicrophoneAcquired: false,
|
|
|
+ isMicrophoneOn: false,
|
|
|
+ },
|
|
|
+ otherParticipants: [
|
|
|
+ {
|
|
|
+ participantType: '', // PRO/CLIENT_GUEST,
|
|
|
+ uid: '',
|
|
|
+ mediaServiceIdentifier: '',
|
|
|
+ displayName: '',
|
|
|
+ media: {
|
|
|
+ isCameraAcquired: false,
|
|
|
+ isCameraOn: false,
|
|
|
+ isMicrophoneAcquired: false,
|
|
|
+ isMicrophoneOn: false,
|
|
|
+ },
|
|
|
+ awayMessage: '',
|
|
|
+ deviceType: '',
|
|
|
+ isMeetingAccessGranted: '',
|
|
|
+ isSocketConnected: '',
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ awayMessage: '',
|
|
|
+
|
|
|
+ // agora
|
|
|
+ mediaServiceClient: null, // set on agora init
|
|
|
+ appId: '{{ config('app.agora_appid') }}',
|
|
|
+ channel: '', // set on mount
|
|
|
+ myMicrophone: null,
|
|
|
+ myCamera: null,
|
|
|
+
|
|
|
+ // sockets
|
|
|
+ backendWsURL: '{{ config('app.backend_ws_url') }}',
|
|
|
+ socketClient: null,
|
|
|
+
|
|
|
+ // other
|
|
|
+ ringer: {{ $pro->is_ring_on ? 'true' : 'false' }},
|
|
|
+
|
|
|
+ // agora <-> WS sync
|
|
|
+ unrenderedParticipants: [], // exists in otherParticipants, but not yet in DOM
|
|
|
+ unresolvedParticipants: [], // does not exist in otherParticipants, but came in via Agora
|
|
|
+ unrenderedParticipantsTimer: false,
|
|
|
+ unresolvedParticipantsTimer: false,
|
|
|
+
|
|
|
+ // main-view & thumb views
|
|
|
+ mainViewParticipant: {
|
|
|
+ self: true,
|
|
|
+ uid: '',
|
|
|
+ type: 'PRO',
|
|
|
+ name: 'You',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+
|
|
|
+ toggleRinger: function () {
|
|
|
+ let self = this, endPoint = this.ringer ? 'turnOffRing' : 'turnOnRing';
|
|
|
+ $.post('/api/pro/' + endPoint, function (_data) {
|
|
|
+ if (_data && _data.success) {
|
|
|
+ self.ringer = !self.ringer;
|
|
|
+ } else {
|
|
|
+ if (_data.message) {
|
|
|
+ toastr.error(_data.message);
|
|
|
+ } else {
|
|
|
+ toastr.error('Unable to change ringer status');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, 'json');
|
|
|
+ },
|
|
|
+ activateParty: function (_stream = 'self') {
|
|
|
+ var current = $('.full-view');
|
|
|
+ if (current.attr('data-stream') === _stream) return;
|
|
|
+ current.removeClass('full-view').addClass('thumb-view');
|
|
|
+ if (current.attr('data-type') === 'CLIENT') {
|
|
|
+ current.prependTo('.thumbs');
|
|
|
+ } else {
|
|
|
+ current.appendTo('.thumbs');
|
|
|
+ }
|
|
|
+ if (_stream === 'self') {
|
|
|
+ $('#self-view')
|
|
|
+ .removeClass('thumb-view')
|
|
|
+ .removeClass('disconnected-view')
|
|
|
+ .addClass('full-view')
|
|
|
+ .prependTo('.main-view');
|
|
|
+ } else {
|
|
|
+ $('div[data-stream="' + _stream + '"]')
|
|
|
+ .removeClass('thumb-view')
|
|
|
+ .removeClass('disconnected-view')
|
|
|
+ .addClass('full-view')
|
|
|
+ .prependTo('.main-view');
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ enterClientRoomAsPro: function() {
|
|
|
+ @if($client)
|
|
|
+ $.post('/api/meeting/enterClientRoomAsPro', {clientUid: '{{ $client->uid }}'}, (_data) => {
|
|
|
+ // TODO: check success
|
|
|
+ console.log(_data);
|
|
|
+ this.getMeetingInfo(true);
|
|
|
+ });
|
|
|
+ @endif
|
|
|
+ },
|
|
|
+ getMeetingInfo: function(_firstRun = false) {
|
|
|
+ $.post('/api/meeting/getMyMeeting', (_data) => {
|
|
|
+ if(_data && _data.success) {
|
|
|
+ let state = _data.data;
|
|
|
+ console.log(state);
|
|
|
+
|
|
|
+ // overwrite model data
|
|
|
+ this.amIInAMeeting = state.amIInAMeeting;
|
|
|
+ this.inMeetingForClientUid = state.inMeetingForClientUid;
|
|
|
+ this.inMeetingForClient.clientMediaServiceRoomIdentifier =
|
|
|
+ state.inMeetingForClient.clientMediaServiceRoomIdentifier;
|
|
|
+ this.inMeetingForClient.uid = state.inMeetingForClient.uid;
|
|
|
+ this.inMeetingForClient.displayName = state.inMeetingForClient.displayName;
|
|
|
+ this.inMeetingForClient.dob = state.inMeetingForClient.dob;
|
|
|
+ this.meetingType = state.meetingType;
|
|
|
+ // NOTE: this now comes from its own end-point (see below)
|
|
|
+ // this.myMediaServiceToken = state.myMediaServiceToken;
|
|
|
+ this.myMediaServiceIdentifier = +state.myMediaServiceIdentifier;
|
|
|
+ this.otherParticipants = state.otherParticipants;
|
|
|
+
|
|
|
+ if(_firstRun) {
|
|
|
+ this.mainViewParticipant.uid = +state.myMediaServiceIdentifier;
|
|
|
+ $.post('/api/meeting/refreshMyMediaServiceToken', (_data) => { // get new agora token
|
|
|
+ if(!this.hasError(_data)) {
|
|
|
+ this.myMediaServiceToken = _data.data;
|
|
|
+ this.channel = this.inMeetingForClient.clientMediaServiceRoomIdentifier;
|
|
|
+ this.registerSocket();
|
|
|
+ }
|
|
|
+ }, 'json');
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(this.$data);
|
|
|
+ }
|
|
|
+ }, 'json');
|
|
|
+ },
|
|
|
+ registerSocket: function(_done) {
|
|
|
+ let socket = new SockJS(this.backendWsURL);
|
|
|
+ this.socketClient = Stomp.over(socket);
|
|
|
+ this.socketClient.connect({}, (frame) => {
|
|
|
+ console.log('Connected: ' + frame);
|
|
|
+ this.initSocketListeners(); // init listeners
|
|
|
+ this.socketClient.send("/app/register", {}, // register self
|
|
|
+ JSON.stringify({
|
|
|
+ sessionKey: '{{$performer->session_key}}'
|
|
|
+ })
|
|
|
+ );
|
|
|
+ this.initMediaService();
|
|
|
+ });
|
|
|
+ },
|
|
|
+ initSocketListeners: function() {
|
|
|
+
|
|
|
+ function _isSelf(_eventData) {
|
|
|
+ return _eventData.performer === '{{ $session->uid }}';
|
|
|
+ }
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/newParticipant", (message) => {
|
|
|
+ console.log("newParticipant received:", message.body);
|
|
|
+ if(message && message.body) {
|
|
|
+ let eventData = JSON.parse(message.body);
|
|
|
+ if(!_isSelf(eventData) && eventData.data) {
|
|
|
+ let existing = this.otherParticipants.filter(_participant => {
|
|
|
+ return _participant.uid === eventData.performer;
|
|
|
+ });
|
|
|
+ if(!existing.length) this.otherParticipants.push(eventData.data);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/myMicrophoneIsAcquired", (message) => {
|
|
|
+ console.log("myMicrophoneIsAcquired received:", message.body);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/myMicrophoneIsNotAcquired", (message) => {
|
|
|
+ console.log("myMicrophoneIsNotAcquired received:", message.body);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/myMicrophoneIsOn", (message) => {
|
|
|
+ console.log("myMicrophoneIsOn received:", message.body);
|
|
|
+ if(message && message.body) {
|
|
|
+ let eventData = JSON.parse(message.body);
|
|
|
+ if(!_isSelf(eventData) && eventData.data) {
|
|
|
+ for (let i = 0; i < this.otherParticipants.length; i++) {
|
|
|
+ if(this.otherParticipants[i].uid === eventData.performer) {
|
|
|
+ this.otherParticipants[i].media.isMicrophoneOn = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/myMicrophoneIsOff", (message) => {
|
|
|
+ console.log("ALIX myMicrophoneIsOff received:", message.body);
|
|
|
+ if(message && message.body) {
|
|
|
+ let eventData = JSON.parse(message.body);
|
|
|
+ if(!_isSelf(eventData) && eventData.data) {
|
|
|
+ for (let i = 0; i < this.otherParticipants.length; i++) {
|
|
|
+ if(this.otherParticipants[i].uid === eventData.performer) {
|
|
|
+ this.otherParticipants[i].media.isMicrophoneOn = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/myCameraIsAcquired", (message) => {
|
|
|
+ console.log("myCameraIsAcquired received:", message.body);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/myCameraIsNotAcquired", (message) => {
|
|
|
+ console.log("myCameraIsNotAcquired received:", message.body);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/myCameraIsOn", (message) => {
|
|
|
+ console.log("myCameraIsOn received:", message.body);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/myCameraIsOff", (message) => {
|
|
|
+ console.log("myCameraIsOff received:", message.body);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/editMyName", (message) => {
|
|
|
+ console.log("editMyName received:", message.body);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/setMyAwayMessage", (message) => {
|
|
|
+ console.log("setMyAwayMessage received:", message.body);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/removeMyAwayMessage", (message) => {
|
|
|
+ console.log("removeMyAwayMessage received:", message.body);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/leaveClientRoom", (message) => {
|
|
|
+ console.log("leaveClientRoom received:", message.body);
|
|
|
+ if(message && message.body) {
|
|
|
+ let eventData = JSON.parse(message.body);
|
|
|
+ if(!_isSelf(eventData) && eventData.data) {
|
|
|
+
|
|
|
+ // if the participant who left is in center view, switch center view to self
|
|
|
+ for (let i = 0; i < this.otherParticipants.length; i++) {
|
|
|
+ if (this.otherParticipants[i].uid === eventData.performer) {
|
|
|
+ if(this.mainViewParticipant.uid === (+this.otherParticipants[i].mediaServiceIdentifier)) {
|
|
|
+ this.showInCenterView(true, this.myMediaServiceIdentifier, 'You', 'PRO');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.otherParticipants = this.otherParticipants.filter(_participant => {
|
|
|
+ return _participant.uid !== eventData.performer;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ },
|
|
|
+ initMediaService: function() {
|
|
|
+
|
|
|
+ this.mediaServiceClient = AgoraRTC.createClient({mode:'rtc', codec:'h264'})
|
|
|
+
|
|
|
+ async function _acquireMicrophone() {
|
|
|
+ this.myMedia.isMicrophoneAcquired = false;
|
|
|
+ this.myMedia.isMicrophoneOn = false;
|
|
|
+ try {
|
|
|
+ this.myMicrophone = await AgoraRTC.createMicrophoneAudioTrack();
|
|
|
+ this.myMedia.isMicrophoneAcquired = true;
|
|
|
+ }
|
|
|
+ catch (e) {
|
|
|
+ console.log('ALIX: error in getting mic');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ async function _acquireCamera() {
|
|
|
+ this.myMedia.isCameraAcquired = false;
|
|
|
+ this.myMedia.isCameraOn = false;
|
|
|
+ try {
|
|
|
+ @if(config('app.agora_mode') === 'screen') // testing
|
|
|
+ this.myCamera = await AgoraRTC.createScreenVideoTrack();
|
|
|
+ @else
|
|
|
+ this.myCamera = await AgoraRTC.createCameraVideoTrack();
|
|
|
+ @endif
|
|
|
+ this.myMedia.isCameraAcquired = true;
|
|
|
+ }
|
|
|
+ catch (e) {
|
|
|
+ console.log('ALIX: error in getting camera');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ async function _initMediaServiceEvents() {
|
|
|
+ this.mediaServiceClient.on('user-joined', user => {
|
|
|
+ // do nothing, newParticipant logic handled via WS
|
|
|
+ });
|
|
|
+ this.mediaServiceClient.on('user-left', user => {
|
|
|
+ // do nothing, leaveClientRoom logic handled via WS
|
|
|
+ });
|
|
|
+ this.mediaServiceClient.on('user-published', async (user, mediaType) => {
|
|
|
+ await this.mediaServiceClient.subscribe(user, mediaType)
|
|
|
+ this.attemptToPlayParticipantMedia(user, mediaType);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ async function _initMediaService() {
|
|
|
+
|
|
|
+ await _acquireMicrophone.call(this); // get mic
|
|
|
+ await _acquireCamera.call(this); // get cam (or screen for testing)
|
|
|
+
|
|
|
+ if (!this.myMicrophone && !this.myCamera) {
|
|
|
+ alert('Do you have camera/mic? Unable to hear or see you.');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ await _initMediaServiceEvents.call(this);
|
|
|
+
|
|
|
+ // Show own feed
|
|
|
+ if(this.myCamera && this.myMedia.isCameraAcquired) {
|
|
|
+ this.myCamera.play($('#self-view')[0], {fit: 'contain'});
|
|
|
+ }
|
|
|
+
|
|
|
+ // init unrenderedParticipantsTimer and unresolvedParticipantsTimer
|
|
|
+ this.initUnrenderedParticipantsTimer();
|
|
|
+ this.initUnresolvedParticipantsTimer();
|
|
|
+
|
|
|
+ await this.mediaServiceClient.join( // join agora channel
|
|
|
+ this.appId,
|
|
|
+ this.channel,
|
|
|
+ this.myMediaServiceToken,
|
|
|
+ +this.myMediaServiceIdentifier
|
|
|
+ );
|
|
|
+ await this.mediaServiceClient.publish( // publish audio/video
|
|
|
+ [this.myMicrophone, this.myCamera].filter(Boolean)
|
|
|
+ );
|
|
|
+
|
|
|
+ // notify others that I have arrived
|
|
|
+ if(this.myCamera && this.myMedia.isCameraAcquired) {
|
|
|
+ this.myCameraIsOn();
|
|
|
+ }
|
|
|
+ if(this.myMicrophone && this.myMedia.isMicrophoneAcquired) {
|
|
|
+ this.myMicrophoneIsOn();
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ _initMediaService.call(this);
|
|
|
+ },
|
|
|
+
|
|
|
+ // attemptToPlayParticipantMedia
|
|
|
+ // if user already in otherParticipants
|
|
|
+ // if user's thumb already rendered
|
|
|
+ // if yes, check participant's isCameraOn is true
|
|
|
+ // if yes, play participant's video in his thumb
|
|
|
+ // else store "user" in unrenderedParticipants
|
|
|
+ // and keep retrying after 500mx (max 2 times) - i.e. give vue a cycle or 2 to render thumb
|
|
|
+ // else store "user" in unresolvedParticipants
|
|
|
+ // and keep retrying after 500ms (max 10 times) till resolved - i.e. give WS time to receive the newParticipant event
|
|
|
+ attemptToPlayParticipantMedia: function(user, mediaType) {
|
|
|
+ let participant = this.otherParticipants.filter(function(_participant) {
|
|
|
+ return (+_participant.mediaServiceIdentifier) === user.uid;
|
|
|
+ });
|
|
|
+ if(participant && participant.length) {
|
|
|
+ participant = participant[0];
|
|
|
+ if($('[data-uid="' + participant.mediaServiceIdentifier + '"]').length) {
|
|
|
+ if(mediaType === 'audio' && participant.media.isMicrophoneOn) {
|
|
|
+ user.audioTrack.play();
|
|
|
+ }
|
|
|
+ else if(mediaType === 'video' && participant.media.isCameraOn) {
|
|
|
+ user.videoTrack.play($('[data-uid="' + user.uid + '"]')[0], {fit: 'contain'});
|
|
|
+ }
|
|
|
+ this.markUserAsRendered(user);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ console.warn('Thumb not yet in DOM for participant!', user.uid);
|
|
|
+ this.markUserAsUnrendered(user, mediaType);
|
|
|
+ }
|
|
|
+ this.markUserAsResolved(user);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ console.warn('Participant not found in otherParticipants!', user.uid);
|
|
|
+ this.markUserAsUnresolved(user, mediaType);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // start: agora <-> WS sync helpers
|
|
|
+ initUnrenderedParticipantsTimer: function() {
|
|
|
+ this.unrenderedParticipantsTimer = window.setInterval(() => {
|
|
|
+ this.unrenderedParticipants.forEach((_user) => {
|
|
|
+ this.attemptToPlayParticipantMedia(_user, _user.mediaType);
|
|
|
+ });
|
|
|
+ }, 500);
|
|
|
+ },
|
|
|
+ initUnresolvedParticipantsTimer: function() {
|
|
|
+ this.unresolvedParticipantsTimer = window.setInterval(() => {
|
|
|
+ this.unresolvedParticipants.forEach((_user) => {
|
|
|
+ this.attemptToPlayParticipantMedia(_user, _user.mediaType);
|
|
|
+ });
|
|
|
+ }, 1000);
|
|
|
+ },
|
|
|
+ markUserAsUnrendered: function(_user, _mediaType) {
|
|
|
+ let existing = !!this.unrenderedParticipants.filter((_item) => _item.uid === _user.uid).length;
|
|
|
+ if(!existing) {
|
|
|
+ _user.mediaType = _mediaType;
|
|
|
+ this.unrenderedParticipants.push(_user);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ markUserAsRendered: function(_user) {
|
|
|
+ this.unrenderedParticipants = this.unrenderedParticipants.filter((_item) => _item.uid !== _user.uid);
|
|
|
+ },
|
|
|
+ markUserAsUnresolved: function(_user, _mediaType) {
|
|
|
+ let existing = !!this.unresolvedParticipants.filter((_item) => _item.uid === _user.uid).length;
|
|
|
+ if(!existing) {
|
|
|
+ _user.mediaType = _mediaType;
|
|
|
+ this.unresolvedParticipants.push(_user);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ markUserAsResolved: function(_user) {
|
|
|
+ this.unresolvedParticipants = this.unresolvedParticipants.filter((_item) => _item.uid !== _user.uid);
|
|
|
+ },
|
|
|
+ // end: agora <-> WS sync helpers
|
|
|
+
|
|
|
+ // start: actions that notify participants via socket
|
|
|
+ myMicrophoneIsAcquired: function () {
|
|
|
+ this.socketClient.send("/app/myMicrophoneIsAcquired", {},
|
|
|
+ JSON.stringify({sessionKey: '{{$performer->session_key}}'})
|
|
|
+ );
|
|
|
+ },
|
|
|
+ myMicrophoneIsNotAcquired: function () {
|
|
|
+ this.socketClient.send("/app/myMicrophoneIsNotAcquired", {},
|
|
|
+ JSON.stringify({sessionKey: '{{$performer->session_key}}'})
|
|
|
+ );
|
|
|
+ },
|
|
|
+ myMicrophoneIsOn: function () {
|
|
|
+ if(!this.myMedia.isMicrophoneAcquired) return;
|
|
|
+ this.myMicrophone.setEnabled(true);
|
|
|
+ this.myMedia.isMicrophoneOn = true;
|
|
|
+ this.socketClient.send("/app/myMicrophoneIsOn", {},
|
|
|
+ JSON.stringify({sessionKey: '{{$performer->session_key}}'})
|
|
|
+ );
|
|
|
+ },
|
|
|
+ myMicrophoneIsOff: function () {
|
|
|
+ if(!this.myMedia.isMicrophoneAcquired) return;
|
|
|
+ this.myMicrophone.setEnabled(false);
|
|
|
+ this.myMedia.isMicrophoneOn = false;
|
|
|
+ this.socketClient.send("/app/myMicrophoneIsOff", {},
|
|
|
+ JSON.stringify({sessionKey: '{{$performer->session_key}}'})
|
|
|
+ );
|
|
|
+ },
|
|
|
+ myCameraIsAcquired: function () {
|
|
|
+ this.socketClient.send("/app/myCameraIsAcquired", {},
|
|
|
+ JSON.stringify({sessionKey: '{{$performer->session_key}}'})
|
|
|
+ );
|
|
|
+ },
|
|
|
+ myCameraIsNotAcquired: function () {
|
|
|
+ this.socketClient.send("/app/myCameraIsNotAcquired", {},
|
|
|
+ JSON.stringify({sessionKey: '{{$performer->session_key}}'})
|
|
|
+ );
|
|
|
+ },
|
|
|
+ myCameraIsOn: function () {
|
|
|
+ if(!this.myMedia.isCameraAcquired) return;
|
|
|
+ this.myCamera.setEnabled(true);
|
|
|
+ this.myMedia.isCameraOn = true;
|
|
|
+ this.socketClient.send("/app/myCameraIsOn", {},
|
|
|
+ JSON.stringify({sessionKey: '{{$performer->session_key}}'})
|
|
|
+ );
|
|
|
+ },
|
|
|
+ myCameraIsOff: function () {
|
|
|
+ if(!this.myMedia.isCameraAcquired) return;
|
|
|
+ this.myCamera.setEnabled(false);
|
|
|
+ this.myMedia.isCameraOn = false;
|
|
|
+ this.socketClient.send("/app/myCameraIsOff", {},
|
|
|
+ JSON.stringify({sessionKey: '{{$performer->session_key}}'})
|
|
|
+ );
|
|
|
+ },
|
|
|
+ editMyName: function (_myNewName) {
|
|
|
+ this.socketClient.send("/app/editMyName", {},
|
|
|
+ JSON.stringify({
|
|
|
+ sessionKey: '{{$performer->session_key}}',
|
|
|
+ myNewName: _myNewName
|
|
|
+ })
|
|
|
+ );
|
|
|
+ },
|
|
|
+ setMyAwayMessage: function (_message) {
|
|
|
+ this.socketClient.send("/app/setMyAwayMessage", {},
|
|
|
+ JSON.stringify({
|
|
|
+ sessionKey: '{{$performer->session_key}}',
|
|
|
+ message: _message
|
|
|
+ })
|
|
|
+ );
|
|
|
+ },
|
|
|
+ removeMyAwayMessage: function () {
|
|
|
+ this.socketClient.send("/app/removeMyAwayMessage", {},
|
|
|
+ JSON.stringify({sessionKey: '{{$performer->session_key}}'})
|
|
|
+ );
|
|
|
+ },
|
|
|
+ leaveClientRoom: function () {
|
|
|
+ this.socketClient.send("/app/leaveClientRoom", {},
|
|
|
+ JSON.stringify({sessionKey: '{{$performer->session_key}}'})
|
|
|
+ );
|
|
|
+ window.setTimeout(() => { // a little timeout for the WS message sending op to complete
|
|
|
+ window.location.href = '/pro/meet';
|
|
|
+ }, 250);
|
|
|
+ },
|
|
|
+ // end: actions that notify participants via socket
|
|
|
+
|
|
|
+ // start: main view / thumb views
|
|
|
+ showInCenterView: function(_self, _uid, _name, _type) {
|
|
|
+ this.mainViewParticipant = {
|
|
|
+ self: _self,
|
|
|
+ uid: +_uid,
|
|
|
+ type: _type,
|
|
|
+ name: _name,
|
|
|
+ };
|
|
|
+ Vue.nextTick(() => {
|
|
|
+ this.refreshVideos();
|
|
|
+ });
|
|
|
+ },
|
|
|
+ refreshVideos: function() {
|
|
|
+ // play self (only video)
|
|
|
+ // no need to check camera/mic acquired/on etc. as only published tracks will appear here
|
|
|
+ for(let track in this.mediaServiceClient.localTracks) {
|
|
|
+ if(this.mediaServiceClient.localTracks.hasOwnProperty(track)) {
|
|
|
+ track = this.mediaServiceClient.localTracks[track];
|
|
|
+ if(track.trackMediaType === 'video') {
|
|
|
+ track.play($('[data-uid="' + this.myMediaServiceIdentifier + '"]')[0], {fit: 'contain'});
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // play others
|
|
|
+ for(let remoteParticipant in this.mediaServiceClient.remoteUsers) {
|
|
|
+ if(this.mediaServiceClient.remoteUsers.hasOwnProperty(remoteParticipant)) {
|
|
|
+ remoteParticipant = this.mediaServiceClient.remoteUsers[remoteParticipant];
|
|
|
+ if(remoteParticipant.hasAudio && remoteParticipant.audioTrack) {
|
|
|
+ remoteParticipant.audioTrack.play();
|
|
|
+ }
|
|
|
+ if(remoteParticipant.hasVideo && remoteParticipant.videoTrack) {
|
|
|
+ remoteParticipant.videoTrack.play($('[data-uid="' + remoteParticipant.uid + '"]')[0], {fit: 'contain'});
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // end: main view / thumb views
|
|
|
+
|
|
|
+ // start: utils
|
|
|
+ hasError: function(_data) { // check and report error if exists via toastr
|
|
|
+ let msg = 'Unknown error!';
|
|
|
+ if(_data) {
|
|
|
+ if(_data.success) return false;
|
|
|
+ else if(_data.message) msg = _data.message;
|
|
|
+ }
|
|
|
+ toastr.error(msg);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ // end: utils
|
|
|
+ },
|
|
|
+ mounted: function () {
|
|
|
+ this.enterClientRoomAsPro();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ })();
|
|
|
+ </script>
|
|
|
+ <script>
|
|
|
+ (function() {
|
|
|
+ window.queueComponent = new Vue({
|
|
|
+ el: '#queueComponent',
|
|
|
+ data: {
|
|
|
+ items: []
|
|
|
+ },
|
|
|
+ mounted: function() {
|
|
|
+ let self = this;
|
|
|
+ this.refresh();
|
|
|
+ window.setInterval(function() {
|
|
|
+ self.refresh();
|
|
|
+ }, 15000); // once in 15 seconds
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ refresh: function() {
|
|
|
+ let self = this;
|
|
|
+ $.get('/patients-in-queue', function(_data) {
|
|
|
+ self.items = _data;
|
|
|
+ }, 'json');
|
|
|
+ },
|
|
|
+ claim: function(_uid) {
|
|
|
+ $.post('/api/mcpRequest/claim', {clientUid: _uid}, function(_data) {
|
|
|
+ if(_data && _data.success) {
|
|
|
+ // open patient in LHS
|
|
|
+ window.top.openInLHS('/patients/view/' + _uid);
|
|
|
+ // open patient video in RHS
|
|
|
+ window.top.openInRHS('/pro/meet/' + _uid);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ if (_data.message) {
|
|
|
+ window.top.toastr.error(_data.message);
|
|
|
+ } else {
|
|
|
+ window.top.toastr.error('Unable to claim the patient');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, 'json');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ })();
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+</html>
|