/* Jappix - An open social platform These are the call common management functions ------------------------------------------------- License: AGPL Author: Valérian Saliou */ // Bundle var Call = (function() { /** * Alias of this * @private */ var self = {}; /* Variables */ self._start_stamp = 0; /** * Provides an adapter to the JSJaCJingle console implementation which is different * @private * @return {object} */ self._consoleAdapter = (function() { /** * Alias of this * @private */ var _console = {}; /** * Console logging interface (adapted) * @public * @param {string} message * @param {number} loglevel * @return {undefined} */ _console.log = function(message, loglevel) { try { if(!message) { throw 'No message passed to console adapter!'; } switch(loglevel) { case 0: Console.warn(message); break; case 1: Console.error(message); break; case 2: Console.info(message); break; case 4: Console.debug(message); break; default: Console.log(message); } } catch(e) { Console.error('Call._consoleAdapter.log', e); } }; /** * Return sub-class scope */ return _console; })(); /** * Initializes Jingle router * @public * @return {undefined} */ self.init = function() { try { // Listen for incoming Jingle/Muji packet JSJaCJingle.listen({ connection: con, debug: self._consoleAdapter, // TODO: setting a fallback fucks up some calls... // fallback: './server/jingle.php', single_initiate: function(stanza) { try { // Already in a call? if(self.is_ongoing()) { // Try to restore SID there var stanza_id = stanza.getID(); var sid = null; if(stanza_id) { var stanza_id_split = stanza_id.split('_'); sid = stanza_id_split[1]; } // Build a temporary Jingle session var jingle_close = new JSJaCJingle.session( JSJAC_JINGLE_SESSION_SINGLE, { to: stanza.getFrom(), debug: JSJaCJingleStorage.get_debug() } ); if(sid) { jingle_close._set_sid(sid); } jingle_close.terminate(JSJAC_JINGLE_REASON_BUSY); Console.warn('session_initiate_success', 'Dropped incoming call because already in a call.'); return; } var xid = Common.fullXID(Common.getStanzaFrom(stanza)); Console.info('Incoming call from: ' + xid); // Session values Jingle.receive(xid, stanza); } catch(e) { Console.error('Call.init[single_initiate]', e); } }, // Receive a multiparty (Muji) call muji_invite: function(stanza, args) { try { if(!self.is_ongoing()) { // Session values Muji.receive(args, stanza); } } catch(e) { Console.error('Call.init[muji_invite]', e); } } }); // Enable Jingle/Muji UI elements if plugin could start if(JSJAC_JINGLE_AVAILABLE) { $('.jingle-hidable, .muji-hidable').show(); } } catch(e) { Console.error('Call.init', e); } }; /** * Opens the call interface * @public * @return {undefined} */ self.open = function() { try { if(Jingle.in_call()) { Jingle.open(); } else if(Muji.in_call()) { Muji.open(); } } catch(e) { Console.error('Call.open', e); } }; /** * Stops current call * @public * @return {boolean} */ self.stop = function() { try { Jingle.stop(); Muji.stop(); } catch(e) { Console.error('Call.stop', e); } finally { return false; } }; /** * Mutes current call * @public * @param {object} session * @param {object} controls * @return {undefined} */ self.mute = function(session, controls) { try { if(session) { // Toggle interface buttons controls.filter('.mute').hide(); controls.filter('.unmute').show(); // Actually mute audio stream if(session.get_mute(JSJAC_JINGLE_MEDIA_AUDIO) === false) { session.mute(JSJAC_JINGLE_MEDIA_AUDIO); } } } catch(e) { Console.error('Call.mute', e); } }; /** * Unmutes current call * @public * @param {object} session * @param {object} controls * @return {undefined} */ self.unmute = function(session, controls) { try { if(session) { controls.filter('.unmute').hide(); controls.filter('.mute').show(); if(session.get_mute(JSJAC_JINGLE_MEDIA_AUDIO) === true) { session.unmute(JSJAC_JINGLE_MEDIA_AUDIO); } } } catch(e) { Console.error('Call.mute', e); } }; /** * Checks whether user is in call or not * @public * @return {boolean} */ self.is_ongoing = function() { is_ongoing = false; try { is_ongoing = (Jingle.in_call() === true || Muji.in_call() === true); } catch(e) { Console.error('Call.is_ongoing', e); } finally { return is_ongoing; } }; /** * Checks if the given call SID is the same as the current call's one * @public * @param {object} session * @param {object} compare_session * @return {boolean} */ self.is_same_sid = function(session, compare_session) { is_same = false; try { if(compare_session && session && compare_session.get_sid() === session.get_sid()) { is_same = true; } } catch(e) { Console.error('Call.is_same_sid', e); } finally { return is_same; } }; /** * Returns if current call is audio * @public * @param {object} session * @return {boolean} */ self.is_audio = function(session) { audio = false; try { if(session && session.get_media() === JSJAC_JINGLE_MEDIA_AUDIO) { audio = true; } } catch(e) { Console.error('Call.is_audio', e); } finally { return audio; } }; /** * Returns if current call is video * @public * @param {object} session * @return {boolean} */ self.is_video = function(session) { video = false; try { if(session && session.get_media() === JSJAC_JINGLE_MEDIA_VIDEO) { video = true; } } catch(e) { Console.error('Call.is_video', e); } finally { return video; } }; /** * Set the Muji session as started * @public * @param {string} mode * @return {boolean} */ self.start_session = function(mode) { try { if(!(mode in JSJAC_JINGLE_MEDIAS)) { throw 'Unknown mode: ' + (mode || 'none'); } var call_tool_sel = $('#top-content .tools.call'); call_tool_sel.removeClass('audio video active'); call_tool_sel.addClass('streaming').addClass(mode); Console.info('Call session successfully started, mode: ' + (mode || 'none')); } catch(e) { Console.error('Call.start_session', e); } finally { return false; } }; /** * Set the Jingle session as stopped * @public * @param {string} mode * @return {boolean} */ self.stop_session = function() { try { $('#top-content .tools.call').removeClass('audio video active streaming'); Console.info('Call session successfully stopped'); } catch(e) { Console.error('Call.stop_session', e); } finally { return false; } }; /** * Generates ICE servers configuration * @public * @return {object} */ self.generate_ice_servers = function() { ice_servers = { stun: [], turn: [] }; try { if(HOST_STUN) { ice_servers.stun.push({ 'host': HOST_STUN }); } if(HOST_TURN) { ice_servers.turn.push({ 'host': HOST_TURN, 'username': HOST_TURN_USERNAME, 'credential': HOST_TURN_PASSWORD }); } } catch(e) { Console.error('Call.generate_ice_servers', e); } finally { return is_ongoing; } }; /** * Returns the notification map (based on call type) * @private * @param {string} call_type * @return {object} */ self._get_notify_map = function(call_type) { var map = {}; try { switch(call_type) { case JSJAC_JINGLE_SESSION_SINGLE: map = Jingle._notify_map(); break; case JSJAC_JINGLE_SESSION_MUJI: map = Muji._notify_map(); break; default: return; } } catch(e) { Console.error('Call._get_notify_map', e); } finally { return map; } }; /** * Notify for something related to calls * @public * @param {string} call_type * @param {string} xid * @param {string} type * @param {string} mode * @return {boolean} */ self.notify = function(call_type, xid, type, mode, sender_xid) { try { sender_xid = sender_xid || xid; // Notification data map var map = self._get_notify_map(call_type); if(!(type in map)) { throw 'Notification type not recognized!'; } // Selectors var call_tools_all_sel = $('#top-content .tools-all:has(.tools.call)'); var call_tool_sel = call_tools_all_sel.find('.tools.call'); var call_content_sel = call_tools_all_sel.find('.call-content'); var call_subitem_sel = call_content_sel.find('.tools-content-subitem'); // Generate proper full name var fullname; if(call_type === JSJAC_JINGLE_SESSION_MUJI && sender_xid === Common.getXID()) { fullname = Common._e("Conference call"); } else { fullname = Name.getBuddy(sender_xid).htmlEnc(); } // Generate buttons code var buttons_html = ''; var i = 0; if(typeof map[type].buttons === 'object') { $.each(map[type].buttons, function(button, attrs) { buttons_html += '' + attrs.text + ''; }); } // Append notification to DOM call_subitem_sel.html( '
' + '
' + '
' + '' + '
' + '' + '
' + '
' + '' + fullname + '' + '' + map[type].text + '' + '
' + buttons_html + '
' + '
' + '
' ); // Apply user avatar Avatar.get(sender_xid, 'cache', 'true', 'forget'); // Apply button events if(typeof map[type].buttons === 'object') { $.each(map[type].buttons, function(button, attrs) { call_tools_all_sel.find('a.reply-button[data-action="' + button + '"]').click(function() { try { // Remove notification self._unnotify(xid); // Execute callback, if any if(typeof attrs.cb === 'function') { attrs.cb(xid, mode); } Console.info('Closed call notification drawer'); } catch(e) { Console.error('Call.notify[async]', e); } finally { return false; } }); }); } // Enable notification box! call_tool_sel.addClass('active'); // Open notification box! call_content_sel.show(); } catch(e) { Console.error('Call.notify', e); } finally { return false; } }; /** * Remove notification * @private * @return {boolean} */ self._unnotify = function() { try { // Selectors var call_tools_all_sel = $('#top-content .tools-all:has(.tools.call)'); var call_tool_sel = call_tools_all_sel.find('.tools.call'); var call_content_sel = call_tools_all_sel.find('.call-content'); var call_subitem_sel = call_content_sel.find('.tools-content-subitem'); // Close & disable notification box call_content_sel.hide(); call_subitem_sel.empty(); call_tool_sel.removeClass('active'); // Stop all sounds Audio.stop('incoming-call'); Audio.stop('outgoing-call'); } catch(e) { Console.error('Call._unnotify', e); } finally { return false; } }; /** * Processes the video elements size * @private * @param {object} screen * @param {object} video * @return {object} */ self._process_size = function(screen, video) { try { if(!(typeof screen === 'object' && typeof video === 'object')) { throw 'Invalid object passed, aborting!'; } // Get the intrinsic size of the video var video_w = video[0].videoWidth || video.width(); var video_h = video[0].videoHeight || video.height(); // Get the screen size of the video var screen_w = screen.width(); var screen_h = screen.height(); // Process resize ratios (2 cases) var r_1 = screen_h / video_h; var r_2 = screen_w / video_w; // Process resized video sizes var video_w_1 = video_w * r_1; var video_h_1 = video_h * r_1; var video_w_2 = video_w * r_2; var video_h_2 = video_h * r_2; // DOM view modifiers var dom_width = 'auto'; var dom_height = 'auto'; var dom_left = 0; var dom_top = 0; // Landscape/Portrait/Equal container? if(video_w > video_h || (video_h == video_w && screen_w < screen_h)) { // Not sufficient? if(video_w_1 < screen_w) { dom_width = screen_w + 'px'; dom_top = -1 * (video_h_2 - screen_h) / 2; } else { dom_height = screen_h + 'px'; dom_left = -1 * (video_w_1 - screen_w) / 2; } } else if(video_h > video_w || (video_h == video_w && screen_w > screen_h)) { // Not sufficient? if(video_h_1 < screen_h) { dom_height = screen_h + 'px'; dom_left = -1 * (video_w_1 - screen_w) / 2; } else { dom_width = screen_w + 'px'; dom_top = -1 * (video_h_2 - screen_h) / 2; } } else if(screen_w == screen_h) { dom_width = screen_w + 'px'; dom_height = screen_h + 'px'; } return { width : dom_width, height : dom_height, left : dom_left, top : dom_top }; } catch(e) { Console.error('Call._process_size', e); } }; /** * Adapts the local video view * @public * @param {object} local_sel * @return {undefined} */ self.adapt_local = function(local_sel) { try { var local_video_sel = local_sel.find('video'); // Process new sizes var sizes = Call._process_size( local_sel, local_video_sel ); // Apply new sizes local_video_sel.css({ 'height': sizes.height, 'width': sizes.width, 'margin-top': sizes.top, 'margin-left': sizes.left }); } catch(e) { Console.error('Call.adapt_local', e); } }; /** * Adapts the remote video view * @public * @param {object} videobox_sel * @return {undefined} */ self.adapt_remote = function(videobox_sel) { try { var remote_video_sel, sizes; videobox_sel.find('.remote_video').each(function() { remote_video_sel = $(this).find('video'); if(remote_video_sel.size()) { // Process new sizes sizes = Call._process_size( $(this), remote_video_sel ); // Apply new sizes remote_video_sel.css({ 'height': sizes.height, 'width': sizes.width, 'margin-top': sizes.top, 'margin-left': sizes.left }); } }); } catch(e) { Console.error('Call.adapt_remote', e); } }; /** * Start call elpsed time counter * @public * @return {boolean} */ self.start_counter = function() { try { // Initialize counter self.stop_counter(); self._start_stamp = DateUtils.getTimeStamp(); self._fire_clock(); // Fire it every second $('#top-content .tools.call .counter').everyTime('1s', self._fire_clock); Console.info('Call counter started'); } catch(e) { Console.error('Call.start_counter', e); } finally { return false; } }; /** * Stop call elpsed time counter * @public * @return {boolean} */ self.stop_counter = function() { try { // Reset stamp storage self._start_stamp = 0; // Reset counter var counter_sel = $('#top-content .tools.call .counter'); var default_count = counter_sel.attr('data-default'); counter_sel.stopTime(); $('#top-content .tools.call .counter').text(default_count); $('#jingle, #muji').find('.elapsed').text(default_count); Console.info('Call counter stopped'); } catch(e) { Console.error('Call.stop_counter', e); } finally { return false; } }; /** * Fires the counter clock (once more) * @private * @return {undefined} */ self._fire_clock = function() { try { // Process updated time var count = DateUtils.difference( DateUtils.getTimeStamp(), self._start_stamp ); if(count.getHours()) { count = count.toString('H:mm:ss'); } else { count = count.toString('mm:ss'); } // Display updated counter $('#top-content .tools.call .counter').text(count); $('#jingle, #muji').find('.elapsed').text(count); } catch(e) { Console.error('Call._fire_clock', e); } }; /** * Destroy the call interface * @public * @return {undefined} */ self.destroy_interface = function(container_sel) { try { container_sel.stopTime(); container_sel.find('*').stopTime(); container_sel.remove(); } catch(e) { Console.error('Call.destroy_interface', e); } }; /** * Show the call interface * @public * @param {object} manager * @param {object} call_sel * @param {object} video_container_sel * @return {boolean} */ self.show_interface = function(manager, call_sel, video_container_sel) { try { if(manager.in_call()) { call_sel.filter(':hidden').show(); // Launch back some events video_container_sel.mousemove(); } } catch(e) { Console.error('Call.show_interface', e); } finally { return false; } }; /** * Hide the call interface * @public * @param {object} call_sel * @param {object} video_container_sel * @return {boolean} */ self.hide_interface = function(call_sel, video_container_sel) { try { call_sel.filter(':visible').hide(); // Reset some events video_container_sel.find('.topbar').stopTime().hide(); } catch(e) { Console.error('Call.hide_interface', e); } finally { return false; } }; /** * Attaches interface events * @public * @param {object} manager * @param {object} call_sel * @param {object} video_container_sel * @return {undefined} */ self.events_interface = function(manager, call_sel, video_container_sel) { try { call_sel.everyTime(50, function() { manager._adapt(); }); // Close interface on click on semi-transparent background call_sel.click(function(evt) { try { // Click on lock background? if($(evt.target).is('.lock')) { return manager._hide_interface(); } } catch(e) { Console.error('Call.events_interface[async]', e); } }); // Click on a control or action button call_sel.find('.topbar').find('.controls a, .actions a').click(function() { try { switch($(this).data('type')) { case 'close': manager._hide_interface(); break; case 'stop': case 'leave': manager.stop(); break; case 'mute': manager.mute(); break; case 'unmute': manager.unmute(); break; } } catch(e) { Console.error('Call.events_interface[async]', e); } finally { return false; } }); // Auto Hide/Show interface topbar video_container_sel.mousemove(function() { try { var topbar_sel = $(this).find('.topbar'); if(topbar_sel.is(':hidden')) { topbar_sel.stop(true).fadeIn(250); } topbar_sel.stopTime(); topbar_sel.oneTime('5s', function() { topbar_sel.stop(true).fadeOut(250); }); } catch(e) { Console.error('Call.events_interface[async]', e); } }); } catch(e) { Console.error('Call.events_interface', e); } }; /** * Return class scope */ return self; })();