meet.blade.php 22 KB

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