pro-call.blade.php 18 KB

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