您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990
  1. /* jshint -W117 */
  2. /* application specific logic */
  3. var connection = null;
  4. var focus = null;
  5. var activecall = null;
  6. var RTC = null;
  7. var nickname = null;
  8. var sharedKey = '';
  9. var roomUrl = null;
  10. var ssrc2jid = {};
  11. /**
  12. * Indicates whether ssrc is camera video or desktop stream.
  13. * FIXME: remove those maps
  14. */
  15. var ssrc2videoType = {};
  16. var videoSrcToSsrc = {};
  17. /**
  18. * Currently focused video "src"(displayed in large video).
  19. * @type {String}
  20. */
  21. var focusedVideoSrc = null;
  22. var mutedAudios = {};
  23. var localVideoSrc = null;
  24. var flipXLocalVideo = true;
  25. var isFullScreen = false;
  26. var toolbarTimeout = null;
  27. var currentVideoWidth = null;
  28. var currentVideoHeight = null;
  29. /**
  30. * Method used to calculate large video size.
  31. * @type {function ()}
  32. */
  33. var getVideoSize;
  34. /**
  35. * Method used to get large video position.
  36. * @type {function ()}
  37. */
  38. var getVideoPosition;
  39. /* window.onbeforeunload = closePageWarning; */
  40. var preMuted = false;
  41. var sessionTerminated = false;
  42. function init() {
  43. RTC = setupRTC();
  44. if (RTC === null) {
  45. window.location.href = 'webrtcrequired.html';
  46. return;
  47. } else if (RTC.browser !== 'chrome') {
  48. window.location.href = 'chromeonly.html';
  49. return;
  50. }
  51. connection = new Strophe.Connection(document.getElementById('boshURL').value || config.bosh || '/http-bind');
  52. if (nickname) {
  53. connection.emuc.addDisplayNameToPresence(nickname);
  54. }
  55. if (connection.disco) {
  56. // for chrome, add multistream cap
  57. }
  58. connection.jingle.pc_constraints = RTC.pc_constraints;
  59. if (config.useIPv6) {
  60. // https://code.google.com/p/webrtc/issues/detail?id=2828
  61. if (!connection.jingle.pc_constraints.optional) connection.jingle.pc_constraints.optional = [];
  62. connection.jingle.pc_constraints.optional.push({googIPv6: true});
  63. }
  64. var jid = document.getElementById('jid').value || config.hosts.domain || window.location.hostname;
  65. connection.connect(jid, document.getElementById('password').value, function (status) {
  66. if (status === Strophe.Status.CONNECTED) {
  67. console.log('connected');
  68. if (config.useStunTurn) {
  69. connection.jingle.getStunAndTurnCredentials();
  70. }
  71. obtainAudioAndVideoPermissions(function () {
  72. getUserMediaWithConstraints(['audio'], audioStreamReady,
  73. function (error) {
  74. console.error('failed to obtain audio stream - stop', error);
  75. });
  76. });
  77. document.getElementById('connect').disabled = true;
  78. } else {
  79. console.log('status', status);
  80. }
  81. });
  82. }
  83. /**
  84. * HTTPS only:
  85. * We first ask for audio and video combined stream in order to get permissions and not to ask twice.
  86. * Then we dispose the stream and continue with separate audio, video streams(required for desktop sharing).
  87. */
  88. function obtainAudioAndVideoPermissions(callback) {
  89. // This makes sense only on https sites otherwise we'll be asked for permissions every time
  90. if (location.protocol !== 'https:') {
  91. callback();
  92. return;
  93. }
  94. // Get AV
  95. getUserMediaWithConstraints(
  96. ['audio', 'video'],
  97. function (avStream) {
  98. avStream.stop();
  99. callback();
  100. },
  101. function (error) {
  102. console.error('failed to obtain audio/video stream - stop', error);
  103. });
  104. }
  105. function audioStreamReady(stream) {
  106. change_local_audio(stream);
  107. if (RTC.browser !== 'firefox') {
  108. getUserMediaWithConstraints(['video'], videoStreamReady, videoStreamFailed, config.resolution || '360');
  109. } else {
  110. doJoin();
  111. }
  112. }
  113. function videoStreamReady(stream) {
  114. change_local_video(stream, true);
  115. doJoin();
  116. }
  117. function videoStreamFailed(error) {
  118. console.warn("Failed to obtain video stream - continue anyway", error);
  119. doJoin();
  120. }
  121. function doJoin() {
  122. var roomnode = null;
  123. var path = window.location.pathname;
  124. var roomjid;
  125. // determinde the room node from the url
  126. // TODO: just the roomnode or the whole bare jid?
  127. if (config.getroomnode && typeof config.getroomnode === 'function') {
  128. // custom function might be responsible for doing the pushstate
  129. roomnode = config.getroomnode(path);
  130. } else {
  131. /* fall back to default strategy
  132. * this is making assumptions about how the URL->room mapping happens.
  133. * It currently assumes deployment at root, with a rewrite like the
  134. * following one (for nginx):
  135. location ~ ^/([a-zA-Z0-9]+)$ {
  136. rewrite ^/(.*)$ / break;
  137. }
  138. */
  139. if (path.length > 1) {
  140. roomnode = path.substr(1).toLowerCase();
  141. } else {
  142. roomnode = Math.random().toString(36).substr(2, 20);
  143. window.history.pushState('VideoChat',
  144. 'Room: ' + roomnode, window.location.pathname + roomnode);
  145. }
  146. }
  147. roomjid = roomnode + '@' + config.hosts.muc;
  148. if (config.useNicks) {
  149. var nick = window.prompt('Your nickname (optional)');
  150. if (nick) {
  151. roomjid += '/' + nick;
  152. } else {
  153. roomjid += '/' + Strophe.getNodeFromJid(connection.jid);
  154. }
  155. } else {
  156. roomjid += '/' + Strophe.getNodeFromJid(connection.jid).substr(0, 8);
  157. }
  158. connection.emuc.doJoin(roomjid);
  159. }
  160. function change_local_audio(stream) {
  161. connection.jingle.localAudio = stream;
  162. RTC.attachMediaStream($('#localAudio'), stream);
  163. document.getElementById('localAudio').autoplay = true;
  164. document.getElementById('localAudio').volume = 0;
  165. if (preMuted) {
  166. toggleAudio();
  167. preMuted = false;
  168. }
  169. }
  170. function change_local_video(stream, flipX) {
  171. connection.jingle.localVideo = stream;
  172. var localVideo = document.createElement('video');
  173. localVideo.id = 'localVideo_' + stream.id;
  174. localVideo.autoplay = true;
  175. localVideo.volume = 0; // is it required if audio is separated ?
  176. localVideo.oncontextmenu = function () { return false; };
  177. var localVideoContainer = document.getElementById('localVideoWrapper');
  178. localVideoContainer.appendChild(localVideo);
  179. var localVideoSelector = $('#' + localVideo.id);
  180. // Add click handler
  181. localVideoSelector.click(function () {
  182. handleVideoThumbClicked(localVideo.src);
  183. });
  184. // Add stream ended handler
  185. stream.onended = function () {
  186. localVideoContainer.removeChild(localVideo);
  187. checkChangeLargeVideo(localVideo.src);
  188. };
  189. // Flip video x axis if needed
  190. flipXLocalVideo = flipX;
  191. if (flipX) {
  192. localVideoSelector.addClass("flipVideoX");
  193. }
  194. // Attach WebRTC stream
  195. RTC.attachMediaStream(localVideoSelector, stream);
  196. localVideoSrc = localVideo.src;
  197. updateLargeVideo(localVideoSrc, 0);
  198. }
  199. $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
  200. function waitForRemoteVideo(selector, sid, ssrc) {
  201. if (selector.removed) {
  202. console.warn("media removed before had started", selector);
  203. return;
  204. }
  205. var sess = connection.jingle.sessions[sid];
  206. if (data.stream.id === 'mixedmslabel') return;
  207. var videoTracks = data.stream.getVideoTracks();
  208. // console.log("waiting..", videoTracks, selector[0]);
  209. if (videoTracks.length === 0 || selector[0].currentTime > 0) {
  210. RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF?
  211. // FIXME: add a class that will associate peer Jid, video.src, it's ssrc and video type
  212. // in order to get rid of too many maps
  213. if (ssrc) {
  214. videoSrcToSsrc[sel.attr('src')] = ssrc;
  215. } else {
  216. console.warn("No ssrc given for video", sel);
  217. }
  218. $(document).trigger('callactive.jingle', [selector, sid]);
  219. console.log('waitForremotevideo', sess.peerconnection.iceConnectionState, sess.peerconnection.signalingState);
  220. } else {
  221. setTimeout(function () { waitForRemoteVideo(selector, sid, ssrc); }, 250);
  222. }
  223. }
  224. var sess = connection.jingle.sessions[sid];
  225. var thessrc;
  226. // look up an associated JID for a stream id
  227. if (data.stream.id.indexOf('mixedmslabel') === -1) {
  228. var ssrclines = SDPUtil.find_lines(sess.peerconnection.remoteDescription.sdp, 'a=ssrc');
  229. ssrclines = ssrclines.filter(function (line) {
  230. return line.indexOf('mslabel:' + data.stream.label) !== -1;
  231. });
  232. if (ssrclines.length) {
  233. thessrc = ssrclines[0].substring(7).split(' ')[0];
  234. // ok to overwrite the one from focus? might save work in colibri.js
  235. console.log('associated jid', ssrc2jid[thessrc], data.peerjid);
  236. if (ssrc2jid[thessrc]) {
  237. data.peerjid = ssrc2jid[thessrc];
  238. }
  239. }
  240. }
  241. var container;
  242. var remotes = document.getElementById('remoteVideos');
  243. if (data.peerjid) {
  244. ensurePeerContainerExists(data.peerjid);
  245. container = document.getElementById(
  246. 'participant_' + Strophe.getResourceFromJid(data.peerjid));
  247. } else {
  248. if (data.stream.id !== 'mixedmslabel') {
  249. console.error('can not associate stream', data.stream.id, 'with a participant');
  250. // We don't want to add it here since it will cause troubles
  251. return;
  252. }
  253. // FIXME: for the mixed ms we dont need a video -- currently
  254. container = document.createElement('span');
  255. container.className = 'videocontainer';
  256. remotes.appendChild(container);
  257. Util.playSoundNotification('userJoined');
  258. }
  259. var isVideo = data.stream.getVideoTracks().length > 0;
  260. var vid = isVideo ? document.createElement('video') : document.createElement('audio');
  261. var id = (isVideo ? 'remoteVideo_' : 'remoteAudio_') + sid + '_' + data.stream.id;
  262. vid.id = id;
  263. vid.autoplay = true;
  264. vid.oncontextmenu = function () { return false; };
  265. container.appendChild(vid);
  266. // TODO: make mixedstream display:none via css?
  267. if (id.indexOf('mixedmslabel') !== -1) {
  268. container.id = 'mixedstream';
  269. $(container).hide();
  270. }
  271. var sel = $('#' + id);
  272. sel.hide();
  273. RTC.attachMediaStream(sel, data.stream);
  274. if (isVideo) {
  275. waitForRemoteVideo(sel, sid, thessrc);
  276. }
  277. data.stream.onended = function () {
  278. console.log('stream ended', this.id);
  279. // Mark video as removed to cancel waiting loop(if video is removed before has started)
  280. sel.removed = true;
  281. sel.remove();
  282. var audioCount = $('#' + container.id + '>audio').length;
  283. var videoCount = $('#' + container.id + '>video').length;
  284. if (!audioCount && !videoCount) {
  285. console.log("Remove whole user", container.id);
  286. // Remove whole container
  287. container.remove();
  288. Util.playSoundNotification('userLeft');
  289. resizeThumbnails();
  290. }
  291. checkChangeLargeVideo(vid.src);
  292. };
  293. // Add click handler
  294. sel.click(function () {
  295. handleVideoThumbClicked(vid.src);
  296. });
  297. // an attempt to work around https://github.com/jitsi/jitmeet/issues/32
  298. if (isVideo &&
  299. data.peerjid && sess.peerjid === data.peerjid &&
  300. data.stream.getVideoTracks().length === 0 &&
  301. connection.jingle.localVideo.getVideoTracks().length > 0) {
  302. //
  303. window.setTimeout(function () {
  304. sendKeyframe(sess.peerconnection);
  305. }, 3000);
  306. }
  307. });
  308. /**
  309. * Returns the JID of the user to whom given <tt>videoSrc</tt> belongs.
  310. * @param videoSrc the video "src" identifier.
  311. * @returns {null | String} the JID of the user to whom given <tt>videoSrc</tt>
  312. * belongs.
  313. */
  314. function getJidFromVideoSrc(videoSrc)
  315. {
  316. if (videoSrc === localVideoSrc)
  317. return connection.emuc.myroomjid;
  318. var ssrc = videoSrcToSsrc[videoSrc];
  319. if (!ssrc)
  320. {
  321. return null;
  322. }
  323. return ssrc2jid[ssrc];
  324. }
  325. /**
  326. * Gets the selector of video thumbnail container for the user identified by
  327. * given <tt>userJid</tt>
  328. * @param userJid user's Jid for whom we want to get the video container.
  329. */
  330. function getParticipantContainer(userJid)
  331. {
  332. if (!userJid)
  333. return null;
  334. if (userJid === connection.emuc.myroomjid)
  335. return $("#localVideoContainer");
  336. else
  337. return $("#participant_" + Strophe.getResourceFromJid(userJid));
  338. }
  339. function handleVideoThumbClicked(videoSrc) {
  340. // Restore style for previously focused video
  341. var oldContainer =
  342. getParticipantContainer(
  343. getJidFromVideoSrc(focusedVideoSrc));
  344. if (oldContainer)
  345. oldContainer.removeClass("videoContainerFocused");
  346. // Unlock
  347. if (focusedVideoSrc === videoSrc)
  348. {
  349. focusedVideoSrc = null;
  350. return;
  351. }
  352. // Lock new video
  353. focusedVideoSrc = videoSrc;
  354. var userJid = getJidFromVideoSrc(videoSrc);
  355. if (userJid)
  356. {
  357. var container = getParticipantContainer(userJid);
  358. container.addClass("videoContainerFocused");
  359. }
  360. $(document).trigger("video.selected", [false]);
  361. updateLargeVideo(videoSrc, 1);
  362. $('audio').each(function (idx, el) {
  363. if (el.id.indexOf('mixedmslabel') !== -1) {
  364. el.volume = 0;
  365. el.volume = 1;
  366. }
  367. });
  368. }
  369. /**
  370. * Checks if removed video is currently displayed and tries to display another one instead.
  371. * @param removedVideoSrc src stream identifier of the video.
  372. */
  373. function checkChangeLargeVideo(removedVideoSrc) {
  374. if (removedVideoSrc === $('#largeVideo').attr('src')) {
  375. // this is currently displayed as large
  376. // pick the last visible video in the row
  377. // if nobody else is left, this picks the local video
  378. var pick = $('#remoteVideos>span[id!="mixedstream"]:visible:last>video').get(0);
  379. if (!pick) {
  380. console.info("Last visible video no longer exists");
  381. pick = $('#remoteVideos>span[id!="mixedstream"]>video').get(0);
  382. if (!pick) {
  383. // Try local video
  384. console.info("Fallback to local video...");
  385. pick = $('#remoteVideos>span>span>video').get(0);
  386. }
  387. }
  388. // mute if localvideo
  389. if (pick) {
  390. updateLargeVideo(pick.src, pick.volume);
  391. } else {
  392. console.warn("Failed to elect large video");
  393. }
  394. }
  395. }
  396. // an attempt to work around https://github.com/jitsi/jitmeet/issues/32
  397. function sendKeyframe(pc) {
  398. console.log('sendkeyframe', pc.iceConnectionState);
  399. if (pc.iceConnectionState !== 'connected') return; // safe...
  400. pc.setRemoteDescription(
  401. pc.remoteDescription,
  402. function () {
  403. pc.createAnswer(
  404. function (modifiedAnswer) {
  405. pc.setLocalDescription(
  406. modifiedAnswer,
  407. function () {
  408. // noop
  409. },
  410. function (error) {
  411. console.log('triggerKeyframe setLocalDescription failed', error);
  412. }
  413. );
  414. },
  415. function (error) {
  416. console.log('triggerKeyframe createAnswer failed', error);
  417. }
  418. );
  419. },
  420. function (error) {
  421. console.log('triggerKeyframe setRemoteDescription failed', error);
  422. }
  423. );
  424. }
  425. // really mute video, i.e. dont even send black frames
  426. function muteVideo(pc, unmute) {
  427. // FIXME: this probably needs another of those lovely state safeguards...
  428. // which checks for iceconn == connected and sigstate == stable
  429. pc.setRemoteDescription(pc.remoteDescription,
  430. function () {
  431. pc.createAnswer(
  432. function (answer) {
  433. var sdp = new SDP(answer.sdp);
  434. if (sdp.media.length > 1) {
  435. if (unmute)
  436. sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
  437. else
  438. sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
  439. sdp.raw = sdp.session + sdp.media.join('');
  440. answer.sdp = sdp.raw;
  441. }
  442. pc.setLocalDescription(answer,
  443. function () {
  444. console.log('mute SLD ok');
  445. },
  446. function (error) {
  447. console.log('mute SLD error');
  448. }
  449. );
  450. },
  451. function (error) {
  452. console.log(error);
  453. }
  454. );
  455. },
  456. function (error) {
  457. console.log('muteVideo SRD error');
  458. }
  459. );
  460. }
  461. $(document).bind('callincoming.jingle', function (event, sid) {
  462. var sess = connection.jingle.sessions[sid];
  463. // TODO: do we check activecall == null?
  464. activecall = sess;
  465. // Bind data channel listener in case we're a regular participant
  466. if (config.openSctp)
  467. {
  468. bindDataChannelListener(sess.peerconnection);
  469. }
  470. // TODO: check affiliation and/or role
  471. console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]);
  472. sess.usedrip = true; // not-so-naive trickle ice
  473. sess.sendAnswer();
  474. sess.accept();
  475. });
  476. $(document).bind('conferenceCreated.jingle', function (event, focus)
  477. {
  478. // Bind data channel listener in case we're the focus
  479. if (config.openSctp)
  480. {
  481. bindDataChannelListener(focus.peerconnection);
  482. }
  483. });
  484. $(document).bind('callactive.jingle', function (event, videoelem, sid) {
  485. if (videoelem.attr('id').indexOf('mixedmslabel') === -1) {
  486. // ignore mixedmslabela0 and v0
  487. videoelem.show();
  488. resizeThumbnails();
  489. if (!focusedVideoSrc)
  490. updateLargeVideo(videoelem.attr('src'), 1);
  491. showFocusIndicator();
  492. }
  493. });
  494. $(document).bind('callterminated.jingle', function (event, sid, jid, reason) {
  495. // Leave the room if my call has been remotely terminated.
  496. if (connection.emuc.joined && focus == null && reason === 'kick') {
  497. sessionTerminated = true;
  498. connection.emuc.doLeave();
  499. openMessageDialog( "Session Terminated",
  500. "Ouch! You have been kicked out of the meet!");
  501. }
  502. });
  503. $(document).bind('setLocalDescription.jingle', function (event, sid) {
  504. // put our ssrcs into presence so other clients can identify our stream
  505. var sess = connection.jingle.sessions[sid];
  506. var newssrcs = {};
  507. var directions = {};
  508. var localSDP = new SDP(sess.peerconnection.localDescription.sdp);
  509. localSDP.media.forEach(function (media) {
  510. var type = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
  511. if (SDPUtil.find_line(media, 'a=ssrc:')) {
  512. // assumes a single local ssrc
  513. var ssrc = SDPUtil.find_line(media, 'a=ssrc:').substring(7).split(' ')[0];
  514. newssrcs[type] = ssrc;
  515. directions[type] = (
  516. SDPUtil.find_line(media, 'a=sendrecv') ||
  517. SDPUtil.find_line(media, 'a=recvonly') ||
  518. SDPUtil.find_line('a=sendonly') ||
  519. SDPUtil.find_line('a=inactive') ||
  520. 'a=sendrecv').substr(2);
  521. }
  522. });
  523. console.log('new ssrcs', newssrcs);
  524. // Have to clear presence map to get rid of removed streams
  525. connection.emuc.clearPresenceMedia();
  526. var i = 0;
  527. Object.keys(newssrcs).forEach(function (mtype) {
  528. i++;
  529. var type = mtype;
  530. // Change video type to screen
  531. if (mtype === 'video' && isUsingScreenStream) {
  532. type = 'screen';
  533. }
  534. connection.emuc.addMediaToPresence(i, type, newssrcs[mtype], directions[mtype]);
  535. });
  536. if (i > 0) {
  537. connection.emuc.sendPresence();
  538. }
  539. });
  540. $(document).bind('joined.muc', function (event, jid, info) {
  541. updateRoomUrl(window.location.href);
  542. document.getElementById('localNick').appendChild(
  543. document.createTextNode(Strophe.getResourceFromJid(jid) + ' (me)')
  544. );
  545. if (Object.keys(connection.emuc.members).length < 1) {
  546. focus = new ColibriFocus(connection, config.hosts.bridge);
  547. }
  548. if (focus && config.etherpad_base) {
  549. Etherpad.init();
  550. }
  551. showFocusIndicator();
  552. // Once we've joined the muc show the toolbar
  553. showToolbar();
  554. var displayName = '';
  555. if (info.displayName)
  556. displayName = info.displayName + ' (me)';
  557. showDisplayName('localVideoContainer', displayName);
  558. });
  559. $(document).bind('entered.muc', function (event, jid, info, pres) {
  560. console.log('entered', jid, info);
  561. console.log('is focus?' + focus ? 'true' : 'false');
  562. // Add Peer's container
  563. ensurePeerContainerExists(jid);
  564. if (focus !== null) {
  565. // FIXME: this should prepare the video
  566. if (focus.confid === null) {
  567. console.log('make new conference with', jid);
  568. focus.makeConference(Object.keys(connection.emuc.members));
  569. } else {
  570. console.log('invite', jid, 'into conference');
  571. focus.addNewParticipant(jid);
  572. }
  573. }
  574. else if (sharedKey) {
  575. updateLockButton();
  576. }
  577. });
  578. $(document).bind('left.muc', function (event, jid) {
  579. console.log('left.muc', jid);
  580. // Need to call this with a slight delay, otherwise the element couldn't be
  581. // found for some reason.
  582. window.setTimeout(function () {
  583. var container = document.getElementById(
  584. 'participant_' + Strophe.getResourceFromJid(jid));
  585. if (container) {
  586. // hide here, wait for video to close before removing
  587. $(container).hide();
  588. resizeThumbnails();
  589. }
  590. }, 10);
  591. // Unlock large video
  592. if (focusedVideoSrc)
  593. {
  594. if (getJidFromVideoSrc(focusedVideoSrc) === jid)
  595. {
  596. console.info("Focused video owner has left the conference");
  597. focusedVideoSrc = null;
  598. }
  599. }
  600. connection.jingle.terminateByJid(jid);
  601. if (focus == null
  602. // I shouldn't be the one that left to enter here.
  603. && jid !== connection.emuc.myroomjid
  604. && connection.emuc.myroomjid === connection.emuc.list_members[0]
  605. // If our session has been terminated for some reason
  606. // (kicked, hangup), don't try to become the focus
  607. && !sessionTerminated) {
  608. console.log('welcome to our new focus... myself');
  609. focus = new ColibriFocus(connection, config.hosts.bridge);
  610. if (Object.keys(connection.emuc.members).length > 0) {
  611. focus.makeConference(Object.keys(connection.emuc.members));
  612. }
  613. $(document).trigger('focusechanged.muc', [focus]);
  614. }
  615. else if (focus && Object.keys(connection.emuc.members).length === 0) {
  616. console.log('everyone left');
  617. // FIXME: closing the connection is a hack to avoid some
  618. // problemswith reinit
  619. disposeConference();
  620. focus = new ColibriFocus(connection, config.hosts.bridge);
  621. }
  622. if (connection.emuc.getPrezi(jid)) {
  623. $(document).trigger('presentationremoved.muc',
  624. [jid, connection.emuc.getPrezi(jid)]);
  625. }
  626. });
  627. $(document).bind('presence.muc', function (event, jid, info, pres) {
  628. // Remove old ssrcs coming from the jid
  629. Object.keys(ssrc2jid).forEach(function (ssrc) {
  630. if (ssrc2jid[ssrc] == jid) {
  631. delete ssrc2jid[ssrc];
  632. }
  633. if (ssrc2videoType == jid) {
  634. delete ssrc2videoType[ssrc];
  635. }
  636. });
  637. $(pres).find('>media[xmlns="http://estos.de/ns/mjs"]>source').each(function (idx, ssrc) {
  638. //console.log(jid, 'assoc ssrc', ssrc.getAttribute('type'), ssrc.getAttribute('ssrc'));
  639. var ssrcV = ssrc.getAttribute('ssrc');
  640. ssrc2jid[ssrcV] = jid;
  641. var type = ssrc.getAttribute('type');
  642. ssrc2videoType[ssrcV] = type;
  643. // might need to update the direction if participant just went from sendrecv to recvonly
  644. if (type === 'video' || type === 'screen') {
  645. var el = $('#participant_' + Strophe.getResourceFromJid(jid) + '>video');
  646. switch (ssrc.getAttribute('direction')) {
  647. case 'sendrecv':
  648. el.show();
  649. break;
  650. case 'recvonly':
  651. el.hide();
  652. // FIXME: Check if we have to change large video
  653. //checkChangeLargeVideo(el);
  654. break;
  655. }
  656. }
  657. });
  658. if (info.displayName) {
  659. if (jid === connection.emuc.myroomjid) {
  660. showDisplayName('localVideoContainer', info.displayName + ' (me)');
  661. } else {
  662. ensurePeerContainerExists(jid);
  663. showDisplayName('participant_' + Strophe.getResourceFromJid(jid),
  664. info.displayName);
  665. }
  666. }
  667. });
  668. $(document).bind('passwordrequired.muc', function (event, jid) {
  669. console.log('on password required', jid);
  670. $.prompt('<h2>Password required</h2>' +
  671. '<input id="lockKey" type="text" placeholder="shared key" autofocus>', {
  672. persistent: true,
  673. buttons: { "Ok": true, "Cancel": false},
  674. defaultButton: 1,
  675. loaded: function (event) {
  676. document.getElementById('lockKey').focus();
  677. },
  678. submit: function (e, v, m, f) {
  679. if (v) {
  680. var lockKey = document.getElementById('lockKey');
  681. if (lockKey.value !== null) {
  682. setSharedKey(lockKey.value);
  683. connection.emuc.doJoin(jid, lockKey.value);
  684. }
  685. }
  686. }
  687. });
  688. });
  689. $(document).bind('audiomuted.muc', function (event, jid, isMuted) {
  690. var videoSpanId = null;
  691. if (jid === connection.emuc.myroomjid) {
  692. videoSpanId = 'localVideoContainer';
  693. } else {
  694. ensurePeerContainerExists(jid);
  695. videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
  696. }
  697. if (focus) {
  698. mutedAudios[jid] = isMuted;
  699. updateRemoteVideoMenu(jid, isMuted);
  700. }
  701. if (videoSpanId)
  702. showAudioIndicator(videoSpanId, isMuted);
  703. });
  704. $(document).bind('videomuted.muc', function (event, jid, isMuted) {
  705. var videoSpanId = null;
  706. if (jid === connection.emuc.myroomjid) {
  707. videoSpanId = 'localVideoContainer';
  708. } else {
  709. ensurePeerContainerExists(jid);
  710. videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
  711. }
  712. if (videoSpanId)
  713. showVideoIndicator(videoSpanId, isMuted);
  714. });
  715. /**
  716. * Updates the large video with the given new video source.
  717. */
  718. function updateLargeVideo(newSrc, vol) {
  719. console.log('hover in', newSrc);
  720. if ($('#largeVideo').attr('src') != newSrc) {
  721. var isVisible = $('#largeVideo').is(':visible');
  722. $('#largeVideo').fadeOut(300, function () {
  723. $(this).attr('src', newSrc);
  724. // Screen stream is already rotated
  725. var flipX = (newSrc === localVideoSrc) && flipXLocalVideo;
  726. var videoTransform = document.getElementById('largeVideo').style.webkitTransform;
  727. if (flipX && videoTransform !== 'scaleX(-1)') {
  728. document.getElementById('largeVideo').style.webkitTransform = "scaleX(-1)";
  729. }
  730. else if (!flipX && videoTransform === 'scaleX(-1)') {
  731. document.getElementById('largeVideo').style.webkitTransform = "none";
  732. }
  733. // Change the way we'll be measuring and positioning large video
  734. var isDesktop = isVideoSrcDesktop(newSrc);
  735. getVideoSize = isDesktop ? getDesktopVideoSize : getCameraVideoSize;
  736. getVideoPosition = isDesktop ? getDesktopVideoPosition : getCameraVideoPosition;
  737. if (isVisible)
  738. $(this).fadeIn(300);
  739. });
  740. }
  741. }
  742. /**
  743. * Checks if video identified by given src is desktop stream.
  744. * @param videoSrc eg. blob:https%3A//pawel.jitsi.net/9a46e0bd-131e-4d18-9c14-a9264e8db395
  745. * @returns {boolean}
  746. */
  747. function isVideoSrcDesktop(videoSrc) {
  748. // FIXME: fix this mapping mess...
  749. // figure out if large video is desktop stream or just a camera
  750. var isDesktop = false;
  751. if (localVideoSrc === videoSrc) {
  752. // local video
  753. isDesktop = isUsingScreenStream;
  754. } else {
  755. // Do we have associations...
  756. var videoSsrc = videoSrcToSsrc[videoSrc];
  757. if (videoSsrc) {
  758. var videoType = ssrc2videoType[videoSsrc];
  759. if (videoType) {
  760. // Finally there...
  761. isDesktop = videoType === 'screen';
  762. } else {
  763. console.error("No video type for ssrc: " + videoSsrc);
  764. }
  765. } else {
  766. console.error("No ssrc for src: " + videoSrc);
  767. }
  768. }
  769. return isDesktop;
  770. }
  771. /**
  772. * Shows/hides the large video.
  773. */
  774. function setLargeVideoVisible(isVisible) {
  775. if (isVisible) {
  776. $('#largeVideo').css({visibility: 'visible'});
  777. $('.watermark').css({visibility: 'visible'});
  778. }
  779. else {
  780. $('#largeVideo').css({visibility: 'hidden'});
  781. $('.watermark').css({visibility: 'hidden'});
  782. }
  783. }
  784. function getConferenceHandler() {
  785. return focus ? focus : activecall;
  786. }
  787. function toggleVideo() {
  788. if (!(connection && connection.jingle.localVideo))
  789. return;
  790. var sess = getConferenceHandler();
  791. if (sess) {
  792. sess.toggleVideoMute(
  793. function (isMuted) {
  794. if (isMuted) {
  795. $('#video').removeClass("icon-camera");
  796. $('#video').addClass("icon-camera icon-camera-disabled");
  797. } else {
  798. $('#video').removeClass("icon-camera icon-camera-disabled");
  799. $('#video').addClass("icon-camera");
  800. }
  801. }
  802. );
  803. }
  804. sess = focus || activecall;
  805. if (!sess) {
  806. return;
  807. }
  808. sess.pendingop = ismuted ? 'unmute' : 'mute';
  809. // connection.emuc.addVideoInfoToPresence(!ismuted);
  810. // connection.emuc.sendPresence();
  811. sess.modifySources();
  812. }
  813. /**
  814. * Mutes / unmutes audio for the local participant.
  815. */
  816. function toggleAudio() {
  817. if (!(connection && connection.jingle.localAudio)) {
  818. preMuted = true;
  819. // We still click the button.
  820. buttonClick("#mute", "icon-microphone icon-mic-disabled");
  821. return;
  822. }
  823. var localAudio = connection.jingle.localAudio;
  824. for (var idx = 0; idx < localAudio.getAudioTracks().length; idx++) {
  825. var audioEnabled = localAudio.getAudioTracks()[idx].enabled;
  826. localAudio.getAudioTracks()[idx].enabled = !audioEnabled;
  827. // isMuted is the opposite of audioEnabled
  828. connection.emuc.addAudioInfoToPresence(audioEnabled);
  829. connection.emuc.sendPresence();
  830. }
  831. buttonClick("#mute", "icon-microphone icon-mic-disabled");
  832. }
  833. /**
  834. * Positions the large video.
  835. *
  836. * @param videoWidth the stream video width
  837. * @param videoHeight the stream video height
  838. */
  839. var positionLarge = function (videoWidth, videoHeight) {
  840. var videoSpaceWidth = $('#videospace').width();
  841. var videoSpaceHeight = window.innerHeight;
  842. var videoSize = getVideoSize(videoWidth,
  843. videoHeight,
  844. videoSpaceWidth,
  845. videoSpaceHeight);
  846. var largeVideoWidth = videoSize[0];
  847. var largeVideoHeight = videoSize[1];
  848. var videoPosition = getVideoPosition(largeVideoWidth,
  849. largeVideoHeight,
  850. videoSpaceWidth,
  851. videoSpaceHeight);
  852. var horizontalIndent = videoPosition[0];
  853. var verticalIndent = videoPosition[1];
  854. positionVideo($('#largeVideo'),
  855. largeVideoWidth,
  856. largeVideoHeight,
  857. horizontalIndent, verticalIndent);
  858. };
  859. /**
  860. * Returns an array of the video horizontal and vertical indents,
  861. * so that if fits its parent.
  862. *
  863. * @return an array with 2 elements, the horizontal indent and the vertical
  864. * indent
  865. */
  866. function getCameraVideoPosition(videoWidth,
  867. videoHeight,
  868. videoSpaceWidth,
  869. videoSpaceHeight) {
  870. // Parent height isn't completely calculated when we position the video in
  871. // full screen mode and this is why we use the screen height in this case.
  872. // Need to think it further at some point and implement it properly.
  873. var isFullScreen = document.fullScreen ||
  874. document.mozFullScreen ||
  875. document.webkitIsFullScreen;
  876. if (isFullScreen)
  877. videoSpaceHeight = window.innerHeight;
  878. var horizontalIndent = (videoSpaceWidth - videoWidth) / 2;
  879. var verticalIndent = (videoSpaceHeight - videoHeight) / 2;
  880. return [horizontalIndent, verticalIndent];
  881. }
  882. /**
  883. * Returns an array of the video horizontal and vertical indents.
  884. * Centers horizontally and top aligns vertically.
  885. *
  886. * @return an array with 2 elements, the horizontal indent and the vertical
  887. * indent
  888. */
  889. function getDesktopVideoPosition(videoWidth,
  890. videoHeight,
  891. videoSpaceWidth,
  892. videoSpaceHeight) {
  893. var horizontalIndent = (videoSpaceWidth - videoWidth) / 2;
  894. var verticalIndent = 0;// Top aligned
  895. return [horizontalIndent, verticalIndent];
  896. }
  897. /**
  898. * Returns an array of the video dimensions, so that it covers the screen.
  899. * It leaves no empty areas, but some parts of the video might not be visible.
  900. *
  901. * @return an array with 2 elements, the video width and the video height
  902. */
  903. function getCameraVideoSize(videoWidth,
  904. videoHeight,
  905. videoSpaceWidth,
  906. videoSpaceHeight) {
  907. if (!videoWidth)
  908. videoWidth = currentVideoWidth;
  909. if (!videoHeight)
  910. videoHeight = currentVideoHeight;
  911. var aspectRatio = videoWidth / videoHeight;
  912. var availableWidth = Math.max(videoWidth, videoSpaceWidth);
  913. var availableHeight = Math.max(videoHeight, videoSpaceHeight);
  914. if (availableWidth / aspectRatio < videoSpaceHeight) {
  915. availableHeight = videoSpaceHeight;
  916. availableWidth = availableHeight * aspectRatio;
  917. }
  918. if (availableHeight * aspectRatio < videoSpaceWidth) {
  919. availableWidth = videoSpaceWidth;
  920. availableHeight = availableWidth / aspectRatio;
  921. }
  922. return [availableWidth, availableHeight];
  923. }
  924. /**
  925. * Returns an array of the video dimensions, so that it keeps it's aspect ratio and fits available area with it's
  926. * larger dimension. This method ensures that whole video will be visible and can leave empty areas.
  927. *
  928. * @return an array with 2 elements, the video width and the video height
  929. */
  930. function getDesktopVideoSize(videoWidth,
  931. videoHeight,
  932. videoSpaceWidth,
  933. videoSpaceHeight) {
  934. if (!videoWidth)
  935. videoWidth = currentVideoWidth;
  936. if (!videoHeight)
  937. videoHeight = currentVideoHeight;
  938. var aspectRatio = videoWidth / videoHeight;
  939. var availableWidth = Math.max(videoWidth, videoSpaceWidth);
  940. var availableHeight = Math.max(videoHeight, videoSpaceHeight);
  941. videoSpaceHeight -= $('#remoteVideos').outerHeight();
  942. if (availableWidth / aspectRatio >= videoSpaceHeight)
  943. {
  944. availableHeight = videoSpaceHeight;
  945. availableWidth = availableHeight * aspectRatio;
  946. }
  947. if (availableHeight * aspectRatio >= videoSpaceWidth)
  948. {
  949. availableWidth = videoSpaceWidth;
  950. availableHeight = availableWidth / aspectRatio;
  951. }
  952. return [availableWidth, availableHeight];
  953. }
  954. /**
  955. * Sets the size and position of the given video element.
  956. *
  957. * @param video the video element to position
  958. * @param width the desired video width
  959. * @param height the desired video height
  960. * @param horizontalIndent the left and right indent
  961. * @param verticalIndent the top and bottom indent
  962. */
  963. function positionVideo(video,
  964. width,
  965. height,
  966. horizontalIndent,
  967. verticalIndent) {
  968. video.width(width);
  969. video.height(height);
  970. video.css({ top: verticalIndent + 'px',
  971. bottom: verticalIndent + 'px',
  972. left: horizontalIndent + 'px',
  973. right: horizontalIndent + 'px'});
  974. }
  975. var resizeLargeVideoContainer = function () {
  976. Chat.resizeChat();
  977. var availableHeight = window.innerHeight;
  978. var availableWidth = Util.getAvailableVideoWidth();
  979. if (availableWidth < 0 || availableHeight < 0) return;
  980. $('#videospace').width(availableWidth);
  981. $('#videospace').height(availableHeight);
  982. $('#largeVideoContainer').width(availableWidth);
  983. $('#largeVideoContainer').height(availableHeight);
  984. resizeThumbnails();
  985. };
  986. var calculateThumbnailSize = function () {
  987. // Calculate the available height, which is the inner window height minus
  988. // 39px for the header minus 2px for the delimiter lines on the top and
  989. // bottom of the large video, minus the 36px space inside the remoteVideos
  990. // container used for highlighting shadow.
  991. var availableHeight = 100;
  992. var numvids = $('#remoteVideos>span:visible').length;
  993. // Remove the 1px borders arround videos and the chat width.
  994. var availableWinWidth = $('#remoteVideos').width() - 2 * numvids - 50;
  995. var availableWidth = availableWinWidth / numvids;
  996. var aspectRatio = 16.0 / 9.0;
  997. var maxHeight = Math.min(160, availableHeight);
  998. availableHeight = Math.min(maxHeight, availableWidth / aspectRatio);
  999. if (availableHeight < availableWidth / aspectRatio) {
  1000. availableWidth = Math.floor(availableHeight * aspectRatio);
  1001. }
  1002. return [availableWidth, availableHeight];
  1003. };
  1004. function resizeThumbnails() {
  1005. var thumbnailSize = calculateThumbnailSize();
  1006. var width = thumbnailSize[0];
  1007. var height = thumbnailSize[1];
  1008. // size videos so that while keeping AR and max height, we have a nice fit
  1009. $('#remoteVideos').height(height);
  1010. $('#remoteVideos>span').width(width);
  1011. $('#remoteVideos>span').height(height);
  1012. }
  1013. $(document).ready(function () {
  1014. Chat.init();
  1015. // Set the defaults for prompt dialogs.
  1016. jQuery.prompt.setDefaults({persistent: false});
  1017. // Set default desktop sharing method
  1018. setDesktopSharing(config.desktopSharing);
  1019. // Initialize Chrome extension inline installs
  1020. if (config.chromeExtensionId) {
  1021. initInlineInstalls();
  1022. }
  1023. // By default we use camera
  1024. getVideoSize = getCameraVideoSize;
  1025. getVideoPosition = getCameraVideoPosition;
  1026. resizeLargeVideoContainer();
  1027. $(window).resize(function () {
  1028. resizeLargeVideoContainer();
  1029. positionLarge();
  1030. });
  1031. // Listen for large video size updates
  1032. document.getElementById('largeVideo')
  1033. .addEventListener('loadedmetadata', function (e) {
  1034. currentVideoWidth = this.videoWidth;
  1035. currentVideoHeight = this.videoHeight;
  1036. positionLarge(currentVideoWidth, currentVideoHeight);
  1037. });
  1038. if (!$('#settings').is(':visible')) {
  1039. console.log('init');
  1040. init();
  1041. } else {
  1042. loginInfo.onsubmit = function (e) {
  1043. if (e.preventDefault) e.preventDefault();
  1044. $('#settings').hide();
  1045. init();
  1046. };
  1047. }
  1048. });
  1049. $(window).bind('beforeunload', function () {
  1050. if (connection && connection.connected) {
  1051. // ensure signout
  1052. $.ajax({
  1053. type: 'POST',
  1054. url: config.bosh,
  1055. async: false,
  1056. cache: false,
  1057. contentType: 'application/xml',
  1058. data: "<body rid='" + (connection.rid || connection._proto.rid) + "' xmlns='http://jabber.org/protocol/httpbind' sid='" + (connection.sid || connection._proto.sid) + "' type='terminate'><presence xmlns='jabber:client' type='unavailable'/></body>",
  1059. success: function (data) {
  1060. console.log('signed out');
  1061. console.log(data);
  1062. },
  1063. error: function (XMLHttpRequest, textStatus, errorThrown) {
  1064. console.log('signout error', textStatus + ' (' + errorThrown + ')');
  1065. }
  1066. });
  1067. }
  1068. disposeConference();
  1069. });
  1070. function disposeConference() {
  1071. var handler = getConferenceHandler();
  1072. if (handler && handler.peerconnection) {
  1073. // FIXME: probably removing streams is not required and close() should be enough
  1074. if (connection.jingle.localAudio) {
  1075. handler.peerconnection.removeStream(connection.jingle.localAudio);
  1076. }
  1077. if (connection.jingle.localVideo) {
  1078. handler.peerconnection.removeStream(connection.jingle.localVideo);
  1079. }
  1080. handler.peerconnection.close();
  1081. }
  1082. focus = null;
  1083. activecall = null;
  1084. }
  1085. function dump(elem, filename) {
  1086. elem = elem.parentNode;
  1087. elem.download = filename || 'meetlog.json';
  1088. elem.href = 'data:application/json;charset=utf-8,\n';
  1089. var data = {};
  1090. if (connection.jingle) {
  1091. Object.keys(connection.jingle.sessions).forEach(function (sid) {
  1092. var session = connection.jingle.sessions[sid];
  1093. if (session.peerconnection && session.peerconnection.updateLog) {
  1094. // FIXME: should probably be a .dump call
  1095. data["jingle_" + session.sid] = {
  1096. updateLog: session.peerconnection.updateLog,
  1097. stats: session.peerconnection.stats,
  1098. url: window.location.href
  1099. };
  1100. }
  1101. });
  1102. }
  1103. metadata = {};
  1104. metadata.time = new Date();
  1105. metadata.url = window.location.href;
  1106. metadata.ua = navigator.userAgent;
  1107. if (connection.logger) {
  1108. metadata.xmpp = connection.logger.log;
  1109. }
  1110. data.metadata = metadata;
  1111. elem.href += encodeURIComponent(JSON.stringify(data, null, ' '));
  1112. return false;
  1113. }
  1114. /**
  1115. * Changes the style class of the element given by id.
  1116. */
  1117. function buttonClick(id, classname) {
  1118. $(id).toggleClass(classname); // add the class to the clicked element
  1119. }
  1120. /**
  1121. * Shows a message to the user.
  1122. *
  1123. * @param titleString the title of the message
  1124. * @param messageString the text of the message
  1125. */
  1126. function openMessageDialog(titleString, messageString) {
  1127. $.prompt(messageString,
  1128. {
  1129. title: titleString,
  1130. persistent: false
  1131. }
  1132. );
  1133. }
  1134. /**
  1135. * Opens the lock room dialog.
  1136. */
  1137. function openLockDialog() {
  1138. // Only the focus is able to set a shared key.
  1139. if (focus === null) {
  1140. if (sharedKey)
  1141. $.prompt("This conversation is currently protected by a shared secret key.",
  1142. {
  1143. title: "Secrect key",
  1144. persistent: false
  1145. }
  1146. );
  1147. else
  1148. $.prompt("This conversation isn't currently protected by a secret key. Only the owner of the conference could set a shared key.",
  1149. {
  1150. title: "Secrect key",
  1151. persistent: false
  1152. }
  1153. );
  1154. } else {
  1155. if (sharedKey) {
  1156. $.prompt("Are you sure you would like to remove your secret key?",
  1157. {
  1158. title: "Remove secrect key",
  1159. persistent: false,
  1160. buttons: { "Remove": true, "Cancel": false},
  1161. defaultButton: 1,
  1162. submit: function (e, v, m, f) {
  1163. if (v) {
  1164. setSharedKey('');
  1165. lockRoom(false);
  1166. }
  1167. }
  1168. }
  1169. );
  1170. } else {
  1171. $.prompt('<h2>Set a secrect key to lock your room</h2>' +
  1172. '<input id="lockKey" type="text" placeholder="your shared key" autofocus>',
  1173. {
  1174. persistent: false,
  1175. buttons: { "Save": true, "Cancel": false},
  1176. defaultButton: 1,
  1177. loaded: function (event) {
  1178. document.getElementById('lockKey').focus();
  1179. },
  1180. submit: function (e, v, m, f) {
  1181. if (v) {
  1182. var lockKey = document.getElementById('lockKey');
  1183. if (lockKey.value) {
  1184. setSharedKey(Util.escapeHtml(lockKey.value));
  1185. lockRoom(true);
  1186. }
  1187. }
  1188. }
  1189. }
  1190. );
  1191. }
  1192. }
  1193. }
  1194. /**
  1195. * Opens the invite link dialog.
  1196. */
  1197. function openLinkDialog() {
  1198. var inviteLink;
  1199. if (roomUrl == null)
  1200. inviteLink = "Your conference is currently being created...";
  1201. else
  1202. inviteLink = encodeURI(roomUrl);
  1203. $.prompt('<input id="inviteLinkRef" type="text" value="' +
  1204. inviteLink + '" onclick="this.select();" readonly>',
  1205. {
  1206. title: "Share this link with everyone you want to invite",
  1207. persistent: false,
  1208. buttons: { "Invite": true, "Cancel": false},
  1209. defaultButton: 1,
  1210. loaded: function (event) {
  1211. if (roomUrl)
  1212. document.getElementById('inviteLinkRef').select();
  1213. else
  1214. document.getElementById('jqi_state0_buttonInvite')
  1215. .disabled = true;
  1216. },
  1217. submit: function (e, v, m, f) {
  1218. if (v) {
  1219. if (roomUrl) {
  1220. inviteParticipants();
  1221. }
  1222. }
  1223. }
  1224. }
  1225. );
  1226. }
  1227. /**
  1228. * Invite participants to conference.
  1229. */
  1230. function inviteParticipants() {
  1231. if (roomUrl == null)
  1232. return;
  1233. var conferenceName = roomUrl.substring(roomUrl.lastIndexOf('/') + 1);
  1234. var subject = "Invitation to a Jitsi Meet (" + conferenceName + ")";
  1235. var body = "Hey there, I%27d like to invite you to a Jitsi Meet"
  1236. + " conference I%27ve just set up.%0D%0A%0D%0A"
  1237. + "Please click on the following link in order"
  1238. + " to join the conference.%0D%0A%0D%0A"
  1239. + roomUrl + "%0D%0A%0D%0A"
  1240. + "Note that Jitsi Meet is currently only supported by Chromim,"
  1241. + " Google Chrome and Opera, so you need"
  1242. + " to be using one of these browsers.%0D%0A%0D%0A"
  1243. + "Talk to you in a sec!";
  1244. if (window.localStorage.displayname)
  1245. body += "%0D%0A%0D%0A" + window.localStorage.displayname;
  1246. window.open("mailto:?subject=" + subject + "&body=" + body, '_blank');
  1247. }
  1248. /**
  1249. * Opens the settings dialog.
  1250. */
  1251. function openSettingsDialog() {
  1252. $.prompt('<h2>Configure your conference</h2>' +
  1253. '<input type="checkbox" id="initMuted"> Participants join muted<br/>' +
  1254. '<input type="checkbox" id="requireNicknames"> Require nicknames<br/><br/>' +
  1255. 'Set a secrect key to lock your room: <input id="lockKey" type="text" placeholder="your shared key" autofocus>',
  1256. {
  1257. persistent: false,
  1258. buttons: { "Save": true, "Cancel": false},
  1259. defaultButton: 1,
  1260. loaded: function (event) {
  1261. document.getElementById('lockKey').focus();
  1262. },
  1263. submit: function (e, v, m, f) {
  1264. if (v) {
  1265. if ($('#initMuted').is(":checked")) {
  1266. // it is checked
  1267. }
  1268. if ($('#requireNicknames').is(":checked")) {
  1269. // it is checked
  1270. }
  1271. /*
  1272. var lockKey = document.getElementById('lockKey');
  1273. if (lockKey.value)
  1274. {
  1275. setSharedKey(lockKey.value);
  1276. lockRoom(true);
  1277. }
  1278. */
  1279. }
  1280. }
  1281. }
  1282. );
  1283. }
  1284. /**
  1285. * Locks / unlocks the room.
  1286. */
  1287. function lockRoom(lock) {
  1288. if (lock)
  1289. connection.emuc.lockRoom(sharedKey);
  1290. else
  1291. connection.emuc.lockRoom('');
  1292. updateLockButton();
  1293. }
  1294. /**
  1295. * Sets the shared key.
  1296. */
  1297. function setSharedKey(sKey) {
  1298. sharedKey = sKey;
  1299. }
  1300. /**
  1301. * Updates the lock button state.
  1302. */
  1303. function updateLockButton() {
  1304. buttonClick("#lockIcon", "icon-security icon-security-locked");
  1305. }
  1306. /**
  1307. * Hides the toolbar.
  1308. */
  1309. var hideToolbar = function () {
  1310. var isToolbarHover = false;
  1311. $('#header').find('*').each(function () {
  1312. var id = $(this).attr('id');
  1313. if ($("#" + id + ":hover").length > 0) {
  1314. isToolbarHover = true;
  1315. }
  1316. });
  1317. clearTimeout(toolbarTimeout);
  1318. toolbarTimeout = null;
  1319. if (!isToolbarHover) {
  1320. $('#header').hide("slide", { direction: "up", duration: 300});
  1321. }
  1322. else {
  1323. toolbarTimeout = setTimeout(hideToolbar, 2000);
  1324. }
  1325. };
  1326. /**
  1327. * Shows the call main toolbar.
  1328. */
  1329. function showToolbar() {
  1330. if (!$('#header').is(':visible')) {
  1331. $('#header').show("slide", { direction: "up", duration: 300});
  1332. if (toolbarTimeout) {
  1333. clearTimeout(toolbarTimeout);
  1334. toolbarTimeout = null;
  1335. }
  1336. toolbarTimeout = setTimeout(hideToolbar, 2000);
  1337. }
  1338. if (focus != null)
  1339. {
  1340. // TODO: Enable settings functionality. Need to uncomment the settings button in index.html.
  1341. // $('#settingsButton').css({visibility:"visible"});
  1342. }
  1343. // Show/hide desktop sharing button
  1344. showDesktopSharingButton();
  1345. }
  1346. /**
  1347. * Docks/undocks the toolbar.
  1348. *
  1349. * @param isDock indicates what operation to perform
  1350. */
  1351. function dockToolbar(isDock) {
  1352. if (isDock) {
  1353. // First make sure the toolbar is shown.
  1354. if (!$('#header').is(':visible')) {
  1355. showToolbar();
  1356. }
  1357. // Then clear the time out, to dock the toolbar.
  1358. clearTimeout(toolbarTimeout);
  1359. toolbarTimeout = null;
  1360. }
  1361. else {
  1362. if (!$('#header').is(':visible')) {
  1363. showToolbar();
  1364. }
  1365. else {
  1366. toolbarTimeout = setTimeout(hideToolbar, 2000);
  1367. }
  1368. }
  1369. }
  1370. /**
  1371. * Updates the room invite url.
  1372. */
  1373. function updateRoomUrl(newRoomUrl) {
  1374. roomUrl = newRoomUrl;
  1375. // If the invite dialog has been already opened we update the information.
  1376. var inviteLink = document.getElementById('inviteLinkRef');
  1377. if (inviteLink) {
  1378. inviteLink.value = roomUrl;
  1379. inviteLink.select();
  1380. document.getElementById('jqi_state0_buttonInvite').disabled = false;
  1381. }
  1382. }
  1383. /**
  1384. * Warning to the user that the conference window is about to be closed.
  1385. */
  1386. function closePageWarning() {
  1387. if (focus !== null)
  1388. return "You are the owner of this conference call and you are about to end it.";
  1389. else
  1390. return "You are about to leave this conversation.";
  1391. }
  1392. /**
  1393. * Shows a visual indicator for the focus of the conference.
  1394. * Currently if we're not the owner of the conference we obtain the focus
  1395. * from the connection.jingle.sessions.
  1396. */
  1397. function showFocusIndicator() {
  1398. if (focus !== null) {
  1399. var indicatorSpan = $('#localVideoContainer .focusindicator');
  1400. if (indicatorSpan.children().length === 0)
  1401. {
  1402. createFocusIndicatorElement(indicatorSpan[0]);
  1403. }
  1404. }
  1405. else if (Object.keys(connection.jingle.sessions).length > 0) {
  1406. // If we're only a participant the focus will be the only session we have.
  1407. var session
  1408. = connection.jingle.sessions
  1409. [Object.keys(connection.jingle.sessions)[0]];
  1410. var focusId
  1411. = 'participant_' + Strophe.getResourceFromJid(session.peerjid);
  1412. var focusContainer = document.getElementById(focusId);
  1413. if (!focusContainer) {
  1414. console.error("No focus container!");
  1415. return;
  1416. }
  1417. var indicatorSpan = $('#' + focusId + ' .focusindicator');
  1418. if (!indicatorSpan || indicatorSpan.length === 0) {
  1419. indicatorSpan = document.createElement('span');
  1420. indicatorSpan.className = 'focusindicator';
  1421. focusContainer.appendChild(indicatorSpan);
  1422. createFocusIndicatorElement(indicatorSpan);
  1423. }
  1424. }
  1425. }
  1426. /**
  1427. * Checks if container for participant identified by given peerJid exists in the document and creates it eventually.
  1428. * @param peerJid peer Jid to check.
  1429. */
  1430. function ensurePeerContainerExists(peerJid) {
  1431. var peerResource = Strophe.getResourceFromJid(peerJid);
  1432. var videoSpanId = 'participant_' + peerResource;
  1433. if ($('#' + videoSpanId).length > 0) {
  1434. // If there's been a focus change, make sure we add focus related
  1435. // interface!!
  1436. if (focus && $('#remote_popupmenu_' + peerResource).length <= 0)
  1437. addRemoteVideoMenu( peerJid,
  1438. document.getElementById(videoSpanId));
  1439. return;
  1440. }
  1441. var container = addRemoteVideoContainer(peerJid, videoSpanId);
  1442. var nickfield = document.createElement('span');
  1443. nickfield.className = "nick";
  1444. nickfield.appendChild(document.createTextNode(peerResource));
  1445. container.appendChild(nickfield);
  1446. resizeThumbnails();
  1447. }
  1448. function addRemoteVideoContainer(peerJid, spanId) {
  1449. var container = document.createElement('span');
  1450. container.id = spanId;
  1451. container.className = 'videocontainer';
  1452. var remotes = document.getElementById('remoteVideos');
  1453. // If the peerJid is null then this video span couldn't be directly
  1454. // associated with a participant (this could happen in the case of prezi).
  1455. if (focus && peerJid != null)
  1456. addRemoteVideoMenu(peerJid, container);
  1457. remotes.appendChild(container);
  1458. return container;
  1459. }
  1460. /**
  1461. * Creates the element indicating the focus of the conference.
  1462. */
  1463. function createFocusIndicatorElement(parentElement) {
  1464. var focusIndicator = document.createElement('i');
  1465. focusIndicator.className = 'fa fa-star';
  1466. focusIndicator.title = "The owner of this conference";
  1467. parentElement.appendChild(focusIndicator);
  1468. }
  1469. function addRemoteVideoMenu(jid, parentElement) {
  1470. var spanElement = document.createElement('span');
  1471. spanElement.className = 'remotevideomenu';
  1472. parentElement.appendChild(spanElement);
  1473. var menuElement = document.createElement('i');
  1474. menuElement.className = 'fa fa-angle-down';
  1475. menuElement.title = 'Remote user controls';
  1476. spanElement.appendChild(menuElement);
  1477. // <ul class="popupmenu">
  1478. // <li><a href="#">Mute</a></li>
  1479. // <li><a href="#">Eject</a></li>
  1480. // </ul>
  1481. var popupmenuElement = document.createElement('ul');
  1482. popupmenuElement.className = 'popupmenu';
  1483. popupmenuElement.id = 'remote_popupmenu_' + Strophe.getResourceFromJid(jid);
  1484. spanElement.appendChild(popupmenuElement);
  1485. var muteMenuItem = document.createElement('li');
  1486. var muteLinkItem = document.createElement('a');
  1487. var mutedIndicator = "<span><i class='icon-mic-disabled'></i></span>";
  1488. if (!mutedAudios[jid]) {
  1489. muteLinkItem.innerHTML = mutedIndicator + 'Mute';
  1490. muteLinkItem.className = 'mutelink';
  1491. }
  1492. else {
  1493. muteLinkItem.innerHTML = mutedIndicator + 'Muted';
  1494. muteLinkItem.className = 'mutelink disabled';
  1495. }
  1496. muteLinkItem.onclick = function(){
  1497. if ($(this).attr('disabled') != undefined) {
  1498. event.preventDefault();
  1499. }
  1500. var isMute = !mutedAudios[jid];
  1501. connection.moderate.setMute(jid, isMute);
  1502. popupmenuElement.setAttribute('style', 'display:none;');
  1503. if (isMute) {
  1504. this.innerHTML = mutedIndicator + 'Muted';
  1505. this.className = 'mutelink disabled';
  1506. }
  1507. else {
  1508. this.innerHTML = mutedIndicator + 'Mute';
  1509. this.className = 'mutelink';
  1510. }
  1511. };
  1512. muteMenuItem.appendChild(muteLinkItem);
  1513. popupmenuElement.appendChild(muteMenuItem);
  1514. var ejectIndicator = "<span><i class='icon-kick'></i></span>";
  1515. var ejectMenuItem = document.createElement('li');
  1516. var ejectLinkItem = document.createElement('a');
  1517. ejectLinkItem.innerHTML = ejectIndicator + 'Kick out';
  1518. ejectLinkItem.onclick = function(){
  1519. connection.moderate.eject(jid);
  1520. popupmenuElement.setAttribute('style', 'display:none;');
  1521. };
  1522. ejectMenuItem.appendChild(ejectLinkItem);
  1523. popupmenuElement.appendChild(ejectMenuItem);
  1524. }
  1525. function updateRemoteVideoMenu(jid, isMuted) {
  1526. var muteMenuItem
  1527. = $('#remote_popupmenu_'
  1528. + Strophe.getResourceFromJid(jid)
  1529. + '>li>a.mutelink');
  1530. var mutedIndicator = "<span><i class='icon-mic-disabled'></i></span>";
  1531. if (muteMenuItem.length) {
  1532. var muteLink = muteMenuItem.get(0);
  1533. if (isMuted === 'true') {
  1534. muteLink.innerHTML = mutedIndicator + 'Muted';
  1535. muteLink.className = 'mutelink disabled';
  1536. }
  1537. else {
  1538. muteLink.innerHTML = mutedIndicator + 'Mute';
  1539. muteLink.className = 'mutelink';
  1540. }
  1541. }
  1542. }
  1543. /**
  1544. * Toggles the application in and out of full screen mode
  1545. * (a.k.a. presentation mode in Chrome).
  1546. */
  1547. function toggleFullScreen() {
  1548. var fsElement = document.documentElement;
  1549. if (!document.mozFullScreen && !document.webkitIsFullScreen) {
  1550. //Enter Full Screen
  1551. if (fsElement.mozRequestFullScreen) {
  1552. fsElement.mozRequestFullScreen();
  1553. }
  1554. else {
  1555. fsElement.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
  1556. }
  1557. } else {
  1558. //Exit Full Screen
  1559. if (document.mozCancelFullScreen) {
  1560. document.mozCancelFullScreen();
  1561. } else {
  1562. document.webkitCancelFullScreen();
  1563. }
  1564. }
  1565. }
  1566. /**
  1567. * Shows the display name for the given video.
  1568. */
  1569. function showDisplayName(videoSpanId, displayName) {
  1570. var nameSpan = $('#' + videoSpanId + '>span.displayname');
  1571. // If we already have a display name for this video.
  1572. if (nameSpan.length > 0) {
  1573. var nameSpanElement = nameSpan.get(0);
  1574. if (nameSpanElement.id === 'localDisplayName' &&
  1575. $('#localDisplayName').text() !== displayName) {
  1576. $('#localDisplayName').text(displayName);
  1577. } else {
  1578. $('#' + videoSpanId + '_name').text(displayName);
  1579. }
  1580. } else {
  1581. var editButton = null;
  1582. if (videoSpanId === 'localVideoContainer') {
  1583. editButton = createEditDisplayNameButton();
  1584. }
  1585. if (displayName.length) {
  1586. nameSpan = document.createElement('span');
  1587. nameSpan.className = 'displayname';
  1588. nameSpan.innerText = displayName;
  1589. $('#' + videoSpanId)[0].appendChild(nameSpan);
  1590. }
  1591. if (!editButton) {
  1592. nameSpan.id = videoSpanId + '_name';
  1593. } else {
  1594. nameSpan.id = 'localDisplayName';
  1595. $('#' + videoSpanId)[0].appendChild(editButton);
  1596. var editableText = document.createElement('input');
  1597. editableText.className = 'displayname';
  1598. editableText.id = 'editDisplayName';
  1599. if (displayName.length) {
  1600. editableText.value = displayName.substring(0, displayName.indexOf(' (me)'));
  1601. }
  1602. editableText.setAttribute('style', 'display:none;');
  1603. editableText.setAttribute('placeholder', 'ex. Jane Pink');
  1604. $('#' + videoSpanId)[0].appendChild(editableText);
  1605. $('#localVideoContainer .displayname').bind("click", function (e) {
  1606. e.preventDefault();
  1607. $('#localDisplayName').hide();
  1608. $('#editDisplayName').show();
  1609. $('#editDisplayName').focus();
  1610. $('#editDisplayName').select();
  1611. var inputDisplayNameHandler = function (name) {
  1612. if (nickname !== name) {
  1613. nickname = name;
  1614. window.localStorage.displayname = nickname;
  1615. connection.emuc.addDisplayNameToPresence(nickname);
  1616. connection.emuc.sendPresence();
  1617. Chat.setChatConversationMode(true);
  1618. }
  1619. if (!$('#localDisplayName').is(":visible")) {
  1620. $('#localDisplayName').text(nickname + " (me)");
  1621. $('#localDisplayName').show();
  1622. $('#editDisplayName').hide();
  1623. }
  1624. };
  1625. $('#editDisplayName').one("focusout", function (e) {
  1626. inputDisplayNameHandler(this.value);
  1627. });
  1628. $('#editDisplayName').on('keydown', function (e) {
  1629. if (e.keyCode === 13) {
  1630. e.preventDefault();
  1631. inputDisplayNameHandler(this.value);
  1632. }
  1633. });
  1634. });
  1635. }
  1636. }
  1637. }
  1638. /**
  1639. * Creates the edit display name button.
  1640. *
  1641. * @returns the edit button
  1642. */
  1643. function createEditDisplayNameButton() {
  1644. var editButton = document.createElement('a');
  1645. editButton.className = 'displayname';
  1646. editButton.innerHTML = '<i class="fa fa-pencil"></i>';
  1647. return editButton;
  1648. }
  1649. /**
  1650. * Shows audio muted indicator over small videos.
  1651. */
  1652. function showAudioIndicator(videoSpanId, isMuted) {
  1653. var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted');
  1654. if (isMuted === 'false') {
  1655. if (audioMutedSpan.length > 0) {
  1656. audioMutedSpan.remove();
  1657. }
  1658. }
  1659. else {
  1660. var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted');
  1661. audioMutedSpan = document.createElement('span');
  1662. audioMutedSpan.className = 'audioMuted';
  1663. if (videoMutedSpan) {
  1664. audioMutedSpan.right = '30px';
  1665. }
  1666. $('#' + videoSpanId)[0].appendChild(audioMutedSpan);
  1667. var mutedIndicator = document.createElement('i');
  1668. mutedIndicator.className = 'icon-mic-disabled';
  1669. mutedIndicator.title = "Participant is muted";
  1670. audioMutedSpan.appendChild(mutedIndicator);
  1671. }
  1672. }
  1673. /**
  1674. * Shows video muted indicator over small videos.
  1675. */
  1676. function showVideoIndicator(videoSpanId, isMuted) {
  1677. var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted');
  1678. if (isMuted === 'false') {
  1679. if (videoMutedSpan.length > 0) {
  1680. videoMutedSpan.remove();
  1681. }
  1682. }
  1683. else {
  1684. var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted');
  1685. videoMutedSpan = document.createElement('span');
  1686. videoMutedSpan.className = 'videoMuted';
  1687. if (audioMutedSpan) {
  1688. videoMutedSpan.right = '30px';
  1689. }
  1690. $('#' + videoSpanId)[0].appendChild(videoMutedSpan);
  1691. var mutedIndicator = document.createElement('i');
  1692. mutedIndicator.className = 'icon-camera-disabled';
  1693. mutedIndicator.title = "Participant has stopped the camera.";
  1694. videoMutedSpan.appendChild(mutedIndicator);
  1695. }
  1696. }
  1697. /**
  1698. * Resizes and repositions videos in full screen mode.
  1699. */
  1700. $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange',
  1701. function () {
  1702. resizeLargeVideoContainer();
  1703. positionLarge();
  1704. isFullScreen = document.fullScreen ||
  1705. document.mozFullScreen ||
  1706. document.webkitIsFullScreen;
  1707. if (isFullScreen) {
  1708. setView("fullscreen");
  1709. }
  1710. else {
  1711. setView("default");
  1712. }
  1713. }
  1714. );
  1715. /**
  1716. * Sets the current view.
  1717. */
  1718. function setView(viewName) {
  1719. // if (viewName == "fullscreen") {
  1720. // document.getElementById('videolayout_fullscreen').disabled = false;
  1721. // document.getElementById('videolayout_default').disabled = true;
  1722. // }
  1723. // else {
  1724. // document.getElementById('videolayout_default').disabled = false;
  1725. // document.getElementById('videolayout_fullscreen').disabled = true;
  1726. // }
  1727. }