Vijayakrishnan преди 4 години
родител
ревизия
417ebae4b1
променени са 3 файла, в които са добавени 614 реда и са изтрити 1 реда
  1. 1 1
      app/Http/Controllers/PracticeManagementController.php
  2. 2 0
      config/app.php
  3. 611 0
      resources/views/app/video/call-agora-v2.blade.php

+ 1 - 1
app/Http/Controllers/PracticeManagementController.php

@@ -254,7 +254,7 @@ class PracticeManagementController extends Controller
     public function meet(Request $request, $uid = false) {
         $session = AppSession::where('session_key', $request->cookie('sessionKey'))->first();
         $client = !empty($uid) ? Client::where('uid', $uid)->first() : null;
-        return view('app.video.call', compact('session', 'client'));
+        return view('app.video.call-agora-v2', compact('session', 'client'));
     }
 
     public function getParticipantInfo(Request $request) {

+ 2 - 0
config/app.php

@@ -59,6 +59,8 @@ return [
     'agora_appid' => env('AGORA_APPID'),
     'agora_mode' => env('AGORA_MODE'),
 
+    'backend_ws_url' => env('BACKEND_WS_URL'),
+
     /*
     |--------------------------------------------------------------------------
     | Application Timezone

+ 611 - 0
resources/views/app/video/call-agora-v2.blade.php

@@ -0,0 +1,611 @@
+<!DOCTYPE html>
+<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <link href="https://fonts.googleapis.com/css?family=Nunito:200,600,700" rel="stylesheet">
+    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
+    <link href="/css/app.css" rel="stylesheet">
+    <link rel="stylesheet" href="/fontawesome-free/css/all.min.css">
+    <link href="/css/meeting.css" rel="stylesheet">
+    <link href="/css/style.css" rel="stylesheet">
+    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.0/sockjs.min.js"
+            integrity="sha512-5yJ548VSnLflcRxWNqVWYeQZnby8D8fJTmYRLyvs445j1XmzR8cnWi85lcHx3CUEeAX+GrK3TqTfzOO6LKDpdw=="
+            crossorigin="anonymous"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"
+            integrity="sha512-iKDtgDyTHjAitUDdLljGhenhPwrbBfqTKWO1mkhSFH3A7blITC9MhYon6SjnMhp4o0rADGw9yAC6EW4t5a4K3g=="
+            crossorigin="anonymous"></script>
+    <script defer src=//download.agora.io/sdk/web/AgoraRTC_N-4.1.0.js></script>
+</head>
+
+<body class="p-0 m-0">
+
+    <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>
+            <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>
+            </div>
+            <div class="main-view mx-auto">
+                <div id="self-view" class="full-view" data-stream="self" data-name="You" data-type="PRO"></div>
+                <div class="thumbs">
+
+                </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>
+
+    <div class="border-top patient-queue mcp-theme-1" id="queueComponent">
+        <div class="bg-secondary text-white font-weight-bold text-center py-1" v-if="items.length > 0">
+            @{{ items.length }} patient@{{ items.length > 1 ? 's' : '' }} in the queue
+        </div>
+        <div class="bg-secondary text-white font-weight-bold text-center py-1" v-if="items.length === 0">
+            No patients in the queue
+        </div>
+        <div v-if="items && items.length" class="d-flex align-items-center my-1">
+            <div v-for="item in items">
+                <div class="queue-item border border-primary rounded mx-1" :title="item.name">
+                    <div class="patient-avatar mb-1 text-dark">@{{ item.initials }}</div>
+                    <div class="font-weight-bold small text-nowrap text-ellipsis">@{{ item.name }}</div>
+                </div>
+                <button class="btn btn-sm btn-primary mt-1 text-white font-weight-bold py-0 mx-auto d-block"
+                        v-on:click.prevent="claim(item.clientUid)">Claim</button>
+            </div>
+        </div>
+    </div>
+
+    <script>
+        (function () {
+            new Vue({
+                el: '#proCallComponent',
+                delimiters: ['@{{', '}}'],
+                data: {
+
+                    // model - declare up-front to make reactive - override with server data on mount
+                    amIInAMeeting: false,
+                    meetingType: '', // PRO/CLIENT,
+                    inMeetingForClientUid: '',
+                    inMeetingForClient: {
+                        clientMediaServiceRoomIdentifier: '',
+                        uid: '',
+                        displayName: '',
+                        dob: '',
+                    },
+                    myMediaServiceToken: '',
+                    myMediaServiceIdentifier: '',
+                    myMedia: {
+                        isCameraAcquired: false,
+                        isCameraOn: false,
+                        isMicrophoneAcquired: false,
+                        isMicrophoneOn: false,
+                    },
+                    otherParticipants: [
+                        {
+                            participantType: '', // PRO/CLIENT_GUEST,
+                            uid: '',
+                            mediaServiceIdentifier: '',
+                            displayName: '',
+                            media: {
+                                isCameraAcquired: false,
+                                isCameraOn: false,
+                                isMicrophoneAcquired: false,
+                                isMicrophoneOn: false,
+                            },
+                            awayMessage: '',
+                            deviceType: '',
+                            isMeetingAccessGranted: '',
+                            isSocketConnected: '',
+                        }
+                    ],
+                    awayMessage: '',
+
+                    // agora
+                    agoraClient: null, // set on agora init
+                    appId: '{{ config('app.agora_appid') }}',
+                    channel: '', // set on mount
+                    ringer: {{ $pro->is_ring_on ? 'true' : 'false' }},
+
+                    // sockets
+                    backendWsURL: '{{ config('app.backend_ws_url') }}',
+                    socketClient: null,
+
+                },
+                methods: {
+
+                    toggleRinger: function () {
+                        let self = this, endPoint = this.ringer ? 'turnOffRing' : 'turnOnRing';
+                        $.post('/api/pro/' + endPoint, function (_data) {
+                            if (_data && _data.success) {
+                                self.ringer = !self.ringer;
+                            } else {
+                                if (_data.message) {
+                                    toastr.error(_data.message);
+                                } else {
+                                    toastr.error('Unable to change ringer status');
+                                }
+                            }
+                        }, 'json');
+                    },
+                    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;
+                        current.removeClass('full-view').addClass('thumb-view');
+                        if (current.attr('data-type') === 'CLIENT') {
+                            current.prependTo('.thumbs');
+                        } else {
+                            current.appendTo('.thumbs');
+                        }
+                        if (_stream === 'self') {
+                            $('#self-view')
+                                .removeClass('thumb-view')
+                                .removeClass('disconnected-view')
+                                .addClass('full-view')
+                                .prependTo('.main-view');
+                        } else {
+                            $('div[data-stream="' + _stream + '"]')
+                                .removeClass('thumb-view')
+                                .removeClass('disconnected-view')
+                                .addClass('full-view')
+                                .prependTo('.main-view');
+                        }
+                    },
+
+                    enterClientRoomAsPro: function(_done) {
+                        @if($client)
+                        $.post('/api/meeting/enterClientRoomAsPro', {clientUid: '{{ $client->uid }}'}, (_data) => {
+                            // TODO: check success
+                            console.log(_data);
+                            _done.call(this);
+                        });
+                        @endif
+                    },
+                    getMeetingInfo: function(_done) {
+                        $.post('/api/meeting/getMyMeeting', (_data) => {
+                            if(_data && _data.success) {
+                                let state = JSON.parse(_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;
+                                this.myMediaServiceToken = state.myMediaServiceToken;
+                                this.myMediaServiceIdentifier = state.myMediaServiceIdentifier;
+                                this.otherParticipants = state.otherParticipants;
+
+                                // agora stuff
+                                this.channel = this.inMeetingForClient.clientMediaServiceRoomIdentifier;
+
+                                _done.call(this);
+                            }
+                        });
+                    },
+                    registerSocket: function() {
+                        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}}'
+                                })
+                            );
+                        });
+                    },
+                    initSocketListeners: function() {
+
+                        this.socketClient.subscribe("/user/topic/newParticipant", function(message) {
+                            console.log("newParticipant received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myMicrophoneIsAcquired", function(message) {
+                            console.log("myMicrophoneIsAcquired received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myMicrophoneIsNotAcquired", function(message) {
+                            console.log("myMicrophoneIsNotAcquired received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myMicrophoneIsOn", function(message) {
+                            console.log("myMicrophoneIsOn received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myMicrophoneIsOff", function(message) {
+                            console.log("myMicrophoneIsOff received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myCameraIsAcquired", function(message) {
+                            console.log("myCameraIsAcquired received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myCameraIsNotAcquired", function(message) {
+                            console.log("myCameraIsNotAcquired received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myCameraIsOn", function(message) {
+                            console.log("myCameraIsOn received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myCameraIsOff", function(message) {
+                            console.log("myCameraIsOff received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/editMyName", function(message) {
+                            console.log("editMyName received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/setMyAwayMessage", function(message) {
+                            console.log("setMyAwayMessage received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/removeMyAwayMessage", function(message) {
+                            console.log("removeMyAwayMessage received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/leaveClientRoom", function(message) {
+                            console.log("leaveClientRoom received:", message.body);
+                        });
+
+                    },
+
+                    // start: actions that notify participants via socket
+                    myMicrophoneIsAcquired: function () {
+                        self.socketClient.send("/app/myMicrophoneIsAcquired", {},
+                            JSON.stringify({
+                                sessionKey: '{{$performer->session_key}}'
+                            })
+                        );
+                    },
+                    myMicrophoneIsNotAcquired: function () {
+                        self.socketClient.send("/app/myMicrophoneIsNotAcquired", {},
+                            JSON.stringify({
+                                sessionKey: '{{$performer->session_key}}'
+                            })
+                        );
+                    },
+                    myMicrophoneIsOn: function () {
+                        self.socketClient.send("/app/myMicrophoneIsOn", {},
+                            JSON.stringify({
+                                sessionKey: '{{$performer->session_key}}'
+                            })
+                        );
+                    },
+                    myMicrophoneIsOff: function () {
+                        self.socketClient.send("/app/myMicrophoneIsOff", {},
+                            JSON.stringify({
+                                sessionKey: '{{$performer->session_key}}'
+                            })
+                        );
+                    },
+                    myCameraIsAcquired: function () {
+                        self.socketClient.send("/app/myCameraIsAcquired", {},
+                            JSON.stringify({
+                                sessionKey: '{{$performer->session_key}}'
+                            })
+                        );
+                    },
+                    myCameraIsNotAcquired: function () {
+                        self.socketClient.send("/app/myCameraIsNotAcquired", {},
+                            JSON.stringify({
+                                sessionKey: '{{$performer->session_key}}'
+                            })
+                        );
+                    },
+                    myCameraIsOn: function () {
+                        self.socketClient.send("/app/myCameraIsOn", {},
+                            JSON.stringify({
+                                sessionKey: '{{$performer->session_key}}'
+                            })
+                        );
+                    },
+                    myCameraIsOff: function () {
+                        self.socketClient.send("/app/myCameraIsOff", {},
+                            JSON.stringify({
+                                sessionKey: '{{$performer->session_key}}'
+                            })
+                        );
+                    },
+                    editMyName: function (_myNewName) {
+                        self.socketClient.send("/app/editMyName", {},
+                            JSON.stringify({
+                                sessionKey: '{{$performer->session_key}}',
+                                myNewName: _myNewName
+                            })
+                        );
+                    },
+                    setMyAwayMessage: function (_message) {
+                        self.socketClient.send("/app/setMyAwayMessage", {},
+                            JSON.stringify({
+                                sessionKey: '{{$performer->session_key}}',
+                                message: _message
+                            })
+                        );
+                    },
+                    removeMyAwayMessage: function () {
+                        self.socketClient.send("/app/removeMyAwayMessage", {},
+                            JSON.stringify({
+                                sessionKey: '{{$performer->session_key}}'
+                            })
+                        );
+                    },
+                    leaveClientRoom: function () {
+                        self.socketClient.send("/app/leaveClientRoom", {},
+                            JSON.stringify({
+                                sessionKey: '{{$performer->session_key}}'
+                            })
+                        );
+                    },
+                    // 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--}}
+                }
+            });
+            new Vue({
+                el: '#queueComponent',
+                data: {
+                    items: []
+                },
+                mounted: function() {
+                    let self = this;
+                    this.refresh();
+                    window.setInterval(function() {
+                        self.refresh();
+                    }, 15000); // once in 15 seconds
+                },
+                methods: {
+                    refresh: function() {
+                        let self = this;
+                        $.get('/patients-in-queue', function(_data) {
+                            self.items = _data;
+                        }, 'json');
+                    },
+                    claim: function(_uid) {
+                        $.post('/api/mcpRequest/claim', {clientUid: _uid}, function(_data) {
+                            if(_data && _data.success) {
+                                // open patient in LHS
+                                window.top.openInLHS('/patients/view/' + _uid);
+                                // open patient video in RHS
+                                window.top.openInRHS('/pro/meet/' + _uid);
+                            }
+                            else {
+                                if (_data.message) {
+                                    window.top.toastr.error(_data.message);
+                                } else {
+                                    window.top.toastr.error('Unable to claim the patient');
+                                }
+                            }
+                        }, 'json');
+                    }
+                }
+            })
+        })();
+    </script>
+    <!--
+    <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>