meet.blade.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  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"></div>
  42. <div class="thumbs">
  43. <div id="remote-view-1" class="remote-view disconnected-view"
  44. data-stream="" data-from=""></div>
  45. <div id="remote-view-2" class="remote-view disconnected-view"
  46. data-stream="" data-from=""></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. pollForNextPatient: function() {
  90. if(!this.started) {
  91. this.nextPatient(true);
  92. }
  93. },
  94. nextPatient: function(_pollOnly = false) {
  95. var self = this;
  96. if(!_pollOnly) this.checkingForNextPatient = true;
  97. $.post('/api/client/getNextClientForVideoVisit', {}, function(_data) {
  98. if(_pollOnly) {
  99. self.patientInQueue = _data.success;
  100. }
  101. else {
  102. self.checkingForNextPatient = false;
  103. if(!_data.success) {
  104. self.noNextPatient = _data.message;
  105. window.setTimeout(function() {
  106. self.noNextPatient = false;
  107. }, 2000);
  108. }
  109. else {
  110. // get ot session key from client record
  111. self.client = true;
  112. self.clientUid = _data.data;
  113. self.getOpenTokSessionId(function() {
  114. self.selfName = 'Pro'; // TODO: replace with name of authed pro
  115. $.post('/api/openTok/getClientToken', {
  116. opentokSessionId: self.otSessionId,
  117. name: self.selfName
  118. }, function (_data) {
  119. self.selfToken = _data.data;
  120. self.initOpenTok();
  121. });
  122. });
  123. }
  124. }
  125. }, 'json');
  126. },
  127. getInitials: function(_name) {
  128. var parts = _name.split(/\s+/g);
  129. parts = parts.map(_part => _part[0]);
  130. return parts.join('');
  131. },
  132. timeDisplay: function() {
  133. var seconds = this.time / 1000,
  134. minutes = parseInt(seconds / 60, 10);
  135. seconds = parseInt(seconds % 60, 10);
  136. return minutes + " min, " + seconds + " sec";
  137. },
  138. hangUp: function() {
  139. if(this.otSession) {
  140. this.otSession.disconnect();
  141. this.otSession = false;
  142. this.otSessionId = '';
  143. this.started = false;
  144. this.startTime = false;
  145. @if(!$guest)
  146. this.client = false;
  147. @else
  148. window.location = '/join';
  149. @endif
  150. }
  151. },
  152. callMCPPro: function() {
  153. // put client in mcp queue
  154. $.ajax({
  155. type: 'post',
  156. url: '/api/clientVideoVisit/PutVideoVisitInMcpQueue',
  157. headers: {
  158. 'sessionKey': localStorage.sessionKey
  159. },
  160. data: {uid: this.clientUid},
  161. dataType: 'json'
  162. })
  163. .done(function (_data) {
  164. console.log(_data);
  165. if(_data.success) {
  166. new Noty({
  167. theme: 'mint',
  168. type: 'success',
  169. text: 'Client added to MCP call queue',
  170. progressBar: false,
  171. timeout: 1500,
  172. }).show();
  173. }
  174. else {
  175. new Noty({
  176. theme: 'mint',
  177. type: 'alert',
  178. text: _data.message,
  179. progressBar: false,
  180. timeout: 3000,
  181. }).show();
  182. }
  183. })
  184. .fail(function (_data) {
  185. console.log(_data);
  186. // alert(_data.message);
  187. });
  188. },
  189. // OT methods
  190. initOpenTok: function() {
  191. /* fake video feed (temp) */
  192. const randomColour = () => {
  193. return Math.round(Math.random() * 255);
  194. };
  195. const canvas = document.createElement('canvas');
  196. canvas.width = 640;
  197. canvas.height = 480;
  198. const ctx = canvas.getContext('2d');
  199. var pos = 100;
  200. window.setInterval(function() {
  201. ctx.clearRect(0, 0, canvas.width, canvas.height);
  202. ctx.font = "20px Georgia";
  203. ctx.fillStyle = `rgb(220, 220, 220)`;
  204. var userType = '<?= $guest? "Client" : "Pro" ?>';
  205. ctx.fillText("Video feed from the " + userType, 20, pos);
  206. pos += 5;
  207. if(pos > canvas.height) pos = 100;
  208. }, 1000);
  209. var self = this;
  210. var apiKey = '<?= env('TOKBOX_API_KEY', '46678902') ?>';
  211. var sessionId = this.otSessionId;
  212. var token = this.selfToken;
  213. // destroy if existing
  214. self.hangUp();
  215. self.otSession = OT.initSession(apiKey, sessionId);
  216. // peer connected
  217. self.otSession.on('streamCreated', function streamCreated(event) {
  218. var subscriberOptions = {
  219. insertMode: 'append',
  220. width: '100%',
  221. height: '100%'
  222. };
  223. var remoteViewElem = 'remote-view-1';
  224. if($('#remote-view-1').attr('data-stream')) {
  225. remoteViewElem = 'remote-view-2';
  226. }
  227. self.otSession.subscribe(event.stream, remoteViewElem, subscriberOptions, self.handleOpenTokError);
  228. $('#' + remoteViewElem).attr('data-stream', event.stream.id);
  229. $('#' + remoteViewElem).attr('data-from', event.stream.connection.data);
  230. @if($guest)
  231. self.pro = true;
  232. @else
  233. self.joinMeetingAsPro();
  234. self.client = true;
  235. @endif
  236. $('.full-view:not(.disconnected-view)')
  237. .removeClass('full-view')
  238. .addClass('thumb-view')
  239. .prependTo('.thumbs');
  240. $('#' + remoteViewElem)
  241. .removeClass('thumb-view')
  242. .removeClass('disconnected-view')
  243. .addClass('full-view')
  244. .prependTo('.main-view');
  245. if(!self.startTime) {
  246. self.startTime = new Date().getTime();
  247. window.setInterval(function() {
  248. self.time = new Date().getTime() - self.startTime;
  249. }, 1000);
  250. self.started = true;
  251. }
  252. });
  253. // peer disconnected
  254. self.otSession.on("streamDestroyed", function(event) {
  255. @if($guest)
  256. var remoteViewElem = $('[data-stream="' + event.stream.id + '"]');
  257. if(remoteViewElem.length) {
  258. remoteViewElem
  259. .removeClass('full-view')
  260. .removeClass('thumb-view')
  261. .addClass('disconnected-view')
  262. .prependTo('.thumbs');
  263. remoteViewElem.attr('data-stream', '');
  264. remoteViewElem.attr('data-from', '');
  265. }
  266. $('#self-view')
  267. .removeClass('thumb-view')
  268. .removeClass('disconnected-view')
  269. .addClass('full-view')
  270. .prependTo('.main-view');
  271. self.pro = false;
  272. @else
  273. $('#self-view')
  274. .removeClass('thumb-view')
  275. .removeClass('disconnected-view')
  276. .removeClass('full-view')
  277. .prependTo('.thumbs');
  278. self.client = false;
  279. @endif
  280. self.started = false;
  281. self.startTime = false;
  282. // if no other parties in call, hang up
  283. if(!$(':not([data-stream=""])').length) {
  284. self.hangUp();
  285. }
  286. });
  287. // self connected
  288. self.otSession.on("sessionConnected", function(event) {
  289. console.log(event);
  290. // call join as assistant/pro here
  291. self.selfUserType = event.target.connection.data.split('|')[1];
  292. });
  293. // self disconnected
  294. self.otSession.on('sessionDisconnected', function sessionDisconnected(event) {
  295. console.log('You were disconnected from the session.', event.reason);
  296. });
  297. // initialize the publisher
  298. var publisherOptions = {
  299. videoSource: canvas.captureStream(1).getVideoTracks()[0],
  300. insertMode: 'append',
  301. width: '100%',
  302. height: '100%',
  303. };
  304. var publisher = OT.initPublisher('self-view', publisherOptions, self.handleOpenTokError);
  305. // Connect to the session
  306. self.otSession.connect(token, function callback(error) {
  307. if (error) {
  308. self.handleOpenTokError(error);
  309. } else {
  310. // If the connection is successful, publish the publisher to the session
  311. self.otSession.publish(publisher, self.handleOpenTokError);
  312. }
  313. });
  314. },
  315. handleOpenTokError: function(e) {
  316. },
  317. getClientCheckinToken: function(_done) {
  318. var self = this;
  319. $.get('/get-client-checkin-token/' + this.clientUid, function(_data) {
  320. console.log(_data);
  321. self.checkInToken = _data.data;
  322. _done();
  323. }, 'json');
  324. },
  325. getOpenTokSessionId: function(_done) {
  326. var self = this;
  327. @if($guest)
  328. $.ajax({
  329. type: 'post',
  330. url: '/api/clientVideoVisit/startVideoVisitAsClient',
  331. headers: {
  332. 'sessionKey': localStorage.sessionKey
  333. },
  334. data: {checkInToken: this.checkInToken},
  335. dataType: 'json'
  336. })
  337. .done(function (_data) {
  338. console.log(_data);
  339. if(_data.success) {
  340. self.otSessionId = _data.data;
  341. _done();
  342. }
  343. else {
  344. alert(_data.message);
  345. }
  346. })
  347. .fail(function (_data) {
  348. console.log(_data);
  349. alert(_data.message);
  350. });
  351. @else
  352. $.get('/pro/get-opentok-session-key/' + self.clientUid, function(_data) {
  353. self.otSessionId = _data.data;
  354. _done();
  355. }, 'json');
  356. @endif
  357. },
  358. joinMeetingAsPro: function() {
  359. var self = this;
  360. $.ajax({
  361. type: 'post',
  362. url: '/api/clientVideoVisit/joinVideoVisitAsMcpPro',
  363. headers: {
  364. 'sessionKey': localStorage.sessionKey
  365. },
  366. data: {uid: self.clientUid},
  367. dataType: 'json'
  368. })
  369. .done(function (_data) {
  370. console.log(_data);
  371. })
  372. .fail(function (_data) {
  373. console.warn(_data);
  374. alert(_data.message);
  375. });
  376. }
  377. },
  378. mounted: function() {
  379. var self = this;
  380. @if($guest)
  381. this.clientUid = localStorage.clientUid;
  382. this.getClientCheckinToken(function() { // get client check-in token
  383. self.getOpenTokSessionId(function() { // get opentok session id
  384. var name = [];
  385. if (localStorage.clientFirstName) name.push(localStorage.clientFirstName);
  386. if (localStorage.clientLastName) name.push(localStorage.clientLastName);
  387. this.selfName = name.join(' ');
  388. $.ajax({
  389. type: 'post',
  390. url: '/api/openTok/getClientToken',
  391. headers: {
  392. 'sessionKey': localStorage.sessionKey
  393. },
  394. data: {
  395. opentokSessionId: self.otSessionId,
  396. name: name.join(' ')
  397. },
  398. dataType: 'json'
  399. })
  400. .done(function (_data) {
  401. console.log(_data);
  402. self.selfToken = _data.data;
  403. self.initOpenTok();
  404. })
  405. .fail(function (_data) {
  406. console.warn(_data);
  407. alert(_data.message);
  408. });
  409. });
  410. });
  411. @endif
  412. $(document).on('click', '.thumbs>div', function() {
  413. $('.full-view')
  414. .removeClass('full-view')
  415. .removeClass('disconnected-view')
  416. .addClass('thumb-view')
  417. .prependTo('.thumbs');
  418. $(this)
  419. .removeClass('thumb-view')
  420. .removeClass('disconnected-view')
  421. .addClass('full-view')
  422. .prependTo('.main-view');
  423. return false;
  424. });
  425. @if(!$guest)
  426. // poll for new patients and alert
  427. window.setInterval(function() {
  428. self.pollForNextPatient();
  429. }, 5000);
  430. @endif
  431. }
  432. });
  433. </script>
  434. @endsection