|
@@ -0,0 +1,803 @@
|
|
|
+<!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>
|
|
|
+ <link rel="stylesheet" href="{{ asset('/css/toastr.min.css') }}">
|
|
|
+ <script src="{{ asset('js/toastr.min.js') }}" type="application/javascript"></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">{{ 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 v-if="appMode === 'name'" class="d-flex">
|
|
|
+ <button v-on:click.prevent="enterClientRoomAsPro()"
|
|
|
+ class="btn btn-primary px-4 font-weight-bold mx-auto my-3">Start Video</button>
|
|
|
+ </div>
|
|
|
+ <div v-show="appMode === 'video'">
|
|
|
+ <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 class="user-type-indicator">
|
|
|
+ <i v-if="mainViewParticipant.type === 'CLIENT_GUEST'" class="fa fa-user text-white"></i>
|
|
|
+ <i v-if="mainViewParticipant.type === 'PRO'" class="fa fa-stethoscope text-white"></i>
|
|
|
+ </div>
|
|
|
+ <div class="media-status-indicator">
|
|
|
+ <i v-show="!getMediaByMediaServiceId(mainViewParticipant.uid).isCameraOn"
|
|
|
+ class="fa fa-video-slash muted ml-1"
|
|
|
+ :class="!getMediaByMediaServiceId(mainViewParticipant.uid).isCameraAcquired ? 'text-danger' : 'text-white'"></i>
|
|
|
+ <i v-show="!getMediaByMediaServiceId(mainViewParticipant.uid).isMicrophoneOn"
|
|
|
+ class="fa fa-microphone-slash muted ml-1"
|
|
|
+ :class="!getMediaByMediaServiceId(mainViewParticipant.uid).isMicrophoneAcquired ? 'text-danger' : 'text-white'"></i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="thumbs">
|
|
|
+ <div v-if="mainViewParticipant.uid !== myMediaServiceIdentifier"
|
|
|
+ :id="'remote-view-' + myMediaServiceIdentifier"
|
|
|
+ :data-self="true"
|
|
|
+ :data-uid="myMediaServiceIdentifier"
|
|
|
+ :data-name="'You (' + myName + ')'"
|
|
|
+ :data-type="'PRO'" {{-- TODO: change in FE4 --}}
|
|
|
+ :data-audio="myMedia && myMedia.isMicrophoneOn ? 'on' : 'off'"
|
|
|
+ v-on:click.prevent="showInCenterView(true, myMediaServiceIdentifier, 'You (' + myName + ')', 'PRO')"
|
|
|
+ class="remote-view thumb-view c-pointer">
|
|
|
+ <div class="user-type-indicator">
|
|
|
+ <i class="fa fa-stethoscope text-white"></i>
|
|
|
+ </div>
|
|
|
+ <div class="media-status-indicator">
|
|
|
+ <i v-show="!myMedia || !myMedia.isCameraOn"
|
|
|
+ class="fa fa-video-slash muted ml-1"
|
|
|
+ :class="!myMedia || !myMedia.isCameraAcquired ? 'text-danger' : 'text-white'"></i>
|
|
|
+ <i v-show="!myMedia || !myMedia.isMicrophoneOn"
|
|
|
+ class="fa fa-microphone-slash muted ml-1"
|
|
|
+ :class="!myMedia || !myMedia.isMicrophoneAcquired ? 'text-danger' : 'text-white'"></i>
|
|
|
+ </div>
|
|
|
+ </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">
|
|
|
+ <div class="user-type-indicator">
|
|
|
+ <i v-if="participant.participantType === 'CLIENT_GUEST'" class="fa fa-user text-white"></i>
|
|
|
+ <i v-if="participant.participantType === 'PRO'" class="fa fa-stethoscope text-white"></i>
|
|
|
+ </div>
|
|
|
+ <div class="media-status-indicator">
|
|
|
+ <i v-show="!participant.media || !participant.media.isCameraOn"
|
|
|
+ class="fa fa-video-slash muted ml-1"
|
|
|
+ :class="!participant.media || !participant.media.isCameraAcquired ? 'text-danger' : 'text-white'"></i>
|
|
|
+ <i v-show="!participant.media || !participant.media.isMicrophoneOn"
|
|
|
+ class="fa fa-microphone-slash muted ml-1"
|
|
|
+ :class="!participant.media || !participant.media.isMicrophoneAcquired ? 'text-danger' : 'text-white'"></i>
|
|
|
+ </div>
|
|
|
+ </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>
|
|
|
+ <script>
|
|
|
+ (function () {
|
|
|
+ window.proCallComponent = new Vue({
|
|
|
+ el: '#proCallComponent',
|
|
|
+ delimiters: ['@{{', '}}'],
|
|
|
+ data: {
|
|
|
+
|
|
|
+ // main 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: '',
|
|
|
+ myName: '{{ $performer->pro->displayName() }}',
|
|
|
+
|
|
|
+ // agora
|
|
|
+ mediaServiceClient: null, // set on agora init
|
|
|
+ appId: '{{ config('app.agora_appid') }}',
|
|
|
+ channel: '', // set on mount
|
|
|
+ myMicrophone: null,
|
|
|
+ myCamera: null,
|
|
|
+ appMode: 'name', // Agora needs a user-gesture to init correctly
|
|
|
+
|
|
|
+ // 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 participant
|
|
|
+ mainViewParticipant: {
|
|
|
+ self: true,
|
|
|
+ uid: '',
|
|
|
+ type: 'PRO',
|
|
|
+ name: 'You ({{ $performer->pro->displayName() }})',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+
|
|
|
+ // start: main flow
|
|
|
+ enterClientRoomAsPro: function() {
|
|
|
+ @if($client)
|
|
|
+ this.socketClient.send("/app/leaveClientRoom", {},
|
|
|
+ JSON.stringify({sessionKey: '{{$performer->session_key}}'})
|
|
|
+ );
|
|
|
+ window.setTimeout(() => {
|
|
|
+ $.post('/api/meeting/enterClientRoomAsPro', {clientUid: '{{ $client->uid }}'}, (_data) => {
|
|
|
+ if(!this.hasError(_data)) {
|
|
|
+ this.appMode = 'video';
|
|
|
+ this.getMeetingInfo(true);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }, 250);
|
|
|
+ @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.initMediaService();
|
|
|
+ }
|
|
|
+ }, '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}}'
|
|
|
+ })
|
|
|
+ );
|
|
|
+ window.setInterval(() => {
|
|
|
+ this.socketClient.send("/app/heartbeat", {},
|
|
|
+ JSON.stringify({sessionKey: '{{ request()->cookie('sessionKey') }}'})
|
|
|
+ );
|
|
|
+ }, 5000);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ initSocketListeners: function() {
|
|
|
+
|
|
|
+ function _isSelf(_eventData) {
|
|
|
+ return _eventData.performer === '{{ $session->uid }}';
|
|
|
+ }
|
|
|
+
|
|
|
+ function _setParticipantProperty(_message, _propertyName, _valueKeyName) {
|
|
|
+ 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][_propertyName] = eventData.data[_valueKeyName];
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function _setParticipantMediaProperty(_message, _propertyName, _value) {
|
|
|
+ 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[_propertyName] = _value;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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);
|
|
|
+ Vue.nextTick(() => {
|
|
|
+ this.refreshVideos();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/myMicrophoneIsAcquired", (message) => {
|
|
|
+ console.log("myMicrophoneIsAcquired received:", message.body);
|
|
|
+ _setParticipantMediaProperty.call(this, message, 'isMicrophoneAcquired', true);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/myMicrophoneIsNotAcquired", (message) => {
|
|
|
+ console.log("myMicrophoneIsNotAcquired received:", message.body);
|
|
|
+ _setParticipantMediaProperty.call(this, message, 'isMicrophoneAcquired', false);
|
|
|
+ _setParticipantMediaProperty.call(this, message, 'isMicrophoneOn', false);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/myMicrophoneIsOn", (message) => {
|
|
|
+ console.log("myMicrophoneIsOn received:", message.body);
|
|
|
+ _setParticipantMediaProperty.call(this, message, 'isMicrophoneOn', true);
|
|
|
+ _setParticipantMediaProperty.call(this, message, 'isMicrophoneAcquired', true);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/myMicrophoneIsOff", (message) => {
|
|
|
+ console.log("ALIX myMicrophoneIsOff received:", message.body);
|
|
|
+ _setParticipantMediaProperty.call(this, message, 'isMicrophoneOn', false);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/myCameraIsAcquired", (message) => {
|
|
|
+ console.log("myCameraIsAcquired received:", message.body);
|
|
|
+ _setParticipantMediaProperty.call(this, message, 'isCameraAcquired', true);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/myCameraIsNotAcquired", (message) => {
|
|
|
+ console.log("myCameraIsNotAcquired received:", message.body);
|
|
|
+ _setParticipantMediaProperty.call(this, message, 'isCameraAcquired', false);
|
|
|
+ _setParticipantMediaProperty.call(this, message, 'isCameraOn', false);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/myCameraIsOn", (message) => {
|
|
|
+ console.log("myCameraIsOn received:", message.body);
|
|
|
+ _setParticipantMediaProperty.call(this, message, 'isCameraOn', true);
|
|
|
+ _setParticipantMediaProperty.call(this, message, 'isCameraAcquired', true);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/myCameraIsOff", (message) => {
|
|
|
+ console.log("myCameraIsOff received:", message.body);
|
|
|
+ _setParticipantMediaProperty.call(this, message, 'isCameraOn', false);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketClient.subscribe("/user/topic/editMyName", (message) => {
|
|
|
+ console.log("editMyName received:", message.body);
|
|
|
+ _setParticipantProperty.call(this, message, 'displayName', 'myName');
|
|
|
+ });
|
|
|
+
|
|
|
+ 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 (' + this.myName + ')', 'PRO');
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.otherParticipants = this.otherParticipants.filter(_participant => {
|
|
|
+ return _participant.uid !== eventData.performer;
|
|
|
+ });
|
|
|
+
|
|
|
+ Vue.nextTick(() => {
|
|
|
+ this.refreshVideos();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ },
|
|
|
+ initMediaService: function() {
|
|
|
+
|
|
|
+ this.mediaServiceClient = AgoraRTC.createClient({mode:'rtc', codec:'vp8'});
|
|
|
+
|
|
|
+ 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({
|
|
|
+ optimizationMode: "motion"
|
|
|
+ });
|
|
|
+ @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) => {
|
|
|
+ console.log('ALIX user-published', user);
|
|
|
+ await this.mediaServiceClient.subscribe(user, mediaType)
|
|
|
+ this.attemptToPlayParticipantMedia(user, mediaType);
|
|
|
+ window.setTimeout(() => {
|
|
|
+ this.refreshVideos();
|
|
|
+ }, 250);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ 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; // allow to proceed without any device!
|
|
|
+ }
|
|
|
+
|
|
|
+ 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
|
|
|
+ );
|
|
|
+
|
|
|
+ if(this.myMicrophone || this.myCamera) {
|
|
|
+ await this.mediaServiceClient.publish( // publish audio/video
|
|
|
+ [this.myMicrophone, this.myCamera].filter(Boolean)
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // notify others about my camera status
|
|
|
+ if(this.myCamera && this.myMedia.isCameraAcquired) {
|
|
|
+ this.myCameraIsAcquired();
|
|
|
+ this.myCameraIsOn();
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ this.myCameraIsNotAcquired();
|
|
|
+ this.myCameraIsOff();
|
|
|
+ }
|
|
|
+
|
|
|
+ // notify others about my microphone status
|
|
|
+ if(this.myMicrophone && this.myMedia.isMicrophoneAcquired) {
|
|
|
+ this.myMicrophoneIsAcquired();
|
|
|
+ this.myMicrophoneIsOn();
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ this.myMicrophoneIsNotAcquired();
|
|
|
+ this.myMicrophoneIsOff();
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ _initMediaService.call(this);
|
|
|
+ },
|
|
|
+ attemptToPlayParticipantMedia: function(user, mediaType) {
|
|
|
+ // LOGIC
|
|
|
+ // 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
|
|
|
+ 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' && user.hasAudio && user.audioTrack) {
|
|
|
+ participant.media.isMicrophoneAcquired = true;
|
|
|
+ participant.media.isMicrophoneOn = true;
|
|
|
+ user.audioTrack.play();
|
|
|
+ }
|
|
|
+ else if(mediaType === 'video' && user.hasVideo && user.videoTrack) {
|
|
|
+ participant.media.isCameraAcquired = true;
|
|
|
+ participant.media.isCameraOn = true;
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // end: main flow
|
|
|
+
|
|
|
+ // 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.myMicrophone && this.myMedia.isMicrophoneAcquired) {
|
|
|
+ this.myMedia.isMicrophoneOn = true;
|
|
|
+ this.myMicrophone.setEnabled(true);
|
|
|
+ this.socketClient.send("/app/myMicrophoneIsOn", {},
|
|
|
+ JSON.stringify({sessionKey: '{{$performer->session_key}}'})
|
|
|
+ );
|
|
|
+ }
|
|
|
+ },
|
|
|
+ myMicrophoneIsOff: function () {
|
|
|
+ if(this.myMicrophone) {
|
|
|
+ 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.myCamera && this.myMedia.isCameraAcquired) {
|
|
|
+ this.myCamera.setEnabled(true);
|
|
|
+ this.myMedia.isCameraOn = true;
|
|
|
+ this.socketClient.send("/app/myCameraIsOn", {},
|
|
|
+ JSON.stringify({sessionKey: '{{$performer->session_key}}'})
|
|
|
+ );
|
|
|
+ }
|
|
|
+ },
|
|
|
+ myCameraIsOff: function () {
|
|
|
+ if(this.myCamera) {
|
|
|
+ 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') {
|
|
|
+ let videoContainer = $('[data-uid="' + this.myMediaServiceIdentifier + '"]');
|
|
|
+ if(videoContainer.length) {
|
|
|
+ track.play(videoContainer[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) {
|
|
|
+ let videoContainer = $('[data-uid="' + remoteParticipant.uid + '"]');
|
|
|
+ if(videoContainer.length) {
|
|
|
+ remoteParticipant.videoTrack.play(videoContainer[0], {fit: 'contain'});
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // end: main view / thumb views
|
|
|
+
|
|
|
+ // start: other/misc
|
|
|
+ getMediaByMediaServiceId: function(_msid) {
|
|
|
+ if((+this.myMediaServiceIdentifier) === _msid) { // is it self?
|
|
|
+ return this.myMedia;
|
|
|
+ }
|
|
|
+ for (let i = 0; i < this.otherParticipants.length; i++) { // or a remote participant
|
|
|
+ if((+this.otherParticipants[i].mediaServiceIdentifier) === _msid) {
|
|
|
+ return this.otherParticipants[i].media;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return { // return falsy object if nothing found
|
|
|
+ isCameraAcquired: false,
|
|
|
+ isCameraOn: false,
|
|
|
+ isMicrophoneAcquired: false,
|
|
|
+ isMicrophoneOn: false,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ toggleRinger: function () {
|
|
|
+ $.post('/api/pro/' + (this.ringer ? 'turnOffRing' : 'turnOnRing'), (_data) => {
|
|
|
+ if(!this.hasError(_data)) {
|
|
|
+ this.ringer = !this.ringer;
|
|
|
+ }
|
|
|
+ }, 'json');
|
|
|
+ },
|
|
|
+ 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: other/misc
|
|
|
+ },
|
|
|
+ mounted: function () {
|
|
|
+ this.registerSocket();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ })();
|
|
|
+ </script>
|
|
|
+
|
|
|
+ @include('app/patient/partials/mcp-queue')
|
|
|
+
|
|
|
+</body>
|
|
|
+</html>
|