Pārlūkot izejas kodu

Call minimal vanilla impl [wip]

Vijayakrishnan 4 gadi atpakaļ
vecāks
revīzija
5180615850
1 mainītis faili ar 131 papildinājumiem un 745 dzēšanām
  1. 131 745
      resources/views/app/video/call-minimal.blade.php

+ 131 - 745
resources/views/app/video/call-minimal.blade.php

@@ -37,110 +37,50 @@
     </button>
 
     <div class="instruction-container">
-        <div class="mic-access-chrome-desktop border bg-light rounded mx-3 p-2 d-none">
+        <div class="mic-access-chrome-desktop border bg-light rounded mx-3 px-3 pt-3 pb-2 d-none">
             <p><b>We were unable to access your microphone!</b></p>
-            <p>To allow access, please tap the media icon (indicated in the figure below).</p>
-            <img src="/img/mic-access-chrome-desktop.png" class="mw-100 mx-auto my-2 d-block">
+            <p>To allow access, please tap the blocked media icon in your browser's address bar (indicated in the figure below).</p>
+            <img src="/img/mic-access-chrome-desktop.png" class="mw-100 mx-auto mb-3 d-block">
+            <p class="mb-0">Once you have allowed access, please click <b>Start Video</b> again to retry.</p>
+        </div>
+        <div class="cam-access-chrome-desktop border bg-light rounded mx-3 px-3 pt-3 pb-2 d-none">
+            <p><b>We were unable to access your camera!</b></p>
+            <p>To allow access, please tap the blocked media icon in your browser's address bar (indicated in the figure below).</p>
+            <img src="/img/mic-access-chrome-desktop.png" class="mw-100 mx-auto mb-3 d-block">
             <p class="mb-0">Once you have allowed access, please click <b>Start Video</b> again to retry.</p>
         </div>
     </div>
 
-    <div id="video-container" class="d-none">
+    <div id="video-container">
         <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 id="self-view" class="video-view"></div>
         </div>
     </div>
 
+<!--    <div class="call-actions d-none align-items-center">
+        <button class="btn btn-danger rounded-circle"
+                title="Leave Call">
+            <i class="fa fa-phone"></i>
+        </button>
+        <button v-if="myMedia.isCameraOn" class="btn btn-default bg-light border rounded-circle"
+                title="Stop Camera">
+            <i class="fa fa-video"></i>
+        </button>
+        <button v-if="!myMedia.isCameraOn" class="btn btn-secondary rounded-circle"
+                title="Start Camera">
+            <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">
+            <i class="fa fa-microphone-slash"></i>
+        </button>
+    </div>-->
+
 </div>
 
 <script>
@@ -247,6 +187,22 @@
                 // create client
                 this.mediaServiceClient = AgoraRTC.createClient({mode: 'rtc', codec: 'vp8'});
 
+                // acquire devices
+                this.attemptToAcquireMicrophone(() => {
+                    this.attemptToAcquireCamera(() => {
+                        console.log('ALIX: Hurrah! Mic and camera both acquired :)');
+
+                        // go to video UI
+                        $('#btn-start-video').removeClass('d-block').addClass('d-none');
+                        $('#video-container').removeClass('d-none').addClass('d-block');
+                        $('#self-view').height($('#self-view').width() * 0.75);
+                        this.myCamera.play($('#self-view')[0], {fit: 'contain'});
+                    })
+                });
+
+            },
+
+            attemptToAcquireMicrophone: function(_done) {
                 AgoraRTC.onMicrophoneChanged = (_info) => {
                     console.log("ALIX: microphone changed!", _info.state, _info.device);
                     if(_info.state === 'ACTIVE') {
@@ -260,6 +216,11 @@
                             console.log('ALIX: microphone acquisition attempt completed');
                             console.log('ALIX: (reactive) isMicrophoneAcquired = ', this.meetingData.myMedia.isMicrophoneAcquired);
                             console.log('ALIX: (reactive) isMicrophoneOn = ', this.meetingData.myMedia.isMicrophoneOn);
+
+                            // if all good, allow to proceed
+                            if(this.myMicrophone && this.meetingData.myMedia.isMicrophoneAcquired) {
+                                _done.call(this);
+                            }
                         });
                     }
                 };
@@ -269,11 +230,15 @@
                     console.log('ALIX: microphone acquisition attempt completed');
                     console.log('ALIX: (proactive) isMicrophoneAcquired = ', this.meetingData.myMedia.isMicrophoneAcquired);
                     console.log('ALIX: (proactive) isMicrophoneOn = ', this.meetingData.myMedia.isMicrophoneOn);
-                });
 
+                    // if all good, allow to proceed
+                    if(this.myMicrophone && this.meetingData.myMedia.isMicrophoneAcquired) {
+                        _done.call(this);
+                    }
+                });
             },
 
-            acquireMicrophone: async function(_deviceId = false) {
+            acquireMicrophone: async function() {
 
                 // if already acquired, ignore
                 if(this.myMicrophone && this.meetingData.myMedia.isMicrophoneAcquired) {
@@ -292,7 +257,7 @@
                     // TODO: notify others via WS
 
                     this.myMicrophone.setEnabled(true);
-                    this.myMicrophone.setVolume(1000);
+                    this.myMicrophone.setVolume(1000); // 1000 is the max allowed value
                     this.meetingData.myMedia.isMicrophoneOn = true;
                     console.log('ALIX: microphone is ON and not on mute :)');
                     // TODO: log
@@ -312,20 +277,79 @@
                 }
             },
 
+            attemptToAcquireCamera: function(_done) {
+                AgoraRTC.onCameraChanged = (_info) => {
+                    console.log("ALIX: camera changed!", _info.state, _info.device);
+                    if(_info.state === 'ACTIVE') {
+
+                        if(!this.myCamera || !this.meetingData.myMedia.isCameraAcquired) {
+                            window.location.reload();
+                        }
+
+                        // reactive acquisition (because user later granted access)
+                        this.acquireCamera().then(() => {
+                            console.log('ALIX: camera acquisition attempt completed');
+                            console.log('ALIX: (reactive) isCameraAcquired = ', this.meetingData.myMedia.isCameraAcquired);
+                            console.log('ALIX: (reactive) isCameraOn = ', this.meetingData.myMedia.isCameraOn);
+
+                            // if all good, allow to proceed
+                            if(this.myCamera && this.meetingData.myMedia.isCameraAcquired) {
+                                _done.call(this);
+                            }
+                        });
+                    }
+                };
+
+                // proactive acquisition (already allowed OR user clicked "Allow" when prompted
+                this.acquireCamera().then(() => {
+                    console.log('ALIX: camera acquisition attempt completed');
+                    console.log('ALIX: (proactive) isCameraAcquired = ', this.meetingData.myMedia.isCameraAcquired);
+                    console.log('ALIX: (proactive) isCameraOn = ', this.meetingData.myMedia.isCameraOn);
+
+                    // if all good, allow to proceed
+                    if(this.myCamera && this.meetingData.myMedia.isCameraAcquired) {
+                        _done.call(this);
+                    }
+                });
+            },
+
             acquireCamera: async function() {
-                this.myMedia.isCameraAcquired = false;
-                this.myMedia.isCameraOn = false;
+
+                // if already acquired, ignore
+                if(this.myCamera && this.meetingData.myMedia.isCameraAcquired) {
+                    console.log("ALIX: Skipping camera acquisition..")
+                    return;
+                }
+
+                this.meetingData.myMedia.isCameraAcquired = false;
+                this.meetingData.myMedia.isCameraOn = false;
                 try {
-                    @if(config('app.agora_mode') === 'screen') // testing
-                    this.myCamera = await AgoraRTC.createScreenVideoTrack();
-                    @else
-                        this.myCamera = await AgoraRTC.createCameraVideoTrack({
+                    this.myCamera = await AgoraRTC.createCameraVideoTrack({
                         optimizationMode: "motion"
                     });
-                    @endif
-                        this.myMedia.isCameraAcquired = true;
+
+                    console.log('ALIX: acquired camera :)');
+                    this.meetingData.myMedia.isCameraAcquired = true;
+                    // TODO: log
+                    // TODO: notify others via WS
+
+                    this.myCamera.setEnabled(true);
+                    this.meetingData.myMedia.isCameraOn = true;
+                    console.log('ALIX: camera is ON :)');
+                    // TODO: log
+                    // TODO: notify others via WS
+
                 } catch (e) {
-                    console.log('ALIX: error in getting camera');
+                    console.error('ALIX: could not acquire camera');
+
+                    this.meetingData.myMedia.isCameraAcquired = false;
+                    this.meetingData.myMedia.isCameraOn = false;
+                    // TODO: log
+                    // TODO: notify others via WS
+
+                    $('#btn-start-video').removeClass('d-block').addClass('d-none');
+                    // TODO: Use device/browser specific instruction & image
+                    $('.cam-access-chrome-desktop').removeClass('d-none').addClass('d-block');
                 }
             },
 
@@ -379,643 +403,5 @@
     }).call(window);
 </script>
 
-
-<script class="d-none">
-    (function () {
-        window.proCallComponent = new Vue({
-            el: '#proCallComponentFF',
-            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: 'http://localhost:8080/ws', // {{ 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();
-                            }, 500);
-                        });
-                    }
-
-                    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>
-
 </body>
 </html>