浏览代码

Agora V2 [wip #2]

Agora V2 [wip #3]

Agora V2 [wip #4]

Agora V2 [wip #5]

Agora V2 [wip #6]
Vijayakrishnan 4 年之前
父节点
当前提交
c7c7bce27e
共有 3 个文件被更改,包括 325 次插入287 次删除
  1. 22 2
      public/css/meeting.css
  2. 1 0
      public/css/style.css
  3. 302 285
      resources/views/app/video/call-agora-v2.blade.php

+ 22 - 2
public/css/meeting.css

@@ -151,13 +151,28 @@ h1 {
     left: 0;
     width: 100%;
     bottom: 0;
-    background: rgba(0, 0, 0, 0.5);
+    background: rgba(0, 0, 0, 0.75);
     color: #fff;
     font-size: 11px;
     white-space: nowrap;
     padding: 0 5px;
     overflow: hidden;
     text-overflow: ellipsis;
+    font-weight: bold;
+    text-align: center;
+}
+.main-view .thumbs .thumb-view>i.muted {
+    position: absolute;
+    z-index: 1;
+    right: 3px;
+    top: 3px;
+    font-size: 12px;
+    color: #fff;
+    background: #333;
+    width: 18px;
+    height: 18px;
+    text-align: center;
+    line-height: 18px;
 }
 .main-view .thumbs .disconnected-view {
     opacity: 0;
@@ -212,14 +227,19 @@ h1 {
 body .OT_fit-mode-cover .OT_video-element {
     object-fit: contain;
 }
-.hang-up {
+.call-actions {
     position: absolute;
     z-index: 2;
     bottom: 1rem;
     left: 1rem;
+    width: 200px;
+    height: 50px;
+}
+.call-actions>button {
     width: 50px;
     height: 50px;
     font-size: 120%;
+    margin-right: 1rem;
 }
 .call-mcp {
     position: absolute;

+ 1 - 0
public/css/style.css

@@ -853,6 +853,7 @@ body .node input[type="number"] {
     left: 0;
     width: 100%;
     bottom: 0;
+    z-index: 4;
 }
 .queue-item {
     width: 100px;

+ 302 - 285
resources/views/app/video/call-agora-v2.blade.php

@@ -24,57 +24,75 @@
 
     <div id="proCallComponent">
 
-        @if($client)
-        <div class="text-center py-2 border-bottom font-weight-normal mcp-theme-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>
-        </div>
-        @endif
-
-        <div class="py-2 d-flex align-items-center justify-content-center border-bottom">
-            <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 class="" v-show="false">
-            <div class="py-3 text-center" xv-if="started">
-                <h6 class="text-black font-weight-bold m-0">Call in progress: @{{ timeDisplay() }}</h6>
+        <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 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>
+                    <span class="text-secondary ml-4">{{ $pro->displayName() }}</span>
+                    <span class="text-secondary ml-4">{{ date('H:i:s') }}</span>
+                </a>
             </div>
-            <div class="py-3 text-center" v-if="!otherParticipants.length">
-                <h6 class="text-black font-weight-bold m-0">No other participants in the call.
-                    <a href="#" class="text-danger font-weight-bold" v-on:click.prevent="hangUp()">Hang up</a>
-                </h6>
+            @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-stream="self" data-name="You" data-type="PRO"></div>
+                <div id="self-view" class="full-view" :data-uid="myMediaServiceIdentifier" data-name="You" data-type="PRO"></div>
                 <div class="thumbs">
-
+                    <div v-for="participant in otherParticipants"
+                         :id="'remote-view-' + participant.uid"
+                         :data-uid="participant.mediaServiceIdentifier"
+                         :data-name="participant.displayName"
+                         :data-type="participant.participantType"
+                         :data-audio="participant.media.isMicrophoneOn ? 'on' : 'off'"
+                         class="remote-view thumb-view">
+                        <i v-show="!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>
-                <button class="btn btn-danger rounded-circle hang-up"
-                        xv-if="started"
-                        title="Leave Call"
-                        v-on:click.prevent="hangUp()">
-                    <i class="fa fa-phone"></i>
-                </button>
             </div>
         </div>
-
-        @if($client)
-        {{--<div class="" v-show="!videoActive">
-            <button class="btn btn-sm btn-primary font-weight-bold mx-auto mt-4 d-block"
-                    v-on:click.prevent="connect()">
-                Start video call with {{ $client->displayName() }}
-            </button>
-        </div>--}}
         @endif
 
     </div>
@@ -100,7 +118,7 @@
 
     <script>
         (function () {
-            new Vue({
+            window.proCallComponent = new Vue({
                 el: '#proCallComponent',
                 delimiters: ['@{{', '}}'],
                 data: {
@@ -144,15 +162,24 @@
                     awayMessage: '',
 
                     // agora
-                    agoraClient: null, // set on agora init
+                    mediaServiceClient: null, // set on agora init
                     appId: '{{ config('app.agora_appid') }}',
                     channel: '', // set on mount
-                    ringer: {{ $pro->is_ring_on ? 'true' : 'false' }},
+                    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,
                 },
                 methods: {
 
@@ -170,132 +197,6 @@
                             }
                         }, 'json');
                     },
-                    connect: function () {
-                        var self = this;
-                        self.selfName = '{{ $pro->name_display  }}';
-                        $.get('/api/agora/getClientToken', {
-                            clientUid: self.clientUid,
-                        }, function (_data) {
-                            console.log(_data);
-                            self.selfToken = _data.data;
-                            self.initAgora();
-                        });
-                    },
-                    timeDisplay: function () {
-                        var seconds = this.time / 1000,
-                            minutes = parseInt(seconds / 60, 10);
-                        seconds = parseInt(seconds % 60, 10);
-                        return minutes + " min, " + seconds + " sec";
-                    },
-                    hangUp: function () {
-                        var self = this;
-                        async function _leave() {
-                            if(self.agoraClient) {
-                                await self.agoraClient.leave();
-                                window.top.hideRHS();
-                                window.location.reload();
-                            }
-                        }
-                        _leave();
-                    },
-                    initAgora: function () {
-
-                        let self = this;
-
-                        async function _initAgora(){
-
-                            self.agoraClient = AgoraRTC.createClient({mode:'rtc', codec:'h264'})
-                            let camera, mic
-                            try { mic = await AgoraRTC.createMicrophoneAudioTrack() } catch {
-                                console.log('ALIX: error in getting mic');
-                            }
-                            try { camera = await AgoraRTC.createCameraVideoTrack() } catch {
-                                console.log('ALIX: error in getting camera');
-                            }
-
-                            // testing
-                            @if(config('app.agora_mode') === 'screen')
-                                try { camera = await AgoraRTC.createScreenVideoTrack() } catch { }
-                            @endif
-
-                            if (!mic && !camera){
-                                alert('Do you have camera/mic? Unable to hear or see you.')
-                                return
-                            }
-
-                            // Add myself to the page.
-                            if(camera) {
-                                camera.play($('#self-view')[0]);
-                            }
-
-                            // events
-                            self.agoraClient.on('user-joined', user => {
-
-                                // add a div for remove view
-                                $('[data-stream="' + user.uid + '"]').remove();
-                                var remoteViewID = 'remote-view-' + user.uid;
-                                var remoteElem = $('<div id="' + remoteViewID + '" class="remote-view thumb-view" data-stream="' + user.uid + '"></div>');
-                                remoteElem.appendTo('.thumbs');
-
-                                if (!self.startTime) {
-                                    self.startTime = new Date().getTime();
-                                    window.setInterval(function () {
-                                        self.time = new Date().getTime() - self.startTime;
-                                    }, 1000);
-                                    self.started = true;
-                                }
-                                self.activateParty(user.uid);
-                                self.noOneElseInCall = false;
-                                self.resolveParticipantNames();
-                            })
-                            self.agoraClient.on('user-left', user => {
-
-                                if ($('.full-view[data-stream="' + user.uid + '"]').length) {
-                                    var allThumbs = $('.thumbs [data-stream]:not([data-stream=""]):not(.disconnected-view):visible');
-                                    if (allThumbs.length) {
-                                        $('.thumbs [data-stream]:not([data-stream=""])').each(function () {
-                                            if ($(this).attr('data-stream') !== user.uid) {
-                                                self.activateParty($(this).attr('data-stream'));
-                                                return false;
-                                            }
-                                        });
-                                    } else {
-                                        self.noOneElseInCall = true;
-                                    }
-                                }
-
-                                $('[data-stream="' + user.uid + '"]').remove();
-
-                                // if no other parties in call, hang up
-                                if (!$('[data-stream]:not([data-stream="' + {{ $session->id }} + '"])').length) {
-                                    console.warn('No other parties in the call!');
-                                    self.startTime = 0;
-                                    self.started = false;
-                                    self.noOneElseInCall = true;
-                                }
-                            })
-                            self.agoraClient.on('user-published', async function(user, mediaType){
-                                await self.agoraClient.subscribe(user, mediaType)
-                                mediaType === 'audio'
-                                    ? user.audioTrack.play()
-                                    : user.videoTrack.play($('[data-stream="' + user.uid + '"]')[0]);
-                            })
-
-                            await self.agoraClient.join(self.appId, self.channel, self.selfToken, self.uid)
-                            await self.agoraClient.publish([mic, camera].filter(Boolean))
-
-                            // assume connected by this point, notify backend & show self video
-                            if (mic || camera) {
-                                self.joinMeetingAsPro(self.selfUserType);
-                                $('#self-view').attr('data-type', 'PRO').show();
-                                self.activateParty('self');
-                                self.videoActive = true;
-                            }
-                        }
-
-                        _initAgora();
-
-                    },
                     activateParty: function (_stream = 'self') {
                         var current = $('.full-view');
                         if (current.attr('data-stream') === _stream) return;
@@ -320,16 +221,17 @@
                         }
                     },
 
-                    enterClientRoomAsPro: function(_done) {
+                    enterClientRoomAsPro: function() {
                         @if($client)
+
                         $.post('/api/meeting/enterClientRoomAsPro', {clientUid: '{{ $client->uid }}'}, (_data) => {
                             // TODO: check success
                             console.log(_data);
-                            _done.call(this);
+                            this.getMeetingInfo(true);
                         });
                         @endif
                     },
-                    getMeetingInfo: function(_done) {
+                    getMeetingInfo: function(_firstRun = false) {
                         $.post('/api/meeting/getMyMeeting', (_data) => {
                             if(_data && _data.success) {
                                 let state = JSON.parse(_data.data);
@@ -348,14 +250,16 @@
                                 this.myMediaServiceIdentifier = state.myMediaServiceIdentifier;
                                 this.otherParticipants = state.otherParticipants;
 
-                                // agora stuff
-                                this.channel = this.inMeetingForClient.clientMediaServiceRoomIdentifier;
+                                if(_firstRun) {
+                                    this.channel = this.inMeetingForClient.clientMediaServiceRoomIdentifier;
+                                    this.registerSocket();
+                                }
 
-                                _done.call(this);
+                                console.log(this.$data);
                             }
                         });
                     },
-                    registerSocket: function() {
+                    registerSocket: function(_done) {
                         let socket = new SockJS(this.backendWsURL);
                         this.socketClient = Stomp.over(socket);
                         this.socketClient.connect({}, (frame) => {
@@ -366,123 +270,288 @@
                                     sessionKey: '{{$performer->session_key}}'
                                 })
                             );
+                            this.initMediaService();
                         });
                     },
                     initSocketListeners: function() {
 
-                        this.socketClient.subscribe("/user/topic/newParticipant", function(message) {
+                        this.socketClient.subscribe("/user/topic/newParticipant", (message) => {
                             console.log("newParticipant received:", message.body);
+
+                            // refresh meeting data
+                            // TODO: do participant replace
+                            this.getMeetingInfo();
                         });
 
-                        this.socketClient.subscribe("/user/topic/myMicrophoneIsAcquired", function(message) {
+                        this.socketClient.subscribe("/user/topic/myMicrophoneIsAcquired", (message) => {
                             console.log("myMicrophoneIsAcquired received:", message.body);
                         });
 
-                        this.socketClient.subscribe("/user/topic/myMicrophoneIsNotAcquired", function(message) {
+                        this.socketClient.subscribe("/user/topic/myMicrophoneIsNotAcquired", (message) => {
                             console.log("myMicrophoneIsNotAcquired received:", message.body);
                         });
 
-                        this.socketClient.subscribe("/user/topic/myMicrophoneIsOn", function(message) {
+                        this.socketClient.subscribe("/user/topic/myMicrophoneIsOn", (message) => {
                             console.log("myMicrophoneIsOn received:", message.body);
+                            // refresh meeting data
+                            // TODO: do participant replace
+                            this.getMeetingInfo();
                         });
 
-                        this.socketClient.subscribe("/user/topic/myMicrophoneIsOff", function(message) {
+                        this.socketClient.subscribe("/user/topic/myMicrophoneIsOff", (message) => {
                             console.log("myMicrophoneIsOff received:", message.body);
+                            // refresh meeting data
+                            // TODO: do participant replace
+                            this.getMeetingInfo();
                         });
 
-                        this.socketClient.subscribe("/user/topic/myCameraIsAcquired", function(message) {
+                        this.socketClient.subscribe("/user/topic/myCameraIsAcquired", (message) => {
                             console.log("myCameraIsAcquired received:", message.body);
                         });
 
-                        this.socketClient.subscribe("/user/topic/myCameraIsNotAcquired", function(message) {
+                        this.socketClient.subscribe("/user/topic/myCameraIsNotAcquired", (message) => {
                             console.log("myCameraIsNotAcquired received:", message.body);
                         });
 
-                        this.socketClient.subscribe("/user/topic/myCameraIsOn", function(message) {
+                        this.socketClient.subscribe("/user/topic/myCameraIsOn", (message) => {
                             console.log("myCameraIsOn received:", message.body);
                         });
 
-                        this.socketClient.subscribe("/user/topic/myCameraIsOff", function(message) {
+                        this.socketClient.subscribe("/user/topic/myCameraIsOff", (message) => {
                             console.log("myCameraIsOff received:", message.body);
                         });
 
-                        this.socketClient.subscribe("/user/topic/editMyName", function(message) {
+                        this.socketClient.subscribe("/user/topic/editMyName", (message) => {
                             console.log("editMyName received:", message.body);
                         });
 
-                        this.socketClient.subscribe("/user/topic/setMyAwayMessage", function(message) {
+                        this.socketClient.subscribe("/user/topic/setMyAwayMessage", (message) => {
                             console.log("setMyAwayMessage received:", message.body);
                         });
 
-                        this.socketClient.subscribe("/user/topic/removeMyAwayMessage", function(message) {
+                        this.socketClient.subscribe("/user/topic/removeMyAwayMessage", (message) => {
                             console.log("removeMyAwayMessage received:", message.body);
                         });
 
-                        this.socketClient.subscribe("/user/topic/leaveClientRoom", function(message) {
+                        this.socketClient.subscribe("/user/topic/leaveClientRoom", (message) => {
                             console.log("leaveClientRoom received:", message.body);
                         });
 
                     },
+                    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 mic');
+                            }
+                        }
+                        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]);
+                            }
+
+                            // 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]);
+                                }
+                                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 () {
-                        self.socketClient.send("/app/myMicrophoneIsAcquired", {},
-                            JSON.stringify({
-                                sessionKey: '{{$performer->session_key}}'
-                            })
+                        this.socketClient.send("/app/myMicrophoneIsAcquired", {},
+                            JSON.stringify({sessionKey: '{{$performer->session_key}}'})
                         );
                     },
                     myMicrophoneIsNotAcquired: function () {
-                        self.socketClient.send("/app/myMicrophoneIsNotAcquired", {},
-                            JSON.stringify({
-                                sessionKey: '{{$performer->session_key}}'
-                            })
+                        this.socketClient.send("/app/myMicrophoneIsNotAcquired", {},
+                            JSON.stringify({sessionKey: '{{$performer->session_key}}'})
                         );
                     },
                     myMicrophoneIsOn: function () {
-                        self.socketClient.send("/app/myMicrophoneIsOn", {},
-                            JSON.stringify({
-                                sessionKey: '{{$performer->session_key}}'
-                            })
+                        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 () {
-                        self.socketClient.send("/app/myMicrophoneIsOff", {},
-                            JSON.stringify({
-                                sessionKey: '{{$performer->session_key}}'
-                            })
+                        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 () {
-                        self.socketClient.send("/app/myCameraIsAcquired", {},
-                            JSON.stringify({
-                                sessionKey: '{{$performer->session_key}}'
-                            })
+                        this.socketClient.send("/app/myCameraIsAcquired", {},
+                            JSON.stringify({sessionKey: '{{$performer->session_key}}'})
                         );
                     },
                     myCameraIsNotAcquired: function () {
-                        self.socketClient.send("/app/myCameraIsNotAcquired", {},
-                            JSON.stringify({
-                                sessionKey: '{{$performer->session_key}}'
-                            })
+                        this.socketClient.send("/app/myCameraIsNotAcquired", {},
+                            JSON.stringify({sessionKey: '{{$performer->session_key}}'})
                         );
                     },
                     myCameraIsOn: function () {
-                        self.socketClient.send("/app/myCameraIsOn", {},
-                            JSON.stringify({
-                                sessionKey: '{{$performer->session_key}}'
-                            })
+                        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 () {
-                        self.socketClient.send("/app/myCameraIsOff", {},
-                            JSON.stringify({
-                                sessionKey: '{{$performer->session_key}}'
-                            })
+                        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) {
-                        self.socketClient.send("/app/editMyName", {},
+                        this.socketClient.send("/app/editMyName", {},
                             JSON.stringify({
                                 sessionKey: '{{$performer->session_key}}',
                                 myNewName: _myNewName
@@ -490,7 +559,7 @@
                         );
                     },
                     setMyAwayMessage: function (_message) {
-                        self.socketClient.send("/app/setMyAwayMessage", {},
+                        this.socketClient.send("/app/setMyAwayMessage", {},
                             JSON.stringify({
                                 sessionKey: '{{$performer->session_key}}',
                                 message: _message
@@ -498,49 +567,21 @@
                         );
                     },
                     removeMyAwayMessage: function () {
-                        self.socketClient.send("/app/removeMyAwayMessage", {},
-                            JSON.stringify({
-                                sessionKey: '{{$performer->session_key}}'
-                            })
+                        this.socketClient.send("/app/removeMyAwayMessage", {},
+                            JSON.stringify({sessionKey: '{{$performer->session_key}}'})
                         );
                     },
                     leaveClientRoom: function () {
-                        self.socketClient.send("/app/leaveClientRoom", {},
-                            JSON.stringify({
-                                sessionKey: '{{$performer->session_key}}'
-                            })
+                        this.socketClient.send("/app/leaveClientRoom", {},
+                            JSON.stringify({sessionKey: '{{$performer->session_key}}'})
                         );
+                        window.location.href = '/pro/meet';
                     },
                     // end: actions that notify participants via socket
 
                 },
                 mounted: function () {
-
-                    // enter the room
-                    this.enterClientRoomAsPro(function() {
-
-                        // get meeting info
-                        this.getMeetingInfo(function() {
-
-                            // register socket
-                            this.registerSocket();
-
-                        });
-
-                    });
-
-                    {{--var self = this;--}}
-
-                    {{--$(document).on('click', '.thumbs>div[data-stream]', function () {--}}
-                    {{--    self.activateParty($(this).attr('data-stream'));--}}
-                    {{--    return false;--}}
-                    {{--});--}}
-
-                    {{--@if(isset($client))--}}
-                    {{--    self.client = true;--}}
-                    {{--    self.clientUid = '{{ $client->uid }}';--}}
-                    {{--    self.videoActive = false;--}}
-                    {{--@endif--}}
+                    this.enterClientRoomAsPro();
                 }
             });
             new Vue({
@@ -583,29 +624,5 @@
             })
         })();
     </script>
-    <!--
-    <script>
-        // connect to WS
-        self.socket = new SockJS('{{ config('app.backend_ws_url') }}');
-        self.socketClient = Stomp.over(self.socket);
-        self.socketClient.connect({}, function(frame) {
-            console.log('Connected: ' + frame);
-
-            self.socketClient.subscribe("/user/topic/registration", function(message) {
-                console.log("Receiving message")
-                console.log("registration result:", message.body);
-            });
-
-            // join self
-            console.log("Sending message")
-            self.socketClient.send("/app/register", {},
-                JSON.stringify({
-                    sessionKey: '{{$performer->session_key}}'
-                })
-            );
-
-        });
-    </script>
-    -->
 </body>
 </html>