pro-call.blade.php 21 KB

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