Kaynağa Gözat

Integrate video-call page into RHS

Vijayakrishnan 5 yıl önce
ebeveyn
işleme
db8d22ab05

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

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

+ 250 - 0
public/css/meeting.css

@@ -0,0 +1,250 @@
+html, body {
+    background-color: #fff;
+    color: #636b6f;
+    font-family: 'Nunito', sans-serif;
+    font-weight: 200;
+    height: 100vh;
+    margin: 0;
+}
+
+h1 {
+    color: #555;
+    width: 50%;
+    text-align: center;
+}
+
+@media (max-width: 900px) {
+    h1 {
+        width: auto;
+        text-align: center;
+        font-size: 2rem;
+        text-shadow: 1px 1px 2px #c0e4e2;
+    }
+}
+
+@media (max-width: 400px) {
+    h1 {
+        font-size: 1.75rem;
+        text-shadow: 1px 1px 2px #c0e4e2;
+    }
+}
+
+.form-container {
+    width: 50%;
+    margin-right: auto;
+}
+
+@media (max-width: 900px) {
+    .form-container {
+        margin: auto;
+        width: 100%;
+    }
+}
+
+.form-container .form {
+    background: #ffffffd1;
+    max-width: 400px;
+    margin: auto;
+    border-radius: 4px;
+}
+
+.form-container .form h3 {
+    font-size: 1.3rem;
+}
+
+.border-success {
+    border-color: #44bdad85 !important;
+}
+
+.text-dark {
+    color: #555;
+    font-weight: 600;
+}
+
+.form-container .form input {
+    border-color: #44bdad85 !important;
+}
+
+.form-container .form input:focus {
+    box-shadow: 0 0 0 0.2rem #44bdad85;
+}
+
+.form-container .form input.has-error {
+    border-color: #bd424285 !important;
+}
+
+.form-container .form input.has-error:focus {
+    box-shadow: 0 0 0 0.2rem #bd444454;
+}
+
+.field-error {
+    font-size: 0.9rem;
+    font-weight: 600;
+    padding-left: 0.75rem;
+    margin-top: 0.25rem;
+}
+
+.main-view {
+    width: 800px;
+    height: 600px;
+    max-width: 100%;
+    max-height: 100%;
+    margin: 0 1rem;
+    position: relative;
+    background: #000;
+}
+
+.main-view .full-view {
+    width: 100%;
+    height: 100%;
+    background: #444;
+    border-radius: 3px;
+    margin: 0 auto;
+}
+
+.main-view .full-view[data-name]::after {
+    position: absolute;
+    content: attr(data-name);
+    left: 0;
+    width: 100%;
+    top: 0;
+    background: rgba(0, 0, 0, 0.5);
+    color: #fff;
+    font-size: 13px;
+    white-space: nowrap;
+    padding: 0.5rem;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    pointer-events: none;
+}
+
+.main-view .thumbs {
+    position: absolute;
+    z-index: 2;
+    bottom: 1.5rem;
+    right: 1rem;
+    width: 180px;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-end;
+    justify-content: flex-end;
+}
+.main-view .thumbs .thumb-view {
+    width: 100px;
+    height: 75px;
+    border: 1px solid #0d5875;
+    box-shadow: 0 0 3px #888;
+    border-radius: 3px;
+    background: #444;
+    margin-top: 1rem;
+    position: relative;
+    cursor: pointer;
+}
+.main-view .thumbs .thumb-view>* {
+    pointer-events: none;
+}
+.main-view .thumbs .thumb-view[data-name]::after {
+    position: absolute;
+    content: attr(data-name);
+    left: 0;
+    width: 100%;
+    bottom: 0;
+    background: rgba(0, 0, 0, 0.5);
+    color: #fff;
+    font-size: 11px;
+    white-space: nowrap;
+    padding: 0 5px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+.main-view .thumbs .disconnected-view {
+    opacity: 0;
+    height: 0;
+}
+.main-view .not-publishing {
+    background: grey;
+}
+.main-view .not-publishing * {
+    opacity: 0;
+}
+
+.tp-bar {
+    width: 120px;
+}
+.tp-bar .tp-item {
+    padding-top: 1rem;
+    padding-bottom: 0.5rem;
+    border: 0.3rem solid transparent;
+}
+
+.tp-bar .tp-item.active {
+    border: 0.3rem solid #44bdad85;
+}
+
+.avatar {
+    margin: auto;
+    width: 80px;
+    height: 80px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 100%;
+    font-size: 2rem;
+    font-weight: 600;
+}
+.invite-image {
+    width: 80px;
+    margin: auto;
+    display: block;
+}
+
+.text-single {
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    overflow: hidden;
+}
+
+.btn[disabled] {
+    opacity: 0.3;
+}
+body .OT_fit-mode-cover .OT_video-element {
+    object-fit: contain;
+}
+.hang-up {
+    position: absolute;
+    z-index: 2;
+    bottom: 1rem;
+    left: 1rem;
+    width: 50px;
+    height: 50px;
+    font-size: 120%;
+}
+.call-mcp {
+    position: absolute;
+    z-index: 2;
+    bottom: 1rem;
+    left: calc(2rem + 50px);
+    width: 50px;
+    height: 50px;
+    font-size: 120%;
+}
+
+.patient-in-q-alert {
+    -webkit-animation: blink 1s linear infinite;
+    -moz-animation: blink 1s linear infinite;
+    animation: blink 1s linear infinite;
+    opacity: 0;
+}
+@-moz-keyframes blink {
+    50% { opacity: 1; }
+    100% { opacity: 0; }
+}
+@-webkit-keyframes blink {
+    50% { opacity: 1; }
+    100% { opacity: 0; }
+}
+@keyframes blink {
+    50% { opacity: 1; }
+    100% { opacity: 0; }
+}

+ 484 - 0
resources/views/app/video/call.blade.php

@@ -0,0 +1,484 @@
+<!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">
+    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
+    <script src="https://static.opentok.com/v2/js/opentok.js"></script>
+</head>
+
+<body class="p-0 m-0">
+
+    <div id="proCallComponent">
+
+        <div class="d-flex align-items-center justify-content-center py-3 border-bottom">
+            <span class="mr-3">
+                {{ $pro->name_display }} | PRO
+            </span>
+            <button class="btn btn-sm btn-primary px-4 font-weight-bold"
+                    v-on:click.prevent="nextPatient()"
+                    :disabled="client || checkingForNextPatient || started">Next Patient</button>
+            <span v-if="patientInQueue && !started" class="patient-in-q-alert text-warning text-sm ml-2 small">
+                <i class="fa fa-circle"></i>
+            </span>
+            <span v-if="!patientInQueue && !started" class="text-success text-sm ml-2 small">
+                <i class="fa fa-circle"></i>
+            </span>
+        </div>
+        <div v-if="!started && noNextPatient" class="bg-light rounded text-center py-1 font-weight-bold text-sm my-3 mx-3">@{{ noNextPatient }}</div>
+
+        <div class="" v-show="videoActive">
+            <div class="py-3 text-center" v-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="noOneElseInCall">
+                <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-name="{{ $pro->name_display }}" data-type="PRO"></div>
+                <div class="thumbs">
+
+                </div>
+                <button class="btn btn-danger rounded-circle hang-up"
+                        v-if="started"
+                        title="Leave Call"
+                        v-on:click.prevent="hangUp()">
+                    <i class="fa fa-phone"></i>
+                </button>
+            </div>
+        </div>
+
+    </div>
+
+    <script>
+
+        new Vue({
+            el: '#proCallComponent',
+            delimiters: ['@{{', '}}'],
+            data: {
+                time: 0,
+                startTime: 0,
+                started: false,
+                client: false,
+                pro: false,
+
+                selfName: '',
+                selfToken: '',
+
+                clientUid: '',
+
+                otSessionId: '',
+
+                checkingForNextPatient: false,
+                noNextPatient: false,
+
+                otSession: false,
+
+                selfUserType: 'PRO',
+                selfStreamId: '',
+                noOneElseInCall: true,
+                patientInQueue: false,
+
+                videoActive: false,
+
+                heartbeatTimer: false,
+            },
+            methods: {
+                pollForNextPatient: function() {
+                    if(!this.started) {
+                        this.nextPatient(true);
+                    }
+                },
+                nextPatient: function(_pollOnly = false) {
+                    var self = this;
+                    if(!_pollOnly) this.checkingForNextPatient = true;
+                    $.post('/api/client/getNextClientForVideoVisit', {}, function(_data) {
+                        if(_pollOnly) {
+                            self.patientInQueue = _data.success;
+                        }
+                        else {
+                            self.checkingForNextPatient = false;
+                            if(!_data.success) {
+                                self.noNextPatient = _data.message;
+                                window.setTimeout(function() {
+                                    self.noNextPatient = false;
+                                }, 2000);
+                            }
+                            else {
+                                // get ot session key from client record
+                                self.client = true;
+                                self.clientUid = _data.data;
+                                self.videoActive = true;
+                                self.startOpenTokSession();
+                            }
+                        }
+                    }, 'json');
+                },
+                startOpenTokSession: function() {
+                    var self = this;
+                    self.getOpenTokSessionId(function() {
+                        self.selfName = '{{ $pro->name_display  }}';
+                        $.post('/api/openTok/getClientToken', {
+                            opentokSessionId: self.otSessionId,
+                            data: JSON.stringify({
+                                uid: '{{ $pro->uid  }}',
+                                name: self.selfName,
+                                type: 'PRO'
+                            })
+                        }, function (_data) {
+                            console.log(_data);
+                            self.selfToken = _data.data;
+                            self.initOpenTok();
+                        });
+                    });
+                },
+                timeDisplay: function() {
+                    var seconds = this.time / 1000,
+                        minutes = parseInt(seconds / 60, 10);
+                    seconds = parseInt(seconds % 60, 10);
+                    return minutes + " min, " + seconds + " sec";
+                },
+                hangUp: function() {
+                    if(this.otSession) {
+                        try {
+                            this.otSession.disconnect();
+                        }
+                        catch (e) {
+                            console.log('Was already disconnected.');
+                        }
+                        this.otSession = false;
+                        this.otSessionId = '';
+                        this.started = false;
+                        this.startTime = false;
+                        this.videoActive = false;
+                        // this.client = false;
+                    }
+                },
+                initOpenTok: function() {
+
+                    /* fake video feed (temp) */
+                    const randomColour = () => {
+                        return Math.round(Math.random() * 255);
+                    };
+
+                    const canvas = document.createElement('canvas');
+                    canvas.width = 640;
+                    canvas.height = 480;
+                    const ctx = canvas.getContext('2d');
+                    var pos = 100;
+                    window.setInterval(function() {
+                        ctx.clearRect(0, 0, canvas.width, canvas.height);
+                        ctx.font = "20px Georgia";
+                        ctx.fillStyle = `rgb(220, 220, 220)`;
+                        ctx.fillText("Video feed from {{ $pro->name_display }}", 20, pos);
+                        pos += 5;
+                        if(pos > canvas.height) pos = 100;
+                    }, 1000);
+
+                    var self = this;
+
+                    var apiKey = '<?= env('TOKBOX_API_KEY', '46678902') ?>';
+                    var sessionId = this.otSessionId;
+                    var token = this.selfToken;
+
+                    // destroy if existing
+                    // self.hangUp();
+
+                    self.otSession = OT.initSession(apiKey, sessionId);
+
+                    // peer connected
+                    self.otSession.on('streamCreated', function streamCreated(event) {
+                        console.log('streamCreated', arguments);
+                        var subscriberOptions = {
+                            insertMode: 'append',
+                            width: '100%',
+                            height: '100%'
+                        };
+
+                        var connectionData = JSON.parse(event.stream.connection.data);
+
+                        // add a div for remove view
+                        var remoteViewID = 'remote-view-' + event.stream.id;
+                        var remoteElem = $('<div id="' + remoteViewID + '" class="remote-view thumb-view" ' +
+                            'data-stream="' + event.stream.id + '" ' +
+                            'data-connection-data="' + event.stream.connection.data + '" ' +
+                            'data-name="' + connectionData.name + '" ' +
+                            'data-type="' + connectionData.type + '"></div>');
+                        remoteElem.appendTo('.thumbs');
+
+                        self.otSession.subscribe(event.stream, remoteViewID, subscriberOptions, self.handleOpenTokError);
+
+                        if (connectionData.type === 'CLIENT') {
+                            self.client = true;
+                        }
+
+                        if(!self.startTime) {
+                            self.startTime = new Date().getTime();
+                            window.setInterval(function() {
+                                self.time = new Date().getTime() - self.startTime;
+                            }, 1000);
+                            self.started = true;
+                        }
+
+                        self.activateParty(event.stream.id);
+
+                        self.noOneElseInCall = false;
+                    });
+
+                    // peer disconnected
+                    self.otSession.on("streamDestroyed", function(event) {
+                        onPeerDisconnection(event, event.stream.connection.data);
+                    });
+                    // self.otSession.on("connectionDestroyed", function(event) {
+                    //     debugger;
+                    //     console.log('connectionDestroyed from ' + event.connection.data);
+                    //     onPeerDisconnection(event, event.connection.data);
+                    // });
+
+                    self.otSession.on("connectionCreated", function(event) {
+                        console.log('connectionCreated');
+                        console.log(event);
+                    });
+
+                    function onPeerDisconnection(event, data) {
+
+                        if(event.stream && $('.full-view[data-stream="' + event.stream.id + '"]').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') !== event.stream.id) {
+                                        self.activateParty($(this).attr('data-stream'));
+                                        return false;
+                                    }
+                                });
+                            }
+                            else {
+                                self.noOneElseInCall = true;
+                            }
+                        }
+
+                        if(event.stream) {
+                            var remoteViewElem = $('[data-stream="' + event.stream.id + '"]');
+                            remoteViewElem.remove();
+                            // if(remoteViewElem.length) {
+                            //     remoteViewElem.attr('data-stream', '');
+                            //     remoteViewElem.attr('data-connection-data', '');
+                            //     remoteViewElem.attr('data-type', '');
+                            //     remoteViewElem.attr('data-name', '');
+                            // }
+                            // remoteViewElem.addClass('disconnected-view')
+                        }
+
+                        var connectionData = JSON.parse(data);
+                        if(connectionData.type === 'CLIENT') {
+                            self.client = false;
+                        }
+
+                        // if no other parties in call, hang up
+                        if(!$('[data-stream]:not([data-stream="' + self.selfStreamId + '"])').length) {
+                            console.warn('No other parties in the call!');
+                            self.startTime = 0;
+                            self.started = false;
+                            self.noOneElseInCall = true;
+                        }
+                    }
+
+                    // self connected
+                    self.otSession.on("sessionConnected", function(event) {
+                        self.joinMeetingAsPro(self.selfUserType);
+                    });
+
+                    // self disconnected
+                    self.otSession.on('sessionDisconnected', function sessionDisconnected(event) {
+                        console.log('You were disconnected from the session.', event.reason);
+
+                        // turn pro video off
+                        $.post('/api/clientVideoVisit/turnProVideoOff', {}, function(_data) {
+                            console.log(_data);
+
+                            // stop heart beat
+                            if(self.heartbeatTimer) {
+                                window.clearInterval(self.heartbeatTimer);
+                                self.heartbeatTimer = false;
+                            }
+                        });
+                    });
+
+                    // initialize the publisher
+                    var publisherOptions = {
+                        videoSource: canvas.captureStream(1).getVideoTracks()[0], // TODO: Comment this line to use webcam
+                        insertMode: 'append',
+                        width: '100%',
+                        height: '100%',
+                    };
+                    var publisher = OT.initPublisher('self-view', publisherOptions, self.handleOpenTokError);
+
+                    publisher.on('streamCreated', function(event) {
+                        var selfView = $('#self-view');
+                        selfView.attr('data-stream', event.stream.id);
+                        selfView.attr('data-connection-data', event.stream.connection.data);
+                        self.selfStreamId = event.stream.id;
+                    });
+
+                    publisher.on('streamCreated', function(event) {
+                        console.log('publisher->streamCreated');
+                        var selfView = $('#self-view');
+                        selfView.attr('data-stream', event.stream.id);
+                        selfView.attr('data-connection-data', event.stream.connection.data);
+                        selfView.attr('data-type', 'PRO');
+                        self.activateParty('self');
+                        $('#self-view').show();
+
+                        // turn pro video on
+                        $.post('/api/clientVideoVisit/turnProVideoOn', {}, function(_data) {
+                            console.log(_data);
+
+                            // start heart beat
+                            self.heartbeatTimer = window.setInterval(function() {
+                                $.post('/api/clientVideoVisit/registerProMeetingHeartbeat', {}, function(_data) {
+                                    console.log(_data);
+                                });
+                            }, 5000);
+                        });
+                    });
+
+                    publisher.on('streamDestroyed', function(event) {
+                        event.preventDefault();
+                        console.log('publisher->streamDestroyed');
+                        $('#self-view').hide();
+                        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') !== $('#self-view').attr('data-stream')) {
+                                    self.activateParty($(this).attr('data-stream'));
+                                    return false;
+                                }
+                            });
+                        }
+                        else {
+                            self.hangUp();
+                        }
+
+                        // turn pro video off
+                        $.post('/api/clientVideoVisit/turnProVideoOff', {}, function(_data) {
+                            console.log(_data);
+
+                            // stop heart beat
+                            if(self.heartbeatTimer) {
+                                window.clearInterval(self.heartbeatTimer);
+                                self.heartbeatTimer = false;
+                            }
+                        });
+                    });
+
+
+                    // Connect to the session
+                    self.otSession.connect(token, function callback(error) {
+                        if (error) {
+                            self.handleOpenTokError(error);
+                        } else {
+                            // If the connection is successful, publish the publisher to the session
+                            self.otSession.publish(publisher, self.handleOpenTokError);
+                        }
+                    });
+                },
+                handleOpenTokError: function(e) {
+
+                },
+
+                getOpenTokSessionId: function(_done) {
+                    var self = this;
+                    $.get('/pro/get-opentok-session-key/' + self.clientUid, function(_data) {
+                        self.otSessionId = _data.data;
+                        console.log(_data);
+                        _done();
+                    }, 'json');
+                },
+
+                joinMeetingAsPro: function(_type) {
+                    var self = this;
+                    $.ajax({
+                        type: 'post',
+                        url: '/api/clientVideoVisit/joinVideoVisitAsPro',
+                        headers: {
+                            'sessionKey': '{{ request()->cookie('sessionKey') }}'
+                        },
+                        data: {uid: self.clientUid},
+                        dataType: 'json'
+                    })
+                        .done(function (_data) {
+                            console.log(_data);
+                        })
+                        .fail(function (_data) {
+                            console.warn(_data);
+                            alert(_data.message);
+                        });
+                },
+
+                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');
+                    }
+                }
+
+            },
+            mounted: function() {
+
+                var self = this;
+
+                $(document).on('click', '.thumbs>div[data-stream]', function() {
+                    self.activateParty($(this).attr('data-stream'));
+                    return false;
+                });
+
+                // poll for new patients and alert
+                window.setInterval(function() {
+                    self.pollForNextPatient();
+                }, 5000);
+
+                window.onbeforeunload = function() {
+                    if(self.started) {
+                        return "A call is in progress";
+                    }
+                };
+
+                @if(isset($client))
+                    self.client = true;
+                self.clientUid = '{{ $client->uid }}';
+                self.videoActive = true;
+                self.startOpenTokSession();
+                @endif
+            }
+        });
+    </script>
+
+</body>
+</html>