Commit 1db630d0 authored by Dele Olajide's avatar Dele Olajide Committed by dele

Jitsi-Videobridge plugin : Replaced jitmeet with ofmeet

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/trunk@13852 b35dd754-fafc-0310-a699-88a17e54d16e
parent 15c082a4
......@@ -47,7 +47,7 @@ Jitsi Video Bridge Plugin Changelog
<p><b>1.1</b> -- Jan 4th, 2014</p>
<ul>
<li>OF-716 Added to Openfire plugins with jitmeet web video conference application</li>
<li>OF-716 Added to Openfire plugins with embedded ofmeet web video conference application</li>
</ul>
<p><b>1.0</b> -- Apr 12, 2013</p>
......
......@@ -63,7 +63,7 @@ Jitsi Videobridge does not mix the video channels into a composite video stream,
but only relays the received video channels to all call participants.
Therefore, while it does need to run on a server with good network bandwidth,
CPU horsepower is not that critical for performance.
The JitMeet video conference application using WebRTC is included.
The OfMeet video conference application using WebRTC is included.
</p>
......@@ -79,7 +79,7 @@ Under Server settings -> Jitsi Videobridge tab you can configure it.
<h2>How to use</h2>
To run the demo video conference application, point your browser at https://your_server:7443/jitmeet
To run the demo video conference application, point your browser at https://your_server:7443/ofmeet
</body>
</html>
The MIT License (MIT)
Copyright (c) 2013 ESTOS GmbH
Copyright (c) 2013 BlueJimp SARL
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
meet - a colibri.js sample application
====
A WebRTC-powered multi-user videochat. For a live demo, check out either https://meet.estos.de/ or https://meet.jit.si/.
Built using [colibri.js](https://github.com/ESTOS/colibri.js) and [strophe.jingle](https://github.com/ESTOS/strophe.jingle), powered by the [jitsi-videobridge](https://github.com/jitsi/jitsi-videobridge) and [prosody](http://prosody.im/).
/* jshint -W117 */
/* application specific logic */
var connection = null;
var focus = null;
var RTC;
var RTCPeerConnection = null;
var nickname = null;
var sharedKey = '';
var roomUrl = null;
window.onbeforeunload = closePageWarning;
function init() {
RTC = setupRTC();
if (RTC === null) {
window.location.href = '/webrtcrequired.html';
return;
} else if (RTC.browser != 'chrome') {
window.location.href = '/chromeonly.html';
return;
}
RTCPeerconnection = RTC.peerconnection;
connection = new Strophe.Connection(document.getElementById('boshURL').value || config.bosh || '/http-bind');
/*
connection.rawInput = function (data) { console.log('RECV: ' + data); };
connection.rawOutput = function (data) { console.log('SEND: ' + data); };
*/
connection.jingle.pc_constraints = RTC.pc_constraints;
var jid = document.getElementById('jid').value || config.hosts.domain || window.location.hostname;
connection.connect(jid, document.getElementById('password').value, function (status) {
if (status == Strophe.Status.CONNECTED) {
console.log('connected');
connection.send($pres());
getUserMediaWithConstraints(['audio', 'video'], '360');
document.getElementById('connect').disabled = true;
} else {
console.log('status', status);
}
});
}
function doJoin() {
var roomnode = urlParam("r");
if (!roomnode) {
roomnode = Math.random().toString(36).substr(2, 20);
window.history.pushState('VideoChat', 'Room: ' + roomnode, window.location.pathname + "?r=" + roomnode);
}
roomjid = roomnode + '@' + config.hosts.muc;
if (config.useNicks) {
var nick = window.prompt('Your nickname (optional)');
if (nick) {
roomjid += '/' + nick;
} else {
roomjid += '/' + Strophe.getNodeFromJid(connection.jid);
}
} else {
roomjid += '/' + Strophe.getNodeFromJid(connection.jid);
}
connection.emuc.doJoin(roomjid);
}
$(document).bind('mediaready.jingle', function (event, stream) {
connection.jingle.localStream = stream;
RTC.attachMediaStream($('#localVideo'), stream);
document.getElementById('localVideo').muted = true;
document.getElementById('localVideo').autoplay = true;
document.getElementById('localVideo').volume = 0;
document.getElementById('largeVideo').volume = 0;
document.getElementById('largeVideo').src = document.getElementById('localVideo').src;
doJoin();
});
$(document).bind('mediafailure.jingle', function () {
// FIXME
});
$(document).bind('remotestreamadded.jingle', function (event, data, sid) {
function waitForRemoteVideo(selector, sid) {
var sess = connection.jingle.sessions[sid];
videoTracks = data.stream.getVideoTracks();
if (videoTracks.length === 0 || selector[0].currentTime > 0) {
RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF?
$(document).trigger('callactive.jingle', [selector, sid]);
console.log('waitForremotevideo', sess.peerconnection.iceConnectionState, sess.peerconnection.signalingState);
} else {
setTimeout(function () { waitForRemoteVideo(selector, sid); }, 100);
}
}
var sess = connection.jingle.sessions[sid];
var vid = document.createElement('video');
var id = 'remoteVideo_' + sid + '_' + data.stream.id;
vid.id = id;
vid.autoplay = true;
vid.oncontextmenu = function () { return false; };
var remotes = document.getElementById('remoteVideos');
remotes.appendChild(vid);
var sel = $('#' + id);
sel.hide();
RTC.attachMediaStream(sel, data.stream);
waitForRemoteVideo(sel, sid);
data.stream.onended = function () {
console.log('stream ended', this.id);
var src = $('#' + id).attr('src');
$('#' + id).remove();
if (src === $('#largeVideo').attr('src')) {
// this is currently displayed as large
// pick the last visible video in the row
// if nobody else is left, this picks the local video
var pick = $('#remoteVideos :visible:last').get(0);
// mute if localvideo
document.getElementById('largeVideo').volume = pick.volume;
document.getElementById('largeVideo').src = pick.src;
}
resizeThumbnails();
};
sel.click(
function () {
console.log('hover in', $(this).attr('src'));
var newSrc = $(this).attr('src');
if ($('#largeVideo').attr('src') != newSrc) {
document.getElementById('largeVideo').volume = 1;
$('#largeVideo').fadeOut(300, function () {
$(this).attr('src', newSrc);
$(this).fadeIn(300);
});
}
}
);
});
$(document).bind('callincoming.jingle', function (event, sid) {
var sess = connection.jingle.sessions[sid];
// TODO: check affiliation and/or role
console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]);
sess.sendAnswer();
sess.accept();
});
$(document).bind('callactive.jingle', function (event, videoelem, sid) {
console.log('call active');
if (videoelem.attr('id').indexOf('mixedmslabel') == -1) {
// ignore mixedmslabela0 and v0
videoelem.show();
resizeThumbnails();
document.getElementById('largeVideo').volume = 1;
$('#largeVideo').attr('src', videoelem.attr('src'));
}
});
$(document).bind('callterminated.jingle', function (event, sid, reason) {
// FIXME
});
$(document).bind('joined.muc', function (event, jid, info) {
console.log('onJoinComplete', info);
updateRoomUrl(window.location.href);
// Once we've joined the muc show the toolbar
showToolbar();
if (Object.keys(connection.emuc.members).length < 1) {
focus = new ColibriFocus(connection, config.hosts.bridge);
return;
}
});
$(document).bind('entered.muc', function (event, jid, info) {
console.log('entered', jid, info);
console.log(focus);
if (focus !== null) {
// FIXME: this should prepare the video
if (focus.confid === null) {
console.log('make new conference with', jid);
focus.makeConference(Object.keys(connection.emuc.members));
} else {
console.log('invite', jid, 'into conference');
focus.addNewParticipant(jid);
}
}
else if (sharedKey) {
updateLockButton();
}
});
$(document).bind('left.muc', function (event, jid) {
console.log('left', jid);
connection.jingle.terminateByJid(jid);
// FIXME: this should actually hide the video already for a nicer UX
if (Object.keys(connection.emuc.members).length === 0) {
console.log('everyone left');
if (focus !== null) {
// FIXME: closing the connection is a hack to avoid some
// problemswith reinit
if (focus.peerconnection !== null) {
focus.peerconnection.close();
}
focus = new ColibriFocus(connection, config.hosts.bridge);
}
}
});
$(document).bind('passwordrequired.muc', function (event, jid) {
console.log('on password required', jid);
$.prompt('<h2>Password required</h2>' +
'<input id="lockKey" type="text" placeholder="shared key" autofocus>',
{
persistent: true,
buttons: { "Ok": true , "Cancel": false},
defaultButton: 1,
loaded: function(event) {
document.getElementById('lockKey').focus();
},
submit: function(e,v,m,f){
if(v)
{
var lockKey = document.getElementById('lockKey');
if (lockKey.value != null)
{
setSharedKey(lockKey);
connection.emuc.doJoin(jid, lockKey.value);
}
}
}
});
});
function toggleVideo() {
if (!(connection && connection.jingle.localStream)) return;
for (var idx = 0; idx < connection.jingle.localStream.getVideoTracks().length; idx++) {
connection.jingle.localStream.getVideoTracks()[idx].enabled = !connection.jingle.localStream.getVideoTracks()[idx].enabled;
}
}
function toggleAudio() {
if (!(connection && connection.jingle.localStream)) return;
for (var idx = 0; idx < connection.jingle.localStream.getAudioTracks().length; idx++) {
connection.jingle.localStream.getAudioTracks()[idx].enabled = !connection.jingle.localStream.getAudioTracks()[idx].enabled;
}
}
function resizeLarge() {
var availableHeight = window.innerHeight;
var chatspaceWidth = $('#chatspace').width();
var numvids = $('#remoteVideos>video:visible').length;
if (numvids < 5)
availableHeight -= 100; // min thumbnail height for up to 4 videos
else
availableHeight -= 50; // min thumbnail height for more than 5 videos
availableHeight -= 79; // padding + link ontop
var availableWidth = window.innerWidth - chatspaceWidth;
var aspectRatio = 16.0 / 9.0;
if (availableHeight < availableWidth / aspectRatio) {
availableWidth = Math.floor(availableHeight * aspectRatio);
}
if (availableWidth < 0 || availableHeight < 0) return;
$('#largeVideo').width(availableWidth);
$('#largeVideo').height(availableWidth / aspectRatio);
resizeThumbnails();
}
function resizeThumbnails() {
// Calculate the available height, which is the inner window height minus 39px for the header
// minus 4px for the delimiter lines on the top and bottom of the large video,
// minus the 36px space inside the remoteVideos container used for highlighting shadow.
var availableHeight = window.innerHeight - $('#largeVideo').height() - 79;
var numvids = $('#remoteVideos>video:visible').length;
// Remove the 1px borders arround videos.
var availableWinWidth = $('#remoteVideos').width() - 2 * numvids;
var availableWidth = availableWinWidth / numvids;
var aspectRatio = 16.0 / 9.0;
var maxHeight = Math.min(160, availableHeight);
availableHeight = Math.min(maxHeight, availableWidth / aspectRatio);
if (availableHeight < availableWidth / aspectRatio) {
availableWidth = Math.floor(availableHeight * aspectRatio);
}
// size videos so that while keeping AR and max height, we have a nice fit
$('#remoteVideos').height(availableHeight + 36); // add the 2*18px border used for highlighting shadow.
$('#remoteVideos>video:visible').width(availableWidth);
$('#remoteVideos>video:visible').height(availableHeight);
}
$(document).ready(function () {
$('#nickinput').keydown(function(event) {
if (event.keyCode == 13) {
event.preventDefault();
var val = this.value;
this.value = '';
if (!nickname) {
nickname = val;
$('#nickname').css({visibility:"hidden"});
$('#chatconversation').css({visibility:'visible'});
$('#usermsg').css({visibility:'visible'});
$('#usermsg').focus();
return;
}
}
});
$('#usermsg').keydown(function(event) {
if (event.keyCode == 13) {
event.preventDefault();
var message = this.value;
$('#usermsg').val('').trigger('autosize.resize');
this.focus();
connection.emuc.sendMessage(message, nickname);
}
});
$('#usermsg').autosize();
resizeLarge();
$(window).resize(function () {
resizeLarge();
});
if (!$('#settings').is(':visible')) {
console.log('init');
init();
} else {
loginInfo.onsubmit = function (e) {
if (e.preventDefault) e.preventDefault();
$('#settings').hide();
init();
};
}
});
$(window).bind('unload', function () {
if (connection && connection.connected) {
// ensure signout
$.ajax({
type: 'POST',
url: config.bosh,
async: false,
cache: false,
contentType: 'application/xml',
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>",
success: function (data) {
console.log('signed out');
console.log(data);
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
console.log('signout error', textStatus + ' (' + errorThrown + ')');
}
});
}
});
/*
* Appends the given message to the chat conversation.
*/
function updateChatConversation(nick, message)
{
var divClassName = '';
if (nickname == nick)
divClassName = "localuser";
else
divClassName = "remoteuser";
$('#chatconversation').append('<div class="' + divClassName + '"><b>' + nick + ': </b>' + message + '</div>');
$('#chatconversation').animate({ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
}
/*
* Changes the style class of the element given by id.
*/
function buttonClick(id, classname) {
$(id).toggleClass(classname); // add the class to the clicked element
}
/*
* Opens the lock room dialog.
*/
function openLockDialog() {
// Only the focus is able to set a shared key.
if (focus == null) {
if (sharedKey)
$.prompt("This conversation is currently protected by a shared secret key.",
{
title: "Secrect key",
persistent: false
});
else
$.prompt("This conversation isn't currently protected by a secret key. Only the owner of the conference could set a shared key.",
{
title: "Secrect key",
persistent: false
});
}
else {
if (sharedKey)
$.prompt("Are you sure you would like to remove your secret key?",
{
title: "Remove secrect key",
persistent: false,
buttons: { "Remove": true, "Cancel": false},
defaultButton: 1,
submit: function(e,v,m,f){
if(v)
{
setSharedKey('');
lockRoom();
}
}
});
else
$.prompt('<h2>Set a secrect key to lock your room</h2>' +
'<input id="lockKey" type="text" placeholder="your shared key" autofocus>',
{
persistent: false,
buttons: { "Save": true , "Cancel": false},
defaultButton: 1,
loaded: function(event) {
document.getElementById('lockKey').focus();
},
submit: function(e,v,m,f){
if(v)
{
var lockKey = document.getElementById('lockKey');
if (lockKey.value)
{
console.log("LOCK KEY", lockKey.value);
setSharedKey(lockKey.value);
lockRoom(true);
}
}
}
});
}
}
/*
* Opens the invite link dialog.
*/
function openLinkDialog() {
$.prompt('<input id="inviteLinkRef" type="text" value="' + roomUrl + '" onclick="this.select();">',
{
title: "Share this link with everyone you want to invite",
persistent: false,
buttons: { "Cancel": false},
loaded: function(event) {
document.getElementById('inviteLinkRef').select();
}
});
}
/*
* Locks / unlocks the room.
*/
function lockRoom(lock) {
connection.emuc.lockRoom(sharedKey);
updateLockButton();
}
/*
* Sets the shared key.
*/
function setSharedKey(sKey) {
sharedKey = sKey;
}
/*
* Updates the lock button state.
*/
function updateLockButton() {
buttonClick("#lockIcon", "fa fa-unlock fa-lg fa fa-lock fa-lg");
}
/*
* Opens / closes the chat area.
*/
function openChat() {
var chatspace = $('#chatspace');
var videospace = $('#videospace');
var chatspaceWidth = chatspace.width();
if (chatspace.css("opacity") == 1) {
chatspace.animate({opacity: 0}, "fast");
chatspace.animate({width: 0}, "slow");
videospace.animate({right: 0, width:"100%"}, "slow");
}
else {
chatspace.animate({width:"20%"}, "slow");
chatspace.animate({opacity: 1}, "slow");
videospace.animate({right:chatspaceWidth, width:"80%"}, "slow");
}
// Request the focus in the nickname field or the chat input field.
if ($('#nickinput').is(':visible'))
$('#nickinput').focus();
else
$('#usermsg').focus();
}
/*
* Shows the call main toolbar.
*/
function showToolbar() {
$('#toolbar').css({visibility:"visible"});
}
/*
* Updates the room invite url.
*/
function updateRoomUrl(newRoomUrl) {
roomUrl = newRoomUrl;
}
/*
* Warning to the user that the conference window is about to be closed.
*/
function closePageWarning() {
if (focus != null)
return "You are the owner of this conference call and you are about to end it.";
else
return "You are about to leave this conversation.";
}
......@@ -7,6 +7,7 @@ html, body{
background: #e9e9e9;
}
#videospace {
display: block;
position: absolute;
......@@ -70,26 +71,13 @@ html, body{
top: 40px;
bottom: 0px;
right: 0px;
width:0;
width:450px;
opacity: 0;
overflow: hidden;
background-color:#f6f6f6;
border-left:1px solid #424242;
}
#chatconversation {
display:block;
position:relative;
top: -120px;
float:top;
text-align:left;
line-height:20px;
font-size:14px;
padding:5px;
height:90%;
overflow:scroll;
visibility:hidden;
}
.localuser {
color: #087dba;
......@@ -154,7 +142,6 @@ html, body{
top: 0px;
width: 100px;
height: 39px;
background-image:url(../images/left1.png);
background-repeat:no-repeat;
margin: 0;
padding: 0;
......@@ -163,8 +150,8 @@ html, body{
#leftlogo {
position:absolute;
top: 5px;
color: white;
left: 15px;
background-image:url(../images/jitsilogo.png);
background-repeat:no-repeat;
height: 31px;
width: 68px;
......@@ -229,7 +216,6 @@ a.button:hover {
position:absolute;
right: 0px;
top: 0px;
background-image:url(../images/right1.png);
background-repeat:no-repeat;
margin:0;
padding:0;
......@@ -241,7 +227,7 @@ a.button:hover {
position:absolute;
top: 6px;
right: 15px;
background-image:url(../images/estoslogo.png);
color: white;
background-repeat:no-repeat;
height: 25px;
width: 62px;
......
.jqistates {
font-size: 14px;
}
}
.jqistates h2 {
padding-bottom: 10px;
border-bottom: 1px solid #eee;
......@@ -9,18 +9,18 @@
line-height: 25px;
text-align: center;
color: #424242;
}
}
.jqistates input {
width: 100%;
margin: 20px 0;
}
}
.jqibuttons button {
margin-right: 5px;
float:right;
}
}
button.jqidefaultbutton #inviteLinkRef {
button.jqidefaultbutton #inviteLinkRef {
color: #2c8ad2;
}
\ No newline at end of file
}
\ No newline at end of file
div#ofmeet {
border: 1px solid #C9C9C9;
margin: 10px auto 20px auto;
padding: 10px;
font-family: Arial,Helvetica,sans-serif,"sans serif";
background-color: #F9F9F9;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
display:block;
position:relative;
top: -100px;
float:top;
text-align:left;
line-height:20px;
font-size:14px;
padding:5px;
overflow:scroll;
visibility:hidden;
}
div#ofmeet div.ofmeet-clear {
clear: both;
}
div#ofmeet form {
margin: 0;
padding: 0;
}
div#ofmeet div#ofmeet-login {
overflow: hidden;
position: relative;
}
div#ofmeet div#ofmeet-login label,
div#ofmeet div#ofmeet-login span#ofmeet-usernameDisplay {
font-size: 0.9em;
line-height: 22px;
margin: 0px 6px;
}
div#ofmeet div#ofmeet-login span#ofmeet-usernameDisplay {
font-weight: bold;
margin: 0;
}
div#ofmeet div#ofmeet-login label,
div#ofmeet div#.ofmeet-login input#ofmeet-username,
div#ofmeet div#ofmeet-login span#ofmeet-usernameDisplay {
float: left;
vertical-align: middle;
}
div#ofmeet div#ofmeet-login input#ofmeet-connect {
float: right;
margin-right: 5px;
}
div#ofmeet div#ofmeet-messaging {
position: relative;
overflow: hidden;
zoom: 1; /* uh, dirty ie6 hack... */
}
div#ofmeet div#ofmeet-log {
height: 85%;
border: 1px solid #999999;
background-color: #FFFFFF;
overflow: auto;
margin: 5px;
padding-top: 10px;
font-size: 0.8em;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
position: relative;
}
div#ofmeet div#ofmeet-log div.message {
width: 95%;
background: transparent url(../images/bubble_top.png) left top no-repeat;
padding-top: 9px;
margin: 0 auto;
}
div#ofmeet div#ofmeet-log div.message a:link,
div#ofmeet div#ofmeet-log div.message a:visited,
div#ofmeet div#ofmeet-log div.message a:hover,
div#ofmeet div#ofmeet-log div.message a:active {
text-decoration: underline;
font-weight: bold;
}
div#ofmeet div#ofmeet-log div.message a:link,
div#ofmeet div#ofmeet-log div.message a:visited {
color: #C64100;
}
div#ofmeet div#ofmeet-log div.message a:hover,
div#ofmeet div#ofmeet-log div.message a:active {
color: #333333;
background-color: #EAEAEA;
}
div#ofmeet div#ofmeet-log div.messageIn {
color: #444444;
}
div#ofmeet div#ofmeet-log div.messageOut {
color: #888888;
}
div#ofmeet div#ofmeet-log div.messageSystem {
color: #BBBBBB;
}
div#ofmeet div#ofmeet-log div.message span.msgText {
display: block;
font-weight: bold;
background: #E0E1E1 url(../images/bubble_content.png) left top no-repeat;
padding: 0px 12px;
}
div#ofmeet div#ofmeet-log div.message span.msgPerson {
display: block;
background: transparent url(../images/bubble_bottom.png) left top no-repeat;
height: 25px;
padding: 12px 0 0 50px;
font-size: 0.8em;
}
div#ofmeet div#ofmeet-log div.message span.msgPerson span.msgTime {
color: #AAAAAA;
}
div#ofmeet div#ofmeet-messaging textarea#ofmeet-message {
width: 388px;
border: 1px solid #999999;
margin: 5px 5px 0;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
position: relative;
}
div#ofmeet div#ofmeet-messaging input#ofmeet-sendmessage {
float: right;
position: relative;
text-transform: uppercase;
font-weight: bold;
padding: 0px 10px;
margin: 7px 5px 0 0;
}
Strophe.addConnectionPlugin('logger', {
// logs raw stanzas and makes them available for download as JSON
connection: null,
log: [],
init: function (conn) {
this.connection = conn;
this.connection.rawInput = this.log_incoming.bind(this);;
this.connection.rawOutput = this.log_outgoing.bind(this);;
},
log_incoming: function (stanza) {
this.log.push([new Date().getTime(), 'incoming', stanza]);
},
log_outgoing: function (stanza) {
this.log.push([new Date().getTime(), 'outgoing', stanza]);
},
// <a onclick="connection.logger.dump(event.target);">my download button</a>
dump: function (what, filename){
what.download = filename || 'xmpplog.json';
what.href = 'data:application/json;charset=utf-8,\n';
what.href += encodeURIComponent(JSON.stringify(this.log, null, ' '));
return true;
}
});
<html>
<head>
<title>WebRTC, meet the Jitsi Videobridge</title>
<script src="jquery.min.js"></script>
<script src="libs/strophejingle.bundle.js?v=1"></script><!-- strophe.jingle bundle -->
<script src="libs/colibri.js?v=1"></script><!-- colibri focus implementation -->
<script src="muc.js?v=2"></script><!-- simple MUC library -->
<script src="estos_log.js?v=1"></script><!-- simple stanza logger -->
<script src="app.js?v=2"></script><!-- application logic -->
<title>Openfire Meet</title>
<script src="js/jquery.min.js"></script>
<script src="js/jquery-impromptu.js"></script>
<script src="js/jquery.autosize.js"></script>
<script src="js/md5.js"></script>
<script src="js/base64.js"></script>
<script src="js/strophe.js"></script>
<script src="js/muc.js"></script>
<script src="js/config.js"></script>
<script src="js/webrtc.sdp.js"></script>
<script src="js/strophe-openfire.js"></script>
<script src="js/main.js"></script>
<link rel="stylesheet" href="font-awesome-4.0.3/css/font-awesome.css">
<link rel="stylesheet" href="css/main.css?v=1"/>
<link rel="stylesheet" href="css/jquery-impromptu.css?v=1">
<link rel="stylesheet" href="css/modaldialog.css?v=1">
<script src="libs/jquery-impromptu.js"></script>
<script src="libs/jquery.autosize.js"></script>
<script src="config.js"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
<link rel="stylesheet" href="css/styles.css" />
<link rel="stylesheet" href="css/main.css" />
<link rel="stylesheet" href="css/jquery-impromptu.css">
<link rel="stylesheet" href="css/modaldialog.css">
</head>
<body>
<div id="header">
<a href="http://jitsi.org" target="_blank"><div id="leftlogo"></div></a>
<a href="http://www.estos.com/" target="_blank"><div id="rightlogo"></div></a>
<a href="http://community.igniterealtime.org/community/support/jitsi_videobridge_plugin" target="_blank"><div id="leftlogo"><nobr>OfMeet</nobr></div></a>
<div id="toolbar">
<a class="button" onclick='buttonClick("#mute", "fa fa-microphone fa-lg fa fa-microphone-slash fa-lg");toggleAudio();'><i id="mute" title="Mute / unmute" class="fa fa-microphone fa-lg"></i></a>
<div class="header_button_separator"></div>
......@@ -29,18 +33,8 @@
<a class="button" onclick="openLinkDialog();"><i title="Invite others" class="fa fa-link fa-lg"></i></a>
<div class="header_button_separator"></div>
<a class="button" onclick='openChat();'><i id="chat" title="Open chat" class="fa fa-comments fa-lg"></i></a>
<!--i class='fa fa-external-link'>&nbsp;</i>Others can join you by just going to <span id='roomurl'></span-->
</div>
</div>
<div id="settings">
<h1>Connection Settings</h1>
<form id="loginInfo">
<label>JID: <input id="jid" type="text" name="jid" placeholder="me@example.com"/></label>
<label>Password: <input id="password" type="password" name="password" placeholder="secret"/></label>
<label>BOSH URL: <input id="boshURL" type="text" name="boshURL" placeholder="/http-bind"/></label>
<input id="connect" type="submit" value="Connect" />
</form>
</div>
<div id="videospace">
<div class="fade_line"></div>
......@@ -58,17 +52,11 @@
</form>
</div>
<!--div><i class="fa fa-comments">&nbsp;</i><span class='nick'></span>:&nbsp;<span class='chattext'></span></div-->
<div id="chatconversation"></div>
<div id="ofmeet"><div id="ofmeet-messaging"><div id="ofmeet-log"></div></div></div>
<textarea id="usermsg" class= "animated" placeholder='Enter text...' autofocus></textarea>
</div>
<a id="downloadlog" class="fa fa-cloud-download" title="Download logfile for support" onclick="connection.logger.dump(event.target);" style="position: absolute; bottom: 5; left: 5; overflow: visible; z-index: 100;"></a>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-319188-14', 'jit.si');
ga('send', 'pageview');
</script>
</body>
</html>
......@@ -202,7 +202,7 @@ public class PluginImpl implements Plugin, PropertyEventListener
// start video conference web application
try {
String appName = JiveGlobals.getProperty(VIDEO_CONFERENCE_PROPERTY_NAME, "jitmeet");
String appName = JiveGlobals.getProperty(VIDEO_CONFERENCE_PROPERTY_NAME, "ofmeet");
Log.info("Initialize Web App " + appName);
ContextHandlerCollection contexts = HttpBindManager.getInstance().getContexts();
......
// This code was written by Tyler Akins and has been placed in the
// public domain. It would be nice if you left this header intact.
// Base64 code from Tyler Akins -- http://rumkin.com
var Base64 = (function () {
var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var obj = {
/**
* Encodes a string in base64
* @param {String} input The string to encode in base64.
*/
encode: function (input) {
var output = "";
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
do {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) +
keyStr.charAt(enc3) + keyStr.charAt(enc4);
} while (i < input.length);
return output;
},
/**
* Decodes a base64 string.
* @param {String} input The string to decode.
*/
decode: function (input) {
var output = "";
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
// remove all characters that are not A-Z, a-z, 0-9, +, /, or =
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
do {
enc1 = keyStr.indexOf(input.charAt(i++));
enc2 = keyStr.indexOf(input.charAt(i++));
enc3 = keyStr.indexOf(input.charAt(i++));
enc4 = keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 != 64) {
output = output + String.fromCharCode(chr2);
}
if (enc4 != 64) {
output = output + String.fromCharCode(chr3);
}
} while (i < input.length);
return output;
}
};
return obj;
})();
......@@ -5,12 +5,6 @@ var config = {
bridge: 'jitsi-videobridge.' + window.location.hostname // FIXME: use XEP-0030
},
useNicks: false,
bosh: '/http-bind/' // FIXME: use xep-0156 for that
useWebsockets: true,
bosh: window.location.protocol + "//" + window.location.host + '/http-bind/' // FIXME: use xep-0156 for that
};
function urlParam(name)
{
var results = new RegExp('[\\?&]' + name + '=([^&#]*)').exec(window.location.href);
if (!results) { return undefined; }
return results[1] || undefined;
}
\ No newline at end of file
var connection = null;
var roomjid;
var nickname = null;
var roomUrl = null;
var sharedKey = '';
$(document).ready(function ()
{
window.RTC = setupRTC();
if (window.RTC === null) {
window.location.href = 'webrtcrequired.html';
return;
} else if (window.RTC.browser != 'chrome') {
window.location.href = 'chromeonly.html';
return;
}
$('#nickinput').keydown(function(event)
{
if (event.keyCode == 13) {
event.preventDefault();
var val = this.value;
this.value = '';
if (!nickname) {
nickname = val;
$('#nickname').css({visibility:"hidden"});
$('#ofmeet').css({visibility:'visible'});
$('#usermsg').css({visibility:'visible'});
$('#usermsg').focus();
if (connection && roomjid) connection.emuc.changeNick(roomjid + "/" + nickname);
return;
}
}
});
$('#usermsg').keydown(function(event)
{
if (event.keyCode == 13) {
event.preventDefault();
var message = this.value;
$('#usermsg').val('').trigger('autosize.resize');
this.focus();
connection.emuc.sendMessage(message, nickname);
}
});
$('#usermsg').autosize();
resizeLarge();
$(window).resize(function () {
resizeLarge();
});
RTCPeerconnection = RTC.peerconnection;
if (config.useWebsockets)
connection = new Openfire.Connection(config.bosh);
else
connection = new Strophe.Connection(config.bosh);
connection.resource = Math.random().toString(36).substr(2, 20);
/*
connection.rawInput = function (data) { console.log('RECV: ' + data); };
connection.rawOutput = function (data) { console.log('SEND: ' + data); };
*/
var jid = config.hosts.domain;
connection.connect(jid, null, function (status)
{
if (status == Strophe.Status.CONNECTED)
{
console.log('connected');
connection.send($pres());
getConstraints(['audio', 'video'], '720');
getUserMedia();
} else {
console.log('status', status);
}
});
});
$(window).bind('beforeunload', function ()
{
if (connection && connection.connected) {
connection.disconnect();
}
});
$(document).bind('mediaready.rayo', function(event, stream)
{
window.RTC.rayo.localStream = stream;
window.RTC.attachMediaStream($('#localVideo'), stream);
document.getElementById('localVideo').muted = true;
document.getElementById('localVideo').autoplay = true;
document.getElementById('localVideo').volume = 0;
document.getElementById('largeVideo').volume = 0;
document.getElementById('largeVideo').src = document.getElementById('localVideo').src;
console.log("mediaready.rayo");
doJoin();
});
$(document).bind('mediafailure.rayo', function(error) {
console.error('mediafailure.rayo ' + error);
});
$(document).bind('remotestreamadded.rayo', function(event, data, sid)
{
var id = 'remoteVideo_' + sid + '_' + data.stream.id;
if (!document.getElementById(id))
{
var vid = document.createElement('video');
vid.id = id;
vid.autoplay = true;
vid.oncontextmenu = function() { return false; };
var remotes = document.getElementById('remoteVideos');
remotes.appendChild(vid);
}
var sel = $('#' + id);
sel.hide();
window.RTC.attachMediaStream(sel, data.stream);
if (sel.attr('id').indexOf('mixedmslabel') == -1) {
// ignore mixedmslabela0 and non room members
sel.show();
resizeThumbnails();
document.getElementById('largeVideo').volume = 1;
$('#largeVideo').attr('src', sel.attr('src'));
}
data.stream.onended = function() {
console.log('stream ended', this.id);
var src = $('#' + id).attr('src');
$('#' + id).remove();
if (src === $('#largeVideo').attr('src')) {
// this is currently displayed as large
// pick the last visible video in the row
// ... well, if nobody else is left, this picks the local video
var pick = $('#remoteVideos :visible:last').get(0);
// mute if localvideo
document.getElementById('largeVideo').volume = pick.volume;
document.getElementById('largeVideo').src = pick.src;
}
resizeThumbnails();
}
// FIXME: hover is bad, this causes flicker. How about moving this?
// remember that moving this in the DOM requires to play() again
sel.click(
function() {
console.log('hover in', $(this).attr('src'));
$("#largeVideo").css("visibility", "visible");
var newSrc = $(this).attr('src');
if ($('#largeVideo').attr('src') != newSrc) {
document.getElementById('largeVideo').volume = 1;
$('#largeVideo').fadeOut(300, function(){
$(this).attr('src', newSrc);
$(this).fadeIn(300);
});
}
}
);
});
function setupRTC()
{
var RTC = null;
if (navigator.mozGetUserMedia) {
console.log('This appears to be Firefox');
var version = parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
if (version >= 22) {
RTC = {
rayo: {
channels: {},
confid: {},
pc: null,
addssrc: {},
localStream: null,
constraints: {audio: false, video: false}
},
peerconnection: mozRTCPeerConnection,
browser: 'firefox',
getUserMedia: navigator.mozGetUserMedia.bind(navigator),
attachMediaStream: function (element, stream) {
element[0].mozSrcObject = stream;
element[0].play();
},
pc_constraints: {}
};
if (!MediaStream.prototype.getVideoTracks)
MediaStream.prototype.getVideoTracks = function () { return []; };
if (!MediaStream.prototype.getAudioTracks)
MediaStream.prototype.getAudioTracks = function () { return []; };
RTCSessionDescription = mozRTCSessionDescription;
RTCIceCandidate = mozRTCIceCandidate;
}
} else if (navigator.webkitGetUserMedia) {
console.log('This appears to be Chrome');
RTC = {
rayo: {
channels: {},
confid: {},
pc: null,
addssrc: {},
localStream: null,
constraints: {audio: false, video: false}
},
peerconnection: webkitRTCPeerConnection,
browser: 'chrome',
getUserMedia: navigator.webkitGetUserMedia.bind(navigator),
attachMediaStream: function (element, stream) {
element.attr('src', webkitURL.createObjectURL(stream));
},
pc_constraints: {'optional': [{'DtlsSrtpKeyAgreement': 'true'}]} // enable dtls support in canary
};
if (navigator.userAgent.indexOf('Android') != -1) {
RTC.pc_constraints = {}; // disable DTLS on Android
}
if (!webkitMediaStream.prototype.getVideoTracks) {
webkitMediaStream.prototype.getVideoTracks = function () {
return this.videoTracks;
};
}
if (!webkitMediaStream.prototype.getAudioTracks) {
webkitMediaStream.prototype.getAudioTracks = function () {
return this.audioTracks;
};
}
}
if (RTC === null) {
try { console.log('Browser does not appear to be WebRTC-capable'); } catch (e) { }
}
return RTC;
}
function getUserMedia()
{
console.log("getUserMedia", window.RTC.rayo.constraints);
try {
window.RTC.getUserMedia(window.RTC.rayo.constraints,
function (stream) {
console.log('onUserMediaSuccess');
$(document).trigger('mediaready.rayo', [stream]);
},
function (error) {
console.warn('Failed to get access to local media. Error ', error);
$(document).trigger('mediafailure.rayo');
});
} catch (e) {
console.error('GUM failed: ', e);
$(document).trigger('mediafailure.rayo');
}
};
function getConstraints(um, resolution, bandwidth, fps)
{
console.log("getConstraints", um, resolution, bandwidth, fps);
window.RTC.rayo.constraints = {audio: false, video: false};
if (um.indexOf('video') >= 0) {
window.RTC.rayo.constraints.video = {mandatory: {}};// same behaviour as true
}
if (um.indexOf('audio') >= 0) {
window.RTC.rayo.constraints.audio = {};// same behaviour as true
}
if (um.indexOf('screen') >= 0) {
window.RTC.rayo.constraints.video = {
"mandatory": {
"chromeMediaSource": "screen"
}
};
}
if (resolution && window.RTC.rayo.constraints.video)
{
window.RTC.rayo.constraints.video = {mandatory: {}};// same behaviour as true
// see https://code.google.com/p/chromium/issues/detail?id=143631#c9 for list of supported resolutions
switch (resolution) {
// 16:9 first
case '1080':
case 'fullhd':
window.RTC.rayo.constraints.video.mandatory.minWidth = 1920;
window.RTC.rayo.constraints.video.mandatory.minHeight = 1080;
window.RTC.rayo.constraints.video.mandatory.minAspectRatio = 1.77;
break;
case '720':
case 'hd':
window.RTC.rayo.constraints.video.mandatory.minWidth = 1280;
window.RTC.rayo.constraints.video.mandatory.minHeight = 720;
window.RTC.rayo.constraints.video.mandatory.minAspectRatio = 1.77;
break;
case '360':
window.RTC.rayo.constraints.video.mandatory.minWidth = 640;
window.RTC.rayo.constraints.video.mandatory.minHeight = 360;
window.RTC.rayo.constraints.video.mandatory.minAspectRatio = 1.77;
break;
case '180':
window.RTC.rayo.constraints.video.mandatory.minWidth = 320;
window.RTC.rayo.constraints.video.mandatory.minHeight = 180;
window.RTC.rayo.constraints.video.mandatory.minAspectRatio = 1.77;
break;
// 4:3
case '960':
window.RTC.rayo.constraints.video.mandatory.minWidth = 960;
window.RTC.rayo.constraints.video.mandatory.minHeight = 720;
break;
case '640':
case 'vga':
window.RTC.rayo.constraints.video.mandatory.minWidth = 640;
window.RTC.rayo.constraints.video.mandatory.minHeight = 480;
break;
case '320':
window.RTC.rayo.constraints.video.mandatory.minWidth = 320;
window.RTC.rayo.constraints.video.mandatory.minHeight = 240;
break;
default:
if (navigator.userAgent.indexOf('Android') != -1) {
window.RTC.rayo.constraints.video.mandatory.minWidth = 320;
window.RTC.rayo.constraints.video.mandatory.minHeight = 240;
window.RTC.rayo.constraints.video.mandatory.maxFrameRate = 15;
}
break;
}
}
if (bandwidth) { // doesn't work currently, see webrtc issue 1846
if (!window.RTC.rayo.constraints.video) window.RTC.rayo.constraints.video = {mandatory: {}};//same behaviour as true
window.RTC.rayo.constraints.video.optional = [{bandwidth: bandwidth}];
}
if (fps) { // for some cameras it might be necessary to request 30fps
// so they choose 30fps mjpg over 10fps yuy2
if (!window.RTC.rayo.constraints.video) window.RTC.rayo.constraints.video = {mandatory: {}};// same behaviour as tru;
window.RTC.rayo.constraints.video.mandatory.minFrameRate = fps;
}
}
function resizeLarge()
{
var availableHeight = window.innerHeight;
var numvids = $('#remoteVideos>video:visible').length;
if (numvids < 5)
availableHeight -= 100; // min thumbnail height for up to 4 videos
else
availableHeight -= 50; // min thumbnail height for more than 5 videos
availableHeight -= 79; // padding + link ontop
var availableWidth = window.innerWidth;
var aspectRatio = 16.0 / 9.0;
if (availableHeight < availableWidth / aspectRatio) {
availableWidth = Math.floor(availableHeight * aspectRatio);
}
if (availableWidth < 0 || availableHeight < 0) return;
$('#largeVideo').width(availableWidth);
$('#largeVideo').height(availableWidth/aspectRatio);
if (availableWidth <= 450) $('#chatspace').width(availableWidth);
resizeThumbnails() ;
}
function resizeThumbnails()
{
// Calculate the available height, which is the inner window height minus 39px for the header
// minus 4px for the delimiter lines on the top and bottom of the large video,
// minus the 36px space inside the remoteVideos container used for highlighting shadow.
var availableHeight = window.innerHeight - $('#largeVideo').height() - 79;
var numvids = $('#remoteVideos>video:visible').length;
// Remove the 1px borders arround videos.
var availableWinWidth = $('#remoteVideos').width() - numvids*2;
var availableWidth = availableWinWidth / numvids;
var aspectRatio = 16.0 / 9.0;
var maxHeight = Math.min(160, availableHeight);
var availableHeight = Math.min(maxHeight, availableWidth / aspectRatio);
if (availableHeight < availableWidth / aspectRatio) {
availableWidth = Math.floor(availableHeight * aspectRatio);
}
// size videos so that while keeping AR and max height, we have a nice fit
$('#remoteVideos').height(availableHeight + 36); // add the 2*18px border used for highlighting shadow.
$('#remoteVideos>video:visible').width(availableWidth);
$('#remoteVideos>video:visible').height(availableHeight);
}
function urlParam(name)
{
var results = new RegExp('[\\?&]' + name + '=([^&#]*)').exec(window.location.href);
if (!results) { return undefined; }
return results[1] || undefined;
}
function doJoin() {
var roomnode = urlParam("r");
console.log("doJoin", roomnode);
if (!roomnode) {
roomnode = Math.random().toString(36).substr(2, 20);
window.history.pushState('VideoChat', 'Room: ' + roomnode, window.location.pathname + "?r=" + roomnode);
}
roomjid = roomnode + '@' + config.hosts.muc;
var myroomjid = roomjid;
if (config.useNicks) {
var nick = window.prompt('Your nickname (optional)');
if (nick) {
myroomjid += '/' + nick;
} else {
myroomjid += '/' + Strophe.getNodeFromJid(connection.jid);
}
} else {
myroomjid += '/' + Strophe.getNodeFromJid(connection.jid);
}
connection.addHandler(rayoCallback, 'urn:xmpp:rayo:colibri:1');
connection.emuc.doJoin(myroomjid);
}
function rayoCallback(presence)
{
console.log("rayoCallback", presence);
var from = $(presence).attr('from');
$(presence).find('offer').each(function()
{
handleOffer(from, this);
});
$(presence).find('removesource').each(function()
{
removeSSRC(from, this);
});
$(presence).find('addsource').each(function()
{
handleAddSSRC(from, this);
});
return true;
};
function removeSSRC(from, removesource)
{
console.log("removeSSRC input ssrc ", removesource);
var videobridge = $(removesource).attr('videobridge');
var sdp = new SDP(window.RTC.rayo.pc.remoteDescription.sdp);
console.log("removeSSRC unmodified SDP", videobridge);
$(removesource).find('content').each(function()
{
var name = $(this).attr('name');
var ssrc = null;
$(this).find('source').each(function()
{
ssrc = $(this).attr('ssrc');
});
if (ssrc != null)
{
var idx = (name == "audio" ? 0 : 1);
sdp.removeMediaLines(idx, 'a=ssrc:' + ssrc);
}
});
sdp.raw = sdp.session + sdp.media.join('');
//console.log("removeSSRC modified SDP", sdp.raw);
window.RTC.rayo.pc.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp.raw}
), function() {
console.log('modify ok');
}, function(error) {
console.log('handleSSRC modify failed');
});
};
function handleAddSSRC(from, addsource)
{
console.log("handleSSRC input ssrc ", addsource);
var videobridge = $(addsource).attr('videobridge');
var sdp = new SDP(window.RTC.rayo.pc.remoteDescription.sdp);
$(addsource).find('content').each(function()
{
var name = $(this).attr('name');
var lines = '';
$(this).find('source').each(function()
{
var ssrc = $(this).attr('ssrc');
$(this).find('>parameter').each(function () {
lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
if ($(this).attr('value') && $(this).attr('value').length)
lines += ':' + $(this).attr('value');
lines += '\r\n';
});
});
var idx = (name == "audio" ? 0 : 1);
sdp.media[idx] += lines;
});
sdp.raw = sdp.session + sdp.media.join('');
window.RTC.rayo.addssrc = true;
window.RTC.rayo.pc.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp.raw}
), function() {
console.log('modify ok', window.RTC.rayo.pc.signalingState);
}, function(error) {
console.log('handleSSRC modify failed');
});
};
function handleOffer (from, offer)
{
console.log("handleOffer", offer);
var bridgeSDP = new SDP('v=0\r\no=- 5151055458874951233 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\nm=audio 1 RTP/SAVPF 111 0 126\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=mid:audio\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=sendrecv\r\na=rtpmap:111 opus/48000/2\r\na=fmtp:111 minptime=10\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:126 telephone-event/8000\r\na=maxptime:60\r\nm=video 1 RTP/SAVPF 100 116 117\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=mid:video\r\na=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=sendrecv\r\na=rtpmap:100 VP8/90000\r\na=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 goog-remb\r\na=rtpmap:116 red/90000\r\na=rtpmap:117 ulpfec/90000\r\n');
var muc = $(offer).attr('muc');
var nick = $(offer).attr('nickname');
var participant = $(offer).attr('participant');
var videobridge = $(offer).attr('videobridge');
var confid = null;
var channelId = [];
window.RTC.rayo.channels = {}
window.RTC.rayo.addssrc = false;
$(offer).find('conference').each(function()
{
confid = $(this).attr('id');
window.RTC.rayo.channels.id = confid;
$(this).find('content').each(function()
{
var name = $(this).attr('name');
var channel = name == "audio" ? 0 : 1;
if ((window.RTC.rayo.localStream.getVideoTracks().length > 0 && name == "video") || (window.RTC.rayo.localStream.getAudioTracks().length > 0 && name == "audio"))
{
console.log("handleOffer track", name);
$(this).find('channel').each(function()
{
channelId[channel] = $(this).attr('id');
$(this).find('source').each(function()
{
var ssrc = $(this).attr('ssrc');
if (ssrc)
{
bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' ' + 'cname:mixed' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' ' + 'label:mixedlabela0' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' ' + 'msid:mixedmslabela0 mixedlabela0' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' ' + 'mslabel:mixedmslabela0' + '\r\n';
} else {
// make chrome happy... '3735928559' == 0xDEADBEEF
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'msid:mixedmslabelv0 mixedlabelv0' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'mslabel:mixedmslabelv0' + '\r\n';
}
});
$(this).find('transport').each(function()
{
var pwd = $(this).attr('pwd');
var ufrag = $(this).attr('ufrag');
if (ufrag) bridgeSDP.media[channel] += 'a=ice-ufrag:' + ufrag + '\r\n';
if (pwd) bridgeSDP.media[channel] += 'a=ice-pwd:' + pwd + '\r\n';
$(this).find('candidate').each(function()
{
bridgeSDP.media[channel] += SDPUtil.candidateFromJingle(this);
});
$(this).find('fingerprint').each(function()
{
var hash = $(this).attr('hash');
var setup = $(this).attr('setup');
var fingerprint = $(this).text();
if (hash && fingerprint) bridgeSDP.media[channel] += 'a=fingerprint:' + hash + ' ' + fingerprint + '\r\n';
if (setup) bridgeSDP.media[channel] += 'a=setup:' + setup + '\r\n';
});
});
});
} else {
bridgeSDP.media[channel] = null;
}
});
});
bridgeSDP.raw = bridgeSDP.session + bridgeSDP.media.join('');
window.RTC.rayo.channels.sdp = bridgeSDP.raw;
//console.log("bridgeSDP.raw", bridgeSDP.raw);
window.RTC.rayo.pc = new window.RTC.peerconnection(null, {'optional': [{'DtlsSrtpKeyAgreement': 'true'}]});
window.RTC.rayo.pc.onicecandidate = function(event)
{
//console.log('candidate', event.candidate);
if (!event.candidate)
{
sendAnswer(from, videobridge, confid, channelId);
}
}
window.RTC.rayo.pc.onaddstream = function(e)
{
console.log("onstream", e, window.RTC.rayo.addssrc);
if (window.RTC.rayo.pc.signalingState == "have-remote-offer")
$(document).trigger('remotestreamadded.rayo', [e, nick]);
window.RTC.rayo.pc.createAnswer(function(desc)
{
if (!window.RTC.rayo.addssrc)
window.RTC.rayo.pc.setLocalDescription(desc);
});
};
window.RTC.rayo.pc.addStream(window.RTC.rayo.localStream);
window.RTC.rayo.pc.setRemoteDescription(new RTCSessionDescription({type: "offer", sdp : bridgeSDP.raw}));
showToolbar();
updateRoomUrl(window.location.href);
};
function sendAnswer(from, videobridge, confid, channelId)
{
console.log("sendAnswer");
var remoteSDP = new SDP(window.RTC.rayo.pc.localDescription.sdp);
//console.log("remoteSDP ", window.RTC.rayo.pc.localDescription.sdp);
var change = $iq({to: from, type: 'set'});
change.c('colibri', {xmlns: 'urn:xmpp:rayo:colibri:1', videobridge: videobridge});
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: confid});
for (channel = 0; channel < 2; channel++)
{
if (channelId[channel])
{
change.c('content', {name: channel === 0 ? 'audio' : 'video'});
change.c('channel', {id: channelId[channel]});
tmp = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:');
change.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
tmp.forEach(function (line) {
var idx = line.indexOf(' ');
var linessrc = line.substr(0, idx).substr(7);
change.attrs({ssrc: linessrc});
var kv = line.substr(idx + 1);
change.c('parameter');
if (kv.indexOf(':') == -1) {
change.attrs({ name: kv });
} else {
change.attrs({ name: kv.split(':', 2)[0] });
change.attrs({ value: kv.split(':', 2)[1] });
}
change.up();
});
change.up(); // end of source
var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
rtpmap.forEach(function (val) {
var rtpmap = SDPUtil.parse_rtpmap(val);
change.c('payload-type', rtpmap);
change.up();
});
change.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
var fingerprints = SDPUtil.find_lines(remoteSDP.media[channel], 'a=fingerprint:', remoteSDP.session);
fingerprints.forEach(function (line)
{
var tmp = SDPUtil.parse_fingerprint(line);
tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0';
change.c('fingerprint').t(tmp.fingerprint);
delete tmp.fingerprint;
var line = SDPUtil.find_line(remoteSDP.media[channel], 'a=setup:', remoteSDP.session);
if (line) {
tmp.setup = line.substr(8);
}
change.attrs(tmp);
change.up();
});
var candidates = SDPUtil.find_lines(remoteSDP.media[channel], 'a=candidate:', remoteSDP.session);
candidates.forEach(function (line) {
var tmp = SDPUtil.candidateToJingle(line);
change.c('candidate', tmp).up();
});
tmp = SDPUtil.iceparams(remoteSDP.media[channel], remoteSDP.session);
if (tmp) {
change.attrs(tmp);
}
change.up(); // end of transport
change.up(); // end of channel
change.up(); // end of content
}
}
connection.sendIQ(change,
function (res) {
console.log('rayo colibri answer set ok', window.RTC.rayo.pc.signalingState);
},
function (err) {
console.log('rayo colibri answer got error ' + err);
}
);
};
function toggleVideo()
{
if (!connection && !window.RTC.rayo.localStream) return;
for (var idx = 0; idx < window.RTC.rayo.localStream.getVideoTracks().length; idx++) {
window.RTC.rayo.localStream.getVideoTracks()[idx].enabled = !window.RTC.rayo.localStream.getVideoTracks()[idx].enabled;
}
}
function toggleAudio()
{
if (!connection && !window.RTC.rayo.localStream) return;
for (var idx = 0; idx < window.RTC.rayo.localStream.getAudioTracks().length; idx++) {
window.RTC.rayo.localStream.getAudioTracks()[idx].enabled = !window.RTC.rayo.localStream.getAudioTracks()[idx].enabled;
}
}
function updateChatConversation(nick, message)
{
var timestamp = new Date();
//console.log("updateChatConversation", nick, message, timestamp);
if (!nick) nick = "System";
divClassName = "Out";
if (nickname == nick)
divClassName = "In";
var content = '<div class="message message' + divClassName + '">'
+'<span class="msgText">' + setEmoticons(message) + '</span>'
+'<span class="msgPerson">' + nick + '<span class="msgTime">&nbsp;-&nbsp;' + new Date().format("m-d-Y H:i:s") + '</span></span>'
+'</div>';
$('#ofmeet-log').append(content);
$('#ofmeet-log').animate({ scrollTop: $('#ofmeet-log')[0].scrollHeight}, 1000);
}
function buttonClick(id, classname) {
console.log("buttonClick", id, classname);
$(id).toggleClass(classname); // add the class to the clicked element
}
function openLockDialog() {
if (sharedKey)
$.prompt("Are you sure you would like to remove your secret key?",
{
title: "Remove secrect key",
persistent: false,
buttons: { "Remove": true, "Cancel": false},
defaultButton: 1,
submit: function(e,v,m,f){
if(v)
{
sharedKey = '';
lockRoom();
}
}
});
else
$.prompt('<h2>Set a secrect key to lock your room</h2>' +
'<input id="lockKey" type="text" placeholder="your shared key" autofocus>',
{
persistent: false,
buttons: { "Save": true , "Cancel": false},
defaultButton: 1,
loaded: function(event) {
document.getElementById('lockKey').focus();
},
submit: function(e,v,m,f){
if(v)
{
var lockKey = document.getElementById('lockKey');
if (lockKey.value != null)
{
sharedKey = lockKey.value;
lockRoom(true);
}
}
}
});
}
function openLinkDialog() {
$.prompt('<input id="inviteLinkRef" type="text" value="' + roomUrl + '" onclick="this.select();">',
{
title: "Share this link with everyone you want to invite",
persistent: false,
buttons: { "Share": false},
loaded: function(event) {
document.getElementById('inviteLinkRef').select();
},
submit: function(e,v,m,f) {
connection.emuc.sendMessage(roomUrl, nickname);
}
});
}
function lockRoom(lock) {
connection.emuc.lockRoom(sharedKey);
buttonClick("#lockIcon", "fa fa-unlock fa-lg fa fa-lock fa-lg");
}
function openChat() {
var chatspace = $('#chatspace');
var videospace = $('#videospace');
var chatspaceWidth = chatspace.width();
if (chatspace.css("opacity") == 1) {
chatspace.animate({opacity: 0}, "fast");
chatspace.animate({width: 0}, "slow");
videospace.animate({right: 0, width:"100%"}, "slow");
}
else {
chatspace.animate({width: "23%"}, "slow");
chatspace.animate({opacity: 1}, "slow");
videospace.animate({right:chatspaceWidth, width:"80%"}, "slow");
}
// Request the focus in the nickname field or the chat input field.
if ($('#nickinput').is(':visible'))
$('#nickinput').focus();
else
$('#usermsg').focus();
}
function showToolbar() {
$('#toolbar').css({visibility:"visible"});
}
function updateRoomUrl(newRoomUrl) {
roomUrl = newRoomUrl;
}
function setEmoticons(body)
{
if (body)
{
body = body.replace(/:\)/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZSAPrkL1pSI/jhMFlTTPPYNPvnLZCEJnttUuvIOfniMIF7du7OOJ6RJsKgN/vmLYRtLoRsL4FoL/niL/HTNu/cKvHUNYRtL/nhMPnjL/rlLoNrL8WnNYFmMPbcMvffMdixO969OI+DJvHUNu/PN+7MOO/QN+zLOaudJ3xvUWdeI4J8dntsUoBmMIJ3JYVwLu3MOL+aOPznLPLVNYF8dvDRN926Of3oLHxwUerGOtq1OsKiN/jgMJ2QJ4+LhPTaM9TDKXRqJe/POPvlLmdeJD44IdPBK+LQKuDOK9LAK5yPKHZzdO3aLExFIoJ9dlhSTPfeMY+KhPzoLPHx8dbV1f3pK////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+JzxDT1BZPkNvcHlyaWdodCBKaXZlIFNvZnR3YXJlIDIwMDItMjAwMwAh+QQBAABSACwAAAAAEAAQAAAH1YBSgoIKKywcBzODi1JQER84LyQIORpQjE4wJgRPAhcePiMNA4NQMEFPGQVRUTFCOyI6l1IRJk8OISlUVCkGBQI0D1IKHwQZUQEBu8lUDh01Kis4TwVUDAa7BidUUQkLKCwvAlG75eU2ABMQHCQX5ObnABUWBwgeMfDlURIlNzM5PpC0oGCOQosiT0AokaJhxI4kTBj8MHKCCA8MMlwIgtJAhIAlPIAMCXEEA4ENPQYN0EGjQwIAACQ8kbGB1CIoD2osmFChBAgXKRkJUoECgoUbTRgFAgA7' border='0'>");
body = body.replace(/:-\)/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZSAPrkL1pSI/jhMFlTTPPYNPvnLZCEJnttUuvIOfniMIF7du7OOJ6RJsKgN/vmLYRtLoRsL4FoL/niL/HTNu/cKvHUNYRtL/nhMPnjL/rlLoNrL8WnNYFmMPbcMvffMdixO969OI+DJvHUNu/PN+7MOO/QN+zLOaudJ3xvUWdeI4J8dntsUoBmMIJ3JYVwLu3MOL+aOPznLPLVNYF8dvDRN926Of3oLHxwUerGOtq1OsKiN/jgMJ2QJ4+LhPTaM9TDKXRqJe/POPvlLmdeJD44IdPBK+LQKuDOK9LAK5yPKHZzdO3aLExFIoJ9dlhSTPfeMY+KhPzoLPHx8dbV1f3pK////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+JzxDT1BZPkNvcHlyaWdodCBKaXZlIFNvZnR3YXJlIDIwMDItMjAwMwAh+QQBAABSACwAAAAAEAAQAAAH1YBSgoIKKywcBzODi1JQER84LyQIORpQjE4wJgRPAhcePiMNA4NQMEFPGQVRUTFCOyI6l1IRJk8OISlUVCkGBQI0D1IKHwQZUQEBu8lUDh01Kis4TwVUDAa7BidUUQkLKCwvAlG75eU2ABMQHCQX5ObnABUWBwgeMfDlURIlNzM5PpC0oGCOQosiT0AokaJhxI4kTBj8MHKCCA8MMlwIgtJAhIAlPIAMCXEEA4ENPQYN0EGjQwIAACQ8kbGB1CIoD2osmFChBAgXKRkJUoECgoUbTRgFAgA7' border='0'>");
body = body.replace(/:\(/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZiANDn8cji74aKjsXg7qrR6kdpgLLV63p8gFBSVnqDibjY7FhdYbDU64eKjpDD5Z7L6M7m8M/m8E1QVIrB5JfH5rHV67TW65bG5rzb7cXg75jH5qvS6nh7gGaava/T6pLE5b7c7a3T6kVof26evmiq1LXX65jI5onA5Hix1szk78Pe7lqWu6fP6Xh8gIe/5KLN6FhreU1rgW+t1YiKjl1uej1lfjxlfo7D5Y3C5U9RVFlseUlpgMDd7sXa5Mbg77rZ7LfY7E5RVIK942SavH2z18He7qu+yF5uekBmf8ni70NER3+74nZzdG93e4+cosbb5J2wu6q+yMHd7p/L6FFTVsfh74/D5YSPlXqJkzk4On+84o+co6m9yL7b7Whyep6xu4WQlnmDiPHx8c3l8NbV1dHn8f///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+JzxDT1BZPkNvcHlyaWdodCBKaXZlIFNvZnR3YXJlIDIwMDItMjAwMwAh+QQBAABiACwAAAAAEAAQAAAH6IBigoIcMDY1Oi2Di2ICSCRaJxNCMiICjAgrLg8bFQYhLzdDCIMCK1YEP0UDGSoYHhodDYJILgRdUAtjYwtfUgwfO2IcJA8/VRJBAABBEkkgLCgHMEsbRSlXCWVlCU5jAwY4NDYnFQNjANvqEAFAFwU1EwYZ6OrbEQEKFAU6QiEqKXqAyZGjSY8xPiw4ONJCxgsMXJSEeVJmSxYjPAgQYSJGxA0PWKKMgRBhjBEvJUzEECRgiAYGIAYECOCDR4kpI2YMQtDhAwsDQBRYIGBiBBVGDXagwHGBggMiMXQyEnSARoECRw4wCgQAOw==' border='0'>");
body = body.replace(/:-\(/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZiANDn8cji74aKjsXg7qrR6kdpgLLV63p8gFBSVnqDibjY7FhdYbDU64eKjpDD5Z7L6M7m8M/m8E1QVIrB5JfH5rHV67TW65bG5rzb7cXg75jH5qvS6nh7gGaava/T6pLE5b7c7a3T6kVof26evmiq1LXX65jI5onA5Hix1szk78Pe7lqWu6fP6Xh8gIe/5KLN6FhreU1rgW+t1YiKjl1uej1lfjxlfo7D5Y3C5U9RVFlseUlpgMDd7sXa5Mbg77rZ7LfY7E5RVIK942SavH2z18He7qu+yF5uekBmf8ni70NER3+74nZzdG93e4+cosbb5J2wu6q+yMHd7p/L6FFTVsfh74/D5YSPlXqJkzk4On+84o+co6m9yL7b7Whyep6xu4WQlnmDiPHx8c3l8NbV1dHn8f///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+JzxDT1BZPkNvcHlyaWdodCBKaXZlIFNvZnR3YXJlIDIwMDItMjAwMwAh+QQBAABiACwAAAAAEAAQAAAH6IBigoIcMDY1Oi2Di2ICSCRaJxNCMiICjAgrLg8bFQYhLzdDCIMCK1YEP0UDGSoYHhodDYJILgRdUAtjYwtfUgwfO2IcJA8/VRJBAABBEkkgLCgHMEsbRSlXCWVlCU5jAwY4NDYnFQNjANvqEAFAFwU1EwYZ6OrbEQEKFAU6QiEqKXqAyZGjSY8xPiw4ONJCxgsMXJSEeVJmSxYjPAgQYSJGxA0PWKKMgRBhjBEvJUzEECRgiAYGIAYECOCDR4kpI2YMQtDhAwsDQBRYIGBiBBVGDXagwHGBggMiMXQyEnSARoECRw4wCgQAOw==' border='0'>");
body = body.replace(/:D/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZVAMjHx2deI/PYNPvnLVlTTPjhMIFmMI6AKIJ8doBmMIRsL4RtL+/PN4RtLu7OOIF8dsKiN7+aOPnhMCMfIIFoL/3oLPvmLXxvUfrkL8KgN/DRN/HUNe/QN+zWLp+dnd69OPLVNa2rq4VwLsWnNe7MOPHUNlpXWHRpJY+LhOvIOe3MONixO9q1Orq5ud26OXxwUfHTNuzLOfbcMurGOoNrL3ttUvrlLntsUvTaM4F7dqudJ+rUL3ZzdFlRJI+DJvznLPjgMPvlLtC8LdG9LUxEI5uLKVlQJPffMe/POFhSTEtEI+vUL5qKKvniMIJ9dp6RJvfeMfzoLI+KhFpSI5CEJvHx8dbV1f3pK////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+JzxDT1BZPkNvcHlyaWdodCBKaXZlIFNvZnR3YXJlIDIwMDItMjAwMwAh+QQBAABVACwAAAAAEAAQAAAH2oBVgoI5NwkGNQ+Di1VSFCszKiQpLDRSjEkRMQJQBRJHOAwZBINSEUhQNgNRUT9BQCUQl1UUMVAWPgFXVwFUAwUaDVU5KwI2UVNTu8lXFjIuCDczUANXT1S7VDpXUU0OFwkqBVG75eUVGDAKBiQS5ObnGBsLNSlME8lU+lQBUxNFHF48YIHDSAgsABICwBKiB5QPPKrQYLBDiQcsGLF4INIBhAhBUjKUWHLCRIsWJk50EDACxSACEDTIEHLgwBAoIEaQWiSlgQsHMDZw+CDCJSNBCC4oWPDCCaNAADs=' border='0'>");
body = body.replace(/:x/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAPcAAKyrq/Hx8dbV1YNyMP3oN2ZiWunNPiMfIPXeOu7UPe/cNuzSPT44I396asjHx395anRqKXBoUHpvRvrkOGdeJ+zRPvbfOufLP4J9du7VPe3TPbq5ufPbO9PCM9m+PfLaO+iMbfPcO9TDM3hrOndpOvbeOpuHNOiNbvnjOJ+NM+DNNdzIN7SdOO7aNuiDY+DGPN/MNfDYPNyAY7GZOGNMMPXdO5CELGFVKrioMPHZPMa2McuxPHVqKWBUNtxWO/zmOPDXPGRdTuh8XO/VPC8pIjArItvAPcNrUX11XtyFaKqcL+7bNtzBPXBpUNrAPeXJP6udLvzoN/nkOOh5WJ2KM9i8PoteK/rlOOrVOFlRJoY4K+rPPvvlOPjjOXBcKvjhOe/VPfHYPIFvMeiObuiLbJ5fS8SzMnBNRcOxMod4L29mUIJ2Kox+LWZZWefROXFrXJ5eSpmKL+XIP5yJNHBlKu3YNvTdO+HPNG5MOHVrKXs8NPznN7agN+hcP/jiOcKxM892W3ZmLuiOb9zCPZ2QLZJdTGxgOOjNPm9nUPbgOnRpKXlOOrObOH9yLOLQNP3pN////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+JzxDT1BZPkNvcHlyaWdodCBKaXZlIFNvZnR3YXJlIDIwMDItMjAwMwAh+QQBAAAAACwAAAAAEAAQAAAI7gABCBz4gIQYEg8GKhSoZoYcA1sMPDHxZiGAHlUqhAlhJ0SOBR4iDGwTZEcaL1hQTJDypUYGDwUA6Omz6MYRH2s6HOiAo4gbDSYAaBGSpBCZKVagHHik48CfDxcwnHFxQtAJQDYcUXikIEsdBBUkAIADYgwIGgoeqX1E4EeiBAMAECkjA48IEWvZtliRIW6gFGxg3Nm6NoqKRi/EIrnwwY8SCHnSEliiiA4fDAIZJSgxYQ8hBjwgMIgDZs5ARE6AlOhyhYsZNBZiMIk50JCRBBwQWEDAYcigJhYjsDi0QMMCA1RoWwTQYMSAEQ0WBgQAOw==' border='0'>");
body = body.replace(/;\\/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZaAOvQPvLaO4J9dvbfOvrkOHxzUu3TPXtxUvjiOYNyMPvlOOrOPvznN/TcO1lUTPfhOZ6RLfvmOPTdO4FvMWdeJ+7UPe3SPde7PtzBPfnjOfXeO+jMP/XdO+vRPoF9dsCoOoFwMP3oN/HZPFpSJufLP/znOOzRPoR0MIJxMNi9PbmqMIBvMfnkOIJyMPrlOL6kOtrAPcCnOsKrOffgOT44I5uMLnNpKdK/NFlRJu/WPO/bNtPCM+7aNnVrKa+cNPDXPIp8Lsy4NuXOOkxFJJWEMN3KNu/cNvnjOODNNXZzdDEsIe7bNunNPlhTTPPbO8e2Me/WPSMfILehN+LQNHRqKc+8NfPcO9TDM56QLcy5NvHx8dbV1Y+LhPzoN/3pN////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+JzxDT1BZPkNvcHlyaWdodCBKaXZlIFNvZnR3YXJlIDIwMDItMjAwMwAh+QQBAABaACwAAAAAEAAQAAAH54BagoIeBysTBwKDi1pcIBckCwsbKShcjE0vUkRCDVlAPgAxDoNcLwABVTYsNxk1QQYfl1ogTAEPLjxdXUtIRQ0mCVoeF1ADLlQUXipKXREPIjACByQBCAw4I14QUUYMBBIABSsLDSxdKhBeU8pdCgMWLRMLEgRdXvj5Ie8GCQcbTjIwwHIFH4QnXQhw6FBAQIofMyLsGDKCRg8dJRAEwJBECwoAVhBEYNAlRJcSRzRUOCGISwwDDR4QUKCAAAINOWTM0uLggwkREgYM4BCgggxSi7gkgAHAgoEOGE7sZCSgQIsEDRkFAgA7' border='0'>");
body = body.replace(/B-\)/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZdAOvQPvLaO4J9dvbfOvTcO+3TPXxzUntxUoNyMFlRJurOPllUTPzoN/rkOPTdO+/cNvznN/njOYJyMOvRPujMP8KrOdrAPfXdO+7UPTEsIfPbO+3SPdzBPenNPoJxMKudLufLP8CoOvjiOYBvMYF9doFvMZuNLsCnOuzRPr6kOte7PvPcO4R0MOHPNPXeO/DXPHZzdD44I4FwMNi9Pe/bNo6BLFpRJse3MdC9NKmbL6maL/rlOFlQJu/WPXNpKYJ2KoJ3KvfhOUtEJJ6XazEtLqqjesq1N+/WPOXOOlhTTPHZPJ2QLWZcKGdfNMiyN1hPJvvmOMe2MdTCM93JNurVOM+8NfvlOPnkOOfROZqMLrmqMIN+affgOfHx8dbV1Y+LhCMfIP3pN////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+JzxDT1BZPkNvcHlyaWdodCBKaXZlIFNvZnR3YXJlIDIwMDItMjAwMwAh+QQBAABdACwAAAAAEAAQAAAH4YBdgoIkByMlBwKDi11fMiogCgoUMx5fjEkpHT0BBA4aLwAnC4NfKQABAyJXDRFcKwUhl10yHQFBOxAMDBBQIgQoCF0kKk5FPAkJNkNNyU9GFgIHIEgwYNcxW0TXYFgABiMKBDrXGR9aGdc5AxsSJQoODTRRD2FhDzctVgMFCAcUGiJAsEcwDIMGFyYYEDDjRZUfNAo+AIIjAAcYXTwAWJFFyBIpLT7EMOECAwtBX04UIEDFhA8mNaa4OFJhVpcFIVAocTBgwIUAGCqQWvQFgQUAGwpM4MDCJiMBBiQgWMgoEAA7' border='0'>");
body = body.replace(/8-\)/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZdAOvQPvLaO4J9dvbfOvTcO+3TPXxzUntxUoNyMFlRJurOPllUTPzoN/rkOPTdO+/cNvznN/njOYJyMOvRPujMP8KrOdrAPfXdO+7UPTEsIfPbO+3SPdzBPenNPoJxMKudLufLP8CoOvjiOYBvMYF9doFvMZuNLsCnOuzRPr6kOte7PvPcO4R0MOHPNPXeO/DXPHZzdD44I4FwMNi9Pe/bNo6BLFpRJse3MdC9NKmbL6maL/rlOFlQJu/WPXNpKYJ2KoJ3KvfhOUtEJJ6XazEtLqqjesq1N+/WPOXOOlhTTPHZPJ2QLWZcKGdfNMiyN1hPJvvmOMe2MdTCM93JNurVOM+8NfvlOPnkOOfROZqMLrmqMIN+affgOfHx8dbV1Y+LhCMfIP3pN////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+JzxDT1BZPkNvcHlyaWdodCBKaXZlIFNvZnR3YXJlIDIwMDItMjAwMwAh+QQBAABdACwAAAAAEAAQAAAH4YBdgoIkByMlBwKDi11fMiogCgoUMx5fjEkpHT0BBA4aLwAnC4NfKQABAyJXDRFcKwUhl10yHQFBOxAMDBBQIgQoCF0kKk5FPAkJNkNNyU9GFgIHIEgwYNcxW0TXYFgABiMKBDrXGR9aGdc5AxsSJQoODTRRD2FhDzctVgMFCAcUGiJAsEcwDIMGFyYYEDDjRZUfNAo+AIIjAAcYXTwAWJFFyBIpLT7EMOECAwtBX04UIEDFhA8mNaa4OFJhVpcFIVAocTBgwIUAGCqQWvQFgQUAGwpM4MDCJiMBBiQgWMgoEAA7' border='0'>");
body = body.replace(/:p/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZZAPzoN4J9dvLaO+vQPvTcO3xzUurOPu3TPfjiOYNyMPbfOntxUllUTPrkOPTdO/fhOZCELMKrOYJyMNi9PenNPtzBPfPbO9rAPfvlOPPcO+jMP+7UPde7PvHZPO3SPYF9dvDXPMCoOvznN+ViRsCnOvnkOOzRPr5UP4BvMfXeO2VUKYFvMb6kOmdeJ4R0MFpRJllRJuvRPu/WPVpSJpdHN+fLP4JxMIFwMFhTTD44I+/WPOvWN+vXN/XdO0s9JezYN35qLfnjOY+CLNPBM/vmOMa2Md7KNeXOOoF2K+fROf3oN2ddKHVrKYtgMXZzdMa1MX5rLaudLp6QLenUOPfgOdhdRPrlOPJmSY6CLPHx8dbV1Y+LhP3pN////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+JzxDT1BZPkNvcHlyaWdodCBKaXZlIFNvZnR3YXJlIDIwMDItMjAwMwAh+QQBAABZACwAAAAAEAAQAAAH34BZgoIfCygrCwGDi1lbNxw1BgYaEzZbjDgsFDICBA4WIAMkDINbLAMCCgglDUFUGQchl1k3FAIPWC0AAC1CCAQmCVkfHDIKVjAvXFwvMEQPHRcBCzUCCCJSEMsQUSINDgMFKAYEJQDL6FwAGAoeEisGDg3n6VxK7AcJCxoWPE/1XIr86BGjQIAJIKYgyTGDCYQZPpbsEFDBSRYbAzIgGELjxIkqNIyk2OBC0BYSBwg8UDHiyggVKXREmJWFQQgTHZJAaQLkyIYIpBZtSXBhgIcDMSq4oMkoQAEJCQwyCgQAOw==' border='0'>");
body = body.replace(/X-\(/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZdAO64qOu2pXJNOm5IM9qehI2GhOixn92iilZGQuSsmOOqley2plZPTYhrZNKSdNqehdKTdeGnkaZtUHJOO3NdVOCmj42GhdGSdOWtmY2Hhe24qLd2UsuJaNibgcyKanRQPsyLauiyoMiFYnFMON2iidGRc6JoSNufhteZfeewnc2MbL19Xc6Nbbp5VtWWenNgVuWumuq0o+q1pHNgV6lyVtyhiIlsZYlsZHJdU8+PcM+Ob96ji7+AYd6kjN+kjaVsTsmHZeKplG9KNeCmkH95dnZzdM6NbnleV8SYiXlfWIhsZKJ/dbmQhOy3p6F+de23p4drY1ZOTOixnohrY8SXiLmQg+u1pOmzouaum9WXeu65qH94duavnPHx8dbV1e65qf///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+JzxDT1BZPkNvcHlyaWdodCBKaXZlIFNvZnR3YXJlIDIwMDItMjAwMwAh+QQBAABdACwAAAAAEAAQAAAH5oBdgoJbOAMDFFuDi10FQhsiHiBALSMFjFEmHC4PBwcnKCw/DIMFJkYEERhcXDBBNQ4SFoJCHAQKUjEBATIhCSQ5E11bGy4RR1VPAAAaTEkKHSuFIg8YMU4IX18IS1ZcOyovAx4HXAEN2ds3CwYVJQIDIOUBSjbaNusGQxcCFEAnMGRo0KYNQIAUPXTM2NICRZAQCwBo+QKgyZUEBHgU6TKCRY0EVKA0aDAFCRYfED4IKvDDAQkFXAwYSJHARxYaGQYxkJCjw44KQ3oQgECD1CILE1aoKHFBB48PORkReiFAwAwijAIBADs=' border='0'>");
body = body.replace(/:\^O/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZTAMe3KPjhMFlTTPPYNPzoLIJ8doBmMOvIOe/QN3tsUvrkL+zLOY6AKPnjL3ttUu7OOOrGOoF8doRsL+HPKuDOK4FmMHxvUfLVNYRtLsWnNd69OINrL9ixO+/PN/TaM3xwUfHUNe3MOIRtLzEsIfDRN0xJSvffMdq1OnRqJe7MOIVwLr+aOMKgN/bcMt26OfjgMPnhMIFoL/HUNsKiN4F7dtTDKY+LhPHTNoJ9dj46L/niMLakK8W1KnZzdFlRJJ2QJ+/POO3YLeDMLIJ2JvvnLbWkK4F2JuDNLO3ZLVhSTPniL/rlLllQJD45L/vmLcW1KffeMVpXWI+KhPHx8cjHx9bV1ZCEJv3pKyMfIP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+JzxDT1BZPkNvcHlyaWdodCBKaXZlIFNvZnR3YXJlIDIwMDItMjAwMwAh+QQBAABTACwAAAAAEAAQAAAH0IBTgoI0CQYVDhGDi1NSMRwQISkHJxtSjEkrCwNQATAmHh0sAoNSK0BQQTwEBE9ILzIzl1MxC1BHI1gAAFgjFAEkGFM0HANLEzUAV1cANRNOLS4FCRBQRMvY2AQ6DxYGIQE/VuPk5Ao3EhUpME1U7u/uOQogIg4HJiglUfv8JShKCD5EOOFBCJaDCA9SgKKhx5QNHV4YSXhwSIMLKgRJYSEjAJOEPhoMyGBjkIAZJFoUYcBgB5QLGUgtkoLBxYMbIBBoUFGSkaACFiSI+ICDUSAAOw==' border='0'>");
body = body.replace(/:\^0/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZTAMe3KPjhMFlTTPPYNPzoLIJ8doBmMOvIOe/QN3tsUvrkL+zLOY6AKPnjL3ttUu7OOOrGOoF8doRsL+HPKuDOK4FmMHxvUfLVNYRtLsWnNd69OINrL9ixO+/PN/TaM3xwUfHUNe3MOIRtLzEsIfDRN0xJSvffMdq1OnRqJe7MOIVwLr+aOMKgN/bcMt26OfjgMPnhMIFoL/HUNsKiN4F7dtTDKY+LhPHTNoJ9dj46L/niMLakK8W1KnZzdFlRJJ2QJ+/POO3YLeDMLIJ2JvvnLbWkK4F2JuDNLO3ZLVhSTPniL/rlLllQJD45L/vmLcW1KffeMVpXWI+KhPHx8cjHx9bV1ZCEJv3pKyMfIP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+JzxDT1BZPkNvcHlyaWdodCBKaXZlIFNvZnR3YXJlIDIwMDItMjAwMwAh+QQBAABTACwAAAAAEAAQAAAH0IBTgoI0CQYVDhGDi1NSMRwQISkHJxtSjEkrCwNQATAmHh0sAoNSK0BQQTwEBE9ILzIzl1MxC1BHI1gAAFgjFAEkGFM0HANLEzUAV1cANRNOLS4FCRBQRMvY2AQ6DxYGIQE/VuPk5Ao3EhUpME1U7u/uOQogIg4HJiglUfv8JShKCD5EOOFBCJaDCA9SgKKhx5QNHV4YSXhwSIMLKgRJYSEjAJOEPhoMyGBjkIAZJFoUYcBgB5QLGUgtkoLBxYMbIBBoUFGSkaACFiSI+ICDUSAAOw==' border='0'>");
body = body.replace(/;\)/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZTAPvnLVpSI/rkL/jhMPPYNFlTTIJ3JZ6RJnVrJHtsUvznLPniL6udJ4RtL4F7doNrL9q1OsKiN4RtLoFoL7+aOMKgN4VwLuzLOYFmMI+LhPHUNfrlLt26Oe/QN/ffMfbcMvLVNdixO/niMO3MOIRsL+7MOMWnNYJ8dvnhMP3oLOrGOnxvUfnjL+/PN4BmMO7OOHxwUd69OOLQKkxFIpCEJvDRN4F8dvHUNvTaM3ttUvHTNuvIOYF2Ju/cKo+CJ+rUL+HPKsW1Ke/cK8a2KXZzdPvmLcW0Ku3aLFhSTCMfIPvlLmdeI+/POD44IYJ9dp2QJ/jgMPfeMY+KhPHx8fzoLNbV1f3pK////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAFMALAAAAAAQABAAAAfXgFOCgg4JLhg5NoOLU1ITISojJTsQD1KMSBQXBFEDKB44LRUFg1IUTFEbAFRUCkpQNxGXUxMXUT5PVrq6AAM1ElMOIQQbSQFWBwxWMlZFHxwnCSpRAAFLVj0IAQEyVCIvKy4jA1S75lYpAjokGCUo5ee66RoNOTseCvEGVlQLHTA2IOAwcsAcgxkGAESJQWTKgxY/eDShQWMGAiFUWICwIEhKhRsDjgQZAoQKABYETGQYVCBCjQ8iBAhYEAWECVKLpEjg8EKHhg4xLKxkJOjEChINYDhhFAgAOw==' border='0'>");
body = body.replace(/;-\)/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZTAPvnLVpSI/rkL/jhMPPYNFlTTIJ3JZ6RJnVrJHtsUvznLPniL6udJ4RtL4F7doNrL9q1OsKiN4RtLoFoL7+aOMKgN4VwLuzLOYFmMI+LhPHUNfrlLt26Oe/QN/ffMfbcMvLVNdixO/niMO3MOIRsL+7MOMWnNYJ8dvnhMP3oLOrGOnxvUfnjL+/PN4BmMO7OOHxwUd69OOLQKkxFIpCEJvDRN4F8dvHUNvTaM3ttUvHTNuvIOYF2Ju/cKo+CJ+rUL+HPKsW1Ke/cK8a2KXZzdPvmLcW0Ku3aLFhSTCMfIPvlLmdeI+/POD44IYJ9dp2QJ/jgMPfeMY+KhPHx8fzoLNbV1f3pK////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAFMALAAAAAAQABAAAAfXgFOCgg4JLhg5NoOLU1ITISojJTsQD1KMSBQXBFEDKB44LRUFg1IUTFEbAFRUCkpQNxGXUxMXUT5PVrq6AAM1ElMOIQQbSQFWBwxWMlZFHxwnCSpRAAFLVj0IAQEyVCIvKy4jA1S75lYpAjokGCUo5ee66RoNOTseCvEGVlQLHTA2IOAwcsAcgxkGAESJQWTKgxY/eDShQWMGAiFUWICwIEhKhRsDjgQZAoQKABYETGQYVCBCjQ8iBAhYEAWECVKLpEjg8EKHhg4xLKxkJOjEChINYDhhFAgAOw==' border='0'>");
body = body.replace(/:8\}/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZcAPXgOoFRK/TbO+7HO+3CO4J5dXtiUPLVO/DNO+/KO/PZO/PYO/TcO/HSO1lQTIJ6deiuOu7IO9ePN/PaO+7JO+7GO755M+myO+amOumzOvHRO+q0OvLWO+itOntlUINZLIFTLOmxOuu4O4JWLOaiOb+ANNaLN+eoOtmaOINcLMGHNI+Hg3xmUIJXLNmWN+jWOLOlMuy/O+y8O4JYLO7FO+enOu3DO+irOuiqOr+BNPLUO/DPO/XhOsGyNOWgOejUONnEOKaZMM25NsGxNLOkMnJoKqeZMWVcKPTeOzEuJHZzdOu5O+jUOZqNL+WfOfLXO4x/Lc26NuisOvTdO1hQTCUiI6aXMOjVOKaZMefTOY+IhPXfOvHx8dbV1fXiOv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+JzxDT1BZPkNvcHlyaWdodCBKaXZlIFNvZnR3YXJlIDIwMDItMjAwMwAh+QQBAABcACwAAAAAEAAQAAAH4oBcgoIFBgEBBgWDi1wrICY+NSckEiNajFQWGCI2AxE0MjclDoMrFlIEOwcLChwaFRk5l1wgGAQNE1FbAFlABwMQH1wFJiI7E1tVRl5NSQwNMS4PBk42B0g8WENePUFbCxQ4HgE1AwtbXunqAAIIIS0BJxEKAOrr7RczBiQ0HFtXL9S9YKIgQQcWBSTI0MDgRxEYXmAcEaKDAAolXEbcqHCAwRYiUKxMeZJgQwpBWkpkGNBggQABCnQkWKJiFhcHOSDEoIAAQQICG1SQWqTlgwscIS50QJHCJqMHHlrMYPGAUSAAOw==' border='0'>");
body = body.replace(/:_\|/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZpANDn8YaKjrLV60dpgMji78Xg7np8gKrR6lBSVjg4Oszk70xJSjxlfoiKjp7L6GNpbsni71VbYEVof4rB5G6evsbg77HV67XX65fH5rfY7I3C5afP6ZLE5WaavU1rgc/m8bjY7GSavM7m8Hh7gJjI5onA5Hh8gJbG5klpgJDD5Ye/5JjH5lqWu4eKjnix1nmDiGRqbrDU61hreV1uemiq1G+t1aW1vbq5uXqDicXg71lseT1lfl5uer7c7Y7D5YK9432z10Bmf6LN6LTW65mnr5CvxJi60KjD06/T6n+84py80avS6s7l8LrZ7Mrj74/D5Tg3Opinr8Pf7n+74nyKlIqfrK3T6oierJ/L6J+xu8He7lFTVm93e8Dd7qzF1HZzdMfh75GwxHqDiM/m8MPe7sHd7n2LlKCyu83l8PHx8dbV1SMfIP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+JzxDT1BZPkNvcHlyaWdodCBKaXZlIFNvZnR3YXJlIDIwMDItMjAwMwAh+QQBAABpACwAAAAAEAAQAAAH54BpgoIjMgw7OiaDi2kBQTRJJRM/NRIBjAgsKg5FFgJhQj4hCIMBLE9GVxEFORFVSisdLYJBKgdUWQpoaApnZjEcKGkjNA5NYGgfAMtMED0bLgYyU0taCh82MDA2AGgFAhozDCUWBWhiCzc3C1wiBBknAzsTAjlRa/j5RAQgGAM6P6yQSZAvX4IKQ1LwMFFDyBEoD17gwPHiQQIvB4B8SSPBB5IyENCIGIPGiZQLJDwIChBiRYweBQgQqNDlAhYKDQYh6MBhg4AMIIYcIEFhC6MWKFxoOIEhBRAPORkJMjBjwAAeBhgFAgA7' border='0'>");
body = body.replace(/\?:\|/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZoAIJ9dvTcO3xzUvjiOevQPu3TPYNyMPzoN/rkOHtxUurOPvLaO1lUTPbfOvfgOe/WPeLPNIF9doBvMejMP+fLPyMfIPTdO+3SPfXdO8CoOti9PevRPvXeO4FwMO3ZNmdeJ8KrOezRPoR0MPPcO9e7PoJyMNzBPdTDM4FvMYJxMPvmOPDXPPznN9rAPcCnOtnDON3aOL6kOvvlOKy7RMGvNMWuOGeDcJavSe/WPNzCPefROT44I3ZzdHaPZtnUOvHZPMvFQXCKa8u3NtTCM73KOsa1MfPbO93KNp6xTXyTYnVqKenNPv3oN1hPJ4edXJCELLamMWiEb4aiU77LOu7UPYB0K4mnR/njOVhTTJCDLNrVOniGeM/GPvfhOY+kV5aqUp2QLca2MVlRJvnkOJOlVH+XYbXCP73JOvHx8dbV1Y+LhP3pN////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+JzxDT1BZPkNvcHlyaWdodCBKaXZlIFNvZnR3YXJlIDIwMDItMjAwMwAh+QQBAABoACwAAAAAEAAQAAAH4oBogoIRCRIoCQCDi2hqHSQUCgoTGilqjFgxOQ8LATpGKwQuDINqMTUvDQNQCFcOIwUZl2gdS0JNHixgRSwqAwEhBmgRJA80FWFMFU9rX1E9QC0ACRQLRzsQax8nU0FEUkkEAhIKAWMHa+npMAdmNhclKAoWCOjqazBIW2QFBgkTL6p8yKbOi5MZGDYIAKBhhQMPYk6oK3NmwAITPNCkIDBigIosSoYcuKGFAxURgtS4KBCgCwIZMhD44IADxCw0DDKE+GGhQQMMVriAILVIjYEWBC4U2GBCxE1GAASUMLCQUSAAOw==' border='0'>");
body = body.replace(/:O/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZoAIJ9dvTcO3xzUvjiOevQPu3TPYNyMPzoN/rkOHtxUurOPvLaO1lUTPbfOvfgOe/WPeLPNIF9doBvMejMP+fLPyMfIPTdO+3SPfXdO8CoOti9PevRPvXeO4FwMO3ZNmdeJ8KrOezRPoR0MPPcO9e7PoJyMNzBPdTDM4FvMYJxMPvmOPDXPPznN9rAPcCnOtnDON3aOL6kOvvlOKy7RMGvNMWuOGeDcJavSe/WPNzCPefROT44I3ZzdHaPZtnUOvHZPMvFQXCKa8u3NtTCM73KOsa1MfPbO93KNp6xTXyTYnVqKenNPv3oN1hPJ4edXJCELLamMWiEb4aiU77LOu7UPYB0K4mnR/njOVhTTJCDLNrVOniGeM/GPvfhOY+kV5aqUp2QLca2MVlRJvnkOJOlVH+XYbXCP73JOvHx8dbV1Y+LhP3pN////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+JzxDT1BZPkNvcHlyaWdodCBKaXZlIFNvZnR3YXJlIDIwMDItMjAwMwAh+QQBAABoACwAAAAAEAAQAAAH4oBogoIRCRIoCQCDi2hqHSQUCgoTGilqjFgxOQ8LATpGKwQuDINqMTUvDQNQCFcOIwUZl2gdS0JNHixgRSwqAwEhBmgRJA80FWFMFU9rX1E9QC0ACRQLRzsQax8nU0FEUkkEAhIKAWMHa+npMAdmNhclKAoWCOjqazBIW2QFBgkTL6p8yKbOi5MZGDYIAKBhhQMPYk6oK3NmwAITPNCkIDBigIosSoYcuKGFAxURgtS4KBCgCwIZMhD44IADxCw0DDKE+GGhQQMMVriAILVIjYEWBC4U2GBCxE1GAASUMLCQUSAAOw==' border='0'>");
body = body.replace(/:0/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZaAIJ9dvznN+rOPoNyMHxzUnVrKXtxUvjiOfrkOPTcO+3TPVlUTOvQPvLaO/TdO/vmOO3SPfnkOO7UPefLP+nNPti9Pde7PllRJufROYF9drOiMujMP8KrOYBvMfrlOO/cNvnjOb6kOuvRPmdeJ9rAPZ6QLcCnOoJyMLipMNPBM4R0MNzBPf3oNzArIvXeO+zRPoFwMIJxMFpSJvbfOsCoOvHZPPvlOIFvMd7EPFhTTODHO3FlKvznOPPcO/XdO/PbO/DXPKeXMN7FPHZzdKiYMK6bNLWkMaOTMfnjOKWUMcKxM3FmKq+cNMa2Md3INr6rNLWlMffgOe/WPd3JNr6rNcOyMse2Me/WPCMfIPzoN/Hx8dbV1Y+LhP3pN////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+JzxDT1BZPkNvcHlyaWdodCBKaXZlIFNvZnR3YXJlIDIwMDItMjAwMwAh+QQBAABaACwAAAAAEAAQAAAH4oBagoIZBh03BgCDi1pcMBYTAgIbFTFcjDkhFEVUCQ5PTDgmC4NcIUI7RgcRCCBQSzo0l1owFEdKHgFZWQEPVUkvA1oZFlJBHiglXV0lKA9ENSQABhMNBwEXMssyFwEIDgwEHQIJEVkFI8sjBVk2MxAnNwIOCFldH8v4LO4KAwYbP0AEWEawSxYEPkQQAFABSJQHWZoUKGCFBY8DDVYM0RKDQY8DKbCIxJICiQsJKgRxMaEggZMWIltMcXGFwywtC2i8qIFBgwYMDSRwILWIywASDCAoELFCxU1GAAicGLCQUSAAOw==' border='0'>");
body = body.replace(/:\|/gi, "<img src='data:image/gif;base64,R0lGODlhEAAQAOZJAJCELPjiOevQPoJ9durOPntxUvTcO/TdO/rkOHxzUu3TPYNyMFlUTMSzMvznN/HZPO3SPe/WPdzBPevRPsKrOe7UPf3oN4JxMPXdO1lRJoFvMcCoOvPbO/vmOPXeO/nkOIR0MNe7Pr6kOuzRPufLP1pRJsCnOoBvMfDXPPPcO4JyMIF9dvvlONrAPffgOWdeJ/njOYFwMNi9PejMP+nNPvfhOcSyMs+8NVhTTJ6QLfnjOJCDLJ2PLfrlOI+CLPznOHZzdO/WPJ2QLY6CLNC9NKudLvLaO/bfOvzoN/Hx8dbV1Y+LhP3pN////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+JzxDT1BZPkNvcHlyaWdodCBKaXZlIFNvZnR3YXJlIDIwMDItMjAwMwAh+QQBAABJACwAAAAAEAAQAAAH2IBJgoIrBScaBQODi0lLMSEkBAQzMhdLjDgiNBFGBgccKAImDINLIgJGRwEfCDAuKQobl0kxNEY1Qy9ISC8+AQYjC0krIRFHPRklTEwlGR01Dy0DBSRGAQ45AMsARQ4IBwIJJwQGH0jL6ExILEcQKhoEBwjn6UwW7AoLBTMcMA716hBgmJBggAwULjrQW2bhRwAjEoAkuSAgRQAeOwBoFKLDQwUQgpaYUGDgho0GKIl4CEJhVhIGG0Y8OHDkCAYjFSiQWrRkQQsBEBRMkADCJaMBCVQsKMgoEAA7' border='0'>");
}
return body
};
Date.prototype.format = function(format) {
var returnStr = '';
var replace = Date.replaceChars;
for (var i = 0; i < format.length; i++) {
var curChar = format.charAt(i);
if (replace[curChar]) {
returnStr += replace[curChar].call(this);
} else {
returnStr += curChar;
}
}
return returnStr;
};
Date.replaceChars = {
shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
longMonths: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
shortDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
longDays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
// Day
d: function() { return (this.getDate() < 10 ? '0' : '') + this.getDate(); },
D: function() { return Date.replaceChars.shortDays[this.getDay()]; },
j: function() { return this.getDate(); },
l: function() { return Date.replaceChars.longDays[this.getDay()]; },
N: function() { return this.getDay() + 1; },
S: function() { return (this.getDate() % 10 == 1 && this.getDate() != 11 ? 'st' : (this.getDate() % 10 == 2 && this.getDate() != 12 ? 'nd' : (this.getDate() % 10 == 3 && this.getDate() != 13 ? 'rd' : 'th'))); },
w: function() { return this.getDay(); },
z: function() { return "Not Yet Supported"; },
// Week
W: function() { return "Not Yet Supported"; },
// Month
F: function() { return Date.replaceChars.longMonths[this.getMonth()]; },
m: function() { return (this.getMonth() < 9 ? '0' : '') + (this.getMonth() + 1); },
M: function() { return Date.replaceChars.shortMonths[this.getMonth()]; },
n: function() { return this.getMonth() + 1; },
t: function() { return "Not Yet Supported"; },
// Year
L: function() { return "Not Yet Supported"; },
o: function() { return "Not Supported"; },
Y: function() { return this.getFullYear(); },
y: function() { return ('' + this.getFullYear()).substr(2); },
// Time
a: function() { return this.getHours() < 12 ? 'am' : 'pm'; },
A: function() { return this.getHours() < 12 ? 'AM' : 'PM'; },
B: function() { return "Not Yet Supported"; },
g: function() { return this.getHours() % 12 || 12; },
G: function() { return this.getHours(); },
h: function() { return ((this.getHours() % 12 || 12) < 10 ? '0' : '') + (this.getHours() % 12 || 12); },
H: function() { return (this.getHours() < 10 ? '0' : '') + this.getHours(); },
i: function() { return (this.getMinutes() < 10 ? '0' : '') + this.getMinutes(); },
s: function() { return (this.getSeconds() < 10 ? '0' : '') + this.getSeconds(); },
// Timezone
e: function() { return "Not Yet Supported"; },
I: function() { return "Not Supported"; },
O: function() { return (-this.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(this.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(this.getTimezoneOffset() / 60)) + '00'; },
T: function() { var m = this.getMonth(); this.setMonth(0); var result = this.toTimeString().replace(/^.+ \(?([^\)]+)\)?$/, '$1'); this.setMonth(m); return result;},
Z: function() { return -this.getTimezoneOffset() * 60; },
// Full Date/Time
c: function() { return "Not Yet Supported"; },
r: function() { return this.toString(); },
U: function() { return this.getTime() / 1000; }
};
\ No newline at end of file
/*
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
* Digest Algorithm, as defined in RFC 1321.
* Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
* Distributed under the BSD License
* See http://pajhome.org.uk/crypt/md5 for more info.
*/
var MD5 = (function () {
/*
* Configurable variables. You may need to tweak these to be compatible with
* the server-side, but the defaults work in most cases.
*/
var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */
var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
/*
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
* to work around bugs in some JS interpreters.
*/
var safe_add = function (x, y) {
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xFFFF);
};
/*
* Bitwise rotate a 32-bit number to the left.
*/
var bit_rol = function (num, cnt) {
return (num << cnt) | (num >>> (32 - cnt));
};
/*
* Convert a string to an array of little-endian words
* If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
*/
var str2binl = function (str) {
var bin = [];
var mask = (1 << chrsz) - 1;
for(var i = 0; i < str.length * chrsz; i += chrsz)
{
bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);
}
return bin;
};
/*
* Convert an array of little-endian words to a string
*/
var binl2str = function (bin) {
var str = "";
var mask = (1 << chrsz) - 1;
for(var i = 0; i < bin.length * 32; i += chrsz)
{
str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
}
return str;
};
/*
* Convert an array of little-endian words to a hex string.
*/
var binl2hex = function (binarray) {
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
var str = "";
for(var i = 0; i < binarray.length * 4; i++)
{
str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF);
}
return str;
};
/*
* Convert an array of little-endian words to a base-64 string
*/
var binl2b64 = function (binarray) {
var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var str = "";
var triplet, j;
for(var i = 0; i < binarray.length * 4; i += 3)
{
triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16) |
(((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) |
((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
for(j = 0; j < 4; j++)
{
if(i * 8 + j * 6 > binarray.length * 32) { str += b64pad; }
else { str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); }
}
}
return str;
};
/*
* These functions implement the four basic operations the algorithm uses.
*/
var md5_cmn = function (q, a, b, x, s, t) {
return safe_add(bit_rol(safe_add(safe_add(a, q),safe_add(x, t)), s),b);
};
var md5_ff = function (a, b, c, d, x, s, t) {
return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
};
var md5_gg = function (a, b, c, d, x, s, t) {
return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
};
var md5_hh = function (a, b, c, d, x, s, t) {
return md5_cmn(b ^ c ^ d, a, b, x, s, t);
};
var md5_ii = function (a, b, c, d, x, s, t) {
return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
};
/*
* Calculate the MD5 of an array of little-endian words, and a bit length
*/
var core_md5 = function (x, len) {
/* append padding */
x[len >> 5] |= 0x80 << ((len) % 32);
x[(((len + 64) >>> 9) << 4) + 14] = len;
var a = 1732584193;
var b = -271733879;
var c = -1732584194;
var d = 271733878;
var olda, oldb, oldc, oldd;
for (var i = 0; i < x.length; i += 16)
{
olda = a;
oldb = b;
oldc = c;
oldd = d;
a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819);
b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426);
c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416);
d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682);
d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329);
a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
c = md5_gg(c, d, a, b, x[i+11], 14, 643717713);
b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083);
c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438);
d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501);
a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473);
b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562);
b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353);
c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174);
d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189);
a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
c = md5_hh(c, d, a, b, x[i+15], 16, 530742520);
b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415);
c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571);
d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359);
d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649);
a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259);
b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
a = safe_add(a, olda);
b = safe_add(b, oldb);
c = safe_add(c, oldc);
d = safe_add(d, oldd);
}
return [a, b, c, d];
};
/*
* Calculate the HMAC-MD5, of a key and some data
*/
var core_hmac_md5 = function (key, data) {
var bkey = str2binl(key);
if(bkey.length > 16) { bkey = core_md5(bkey, key.length * chrsz); }
var ipad = new Array(16), opad = new Array(16);
for(var i = 0; i < 16; i++)
{
ipad[i] = bkey[i] ^ 0x36363636;
opad[i] = bkey[i] ^ 0x5C5C5C5C;
}
var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
return core_md5(opad.concat(hash), 512 + 128);
};
var obj = {
/*
* These are the functions you'll usually want to call.
* They take string arguments and return either hex or base-64 encoded
* strings.
*/
hexdigest: function (s) {
return binl2hex(core_md5(str2binl(s), s.length * chrsz));
},
b64digest: function (s) {
return binl2b64(core_md5(str2binl(s), s.length * chrsz));
},
hash: function (s) {
return binl2str(core_md5(str2binl(s), s.length * chrsz));
},
hmac_hexdigest: function (key, data) {
return binl2hex(core_hmac_md5(key, data));
},
hmac_b64digest: function (key, data) {
return binl2b64(core_hmac_md5(key, data));
},
hmac_hash: function (key, data) {
return binl2str(core_hmac_md5(key, data));
},
/*
* Perform a simple self-test to see if the VM is working
*/
test: function () {
return MD5.hexdigest("abc") === "900150983cd24fb0d6963f7d28e17f72";
}
};
return obj;
})();
......@@ -29,6 +29,11 @@ Strophe.addConnectionPlugin('emuc', {
}
this.connection.send(join);
},
changeNick: function(jid) {
console.log("changeNick", jid);
var presence = $pres({to: jid}).c("x",{xmlns: 'http://jabber.org/protocol/muc'});
this.connection.send(presence);
},
onPresence: function (pres) {
var from = pres.getAttribute('from');
var type = pres.getAttribute('type');
......@@ -73,8 +78,19 @@ Strophe.addConnectionPlugin('emuc', {
},
onPresenceError: function (pres) {
var from = pres.getAttribute('from');
if ($(pres).find('>error[type="auth"]>not-authorized[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
$(document).trigger('passwordrequired.muc', [from]);
if ($(pres).find('>error[type="auth"]>not-authorized[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length)
{
var ob = this;
window.setTimeout(function () {
var given = window.prompt('Password required');
if (given != null) {
ob.connection.send($pres({to: ob.myroomjid }).c('x', {xmlns: 'http://jabber.org/protocol/muc'}).c('password').t(given));
} else {
// user aborted
}
}, 50);
} else {
console.warn('onPresError ', pres);
}
......@@ -94,8 +110,6 @@ Strophe.addConnectionPlugin('emuc', {
// FIXME: this is a hack. but jingle on muc makes nickchanges hard
var nick = $(msg).find('>nick[xmlns="http://jabber.org/protocol/nick"]').text() || Strophe.getResourceFromJid(msg.getAttribute('from'));
if (txt) {
console.log('chat', nick, txt);
updateChatConversation(nick, txt);
}
return true;
......@@ -110,6 +124,8 @@ Strophe.addConnectionPlugin('emuc', {
formsubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
formsubmit.c('field', {'var': 'FORM_TYPE'}).c('value').t('http://jabber.org/protocol/muc#roomconfig').up().up();
formsubmit.c('field', {'var': 'muc#roomconfig_roomsecret'}).c('value').t(key).up().up();
formsubmit.c('field', {'var': 'muc#roomconfig_passwordprotectedroom'}).c('value').t('1').up().up();
// FIXME: is muc#roomconfig_passwordprotectedroom required?
this.connection.sendIQ(formsubmit,
function (res) {
......
var Openfire = {};
/** Class: Openfire.Connection
* WebSockets Connection Manager for Openfire
*
* Thie class manages an WebSockets connection
* to an Openfire XMPP server through the WebSockets plugin and dispatches events to the user callbacks as
* data arrives. It uses the server side Openfire authentication
*
* After creating a Openfire object, the user will typically
* call connect() with a user supplied callback to handle connection level
* events like authentication failure, disconnection, or connection
* complete.
*
* To send data to the connection, use send(doc) or sendRaw(text)
*
* Use xmlInput(doc) and RawInput(text) overrideable function to receive XML data coming into the
* connection.
*
* The user will also have several event handlers defined by using
* addHandler() and addTimedHandler(). These will allow the user code to
* respond to interesting stanzas or do something periodically with the
* connection. These handlers will be active once authentication is
* finished.
*
* Create and initialize a Openfire object.
*
*
* Returns:
* A new Openfire object.
*/
Openfire.Connection = function(url)
{
if (!window.WebSocket)
{
window.WebSocket=window.MozWebSocket;
if (!window.WebSocket)
{
var msg = "WebSocket not supported by this browser";
alert(msg);
throw Error(msg);
}
}
this.host = url.indexOf("/") < 0 ? url : url.split("/")[2];
this.protocol = url.indexOf("/") < 0 ? "wss:" : (url.split("/")[0] == "http:") ? "ws:" : "wss:";
this.jid = "";
this.resource = "ofmeet";
this.streamId = null;
// handler lists
this.timedHandlers = [];
this.handlers = [];
this.removeTimeds = [];
this.removeHandlers = [];
this.addTimeds = [];
this.addHandlers = [];
this._idleTimeout = null;
this.authenticated = false;
this.disconnecting = false;
this.connected = false;
this.errors = 0;
this._uniqueId = Math.round(Math.random() * 10000);
// setup onIdle callback every 1/10th of a second
this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
// initialize plugins
for (var k in Strophe._connectionPlugins)
{
if (Strophe._connectionPlugins.hasOwnProperty(k)) {
var ptype = Strophe._connectionPlugins[k];
// jslint complaints about the below line, but this is fine
var F = function () {};
F.prototype = ptype;
this[k] = new F();
this[k].init(this);
}
}
}
Openfire.Connection.prototype = {
/** Function: reset
* Reset the connection.
*
* This function should be called after a connection is disconnected
* before that connection is reused.
*/
reset: function ()
{
this.streamId = null;
this.timedHandlers = [];
this.handlers = [];
this.removeTimeds = [];
this.removeHandlers = [];
this.addTimeds = [];
this.addHandlers = [];
this.authenticated = false;
this.disconnecting = false;
this.connected = false;
this.errors = 0;
},
/** Function: pause
* UNUSED with websockets
*/
pause: function ()
{
return;
},
/** Function: resume
* UNUSED with websockets
*/
resume: function ()
{
return;
},
/** Function: getUniqueId
* Generate a unique ID for use in <iq/> elements.
*
* All <iq/> stanzas are required to have unique id attributes. This
* function makes creating these easy. Each connection instance has
* a counter which starts from zero, and the value of this counter
* plus a colon followed by the suffix becomes the unique id. If no
* suffix is supplied, the counter is used as the unique id.
*
* Suffixes are used to make debugging easier when reading the stream
* data, and their use is recommended. The counter resets to 0 for
* every new connection for the same reason. For connections to the
* same server that authenticate the same way, all the ids should be
* the same, which makes it easy to see changes. This is useful for
* automated testing as well.
*
* Parameters:
* (String) suffix - A optional suffix to append to the id.
*
* Returns:
* A unique string to be used for the id attribute.
*/
getUniqueId: function (suffix)
{
if (typeof(suffix) == "string" || typeof(suffix) == "number") {
return ++this._uniqueId + ":" + suffix;
} else {
return ++this._uniqueId + "";
}
},
/** Function: connect
* Starts the connection process.
*
*
* Parameters:
* (String) username - The Openfire username.
* (String) pass - The user's password.
* (String) resource - The user resource for this connection.
* (Function) callback The connect callback function.
*/
connect: function (jid, pass, callback, wait, hold, route)
{
this.jid = jid.indexOf("/") > -1 ? jid : jid + '/' + this.resource;
this.username = jid.indexOf("@") < 0 ? null : jid.split("@")[0];
this.pass = pass == "" ? null : pass;
this.connect_callback = callback;
this.disconnecting = false;
this.connected = false;
this.authenticated = false;
this.errors = 0;
this._changeConnectStatus(Strophe.Status.CONNECTING, null);
this.url = this.protocol + "//" + this.host + "/ws/server?username=" + this.username + "&password=" + this.pass + "&resource=" + this.resource;
this._ws = new WebSocket(this.url, "xmpp");
this._ws.onopen = this._onopen.bind(this);
this._ws.onmessage = this._onmessage.bind(this);
this._ws.onclose = this._onclose.bind(this);
window.openfireWebSocket = this;
this.jid = this.jid.indexOf("@") < 0 ? this.resource + "@" + this.jid : this.jid;
this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
},
/**
*
* Private Function: _onopen websocket event handler
*
*/
_onopen: function()
{
this.connected = true;
this.authenticated = true;
this.resource = Strophe.getResourceFromJid(this.jid);
this.domain = Strophe.getDomainFromJid(this.jid);
try {
this._changeConnectStatus(Strophe.Status.CONNECTED, null);
} catch (e) {
throw Error("User connection callback caused an exception: " + e);
}
this.interval = setInterval (function() {window.openfireWebSocket.sendRaw(" ")}, 10000 );
},
/** Function: attach
* UNUSED, use connect again
*/
attach: function()
{
return
},
/** Function: xmlInput
* User overrideable function that receives XML data coming into the
* connection.
*
* The default function does nothing. User code can override this with
* > Openfire.xmlInput = function (elem) {
* > (user code)
* > };
*
* Parameters:
* (XMLElement) elem - The XML data received by the connection.
*/
xmlInput: function (elem)
{
return;
},
/** Function: xmlOutput
* User overrideable function that receives XML data sent to the
* connection.
*
* The default function does nothing. User code can override this with
* > Openfire.xmlOutput = function (elem) {
* > (user code)
* > };
*
* Parameters:
* (XMLElement) elem - The XMLdata sent by the connection.
*/
xmlOutput: function (elem)
{
return;
},
/** Function: rawInput
* User overrideable function that receives raw data coming into the
* connection.
*
* The default function does nothing. User code can override this with
* > Openfire.rawInput = function (data) {
* > (user code)
* > };
*
* Parameters:
* (String) data - The data received by the connection.
*/
rawInput: function (data)
{
return;
},
/** Function: rawOutput
* User overrideable function that receives raw data sent to the
* connection.
*
* The default function does nothing. User code can override this with
* > Openfire.rawOutput = function (data) {
* > (user code)
* > };
*
* Parameters:
* (String) data - The data sent by the connection.
*/
rawOutput: function (data)
{
return;
},
/** Function: sendRaw
* Send a stanza in raw XML text.
*
* This function is called to push data onto the send queue to
* go out over the wire. Whenever a request is sent to the BOSH
* server, all pending data is sent and the queue is flushed.
*
* Parameters:
* xml - The stanza text XML to send.
*/
sendRaw: function(xml) {
if(!this.connected || this._ws == null) {
throw Error("Not connected, cannot send packets.");
}
if (xml != " ")
{
this.xmlOutput(this._textToXML(xml));
this.rawOutput(xml);
}
this._ws.send(xml);
},
/** Function: send
* Send a stanza.
*
* This function is called to push data onto the send queue to
* go out over the wire. Whenever a request is sent to the BOSH
* server, all pending data is sent and the queue is flushed.
*
* Parameters:
* (XMLElement |
* [XMLElement] |
* Strophe.Builder) elem - The stanza to send.
*/
send: function(elem)
{
if(!this.connected || this._ws == null) {
throw Error("Not connected, cannot send packets.");
}
var toSend = "";
if (elem === null) { return ; }
if (typeof(elem.sort) === "function")
{
for (var i = 0; i < elem.length; i++)
{
toSend += Strophe.serialize(elem[i]);
this.xmlOutput(elem[i]);
}
} else if (typeof(elem.tree) === "function") {
toSend = Strophe.serialize(elem.tree());
this.xmlOutput(elem.tree());
} else {
toSend = Strophe.serialize(elem);
this.xmlOutput(elem);
}
this.rawOutput(toSend);
this._ws.send(toSend);
},
/** Function: flush
* UNUSED
*/
flush: function ()
{
return
},
/** Function: sendIQ
* Helper function to send IQ stanzas.
*
* Parameters:
* (XMLElement) elem - The stanza to send.
* (Function) callback - The callback function for a successful request.
* (Function) errback - The callback function for a failed or timed
* out request. On timeout, the stanza will be null.
* (Integer) timeout - The time specified in milliseconds for a
* timeout to occur.
*
* Returns:
* The id used to send the IQ.
*/
sendIQ: function(elem, callback, errback, timeout) {
var timeoutHandler = null;
var that = this;
if (typeof(elem.tree) === "function") {
elem = elem.tree();
}
var id = elem.getAttribute('id');
// inject id if not found
if (!id) {
id = this.getUniqueId("sendIQ");
elem.setAttribute("id", id);
}
var handler = this.addHandler(function (stanza) {
// remove timeout handler if there is one
if (timeoutHandler) {
that.deleteTimedHandler(timeoutHandler);
}
var iqtype = stanza.getAttribute('type');
if (iqtype == 'result')
{
if (callback) {
callback(stanza);
}
} else if (iqtype == 'error') {
if (errback) {
errback(stanza);
}
} else {
throw {
name: "StropheError",
message: "Got bad IQ type of " + iqtype
};
}
}, null, 'iq', null, id);
// if timeout specified, setup timeout handler.
if (timeout)
{
timeoutHandler = this.addTimedHandler(timeout, function () {
// get rid of normal handler
that.deleteHandler(handler);
// call errback on timeout with null stanza
if (errback) {
errback(null);
}
return false;
});
}
this.send(elem);
return id;
},
/** Function: addTimedHandler
* Add a timed handler to the connection.
*
* This function adds a timed handler. The provided handler will
* be called every period milliseconds until it returns false,
* the connection is terminated, or the handler is removed. Handlers
* that wish to continue being invoked should return true.
*
* Because of method binding it is necessary to save the result of
* this function if you wish to remove a handler with
* deleteTimedHandler().
*
* Note that user handlers are not active until authentication is
* successful.
*
* Parameters:
* (Integer) period - The period of the handler.
* (Function) handler - The callback function.
*
* Returns:
* A reference to the handler that can be used to remove it.
*/
addTimedHandler: function (period, handler)
{
var thand = new Strophe.TimedHandler(period, handler);
this.addTimeds.push(thand);
return thand;
},
/** Function: deleteTimedHandler
* Delete a timed handler for a connection.
*
* This function removes a timed handler from the connection. The
* handRef parameter is *not* the function passed to addTimedHandler(),
* but is the reference returned from addTimedHandler().
*
* Parameters:
* (Strophe.TimedHandler) handRef - The handler reference.
*/
deleteTimedHandler: function (handRef)
{
// this must be done in the Idle loop so that we don't change
// the handlers during iteration
this.removeTimeds.push(handRef);
},
/** Function: addHandler
* Add a stanza handler for the connection.
*
* This function adds a stanza handler to the connection. The
* handler callback will be called for any stanza that matches
* the parameters. Note that if multiple parameters are supplied,
* they must all match for the handler to be invoked.
*
* The handler will receive the stanza that triggered it as its argument.
* The handler should return true if it is to be invoked again;
* returning false will remove the handler after it returns.
*
* As a convenience, the ns parameters applies to the top level element
* and also any of its immediate children. This is primarily to make
* matching /iq/query elements easy.
*
* The options argument contains handler matching flags that affect how
* matches are determined. Currently the only flag is matchBare (a
* boolean). When matchBare is true, the from parameter and the from
* attribute on the stanza will be matched as bare JIDs instead of
* full JIDs. To use this, pass {matchBare: true} as the value of
* options. The default value for matchBare is false.
*
* The return value should be saved if you wish to remove the handler
* with deleteHandler().
*
* Parameters:
* (Function) handler - The user callback.
* (String) ns - The namespace to match.
* (String) name - The stanza name to match.
* (String) type - The stanza type attribute to match.
* (String) id - The stanza id attribute to match.
* (String) from - The stanza from attribute to match.
* (String) options - The handler options
*
* Returns:
* A reference to the handler that can be used to remove it.
*/
addHandler: function (handler, ns, name, type, id, from, options)
{
var hand = new Strophe.Handler(handler, ns, name, type, id, from, options);
this.addHandlers.push(hand);
return hand;
},
/** Function: deleteHandler
* Delete a stanza handler for a connection.
*
* This function removes a stanza handler from the connection. The
* handRef parameter is *not* the function passed to addHandler(),
* but is the reference returned from addHandler().
*
* Parameters:
* (Strophe.Handler) handRef - The handler reference.
*/
deleteHandler: function (handRef)
{
// this must be done in the Idle loop so that we don't change
// the handlers during iteration
this.removeHandlers.push(handRef);
},
/** Function: disconnect
* Start the graceful disconnection process.
*
* This function starts the disconnection process. This process starts
* by sending unavailable presence and sending BOSH body of type
* terminate. A timeout handler makes sure that disconnection happens
* even if the BOSH server does not respond.
*
* The user supplied connection callback will be notified of the
* progress as this process happens.
*
* Parameters:
* (String) reason - The reason the disconnect is occuring.
*/
disconnect: function(reason) {
if(!this.connected || this._ws == null) {
return;
}
this._changeConnectStatus(Strophe.Status.DISCONNECTING, reason);
Strophe.info("Disconnect was called because: " + reason);
this._ws.close();
},
/** PrivateFunction: _onDisconnectTimeout
* _Private_ timeout handler for handling non-graceful disconnection.
*
* If the graceful disconnect process does not complete within the
* time allotted, this handler finishes the disconnect anyway.
*
* Returns:
* false to remove the handler.
*/
_onDisconnectTimeout: function ()
{
Strophe.info("_onDisconnectTimeout was called");
this._doDisconnect();
return false;
},
/** PrivateFunction: _doDisconnect
* _Private_ function to disconnect.
*
* This is the last piece of the disconnection logic. This resets the
* connection and alerts the user's connection callback.
*/
_doDisconnect: function ()
{
Strophe.info("_doDisconnect was called");
this._onclose();
},
/** PrivateFunction: _changeConnectStatus
* _Private_ helper function that makes sure plugins and the user's
* callback are notified of connection status changes.
*
* Parameters:
* (Integer) status - the new connection status, one of the values
* in Strophe.Status
* (String) condition - the error condition or null
*/
_changeConnectStatus: function (status, condition)
{
// notify all plugins listening for status changes
for (var k in Strophe._connectionPlugins)
{
if (Strophe._connectionPlugins.hasOwnProperty(k))
{
var plugin = this[k];
if (plugin.statusChanged)
{
try {
plugin.statusChanged(status, condition);
} catch (err) {
Strophe.error("" + k + " plugin caused an exception changing status: " + err);
}
}
}
}
// notify the user's callback
if (typeof this.connect_callback == 'function')
{
try {
this.connect_callback(status, condition);
} catch (e) {
Strophe.error("User connection callback caused an exception: " + e);
}
}
},
/**
*
* Private Function: _onclose websocket event handler
*
*/
_onclose: function()
{
Strophe.info("websocket closed");
//console.log('_onclose - disconnected');
clearInterval(this.interval);
this.authenticated = false;
this.disconnecting = false;
this.streamId = null;
// tell the parent we disconnected
this._changeConnectStatus(Strophe.Status.DISCONNECTED, null);
this.connected = false;
// delete handlers
this.handlers = [];
this.timedHandlers = [];
this.removeTimeds = [];
this.removeHandlers = [];
this.addTimeds = [];
this.addHandlers = [];
if(this._ws.readyState != this._ws.CLOSED)
{
this._ws.close();
}
},
/**
*
* Private Function: _onmessage websocket event handler
*
*/
_onmessage: function(packet)
{
var elem;
try {
elem = this._textToXML(packet.data);
} catch (e) {
if (e != "parsererror") { throw e; }
this.disconnect("strophe-parsererror");
}
if (elem === null) { return; }
this.xmlInput(elem);
this.rawInput(packet.data);
// remove handlers scheduled for deletion
var i, hand;
while (this.removeHandlers.length > 0)
{
hand = this.removeHandlers.pop();
i = this.handlers.indexOf(hand);
if (i >= 0) {
this.handlers.splice(i, 1);
}
}
// add handlers scheduled for addition
while (this.addHandlers.length > 0)
{
this.handlers.push(this.addHandlers.pop());
}
// send each incoming stanza through the handler chain
var i, newList;
newList = this.handlers;
this.handlers = [];
for (i = 0; i < newList.length; i++)
{
var hand = newList[i];
if (hand.isMatch(elem) && (this.authenticated || !hand.user))
{
if (hand.run(elem))
{
this.handlers.push(hand);
}
} else {
this.handlers.push(hand);
}
}
},
/**
*
* Private Function: _textToXML convert text to DOM Document object
*
*/
_textToXML: function (text) {
var doc = null;
if (window['DOMParser']) {
var parser = new DOMParser();
doc = parser.parseFromString(text, 'text/xml');
} else if (window['ActiveXObject']) {
var doc = new ActiveXObject("MSXML2.DOMDocument");
doc.async = false;
doc.loadXML(text);
} else {
throw Error('No DOMParser object found.');
}
return doc.firstChild;
},
/** PrivateFunction: _onIdle
* _Private_ handler to process events during idle cycle.
*
* This handler is called every 100ms to fire timed handlers that
* are ready and keep poll requests going.
*/
_onIdle: function ()
{
var i, thand, since, newList;
// remove timed handlers that have been scheduled for deletion
while (this.removeTimeds.length > 0)
{
thand = this.removeTimeds.pop();
i = this.timedHandlers.indexOf(thand);
if (i >= 0) {
this.timedHandlers.splice(i, 1);
}
}
// add timed handlers scheduled for addition
while (this.addTimeds.length > 0)
{
this.timedHandlers.push(this.addTimeds.pop());
}
// call ready timed handlers
var now = new Date().getTime();
newList = [];
for (i = 0; i < this.timedHandlers.length; i++)
{
thand = this.timedHandlers[i];
if (this.authenticated || !thand.user) {
since = thand.lastCalled + thand.period;
if (since - now <= 0) {
if (thand.run()) {
newList.push(thand);
}
} else {
newList.push(thand);
}
}
}
this.timedHandlers = newList;
// reactivate the timer
clearTimeout(this._idleTimeout);
this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
}
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
function SDP(sdp) {
this.media = sdp.split('\r\nm=');
for (var i = 1; i < this.media.length; i++) {
this.media[i] = 'm=' + this.media[i];
if (i != this.media.length - 1) {
this.media[i] += '\r\n';
}
}
this.session = this.media.shift() + '\r\n';
this.raw = this.session + this.media.join('');
}
// remove iSAC and CN from SDP
SDP.prototype.mangle = function () {
var i, j, mline, lines, rtpmap, newdesc;
for (i = 0; i < this.media.length; i++) {
lines = this.media[i].split('\r\n');
lines.pop(); // remove empty last element
mline = SDPUtil.parse_mline(lines.shift());
if (mline.media != 'audio')
continue;
newdesc = '';
mline.fmt.length = 0;
for (j = 0; j < lines.length; j++) {
if (lines[j].substr(0, 9) == 'a=rtpmap:') {
rtpmap = SDPUtil.parse_rtpmap(lines[j]);
if (rtpmap.name == 'CN' || rtpmap.name == 'ISAC')
continue;
mline.fmt.push(rtpmap.id);
newdesc += lines[j] + '\r\n';
} else {
newdesc += lines[j] + '\r\n';
}
}
this.media[i] = SDPUtil.build_mline(mline) + '\r\n';
this.media[i] += newdesc;
}
this.raw = this.session + this.media.join('');
};
// remove lines matching prefix from session section
SDP.prototype.removeSessionLines = function(prefix) {
var ob = this;
var lines = SDPUtil.find_lines(this.session, prefix);
lines.forEach(function(line) {
ob.session = ob.session.replace(line + '\r\n', '');
});
this.raw = this.session + this.media.join('');
return lines;
}
// remove lines matching prefix from a media section specified by mediaindex
// TODO: non-numeric mediaindex could match mid
SDP.prototype.removeMediaLines = function(mediaindex, prefix) {
var ob = this;
var lines = SDPUtil.find_lines(this.media[mediaindex], prefix);
lines.forEach(function(line) {
ob.media[mediaindex] = ob.media[mediaindex].replace(line + '\r\n', '');
});
this.raw = this.session + this.media.join('');
return lines;
}
// add content's to a jingle element
SDP.prototype.toJingle = function (elem, thecreator) {
var i, j, k, mline, ssrc, rtpmap, tmp, line, lines;
var ob = this;
// new bundle plan
if (SDPUtil.find_line(this.session, 'a=group:')) {
lines = SDPUtil.find_lines(this.session, 'a=group:');
for (i = 0; i < lines.length; i++) {
tmp = lines[i].split(' ');
var semantics = tmp.shift().substr(8);
// new plan
elem.c('group', {xmlns: 'urn:xmpp:jingle:apps:grouping:0', type: semantics, semantics:semantics});
for (j = 0; j < tmp.length; j++) {
elem.c('content', {name: tmp[j]}).up();
}
elem.up();
// temporary plan, to be removed
elem.c('group', {xmlns: 'urn:ietf:rfc:5888', type: semantics});
for (j = 0; j < tmp.length; j++) {
elem.c('content', {name: tmp[j]}).up();
}
elem.up();
}
}
// old bundle plan, to be removed
var bundle = [];
if (SDPUtil.find_line(this.session, 'a=group:BUNDLE')) {
bundle = SDPUtil.find_line(this.session, 'a=group:BUNDLE ').split(' ');
bundle.shift();
}
for (i = 0; i < this.media.length; i++) {
mline = SDPUtil.parse_mline(this.media[i].split('\r\n')[0]);
if (!(mline.media == 'audio' || mline.media == 'video')) {
continue;
}
if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) {
ssrc = SDPUtil.find_line(this.media[i], 'a=ssrc:').substring(7).split(' ')[0]; // take the first
} else {
ssrc = false;
}
elem.c('content', {creator: thecreator, name: mline.media});
if (SDPUtil.find_line(this.media[i], 'a=mid:')) {
// prefer identifier from a=mid if present
var mid = SDPUtil.parse_mid(SDPUtil.find_line(this.media[i], 'a=mid:'));
elem.attrs({ name: mid });
// old BUNDLE plan, to be removed
if (bundle.indexOf(mid) != -1) {
elem.c('bundle', {xmlns: 'http://estos.de/ns/bundle'}).up();
bundle.splice(bundle.indexOf(mid), 1);
}
}
if (SDPUtil.find_line(this.media[i], 'a=rtpmap:').length) {
elem.c('description',
{xmlns: 'urn:xmpp:jingle:apps:rtp:1',
media: mline.media });
if (ssrc) {
elem.attrs({ssrc: ssrc});
}
for (j = 0; j < mline.fmt.length; j++) {
rtpmap = SDPUtil.find_line(this.media[i], 'a=rtpmap:' + mline.fmt[j]);
elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
// put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
if (SDPUtil.find_line(this.media[i], 'a=fmtp:' + mline.fmt[j])) {
tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(this.media[i], 'a=fmtp:' + mline.fmt[j]));
for (k = 0; k < tmp.length; k++) {
elem.c('parameter', tmp[k]).up();
}
}
this.RtcpFbToJingle(this.media[i], elem, mline.fmt[j]); // XEP-0293 -- map a=rtcp-fb
elem.up();
}
if (SDPUtil.find_line(this.media[i], 'a=crypto:', this.session)) {
elem.c('encryption', {required: 1});
var crypto = SDPUtil.find_lines(this.media[i], 'a=crypto:', this.session);
crypto.forEach(function(line) {
elem.c('crypto', SDPUtil.parse_crypto(line)).up();
});
elem.up(); // end of encryption
}
if (ssrc) {
// new style mapping
elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
// FIXME: group by ssrc and support multiple different ssrcs
var ssrclines = SDPUtil.find_lines(this.media[i], 'a=ssrc:');
ssrclines.forEach(function(line) {
idx = line.indexOf(' ');
var linessrc = line.substr(0, idx).substr(7);
if (linessrc != ssrc) {
elem.up();
ssrc = linessrc;
elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
}
var kv = line.substr(idx + 1);
elem.c('parameter');
if (kv.indexOf(':') == -1) {
elem.attrs({ name: kv });
} else {
elem.attrs({ name: kv.split(':', 2)[0] });
elem.attrs({ value: kv.split(':', 2)[1] });
}
elem.up();
});
elem.up();
// old proprietary mapping, to be removed at some point
tmp = SDPUtil.parse_ssrc(this.media[i]);
tmp.xmlns = 'http://estos.de/ns/ssrc';
tmp.ssrc = ssrc;
elem.c('ssrc', tmp).up(); // ssrc is part of description
}
if (SDPUtil.find_line(this.media[i], 'a=rtcp-mux')) {
elem.c('rtcp-mux').up();
}
// XEP-0293 -- map a=rtcp-fb:*
this.RtcpFbToJingle(this.media[i], elem, '*');
// XEP-0294
if (SDPUtil.find_line(this.media[i], 'a=extmap:')) {
lines = SDPUtil.find_lines(this.media[i], 'a=extmap:');
for (j = 0; j < lines.length; j++) {
tmp = SDPUtil.parse_extmap(lines[j]);
elem.c('rtp-hdrext', { xmlns: 'urn:xmpp:jingle:apps:rtp:rtp-hdrext:0',
uri: tmp.uri,
id: tmp.value });
if (tmp.hasOwnProperty('direction')) {
switch (tmp.direction) {
case 'sendonly':
elem.attrs({senders: 'responder'});
break;
case 'recvonly':
elem.attrs({senders: 'initiator'});
break;
case 'sendrecv':
elem.attrs({senders: 'both'});
break;
case 'inactive':
elem.attrs({senders: 'none'});
break;
}
}
// TODO: handle params
elem.up();
}
}
elem.up(); // end of description
}
elem.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
// XEP-0320
var fingerprints = SDPUtil.find_lines(this.media[i], 'a=fingerprint:', this.session);
fingerprints.forEach(function(line) {
tmp = SDPUtil.parse_fingerprint(line);
tmp.xmlns = 'urn:xmpp:tmp:jingle:apps:dtls:0';
// tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0'; -- FIXME: update receivers first
elem.c('fingerprint').t(tmp.fingerprint);
delete tmp.fingerprint;
line = SDPUtil.find_line(ob.media[i], 'a=setup:', ob.session);
if (line) {
tmp.setup = line.substr(8);
}
elem.attrs(tmp);
elem.up();
});
tmp = SDPUtil.iceparams(this.media[i], this.session);
if (tmp) {
elem.attrs(tmp);
// XEP-0176
if (SDPUtil.find_line(this.media[i], 'a=candidate:', this.session)) { // add any a=candidate lines
lines = SDPUtil.find_lines(this.media[i], 'a=candidate:', this.session);
for (j = 0; j < lines.length; j++) {
tmp = SDPUtil.candidateToJingle(lines[j]);
elem.c('candidate', tmp).up();
}
}
elem.up(); // end of transport
}
if (SDPUtil.find_line(this.media[i], 'a=sendrecv', this.session)) {
elem.attrs({senders: 'both'});
} else if (SDPUtil.find_line(this.media[i], 'a=sendonly', this.session)) {
elem.attrs({senders: 'initiator'});
} else if (SDPUtil.find_line(this.media[i], 'a=recvonly', this.session)) {
elem.attrs({senders: 'responder'});
} else if (SDPUtil.find_line(this.media[i], 'a=inactive', this.session)) {
elem.attrs({senders: 'none'});
}
if (mline.port == '0') {
// estos hack to reject an m-line
elem.attrs({senders: 'rejected'});
}
elem.up(); // end of content
}
elem.up();
return elem;
};
SDP.prototype.RtcpFbToJingle = function (sdp, elem, payloadtype) { // XEP-0293
var lines = SDPUtil.find_lines(sdp, 'a=rtcp-fb:' + payloadtype);
for (var i = 0; i < lines.length; i++) {
var tmp = SDPUtil.parse_rtcpfb(lines[i]);
if (tmp.type == 'trr-int') {
elem.c('rtcp-fb-trr-int', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', value: tmp.params[0]});
elem.up();
} else {
elem.c('rtcp-fb', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', type: tmp.type});
if (tmp.params.length > 0) {
elem.attrs({'subtype': tmp.params[0]});
}
elem.up();
}
}
};
SDP.prototype.RtcpFbFromJingle = function (elem, payloadtype) { // XEP-0293
var media = '';
var tmp = elem.find('>rtcp-fb-trr-int[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
if (tmp.length) {
media += 'a=rtcp-fb:' + '*' + ' ' + 'trr-int' + ' ';
if (tmp.attr('value')) {
media += tmp.attr('value');
} else {
media += '0';
}
media += '\r\n';
}
tmp = elem.find('>rtcp-fb[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
tmp.each(function () {
media += 'a=rtcp-fb:' + payloadtype + ' ' + $(this).attr('type');
if ($(this).attr('subtype')) {
media += ' ' + $(this).attr('subtype');
}
media += '\r\n';
});
return media;
};
// construct an SDP from a jingle stanza
SDP.prototype.fromJingle = function (jingle) {
var obj = this;
this.raw = 'v=0\r\n' +
'o=- ' + '1923518516' + ' 2 IN IP4 0.0.0.0\r\n' +// FIXME
's=-\r\n' +
't=0 0\r\n';
// http://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-04#section-8
if ($(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').length) {
try {
$(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').each(function (idx, group) {
var contents = $(group).find('>content').map(function (idx, content) {
return $(content).attr('name');
}).get();
if (contents.length > 0) {
obj.raw += 'a=group:' + ($(group).attr('semantics') || $(group).attr('type')) + ' ' + contents.join(' ') + '\r\n';
}
});
} catch (e) { console.error(e.toString()); }
} else if ($(jingle).find('>group[xmlns="urn:ietf:rfc:5888"]').length) {
// temporary namespace, not to be used. to be removed soon.
$(jingle).find('>group[xmlns="urn:ietf:rfc:5888"]').each(function (idx, group) {
var contents = $(group).find('>content').map(function (idx, content) {
return $(content).attr('name');
}).get();
if ($(group).attr('type') !== null && contents.length > 0) {
obj.raw += 'a=group:' + $(group).attr('type') + ' ' + contents.join(' ') + '\r\n';
}
});
} else {
// for backward compability, to be removed soon
// assume all contents are in the same bundle group, can be improved upon later
var bundle = $(jingle).find('>content').filter(function (idx, content) {
//elem.c('bundle', {xmlns:'http://estos.de/ns/bundle'});
return $(content).find('>bundle').length > 0;
}).map(function (idx, content) {
return $(content).attr('name');
}).get();
if (bundle.length) {
this.raw += 'a=group:BUNDLE ' + bundle.join(' ') + '\r\n';
}
}
this.session = this.raw;
jingle.find('>content').each(function () {
var m = obj.jingle2media($(this));
obj.media.push(m);
});
// reconstruct msid-semantic -- apparently not necessary
/*
var msid = SDPUtil.parse_ssrc(this.raw);
if (msid.hasOwnProperty('mslabel')) {
this.session += "a=msid-semantic: WMS " + msid.mslabel + "\r\n";
}
*/
this.raw = this.session + this.media.join('');
};
// translate a jingle content element into an an SDP media part
SDP.prototype.jingle2media = function (content) {
var media = '',
desc = content.find('description'),
ssrc = desc.attr('ssrc'),
self = this,
tmp;
tmp = { media: desc.attr('media') };
tmp.port = '1';
if (content.attr('senders') == 'rejected') {
// estos hack to reject an m-line.
tmp.port = '0';
}
if (content.find('>transport>fingerprint').length || desc.find('encryption').length) {
tmp.proto = 'RTP/SAVPF';
} else {
tmp.proto = 'RTP/AVPF';
}
tmp.fmt = desc.find('payload-type').map(function () { return $(this).attr('id'); }).get();
media += SDPUtil.build_mline(tmp) + '\r\n';
media += 'c=IN IP4 0.0.0.0\r\n';
media += 'a=rtcp:1 IN IP4 0.0.0.0\r\n';
tmp = content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
if (tmp.length) {
if (tmp.attr('ufrag')) {
media += SDPUtil.build_iceufrag(tmp.attr('ufrag')) + '\r\n';
}
if (tmp.attr('pwd')) {
media += SDPUtil.build_icepwd(tmp.attr('pwd')) + '\r\n';
}
tmp.find('>fingerprint').each(function () {
// FIXME: check namespace at some point
media += 'a=fingerprint:' + $(this).attr('hash');
media += ' ' + $(this).text();
media += '\r\n';
if ($(this).attr('setup')) {
media += 'a=setup:' + $(this).attr('setup') + '\r\n';
}
});
}
switch (content.attr('senders')) {
case 'initiator':
media += 'a=sendonly\r\n';
break;
case 'responder':
media += 'a=recvonly\r\n';
break;
case 'none':
media += 'a=inactive\r\n';
break;
case 'both':
media += 'a=sendrecv\r\n';
break;
}
media += 'a=mid:' + content.attr('name') + '\r\n';
// <description><rtcp-mux/></description>
// see http://code.google.com/p/libjingle/issues/detail?id=309 -- no spec though
// and http://mail.jabber.org/pipermail/jingle/2011-December/001761.html
if (desc.find('rtcp-mux').length) {
media += 'a=rtcp-mux\r\n';
}
if (desc.find('encryption').length) {
desc.find('encryption>crypto').each(function () {
media += 'a=crypto:' + $(this).attr('tag');
media += ' ' + $(this).attr('crypto-suite');
media += ' ' + $(this).attr('key-params');
if ($(this).attr('session-params')) {
media += ' ' + $(this).attr('session-params');
}
media += '\r\n';
});
}
desc.find('payload-type').each(function () {
media += SDPUtil.build_rtpmap(this) + '\r\n';
if ($(this).find('>parameter').length) {
media += 'a=fmtp:' + $(this).attr('id') + ' ';
media += $(this).find('parameter').map(function () { return ($(this).attr('name') ? ($(this).attr('name') + '=') : '') + $(this).attr('value'); }).get().join(';');
media += '\r\n';
}
// xep-0293
media += self.RtcpFbFromJingle($(this), $(this).attr('id'));
});
// xep-0293
media += self.RtcpFbFromJingle(desc, '*');
// xep-0294
tmp = desc.find('>rtp-hdrext[xmlns="urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"]');
tmp.each(function () {
media += 'a=extmap:' + $(this).attr('id') + ' ' + $(this).attr('uri') + '\r\n';
});
content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]>candidate').each(function () {
media += SDPUtil.candidateFromJingle(this);
});
tmp = content.find('description>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
tmp.each(function () {
var ssrc = $(this).attr('ssrc');
$(this).find('>parameter').each(function () {
media += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
if ($(this).attr('value') && $(this).attr('value').length)
media += ':' + $(this).attr('value');
media += '\r\n';
});
});
if (tmp.length === 0) {
// fallback to proprietary mapping of a=ssrc lines
tmp = content.find('description>ssrc[xmlns="http://estos.de/ns/ssrc"]');
if (tmp.length) {
media += 'a=ssrc:' + ssrc + ' cname:' + tmp.attr('cname') + '\r\n';
media += 'a=ssrc:' + ssrc + ' msid:' + tmp.attr('msid') + '\r\n';
media += 'a=ssrc:' + ssrc + ' mslabel:' + tmp.attr('mslabel') + '\r\n';
media += 'a=ssrc:' + ssrc + ' label:' + tmp.attr('label') + '\r\n';
}
}
return media;
};
SDPUtil = {
iceparams: function (mediadesc, sessiondesc) {
var data = null;
if (SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc) &&
SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc)) {
data = {
ufrag: SDPUtil.parse_iceufrag(SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc)),
pwd: SDPUtil.parse_icepwd(SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc))
};
}
return data;
},
parse_iceufrag: function (line) {
return line.substring(12);
},
build_iceufrag: function (frag) {
return 'a=ice-ufrag:' + frag;
},
parse_icepwd: function (line) {
return line.substring(10);
},
build_icepwd: function (pwd) {
return 'a=ice-pwd:' + pwd;
},
parse_mid: function (line) {
return line.substring(6);
},
parse_mline: function (line) {
var parts = line.substring(2).split(' '),
data = {};
data.media = parts.shift();
data.port = parts.shift();
data.proto = parts.shift();
if (parts[parts.length - 1] === '') { // trailing whitespace
parts.pop();
}
data.fmt = parts;
return data;
},
build_mline: function (mline) {
return 'm=' + mline.media + ' ' + mline.port + ' ' + mline.proto + ' ' + mline.fmt.join(' ');
},
parse_rtpmap: function (line) {
var parts = line.substring(9).split(' '),
data = {};
data.id = parts.shift();
parts = parts[0].split('/');
data.name = parts.shift();
data.clockrate = parts.shift();
data.channels = parts.length ? parts.shift() : '1';
return data;
},
build_rtpmap: function (el) {
var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
line += '/' + el.getAttribute('channels');
}
return line;
},
parse_crypto: function (line) {
var parts = line.substring(9).split(' '),
data = {};
data.tag = parts.shift();
data['crypto-suite'] = parts.shift();
data['key-params'] = parts.shift();
if (parts.length) {
data['session-params'] = parts.join(' ');
}
return data;
},
parse_fingerprint: function (line) { // RFC 4572
var parts = line.substring(14).split(' '),
data = {};
data.hash = parts.shift();
data.fingerprint = parts.shift();
// TODO assert that fingerprint satisfies 2UHEX *(":" 2UHEX) ?
return data;
},
parse_fmtp: function (line) {
var parts = line.split(' '),
i, key, value,
data = [];
parts.shift();
parts = parts.join(' ').split(';');
for (i = 0; i < parts.length; i++) {
key = parts[i].split('=')[0];
while (key.length && key[0] == ' ') {
key = key.substring(1);
}
value = parts[i].split('=')[1];
if (key && value) {
data.push({name: key, value: value});
} else if (key) {
// rfc 4733 (DTMF) style stuff
data.push({name: '', value: key});
}
}
return data;
},
parse_icecandidate: function (line) {
var candidate = {},
elems = line.split(' ');
candidate.foundation = elems[0].substring(12);
candidate.component = elems[1];
candidate.protocol = elems[2].toLowerCase();
candidate.priority = elems[3];
candidate.ip = elems[4];
candidate.port = elems[5];
// elems[6] => "typ"
candidate.type = elems[7];
candidate.generation = 0; // default value, may be overwritten below
for (var i = 8; i < elems.length; i += 2) {
switch (elems[i]) {
case 'raddr':
candidate['rel-addr'] = elems[i + 1];
break;
case 'rport':
candidate['rel-port'] = elems[i + 1];
break;
case 'generation':
candidate.generation = elems[i + 1];
break;
default: // TODO
console.log('parse_icecandidate not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
}
}
candidate.network = '1';
candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
return candidate;
},
build_icecandidate: function (cand) {
var line = ['a=candidate:' + cand.foundation, cand.component, cand.protocol, cand.priority, cand.ip, cand.port, 'typ', cand.type].join(' ');
line += ' ';
switch (cand.type) {
case 'srflx':
case 'prflx':
case 'relay':
if (cand.hasOwnAttribute('rel-addr') && cand.hasOwnAttribute('rel-port')) {
line += 'raddr';
line += ' ';
line += cand['rel-addr'];
line += ' ';
line += 'rport';
line += ' ';
line += cand['rel-port'];
line += ' ';
}
break;
}
line += 'generation';
line += ' ';
line += cand.hasOwnAttribute('generation') ? cand.generation : '0';
return line;
},
parse_ssrc: function (desc) {
// proprietary mapping of a=ssrc lines
// TODO: see "Jingle RTP Source Description" by Juberti and P. Thatcher on google docs
// and parse according to that
var lines = desc.split('\r\n'),
data = {};
for (var i = 0; i < lines.length; i++) {
if (lines[i].substring(0, 7) == 'a=ssrc:') {
var idx = lines[i].indexOf(' ');
data[lines[i].substr(idx + 1).split(':', 2)[0]] = lines[i].substr(idx + 1).split(':', 2)[1];
}
}
return data;
},
parse_rtcpfb: function (line) {
var parts = line.substr(10).split(' ');
var data = {};
data.pt = parts.shift();
data.type = parts.shift();
data.params = parts;
return data;
},
parse_extmap: function (line) {
var parts = line.substr(9).split(' ');
var data = {};
data.value = parts.shift();
if (data.value.indexOf('/') != -1) {
data.direction = data.value.substr(data.value.indexOf('/') + 1);
data.value = data.value.substr(0, data.value.indexOf('/'));
} else {
data.direction = 'both';
}
data.uri = parts.shift();
data.params = parts;
return data;
},
find_line: function (haystack, needle, sessionpart) {
var lines = haystack.split('\r\n');
for (var i = 0; i < lines.length; i++) {
if (lines[i].substring(0, needle.length) == needle) {
return lines[i];
}
}
if (!sessionpart) {
return false;
}
// search session part
lines = sessionpart.split('\r\n');
for (var j = 0; j < lines.length; j++) {
if (lines[j].substring(0, needle.length) == needle) {
return lines[j];
}
}
return false;
},
find_lines: function (haystack, needle, sessionpart) {
var lines = haystack.split('\r\n'),
needles = [];
for (var i = 0; i < lines.length; i++) {
if (lines[i].substring(0, needle.length) == needle)
needles.push(lines[i]);
}
if (needles.length || !sessionpart) {
return needles;
}
// search session part
lines = sessionpart.split('\r\n');
for (var j = 0; j < lines.length; j++) {
if (lines[j].substring(0, needle.length) == needle) {
needles.push(lines[j]);
}
}
return needles;
},
candidateToJingle: function (line) {
// a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
// <candidate component=... foundation=... generation=... id=... ip=... network=... port=... priority=... protocol=... type=.../>
if (line.substring(0, 12) != 'a=candidate:') {
console.log('parseCandidate called with a line that is not a candidate line');
console.log(line);
return null;
}
if (line.substring(line.length - 2) == '\r\n') // chomp it
line = line.substring(0, line.length - 2);
var candidate = {},
elems = line.split(' '),
i;
if (elems[6] != 'typ') {
console.log('did not find typ in the right place');
console.log(line);
return null;
}
candidate.foundation = elems[0].substring(12);
candidate.component = elems[1];
candidate.protocol = elems[2].toLowerCase();
candidate.priority = elems[3];
candidate.ip = elems[4];
candidate.port = elems[5];
// elems[6] => "typ"
candidate.type = elems[7];
for (i = 8; i < elems.length; i += 2) {
switch (elems[i]) {
case 'raddr':
candidate['rel-addr'] = elems[i + 1];
break;
case 'rport':
candidate['rel-port'] = elems[i + 1];
break;
case 'generation':
candidate.generation = elems[i + 1];
break;
default: // TODO
console.log('not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
}
}
candidate.network = '1';
candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
return candidate;
},
candidateFromJingle: function (cand) {
var line = 'a=candidate:';
line += cand.getAttribute('foundation');
line += ' ';
line += cand.getAttribute('component');
line += ' ';
line += cand.getAttribute('protocol'); //.toUpperCase(); // chrome M23 doesn't like this
line += ' ';
line += cand.getAttribute('priority');
line += ' ';
line += cand.getAttribute('ip');
line += ' ';
line += cand.getAttribute('port');
line += ' ';
line += 'typ';
line += ' ' + cand.getAttribute('type');
line += ' ';
switch (cand.getAttribute('type')) {
case 'srflx':
case 'prflx':
case 'relay':
if (cand.getAttribute('rel-addr') && cand.getAttribute('rel-port')) {
line += 'raddr';
line += ' ';
line += cand.getAttribute('rel-addr');
line += ' ';
line += 'rport';
line += ' ';
line += cand.getAttribute('rel-port');
line += ' ';
}
break;
}
line += 'generation';
line += ' ';
line += cand.getAttribute('generation') || '0';
return line + '\r\n';
}
};
\ No newline at end of file
/* colibri.js -- a COLIBRI focus
* The colibri spec has been submitted to the XMPP Standards Foundation
* for publications as a XMPP extensions:
* http://xmpp.org/extensions/inbox/colibri.html
*
* colibri.js is a participating focus, i.e. the focus participates
* in the conference. The conference itself can be ad-hoc, through a
* MUC, through PubSub, etc.
*
* colibri.js relies heavily on the strophe.jingle library available
* from https://github.com/ESTOS/strophe.jingle
* and interoperates with the Jitsi videobridge available from
* https://jitsi.org/Projects/JitsiVideobridge
*/
/*
Copyright (c) 2013 ESTOS GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
/* jshint -W117 */
function ColibriFocus(connection, bridgejid) {
this.connection = connection;
this.bridgejid = bridgejid;
this.peers = [];
this.confid = null;
this.peerconnection = null;
this.sid = Math.random().toString(36).substr(2, 12);
this.connection.jingle.sessions[this.sid] = this;
this.mychannel = [];
this.channels = [];
this.remotessrc = {};
// ssrc lines to be added on next update
this.addssrc = [];
// ssrc lines to be removed on next update
this.removessrc = [];
// silly wait flag
this.wait = true;
}
// creates a conferences with an initial set of peers
ColibriFocus.prototype.makeConference = function (peers) {
var ob = this;
if (this.confid !== null) {
console.error('makeConference called twice? Ignoring...');
// FIXME: just invite peers?
return;
}
this.confid = 0; // !null
this.peers = [];
peers.forEach(function (peer) {
ob.peers.push(peer);
ob.channels.push([]);
});
this.peerconnection = new RTC.peerconnection(this.connection.jingle.ice_config, this.connection.jingle.pc_constraints);
this.peerconnection.addStream(this.connection.jingle.localStream);
this.peerconnection.oniceconnectionstatechange = function (event) {
console.warn('ice connection state changed to', ob.peerconnection.iceConnectionState);
/*
if (ob.peerconnection.signalingState == 'stable' && ob.peerconnection.iceConnectionState == 'connected') {
console.log('adding new remote SSRCs from iceconnectionstatechange');
window.setTimeout(function() { ob.modifySources(); }, 1000);
}
*/
};
this.peerconnection.onsignalingstatechange = function (event) {
console.warn(ob.peerconnection.signalingState);
/*
if (ob.peerconnection.signalingState == 'stable' && ob.peerconnection.iceConnectionState == 'connected') {
console.log('adding new remote SSRCs from signalingstatechange');
window.setTimeout(function() { ob.modifySources(); }, 1000);
}
*/
};
this.peerconnection.onaddstream = function (event) {
ob.remoteStream = event.stream;
$(document).trigger('remotestreamadded.jingle', [event, ob.sid]);
};
this.peerconnection.onicecandidate = function (event) {
ob.sendIceCandidate(event.candidate);
};
this.peerconnection.createOffer(
function (offer) {
ob.peerconnection.setLocalDescription(
offer,
function () {
// success
// FIXME: could call _makeConference here and trickle candidates later
},
function (error) {
console.log('setLocalDescription failed', error);
}
);
},
function (error) {
console.warn(error);
}
);
this.peerconnection.onicecandidate = function (event) {
console.log('candidate', event.candidate);
if (!event.candidate) {
console.log('end of candidates');
ob._makeConference();
return;
}
};
};
ColibriFocus.prototype._makeConference = function () {
var ob = this;
var elem = $iq({to: this.bridgejid, type: 'get'});
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'});
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
var contents = SDPUtil.find_lines(localSDP.raw, 'a=mid:').map(SDPUtil.parse_mid);
localSDP.media.forEach(function (media, channel) {
var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
elem.c('content', {name: name});
elem.c('channel', {initiator: 'false', expire: '15'});
// FIXME: should reuse code from .toJingle
var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
for (var j = 0; j < mline.fmt.length; j++) {
var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]);
elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
elem.up();
}
// FIXME: should reuse code from .toJingle
elem.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
var tmp = SDPUtil.iceparams(media, localSDP.session);
if (tmp) {
elem.attrs(tmp);
var fingerprints = SDPUtil.find_lines(media, 'a=fingerprint:', localSDP.session);
fingerprints.forEach(function (line) {
tmp = SDPUtil.parse_fingerprint(line);
//tmp.xmlns = 'urn:xmpp:tmp:jingle:apps:dtls:0';
tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0';
elem.c('fingerprint').t(tmp.fingerprint);
delete tmp.fingerprint;
line = SDPUtil.find_line(media, 'a=setup:', ob.session);
if (line) {
tmp.setup = line.substr(8);
}
elem.attrs(tmp);
elem.up();
});
// XEP-0176
if (SDPUtil.find_line(media, 'a=candidate:', localSDP.session)) { // add any a=candidate lines
lines = SDPUtil.find_lines(media, 'a=candidate:', localSDP.session);
for (j = 0; j < lines.length; j++) {
tmp = SDPUtil.candidateToJingle(lines[j]);
elem.c('candidate', tmp).up();
}
}
elem.up(); // end of transport
}
elem.up(); // end of channel
for (j = 0; j < ob.peers.length; j++) {
elem.c('channel', {initiator: 'true', expire:'15' }).up();
}
elem.up(); // end of content
});
this.connection.sendIQ(elem,
function (result) {
ob.createdConference(result);
},
function (error) {
console.warn(error);
}
);
};
// callback when a conference was created
ColibriFocus.prototype.createdConference = function (result) {
console.log('created a conference on the bridge');
var tmp;
this.confid = $(result).find('>conference').attr('id');
var remotecontents = $(result).find('>conference>content').get();
for (var i = 0; i < remotecontents.length; i++) {
tmp = $(remotecontents[i]).find('>channel').get();
this.mychannel.push($(tmp.shift()));
for (j = 0; j < tmp.length; j++) {
if (this.channels[j] === undefined) {
this.channels[j] = [];
}
this.channels[j].push(tmp[j]);
}
}
console.log('remote channels', this.channels);
// establish our channel with the bridge
// static answer taken from chrome M31, should be replaced by a
// dynamic one that is based on our offer FIXME
var bridgeSDP = new SDP('v=0\r\no=- 5151055458874951233 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\nm=audio 1 RTP/SAVPF 111 103 104 0 8 106 105 13 126\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=mid:audio\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=sendrecv\r\na=rtpmap:111 opus/48000/2\r\na=fmtp:111 minptime=10\r\na=rtpmap:103 ISAC/16000\r\na=rtpmap:104 ISAC/32000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:106 CN/32000\r\na=rtpmap:105 CN/16000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:126 telephone-event/8000\r\na=maxptime:60\r\nm=video 1 RTP/SAVPF 100 116 117\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=mid:video\r\na=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=sendrecv\r\na=rtpmap:100 VP8/90000\r\na=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 goog-remb\r\na=rtpmap:116 red/90000\r\na=rtpmap:117 ulpfec/90000\r\n');
// get the mixed ssrc
for (var channel = 0; channel < remotecontents.length; channel++) {
tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
// FIXME: check rtp-level-relay-type
if (tmp.length) {
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabela0 mixedlabela0' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabela0' + '\r\n';
} else {
// make chrome happy... '3735928559' == 0xDEADBEEF
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'msid:mixedmslabelv0 mixedlabelv0' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'mslabel:mixedmslabelv0' + '\r\n';
}
// FIXME: should take code from .fromJingle
tmp = $(this.mychannel[channel]).find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
if (tmp.length) {
bridgeSDP.media[channel] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
bridgeSDP.media[channel] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
tmp.find('>candidate').each(function () {
bridgeSDP.media[channel] += SDPUtil.candidateFromJingle(this);
});
tmp = tmp.find('>fingerprint');
if (tmp.length) {
bridgeSDP.media[channel] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
if (tmp.attr('setup')) {
bridgeSDP.media[channel] += 'a=setup:' + tmp.attr('setup') + '\r\n';
}
}
}
}
bridgeSDP.raw = bridgeSDP.session + bridgeSDP.media.join('');
var ob = this;
this.peerconnection.setRemoteDescription(
new RTCSessionDescription({type: 'answer', sdp: bridgeSDP.raw}),
function () {
console.log('setRemoteDescription success');
// remote channels == remotecontents length - 1!
for (var i = 0; i < remotecontents.length - 1; i++) {
ob.initiate(ob.peers[i], true);
}
},
function (error) {
console.log('setRemoteDescription failed');
}
);
};
// send a session-initiate to a new participant
ColibriFocus.prototype.initiate = function (peer, isInitiator) {
var participant = this.peers.indexOf(peer);
console.log('tell', peer, participant);
var sdp;
if (this.peerconnection != null && this.peerconnection.signalingState == 'stable') {
sdp = new SDP(this.peerconnection.remoteDescription.sdp);
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
// throw away stuff we don't want
// not needed with static offer
sdp.removeSessionLines('a=group:');
sdp.removeSessionLines('a=msid-semantic:'); // FIXME: not mapped over jingle anyway...
for (var i = 0; i < sdp.media.length; i++) {
sdp.removeMediaLines(i, 'a=rtcp-mux');
sdp.removeMediaLines(i, 'a=ssrc:');
sdp.removeMediaLines(i, 'a=crypto:');
sdp.removeMediaLines(i, 'a=candidate:');
sdp.removeMediaLines(i, 'a=ice-options:google-ice');
sdp.removeMediaLines(i, 'a=ice-ufrag:');
sdp.removeMediaLines(i, 'a=ice-pwd:');
sdp.removeMediaLines(i, 'a=fingerprint:');
sdp.removeMediaLines(i, 'a=setup:');
// re-add all remote a=ssrcs
for (var jid in this.remotessrc) {
if (jid == peer) continue;
sdp.media[i] += this.remotessrc[jid][i];
}
// and local a=ssrc lines
sdp.media[i] += SDPUtil.find_lines(localSDP.media[i], 'a=ssrc').join('\r\n') + '\r\n';
}
sdp.raw = sdp.session + sdp.media.join('');
} else {
console.error('can not initiate a new session without a stable peerconnection');
return;
}
// add stuff we got from the bridge
for (var j = 0; j < sdp.media.length; j++) {
var chan = $(this.channels[participant][j]);
console.log('channel id', chan.attr('id'));
tmp = chan.find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
if (tmp.length) {
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' + '\r\n';
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabela0 mixedlabela0' + '\r\n';
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabela0' + '\r\n';
} else {
// make chrome happy... '3735928559' == 0xDEADBEEF
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n';
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'msid:mixedmslabelv0 mixedlabelv0' + '\r\n';
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'mslabel:mixedmslabelv0' + '\r\n';
}
tmp = chan.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
if (tmp.length) {
if (tmp.attr('ufrag'))
sdp.media[j] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
if (tmp.attr('pwd'))
sdp.media[j] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
// and the candidates...
tmp.find('>candidate').each(function () {
sdp.media[j] += SDPUtil.candidateFromJingle(this);
});
tmp = tmp.find('>fingerprint');
if (tmp.length) {
sdp.media[j] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
/*
if (tmp.attr('direction')) {
sdp.media[j] += 'a=setup:' + tmp.attr('direction') + '\r\n';
}
*/
sdp.media[j] += 'a=setup:actpass\r\n';
}
}
}
// make a new colibri session and configure it
// FIXME: is it correct to use this.connection.jid when used in a MUC?
var sess = new ColibriSession(this.connection.jid,
Math.random().toString(36).substr(2, 12), // random string
this.connection);
sess.initiate(peer);
sess.colibri = this;
sess.localStream = this.connection.jingle.localStream;
sess.media_constraints = this.connection.jingle.media_constraints;
sess.pc_constraints = this.connection.jingle.pc_constraints;
sess.ice_config = this.connection.ice_config;
this.connection.jingle.sessions[sess.sid] = sess;
this.connection.jingle.jid2session[sess.peerjid] = sess;
// send a session-initiate
var init = $iq({to: peer, type: 'set'})
.c('jingle',
{xmlns: 'urn:xmpp:jingle:1',
action: 'session-initiate',
initiator: sess.me,
sid: sess.sid
}
);
sdp.toJingle(init, 'initiator');
this.connection.sendIQ(init,
function (res) {
console.log('got result');
},
function (err) {
console.log('got error');
}
);
};
// pull in a new participant into the conference
ColibriFocus.prototype.addNewParticipant = function (peer) {
var ob = this;
if (this.confid === 0) {
// bad state
console.log('confid does not exist yet, postponing', peer);
window.setTimeout(function () {
ob.addNewParticipant(peer);
}, 250);
return;
}
var index = this.channels.length;
this.channels.push([]);
this.peers.push(peer);
var elem = $iq({to: this.bridgejid, type: 'get'});
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
var contents = SDPUtil.find_lines(localSDP.raw, 'a=mid:').map(SDPUtil.parse_mid);
contents.forEach(function (name) {
elem.c('content', {name: name});
elem.c('channel', {initiator: 'true', expire:'15'});
elem.up(); // end of channel
elem.up(); // end of content
});
this.connection.sendIQ(elem,
function (result) {
var contents = $(result).find('>conference>content').get();
for (var i = 0; i < contents.length; i++) {
tmp = $(contents[i]).find('>channel').get();
ob.channels[index][i] = tmp[0];
}
ob.initiate(peer, true);
},
function (error) {
console.warn(error);
}
);
};
// update the channel description (payload-types + dtls fp) for a participant
ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
console.log('change allocation for', this.confid);
var change = $iq({to: this.bridgejid, type: 'set'});
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
for (channel = 0; channel < this.channels[participant].length; channel++) {
change.c('content', {name: channel === 0 ? 'audio' : 'video'});
change.c('channel', {id: $(this.channels[participant][channel]).attr('id')});
var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
rtpmap.forEach(function (val) {
// TODO: too much copy-paste
var rtpmap = SDPUtil.parse_rtpmap(val);
change.c('payload-type', rtpmap);
//
// put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
/*
if (SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id)) {
tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id));
for (var k = 0; k < tmp.length; k++) {
change.c('parameter', tmp[k]).up();
}
}
*/
change.up();
});
// now add transport
change.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
var fingerprints = SDPUtil.find_lines(remoteSDP.media[channel], 'a=fingerprint:', remoteSDP.session);
fingerprints.forEach(function (line) {
tmp = SDPUtil.parse_fingerprint(line);
tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0';
change.c('fingerprint').t(tmp.fingerprint);
delete tmp.fingerprint;
line = SDPUtil.find_line(remoteSDP.media[channel], 'a=setup:', remoteSDP.session);
if (line) {
tmp.setup = line.substr(8);
}
change.attrs(tmp);
change.up();
});
var candidates = SDPUtil.find_lines(remoteSDP.media[channel], 'a=candidate:', remoteSDP.session);
candidates.forEach(function (line) {
var tmp = SDPUtil.candidateToJingle(line);
change.c('candidate', tmp).up();
});
tmp = SDPUtil.iceparams(remoteSDP.media[channel], remoteSDP.session);
if (tmp) {
change.attrs(tmp);
}
change.up(); // end of transport
change.up(); // end of channel
change.up(); // end of content
}
this.connection.sendIQ(change,
function (res) {
console.log('got result');
},
function (err) {
console.log('got error');
}
);
};
// tell everyone about a new participants a=ssrc lines (isadd is true)
// or a leaving participants a=ssrc lines
// FIXME: should not take an SDP, but rather the a=ssrc lines and probably a=mid
ColibriFocus.prototype.sendSSRCUpdate = function (sdp, exclude, isadd) {
var ob = this;
this.peers.forEach(function (peerjid) {
if (peerjid == exclude) return;
console.log('tell', peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from', exclude);
if (!ob.remotessrc[peerjid]) {
// FIXME: this should only send to participants that are stable, i.e. who have sent a session-accept
// possibly, this.remoteSSRC[session.peerjid] does not exist yet
console.warn('do we really want to bother', peerjid, 'with updates yet?');
}
var channel;
var peersess = ob.connection.jingle.jid2session[peerjid];
var modify = $iq({to: peerjid, type: 'set'})
.c('jingle', {
xmlns: 'urn:xmpp:jingle:1',
action: isadd ? 'addsource' : 'removesource',
initiator: peersess.initiator,
sid: peersess.sid
}
);
for (channel = 0; channel < sdp.media.length; channel++) {
tmp = SDPUtil.find_lines(sdp.media[channel], 'a=ssrc:');
modify.c('content', {name: SDPUtil.parse_mid(SDPUtil.find_line(sdp.media[channel], 'a=mid:'))});
modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
// FIXME: not completly sure this operates on blocks and / or handles different ssrcs correctly
tmp.forEach(function (line) {
var idx = line.indexOf(' ');
var linessrc = line.substr(0, idx).substr(7);
modify.attrs({ssrc: linessrc});
var kv = line.substr(idx + 1);
modify.c('parameter');
if (kv.indexOf(':') == -1) {
modify.attrs({ name: kv });
} else {
modify.attrs({ name: kv.split(':', 2)[0] });
modify.attrs({ value: kv.split(':', 2)[1] });
}
modify.up();
});
modify.up(); // end of source
modify.up(); // end of content
}
ob.connection.sendIQ(modify,
function (res) {
console.warn('got modify result');
},
function (err) {
console.warn('got modify error');
}
);
});
};
ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype) {
var participant = this.peers.indexOf(session.peerjid);
console.log('Colibri.setRemoteDescription from', session.peerjid, participant);
var ob = this;
var remoteSDP = new SDP('');
var tmp;
var channel;
remoteSDP.fromJingle(elem);
// ACT 1: change allocation on bridge
this.updateChannel(remoteSDP, participant);
// ACT 2: tell anyone else about the new SSRCs
this.sendSSRCUpdate(remoteSDP, session.peerjid, true);
// ACT 3: note the SSRCs
this.remotessrc[session.peerjid] = [];
for (channel = 0; channel < this.channels[participant].length; channel++) {
this.remotessrc[session.peerjid][channel] = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n';
}
// ACT 4: add new a=ssrc lines to local remotedescription
for (channel = 0; channel < this.channels[participant].length; channel++) {
if (!this.addssrc[channel]) this.addssrc[channel] = '';
this.addssrc[channel] += SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n';
}
this.modifySources();
};
// relay ice candidates to bridge using trickle
ColibriFocus.prototype.addIceCandidate = function (session, elem) {
var ob = this;
var participant = this.peers.indexOf(session.peerjid);
console.log('change transport allocation for', this.confid, session.peerjid, participant);
var change = $iq({to: this.bridgejid, type: 'set'});
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
$(elem).each(function () {
var name = $(this).attr('name');
var channel = name == 'audio' ? 0 : 1; // FIXME: search mlineindex in localdesc
change.c('content', {name: name});
change.c('channel', {id: $(ob.channels[participant][channel]).attr('id')});
$(this).find('>transport').each(function () {
change.c('transport', {
ufrag: $(this).attr('ufrag'),
pwd: $(this).attr('pwd'),
xmlns: $(this).attr('xmlns')
});
$(this).find('>candidate').each(function () {
/* not yet
if (this.getAttribute('protocol') == 'tcp' && this.getAttribute('port') == 0) {
// chrome generates TCP candidates with port 0
return;
}
*/
var line = SDPUtil.candidateFromJingle(this);
change.c('candidate', SDPUtil.candidateToJingle(line)).up();
});
change.up(); // end of transport
});
change.up(); // end of channel
change.up(); // end of content
});
// FIXME: need to check if there is at least one candidate when filtering TCP ones
this.connection.sendIQ(change,
function (res) {
console.log('got result');
},
function (err) {
console.warn('got error');
}
);
};
// send our own candidate to the bridge
ColibriFocus.prototype.sendIceCandidate = function (candidate) {
//console.log('candidate', candidate);
if (!candidate) {
console.log('end of candidates');
return;
}
var mycands = $iq({to: this.bridgejid, type: 'set'});
mycands.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
mycands.c('content', {name: candidate.sdpMid });
mycands.c('channel', {id: $(this.mychannel[candidate.sdpMLineIndex]).attr('id')});
mycands.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
tmp = SDPUtil.candidateToJingle(candidate.candidate);
mycands.c('candidate', tmp).up();
this.connection.sendIQ(mycands,
function (res) {
console.log('got result');
},
function (err) {
console.warn('got error');
}
);
};
ColibriFocus.prototype.terminate = function (session, reason) {
console.log('remote session terminated from', session.peerjid);
var participant = this.peers.indexOf(session.peerjid);
if (!this.remotessrc[session.peerjid] || participant == -1) {
return;
}
console.log('remote ssrcs:', this.remotessrc[session.peerjid]);
var ssrcs = this.remotessrc[session.peerjid];
for (var i = 0; i < ssrcs.length; i++) {
if (!this.removessrc[i]) this.removessrc[i] = '';
this.removessrc[i] += ssrcs[i];
}
// remove from this.peers
this.peers.splice(participant, 1);
// expire channel on bridge
var change = $iq({to: this.bridgejid, type: 'set'});
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
for (var channel = 0; channel < this.channels[participant].length; channel++) {
change.c('content', {name: channel === 0 ? 'audio' : 'video'});
change.c('channel', {id: $(this.channels[participant][channel]).attr('id'), expire: '0'});
change.up(); // end of channel
change.up(); // end of content
}
this.connection.sendIQ(change,
function (res) {
console.log('got result');
},
function (err) {
console.log('got error');
}
);
// and remove from channels
this.channels.splice(participant, 1);
// tell everyone about the ssrcs to be removed
var sdp = new SDP('');
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
var contents = SDPUtil.find_lines(localSDP.raw, 'a=mid:').map(SDPUtil.parse_mid);
for (var j = 0; j < ssrcs.length; j++) {
sdp.media[j] = 'a=mid:' + contents[j] + '\r\n';
sdp.media[j] += ssrcs[j];
this.removessrc[j] += ssrcs[j];
}
this.sendSSRCUpdate(sdp, session.peerjid, false);
delete this.remotessrc[session.peerjid];
this.modifySources();
};
ColibriFocus.prototype.modifySources = function () {
var ob = this;
if (!(this.addssrc.length || this.removessrc.length)) return;
if (this.peerconnection.signalingState == 'closed') return;
// FIXME: this is a big hack
// https://code.google.com/p/webrtc/issues/detail?id=2688
if (!(this.peerconnection.iceConnectionState == 'connected')) {
console.warn('modifySources not yet', this.peerconnection.signalingState, this.peerconnection.iceConnectionState);
window.setTimeout(function () { ob.modifySources(); }, 250);
this.wait = true;
return;
}
if (this.wait) {
window.setTimeout(function () { ob.modifySources(); }, 2500);
this.wait = false;
return;
}
var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
// add sources
this.addssrc.forEach(function (lines, idx) {
sdp.media[idx] += lines;
});
this.addssrc = [];
// remove sources
this.removessrc.forEach(function (lines, idx) {
lines = lines.split('\r\n');
lines.pop(); // remove empty last element;
lines.forEach(function (line) {
sdp.media[idx] = sdp.media[idx].replace(line + '\r\n', '');
});
});
this.removessrc = [];
sdp.raw = sdp.session + sdp.media.join('');
this.peerconnection.setRemoteDescription(
new RTCSessionDescription({type: 'offer', sdp: sdp.raw }),
function () {
console.log('setModifiedRemoteDescription ok');
ob.peerconnection.createAnswer(
function (modifiedAnswer) {
console.log('modifiedAnswer created');
// FIXME: pushing down an answer while ice connection state
// is still checking is bad...
console.log(ob.peerconnection.iceConnectionState);
ob.peerconnection.setLocalDescription(modifiedAnswer,
function () {
console.log('setModifiedLocalDescription ok');
},
function (error) {
console.log('setModifiedLocalDescription failed');
}
);
},
function (error) {
console.log('createModifiedAnswer failed');
}
);
},
function (error) {
console.log('setModifiedRemoteDescription failed');
}
);
};
// A colibri session is similar to a jingle session, it just implements some things differently
// FIXME: inherit jinglesession, see https://github.com/legastero/Jingle-RTCPeerConnection/blob/master/index.js
function ColibriSession(me, sid, connection) {
this.me = me;
this.sid = sid;
this.connection = connection;
//this.peerconnection = null;
//this.mychannel = null;
//this.channels = null;
this.peerjid = null;
this.colibri = null;
}
// implementation of JingleSession interface
ColibriSession.prototype.initiate = function (peerjid, isInitiator) {
this.peerjid = peerjid;
};
ColibriSession.prototype.sendOffer = function (offer) {
console.log('ColibriSession.sendOffer');
};
ColibriSession.prototype.accept = function () {
console.log('ColibriSession.accept');
};
ColibriSession.prototype.terminate = function (reason) {
this.colibri.terminate(this, reason);
};
ColibriSession.prototype.active = function () {
console.log('ColibriSession.active');
};
ColibriSession.prototype.setRemoteDescription = function (elem, desctype) {
this.colibri.setRemoteDescription(this, elem, desctype);
};
ColibriSession.prototype.addIceCandidate = function (elem) {
this.colibri.addIceCandidate(this, elem);
};
ColibriSession.prototype.sendAnswer = function (sdp, provisional) {
console.log('ColibriSession.sendAnswer');
};
ColibriSession.prototype.sendTerminate = function (reason, text) {
console.log('ColibriSession.sendTerminate');
};
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment