meet.blade.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. <?php /** @var $guest */ ?>
  2. @extends('layouts.meeting')
  3. @section('content')
  4. <div id="meetComponent">
  5. {{--<h5 class="bg-dark font-weight-bold text-white m-0 py-3 px-4 d-flex">
  6. <span>Meeting</span>
  7. <span class="ml-auto" v-if="!started">
  8. Connecting...
  9. </span>
  10. <span class="ml-auto" v-if="started">
  11. <i class="fa fa-clock mr-1 text-light"></i>
  12. @{{ timeDisplay() }}
  13. </span>
  14. </h5>--}}
  15. @if(!$guest)
  16. <div class="d-flex align-items-center justify-content-center py-3 border-bottom">
  17. <span class="mr-3">
  18. {{ $pro->name_display }} | {{ $pro->is_video_visit_assistant ? 'ASSISTANT' : 'MCP' }}
  19. </span>
  20. <button class="btn btn-sm btn-primary px-4 font-weight-bold"
  21. v-on:click.prevent="nextPatient()"
  22. :disabled="client || checkingForNextPatient || started">Next Patient</button>
  23. <span v-if="patientInQueue && !started" class="patient-in-q-alert text-warning text-sm ml-2 small">
  24. <i class="fa fa-circle"></i>
  25. </span>
  26. <span v-if="!patientInQueue && !started" class="text-success text-sm ml-2 small">
  27. <i class="fa fa-circle"></i>
  28. </span>
  29. </div>
  30. <div v-if="!started && noNextPatient" class="bg-light rounded text-center py-1 font-weight-bold text-sm my-3 mx-3">@{{ noNextPatient }}</div>
  31. @endif
  32. <div class="">
  33. <div class="py-3 text-center" v-if="started">
  34. <h6 class="text-black font-weight-bold m-0">Call in progress: @{{ timeDisplay() }}</h6>
  35. </div>
  36. @if($guest)
  37. <div class="py-3 text-center" v-if="!pro || !started">
  38. <h6 class="text-black font-weight-bold m-0">Please wait. Your doctor will be with you shortly...</h6>
  39. </div>
  40. @endif
  41. <div class="main-view mx-auto" <?= !$guest ? 'v-show="!!client"' : '' ?>>
  42. <div id="self-view" class="full-view" data-type="{{ $guest ? 'CLIENT' : 'PRO' }}"></div>
  43. <div class="thumbs">
  44. <div id="remote-view-1" class="remote-view disconnected-view"
  45. data-stream="" data-from="" data-type=""></div>
  46. <div id="remote-view-2" class="remote-view disconnected-view"
  47. data-stream="" data-from="" data-type=""></div>
  48. </div>
  49. <button class="btn btn-danger rounded-circle hang-up"
  50. v-if="started"
  51. title="Leave Call"
  52. v-on:click.prevent="hangUp()">
  53. <i class="fa fa-phone"></i>
  54. </button>
  55. <button class="btn btn-success rounded-circle call-mcp"
  56. v-if="selfUserType === 'ASSISTANT'"
  57. title="Call MCP Pro"
  58. v-on:click.prevent="callMCPPro()">
  59. <i class="fa fa-user-md"></i>
  60. </button>
  61. </div>
  62. </div>
  63. </div>
  64. <script>
  65. new Vue({
  66. el: '#meetComponent',
  67. delimiters: ['@{{', '}}'],
  68. data: {
  69. time: 0,
  70. startTime: 0,
  71. started: false,
  72. client: false,
  73. pro: false,
  74. selfName: '',
  75. selfToken: '',
  76. @if($guest)
  77. clientUid: '',
  78. checkInToken: '',
  79. @endif
  80. otSessionId: '',
  81. @if(!$guest)
  82. checkingForNextPatient: false,
  83. noNextPatient: false,
  84. @endif
  85. otSession: false,
  86. selfUserType: false,
  87. patientInQueue: false,
  88. },
  89. methods: {
  90. @if(!$guest)
  91. pollForNextPatient: function() {
  92. if(!this.started) {
  93. this.nextPatient(true);
  94. }
  95. },
  96. nextPatient: function(_pollOnly = false) {
  97. var self = this;
  98. if(!_pollOnly) this.checkingForNextPatient = true;
  99. $.post('/api/client/getNextClientForVideoVisit', {}, function(_data) {
  100. if(_pollOnly) {
  101. self.patientInQueue = _data.success;
  102. }
  103. else {
  104. self.checkingForNextPatient = false;
  105. if(!_data.success) {
  106. self.noNextPatient = _data.message;
  107. window.setTimeout(function() {
  108. self.noNextPatient = false;
  109. }, 2000);
  110. }
  111. else {
  112. // get ot session key from client record
  113. self.client = true;
  114. self.clientUid = _data.data;
  115. self.getOpenTokSessionId(function() {
  116. self.selfName = '{{ $pro->name_display }}';
  117. $.post('/api/openTok/getClientToken', {
  118. opentokSessionId: self.otSessionId,
  119. name: self.selfName
  120. }, function (_data) {
  121. self.selfToken = _data.data;
  122. self.initOpenTok();
  123. });
  124. });
  125. }
  126. }
  127. }, 'json');
  128. },
  129. @endif
  130. getInitials: function(_name) {
  131. var parts = _name.split(/\s+/g);
  132. parts = parts.map(_part => _part[0]);
  133. return parts.join('');
  134. },
  135. timeDisplay: function() {
  136. var seconds = this.time / 1000,
  137. minutes = parseInt(seconds / 60, 10);
  138. seconds = parseInt(seconds % 60, 10);
  139. return minutes + " min, " + seconds + " sec";
  140. },
  141. hangUp: function() {
  142. if(this.otSession) {
  143. try {
  144. this.otSession.disconnect();
  145. }
  146. catch (e) {
  147. console.log('Was already disconnected.');
  148. }
  149. this.otSession = false;
  150. this.otSessionId = '';
  151. this.started = false;
  152. this.startTime = false;
  153. @if(!$guest)
  154. this.client = false;
  155. @else
  156. window.location = '/join';
  157. @endif
  158. }
  159. },
  160. @if(!$guest)
  161. callMCPPro: function() {
  162. // put client in mcp queue
  163. $.ajax({
  164. type: 'post',
  165. url: '/api/clientVideoVisit/putVideoVisitInMcpQueue',
  166. headers: {
  167. 'sessionKey': localStorage.sessionKey
  168. },
  169. data: {uid: this.clientUid},
  170. dataType: 'json'
  171. })
  172. .done(function (_data) {
  173. console.log(_data);
  174. if(_data.success) {
  175. new Noty({
  176. theme: 'mint',
  177. type: 'success',
  178. text: 'Client added to MCP call queue',
  179. progressBar: false,
  180. timeout: 1500,
  181. }).show();
  182. }
  183. else {
  184. new Noty({
  185. theme: 'mint',
  186. type: 'alert',
  187. text: _data.message,
  188. progressBar: false,
  189. timeout: 3000,
  190. }).show();
  191. }
  192. })
  193. .fail(function (_data) {
  194. console.log(_data);
  195. // alert(_data.message);
  196. });
  197. },
  198. @endif
  199. // OT methods
  200. initOpenTok: function() {
  201. /* fake video feed (temp) */
  202. const randomColour = () => {
  203. return Math.round(Math.random() * 255);
  204. };
  205. const canvas = document.createElement('canvas');
  206. canvas.width = 640;
  207. canvas.height = 480;
  208. const ctx = canvas.getContext('2d');
  209. var pos = 100;
  210. window.setInterval(function() {
  211. ctx.clearRect(0, 0, canvas.width, canvas.height);
  212. ctx.font = "20px Georgia";
  213. ctx.fillStyle = `rgb(220, 220, 220)`;
  214. var userType = '<?= $guest? "Client" : "Pro" ?>';
  215. ctx.fillText("Video feed from " + userType, 20, pos);
  216. pos += 5;
  217. if(pos > canvas.height) pos = 100;
  218. }, 1000);
  219. var self = this;
  220. var apiKey = '<?= env('TOKBOX_API_KEY', '46678902') ?>';
  221. var sessionId = this.otSessionId;
  222. var token = this.selfToken;
  223. // destroy if existing
  224. self.hangUp();
  225. self.otSession = OT.initSession(apiKey, sessionId);
  226. // peer connected
  227. self.otSession.on('streamCreated', function streamCreated(event) {
  228. var subscriberOptions = {
  229. insertMode: 'append',
  230. width: '100%',
  231. height: '100%'
  232. };
  233. var remoteViewElem = 'remote-view-1';
  234. if($('#remote-view-1').attr('data-stream')) {
  235. remoteViewElem = 'remote-view-2';
  236. }
  237. self.otSession.subscribe(event.stream, remoteViewElem, subscriberOptions, self.handleOpenTokError);
  238. remoteViewElem = $('#' + remoteViewElem);
  239. remoteViewElem.attr('data-stream', event.stream.id);
  240. remoteViewElem.attr('data-from', event.stream.connection.data.split('|')[0]);
  241. var userType = event.target.connection.data.split('|')[1];
  242. if(userType === 'CLIENT') {
  243. remoteViewElem.attr('data-type', 'CLIENT');
  244. }
  245. else {
  246. remoteViewElem.attr('data-type', 'PRO');
  247. }
  248. @if($guest)
  249. self.pro = true;
  250. @else
  251. self.client = true;
  252. @endif
  253. if(!self.startTime) {
  254. self.startTime = new Date().getTime();
  255. window.setInterval(function() {
  256. self.time = new Date().getTime() - self.startTime;
  257. }, 1000);
  258. self.started = true;
  259. }
  260. self.activateParty(event.stream.id);
  261. });
  262. // peer disconnected
  263. self.otSession.on("streamDestroyed", function(event) {
  264. console.log('streamDestroyed from ' + event.stream.connection.data);
  265. onPeerDisconnection(event, event.stream.connection.data);
  266. });
  267. // self.otSession.on("connectionDestroyed", function(event) {
  268. // debugger;
  269. // console.log('connectionDestroyed from ' + event.connection.data);
  270. // onPeerDisconnection(event, event.connection.data);
  271. // });
  272. function onPeerDisconnection(event, data) {
  273. if(event.stream && $('.full-view[data-stream="' + event.stream.id + '"]').length) {
  274. self.activateParty('self');
  275. }
  276. @if(!$guest)
  277. if(data.split('|')[1] === 'CLIENT') {
  278. self.hangUp();
  279. }
  280. @endif
  281. if(event.stream) {
  282. var remoteViewElem = $('[data-stream="' + event.stream.id + '"]');
  283. if(remoteViewElem.length) {
  284. remoteViewElem.attr('data-stream', '');
  285. remoteViewElem.attr('data-from', '');
  286. }
  287. remoteViewElem.addClass('disconnected-view')
  288. }
  289. @if($guest)
  290. self.pro = false;
  291. @else
  292. if(data.split('|')[1] === 'CLIENT') {
  293. self.client = false;
  294. }
  295. @endif
  296. // if no other parties in call, hang up
  297. if(!$('[data-stream]:not([data-stream=""])').length) {
  298. self.hangUp();
  299. }
  300. }
  301. // self connected
  302. self.otSession.on("sessionConnected", function(event) {
  303. console.log(event);
  304. self.selfUserType = event.target.connection.data.split('|')[1];
  305. @if(!$guest)
  306. self.joinMeetingAsPro(self.selfUserType);
  307. @endif
  308. });
  309. // self disconnected
  310. self.otSession.on('sessionDisconnected', function sessionDisconnected(event) {
  311. console.log('You were disconnected from the session.', event.reason);
  312. });
  313. // initialize the publisher
  314. var publisherOptions = {
  315. videoSource: canvas.captureStream(1).getVideoTracks()[0], // TODO: Comment this line to use webcam
  316. insertMode: 'append',
  317. width: '100%',
  318. height: '100%',
  319. };
  320. var publisher = OT.initPublisher('self-view', publisherOptions, self.handleOpenTokError);
  321. publisher.on('streamCreated', function(event) {
  322. var selfView = $('#self-view');
  323. selfView.attr('data-stream', event.stream.id);
  324. selfView.attr('data-from', event.stream.connection.data.split('|')[0]);
  325. @if($guest)
  326. selfView.attr('data-type', 'CLIENT');
  327. @else
  328. selfView.attr('data-type', 'PRO');
  329. @endif
  330. });
  331. // Connect to the session
  332. self.otSession.connect(token, function callback(error) {
  333. if (error) {
  334. self.handleOpenTokError(error);
  335. } else {
  336. // If the connection is successful, publish the publisher to the session
  337. self.otSession.publish(publisher, self.handleOpenTokError);
  338. }
  339. });
  340. },
  341. handleOpenTokError: function(e) {
  342. },
  343. getClientCheckinToken: function(_done) {
  344. var self = this;
  345. $.get('/get-client-checkin-token/' + this.clientUid, function(_data) {
  346. console.log(_data);
  347. self.checkInToken = _data.data;
  348. _done();
  349. }, 'json');
  350. },
  351. getOpenTokSessionId: function(_done) {
  352. var self = this;
  353. @if($guest)
  354. $.ajax({
  355. type: 'post',
  356. url: '/api/clientVideoVisit/startVideoVisitAsClient',
  357. headers: {
  358. 'sessionKey': localStorage.sessionKey
  359. },
  360. data: {checkInToken: this.checkInToken},
  361. dataType: 'json'
  362. })
  363. .done(function (_data) {
  364. console.log(_data);
  365. if(_data.success) {
  366. self.otSessionId = _data.data;
  367. _done();
  368. }
  369. else {
  370. alert(_data.message);
  371. }
  372. })
  373. .fail(function (_data) {
  374. console.log(_data);
  375. alert(_data.message);
  376. });
  377. @else
  378. $.get('/pro/get-opentok-session-key/' + self.clientUid, function(_data) {
  379. self.otSessionId = _data.data;
  380. _done();
  381. }, 'json');
  382. @endif
  383. },
  384. @if(!$guest)
  385. joinMeetingAsPro: function(_type) {
  386. var self = this, endPoint = '';
  387. if(_type === 'ASSISTANT') {
  388. endPoint = 'joinVideoVisitAsAssistantPro';
  389. }
  390. else {
  391. endPoint = 'joinVideoVisitAsMcpPro';
  392. }
  393. $.ajax({
  394. type: 'post',
  395. url: '/api/clientVideoVisit/' + endPoint,
  396. headers: {
  397. 'sessionKey': localStorage.sessionKey
  398. },
  399. data: {uid: self.clientUid},
  400. dataType: 'json'
  401. })
  402. .done(function (_data) {
  403. console.log(_data);
  404. })
  405. .fail(function (_data) {
  406. console.warn(_data);
  407. alert(_data.message);
  408. });
  409. },
  410. @endif
  411. activateParty: function(_stream = 'self') {
  412. var current = $('.full-view');
  413. if(current.attr('data-stream') === _stream) return;
  414. current.removeClass('full-view').addClass('thumb-view');
  415. if(current.attr('data-type') === 'CLIENT') {
  416. current.prependTo('.thumbs');
  417. }
  418. else {
  419. current.appendTo('.thumbs');
  420. }
  421. if(_stream === 'self') {
  422. $('#self-view')
  423. .removeClass('thumb-view')
  424. .removeClass('disconnected-view')
  425. .addClass('full-view')
  426. .prependTo('.main-view');
  427. }
  428. else {
  429. $('div[data-stream="' + _stream + '"]')
  430. .removeClass('thumb-view')
  431. .removeClass('disconnected-view')
  432. .addClass('full-view')
  433. .prependTo('.main-view');
  434. }
  435. }
  436. },
  437. mounted: function() {
  438. var self = this;
  439. @if($guest)
  440. this.clientUid = localStorage.clientUid;
  441. this.getClientCheckinToken(function() { // get client check-in token
  442. self.getOpenTokSessionId(function() { // get opentok session id
  443. var name = [];
  444. if (localStorage.clientFirstName) name.push(localStorage.clientFirstName);
  445. if (localStorage.clientLastName) name.push(localStorage.clientLastName);
  446. this.selfName = name.join(' ');
  447. $.ajax({
  448. type: 'post',
  449. url: '/api/openTok/getClientToken',
  450. headers: {
  451. 'sessionKey': localStorage.sessionKey
  452. },
  453. data: {
  454. opentokSessionId: self.otSessionId,
  455. name: name.join(' ')
  456. },
  457. dataType: 'json'
  458. })
  459. .done(function (_data) {
  460. console.log(_data);
  461. self.selfToken = _data.data;
  462. self.initOpenTok();
  463. })
  464. .fail(function (_data) {
  465. console.warn(_data);
  466. alert(_data.message);
  467. });
  468. });
  469. });
  470. @else
  471. localStorage.sessionKey = '{{ $session->session_key }}';
  472. @endif
  473. $(document).on('click', '.thumbs>div[data-stream]', function() {
  474. self.activateParty($(this).attr('data-stream'));
  475. return false;
  476. });
  477. @if(!$guest)
  478. // poll for new patients and alert
  479. window.setInterval(function() {
  480. self.pollForNextPatient();
  481. }, 5000);
  482. @endif
  483. window.onbeforeunload = function() {
  484. if(self.started) {
  485. return "A call is in progress";
  486. }
  487. };
  488. }
  489. });
  490. </script>
  491. @endsection