Pārlūkot izejas kodu

Check patient video feature (no self publish)

Vijayakrishnan 4 gadi atpakaļ
vecāks
revīzija
6dd36e32f0

+ 8 - 0
app/Http/Controllers/PracticeManagementController.php

@@ -280,6 +280,14 @@ class PracticeManagementController extends Controller
         return view('app.video.call-agora-v2', compact('session', 'client'));
     }
 
+    // check video page
+    public function checkVideo(Request $request, $uid) {
+        $session = AppSession::where('session_key', $request->cookie('sessionKey'))->first();
+        $client = !empty($uid) ? Client::where('uid', $uid)->first() : null;
+        $publish = false;
+        return view('app.video.check-video-minimal', compact('session', 'client'));
+    }
+
     public function getParticipantInfo(Request $request) {
         $sid = intval($request->get('uid')) - 1000000;
         $session = AppSession::where('id', $sid)->first();

+ 4 - 0
public/css/style.css

@@ -1234,3 +1234,7 @@ button.note-templates-trigger-assessment {
 .flowsheets-table .collapsed tr:not(:first-child) {
     display: none;
 }
+.col-2-button {
+    width: 80px;
+    text-align: left;
+}

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

@@ -33,7 +33,7 @@
     <button id="btn-start-video"
             disabled
             class="btn btn-primary px-4 font-weight-bold d-block mx-auto my-3">
-        Start Video
+        Join Video
     </button>
 
     <div class="instruction-container">
@@ -41,13 +41,13 @@
             <p><b>We were unable to access your microphone!</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>
+            <p class="mb-0">Once you have allowed access, please click <b>Join Video</b> again to retry.</p>
         </div>
         <div class="cam-access-chrome-desktop border bg-light rounded m-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>
+            <p class="mb-0">Once you have allowed access, please click <b>Join Video</b> again to retry.</p>
         </div>
     </div>
 

+ 706 - 0
resources/views/app/video/check-video-minimal.blade.php

@@ -0,0 +1,706 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>Leadership Health</title>
+
+    <link rel="stylesheet" href="/css/bootstrap.min.css">
+    <link rel="stylesheet" href="/fontawesome-free/css/all.min.css">
+    <link rel="stylesheet" href="/css/toastr.min.css">
+    <script src="/js/jquery-3.5.1.min.js"></script>
+    <script src="/js/toastr.min.js"></script>
+    <script defer src=/js/AgoraRTC_N-4.1.0.js></script>
+    <script src="/js/sockjs.min.js"></script>
+    <script src="/js/stomp.min.js"></script>
+
+    <link href="/css/call-minimal.css" rel="stylesheet">
+</head>
+
+<body class="p-0 m-0">
+
+<div class="d-flex px-3 border-bottom">
+    <div class="py-2 font-weight-normal mcp-theme-1 d-inline-flex align-items-center">
+        <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>
+</div>
+
+<div class="videos-container">
+
+    {{-- check video button --}}
+    <button id="btn-start-video" disabled
+            class="btn btn-primary px-4 font-weight-bold d-none mx-auto my-3">
+        Check Video
+    </button>
+
+    <div class="instruction-container">
+        <div class="mic-access-chrome-desktop border bg-light rounded m-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 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 m-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="container d-none">
+        <div class="main-view mx-auto">
+            <div id="self-view" class="video-view" data-user-id="">
+                <i class="fa fa-volume-mute text-white muted-icon"></i>
+            </div>
+        </div>
+    </div>
+
+    <div id="call-actions" class="d-none">
+        <button class="btn btn-danger" id="btn-hang-up" title="End">
+            <i class="fa fa-phone"></i>
+        </button>
+<!--        <button class="ml-2 btn btn-default bg-light border" id="btn-stop-camera" title="Stop Camera">
+            <i class="fa fa-video"></i>
+        </button>
+        <button class="ml-2 btn btn-secondary d-none" id="btn-start-camera" title="Start Camera">
+            <i class="fa fa-video-slash"></i>
+        </button>
+        <button class="ml-2 btn btn-default bg-light border" id="btn-mute-audio" title="Mute">
+            <i class="fa fa-microphone"></i>
+        </button>
+        <button class="ml-2 btn btn-secondary d-none" id="btn-unmute-audio" title="Unmute">
+            <i class="fa fa-microphone-slash"></i>
+        </button>-->
+    </div>
+
+</div>
+
+<script>
+    (function() {
+
+        window.StagVideo = {
+
+            // model
+
+            // data returned by getMyMeeting
+            meetingData: {
+                amIInAMeeting: false,
+                awayMessage: '',
+                inMeetingForClient: {
+                    clientMediaServiceRoomIdentifier: '',
+                    displayName: '',
+                    dob: '',
+                    uid: '',
+                },
+                inMeetingForClientUid: null,
+                inMeetingForPro: null,
+                inMeetingForProUid: null,
+                meetingType: "CLIENT",
+                myMedia: {
+                    isCameraAcquired: false,
+                    isCameraOn: false,
+                    isMicrophoneAcquired: false,
+                    isMicrophoneOn: false
+                },
+                myMediaServiceIdentifier: '',
+                myMediaServiceToken: null,
+                otherParticipants: []
+            },
+
+            // data local to StagVideo
+            myName: '{{ $performer->pro->displayName() }}',
+            clientUid: '{{ $client->uid }}',
+
+            // agora
+            mediaServiceClient: null, // instantiated on agora init
+            appId: '{{ config('app.agora_appid') }}',
+
+            // handle to own tracks (needed later for muting and unmuting)
+            myAudio: null,
+            myVideo: null,
+
+            // sockets
+            backendWsURL: '{{ config('app.backend_ws_url') }}',
+            socketClient: null,
+
+            // cache elements to avoid running selectors everytime
+            // Notation: $ at the beginning for jQuery objects
+            $btnStartVideo: null,
+            $videoContainer: null,
+            $selfView: null,
+            $callActions: null,
+            $btnHangUp: null,
+            $btnStopCamera: null,
+            $btnStartCamera: null,
+            $btnMuteAudio: null,
+            $btnUnmuteAudio: null,
+
+            // methods
+            init: function() {
+
+                // to distinguish between reloads
+                this.log('page refreshed ----------------------------------');
+
+                // cache elements to avoid running selectors everytime
+                this.$btnStartVideo = $('#btn-start-video');
+                this.$videoContainer = $('#video-container');
+                this.$selfView = $('#self-view');
+                this.$callActions = $('#call-actions');
+                this.$btnHangUp = $('#btn-hang-up');
+
+                this.$btnStopCamera = $('#btn-stop-camera');
+                this.$btnStartCamera = $('#btn-start-camera');
+                this.$btnMuteAudio = $('#btn-mute-audio');
+                this.$btnUnmuteAudio = $('#btn-unmute-audio');
+
+                this.registerSocket(() => {
+                    this.initMedia();
+                });
+
+                // event handlers
+                $(document)
+                    .off('click.start-video', '#btn-start-video')
+                    .on('click.start-video', '#btn-start-video', () => {
+                        this.initMedia();
+                    });
+
+                $(document)
+                    .off('click.hang-up', '#btn-hang-up')
+                    .on('click.hang-up', '#btn-hang-up', () => {
+                        this.leaveRoomAsPro();
+                    });
+
+                $(document)
+                    .off('click.stop-camera', '#btn-stop-camera')
+                    .on('click.stop-camera', '#btn-stop-camera', () => {
+                        this.stopCamera();
+                    });
+
+                $(document)
+                    .off('click.start-camera', '#btn-start-camera')
+                    .on('click.start-camera', '#btn-start-camera', () => {
+                        this.startCamera();
+                    });
+
+                $(document)
+                    .off('click.mute-audio', '#btn-mute-audio')
+                    .on('click.mute-audio', '#btn-mute-audio', () => {
+                        this.muteAudio();
+                    });
+
+                $(document)
+                    .off('click.unmute-audio', '#btn-unmute-audio')
+                    .on('click.unmute-audio', '#btn-unmute-audio', () => {
+                        this.unmuteAudio();
+                    });
+
+
+                this.enterRoomAsPro();
+            },
+
+            // register socket
+            registerSocket: function (_done) {
+                let socket = new SockJS(this.backendWsURL);
+                this.socketClient = Stomp.over(socket);
+                this.socketClient.connect({}, (frame) => {
+                    this.socketClient.send("/app/register", {},
+                        JSON.stringify({
+                            sessionKey: '{{$performer->session_key}}'
+                        })
+                    );
+                    window.setInterval(() => {
+                        this.socketClient.send("/app/heartbeat", {},
+                            JSON.stringify({sessionKey: '{{ request()->cookie('sessionKey') }}'})
+                        );
+                    }, 5000);
+
+                    this.initSocketEvents();
+
+                    _done.call(this);
+                });
+            },
+
+            // on pro side, handle all events
+            initSocketEvents: function() {
+
+                this.socketClient.subscribe("/user/topic/myMicrophoneIsOn", (message) => {
+                    this.log("WSE myMicrophoneIsOn received: " + message.body);
+                    this.handleParticipantMicrophoneMutedChangeWSEvent(message, true);
+                });
+
+                this.socketClient.subscribe("/user/topic/myMicrophoneIsOff", (message) => {
+                    this.log("WSE myMicrophoneIsOff received:" + message.body);
+                    this.handleParticipantMicrophoneMutedChangeWSEvent(message, false);
+                });
+
+                this.log("Initialized WS event handlers")
+
+            },
+
+            handleParticipantMicrophoneMutedChangeWSEvent: function(_event, _value) {
+                if(_event && _event.body) {
+                    let eventData = JSON.parse(_event.body);
+                    if(eventData.performer !== '{{ $session->uid  }}' && eventData.data) {
+                        for (let i = 0; i < this.meetingData.otherParticipants.length; i++) {
+                            if(this.meetingData.otherParticipants[i].uid === eventData.performer) {
+                                this.meetingData.otherParticipants[i].media.isMicrophoneOn = _value;
+                                if(!_value) {
+                                    $('.video-view[data-user-id="' + this.meetingData.otherParticipants[i].mediaServiceIdentifier + '"]')
+                                        .attr('participant-muted', 1);
+                                }
+                                else {
+                                    $('.video-view[data-user-id="' + this.meetingData.otherParticipants[i].mediaServiceIdentifier + '"]')
+                                        .removeAttr('participant-muted');
+                                }
+                            }
+                        }
+                    }
+                }
+            },
+
+            // join meeting
+            enterRoomAsPro: function () {
+                this.ajax('/api/meeting/enterClientRoomAsPro', {clientUid: this.clientUid}, (_data) => {
+                    this.getMeetingInfo(true, () => {
+                        this.$btnStartVideo.prop('disabled', false);
+                    });
+                });
+            },
+
+            // leave meeting
+            leaveRoomAsPro: function() {
+
+                // notify java
+                this.socketClient.send("/app/leaveClientRoom",
+                    {},
+                    JSON.stringify({sessionKey: '{{$performer->session_key}}'})
+                );
+
+                // refresh to non-client page
+                window.location.href = '/pro/meet/';
+
+            },
+
+            // get meeting info and populate model
+            getMeetingInfo: function (_firstRun = false, _done = null) {
+                this.ajax('/api/meeting/getMyMeeting', {}, (_data) => {
+
+                    // fill model
+                    this.meetingData = _data.data;
+
+                    // set own myMediaServiceIdentifier to #selfView
+                    if(_firstRun) this.$selfView.attr('data-user-id', this.meetingData.myMediaServiceIdentifier);
+
+                    if(_done) _done.call(this);
+                }, 'json');
+            },
+
+            // init media
+            initMedia: function() {
+
+                // create client
+                this.mediaServiceClient = AgoraRTC.createClient({mode: 'rtc', codec: 'vp8'});
+
+                // no need to acquire devices in check-video page
+                /*this.attemptToAcquireMicrophone(() => {
+                    this.attemptToAcquireCamera(() => {
+                        this.log('Hurrah! Mic and camera both acquired :)', 'log');
+
+                        // go to video UI
+                        this.$btnStartVideo.removeClass('d-block').addClass('d-none');
+                        this.$videoContainer.removeClass('d-none').addClass('d-block');
+
+                        // show call actions
+                        this.$callActions.removeClass('d-none').addClass('d-flex');
+
+                        // show own video
+                        this.$selfView.height(this.$selfView.width() * 0.75); // 4x3 mode
+                        this.myVideo.play(this.$selfView[0], {fit: 'contain'});
+
+                        // show own name-bar
+                        this.$selfView.find('.name-bar').remove();
+                        this.$selfView.append($('<div class="name-bar"></div>').text(this.myName));
+
+                        // start listening to events
+                        this.initMediaEvents();
+
+                        // start publishing
+                        this.initMediaPublishing();
+
+                    })
+                });*/
+
+                // go to video UI
+                this.$btnStartVideo.removeClass('d-block').addClass('d-none');
+                this.$videoContainer.removeClass('d-none').addClass('d-block');
+
+                // show call actions
+                this.$callActions.removeClass('d-none').addClass('d-flex');
+
+                // own feed not to be displayed in check-video
+                /*// show own video
+                this.$selfView.height(this.$selfView.width() * 0.75); // 4x3 mode
+                this.myVideo.play(this.$selfView[0], {fit: 'contain'});
+
+                // show own name-bar
+                this.$selfView.find('.name-bar').remove();
+                this.$selfView.append($('<div class="name-bar"></div>').text(this.myName));*/
+
+                // start listening to events
+                this.initMediaEvents();
+
+                // start publishing
+                this.initMediaPublishing();
+
+            },
+
+            attemptToAcquireMicrophone: function(_done) {
+                AgoraRTC.onMicrophoneChanged = (_info) => {
+                    this.log("microphone changed! - " + JSON.stringify(_info.state), 'log');
+                    if(_info.state === 'ACTIVE') {
+
+                        if(!this.myAudio || !this.meetingData.myMedia.isMicrophoneAcquired) {
+                            window.location.reload();
+                        }
+
+                        // reactive acquisition (because user later granted access)
+                        this.acquireMicrophone().then(() => {
+                            this.log('microphone acquisition attempt completed', 'log');
+
+                            // if all good, allow to proceed
+                            if(this.myAudio && this.meetingData.myMedia.isMicrophoneAcquired) {
+                                _done.call(this);
+                            }
+                        });
+                    }
+                };
+
+                // proactive acquisition (already allowed OR user clicked "Allow" when prompted
+                this.acquireMicrophone().then(() => {
+                    this.log('microphone acquisition attempt completed', 'log');
+
+                    // if all good, allow to proceed
+                    if(this.myAudio && this.meetingData.myMedia.isMicrophoneAcquired) {
+                        _done.call(this);
+                    }
+                });
+            },
+
+            acquireMicrophone: async function() {
+
+                // if already acquired, ignore
+                if(this.myAudio && this.meetingData.myMedia.isMicrophoneAcquired) {
+                    // Skipping microphone acquisition..
+                    return;
+                }
+
+                this.meetingData.myMedia.isMicrophoneAcquired = false;
+                this.meetingData.myMedia.isMicrophoneOn = false;
+                try {
+                    this.myAudio = await AgoraRTC.createMicrophoneAudioTrack({
+                        AEC: true,
+                        AGC: true,
+                        ANS: true,
+                        encoderConfig: 'speech_standard'
+                    });
+
+                    this.meetingData.myMedia.isMicrophoneAcquired = true;
+                    this.log('acquired microphone :)');
+                    // TODO: notify others via WS
+
+                    this.myAudio.setEnabled(true);
+                    this.myAudio.setVolume(100); // default volume, max allowed is 1000
+                    this.meetingData.myMedia.isMicrophoneOn = true;
+                    this.log('microphone is ON and not on mute :)');
+                    // TODO: notify others via WS
+
+                } catch (e) {
+                    this.log('could not acquire microphone', 'error');
+
+                    this.meetingData.myMedia.isMicrophoneAcquired = false;
+                    this.meetingData.myMedia.isMicrophoneOn = false;
+                    // TODO: notify others via WS
+
+                    this.$btnStartVideo.removeClass('d-block').addClass('d-none');
+                    // TODO: Use device/browser specific instruction & image
+                    $('.mic-access-chrome-desktop').removeClass('d-none').addClass('d-block');
+                }
+            },
+
+            attemptToAcquireCamera: function(_done) {
+                AgoraRTC.onCameraChanged = (_info) => {
+                    this.log("camera changed! - " + JSON.stringify(_info));
+                    if(_info.state === 'ACTIVE') {
+
+                        if(!this.myVideo || !this.meetingData.myMedia.isCameraAcquired) {
+                            window.location.reload();
+                        }
+
+                        // reactive acquisition (because user later granted access)
+                        this.acquireCamera().then(() => {
+                            this.log('camera acquisition attempt completed');
+
+                            // if all good, allow to proceed
+                            if(this.myVideo && this.meetingData.myMedia.isCameraAcquired) {
+                                _done.call(this);
+                            }
+                        });
+                    }
+                };
+
+                // proactive acquisition (already allowed OR user clicked "Allow" when prompted
+                this.acquireCamera().then(() => {
+                    this.log('camera acquisition attempt completed');
+
+                    // if all good, allow to proceed
+                    if(this.myVideo && this.meetingData.myMedia.isCameraAcquired) {
+                        _done.call(this);
+                    }
+                });
+            },
+
+            acquireCamera: async function() {
+
+                // if already acquired, ignore
+                if(this.myVideo && this.meetingData.myMedia.isCameraAcquired) {
+                    // Skipping camera acquisition
+                    return;
+                }
+
+                this.meetingData.myMedia.isCameraAcquired = false;
+                this.meetingData.myMedia.isCameraOn = false;
+                try {
+                    this.myVideo = await AgoraRTC.createCameraVideoTrack({
+                        optimizationMode: "motion"
+                    });
+
+                    this.meetingData.myMedia.isCameraAcquired = true;
+                    this.log('acquired camera :)');
+                    // TODO: notify others via WS
+
+                    this.myVideo.setEnabled(true);
+                    this.meetingData.myMedia.isCameraOn = true;
+                    this.log('camera is ON :)');
+                    // TODO: notify others via WS
+
+                } catch (e) {
+                    this.log('could not acquire camera', 'error');
+
+                    this.meetingData.myMedia.isCameraAcquired = false;
+                    this.meetingData.myMedia.isCameraOn = false;
+                    // TODO: notify others via WS
+
+                    this.$btnStartVideo.removeClass('d-block').addClass('d-none');
+                    // TODO: Use device/browser specific instruction & image
+                    $('.cam-access-chrome-desktop').removeClass('d-none').addClass('d-block');
+                }
+            },
+
+            // join agora & start publishing
+            getMyMediaServiceToken: function(_done) {
+                $.post('/api/meeting/refreshMyMediaServiceToken', (_data) => {  // get new agora token
+                    if (!this.hasError(_data)) {
+                        this.meetingData.myMediaServiceToken = _data.data;
+                        _done.call(this);
+                    }
+                }, 'json');
+            },
+
+            // init media events
+            initMediaEvents: function() {
+                this.mediaServiceClient.on('user-left', user => {
+                    // remove user's video div
+                    $('.video-view[data-user-id="' + user.uid + '"]').remove();
+
+                    // refresh state using getMyMeeting
+                    this.getMeetingInfo();
+
+                    // TODO log
+                });
+                this.mediaServiceClient.on('user-published', async (user, mediaType) => {
+                    this.log('user-published - ' + user.uid);
+
+                    // subscribe to the stream
+                    try {
+                        await this.mediaServiceClient.subscribe(user, mediaType);
+
+                        // subscription success, proceed to playing the stream
+                        if(mediaType === 'video') {
+                            let element = $('.video-view[data-user-id="' + user.uid + '"]');
+                            if(!element.length) {
+                                element = $('<div class="video-view" data-user-id="' + user.uid + '"></div>');
+                                element.appendTo('.main-view');
+                            }
+                            element.height(element.width() * 0.75); // 4x3 mode
+                            user.videoTrack.play(element[0], {fit: 'contain'});
+
+                            // refreshState & show user's name over video
+                            this.getMeetingInfo(false, () => {
+                                let participant = null;
+                                for (let i = 0; i < this.meetingData.otherParticipants.length; i++) {
+                                    if (this.meetingData.otherParticipants[i].mediaServiceIdentifier === ('' + user.uid)) {
+                                        participant = this.meetingData.otherParticipants[i];
+                                        break;
+                                    }
+                                }
+                                if (participant) {
+                                    element.find('.name-bar').remove();
+                                    element.append($('<div class="name-bar"></div>').text(participant.displayName));
+                                    element.append('<i class="fa fa-volume-mute text-white muted-icon"></i>');
+                                }
+                            });
+
+                        }
+                        else if(mediaType === 'audio') {
+                            user.audioTrack.play();
+                        }
+
+                        this.log('got ' + mediaType + ' stream from user: ' + user.uid);
+                    }
+                    catch (e) {
+                        this.log('could not subscribe to ' + mediaType + ' from ' + user.uid + ' - ' + e.toString(), 'error');
+                    }
+                });
+            },
+
+            initMediaPublishing: function() {
+
+                // get a new token
+                this.getMyMediaServiceToken(async () => {
+
+                    try {
+
+                        // join
+                        await this.mediaServiceClient.join(
+                            this.appId,
+                            this.meetingData.inMeetingForClient.clientMediaServiceRoomIdentifier,
+                            this.meetingData.myMediaServiceToken,
+                            +this.meetingData.myMediaServiceIdentifier
+                        );
+
+                        this.log('Joined agora channel :)');
+                    }
+                    catch (e) {
+                        this.log('could not join the room! - ' + e.toString(), 'error');
+                    }
+
+                    // no publishing in check-video page
+                    /*try {
+                        // publish
+                        await this.mediaServiceClient.publish([this.myAudio, this.myVideo]);
+
+                        this.log('Started publishing own audio and video to channel :)');
+                    }
+                    catch (e) {
+                        this.log('could not publish media to media service! - ' + e.toString(), 'error');
+                    }*/
+
+                });
+
+            },
+
+            // mute/unmute cam/mic
+            stopCamera: async function () {
+                try {
+                    await this.myVideo.setEnabled(false);
+                    this.socketClient.send("/app/myCameraIsOff", {},
+                        JSON.stringify({sessionKey: '{{$performer->session_key}}'})
+                    );
+                    this.$btnStopCamera.addClass('d-none');
+                    this.$btnStartCamera.removeClass('d-none');
+                    // TODO log
+                } catch (e) {
+                    this.log('could not stop camera! - ' + e.toString(), 'error');
+                }
+            },
+            startCamera: async function () {
+                try {
+                    await this.myVideo.setEnabled(true);
+                    this.socketClient.send("/app/myCameraIsOn", {},
+                        JSON.stringify({sessionKey: '{{$performer->session_key}}'})
+                    );
+                    this.$btnStartCamera.addClass('d-none');
+                    this.$btnStopCamera.removeClass('d-none');
+                    // TODO log
+                } catch (e) {
+                    this.log('could not start camera! - ' + e.toString(), 'error');
+                }
+            },
+            muteAudio: async function () {
+                try {
+                    await this.myAudio.setEnabled(false);
+                    this.socketClient.send("/app/myMicrophoneIsOff", {},
+                        JSON.stringify({sessionKey: '{{$performer->session_key}}'})
+                    );
+                    this.$btnMuteAudio.addClass('d-none');
+                    this.$btnUnmuteAudio.removeClass('d-none');
+                    this.$selfView.attr('participant-muted', 1);
+                    // TODO log
+                } catch (e) {
+                    this.log('could not mute audio! - ' + e.toString(), 'error');
+                    // TODO log
+                }
+            },
+            unmuteAudio: async function () {
+                try {
+                    await this.myAudio.setEnabled(true);
+                    this.socketClient.send("/app/myMicrophoneIsOn", {},
+                        JSON.stringify({sessionKey: '{{$performer->session_key}}'})
+                    );
+                    this.$btnUnmuteAudio.addClass('d-none');
+                    this.$btnMuteAudio.removeClass('d-none');
+                    this.$selfView.removeAttr('participant-muted');
+                    // TODO log
+                } catch (e) {
+                    this.log('could not unmute audio! ' + e.toString(), 'error');
+                }
+            },
+
+            // utils - start ============================================================== //
+
+            // generic logger
+            log: function(_message, _type = 'log') {
+                console[_type](
+                    'StagVideo => ' + '[' + (new Date()).toLocaleTimeString() + '] ' + this.myName,
+                    _message);
+                // post to userEventLog
+                $.post('/api/userEventLog/create', {
+                    eventName: 'video-event',
+                    dataJson: JSON.stringify({type: _type, message: _message})
+                }, function () {}, 'json');
+            },
+
+            // ajax request with error checking and reporting
+            ajax: function (_url, _input, _callback) {
+                $.post(_url, _input, (_data) => {
+                    if (!this.hasError(_data)) {
+                        _callback.call(this, _data);
+                    }
+                }, 'json');
+            },
+
+            // check and report error
+            hasError: function (_data) {
+                let msg = 'Unknown error!';
+                if (_data) {
+                    if (_data.success) return false;
+                    else if (_data.message) msg = _data.message;
+                }
+                toastr.error(msg);
+                return true;
+            }
+
+            // utils - end   ============================================================== //
+
+        };
+
+        window.StagVideo.init();
+
+    }).call(window);
+</script>
+
+</body>
+</html>

+ 11 - 9
resources/views/layouts/patient.blade.php

@@ -494,10 +494,6 @@
                                                 </form>
                                             </div>
                                         </div>
-                                        <div>
-                                            <button onclick="return openInRHS('/pro/meet/{{ $patient->uid }}')">Video&nbsp;<i
-                                                    class="fa fa-play text-secondary"></i></button>
-                                        </div>
                                         <div>
                                         <span moe relative class="">
                                             <button start show title="SMS check-in link to the patient">Send&nbsp;<i
@@ -527,7 +523,13 @@
                                         </span>
                                         </div>
                                     </section>
-                                    <section class="vbox mt-2 align-self-start ml-2">
+                                    <section class="vbox mt-2 align-self-start ml-1">
+                                        <div>
+                                            <button class="col-2-button" onclick="return openInRHS('/pro/check-video/{{ $patient->uid }}')">Check Video</button>
+                                        </div>
+                                        <div>
+                                            <button class="col-2-button" onclick="return openInRHS('/pro/meet/{{ $patient->uid }}')">Join Video</button>
+                                        </div>
                                         <div class="">
                                             @if($pro->isWorkingOnClient($patient))
                                                 {{-- stop work on client --}}
@@ -536,7 +538,7 @@
                                                           class="mcp-theme-1" show>
                                                         <input type="hidden" name="uid" value="{{$patient->uid}}">
                                                         <div>
-                                                            <button submit><i class="fa fa-stop text-secondary"></i>&nbsp;Work:
+                                                            <button class="col-2-button" submit><i class="fa fa-stop text-secondary"></i>&nbsp;Work:
                                                                 Stop
                                                             </button>
                                                         </div>
@@ -548,7 +550,7 @@
                                                     <form url="/api/proClientWork/create" class="mcp-theme-1" show>
                                                         <input type="hidden" name="clientUid" value="{{$patient->uid}}">
                                                         <div>
-                                                            <button submit><i class="fa fa-play text-secondary"></i>&nbsp;Work:
+                                                            <button class="col-2-button" submit><i class="fa fa-play text-secondary"></i>&nbsp;Work:
                                                                 Start
                                                             </button>
                                                         </div>
@@ -562,7 +564,7 @@
                                                     @if($patient->active_mcp_request_id)
                                                         {{-- kill mcp request for client --}}
                                                         <div moe relative class="">
-                                                            <button href="" start show class="on-hover-opaque"><i
+                                                            <button href="" start show class="col-2-button"><i
                                                                     class="fa fa-times text-danger"></i>&nbsp;MCP Req.
                                                             </button>
                                                             <form url="/api/mcpRequest/kill" class="mcp-theme-1" right>
@@ -582,7 +584,7 @@
                                                     @else
                                                         {{-- create mcp request for client --}}
                                                         <div moe relative class="">
-                                                            <button href="" start show><i
+                                                            <button href="" start show class="col-2-button"><i
                                                                     class="fa fa-plus text-sm text-secondary"></i>&nbsp;MCP
                                                                 Req.
                                                             </button>

+ 1 - 0
routes/web.php

@@ -184,6 +184,7 @@ Route::middleware('pro.auth')->group(function () {
         ->name('mc');
 
     // pro meeting
+    Route::get('/pro/check-video/{uid}', 'PracticeManagementController@checkVideo');
     Route::get('/pro/meet/{uid?}', 'PracticeManagementController@meet');
     Route::post('/pro/meet/get-participant-info', 'PracticeManagementController@getParticipantInfo');
     Route::get('/pro/get-opentok-session-key/{uid}', 'PracticeManagementController@getOpentokSessionKey');