Commit f2485476 authored by Dele Olajide's avatar Dele Olajide Committed by dele

Further work Jitsi videobridge. see changelog

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/trunk@13988 b35dd754-fafc-0310-a699-88a17e54d16e
parent f66df28d
...@@ -43,11 +43,20 @@ ...@@ -43,11 +43,20 @@
Jitsi Video Bridge Plugin Changelog Jitsi Video Bridge Plugin Changelog
</h1> </h1>
<p><b>1.3.0</b> -- Feb 19th, 2014</p> <p><b>1.3.0</b> -- March 13th, 2014</p>
<ul> <ul>
<li>Added username/password protection on web applications</li>
<li>OF-749: Upgraded bouncy castle libraries from 1.49 to 1.50</li> <li>OF-749: Upgraded bouncy castle libraries from 1.49 to 1.50</li>
<li>Jisti Videobridge plugin: Added latest jitmeet web conference application</li>
<li>Jisti Videobridge plugin: Implememted SIP registration and outgoing calls</li>
<li>Jisti Videobridge plugin: Implememted media recording</li>
<li>Ofmeet: Implememted feature to invite telphone user to conference</li>
</ul>
<p><b>1.2.2</b> -- Feb 18th, 2014</p>
<ul>
<li>Added username/password protection on web applications</li>
</ul> </ul>
<p><b>1.2.1</b> -- Feb 17th, 2014</p> <p><b>1.2.1</b> -- Feb 17th, 2014</p>
......
...@@ -19,11 +19,11 @@ function init() { ...@@ -19,11 +19,11 @@ function init() {
if (RTC === null) { if (RTC === null) {
window.location.href = 'webrtcrequired.html'; window.location.href = 'webrtcrequired.html';
return; return;
} else if (RTC.browser != 'chrome') { } else if (RTC.browser !== 'chrome') {
window.location.href = 'chromeonly.html'; window.location.href = 'chromeonly.html';
return; return;
} }
RTCPeerconnection = TraceablePeerConnection; RTCPeerconnection = TraceablePeerConnection;
connection = new Strophe.Connection(document.getElementById('boshURL').value || config.bosh || '/http-bind'); connection = new Strophe.Connection(document.getElementById('boshURL').value || config.bosh || '/http-bind');
...@@ -44,12 +44,12 @@ function init() { ...@@ -44,12 +44,12 @@ function init() {
var jid = document.getElementById('jid').value || config.hosts.domain || window.location.hostname; var jid = document.getElementById('jid').value || config.hosts.domain || window.location.hostname;
connection.connect(jid, document.getElementById('password').value, function (status) { connection.connect(jid, document.getElementById('password').value, function (status) {
if (status == Strophe.Status.CONNECTED) { if (status === Strophe.Status.CONNECTED) {
console.log('connected'); console.log('connected');
if (config.useStunTurn) { if (config.useStunTurn) {
connection.jingle.getStunAndTurnCredentials(); connection.jingle.getStunAndTurnCredentials();
} }
if (RTC.browser == 'firefox') { if (RTC.browser === 'firefox') {
getUserMediaWithConstraints(['audio']); getUserMediaWithConstraints(['audio']);
} else { } else {
getUserMediaWithConstraints(['audio', 'video'], config.resolution || '360'); getUserMediaWithConstraints(['audio', 'video'], config.resolution || '360');
...@@ -118,10 +118,10 @@ $(document).bind('mediaready.jingle', function (event, stream) { ...@@ -118,10 +118,10 @@ $(document).bind('mediaready.jingle', function (event, stream) {
updateLargeVideo($(this).attr('src'), true, 0); updateLargeVideo($(this).attr('src'), true, 0);
$('video').each(function (idx, el) { $('video').each(function (idx, el) {
if (el.id.indexOf('mixedmslabel') != -1) { if (el.id.indexOf('mixedmslabel') !== -1) {
el.volume = 0; el.volume = 0;
el.volume = 1; el.volume = 1;
} }
}); });
}); });
...@@ -131,11 +131,11 @@ $(document).bind('mediaready.jingle', function (event, stream) { ...@@ -131,11 +131,11 @@ $(document).bind('mediaready.jingle', function (event, stream) {
$(document).bind('mediafailure.jingle', function () { $(document).bind('mediafailure.jingle', function () {
// FIXME // FIXME
}); });
$(document).bind('remotestreamadded.jingle', function (event, data, sid) { $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
function waitForRemoteVideo(selector, sid) { function waitForRemoteVideo(selector, sid) {
var sess = connection.jingle.sessions[sid]; var sess = connection.jingle.sessions[sid];
if (data.stream.id == 'mixedmslabel') return; if (data.stream.id === 'mixedmslabel') return;
videoTracks = data.stream.getVideoTracks(); videoTracks = data.stream.getVideoTracks();
if (videoTracks.length === 0 || selector[0].currentTime > 0) { if (videoTracks.length === 0 || selector[0].currentTime > 0) {
RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF? RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF?
...@@ -148,10 +148,10 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) { ...@@ -148,10 +148,10 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
var sess = connection.jingle.sessions[sid]; var sess = connection.jingle.sessions[sid];
// look up an associated JID for a stream id // look up an associated JID for a stream id
if (data.stream.id.indexOf('mixedmslabel') == -1) { if (data.stream.id.indexOf('mixedmslabel') === -1) {
var ssrclines = SDPUtil.find_lines(sess.peerconnection.remoteDescription.sdp, 'a=ssrc'); var ssrclines = SDPUtil.find_lines(sess.peerconnection.remoteDescription.sdp, 'a=ssrc');
ssrclines = ssrclines.filter(function (line) { ssrclines = ssrclines.filter(function (line) {
return line.indexOf('mslabel:' + data.stream.label) != -1; return line.indexOf('mslabel:' + data.stream.label) !== -1;
}); });
if (ssrclines.length) { if (ssrclines.length) {
thessrc = ssrclines[0].substring(7).split(' ')[0]; thessrc = ssrclines[0].substring(7).split(' ')[0];
...@@ -179,7 +179,7 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) { ...@@ -179,7 +179,7 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
//console.log('found container for', data.peerjid); //console.log('found container for', data.peerjid);
} }
} else { } else {
if (data.stream.id != 'mixedmslabel') { if (data.stream.id !== 'mixedmslabel') {
console.warn('can not associate stream', data.stream.id, 'with a participant'); console.warn('can not associate stream', data.stream.id, 'with a participant');
} }
// FIXME: for the mixed ms we dont need a video -- currently // FIXME: for the mixed ms we dont need a video -- currently
...@@ -195,7 +195,7 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) { ...@@ -195,7 +195,7 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
vid.oncontextmenu = function () { return false; }; vid.oncontextmenu = function () { return false; };
container.appendChild(vid); container.appendChild(vid);
// TODO: make mixedstream display:none via css? // TODO: make mixedstream display:none via css?
if (id.indexOf('mixedmslabel') != -1) { if (id.indexOf('mixedmslabel') !== -1) {
container.id = 'mixedstream'; container.id = 'mixedstream';
$(container).hide(); $(container).hide();
} }
...@@ -232,8 +232,8 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) { ...@@ -232,8 +232,8 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
} }
); );
// an attempt to work around https://github.com/jitsi/jitmeet/issues/32 // an attempt to work around https://github.com/jitsi/jitmeet/issues/32
if (data.peerjid && sess.peerjid == data.peerjid && if (data.peerjid && sess.peerjid === data.peerjid &&
data.stream.getVideoTracks().length == 0 && data.stream.getVideoTracks().length === 0 &&
connection.jingle.localStream.getVideoTracks().length > 0) { connection.jingle.localStream.getVideoTracks().length > 0) {
window.setTimeout(function() { window.setTimeout(function() {
sendKeyframe(sess.peerconnection); sendKeyframe(sess.peerconnection);
...@@ -244,7 +244,7 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) { ...@@ -244,7 +244,7 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
// an attempt to work around https://github.com/jitsi/jitmeet/issues/32 // an attempt to work around https://github.com/jitsi/jitmeet/issues/32
function sendKeyframe(pc) { function sendKeyframe(pc) {
console.log('sendkeyframe', pc.iceConnectionState); console.log('sendkeyframe', pc.iceConnectionState);
if (pc.iceConnectionState != 'connected') return; // safe... if (pc.iceConnectionState !== 'connected') return; // safe...
pc.setRemoteDescription( pc.setRemoteDescription(
pc.remoteDescription, pc.remoteDescription,
function () { function () {
...@@ -350,7 +350,7 @@ $(document).bind('callincoming.jingle', function (event, sid) { ...@@ -350,7 +350,7 @@ $(document).bind('callincoming.jingle', function (event, sid) {
}); });
$(document).bind('callactive.jingle', function (event, videoelem, sid) { $(document).bind('callactive.jingle', function (event, videoelem, sid) {
if (videoelem.attr('id').indexOf('mixedmslabel') == -1) { if (videoelem.attr('id').indexOf('mixedmslabel') === -1) {
// ignore mixedmslabela0 and v0 // ignore mixedmslabela0 and v0
videoelem.show(); videoelem.show();
resizeThumbnails(); resizeThumbnails();
...@@ -464,18 +464,18 @@ $(document).bind('left.muc', function (event, jid) { ...@@ -464,18 +464,18 @@ $(document).bind('left.muc', function (event, jid) {
$(container).hide(); $(container).hide();
resizeThumbnails(); resizeThumbnails();
} }
if (focus === null && connection.emuc.myroomjid == connection.emuc.list_members[0]) { if (focus === null && connection.emuc.myroomjid === connection.emuc.list_members[0]) {
console.log('welcome to our new focus... myself'); console.log('welcome to our new focus... myself');
focus = new ColibriFocus(connection, config.hosts.bridge); focus = new ColibriFocus(connection, config.hosts.bridge);
if (Object.keys(connection.emuc.members).length > 0) { if (Object.keys(connection.emuc.members).length > 0) {
focus.makeConference(Object.keys(connection.emuc.members)); focus.makeConference(Object.keys(connection.emuc.members));
} }
$(document).trigger('focusechanged.muc', [focus]); $(document).trigger('focusechanged.muc', [focus]);
} }
else if (focus && Object.keys(connection.emuc.members).length === 0) { else if (focus && Object.keys(connection.emuc.members).length === 0) {
console.log('everyone left'); console.log('everyone left');
if (focus !== null) { if (focus !== null) {
// FIXME: closing the connection is a hack to avoid some // FIXME: closing the connection is a hack to avoid some
// problemswith reinit // problemswith reinit
if (focus.peerconnection !== null) { if (focus.peerconnection !== null) {
focus.peerconnection.close(); focus.peerconnection.close();
...@@ -494,11 +494,11 @@ $(document).bind('presence.muc', function (event, jid, info, pres) { ...@@ -494,11 +494,11 @@ $(document).bind('presence.muc', function (event, jid, info, pres) {
ssrc2jid[ssrc.getAttribute('ssrc')] = jid; ssrc2jid[ssrc.getAttribute('ssrc')] = jid;
// might need to update the direction if participant just went from sendrecv to recvonly // might need to update the direction if participant just went from sendrecv to recvonly
if (ssrc.getAttribute('type') == 'video') { if (ssrc.getAttribute('type') === 'video') {
var el = $('#participant_' + Strophe.getResourceFromJid(jid) + '>video'); var el = $('#participant_' + Strophe.getResourceFromJid(jid) + '>video');
switch(ssrc.getAttribute('direction')) { switch(ssrc.getAttribute('direction')) {
case 'sendrecv': case 'sendrecv':
el.show(); el.show();
break; break;
case 'recvonly': case 'recvonly':
el.hide(); el.hide();
...@@ -532,7 +532,7 @@ $(document).bind('passwordrequired.muc', function (event, jid) { ...@@ -532,7 +532,7 @@ $(document).bind('passwordrequired.muc', function (event, jid) {
{ {
var lockKey = document.getElementById('lockKey'); var lockKey = document.getElementById('lockKey');
if (lockKey.value != null) if (lockKey.value !== null)
{ {
setSharedKey(lockKey.value); setSharedKey(lockKey.value);
connection.emuc.doJoin(jid, lockKey.value); connection.emuc.doJoin(jid, lockKey.value);
...@@ -551,7 +551,7 @@ $(document).bind('presentationremoved.muc', function(event, jid, presUrl) { ...@@ -551,7 +551,7 @@ $(document).bind('presentationremoved.muc', function(event, jid, presUrl) {
setPresentationVisible(false); setPresentationVisible(false);
$('#participant_' + Strophe.getResourceFromJid(jid) + '_' + presId).remove(); $('#participant_' + Strophe.getResourceFromJid(jid) + '_' + presId).remove();
$('#presentation>iframe').remove(); $('#presentation>iframe').remove();
if (preziPlayer != null) { if (preziPlayer !== null) {
preziPlayer.destroy(); preziPlayer.destroy();
preziPlayer = null; preziPlayer = null;
} }
...@@ -585,8 +585,8 @@ $(document).bind('presentationadded.muc', function (event, jid, presUrl, current ...@@ -585,8 +585,8 @@ $(document).bind('presentationadded.muc', function (event, jid, presUrl, current
else { else {
var e = event.toElement || event.relatedTarget; var e = event.toElement || event.relatedTarget;
while(e && e.parentNode && e.parentNode != window) { while(e && e.parentNode && e.parentNode !== window) {
if (e.parentNode == this || e == this) { if (e.parentNode === this || e === this) {
return false; return false;
} }
e = e.parentNode; e = e.parentNode;
...@@ -608,8 +608,8 @@ $(document).bind('presentationadded.muc', function (event, jid, presUrl, current ...@@ -608,8 +608,8 @@ $(document).bind('presentationadded.muc', function (event, jid, presUrl, current
preziPlayer.on(PreziPlayer.EVENT_STATUS, function(event) { preziPlayer.on(PreziPlayer.EVENT_STATUS, function(event) {
console.log("prezi status", event.value); console.log("prezi status", event.value);
if (event.value == PreziPlayer.STATUS_CONTENT_READY) { if (event.value === PreziPlayer.STATUS_CONTENT_READY) {
if (jid != connection.emuc.myroomjid) if (jid !== connection.emuc.myroomjid)
preziPlayer.flyToStep(currentSlide); preziPlayer.flyToStep(currentSlide);
} }
}); });
...@@ -680,8 +680,8 @@ function setPresentationVisible(visible) { ...@@ -680,8 +680,8 @@ function setPresentationVisible(visible) {
} }
} }
var isPresentationVisible = function () { function isPresentationVisible() {
return ($('#presentation>iframe') != null && $('#presentation>iframe').css('opacity') == 1); return ($('#presentation>iframe') !== null && $('#presentation>iframe').css('opacity') == 1);
} }
/** /**
...@@ -692,7 +692,7 @@ function updateLargeVideo(newSrc, localVideo, vol) { ...@@ -692,7 +692,7 @@ function updateLargeVideo(newSrc, localVideo, vol) {
setPresentationVisible(false); setPresentationVisible(false);
if ($('#largeVideo').attr('src') != newSrc) { if ($('#largeVideo').attr('src') !== newSrc) {
document.getElementById('largeVideo').volume = vol; document.getElementById('largeVideo').volume = vol;
...@@ -700,10 +700,10 @@ function updateLargeVideo(newSrc, localVideo, vol) { ...@@ -700,10 +700,10 @@ function updateLargeVideo(newSrc, localVideo, vol) {
$(this).attr('src', newSrc); $(this).attr('src', newSrc);
var videoTransform = document.getElementById('largeVideo').style.webkitTransform; var videoTransform = document.getElementById('largeVideo').style.webkitTransform;
if (localVideo && videoTransform != 'scaleX(-1)') { if (localVideo && videoTransform !== 'scaleX(-1)') {
document.getElementById('largeVideo').style.webkitTransform = "scaleX(-1)"; document.getElementById('largeVideo').style.webkitTransform = "scaleX(-1)";
} }
else if (!localVideo && videoTransform == 'scaleX(-1)') { else if (!localVideo && videoTransform === 'scaleX(-1)') {
document.getElementById('largeVideo').style.webkitTransform = "none"; document.getElementById('largeVideo').style.webkitTransform = "none";
} }
...@@ -880,7 +880,7 @@ function buttonClick(id, classname) { ...@@ -880,7 +880,7 @@ function buttonClick(id, classname) {
*/ */
function openLockDialog() { function openLockDialog() {
// Only the focus is able to set a shared key. // Only the focus is able to set a shared key.
if (focus == null) { if (focus === null) {
if (sharedKey) if (sharedKey)
$.prompt("This conversation is currently protected by a shared secret key.", $.prompt("This conversation is currently protected by a shared secret key.",
{ {
...@@ -981,7 +981,7 @@ function openSettingsDialog() { ...@@ -981,7 +981,7 @@ function openSettingsDialog() {
} }
/* /*
var lockKey = document.getElementById('lockKey'); var lockKey = document.getElementById('lockKey');
if (lockKey.value) if (lockKey.value)
{ {
setSharedKey(lockKey.value); setSharedKey(lockKey.value);
...@@ -1013,7 +1013,7 @@ function openPreziDialog() { ...@@ -1013,7 +1013,7 @@ function openPreziDialog() {
} }
}); });
} }
else if (preziPlayer != null) { else if (preziPlayer !== null) {
$.prompt("Another participant is already sharing a Prezi." + $.prompt("Another participant is already sharing a Prezi." +
"This conference allows only one Prezi at a time.", "This conference allows only one Prezi at a time.",
{ {
...@@ -1044,8 +1044,8 @@ function openPreziDialog() { ...@@ -1044,8 +1044,8 @@ function openPreziDialog() {
var urlValue var urlValue
= encodeURI(Util.escapeHtml(preziUrl.value)); = encodeURI(Util.escapeHtml(preziUrl.value));
if (urlValue.indexOf('http://prezi.com/') != 0 if (urlValue.indexOf('http://prezi.com/') !== 0
&& urlValue.indexOf('https://prezi.com/') != 0) && urlValue.indexOf('https://prezi.com/') !== 0)
{ {
$.prompt.goToState('state1'); $.prompt.goToState('state1');
return false; return false;
...@@ -1077,7 +1077,7 @@ function openPreziDialog() { ...@@ -1077,7 +1077,7 @@ function openPreziDialog() {
defaultButton: 1, defaultButton: 1,
submit:function(e,v,m,f) { submit:function(e,v,m,f) {
e.preventDefault(); e.preventDefault();
if(v==0) if (v === 0)
$.prompt.close(); $.prompt.close();
else else
$.prompt.goToState('state0'); $.prompt.goToState('state0');
...@@ -1127,7 +1127,7 @@ function updateLockButton() { ...@@ -1127,7 +1127,7 @@ function updateLockButton() {
*/ */
function showToolbar() { function showToolbar() {
$('#toolbar').css({visibility:"visible"}); $('#toolbar').css({visibility:"visible"});
if (focus != null) if (focus !== null)
{ {
// TODO: Enable settings functionality. Need to uncomment the settings button in index.html. // TODO: Enable settings functionality. Need to uncomment the settings button in index.html.
// $('#settingsButton').css({visibility:"visible"}); // $('#settingsButton').css({visibility:"visible"});
...@@ -1145,7 +1145,7 @@ function updateRoomUrl(newRoomUrl) { ...@@ -1145,7 +1145,7 @@ function updateRoomUrl(newRoomUrl) {
* Warning to the user that the conference window is about to be closed. * Warning to the user that the conference window is about to be closed.
*/ */
function closePageWarning() { function closePageWarning() {
if (focus != null) if (focus !== null)
return "You are the owner of this conference call and you are about to end it."; return "You are the owner of this conference call and you are about to end it.";
else else
return "You are about to leave this conversation."; return "You are about to leave this conversation.";
...@@ -1157,10 +1157,10 @@ function closePageWarning() { ...@@ -1157,10 +1157,10 @@ function closePageWarning() {
* from the connection.jingle.sessions. * from the connection.jingle.sessions.
*/ */
function showFocusIndicator() { function showFocusIndicator() {
if (focus != null) { if (focus !== null) {
var indicatorSpan = $('#localVideoContainer .focusindicator'); var indicatorSpan = $('#localVideoContainer .focusindicator');
if (indicatorSpan.children().length == 0) if (indicatorSpan.children().length === 0)
{ {
createFocusIndicatorElement(indicatorSpan[0]); createFocusIndicatorElement(indicatorSpan[0]);
} }
...@@ -1172,11 +1172,11 @@ function showFocusIndicator() { ...@@ -1172,11 +1172,11 @@ function showFocusIndicator() {
var focusContainer = document.getElementById(focusId); var focusContainer = document.getElementById(focusId);
var indicatorSpan = $('#' + focusId + ' .focusindicator'); var indicatorSpan = $('#' + focusId + ' .focusindicator');
if (!indicatorSpan || indicatorSpan.length == 0) { if (!indicatorSpan || indicatorSpan.length === 0) {
indicatorSpan = document.createElement('span'); indicatorSpan = document.createElement('span');
indicatorSpan.className = 'focusindicator'; indicatorSpan.className = 'focusindicator';
focusContainer.appendChild(indicatorSpan); focusContainer.appendChild(indicatorSpan);
createFocusIndicatorElement(indicatorSpan); createFocusIndicatorElement(indicatorSpan);
} }
} }
...@@ -1197,12 +1197,12 @@ function addRemoteVideoContainer(id) { ...@@ -1197,12 +1197,12 @@ function addRemoteVideoContainer(id) {
function createFocusIndicatorElement(parentElement) { function createFocusIndicatorElement(parentElement) {
var focusIndicator = document.createElement('i'); var focusIndicator = document.createElement('i');
focusIndicator.className = 'fa fa-star'; focusIndicator.className = 'fa fa-star';
focusIndicator.title = "The owner of this conference" focusIndicator.title = "The owner of this conference";
parentElement.appendChild(focusIndicator); parentElement.appendChild(focusIndicator);
} }
/** /**
* Toggles the application in and out of full screen mode * Toggles the application in and out of full screen mode
* (a.k.a. presentation mode in Chrome). * (a.k.a. presentation mode in Chrome).
*/ */
function toggleFullScreen() { function toggleFullScreen() {
...@@ -1214,7 +1214,7 @@ function toggleFullScreen() { ...@@ -1214,7 +1214,7 @@ function toggleFullScreen() {
if (fsElement.mozRequestFullScreen) { if (fsElement.mozRequestFullScreen) {
fsElement.mozRequestFullScreen(); fsElement.mozRequestFullScreen();
} }
else { else {
fsElement.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); fsElement.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
} }
...@@ -1243,8 +1243,8 @@ function showDisplayName(videoSpanId, displayName) { ...@@ -1243,8 +1243,8 @@ function showDisplayName(videoSpanId, displayName) {
if (nameSpan.length > 0) { if (nameSpan.length > 0) {
var nameSpanElement = nameSpan.get(0); var nameSpanElement = nameSpan.get(0);
if (nameSpanElement.id == 'localDisplayName' if (nameSpanElement.id === 'localDisplayName'
&& $('#localDisplayName').html() != escDisplayName) && $('#localDisplayName').html() !== escDisplayName)
$('#localDisplayName').html(escDisplayName); $('#localDisplayName').html(escDisplayName);
else else
$('#' + videoSpanId + '_name').html(escDisplayName); $('#' + videoSpanId + '_name').html(escDisplayName);
...@@ -1252,7 +1252,7 @@ function showDisplayName(videoSpanId, displayName) { ...@@ -1252,7 +1252,7 @@ function showDisplayName(videoSpanId, displayName) {
else { else {
var editButton = null; var editButton = null;
if (videoSpanId == 'localVideoContainer') { if (videoSpanId === 'localVideoContainer') {
editButton = createEditDisplayNameButton(); editButton = createEditDisplayNameButton();
} }
if (escDisplayName.length) { if (escDisplayName.length) {
...@@ -1289,7 +1289,7 @@ function showDisplayName(videoSpanId, displayName) { ...@@ -1289,7 +1289,7 @@ function showDisplayName(videoSpanId, displayName) {
$('#editDisplayName').select(); $('#editDisplayName').select();
var inputDisplayNameHandler = function(name) { var inputDisplayNameHandler = function(name) {
if (nickname != name) { if (nickname !== name) {
nickname = Util.escapeHtml(name); nickname = Util.escapeHtml(name);
window.localStorage.displayname = nickname; window.localStorage.displayname = nickname;
connection.emuc.addDisplayNameToPresence(nickname); connection.emuc.addDisplayNameToPresence(nickname);
...@@ -1310,7 +1310,7 @@ function showDisplayName(videoSpanId, displayName) { ...@@ -1310,7 +1310,7 @@ function showDisplayName(videoSpanId, displayName) {
}); });
$('#editDisplayName').on('keydown', function (e) { $('#editDisplayName').on('keydown', function (e) {
if (e.keyCode == 13) { if (e.keyCode === 13) {
e.preventDefault(); e.preventDefault();
inputDisplayNameHandler(this.value); inputDisplayNameHandler(this.value);
} }
......
...@@ -17,7 +17,7 @@ var Chat = (function (my) { ...@@ -17,7 +17,7 @@ var Chat = (function (my) {
} }
$('#nickinput').keydown(function(event) { $('#nickinput').keydown(function(event) {
if (event.keyCode == 13) { if (event.keyCode === 13) {
event.preventDefault(); event.preventDefault();
var val = Util.escapeHtml(this.value); var val = Util.escapeHtml(this.value);
this.value = ''; this.value = '';
...@@ -36,7 +36,7 @@ var Chat = (function (my) { ...@@ -36,7 +36,7 @@ var Chat = (function (my) {
}); });
$('#usermsg').keydown(function(event) { $('#usermsg').keydown(function(event) {
if (event.keyCode == 13) { if (event.keyCode === 13) {
event.preventDefault(); event.preventDefault();
var message = Util.escapeHtml(this.value); var message = Util.escapeHtml(this.value);
$('#usermsg').val('').trigger('autosize.resize'); $('#usermsg').val('').trigger('autosize.resize');
...@@ -64,7 +64,7 @@ var Chat = (function (my) { ...@@ -64,7 +64,7 @@ var Chat = (function (my) {
my.updateChatConversation = function (from, displayName, message) { my.updateChatConversation = function (from, displayName, message) {
var divClassName = ''; var divClassName = '';
if (connection.emuc.myroomjid == from) { if (connection.emuc.myroomjid === from) {
divClassName = "localuser"; divClassName = "localuser";
} }
else { else {
...@@ -124,7 +124,7 @@ var Chat = (function (my) { ...@@ -124,7 +124,7 @@ var Chat = (function (my) {
} }
// Request the focus in the nickname field or the chat input field. // Request the focus in the nickname field or the chat input field.
if ($('#nickname').css('visibility') == 'visible') if ($('#nickname').css('visibility') === 'visible')
$('#nickinput').focus(); $('#nickinput').focus();
else { else {
$('#usermsg').focus(); $('#usermsg').focus();
...@@ -171,7 +171,7 @@ var Chat = (function (my) { ...@@ -171,7 +171,7 @@ var Chat = (function (my) {
$('#chatconversation').width($('#chatspace').width() - 10); $('#chatconversation').width($('#chatspace').width() - 10);
$('#chatconversation') $('#chatconversation')
.height(window.innerHeight - 50 - parseInt(usermsgHeight)); .height(window.innerHeight - 50 - parseInt(usermsgHeight));
}; }
/** /**
* Shows/hides a visual notification, indicating that a message has arrived. * Shows/hides a visual notification, indicating that a message has arrived.
...@@ -222,4 +222,4 @@ var Chat = (function (my) { ...@@ -222,4 +222,4 @@ var Chat = (function (my) {
} }
return my; return my;
}(Chat || {})); }(Chat || {}));
\ No newline at end of file
...@@ -4,8 +4,8 @@ Strophe.addConnectionPlugin('logger', { ...@@ -4,8 +4,8 @@ Strophe.addConnectionPlugin('logger', {
log: [], log: [],
init: function (conn) { init: function (conn) {
this.connection = conn; this.connection = conn;
this.connection.rawInput = this.log_incoming.bind(this);; this.connection.rawInput = this.log_incoming.bind(this);
this.connection.rawOutput = this.log_outgoing.bind(this);; this.connection.rawOutput = this.log_outgoing.bind(this);
}, },
log_incoming: function (stanza) { log_incoming: function (stanza) {
this.log.push([new Date().getTime(), 'incoming', stanza]); this.log.push([new Date().getTime(), 'incoming', stanza]);
......
...@@ -40,7 +40,7 @@ var Etherpad = (function (my) { ...@@ -40,7 +40,7 @@ var Etherpad = (function (my) {
else else
largeVideo = $('#largeVideo'); largeVideo = $('#largeVideo');
if ($('#etherpad>iframe').css('visibility') == 'hidden') { if ($('#etherpad>iframe').css('visibility') === 'hidden') {
largeVideo.fadeOut(300, function () { largeVideo.fadeOut(300, function () {
if (isPresentationVisible()) if (isPresentationVisible())
largeVideo.css({opacity:'0'}); largeVideo.css({opacity:'0'});
...@@ -123,9 +123,9 @@ var Etherpad = (function (my) { ...@@ -123,9 +123,9 @@ var Etherpad = (function (my) {
if (!config.etherpad_base) if (!config.etherpad_base)
return; return;
if (etherpadIFrame && etherpadIFrame.style.visibility != 'hidden') if (etherpadIFrame && etherpadIFrame.style.visibility !== 'hidden')
Etherpad.toggleEtherpad(isPresentation); Etherpad.toggleEtherpad(isPresentation);
}); });
return my; return my;
}(Etherpad || {})); }(Etherpad || {}));
\ No newline at end of file
...@@ -2,8 +2,13 @@ ...@@ -2,8 +2,13 @@
<head> <head>
<title>WebRTC, meet the Jitsi Videobridge</title> <title>WebRTC, meet the Jitsi Videobridge</title>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="libs/strophejingle.bundle.js?v=7"></script><!-- strophe.jingle bundle --> <script src="libs/strophe/strophe.jingle.adapter.js?v=1"></script><!-- strophe.jingle bundles -->
<script src="libs/colibri.js?v=7"></script><!-- colibri focus implementation --> <script src="libs/strophe/strophe.jingle.bundle.js?v=8"></script>
<script src="libs/strophe/strophe.jingle.js?v=1"></script>
<script src="libs/strophe/strophe.jingle.sdp.js?v=1"></script>
<script src="libs/strophe/strophe.jingle.session.js?v=1"></script>
<script src="libs/colibri/colibri.focus.js?v=8"></script><!-- colibri focus implementation -->
<script src="libs/colibri/colibri.session.js?v=1"></script>
<script src="//code.jquery.com/ui/1.10.4/jquery-ui.js"></script> <script src="//code.jquery.com/ui/1.10.4/jquery-ui.js"></script>
<script src="muc.js?v=9"></script><!-- simple MUC library --> <script src="muc.js?v=9"></script><!-- simple MUC library -->
<script src="estos_log.js?v=2"></script><!-- simple stanza logger --> <script src="estos_log.js?v=2"></script><!-- simple stanza logger -->
......
/* colibri.js -- a COLIBRI focus /* colibri.js -- a COLIBRI focus
* The colibri spec has been submitted to the XMPP Standards Foundation * The colibri spec has been submitted to the XMPP Standards Foundation
* for publications as a XMPP extensions: * for publications as a XMPP extensions:
* http://xmpp.org/extensions/inbox/colibri.html * http://xmpp.org/extensions/inbox/colibri.html
...@@ -7,32 +7,32 @@ ...@@ -7,32 +7,32 @@
* in the conference. The conference itself can be ad-hoc, through a * in the conference. The conference itself can be ad-hoc, through a
* MUC, through PubSub, etc. * MUC, through PubSub, etc.
* *
* colibri.js relies heavily on the strophe.jingle library available * colibri.js relies heavily on the strophe.jingle library available
* from https://github.com/ESTOS/strophe.jingle * from https://github.com/ESTOS/strophe.jingle
* and interoperates with the Jitsi videobridge available from * and interoperates with the Jitsi videobridge available from
* https://jitsi.org/Projects/JitsiVideobridge * https://jitsi.org/Projects/JitsiVideobridge
*/ */
/* /*
Copyright (c) 2013 ESTOS GmbH Copyright (c) 2013 ESTOS GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions: furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software. all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
*/ */
/* jshint -W117 */ /* jshint -W117 */
function ColibriFocus(connection, bridgejid) { function ColibriFocus(connection, bridgejid) {
this.connection = connection; this.connection = connection;
...@@ -86,20 +86,20 @@ ColibriFocus.prototype.makeConference = function (peers) { ...@@ -86,20 +86,20 @@ ColibriFocus.prototype.makeConference = function (peers) {
this.peerconnection.oniceconnectionstatechange = function (event) { this.peerconnection.oniceconnectionstatechange = function (event) {
console.warn('ice connection state changed to', self.peerconnection.iceConnectionState); console.warn('ice connection state changed to', self.peerconnection.iceConnectionState);
/* /*
if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') { if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
console.log('adding new remote SSRCs from iceconnectionstatechange'); console.log('adding new remote SSRCs from iceconnectionstatechange');
window.setTimeout(function() { self.modifySources(); }, 1000); window.setTimeout(function() { self.modifySources(); }, 1000);
} }
*/ */
}; };
this.peerconnection.onsignalingstatechange = function (event) { this.peerconnection.onsignalingstatechange = function (event) {
console.warn(self.peerconnection.signalingState); console.warn(self.peerconnection.signalingState);
/* /*
if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') { if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
console.log('adding new remote SSRCs from signalingstatechange'); console.log('adding new remote SSRCs from signalingstatechange');
window.setTimeout(function() { self.modifySources(); }, 1000); window.setTimeout(function() { self.modifySources(); }, 1000);
} }
*/ */
}; };
this.peerconnection.onaddstream = function (event) { this.peerconnection.onaddstream = function (event) {
self.remoteStream = event.stream; self.remoteStream = event.stream;
...@@ -163,7 +163,7 @@ ColibriFocus.prototype._makeConference = function () { ...@@ -163,7 +163,7 @@ ColibriFocus.prototype._makeConference = function () {
/* /*
var localSDP = new SDP(this.peerconnection.localDescription.sdp); var localSDP = new SDP(this.peerconnection.localDescription.sdp);
localSDP.media.forEach(function (media, channel) { localSDP.media.forEach(function (media, channel) {
var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media; var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
elem.c('content', {name: name}); elem.c('content', {name: name});
elem.c('channel', {initiator: 'false', expire: '15'}); elem.c('channel', {initiator: 'false', expire: '15'});
...@@ -303,10 +303,10 @@ ColibriFocus.prototype.createdConference = function (result) { ...@@ -303,10 +303,10 @@ ColibriFocus.prototype.createdConference = function (result) {
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: self.confid}); elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: self.confid});
var localSDP = new SDP(self.peerconnection.localDescription.sdp); var localSDP = new SDP(self.peerconnection.localDescription.sdp);
localSDP.media.forEach(function (media, channel) { localSDP.media.forEach(function (media, channel) {
var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media; var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
elem.c('content', {name: name}); elem.c('content', {name: name});
elem.c('channel', { elem.c('channel', {
initiator: 'true', initiator: 'true',
expire: '15', expire: '15',
id: self.mychannel[channel].attr('id') id: self.mychannel[channel].attr('id')
}); });
...@@ -322,9 +322,6 @@ ColibriFocus.prototype.createdConference = function (result) { ...@@ -322,9 +322,6 @@ ColibriFocus.prototype.createdConference = function (result) {
localSDP.TransportToJingle(channel, elem); localSDP.TransportToJingle(channel, elem);
elem.up(); // end of channel elem.up(); // end of channel
for (j = 0; j < self.peers.length; j++) {
elem.c('channel', {initiator: 'true', expire:'15' }).up();
}
elem.up(); // end of content elem.up(); // end of content
}); });
...@@ -498,7 +495,7 @@ ColibriFocus.prototype.addNewParticipant = function (peer) { ...@@ -498,7 +495,7 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid}); elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
var localSDP = new SDP(this.peerconnection.localDescription.sdp); var localSDP = new SDP(this.peerconnection.localDescription.sdp);
localSDP.media.forEach(function (media, channel) { localSDP.media.forEach(function (media, channel) {
var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media; var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
elem.c('content', {name: name}); elem.c('content', {name: name});
elem.c('channel', {initiator: 'true', expire:'15'}); elem.c('channel', {initiator: 'true', expire:'15'});
elem.up(); // end of channel elem.up(); // end of channel
...@@ -534,7 +531,7 @@ ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) { ...@@ -534,7 +531,7 @@ ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
// TODO: too much copy-paste // TODO: too much copy-paste
var rtpmap = SDPUtil.parse_rtpmap(val); var rtpmap = SDPUtil.parse_rtpmap(val);
change.c('payload-type', rtpmap); change.c('payload-type', rtpmap);
// //
// put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/> // 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)) { if (SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id)) {
...@@ -585,7 +582,7 @@ ColibriFocus.prototype.sendSSRCUpdate = function (sdp, jid, isadd) { ...@@ -585,7 +582,7 @@ ColibriFocus.prototype.sendSSRCUpdate = function (sdp, jid, isadd) {
sid: peersess.sid sid: peersess.sid
} }
); );
// FIXME: only announce video ssrcs since we mix audio and dont need // FIXME: only announce video ssrcs since we mix audio and dont need
// the audio ssrcs therefore // the audio ssrcs therefore
var modified = false; var modified = false;
for (channel = 0; channel < sdp.media.length; channel++) { for (channel = 0; channel < sdp.media.length; channel++) {
...@@ -870,7 +867,7 @@ ColibriFocus.prototype.modifySources = function () { ...@@ -870,7 +867,7 @@ ColibriFocus.prototype.modifySources = function () {
self.pendingop = null; self.pendingop = null;
} }
// FIXME: pushing down an answer while ice connection state // FIXME: pushing down an answer while ice connection state
// is still checking is bad... // is still checking is bad...
//console.log(self.peerconnection.iceConnectionState); //console.log(self.peerconnection.iceConnectionState);
...@@ -934,55 +931,3 @@ ColibriFocus.prototype.hardMuteVideo = function (muted) { ...@@ -934,55 +931,3 @@ ColibriFocus.prototype.hardMuteVideo = function (muted) {
track.enabled = !muted; track.enabled = !muted;
}); });
}; };
// 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');
};
/* 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.
*/
// 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');
};
function TraceablePeerConnection(ice_config, constraints) {
var self = this;
var RTCPeerconnection = navigator.mozGetUserMedia ? mozRTCPeerConnection : webkitRTCPeerConnection;
this.peerconnection = new RTCPeerconnection(ice_config, constraints);
this.updateLog = [];
this.stats = {};
this.statsinterval = null;
this.maxstats = 300; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
// override as desired
this.trace = function(what, info) {
//console.warn('WTRACE', what, info);
self.updateLog.push({
time: new Date(),
type: what,
value: info || ""
});
};
this.onicecandidate = null;
this.peerconnection.onicecandidate = function (event) {
self.trace('onicecandidate', JSON.stringify(event.candidate, null, ' '));
if (self.onicecandidate !== null) {
self.onicecandidate(event);
}
};
this.onaddstream = null;
this.peerconnection.onaddstream = function (event) {
self.trace('onaddstream', event.stream.id);
if (self.onaddstream !== null) {
self.onaddstream(event);
}
};
this.onremovestream = null;
this.peerconnection.onremovestream = function (event) {
self.trace('onremovestream', event.stream.id);
if (self.onremovestream !== null) {
self.onremovestream(event);
}
};
this.onsignalingstatechange = null;
this.peerconnection.onsignalingstatechange = function (event) {
self.trace('onsignalingstatechange', event.srcElement.signalingState);
if (self.onsignalingstatechange !== null) {
self.onsignalingstatechange(event);
}
};
this.oniceconnectionstatechange = null;
this.peerconnection.oniceconnectionstatechange = function (event) {
self.trace('oniceconnectionstatechange', event.srcElement.iceConnectionState);
if (self.oniceconnectionstatechange !== null) {
self.oniceconnectionstatechange(event);
}
};
this.onnegotiationneeded = null;
this.peerconnection.onnegotiationneeded = function (event) {
self.trace('onnegotiationneeded');
if (self.onnegotiationneeded !== null) {
self.onnegotiationneeded(event);
}
};
self.ondatachannel = null;
this.peerconnection.ondatachannel = function (event) {
self.trace('ondatachannel', event);
if (self.ondatachannel !== null) {
self.ondatachannel(event);
}
}
if (!navigator.mozGetUserMedia) {
this.statsinterval = window.setInterval(function() {
self.peerconnection.getStats(function(stats) {
var results = stats.result();
for (var i = 0; i < results.length; ++i) {
//console.log(results[i].type, results[i].id, results[i].names())
var now = new Date();
results[i].names().forEach(function (name) {
var id = results[i].id + '-' + name;
if (!self.stats[id]) {
self.stats[id] = {
startTime: now,
endTime: now,
values: [],
times: []
};
}
self.stats[id].values.push(results[i].stat(name));
self.stats[id].times.push(now.getTime());
if (self.stats[id].values.length > self.maxstats) {
self.stats[id].values.shift();
self.stats[id].times.shift();
}
self.stats[id].endTime = now;
});
}
});
}, 1000);
}
};
dumpSDP = function(description) {
return 'type: ' + description.type + '\r\n' + description.sdp;
}
if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) {
TraceablePeerConnection.prototype.__defineGetter__('signalingState', function() { return this.peerconnection.signalingState; });
TraceablePeerConnection.prototype.__defineGetter__('iceConnectionState', function() { return this.peerconnection.iceConnectionState; });
TraceablePeerConnection.prototype.__defineGetter__('localDescription', function() { return this.peerconnection.localDescription; });
TraceablePeerConnection.prototype.__defineGetter__('remoteDescription', function() { return this.peerconnection.remoteDescription; });
}
TraceablePeerConnection.prototype.addStream = function (stream) {
this.trace('addStream', stream.id);
this.peerconnection.addStream(stream);
};
TraceablePeerConnection.prototype.removeStream = function (stream) {
this.trace('removeStream', stream.id);
this.peerconnection.removeStream(stream);
};
TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
this.trace('createDataChannel', label, opts);
this.peerconnection.createDataChannel(label, opts);
}
TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {
var self = this;
this.trace('setLocalDescription', dumpSDP(description));
this.peerconnection.setLocalDescription(description,
function () {
self.trace('setLocalDescriptionOnSuccess');
successCallback();
},
function (err) {
self.trace('setLocalDescriptionOnFailure', err);
failureCallback(err);
}
);
/*
if (this.statsinterval === null && this.maxstats > 0) {
// start gathering stats
}
*/
};
TraceablePeerConnection.prototype.setRemoteDescription = function (description, successCallback, failureCallback) {
var self = this;
this.trace('setRemoteDescription', dumpSDP(description));
this.peerconnection.setRemoteDescription(description,
function () {
self.trace('setRemoteDescriptionOnSuccess');
successCallback();
},
function (err) {
self.trace('setRemoteDescriptionOnFailure', err);
failureCallback(err);
}
);
/*
if (this.statsinterval === null && this.maxstats > 0) {
// start gathering stats
}
*/
};
TraceablePeerConnection.prototype.close = function () {
this.trace('stop');
if (this.statsinterval !== null) {
window.clearInterval(this.statsinterval);
this.statsinterval = null;
}
this.peerconnection.close();
};
TraceablePeerConnection.prototype.createOffer = function (successCallback, failureCallback, constraints) {
var self = this;
this.trace('createOffer', JSON.stringify(constraints, null, ' '));
this.peerconnection.createOffer(
function (offer) {
self.trace('createOfferOnSuccess', dumpSDP(offer));
successCallback(offer);
},
function(err) {
self.trace('createOfferOnFailure', err);
failureCallback(err);
},
constraints
);
};
TraceablePeerConnection.prototype.createAnswer = function (successCallback, failureCallback, constraints) {
var self = this;
this.trace('createAnswer', JSON.stringify(constraints, null, ' '));
this.peerconnection.createAnswer(
function (answer) {
self.trace('createAnswerOnSuccess', dumpSDP(answer));
successCallback(answer);
},
function(err) {
self.trace('createAnswerOnFailure', err);
failureCallback(err);
},
constraints
);
};
TraceablePeerConnection.prototype.addIceCandidate = function (candidate, successCallback, failureCallback) {
var self = this;
this.trace('addIceCandidate', JSON.stringify(candidate, null, ' '));
this.peerconnection.addIceCandidate(candidate);
/* maybe later
this.peerconnection.addIceCandidate(candidate,
function () {
self.trace('addIceCandidateOnSuccess');
successCallback();
},
function (err) {
self.trace('addIceCandidateOnFailure', err);
failureCallback(err);
}
);
*/
};
TraceablePeerConnection.prototype.getStats = function(callback, errback) {
if (navigator.mozGetUserMedia) {
// ignore for now...
} else {
this.peerconnection.getStats(callback);
}
};
// mozilla chrome compat layer -- very similar to adapter.js
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 = {
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 = {
peerconnection: webkitRTCPeerConnection,
browser: 'chrome',
getUserMedia: navigator.webkitGetUserMedia.bind(navigator),
attachMediaStream: function (element, stream) {
element.attr('src', webkitURL.createObjectURL(stream));
},
// DTLS should now be enabled by default but..
pc_constraints: {'optional': [{'DtlsSrtpKeyAgreement': 'true'}]}
};
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 getUserMediaWithConstraints(um, resolution, bandwidth, fps) {
var constraints = {audio: false, video: false};
if (um.indexOf('video') >= 0) {
constraints.video = {mandatory: {}};// same behaviour as true
}
if (um.indexOf('audio') >= 0) {
constraints.audio = {};// same behaviour as true
}
if (um.indexOf('screen') >= 0) {
constraints.video = {
"mandatory": {
"chromeMediaSource": "screen"
}
};
}
if (resolution && !constraints.video) {
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':
constraints.video.mandatory.minWidth = 1920;
constraints.video.mandatory.minHeight = 1080;
constraints.video.mandatory.minAspectRatio = 1.77;
break;
case '720':
case 'hd':
constraints.video.mandatory.minWidth = 1280;
constraints.video.mandatory.minHeight = 720;
constraints.video.mandatory.minAspectRatio = 1.77;
break;
case '360':
constraints.video.mandatory.minWidth = 640;
constraints.video.mandatory.minHeight = 360;
constraints.video.mandatory.minAspectRatio = 1.77;
break;
case '180':
constraints.video.mandatory.minWidth = 320;
constraints.video.mandatory.minHeight = 180;
constraints.video.mandatory.minAspectRatio = 1.77;
break;
// 4:3
case '960':
constraints.video.mandatory.minWidth = 960;
constraints.video.mandatory.minHeight = 720;
break;
case '640':
case 'vga':
constraints.video.mandatory.minWidth = 640;
constraints.video.mandatory.minHeight = 480;
break;
case '320':
constraints.video.mandatory.minWidth = 320;
constraints.video.mandatory.minHeight = 240;
break;
default:
if (navigator.userAgent.indexOf('Android') != -1) {
constraints.video.mandatory.minWidth = 320;
constraints.video.mandatory.minHeight = 240;
constraints.video.mandatory.maxFrameRate = 15;
}
break;
}
if (bandwidth) { // doesn't work currently, see webrtc issue 1846
if (!constraints.video) constraints.video = {mandatory: {}};//same behaviour as true
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 (!constraints.video) constraints.video = {mandatory: {}};// same behaviour as tru;
constraints.video.mandatory.minFrameRate = fps;
}
try {
RTC.getUserMedia(constraints,
function (stream) {
console.log('onUserMediaSuccess');
$(document).trigger('mediaready.jingle', [stream]);
},
function (error) {
console.warn('Failed to get access to local media. Error ', error);
$(document).trigger('mediafailure.jingle');
});
} catch (e) {
console.error('GUM failed: ', e);
$(document).trigger('mediafailure.jingle');
}
}
\ No newline at end of file
/* jshint -W117 */
Strophe.addConnectionPlugin('jingle', {
connection: null,
sessions: {},
jid2session: {},
ice_config: {iceServers: []},
pc_constraints: {},
media_constraints: {
mandatory: {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': true
}
// MozDontOfferDataChannel: true when this is firefox
},
localStream: null,
init: function (conn) {
this.connection = conn;
if (this.connection.disco) {
// http://xmpp.org/extensions/xep-0167.html#support
// http://xmpp.org/extensions/xep-0176.html#support
this.connection.disco.addFeature('urn:xmpp:jingle:1');
this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:1');
this.connection.disco.addFeature('urn:xmpp:jingle:transports:ice-udp:1');
this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:audio');
this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:video');
// this is dealt with by SDP O/A so we don't need to annouce this
//this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtcp-fb:0'); // XEP-0293
//this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtp-hdrext:0'); // XEP-0294
this.connection.disco.addFeature('urn:ietf:rfc:5761'); // rtcp-mux
//this.connection.disco.addFeature('urn:ietf:rfc:5888'); // a=group, e.g. bundle
//this.connection.disco.addFeature('urn:ietf:rfc:5576'); // a=ssrc
}
this.connection.addHandler(this.onJingle.bind(this), 'urn:xmpp:jingle:1', 'iq', 'set', null, null);
},
onJingle: function (iq) {
var sid = $(iq).find('jingle').attr('sid');
var action = $(iq).find('jingle').attr('action');
// send ack first
var ack = $iq({type: 'result',
to: iq.getAttribute('from'),
id: iq.getAttribute('id')
});
console.log('on jingle ' + action);
var sess = this.sessions[sid];
if ('session-initiate' != action) {
if (sess === null) {
ack.type = 'error';
ack.c('error', {type: 'cancel'})
.c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
.c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
this.connection.send(ack);
return true;
}
// compare from to sess.peerjid (bare jid comparison for later compat with message-mode)
// local jid is not checked
if (Strophe.getBareJidFromJid(iq.getAttribute('from')) != Strophe.getBareJidFromJid(sess.peerjid)) {
console.warn('jid mismatch for session id', sid, iq.getAttribute('from'), sess.peerjid);
ack.type = 'error';
ack.c('error', {type: 'cancel'})
.c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
.c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
this.connection.send(ack);
return true;
}
} else if (sess !== undefined) {
// existing session with same session id
// this might be out-of-order if the sess.peerjid is the same as from
ack.type = 'error';
ack.c('error', {type: 'cancel'})
.c('service-unavailable', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up();
console.warn('duplicate session id', sid);
this.connection.send(ack);
return true;
}
// FIXME: check for a defined action
this.connection.send(ack);
// see http://xmpp.org/extensions/xep-0166.html#concepts-session
switch (action) {
case 'session-initiate':
sess = new JingleSession($(iq).attr('to'), $(iq).find('jingle').attr('sid'), this.connection);
// configure session
if (this.localStream) {
sess.localStreams.push(this.localStream);
}
sess.media_constraints = this.media_constraints;
sess.pc_constraints = this.pc_constraints;
sess.ice_config = this.ice_config;
sess.initiate($(iq).attr('from'), false);
// FIXME: setRemoteDescription should only be done when this call is to be accepted
sess.setRemoteDescription($(iq).find('>jingle'), 'offer');
this.sessions[sess.sid] = sess;
this.jid2session[sess.peerjid] = sess;
// the callback should either
// .sendAnswer and .accept
// or .sendTerminate -- not necessarily synchronus
$(document).trigger('callincoming.jingle', [sess.sid]);
break;
case 'session-accept':
sess.setRemoteDescription($(iq).find('>jingle'), 'answer');
sess.accept();
$(document).trigger('callaccepted.jingle', [sess.sid]);
break;
case 'session-terminate':
console.log('terminating...');
sess.terminate();
this.terminate(sess.sid);
if ($(iq).find('>jingle>reason').length) {
$(document).trigger('callterminated.jingle', [
sess.sid,
$(iq).find('>jingle>reason>:first')[0].tagName,
$(iq).find('>jingle>reason>text').text()
]);
} else {
$(document).trigger('callterminated.jingle', [sess.sid]);
}
break;
case 'transport-info':
sess.addIceCandidate($(iq).find('>jingle>content'));
break;
case 'session-info':
var affected;
if ($(iq).find('>jingle>ringing[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
$(document).trigger('ringing.jingle', [sess.sid]);
} else if ($(iq).find('>jingle>mute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
affected = $(iq).find('>jingle>mute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').attr('name');
$(document).trigger('mute.jingle', [sess.sid, affected]);
} else if ($(iq).find('>jingle>unmute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
affected = $(iq).find('>jingle>unmute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').attr('name');
$(document).trigger('unmute.jingle', [sess.sid, affected]);
}
break;
case 'addsource': // FIXME: proprietary
sess.addSource($(iq).find('>jingle>content'));
break;
case 'removesource': // FIXME: proprietary
sess.removeSource($(iq).find('>jingle>content'));
break;
default:
console.warn('jingle action not implemented', action);
break;
}
return true;
},
initiate: function (peerjid, myjid) { // initiate a new jinglesession to peerjid
var sess = new JingleSession(myjid || this.connection.jid,
Math.random().toString(36).substr(2, 12), // random string
this.connection);
// configure session
if (this.localStream) {
sess.localStreams.push(this.localStream);
}
sess.media_constraints = this.media_constraints;
sess.pc_constraints = this.pc_constraints;
sess.ice_config = this.ice_config;
sess.initiate(peerjid, true);
this.sessions[sess.sid] = sess;
this.jid2session[sess.peerjid] = sess;
sess.sendOffer();
return sess;
},
terminate: function (sid, reason, text) { // terminate by sessionid (or all sessions)
if (sid === null || sid === undefined) {
for (sid in this.sessions) {
if (this.sessions[sid].state != 'ended') {
this.sessions[sid].sendTerminate(reason || (!this.sessions[sid].active()) ? 'cancel' : null, text);
this.sessions[sid].terminate();
}
delete this.jid2session[this.sessions[sid].peerjid];
delete this.sessions[sid];
}
} else if (this.sessions.hasOwnProperty(sid)) {
if (this.sessions[sid].state != 'ended') {
this.sessions[sid].sendTerminate(reason || (!this.sessions[sid].active()) ? 'cancel' : null, text);
this.sessions[sid].terminate();
}
delete this.jid2session[this.sessions[sid].peerjid];
delete this.sessions[sid];
}
},
terminateByJid: function (jid) {
if (this.jid2session.hasOwnProperty(jid)) {
var sess = this.jid2session[jid];
if (sess) {
sess.terminate();
console.log('peer went away silently', jid);
delete this.sessions[sess.sid];
delete this.jid2session[jid];
$(document).trigger('callterminated.jingle', [sess.sid, 'gone']);
}
}
},
getStunAndTurnCredentials: function () {
// get stun and turn configuration from server via xep-0215
// uses time-limited credentials as described in
// http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
//
// see https://code.google.com/p/prosody-modules/source/browse/mod_turncredentials/mod_turncredentials.lua
// for a prosody module which implements this
//
// currently, this doesn't work with updateIce and therefore credentials with a long
// validity have to be fetched before creating the peerconnection
// TODO: implement refresh via updateIce as described in
// https://code.google.com/p/webrtc/issues/detail?id=1650
var self = this;
this.connection.sendIQ(
$iq({type: 'get', to: this.connection.domain})
.c('services', {xmlns: 'urn:xmpp:extdisco:1'}).c('service', {host: 'turn.' + this.connection.domain}),
function (res) {
var iceservers = [];
$(res).find('>services>service').each(function (idx, el) {
el = $(el);
var dict = {};
switch (el.attr('type')) {
case 'stun':
dict.url = 'stun:' + el.attr('host');
if (el.attr('port')) {
dict.url += ':' + el.attr('port');
}
iceservers.push(dict);
break;
case 'turn':
dict.url = 'turn:';
if (el.attr('username')) { // https://code.google.com/p/webrtc/issues/detail?id=1508
if (navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./) && parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10) < 28) {
dict.url += el.attr('username') + '@';
} else {
dict.username = el.attr('username'); // only works in M28
}
}
dict.url += el.attr('host');
if (el.attr('port') && el.attr('port') != '3478') {
dict.url += ':' + el.attr('port');
}
if (el.attr('transport') && el.attr('transport') != 'udp') {
dict.url += '?transport=' + el.attr('transport');
}
if (el.attr('password')) {
dict.credential = el.attr('password');
}
iceservers.push(dict);
break;
}
});
self.ice_config.iceServers = iceservers;
},
function (err) {
console.warn('getting turn credentials failed', err);
console.warn('is mod_turncredentials or similar installed?');
}
);
// implement push?
}
});
/* jshint -W117 */
// SDP STUFF
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 self = this;
var lines = SDPUtil.find_lines(this.session, prefix);
lines.forEach(function(line) {
self.session = self.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 self = this;
var lines = SDPUtil.find_lines(this.media[mediaindex], prefix);
lines.forEach(function(line) {
self.media[mediaindex] = self.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 self = 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);
elem.c('group', {xmlns: 'urn:xmpp:jingle:apps:grouping:0', semantics: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(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(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
}
// map ice-ufrag/pwd, dtls fingerprint, candidates
this.TransportToJingle(i, elem);
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.TransportToJingle = function (mediaindex, elem) {
var i = mediaindex;
var tmp;
var self = this;
elem.c('transport');
// XEP-0320
var fingerprints = SDPUtil.find_lines(this.media[mediaindex], '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(self.media[mediaindex], 'a=setup:', self.session);
if (line) {
tmp.setup = line.substr(8);
}
elem.attrs(tmp);
elem.up(); // end of fingerprint
});
tmp = SDPUtil.iceparams(this.media[mediaindex], this.session);
if (tmp) {
tmp.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
elem.attrs(tmp);
// XEP-0176
if (SDPUtil.find_line(this.media[mediaindex], 'a=candidate:', this.session)) { // add any a=candidate lines
var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=candidate:', this.session);
lines.forEach(function (line) {
elem.c('candidate', SDPUtil.candidateToJingle(line)).up();
});
}
}
elem.up(); // end of transport
}
SDP.prototype.RtcpFbToJingle = function (mediaindex, elem, payloadtype) { // XEP-0293
var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=rtcp-fb:' + payloadtype);
lines.forEach(function (line) {
var tmp = SDPUtil.parse_rtcpfb(line);
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 self = 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) {
$(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.getAttribute('name');
}).get();
if (contents.length > 0) {
self.raw += 'a=group:' + (group.getAttribute('semantics') || group.getAttribute('type')) + ' ' + contents.join(' ') + '\r\n';
}
});
} 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.getAttribute('name');
}).get();
if (group.getAttribute('type') !== null && contents.length > 0) {
self.raw += 'a=group:' + group.getAttribute('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.getAttribute('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 = self.jingle2media($(this));
self.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.getAttribute('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.getAttribute('hash');
media += ' ' + $(this).text();
media += '\r\n';
if (this.getAttribute('setup')) {
media += 'a=setup:' + this.getAttribute('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.getAttribute('tag');
media += ' ' + this.getAttribute('crypto-suite');
media += ' ' + this.getAttribute('key-params');
if (this.getAttribute('session-params')) {
media += ' ' + this.getAttribute('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.getAttribute('id') + ' ';
media += $(this).find('parameter').map(function () { return (this.getAttribute('name') ? (this.getAttribute('name') + '=') : '') + this.getAttribute('value'); }).get().join(';');
media += '\r\n';
}
// xep-0293
media += self.RtcpFbFromJingle($(this), this.getAttribute('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.getAttribute('id') + ' ' + this.getAttribute('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.getAttribute('ssrc');
$(this).find('>parameter').each(function () {
media += 'a=ssrc:' + ssrc + ' ' + this.getAttribute('name');
if (this.getAttribute('value') && this.getAttribute('value').length)
media += ':' + this.getAttribute('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';
}
};
/* jshint -W117 */
// Jingle stuff
function JingleSession(me, sid, connection) {
this.me = me;
this.sid = sid;
this.connection = connection;
this.initiator = null;
this.responder = null;
this.isInitiator = null;
this.peerjid = null;
this.state = null;
this.peerconnection = null;
this.remoteStream = null;
this.localSDP = null;
this.remoteSDP = null;
this.localStreams = [];
this.relayedStreams = [];
this.remoteStreams = [];
this.startTime = null;
this.stopTime = null;
this.media_constraints = null;
this.pc_constraints = null;
this.ice_config = {};
this.drip_container = [];
this.usetrickle = true;
this.usepranswer = false; // early transport warmup -- mind you, this might fail. depends on webrtc issue 1718
this.usedrip = false; // dripping is sending trickle candidates not one-by-one
this.hadstuncandidate = false;
this.hadturncandidate = false;
this.lasticecandidate = false;
this.statsinterval = null;
this.reason = null;
this.addssrc = [];
this.removessrc = [];
this.pendingop = null;
this.wait = true;
}
JingleSession.prototype.initiate = function (peerjid, isInitiator) {
var self = this;
if (this.state !== null) {
console.error('attempt to initiate on session ' + this.sid +
'in state ' + this.state);
return;
}
this.isInitiator = isInitiator;
this.state = 'pending';
this.initiator = isInitiator ? this.me : peerjid;
this.responder = !isInitiator ? this.me : peerjid;
this.peerjid = peerjid;
//console.log('create PeerConnection ' + JSON.stringify(this.ice_config));
try {
this.peerconnection = new RTCPeerconnection(this.ice_config,
this.pc_constraints);
} catch (e) {
console.error('Failed to create PeerConnection, exception: ',
e.message);
console.error(e);
return;
}
this.hadstuncandidate = false;
this.hadturncandidate = false;
this.lasticecandidate = false;
this.peerconnection.onicecandidate = function (event) {
self.sendIceCandidate(event.candidate);
};
this.peerconnection.onaddstream = function (event) {
self.remoteStream = event.stream;
self.remoteStreams.push(event.stream);
$(document).trigger('remotestreamadded.jingle', [event, self.sid]);
};
this.peerconnection.onremovestream = function (event) {
self.remoteStream = null;
// FIXME: remove from this.remoteStreams
$(document).trigger('remotestreamremoved.jingle', [event, self.sid]);
};
this.peerconnection.onsignalingstatechange = function (event) {
if (!(self && self.peerconnection)) return;
};
this.peerconnection.oniceconnectionstatechange = function (event) {
if (!(self && self.peerconnection)) return;
switch (self.peerconnection.iceConnectionState) {
case 'connected':
this.startTime = new Date();
break;
case 'disconnected':
this.stopTime = new Date();
break;
}
$(document).trigger('iceconnectionstatechange.jingle', [self.sid, self]);
};
// add any local and relayed stream
this.localStreams.forEach(function(stream) {
self.peerconnection.addStream(stream);
});
this.relayedStreams.forEach(function(stream) {
self.peerconnection.addStream(stream);
});
};
JingleSession.prototype.accept = function () {
var self = this;
this.state = 'active';
var pranswer = this.peerconnection.localDescription;
if (!pranswer || pranswer.type != 'pranswer') {
return;
}
console.log('going from pranswer to answer');
if (this.usetrickle) {
// remove candidates already sent from session-accept
var lines = SDPUtil.find_lines(pranswer.sdp, 'a=candidate:');
for (var i = 0; i < lines.length; i++) {
pranswer.sdp = pranswer.sdp.replace(lines[i] + '\r\n', '');
}
}
while (SDPUtil.find_line(pranswer.sdp, 'a=inactive')) {
// FIXME: change any inactive to sendrecv or whatever they were originally
pranswer.sdp = pranswer.sdp.replace('a=inactive', 'a=sendrecv');
}
var prsdp = new SDP(pranswer.sdp);
var accept = $iq({to: this.peerjid,
type: 'set'})
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
action: 'session-accept',
initiator: this.initiator,
responder: this.responder,
sid: this.sid });
prsdp.toJingle(accept, this.initiator == this.me ? 'initiator' : 'responder');
this.connection.sendIQ(accept,
function () {
var ack = {};
ack.source = 'answer';
$(document).trigger('ack.jingle', [self.sid, ack]);
},
function (stanza) {
var error = ($(stanza).find('error').length) ? {
code: $(stanza).find('error').attr('code'),
reason: $(stanza).find('error :first')[0].tagName,
}:{};
error.source = 'answer';
$(document).trigger('error.jingle', [self.sid, error]);
},
10000);
var sdp = this.peerconnection.localDescription.sdp;
while (SDPUtil.find_line(sdp, 'a=inactive')) {
// FIXME: change any inactive to sendrecv or whatever they were originally
sdp = sdp.replace('a=inactive', 'a=sendrecv');
}
this.peerconnection.setLocalDescription(new RTCSessionDescription({type: 'answer', sdp: sdp}),
function () {
//console.log('setLocalDescription success');
$(document).trigger('setLocalDescription.jingle', [self.sid]);
},
function (e) {
console.error('setLocalDescription failed', e);
}
);
};
JingleSession.prototype.terminate = function (reason) {
this.state = 'ended';
this.reason = reason;
this.peerconnection.close();
if (this.statsinterval !== null) {
window.clearInterval(this.statsinterval);
this.statsinterval = null;
}
};
JingleSession.prototype.active = function () {
return this.state == 'active';
};
JingleSession.prototype.sendIceCandidate = function (candidate) {
var self = this;
if (candidate && !this.lasticecandidate) {
var ice = SDPUtil.iceparams(this.localSDP.media[candidate.sdpMLineIndex], this.localSDP.session);
var jcand = SDPUtil.candidateToJingle(candidate.candidate);
if (!(ice && jcand)) {
console.error('failed to get ice && jcand');
return;
}
ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
if (jcand.type === 'srflx') {
this.hadstuncandidate = true;
} else if (jcand.type === 'relay') {
this.hadturncandidate = true;
}
if (this.usetrickle) {
if (this.usedrip) {
if (this.drip_container.length === 0) {
// start 20ms callout
window.setTimeout(function () {
if (self.drip_container.length === 0) return;
self.sendIceCandidates(self.drip_container);
self.drip_container = [];
}, 20);
}
this.drip_container.push(event.candidate);
return;
} else {
self.sendIceCandidate([event.candidate]);
}
}
} else {
//console.log('sendIceCandidate: last candidate.');
if (!this.usetrickle) {
//console.log('should send full offer now...');
var init = $iq({to: this.peerjid,
type: 'set'})
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
action: this.peerconnection.localDescription.type == 'offer' ? 'session-initiate' : 'session-accept',
initiator: this.initiator,
sid: this.sid});
this.localSDP = new SDP(this.peerconnection.localDescription.sdp);
this.localSDP.toJingle(init, this.initiator == this.me ? 'initiator' : 'responder');
this.connection.sendIQ(init,
function () {
//console.log('session initiate ack');
var ack = {};
ack.source = 'offer';
$(document).trigger('ack.jingle', [self.sid, ack]);
},
function (stanza) {
self.state = 'error';
self.peerconnection.close();
var error = ($(stanza).find('error').length) ? {
code: $(stanza).find('error').attr('code'),
reason: $(stanza).find('error :first')[0].tagName,
}:{};
error.source = 'offer';
$(document).trigger('error.jingle', [self.sid, error]);
},
10000);
}
this.lasticecandidate = true;
console.log('Have we encountered any srflx candidates? ' + this.hadstuncandidate);
console.log('Have we encountered any relay candidates? ' + this.hadturncandidate);
if (!(this.hadstuncandidate || this.hadturncandidate) && this.peerconnection.signalingState != 'closed') {
$(document).trigger('nostuncandidates.jingle', [this.sid]);
}
}
};
JingleSession.prototype.sendIceCandidates = function (candidates) {
console.log('sendIceCandidates', candidates);
var cand = $iq({to: this.peerjid, type: 'set'})
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
action: 'transport-info',
initiator: this.initiator,
sid: this.sid});
for (var mid = 0; mid < this.localSDP.media.length; mid++) {
var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
if (cands.length > 0) {
var ice = SDPUtil.iceparams(this.localSDP.media[mid], this.localSDP.session);
ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
cand.c('content', {creator: this.initiator == this.me ? 'initiator' : 'responder',
name: cands[0].sdpMid
}).c('transport', ice);
for (var i = 0; i < cands.length; i++) {
cand.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
}
// add fingerprint
if (SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session)) {
var tmp = SDPUtil.parse_fingerprint(SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session));
tmp.required = true;
cand.c('fingerprint').t(tmp.fingerprint);
delete tmp.fingerprint;
cand.attrs(tmp);
cand.up();
}
cand.up(); // transport
cand.up(); // content
}
}
// might merge last-candidate notification into this, but it is called alot later. See webrtc issue #2340
//console.log('was this the last candidate', this.lasticecandidate);
this.connection.sendIQ(cand,
function () {
var ack = {};
ack.source = 'transportinfo';
$(document).trigger('ack.jingle', [this.sid, ack]);
},
function (stanza) {
var error = ($(stanza).find('error').length) ? {
code: $(stanza).find('error').attr('code'),
reason: $(stanza).find('error :first')[0].tagName,
}:{};
error.source = 'transportinfo';
$(document).trigger('error.jingle', [this.sid, error]);
},
10000);
};
JingleSession.prototype.sendOffer = function () {
//console.log('sendOffer...');
var self = this;
this.peerconnection.createOffer(function (sdp) {
self.createdOffer(sdp);
},
function (e) {
console.error('createOffer failed', e);
},
this.media_constraints
);
};
JingleSession.prototype.createdOffer = function (sdp) {
//console.log('createdOffer', sdp);
var self = this;
this.localSDP = new SDP(sdp.sdp);
//this.localSDP.mangle();
if (this.usetrickle) {
var init = $iq({to: this.peerjid,
type: 'set'})
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
action: 'session-initiate',
initiator: this.initiator,
sid: this.sid});
this.localSDP.toJingle(init, this.initiator == this.me ? 'initiator' : 'responder');
this.connection.sendIQ(init,
function () {
var ack = {};
ack.source = 'offer';
$(document).trigger('ack.jingle', [self.sid, ack]);
},
function (stanza) {
self.state = 'error';
self.peerconnection.close();
var error = ($(stanza).find('error').length) ? {
code: $(stanza).find('error').attr('code'),
reason: $(stanza).find('error :first')[0].tagName,
}:{};
error.source = 'offer';
$(document).trigger('error.jingle', [self.sid, error]);
},
10000);
}
sdp.sdp = this.localSDP.raw;
this.peerconnection.setLocalDescription(sdp,
function () {
$(document).trigger('setLocalDescription.jingle', [self.sid]);
//console.log('setLocalDescription success');
},
function (e) {
console.error('setLocalDescription failed', e);
}
);
var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:');
for (var i = 0; i < cands.length; i++) {
var cand = SDPUtil.parse_icecandidate(cands[i]);
if (cand.type == 'srflx') {
this.hadstuncandidate = true;
} else if (cand.type == 'relay') {
this.hadturncandidate = true;
}
}
};
JingleSession.prototype.setRemoteDescription = function (elem, desctype) {
//console.log('setting remote description... ', desctype);
this.remoteSDP = new SDP('');
this.remoteSDP.fromJingle(elem);
if (this.peerconnection.remoteDescription !== null) {
console.log('setRemoteDescription when remote description is not null, should be pranswer', this.peerconnection.remoteDescription);
if (this.peerconnection.remoteDescription.type == 'pranswer') {
var pranswer = new SDP(this.peerconnection.remoteDescription.sdp);
for (var i = 0; i < pranswer.media.length; i++) {
// make sure we have ice ufrag and pwd
if (!SDPUtil.find_line(this.remoteSDP.media[i], 'a=ice-ufrag:', this.remoteSDP.session)) {
if (SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session)) {
this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session) + '\r\n';
} else {
console.warn('no ice ufrag?');
}
if (SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session)) {
this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session) + '\r\n';
} else {
console.warn('no ice pwd?');
}
}
// copy over candidates
var lines = SDPUtil.find_lines(pranswer.media[i], 'a=candidate:');
for (var j = 0; j < lines.length; j++) {
this.remoteSDP.media[i] += lines[j] + '\r\n';
}
}
this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join('');
}
}
var remotedesc = new RTCSessionDescription({type: desctype, sdp: this.remoteSDP.raw});
this.peerconnection.setRemoteDescription(remotedesc,
function () {
//console.log('setRemoteDescription success');
},
function (e) {
console.error('setRemoteDescription error', e);
}
);
};
JingleSession.prototype.addIceCandidate = function (elem) {
var self = this;
if (this.peerconnection.signalingState == 'closed') {
return;
}
if (!this.peerconnection.remoteDescription && this.peerconnection.signalingState == 'have-local-offer') {
console.log('trickle ice candidate arriving before session accept...');
// create a PRANSWER for setRemoteDescription
if (!this.remoteSDP) {
var cobbled = 'v=0\r\n' +
'o=- ' + '1923518516' + ' 2 IN IP4 0.0.0.0\r\n' +// FIXME
's=-\r\n' +
't=0 0\r\n';
// first, take some things from the local description
for (var i = 0; i < this.localSDP.media.length; i++) {
cobbled += SDPUtil.find_line(this.localSDP.media[i], 'm=') + '\r\n';
cobbled += SDPUtil.find_lines(this.localSDP.media[i], 'a=rtpmap:').join('\r\n') + '\r\n';
if (SDPUtil.find_line(this.localSDP.media[i], 'a=mid:')) {
cobbled += SDPUtil.find_line(this.localSDP.media[i], 'a=mid:') + '\r\n';
}
cobbled += 'a=inactive\r\n';
}
this.remoteSDP = new SDP(cobbled);
}
// then add things like ice and dtls from remote candidate
elem.each(function () {
for (var i = 0; i < self.remoteSDP.media.length; i++) {
if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
if (!SDPUtil.find_line(self.remoteSDP.media[i], 'a=ice-ufrag:')) {
var tmp = $(this).find('transport');
self.remoteSDP.media[i] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
self.remoteSDP.media[i] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
tmp = $(this).find('transport>fingerprint');
if (tmp.length) {
self.remoteSDP.media[i] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
} else {
console.log('no dtls fingerprint (webrtc issue #1718?)');
self.remoteSDP.media[i] += 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:BAADBAADBAADBAADBAADBAADBAADBAADBAADBAAD\r\n';
}
break;
}
}
}
});
this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join('');
// we need a complete SDP with ice-ufrag/ice-pwd in all parts
// this makes the assumption that the PRANSWER is constructed such that the ice-ufrag is in all mediaparts
// but it could be in the session part as well. since the code above constructs this sdp this can't happen however
var iscomplete = this.remoteSDP.media.filter(function (mediapart) {
return SDPUtil.find_line(mediapart, 'a=ice-ufrag:');
}).length == this.remoteSDP.media.length;
if (iscomplete) {
console.log('setting pranswer');
try {
this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'pranswer', sdp: this.remoteSDP.raw }),
function() {
},
function(e) {
console.log('setRemoteDescription pranswer failed', e.toString());
});
} catch (e) {
console.error('setting pranswer failed', e);
}
} else {
//console.log('not yet setting pranswer');
}
}
// operate on each content element
elem.each(function () {
// would love to deactivate this, but firefox still requires it
var idx = -1;
var i;
for (i = 0; i < self.remoteSDP.media.length; i++) {
if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
idx = i;
break;
}
}
if (idx == -1) { // fall back to localdescription
for (i = 0; i < self.localSDP.media.length; i++) {
if (SDPUtil.find_line(self.localSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
self.localSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
idx = i;
break;
}
}
}
var name = $(this).attr('name');
// TODO: check ice-pwd and ice-ufrag?
$(this).find('transport>candidate').each(function () {
var line, candidate;
line = SDPUtil.candidateFromJingle(this);
candidate = new RTCIceCandidate({sdpMLineIndex: idx,
sdpMid: name,
candidate: line});
try {
self.peerconnection.addIceCandidate(candidate);
} catch (e) {
console.error('addIceCandidate failed', e.toString(), line);
}
});
});
};
JingleSession.prototype.sendAnswer = function (provisional) {
//console.log('createAnswer', provisional);
var self = this;
this.peerconnection.createAnswer(
function (sdp) {
self.createdAnswer(sdp, provisional);
},
function (e) {
console.error('createAnswer failed', e);
},
this.media_constraints
);
};
JingleSession.prototype.createdAnswer = function (sdp, provisional) {
//console.log('createAnswer callback');
var self = this;
this.localSDP = new SDP(sdp.sdp);
//this.localSDP.mangle();
this.usepranswer = provisional === true;
if (this.usetrickle) {
if (!this.usepranswer) {
var accept = $iq({to: this.peerjid,
type: 'set'})
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
action: 'session-accept',
initiator: this.initiator,
responder: this.responder,
sid: this.sid });
this.localSDP.toJingle(accept, this.initiator == this.me ? 'initiator' : 'responder');
this.connection.sendIQ(accept,
function () {
var ack = {};
ack.source = 'answer';
$(document).trigger('ack.jingle', [self.sid, ack]);
},
function (stanza) {
var error = ($(stanza).find('error').length) ? {
code: $(stanza).find('error').attr('code'),
reason: $(stanza).find('error :first')[0].tagName,
}:{};
error.source = 'answer';
$(document).trigger('error.jingle', [self.sid, error]);
},
10000);
} else {
sdp.type = 'pranswer';
for (var i = 0; i < this.localSDP.media.length; i++) {
this.localSDP.media[i] = this.localSDP.media[i].replace('a=sendrecv\r\n', 'a=inactive\r\n');
}
this.localSDP.raw = this.localSDP.session + '\r\n' + this.localSDP.media.join('');
}
}
sdp.sdp = this.localSDP.raw;
this.peerconnection.setLocalDescription(sdp,
function () {
$(document).trigger('setLocalDescription.jingle', [self.sid]);
//console.log('setLocalDescription success');
},
function (e) {
console.error('setLocalDescription failed', e);
}
);
var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:');
for (var j = 0; j < cands.length; j++) {
var cand = SDPUtil.parse_icecandidate(cands[j]);
if (cand.type == 'srflx') {
this.hadstuncandidate = true;
} else if (cand.type == 'relay') {
this.hadturncandidate = true;
}
}
};
JingleSession.prototype.sendTerminate = function (reason, text) {
var self = this,
term = $iq({to: this.peerjid,
type: 'set'})
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
action: 'session-terminate',
initiator: this.initiator,
sid: this.sid})
.c('reason')
.c(reason || 'success');
if (text) {
term.up().c('text').t(text);
}
this.connection.sendIQ(term,
function () {
self.peerconnection.close();
self.peerconnection = null;
self.terminate();
var ack = {};
ack.source = 'terminate';
$(document).trigger('ack.jingle', [self.sid, ack]);
},
function (stanza) {
var error = ($(stanza).find('error').length) ? {
code: $(stanza).find('error').attr('code'),
reason: $(stanza).find('error :first')[0].tagName,
}:{};
$(document).trigger('ack.jingle', [self.sid, error]);
},
10000);
if (this.statsinterval !== null) {
window.clearInterval(this.statsinterval);
this.statsinterval = null;
}
};
JingleSession.prototype.addSource = function (elem) {
console.log('addssrc', new Date().getTime());
console.log('ice', this.peerconnection.iceConnectionState);
var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
var self = this;
$(elem).each(function (idx, content) {
var name = $(content).attr('name');
var lines = '';
tmp = $(content).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
tmp.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';
});
});
sdp.media.forEach(function(media, idx) {
if (!SDPUtil.find_line(media, 'a=mid:' + name))
return;
sdp.media[idx] += lines;
if (!self.addssrc[idx]) self.addssrc[idx] = '';
self.addssrc[idx] += lines;
});
sdp.raw = sdp.session + sdp.media.join('');
});
this.modifySources();
};
JingleSession.prototype.removeSource = function (elem) {
console.log('removessrc', new Date().getTime());
console.log('ice', this.peerconnection.iceConnectionState);
var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
var self = this;
$(elem).each(function (idx, content) {
var name = $(content).attr('name');
var lines = '';
tmp = $(content).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
tmp.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';
});
});
sdp.media.forEach(function(media, idx) {
if (!SDPUtil.find_line(media, 'a=mid:' + name))
return;
sdp.media[idx] += lines;
if (!self.removessrc[idx]) self.removessrc[idx] = '';
self.removessrc[idx] += lines;
});
sdp.raw = sdp.session + sdp.media.join('');
});
this.modifySources();
};
JingleSession.prototype.modifySources = function() {
var self = this;
if (this.peerconnection.signalingState == 'closed') return;
if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null)) return;
if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')) {
console.warn('modifySources not yet', this.peerconnection.signalingState, this.peerconnection.iceConnectionState);
this.wait = true;
window.setTimeout(function() { self.modifySources(); }, 250);
return;
}
if (this.wait) {
window.setTimeout(function() { self.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() {
self.peerconnection.createAnswer(
function(modifiedAnswer) {
// change video direction, see https://github.com/jitsi/jitmeet/issues/41
if (self.pendingop !== null) {
var sdp = new SDP(modifiedAnswer.sdp);
if (sdp.media.length > 1) {
switch(self.pendingop) {
case 'mute':
sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
break;
case 'unmute':
sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
break;
}
sdp.raw = sdp.session + sdp.media.join('');
modifiedAnswer.sdp = sdp.raw;
}
self.pendingop = null;
}
self.peerconnection.setLocalDescription(modifiedAnswer,
function() {
//console.log('modified setLocalDescription ok');
$(document).trigger('setLocalDescription.jingle', [self.sid]);
},
function(error) {
console.log('modified setLocalDescription failed');
}
);
},
function(error) {
console.log('modified answer failed');
}
);
},
function(error) {
console.log('modify failed');
}
);
};
// SDP-based mute by going recvonly/sendrecv
// FIXME: should probably black out the screen as well
JingleSession.prototype.hardMuteVideo = function (muted) {
this.pendingop = muted ? 'mute' : 'unmute';
this.modifySources();
this.connection.jingle.localStream.getVideoTracks().forEach(function (track) {
track.enabled = !muted;
});
};
JingleSession.prototype.sendMute = function (muted, content) {
var info = $iq({to: this.peerjid,
type: 'set'})
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
action: 'session-info',
initiator: this.initiator,
sid: this.sid });
info.c(muted ? 'mute' : 'unmute', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'});
info.attrs({'creator': this.me == this.initiator ? 'creator' : 'responder'});
if (content) {
info.attrs({'name': content});
}
this.connection.send(info);
};
JingleSession.prototype.sendRinging = function () {
var info = $iq({to: this.peerjid,
type: 'set'})
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
action: 'session-info',
initiator: this.initiator,
sid: this.sid });
info.c('ringing', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'});
this.connection.send(info);
};
JingleSession.prototype.getStats = function (interval) {
var self = this;
var recv = {audio: 0, video: 0};
var lost = {audio: 0, video: 0};
var lastrecv = {audio: 0, video: 0};
var lastlost = {audio: 0, video: 0};
var loss = {audio: 0, video: 0};
var delta = {audio: 0, video: 0};
this.statsinterval = window.setInterval(function () {
if (self && self.peerconnection && self.peerconnection.getStats) {
self.peerconnection.getStats(function (stats) {
var results = stats.result();
// TODO: there are so much statistics you can get from this..
for (var i = 0; i < results.length; ++i) {
if (results[i].type == 'ssrc') {
var packetsrecv = results[i].stat('packetsReceived');
var packetslost = results[i].stat('packetsLost');
if (packetsrecv && packetslost) {
packetsrecv = parseInt(packetsrecv, 10);
packetslost = parseInt(packetslost, 10);
if (results[i].stat('googFrameRateReceived')) {
lastlost.video = lost.video;
lastrecv.video = recv.video;
recv.video = packetsrecv;
lost.video = packetslost;
} else {
lastlost.audio = lost.audio;
lastrecv.audio = recv.audio;
recv.audio = packetsrecv;
lost.audio = packetslost;
}
}
}
}
delta.audio = recv.audio - lastrecv.audio;
delta.video = recv.video - lastrecv.video;
loss.audio = (delta.audio > 0) ? Math.ceil(100 * (lost.audio - lastlost.audio) / delta.audio) : 0;
loss.video = (delta.video > 0) ? Math.ceil(100 * (lost.video - lastlost.video) / delta.video) : 0;
$(document).trigger('packetloss.jingle', [self.sid, loss]);
});
}
}, interval || 3000);
return this.statsinterval;
};
/* jshint -W117 */ /* jshint -W117 */
/* a simple MUC connection plugin /* a simple MUC connection plugin
* can only handle a single MUC room * can only handle a single MUC room
*/ */
Strophe.addConnectionPlugin('emuc', { Strophe.addConnectionPlugin('emuc', {
......
...@@ -5,33 +5,33 @@ function processReplacements(body) ...@@ -5,33 +5,33 @@ function processReplacements(body)
{ {
//make links clickable //make links clickable
body = linkify(body); body = linkify(body);
//add smileys //add smileys
body = smilify(body); body = smilify(body);
return body; return body;
} }
/** /**
* Finds and replaces all links in the links in "body" * Finds and replaces all links in the links in "body"
* with their <a href=""></a> * with their <a href=""></a>
*/ */
function linkify(inputText) function linkify(inputText)
{ {
var replacedText, replacePattern1, replacePattern2, replacePattern3; var replacedText, replacePattern1, replacePattern2, replacePattern3;
//URLs starting with http://, https://, or ftp:// //URLs starting with http://, https://, or ftp://
replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim; replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
replacedText = inputText.replace(replacePattern1, '<a href="$1" target="_blank">$1</a>'); replacedText = inputText.replace(replacePattern1, '<a href="$1" target="_blank">$1</a>');
//URLs starting with "www." (without // before it, or it'd re-link the ones done above). //URLs starting with "www." (without // before it, or it'd re-link the ones done above).
replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim; replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
replacedText = replacedText.replace(replacePattern2, '$1<a href="http://$2" target="_blank">$2</a>'); replacedText = replacedText.replace(replacePattern2, '$1<a href="http://$2" target="_blank">$2</a>');
//Change email addresses to mailto:: links. //Change email addresses to mailto:: links.
replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim; replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
replacedText = replacedText.replace(replacePattern3, '<a href="mailto:$1">$1</a>'); replacedText = replacedText.replace(replacePattern3, '<a href="mailto:$1">$1</a>');
return replacedText; return replacedText;
} }
...@@ -42,7 +42,7 @@ function smilify(body) ...@@ -42,7 +42,7 @@ function smilify(body)
{ {
if(!body) if(!body)
return body; return body;
body = body.replace(/(:\(|:\(\(|:-\(\(|:-\(|\(sad\))/gi, "<img src="+smiley1+ ">"); body = body.replace(/(:\(|:\(\(|:-\(\(|:-\(|\(sad\))/gi, "<img src="+smiley1+ ">");
body = body.replace(/(\(angry\))/gi, "<img src="+smiley2+ ">"); body = body.replace(/(\(angry\))/gi, "<img src="+smiley2+ ">");
body = body.replace(/(\(n\))/gi, "<img src="+smiley3+ ">"); body = body.replace(/(\(n\))/gi, "<img src="+smiley3+ ">");
...@@ -63,7 +63,6 @@ function smilify(body) ...@@ -63,7 +63,6 @@ function smilify(body)
body = body.replace(/(:-P|:P|:-p|:p)/gi, "<img src="+smiley18+ ">"); body = body.replace(/(:-P|:P|:-p|:p)/gi, "<img src="+smiley18+ ">");
body = body.replace(/(:-\0|\(shocked\))/gi, "<img src="+smiley19+ ">"); body = body.replace(/(:-\0|\(shocked\))/gi, "<img src="+smiley19+ ">");
body = body.replace(/(\(oops\))/gi, "<img src="+smiley20+ ">"); body = body.replace(/(\(oops\))/gi, "<img src="+smiley20+ ">");
return body return body;
}; }
...@@ -40,7 +40,9 @@ ...@@ -40,7 +40,9 @@
<div class="header_button_separator"></div> <div class="header_button_separator"></div>
<a class="button" onclick='openPDFDialog();'><i id="pdf" title="Share PDF" class="fa fa-file fa-lg"></i></a> <a class="button" onclick='openPDFDialog();'><i id="pdf" title="Share PDF" class="fa fa-file fa-lg"></i></a>
<div class="header_button_separator"></div> <div class="header_button_separator"></div>
<a class="button" onclick='goFullScreen();'><i id="pdf" title="Full Screen" class="fa fa-arrows-alt fa-lg"></i></a> <a class="button" onclick='goFullScreen();'><i id="fullscreen" title="Full Screen" class="fa fa-arrows-alt fa-lg"></i></a>
<div class="header_button_separator"></div>
<a class="button" onclick='inviteParticipant();'><i id="invite" title="Invite Participant" class="fa fa-phone fa-lg"></i></a>
<!-- <!--
<div class="header_button_separator"></div> <div class="header_button_separator"></div>
<a class="button" onclick='goAltView();'><i id="altview" title="Alternate View" class="fa fa-stop fa-lg"></i></a> <a class="button" onclick='goAltView();'><i id="altview" title="Alternate View" class="fa fa-stop fa-lg"></i></a>
......
...@@ -8,6 +8,7 @@ var pdfShare = null; ...@@ -8,6 +8,7 @@ var pdfShare = null;
var pdfFrame = null; var pdfFrame = null;
var pdfPage = "1"; var pdfPage = "1";
var altView = false; var altView = false;
var sipUri = null;
$(document).ready(function () $(document).ready(function ()
{ {
...@@ -699,7 +700,7 @@ function handleOffer (from, offer) ...@@ -699,7 +700,7 @@ function handleOffer (from, offer)
{ {
console.log("handleOffer", 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 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 muc = $(offer).attr('muc');
var nick = $(offer).attr('nickname'); var nick = $(offer).attr('nickname');
var participant = $(offer).attr('participant'); var participant = $(offer).attr('participant');
...@@ -1254,7 +1255,72 @@ function goFullScreen() ...@@ -1254,7 +1255,72 @@ function goFullScreen()
} }
} }
} }
function inviteParticipant()
{
if (sipUri == null)
{
$.prompt('<h2>Enter SIP address or Telephone number to invite a person by phone</h2><input id="sipUri" type="text" placeholder="sip:name@domain or tel:nnnnnnnn" autofocus >',
{
title: "Invite Participant by Phone",
persistent: false,
buttons: { "Invite": true , "Cancel": false},
defaultButton: 1,
loaded: function(event) {
document.getElementById('sipUri').select();
},
submit: function(e,v,m,f)
{
if(v)
{
sipUri = document.getElementById('sipUri').value;
$("#invite").addClass("fa-border");
connection.sendIQ($iq({to: connection.domain, type: 'set'}).c('colibri', {xmlns: 'urn:xmpp:rayo:colibri:1', action: 'invite', muc: roomjid, from: "sip:" + roomjid, to: sipUri}),
function (res) {
console.log('rayo colibri invite ok');
$("#invite").removeClass("fa-spin");
},
function (err) {
console.log('rayo colibri invite error', err);
}
);
}
}
});
} else {
$.prompt("Are you sure you would like to remove the Phone Participant?",
{
title: "Remove Participant by Phone",
buttons: { "Remove": true, "Cancel": false},
defaultButton: 1,
submit: function(e,v,m,f)
{
if(v)
{
$("#invite").removeClass("fa-border fa-spin");
connection.sendIQ($iq({to: connection.domain, type: 'set'}).c('colibri', {xmlns: 'urn:xmpp:rayo:colibri:1', action: 'uninvite', muc: roomjid, callId: sipUri}),
function (res) {
console.log('rayo colibri uninvite ok');
sipUri = null;
},
function (err) {
console.log('rayo colibri uninvite error', err);
sipUri = null;
}
);
}
}
});
}
}
function setEmoticons(body) function setEmoticons(body)
{ {
if (body) if (body)
......
plugin.title=Jitsi Videobridge plugin.title=Jitsi Videobridge
plugin.title.description=Jitsi Videobridge Settings plugin.title.description=Jitsi Videobridge Settings
config.page.title=Jitsi Videobridge Settings Page config.page.title=Jitsi Videobridge Settings Page
config.page.configuration.title=Configuration config.page.configuration.media.title=Configuration
config.page.configuration.security.title=Security
config.page.configuration.recording.title=Recordings
config.page.configuration.telephone.title=SIP Registration
config.page.configuration.min.port=Min port used for media config.page.configuration.min.port=Min port used for media
config.page.configuration.max.port=Max port used for media config.page.configuration.max.port=Max port used for media
config.page.configuration.submit=Save
config.page.configuration.error.minport=Invalid min port value config.page.configuration.error.minport=Invalid min port value
config.page.configuration.error.maxport=Invalid max port value config.page.configuration.error.maxport=Invalid max port value
config.page.configuration.username=Username for web applications config.page.configuration.username=Username for web applications
...@@ -13,3 +15,10 @@ config.page.configuration.record.enabled=Enabled ...@@ -13,3 +15,10 @@ config.page.configuration.record.enabled=Enabled
config.page.configuration.record.enabled_description=Audio and Video Recording enabled config.page.configuration.record.enabled_description=Audio and Video Recording enabled
config.page.configuration.record.disabled=Disabled config.page.configuration.record.disabled=Disabled
config.page.configuration.record.disabled_description=Audio and Video Recording disabled config.page.configuration.record.disabled_description=Audio and Video Recording disabled
config.page.configuration.authusername=Username
config.page.configuration.sippassword=Password
config.page.configuration.server=Registration Server
config.page.configuration.outboundproxy=Outbound Proxy
config.page.configuration.save.title=Save Settings
config.page.configuration.submit=Save
config.page.configuration.restart.warning=Changes to any of these parameters requires a restart of Openfire.
package com.rayo.core.verb;
import java.net.URI;
import javax.validation.constraints.NotNull;
import com.rayo.core.validation.Messages;
import org.xmpp.packet.*;
public class InviteCommand extends AbstractVerbCommand {
@NotNull(message=Messages.MISSING_TO)
private URI to;
@NotNull(message=Messages.MISSING_FROM)
private URI from;
private JID muc;
public InviteCommand(JID muc, URI from, URI to)
{
this.muc = muc;
this.to = to;
this.from = from;
}
public JID getMuc() {
return muc;
}
public void setMuc(JID muc) {
this.muc = muc;
}
public URI getTo() {
return to;
}
public void setTo(URI to) {
this.to = to;
}
public URI getFrom() {
return from;
}
public void setFrom(URI from) {
this.from = from;
}
}
package com.rayo.core.verb;
import org.xmpp.packet.*;
public class UnInviteCommand extends AbstractVerbCommand {
private JID muc;
private String callId;
public UnInviteCommand(JID muc, String callId)
{
this.muc = muc;
this.callId = callId;
}
public JID getMuc() {
return muc;
}
public void setMuc(JID muc) {
this.muc = muc;
}
public String getCallId() {
return callId;
}
public void setCallId(String callId) {
this.callId = callId;
}
}
...@@ -59,6 +59,13 @@ public class ColibriProvider extends BaseProvider { ...@@ -59,6 +59,13 @@ public class ColibriProvider extends BaseProvider {
} else if ("expire".equals(action)) { } else if ("expire".equals(action)) {
command = new ColibriExpireCommand(new JID(element.attributeValue("muc"))); command = new ColibriExpireCommand(new JID(element.attributeValue("muc")));
} else if ("invite".equals(action)) {
command = new InviteCommand(new JID(element.attributeValue("muc")), toURI(element.attributeValue("from")), toURI(element.attributeValue("to")));
} else if ("uninvite".equals(action)) {
command = new UnInviteCommand(new JID(element.attributeValue("muc")), element.attributeValue("callid"));
} }
return command; return command;
......
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.attribute;
import de.javawi.jstun.util.*;
public class ChangeRequest extends MessageAttribute {
/*
* 0 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 A B 0|
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
boolean changeIP = false;
boolean changePort = false;
public ChangeRequest() {
super(MessageAttribute.MessageAttributeType.ChangeRequest);
}
public boolean isChangeIP() {
return changeIP;
}
public boolean isChangePort() {
return changePort;
}
public void setChangeIP() {
changeIP = true;
}
public void setChangePort() {
changePort = true;
}
public byte[] getBytes() throws UtilityException {
byte[] result = new byte[8];
// message attribute header
// type
System.arraycopy(Utility.integerToTwoBytes(typeToInteger(type)), 0, result, 0, 2);
// length
System.arraycopy(Utility.integerToTwoBytes(4), 0, result, 2, 2);
// change request header
if (changeIP) result[7] = Utility.integerToOneByte(4);
if (changePort) result[7] = Utility.integerToOneByte(2);
if (changeIP && changePort) result[7] = Utility.integerToOneByte(6);
return result;
}
public static ChangeRequest parse(byte[] data) throws MessageAttributeParsingException {
try {
if (data.length < 4) {
throw new MessageAttributeParsingException("Data array too short");
}
ChangeRequest cr = new ChangeRequest();
int status = Utility.oneByteToInteger(data[3]);
switch (status) {
case 0: break;
case 2: cr.setChangePort(); break;
case 4: cr.setChangeIP(); break;
case 6: cr.setChangeIP(); cr.setChangePort(); break;
default: throw new MessageAttributeParsingException("Status parsing error");
}
return cr;
} catch (UtilityException ue) {
throw new MessageAttributeParsingException("Parsing error");
}
}
}
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.attribute;
import java.util.logging.Logger;
public class ChangedAddress extends MappedResponseChangedSourceAddressReflectedFrom {
private static Logger logger = Logger.getLogger("de.javawi.stun.attribute.ChangedAddress");
public ChangedAddress() {
super(MessageAttribute.MessageAttributeType.ChangedAddress);
}
public static MessageAttribute parse(byte[] data) throws MessageAttributeParsingException {
ChangedAddress ca = new ChangedAddress();
MappedResponseChangedSourceAddressReflectedFrom.parse(ca, data);
logger.finer("Message Attribute: Changed Address parsed: " + ca.toString() + ".");
return ca;
}
}
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.attribute;
import de.javawi.jstun.util.Utility;
import de.javawi.jstun.util.UtilityException;
public class Dummy extends MessageAttribute {
int lengthValue;
public Dummy() {
super(MessageAttributeType.Dummy);
}
public void setLengthValue(int length) {
this.lengthValue = length;
}
public byte[] getBytes() throws UtilityException {
byte[] result = new byte[lengthValue + 4];
// message attribute header
// type
System.arraycopy(Utility.integerToTwoBytes(typeToInteger(type)), 0, result, 0, 2);
// length
System.arraycopy(Utility.integerToTwoBytes(lengthValue), 0, result, 2, 2);
return result;
}
public static Dummy parse(byte[] data) {
Dummy dummy = new Dummy();
dummy.setLengthValue(data.length);
return dummy;
}
}
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.attribute;
import de.javawi.jstun.util.Utility;
import de.javawi.jstun.util.UtilityException;
public class ErrorCode extends MessageAttribute {
/*
* 0 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | 0 |Class| Number |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Reason Phrase (variable) ..
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
int responseCode;
String reason;
public ErrorCode() {
super(MessageAttribute.MessageAttributeType.ErrorCode);
}
public void setResponseCode(int responseCode) throws MessageAttributeException {
switch (responseCode) {
case 400: reason = "Bad Request"; break;
case 401: reason = "Unauthorized"; break;
case 420: reason = "Unkown Attribute"; break;
case 430: reason = "Stale Credentials"; break;
case 431: reason = "Integrity Check Failure"; break;
case 432: reason = "Missing Username"; break;
case 433: reason = "Use TLS"; break;
case 500: reason = "Server Error"; break;
case 600: reason = "Global Failure"; break;
default: throw new MessageAttributeException("Response Code is not valid");
}
this.responseCode = responseCode;
}
public int getResponseCode() {
return responseCode;
}
public String getReason() {
return reason;
}
public byte[] getBytes() throws UtilityException {
int length = reason.length();
// length adjustment
if ((length % 4) != 0) {
length += 4 - (length % 4);
}
// message attribute header
length += 4;
byte[] result = new byte[length];
// message attribute header
// type
System.arraycopy(Utility.integerToTwoBytes(typeToInteger(type)), 0, result, 0, 2);
// length
System.arraycopy(Utility.integerToTwoBytes(length-4), 0, result, 2, 2);
// error code header
int classHeader = (int) Math.floor(((double)responseCode)/100);
result[6] = Utility.integerToOneByte(classHeader);
result[7] = Utility.integerToOneByte(responseCode%100);
byte[] reasonArray = reason.getBytes();
System.arraycopy(reasonArray, 0, result, 8, reasonArray.length);
return result;
}
public static ErrorCode parse(byte[] data) throws MessageAttributeParsingException {
try {
if (data.length < 4) {
throw new MessageAttributeParsingException("Data array too short");
}
byte classHeaderByte = data[3];
int classHeader = Utility.oneByteToInteger(classHeaderByte);
if ((classHeader < 1) || (classHeader > 6)) throw new MessageAttributeParsingException("Class parsing error");
byte numberByte = data[4];
int number = Utility.oneByteToInteger(numberByte);
if ((number < 0) || (number > 99)) throw new MessageAttributeParsingException("Number parsing error");
int responseCode = (classHeader * 100) + number;
ErrorCode result = new ErrorCode();
result.setResponseCode(responseCode);
return result;
} catch (UtilityException ue) {
throw new MessageAttributeParsingException("Parsing error");
} catch (MessageAttributeException mae) {
throw new MessageAttributeParsingException("Parsing error");
}
}
}
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.attribute;
import java.util.logging.Logger;
public class MappedAddress extends MappedResponseChangedSourceAddressReflectedFrom {
private static Logger logger = Logger.getLogger("de.javawi.stun.attribute.MappedAddress");
public MappedAddress() {
super(MessageAttribute.MessageAttributeType.MappedAddress);
}
public static MessageAttribute parse(byte[] data) throws MessageAttributeParsingException {
MappedAddress ma = new MappedAddress();
MappedResponseChangedSourceAddressReflectedFrom.parse(ma, data);
logger.finer("Message Attribute: Mapped Address parsed: " + ma.toString() + ".");
return ma;
}
}
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.attribute;
import de.javawi.jstun.util.*;
public class MappedResponseChangedSourceAddressReflectedFrom extends MessageAttribute {
int port;
Address address;
/*
* 0 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |x x x x x x x x| Family | Port |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Address |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
public MappedResponseChangedSourceAddressReflectedFrom() {
super();
try {
port = 0;
address = new Address("0.0.0.0");
} catch (UtilityException ue) {
ue.getMessage();
ue.printStackTrace();
}
}
public MappedResponseChangedSourceAddressReflectedFrom(MessageAttribute.MessageAttributeType type) {
super(type);
}
public int getPort() {
return port;
}
public Address getAddress() {
return address;
}
public void setPort(int port) throws MessageAttributeException {
if ((port > 65536) || (port < 0)) {
throw new MessageAttributeException("Port value " + port + " out of range.");
}
this.port = port;
}
public void setAddress(Address address) {
this.address = address;
}
public byte[] getBytes() throws UtilityException {
byte[] result = new byte[12];
// message attribute header
// type
System.arraycopy(Utility.integerToTwoBytes(typeToInteger(type)), 0, result, 0, 2);
// length
System.arraycopy(Utility.integerToTwoBytes(8), 0, result, 2, 2);
// mappedaddress header
// family
result[5] = Utility.integerToOneByte(0x01);
// port
System.arraycopy(Utility.integerToTwoBytes(port), 0, result, 6, 2);
// address
System.arraycopy(address.getBytes(), 0, result, 8, 4);
return result;
}
protected static MappedResponseChangedSourceAddressReflectedFrom parse(MappedResponseChangedSourceAddressReflectedFrom ma, byte[] data) throws MessageAttributeParsingException {
try {
if (data.length < 8) {
throw new MessageAttributeParsingException("Data array too short");
}
int family = Utility.oneByteToInteger(data[1]);
if (family != 0x01) throw new MessageAttributeParsingException("Family " + family + " is not supported");
byte[] portArray = new byte[2];
System.arraycopy(data, 2, portArray, 0, 2);
ma.setPort(Utility.twoBytesToInteger(portArray));
int firstOctet = Utility.oneByteToInteger(data[4]);
int secondOctet = Utility.oneByteToInteger(data[5]);
int thirdOctet = Utility.oneByteToInteger(data[6]);
int fourthOctet = Utility.oneByteToInteger(data[7]);
ma.setAddress(new Address(firstOctet, secondOctet, thirdOctet, fourthOctet));
return ma;
} catch (UtilityException ue) {
throw new MessageAttributeParsingException("Parsing error");
} catch (MessageAttributeException mae) {
throw new MessageAttributeParsingException("Port parsing error");
}
}
public String toString() {
return "Address " +address.toString() + ", Port " + port;
}
}
\ No newline at end of file
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.attribute;
import java.util.logging.*;
import de.javawi.jstun.util.*;
public abstract class MessageAttribute implements MessageAttributeInterface {
private static Logger logger = Logger.getLogger("de.javawi.stun.util.MessageAttribute");
MessageAttributeType type;
public MessageAttribute() {
}
public MessageAttribute(MessageAttributeType type) {
setType(type);
}
public void setType(MessageAttributeType type) {
this.type = type;
}
public MessageAttribute.MessageAttributeType getType() {
return type;
}
public static int typeToInteger(MessageAttributeType type) {
if (type == MessageAttributeType.MappedAddress) return MAPPEDADDRESS;
if (type == MessageAttributeType.ResponseAddress) return RESPONSEADDRESS;
if (type == MessageAttributeType.ChangeRequest) return CHANGEREQUEST;
if (type == MessageAttributeType.SourceAddress) return SOURCEADDRESS;
if (type == MessageAttributeType.ChangedAddress) return CHANGEDADDRESS;
if (type == MessageAttributeType.Username) return USERNAME;
if (type == MessageAttributeType.Password) return PASSWORD;
if (type == MessageAttributeType.MessageIntegrity) return MESSAGEINTEGRITY;
if (type == MessageAttributeType.ErrorCode) return ERRORCODE;
if (type == MessageAttributeType.UnknownAttribute) return UNKNOWNATTRIBUTE;
if (type == MessageAttributeType.ReflectedFrom) return REFLECTEDFROM;
if (type == MessageAttributeType.Dummy) return DUMMY;
return -1;
}
public static MessageAttributeType intToType(long type) {
if (type == MAPPEDADDRESS) return MessageAttributeType.MappedAddress;
if (type == RESPONSEADDRESS) return MessageAttributeType.ResponseAddress;
if (type == CHANGEREQUEST) return MessageAttributeType.ChangeRequest;
if (type == SOURCEADDRESS) return MessageAttributeType.SourceAddress;
if (type == CHANGEDADDRESS) return MessageAttributeType.ChangedAddress;
if (type == USERNAME) return MessageAttributeType.Username;
if (type == PASSWORD) return MessageAttributeType.Password;
if (type == MESSAGEINTEGRITY) return MessageAttributeType.MessageIntegrity;
if (type == ERRORCODE) return MessageAttributeType.ErrorCode;
if (type == UNKNOWNATTRIBUTE) return MessageAttributeType.UnknownAttribute;
if (type == REFLECTEDFROM) return MessageAttributeType.ReflectedFrom;
if (type == DUMMY) return MessageAttributeType.Dummy;
return null;
}
abstract public byte[] getBytes() throws UtilityException;
//abstract public MessageAttribute parse(byte[] data) throws MessageAttributeParsingException;
public int getLength() throws UtilityException {
int length = getBytes().length;
return length;
}
public static MessageAttribute parseCommonHeader(byte[] data) throws MessageAttributeParsingException {
try {
byte[] typeArray = new byte[2];
System.arraycopy(data, 0, typeArray, 0, 2);
int type = Utility.twoBytesToInteger(typeArray);
byte[] lengthArray = new byte[2];
System.arraycopy(data, 2, lengthArray, 0, 2);
int lengthValue = Utility.twoBytesToInteger(lengthArray);
byte[] valueArray = new byte[lengthValue];
System.arraycopy(data, 4, valueArray, 0, lengthValue);
MessageAttribute ma;
switch (type) {
case MAPPEDADDRESS: ma = MappedAddress.parse(valueArray); break;
case RESPONSEADDRESS: ma = ResponseAddress.parse(valueArray); break;
case CHANGEREQUEST: ma = ChangeRequest.parse(valueArray); break;
case SOURCEADDRESS: ma = SourceAddress.parse(valueArray); break;
case CHANGEDADDRESS: ma = ChangedAddress.parse(valueArray); break;
case USERNAME: ma = Username.parse(valueArray); break;
case PASSWORD: ma = Password.parse(valueArray); break;
case MESSAGEINTEGRITY: ma = MessageIntegrity.parse(valueArray); break;
case ERRORCODE: ma = ErrorCode.parse(valueArray); break;
case UNKNOWNATTRIBUTE: ma = UnknownAttribute.parse(valueArray); break;
case REFLECTEDFROM: ma = ReflectedFrom.parse(valueArray); break;
default:
if (type <= 0x7fff) {
throw new UnknownMessageAttributeException("Unkown mandatory message attribute", intToType(type));
} else {
logger.finer("MessageAttribute with type " + type + " unkown.");
ma = Dummy.parse(valueArray);
break;
}
}
return ma;
} catch (UtilityException ue) {
throw new MessageAttributeParsingException("Parsing error");
}
}
}
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.attribute;
public class MessageAttributeException extends Exception {
private static final long serialVersionUID = 3258131345099404850L;
public MessageAttributeException(String mesg) {
super(mesg);
}
}
\ No newline at end of file
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.attribute;
public interface MessageAttributeInterface {
public enum MessageAttributeType { MappedAddress, ResponseAddress, ChangeRequest, SourceAddress, ChangedAddress, Username, Password, MessageIntegrity, ErrorCode, UnknownAttribute, ReflectedFrom, Dummy };
final static int MAPPEDADDRESS = 0x0001;
final static int RESPONSEADDRESS = 0x0002;
final static int CHANGEREQUEST = 0x0003;
final static int SOURCEADDRESS = 0x0004;
final static int CHANGEDADDRESS = 0x0005;
final static int USERNAME = 0x0006;
final static int PASSWORD = 0x0007;
final static int MESSAGEINTEGRITY = 0x0008;
final static int ERRORCODE = 0x0009;
final static int UNKNOWNATTRIBUTE = 0x000a;
final static int REFLECTEDFROM = 0x000b;
final static int DUMMY = 0x0000;
}
\ No newline at end of file
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.attribute;
public class MessageAttributeParsingException extends MessageAttributeException {
private static final long serialVersionUID = 3258409534426263605L;
public MessageAttributeParsingException(String mesg) {
super(mesg);
}
}
\ No newline at end of file
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.attribute;
public class MessageIntegrity extends MessageAttribute {
// incomplete message integrity implementation
public MessageIntegrity() {
super(MessageAttribute.MessageAttributeType.MessageIntegrity);
}
public byte[] getBytes() {
return new byte[0];
}
public static MessageIntegrity parse(byte[] data) {
return new MessageIntegrity();
}
}
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.attribute;
import de.javawi.jstun.util.Utility;
import de.javawi.jstun.util.UtilityException;
public class Password extends MessageAttribute {
String password;
public Password() {
super(MessageAttribute.MessageAttributeType.Password);
}
public Password(String password) {
super(MessageAttribute.MessageAttributeType.Password);
setPassword(password);
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public byte[] getBytes() throws UtilityException {
int length = password.length();
// password header
if ((length % 4) != 0) {
length += 4 - (length % 4);
}
// message attribute header
length += 4;
byte[] result = new byte[length];
// message attribute header
// type
System.arraycopy(Utility.integerToTwoBytes(typeToInteger(type)), 0, result, 0, 2);
// length
System.arraycopy(Utility.integerToTwoBytes(length - 4), 0, result, 2, 2);
// password header
byte[] temp = password.getBytes();
System.arraycopy(temp, 0, result, 4, temp.length);
return result;
}
public static Password parse(byte[] data) {
Password result = new Password();
String password = new String(data);
result.setPassword(password);
return result;
}
}
\ No newline at end of file
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.attribute;
import java.util.logging.Logger;
public class ReflectedFrom extends MappedResponseChangedSourceAddressReflectedFrom {
private static Logger logger = Logger.getLogger("de.javawi.stun.attribute.ReflectedFrom");
public ReflectedFrom() {
super(MessageAttribute.MessageAttributeType.ReflectedFrom);
}
public static ReflectedFrom parse(byte[] data) throws MessageAttributeParsingException {
ReflectedFrom result = new ReflectedFrom();
MappedResponseChangedSourceAddressReflectedFrom.parse(result, data);
logger.finer("Message Attribute: ReflectedFrom parsed: " + result.toString() + ".");
return result;
}
}
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.attribute;
import java.util.logging.Logger;
public class ResponseAddress extends MappedResponseChangedSourceAddressReflectedFrom {
private static Logger logger = Logger.getLogger("de.javawi.stun.attribute.ResponseAddress");
public ResponseAddress() {
super(MessageAttribute.MessageAttributeType.ResponseAddress);
}
public static MessageAttribute parse(byte[] data) throws MessageAttributeParsingException {
ResponseAddress ra = new ResponseAddress();
MappedResponseChangedSourceAddressReflectedFrom.parse(ra, data);
logger.finer("Message Attribute: Response Address parsed: " + ra.toString() + ".");
return ra;
}
}
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.attribute;
import java.util.logging.Logger;
public class SourceAddress extends MappedResponseChangedSourceAddressReflectedFrom {
private static Logger logger = Logger.getLogger("de.javawi.stun.attribute.SourceAddress");
public SourceAddress() {
super(MessageAttribute.MessageAttributeType.SourceAddress);
}
public static MessageAttribute parse(byte[] data) throws MessageAttributeParsingException {
SourceAddress sa = new SourceAddress();
MappedResponseChangedSourceAddressReflectedFrom.parse(sa, data);
logger.finer("Message Attribute: Source Address parsed: " + sa.toString() + ".");
return sa;
}
}
\ No newline at end of file
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.attribute;
import java.util.*;
import de.javawi.jstun.util.Utility;
import de.javawi.jstun.util.UtilityException;
public class UnknownAttribute extends MessageAttribute {
/*
* 0 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Attribute 1 Type | Attribute 2 Type |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Attribute 3 Type | Attribute 4 Type ...
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
Vector<MessageAttributeType> unkown = new Vector<MessageAttributeType>();
public UnknownAttribute() {
super(MessageAttribute.MessageAttributeType.UnknownAttribute);
}
public void addAttribute(MessageAttributeType attribute) {
unkown.add(attribute);
}
public byte[] getBytes() throws UtilityException {
int length = 0;
if (unkown.size()%2 == 1) {
length = 2 * (unkown.size() + 1) + 4;
} else {
length = 2 * unkown.size() + 4;
}
byte[] result = new byte[length];
// message attribute header
// type
System.arraycopy(Utility.integerToTwoBytes(typeToInteger(type)), 0, result, 0, 2);
// length
System.arraycopy(Utility.integerToTwoBytes(length - 4), 0, result, 2, 2);
// unkown attribute header
Iterator<MessageAttributeType> it = unkown.iterator();
while(it.hasNext()) {
MessageAttributeType attri = it.next();
System.arraycopy(Utility.integerToTwoBytes(typeToInteger(attri)), 0, result, 4, 2);
}
// padding
if (unkown.size()%2 == 1) {
System.arraycopy(Utility.integerToTwoBytes(typeToInteger(unkown.elementAt(1))), 0, result, 4, 2);
}
return result;
}
public static UnknownAttribute parse(byte[] data) throws MessageAttributeParsingException {
try {
UnknownAttribute result = new UnknownAttribute();
if (data.length % 4 != 0) throw new MessageAttributeParsingException("Data array too short");
for (int i = 0; i < data.length; i += 4) {
byte[] temp = new byte[4];
System.arraycopy(data, i, temp, 0, 4);
long attri = Utility.fourBytesToLong(temp);
result.addAttribute(MessageAttribute.intToType(attri));
}
return result;
} catch (UtilityException ue) {
throw new MessageAttributeParsingException("Parsing error");
}
}
}
\ No newline at end of file
package de.javawi.jstun.attribute;
import de.javawi.jstun.attribute.MessageAttributeInterface.MessageAttributeType;
public class UnknownMessageAttributeException extends MessageAttributeParsingException {
private static final long serialVersionUID = 5375193544145543299L;
private MessageAttributeType type;
public UnknownMessageAttributeException(String mesg, MessageAttributeType type) {
super(mesg);
this.type = type;
}
public MessageAttributeType getType() {
return type;
}
}
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.attribute;
import de.javawi.jstun.util.Utility;
import de.javawi.jstun.util.UtilityException;
public class Username extends MessageAttribute {
String username;
public Username() {
super(MessageAttribute.MessageAttributeType.Username);
}
public Username(String username) {
super(MessageAttribute.MessageAttributeType.Username);
setUsername(username);
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public byte[] getBytes() throws UtilityException {
int length = username.length();
// username header
if ((length % 4) != 0) {
length += 4 - (length % 4);
}
// message attribute header
length += 4;
byte[] result = new byte[length];
// message attribute header
// type
System.arraycopy(Utility.integerToTwoBytes(typeToInteger(type)), 0, result, 0, 2);
// length
System.arraycopy(Utility.integerToTwoBytes(length-4), 0, result, 2, 2);
// username header
byte[] temp = username.getBytes();
System.arraycopy(temp, 0, result, 4, temp.length);
return result;
}
public static Username parse(byte[] data) {
Username result = new Username();
String username = new String(data);
result.setUsername(username);
return result;
}
}
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.header;
import de.javawi.jstun.attribute.*;
import de.javawi.jstun.util.*;
import java.util.*;
import java.util.logging.*;
public class MessageHeader implements MessageHeaderInterface {
/*
* 0 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | STUN Message Type | Message Length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* Transaction ID
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
private static Logger logger = Logger.getLogger("de.javawi.stun.header.MessageHeader");
MessageHeaderType type;
byte[] id = new byte[16];
TreeMap<MessageAttribute.MessageAttributeType, MessageAttribute> ma = new TreeMap<MessageAttribute.MessageAttributeType, MessageAttribute>();
public MessageHeader() {
super();
}
public MessageHeader(MessageHeaderType type) {
super();
setType(type);
}
public void setType(MessageHeaderType type) {
this.type = type;
}
public MessageHeaderType getType() {
return type;
}
public static int typeToInteger(MessageHeaderType type) {
if (type == MessageHeaderType.BindingRequest) return BINDINGREQUEST;
if (type == MessageHeaderType.BindingResponse) return BINDINGRESPONSE;
if (type == MessageHeaderType.BindingErrorResponse) return BINDINGERRORRESPONSE;
if (type == MessageHeaderType.SharedSecretRequest) return SHAREDSECRETREQUEST;
if (type == MessageHeaderType.SharedSecretResponse) return SHAREDSECRETRESPONSE;
if (type == MessageHeaderType.SharedSecretErrorResponse) return SHAREDSECRETERRORRESPONSE;
return -1;
}
public void setTransactionID(byte[] id) {
System.arraycopy(id, 0, this.id, 0, 16);
}
public void generateTransactionID() throws UtilityException {
System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, id, 0, 2);
System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, id, 2, 2);
System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, id, 4, 2);
System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, id, 6, 2);
System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, id, 8, 2);
System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, id, 10, 2);
System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, id, 12, 2);
System.arraycopy(Utility.integerToTwoBytes((int)(Math.random() * 65536)), 0, id, 14, 2);
}
public byte[] getTransactionID() {
byte[] idCopy = new byte[id.length];
System.arraycopy(id, 0, idCopy, 0, id.length);
return idCopy;
}
public boolean equalTransactionID(MessageHeader header) {
byte[] idHeader = header.getTransactionID();
if (idHeader.length != 16) return false;
if ((idHeader[0] == id[0]) && (idHeader[1] == id[1]) && (idHeader[2] == id[2]) && (idHeader[3] == id[3]) &&
(idHeader[4] == id[4]) && (idHeader[5] == id[5]) && (idHeader[6] == id[6]) && (idHeader[7] == id[7]) &&
(idHeader[8] == id[8]) && (idHeader[9] == id[9]) && (idHeader[10] == id[10]) && (idHeader[11] == id[11]) &&
(idHeader[12] == id[12]) && (idHeader[13] == id[13]) && (idHeader[14] == id[14]) && (idHeader[15] == id[15])) {
return true;
} else {
return false;
}
}
public void addMessageAttribute(MessageAttribute attri) {
ma.put(attri.getType(), attri);
}
public MessageAttribute getMessageAttribute(MessageAttribute.MessageAttributeType type) {
return ma.get(type);
}
public byte[] getBytes() throws UtilityException {
int length = 20;
Iterator<MessageAttribute.MessageAttributeType> it = ma.keySet().iterator();
while (it.hasNext()) {
MessageAttribute attri = ma.get(it.next());
length += attri.getLength();
}
// add attribute size + attributes.getSize();
byte[] result = new byte[length];
System.arraycopy(Utility.integerToTwoBytes(typeToInteger(type)), 0, result, 0, 2);
System.arraycopy(Utility.integerToTwoBytes(length-20), 0, result, 2, 2);
System.arraycopy(id, 0, result, 4, 16);
// arraycopy of attributes
int offset = 20;
it = ma.keySet().iterator();
while (it.hasNext()) {
MessageAttribute attri = ma.get(it.next());
System.arraycopy(attri.getBytes(), 0, result, offset, attri.getLength());
offset += attri.getLength();
}
return result;
}
public int getLength() throws UtilityException {
return getBytes().length;
}
public void parseAttributes(byte[] data) throws MessageAttributeParsingException {
try {
byte[] lengthArray = new byte[2];
System.arraycopy(data, 2, lengthArray, 0, 2);
int length = Utility.twoBytesToInteger(lengthArray);
System.arraycopy(data, 4, id, 0, 16);
byte[] cuttedData;
int offset = 20;
while (length > 0) {
cuttedData = new byte[length];
System.arraycopy(data, offset, cuttedData, 0, length);
MessageAttribute ma = MessageAttribute.parseCommonHeader(cuttedData);
addMessageAttribute(ma);
length -= ma.getLength();
offset += ma.getLength();
}
} catch (UtilityException ue) {
throw new MessageAttributeParsingException("Parsing error");
}
}
public static MessageHeader parseHeader(byte[] data) throws MessageHeaderParsingException {
try {
MessageHeader mh = new MessageHeader();
byte[] typeArray = new byte[2];
System.arraycopy(data, 0, typeArray, 0, 2);
int type = Utility.twoBytesToInteger(typeArray);
switch (type) {
case BINDINGREQUEST: mh.setType(MessageHeaderType.BindingRequest); logger.finer("Binding Request received."); break;
case BINDINGRESPONSE: mh.setType(MessageHeaderType.BindingResponse); logger.finer("Binding Response received."); break;
case BINDINGERRORRESPONSE: mh.setType(MessageHeaderType.BindingErrorResponse); logger.finer("Binding Error Response received."); break;
case SHAREDSECRETREQUEST: mh.setType(MessageHeaderType.SharedSecretRequest); logger.finer("Shared Secret Request received."); break;
case SHAREDSECRETRESPONSE: mh.setType(MessageHeaderType.SharedSecretResponse); logger.finer("Shared Secret Response received."); break;
case SHAREDSECRETERRORRESPONSE: mh.setType(MessageHeaderType.SharedSecretErrorResponse); logger.finer("Shared Secret Error Response received.");break;
default: throw new MessageHeaderParsingException("Message type " + type + "is not supported");
}
return mh;
} catch (UtilityException ue) {
throw new MessageHeaderParsingException("Parsing error");
}
}
}
\ No newline at end of file
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.header;
public class MessageHeaderException extends Exception {
private static final long serialVersionUID = 3689066248944103737L;
public MessageHeaderException(String mesg) {
super(mesg);
}
}
\ No newline at end of file
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.header;
public interface MessageHeaderInterface {
public enum MessageHeaderType { BindingRequest, BindingResponse, BindingErrorResponse, SharedSecretRequest, SharedSecretResponse, SharedSecretErrorResponse };
final static int BINDINGREQUEST = 0x0001;
final static int BINDINGRESPONSE = 0x0101;
final static int BINDINGERRORRESPONSE = 0x0111;
final static int SHAREDSECRETREQUEST = 0x0002;
final static int SHAREDSECRETRESPONSE = 0x0102;
final static int SHAREDSECRETERRORRESPONSE = 0x0112;
}
\ No newline at end of file
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.header;
public class MessageHeaderParsingException extends MessageHeaderException {
private static final long serialVersionUID = 3544393617029607478L;
public MessageHeaderParsingException(String mesg) {
super(mesg);
}
}
\ No newline at end of file
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.util;
import java.util.*;
import java.net.*;
public class Address {
int firstOctet;
int secondOctet;
int thirdOctet;
int fourthOctet;
public Address(int firstOctet, int secondOctet, int thirdOctet, int fourthOctet) throws UtilityException {
if ((firstOctet < 0) || (firstOctet > 255) || (secondOctet < 0) || (secondOctet > 255) || (thirdOctet < 0) || (thirdOctet > 255) || (fourthOctet < 0) || (fourthOctet > 255)) {
throw new UtilityException("Address is malformed.");
}
this.firstOctet = firstOctet;
this.secondOctet = secondOctet;
this.thirdOctet = thirdOctet;
this.fourthOctet = fourthOctet;
}
public Address(String address) throws UtilityException {
StringTokenizer st = new StringTokenizer(address, ".");
if (st.countTokens() != 4) {
throw new UtilityException("4 octets in address string are required.");
}
int i = 0;
while (st.hasMoreTokens()) {
int temp = Integer.parseInt(st.nextToken());
if ((temp < 0) || (temp > 255)) {
throw new UtilityException("Address is in incorrect format.");
}
switch (i) {
case 0: firstOctet = temp; ++i; break;
case 1: secondOctet = temp; ++i; break;
case 2: thirdOctet = temp; ++i; break;
case 3: fourthOctet = temp; ++i; break;
}
}
}
public Address(byte[] address) throws UtilityException {
if (address.length < 4) {
throw new UtilityException("4 bytes are required.");
}
firstOctet = Utility.oneByteToInteger(address[0]);
secondOctet = Utility.oneByteToInteger(address[1]);
thirdOctet = Utility.oneByteToInteger(address[2]);
fourthOctet = Utility.oneByteToInteger(address[3]);
}
public String toString() {
return firstOctet + "." + secondOctet + "." + thirdOctet + "." + fourthOctet;
}
public byte[] getBytes() throws UtilityException {
byte[] result = new byte[4];
result[0] = Utility.integerToOneByte(firstOctet);
result[1] = Utility.integerToOneByte(secondOctet);
result[2] = Utility.integerToOneByte(thirdOctet);
result[3] = Utility.integerToOneByte(fourthOctet);
return result;
}
public InetAddress getInetAddress() throws UtilityException, UnknownHostException {
byte[] address = new byte[4];
address[0] = Utility.integerToOneByte(firstOctet);
address[1] = Utility.integerToOneByte(secondOctet);
address[2] = Utility.integerToOneByte(thirdOctet);
address[3] = Utility.integerToOneByte(fourthOctet);
return InetAddress.getByAddress(address);
}
public boolean equals(Object obj) {
if (obj == null) return false;
try {
byte[] data1 = this.getBytes();
byte[] data2 = ((Address) obj).getBytes();
if ((data1[0] == data2[0]) && (data1[1] == data2[1]) &&
(data1[2] == data2[2]) && (data1[3] == data2[3])) return true;
return false;
} catch (UtilityException ue) {
return false;
}
}
public int hashCode() {
return (firstOctet << 24) + (secondOctet << 16) + (thirdOctet << 8) + fourthOctet;
}
}
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.util;
public class Utility {
public static final byte integerToOneByte(int value) throws UtilityException {
if ((value > Math.pow(2,15)) || (value < 0)) {
throw new UtilityException("Integer value " + value + " is larger than 2^15");
}
return (byte)(value & 0xFF);
}
public static final byte[] integerToTwoBytes(int value) throws UtilityException {
byte[] result = new byte[2];
if ((value > Math.pow(2,31)) || (value < 0)) {
throw new UtilityException("Integer value " + value + " is larger than 2^31");
}
result[0] = (byte)((value >>> 8) & 0xFF);
result[1] = (byte)(value & 0xFF);
return result;
}
public static final byte[] integerToFourBytes(int value) throws UtilityException {
byte[] result = new byte[4];
if ((value > Math.pow(2,63)) || (value < 0)) {
throw new UtilityException("Integer value " + value + " is larger than 2^63");
}
result[0] = (byte)((value >>> 24) & 0xFF);
result[1] = (byte)((value >>> 16) & 0xFF);
result[2] = (byte)((value >>> 8) & 0xFF);
result[3] = (byte)(value & 0xFF);
return result;
}
public static final int oneByteToInteger(byte value) throws UtilityException {
return (int)value & 0xFF;
}
public static final int twoBytesToInteger(byte[] value) throws UtilityException {
if (value.length < 2) {
throw new UtilityException("Byte array too short!");
}
int temp0 = value[0] & 0xFF;
int temp1 = value[1] & 0xFF;
return ((temp0 << 8) + temp1);
}
public static final long fourBytesToLong(byte[] value) throws UtilityException {
if (value.length < 4) {
throw new UtilityException("Byte array too short!");
}
int temp0 = value[0] & 0xFF;
int temp1 = value[1] & 0xFF;
int temp2 = value[2] & 0xFF;
int temp3 = value[3] & 0xFF;
return (((long)temp0 << 24) + (temp1 << 16) + (temp2 << 8) + temp3);
}
}
/*
* This file is part of JSTUN.
*
* Copyright (c) 2005 Thomas King <king@t-king.de> - All rights
* reserved.
*
* This software is licensed under either the GNU Public License (GPL),
* or the Apache 2.0 license. Copies of both license agreements are
* included in this distribution.
*/
package de.javawi.jstun.util;
public class UtilityException extends Exception {
private static final long serialVersionUID = 3545800974716581680L;
UtilityException(String mesg) {
super(mesg);
}
}
/**
* Copyright 2012 Voxbone SA/NV
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ifsoft.sip;
import java.io.IOException;
import java.util.Date;
import java.util.LinkedList;
import java.util.Vector;
import java.net.*;
import javax.sdp.Attribute;
import javax.sdp.MediaDescription;
import javax.sdp.SdpException;
import javax.sdp.SdpFactory;
import javax.sdp.SdpParseException;
import javax.sdp.SessionDescription;
import javax.sdp.Time;
import javax.sip.ClientTransaction;
import javax.sip.Dialog;
import javax.sip.ServerTransaction;
import javax.sip.message.Message;
import org.slf4j.*;
import org.slf4j.Logger;
import org.jitsi.service.neomedia.*;
import org.jitsi.util.*;
import org.jitsi.videobridge.*;
import org.jitsi.service.libjitsi.*;
/**
*
* Represents a call, contains the information for the Jingle as well as the sip side of the call
*
*/
public class CallSession
{
private static final Logger Log = LoggerFactory.getLogger(CallSession.class);
private static LinkedList<Payload> supported = new LinkedList<Payload>();
public static Payload PAYLOAD_SPEEX = new Payload(99, "speex", 16000, 22000);
public static Payload PAYLOAD_SPEEX2 = new Payload(98, "speex", 8000, 11000);
public static Payload PAYLOAD_PCMU = new Payload(0, "PCMU", 8000, 64000);
public static Payload PAYLOAD_PCMA = new Payload(8, "PCMA", 8000, 64000);
public static Payload PAYLOAD_G723 = new Payload(4, "G723", 8000, 6300);
public static VPayload PAYLOAD_H263 = new VPayload(34, "H263", 90000, 512000, 320, 200, 15);
public static VPayload PAYLOAD_H264 = new VPayload(97, "H264", 90000, 512000, 640, 480, 15);
public static VPayload PAYLOAD_H264SVC = new VPayload(96, "H264-SVC", 90000, 512000, 640, 480, 15);
static
{
supported.add(PAYLOAD_SPEEX);
supported.add(PAYLOAD_SPEEX2);
supported.add(PAYLOAD_PCMU);
supported.add(PAYLOAD_PCMA);
supported.add(PAYLOAD_G723);
supported.add(PAYLOAD_H264);
supported.add(PAYLOAD_H263);
supported.add(PAYLOAD_H264SVC);
}
public static class Payload
{
int id;
String name;
int clockRate;
int bitRate;
public Payload(int id, String name, int clockRate, int bitRate)
{
this.id = id;
this.name = name;
this.clockRate = clockRate;
this.bitRate = bitRate;
}
}
public static class VPayload extends Payload
{
int width;
int height;
int framerate;
public VPayload(int id, String name, int clockRate, int bitRate, int width, int height, int framerate)
{
super(id, name, clockRate, bitRate);
this.width = width;
this.height = height;
this.framerate = framerate;
}
}
String jabberSessionId;
String jabberInitiator;
String candidateUser;
String candidateVUser;
boolean sentTransport = false;
boolean sentVTransport = false;
boolean callAccepted = false;
Dialog sipDialog;
ServerTransaction inviteTransaction;
ClientTransaction inviteOutTransaction;
public RtpRelay relay;
public RtpRelay vRelay;
LinkedList<Payload> offerPayloads = new LinkedList<Payload>();
LinkedList<Payload> answerPayloads = new LinkedList<Payload>();
LinkedList<VPayload> offerVPayloads = new LinkedList<VPayload>();
LinkedList<VPayload> answerVPayloads = new LinkedList<VPayload>();
private static int nextInternalCallId = 0;
public String internalCallId;
public MediaStream mediaStream;
public StreamConnector connector;
public String jabberRemote;
public String jabberLocal;
public CallSession(MediaStream mediaStream, String host)
{
Log.info("CallSession creation " + host);
this.mediaStream = mediaStream;
internalCallId = "CS" + String.format("%08x", nextInternalCallId++);
offerPayloads.add(PAYLOAD_PCMU);
try {
InetAddress bindAddr = InetAddress.getByName(host);
connector = new DefaultStreamConnector(bindAddr);
connector.getDataSocket();
connector.getControlSocket();
mediaStream.setDirection(MediaDirection.RECVONLY);
mediaStream.setConnector(connector);
mediaStream.start();
} catch (Exception e) {
Log.error("CallSession failure", e);
}
}
public void sendBye()
{
Log.info("sendBye");
try
{
mediaStream.stop();
}
finally
{
mediaStream.close();
}
}
private boolean isSupportedPayload(Payload payload)
{
for (Payload p : supported)
{
if (p.name.equalsIgnoreCase(payload.name) && payload.clockRate == p.clockRate)
{
return true;
}
}
return false;
}
public Payload getByName(String name, int clockRate)
{
for (Payload p : supported)
{
if (p.name.equalsIgnoreCase(name) && p.clockRate == clockRate)
{
return p;
}
}
return null;
}
public Payload getByName(String name)
{
for (Payload p : supported)
{
if (p instanceof VPayload)
{
VPayload vp = (VPayload) p;
if (p.name.equalsIgnoreCase(name))
{
return vp;
}
}
}
return null;
}
public Payload getById(int id)
{
for (Payload p : supported)
{
if (p.id == id)
{
return p;
}
}
return null;
}
/*
public void parseInitiate(Packet p, boolean jingle)
{
if(!jingle)
{
StreamElement session = p.getFirstElement(new NSI("session", "http://www.google.com/session"));
jabberSessionId = session.getID();
jabberRemote = p.getFrom();
jabberLocal = p.getTo();
jabberInitiator = session.getAttributeValue("initiator");
parseSession(session, true);
}
else
{
StreamElement session = p.getFirstElement("jingle");
jabberSessionId = session.getAttributeValue("sid");
jabberRemote = p.getFrom();
jabberLocal = p.getTo();
jabberInitiator = session.getAttributeValue("initiator");
parseJingleSession(session, true);
}
}
public void parseAccept(Packet p, boolean jingle)
{
if(jingle)
{
StreamElement session = p.getFirstElement("jingle");
parseJingleSession(session, false);
}
else
{
StreamElement session = p.getFirstElement(new NSI("session", "http://www.google.com/session"));
parseSession(session, false);
}
}
private void parseJingleSession(StreamElement session, boolean offer)
{
// StreamElement content = session.getFirstElement("content");
for(Object contObj : session.listElements("content"))
{
// loop content
StreamElement content = (StreamElement) contObj;
for(Object descObj : content.listElements("description"))
{
StreamElement desc = (StreamElement) descObj;
boolean video = false;
// Parse Video Description
if( desc.getAttributeValue("media") != null
&& desc.getAttributeValue("media").equals("video") )
{
video = true;
Log.info("[[" + internalCallId + "]] Video call detected, enabling video rtp stream");
if (vRelay == null)
{
try
{
vRelay = new RtpRelay(this, true);
}
catch (IOException e)
{
Log.error("Can't setup video rtp relay", e);
}
}
for (Object opt : desc.listElements("payload-type"))
{
StreamElement pt = (StreamElement) opt;
try
{
int id = Integer.parseInt(pt.getAttributeValue("id"));
String name = pt.getAttributeValue("name");
Log.debug("[[" + internalCallId + "]] found payload: " + name );
int framerate = 0;
int width = 0;
int height = 0;
for (Object vparamObj : desc.listElements("parameter"))
{
StreamElement vparams = (StreamElement) vparamObj;
if (vparams.getAttributeValue("framerate") != null) {
framerate = Integer.parseInt(vparams.getAttributeValue("framerate"));
}
if (vparams.getAttributeValue("width") != null) {
width = Integer.parseInt(vparams.getAttributeValue("width"));
}
if (vparams.getAttributeValue("height") != null) {
height = Integer.parseInt(vparams.getAttributeValue("height"));
}
}
// add video payload
Payload p = getByName(name);
if (p != null && p instanceof VPayload)
{
VPayload tmp = (VPayload) p;
VPayload vp = null;
// save the rtp map id, but load in our offical config....
if (framerate != 0 && width != 0 && height != 0) {
vp = new VPayload(id, name, tmp.clockRate, tmp.bitRate, width, height, framerate);
} else {
vp = new VPayload(id, tmp.name, tmp.clockRate, tmp.bitRate, tmp.width, tmp.height, tmp.framerate);
}
if (offer)
{
offerVPayloads.add(vp);
}
else
{
answerVPayloads.add(vp);
}
}
}
catch (NumberFormatException e)
{
// ignore tags we don't understand (but write full log, in case we need to investigate)
Log.warn("[[" + internalCallId + "]] failed to parse tag in session : ", e);
Log.debug("[[" + internalCallId + "]] NumberFormatException -> session contents : " + session.toString());
Log.debug("[[" + internalCallId + "]] NumberFormatException -> description item contents : " + pt.toString());
}
}
// Parse Audio Description
} else if ( desc.getAttributeValue("media") != null
&& desc.getAttributeValue("media").equals("audio") )
{
Log.info("[[" + internalCallId + "]] Audio call detected");
for (Object opt : desc.listElements("payload-type"))
{
StreamElement pt = (StreamElement) opt;
try
{
int id = Integer.parseInt(pt.getAttributeValue("id"));
String name = pt.getAttributeValue("name");
Log.debug("[[" + internalCallId + "]] found payload: " + name );
int clockrate = 0;
if (pt.getAttributeValue("clockrate") != null) {
clockrate = Integer.parseInt(pt.getAttributeValue("clockrate"));
}
int bitrate = 0;
if (pt.getAttributeValue("bitrate") != null)
{
bitrate = Integer.parseInt(pt.getAttributeValue("bitrate"));
}
// add audio payload
Payload payload = new Payload(id, name, clockrate, bitrate);
if (isSupportedPayload(payload))
{
if (offer)
{
offerPayloads.add(payload);
}
else
{
answerPayloads.add(payload);
}
}
}
catch (NumberFormatException e)
{
// ignore tags we don't understand (but write full log, in case we need to investigate)
Log.warn("[[" + internalCallId + "]] failed to parse tag in session : ", e);
Log.debug("[[" + internalCallId + "]] NumberFormatException -> session contents : " + session.toString());
Log.debug("[[" + internalCallId + "]] NumberFormatException -> description item contents : " + pt.toString());
}
}
}
}
}
}
private void parseSession(StreamElement session, boolean offer)
{
StreamElement description = session.getFirstElement("description");
if (description.getNamespaceURI().equals("http://www.google.com/session/video"))
{
Log.info("[[" + internalCallId + "]] Video call detected, enabling video rtp stream");
if (vRelay == null)
{
try
{
vRelay = new RtpRelay(this, true);
}
catch (IOException e)
{
Log.error("Can't setup video rtp relay", e);
}
}
}
for (Object opt : description.listElements())
{
StreamElement pt = (StreamElement) opt;
if (pt.getNamespaceURI().equals("http://www.google.com/session/video") && pt.getLocalName().equals("payload-type"))
{
try
{
int id = Integer.parseInt(pt.getAttributeValue("id"));
String name = pt.getAttributeValue("name");
// int width = Integer.parseInt(pt.getAttributeValue("width"));
// int height = Integer.parseInt(pt.getAttributeValue("height"));
//int framerate = Integer.parseInt(pt.getAttributeValue("framerate"));
Payload p = getByName(name);
if (p != null && p instanceof VPayload)
{
VPayload tmp = (VPayload) p;
// save the rtp map id, but load in our offical config....
VPayload vp = new VPayload(id, tmp.name, tmp.clockRate, tmp.bitRate, tmp.width, tmp.height, tmp.framerate);
if (offer)
{
offerVPayloads.add(vp);
}
else
{
answerVPayloads.add(vp);
}
}
}
catch (NumberFormatException e)
{
// ignore tags we don't understand (but write full log, in case we need to investigate)
Log.warn("[[" + internalCallId + "]] failed to parse tag in session : ", e);
Log.debug("[[" + internalCallId + "]] NumberFormatException -> session contents : " + session.toString());
Log.debug("[[" + internalCallId + "]] NumberFormatException -> description item contents : " + pt.toString());
}
}
else if(pt.getNamespaceURI().equals("http://www.google.com/session/phone") && pt.getLocalName().equals("payload-type"))
{
try
{
int id = Integer.parseInt(pt.getAttributeValue("id"));
String name = pt.getAttributeValue("name");
int clockrate = 0;
if (pt.getAttributeValue("clockrate") != null) {
clockrate = Integer.parseInt(pt.getAttributeValue("clockrate"));
}
int bitrate = 0;
if (pt.getAttributeValue("bitrate") != null)
{
bitrate = Integer.parseInt(pt.getAttributeValue("bitrate"));
}
Payload payload = new Payload(id, name, clockrate, bitrate);
if (isSupportedPayload(payload))
{
if (offer)
{
offerPayloads.add(payload);
}
else
{
answerPayloads.add(payload);
}
}
}
catch (NumberFormatException e)
{
// ignore tags we don't understand (but write full log, in case we need to investigate)
Log.warn("[[" + internalCallId + "]] failed to parse tag in session : ", e);
Log.debug("[[" + internalCallId + "]] NumberFormatException -> session contents : " + session.toString());
Log.debug("[[" + internalCallId + "]] NumberFormatException -> description item contents : " + pt.toString());
}
}
}
}
*/
public SessionDescription buildSDP(boolean offer)
{
SdpFactory sdpFactory = SdpFactory.getInstance();
try
{
SessionDescription sd = sdpFactory.createSessionDescription();
sd.setVersion(sdpFactory.createVersion(0));
long ntpts = SdpFactory.getNtpTime(new Date());
sd.setOrigin(sdpFactory.createOrigin("JabberGW", ntpts, ntpts, "IN", "IP4", SipService.getLocalIP()));
sd.setSessionName(sdpFactory.createSessionName("Jabber Call"));
Vector<Time> times = new Vector<Time>();
times.add(sdpFactory.createTime());
sd.setTimeDescriptions(times);
sd.setConnection(sdpFactory.createConnection(SipService.getLocalIP()));
int [] formats;
Vector<Attribute> attributes = new Vector<Attribute>();
attributes.add(sdpFactory.createAttribute("a", "sendrecv"));
attributes.add(sdpFactory.createAttribute("a", "rtcp-mux"));
if (offer)
{
formats = new int[offerPayloads.size() + 1];
int i = 0;
for (Payload p : offerPayloads)
{
formats[i++] = p.id;
attributes.add(sdpFactory.createAttribute("rtpmap", Integer.toString(p.id) + " " + p.name + "/" + p.clockRate));
}
}
else
{
formats = new int[answerPayloads.size() + 1];
int i = 0;
for (Payload p : answerPayloads)
{
formats[i++] = p.id;
attributes.add(sdpFactory.createAttribute("rtpmap", Integer.toString(p.id) + " " + p.name + "/" + p.clockRate));
}
}
formats[formats.length - 1] = 101;
attributes.add(sdpFactory.createAttribute("rtpmap", "101 telephone-event/8000"));
attributes.add(sdpFactory.createAttribute("fmtp", "101 0-15"));
MediaDescription md = sdpFactory.createMediaDescription("audio", connector.getDataSocket().getLocalPort(), 1, "RTP/AVP", formats);
md.setAttributes(attributes);
Vector<MediaDescription> mds = new Vector<MediaDescription>();
mds.add(md);
if (vRelay != null)
{
// video call, add video m-line
attributes = new Vector<Attribute>();
if (offer)
{
formats = new int[offerVPayloads.size()];
int i = 0;
for (Payload p : offerVPayloads)
{
formats[i++] = p.id;
attributes.add(sdpFactory.createAttribute("rtpmap", Integer.toString(p.id) + " " + p.name + "/" + p.clockRate));
attributes.add(sdpFactory.createAttribute("fmtp", Integer.toString(p.id) + " packetization-rate=1"));
}
}
else
{
formats = new int[answerVPayloads.size()];
int i = 0;
for (Payload p : answerVPayloads)
{
formats[i++] = p.id;
attributes.add(sdpFactory.createAttribute("rtpmap", Integer.toString(p.id) + " " + p.name + "/" + p.clockRate));
attributes.add(sdpFactory.createAttribute("fmtp", Integer.toString(p.id) + " packetization-rate=1"));
}
}
attributes.add(sdpFactory.createAttribute("framerate", "30"));
attributes.add(sdpFactory.createAttribute("rtcp", Integer.toString(this.vRelay.getSipRtcpPort())));
md.setBandwidth("AS", 960);
md = sdpFactory.createMediaDescription("video", this.vRelay.getSipPort(), 1, "RTP/AVP", formats);
md.setAttributes(attributes);
mds.add(md);
}
sd.setMediaDescriptions(mds);
Log.info("buildSDP " + sd);
return sd;
}
catch (SdpException e)
{
Log.error("Error building SDP", e);
}
return null;
}
public void parseSDP(String sdp, boolean offer)
{
Log.info("parseSDP " + sdp);
SdpFactory sdpFactory = SdpFactory.getInstance();
try
{
SessionDescription sd = sdpFactory.createSessionDescription(sdp);
@SuppressWarnings("unchecked")
Vector<MediaDescription> mdesc = (Vector<MediaDescription>) sd.getMediaDescriptions(false);
for (MediaDescription md : mdesc)
{
javax.sdp.Media media = md.getMedia();
if (media.getMediaType().equals("video") && media.getMediaPort() == 0 && vRelay != null )
{
Log.debug("[[" + internalCallId + "]] Video mapping conflict!! (SDP VIDEO PORT = 0)");
// Issue: An video capable XMPP contact video calling a non-video capable SIP endpoint
// Options: remote error (current), proper terminate session, or update candidates for XMPP side
}
if (media.getMediaType().equals("video") && media.getMediaPort() != 0 )
{
Log.info("[[" + internalCallId + "]] Video sdp detected! starting video rtp stream...");
if (vRelay == null)
{
try
{
vRelay = new RtpRelay(this, true);
}
catch (IOException e)
{
Log.error("unable to create video relay!", e);
}
}
int remotePort = media.getMediaPort();
String remoteParty = null;
if (md.getConnection() != null)
{
remoteParty = md.getConnection().getAddress();
}
else
{
remoteParty = sd.getConnection().getAddress();
}
vRelay.setSipDest(remoteParty, remotePort);
@SuppressWarnings("unchecked")
Vector<Attribute> attributes = (Vector<Attribute>) md.getAttributes(false);
for (Attribute attrib : attributes)
{
if (attrib.getName().equals("rtpmap"))
{
Log.debug("[[" + internalCallId + "]] Got attribute value " + attrib.getValue());
String fields[] = attrib.getValue().split(" ", 2);
int codec = Integer.parseInt(fields[0]);
String name = fields[1].split("/")[0];
int clockRate = Integer.parseInt(fields[1].split("/")[1]);
Log.debug("[[" + internalCallId + "]] Payload " + codec + " rate " + clockRate + " is mapped to " + name);
if (codec >= 96)
{
Payload bitRatePayload = getByName(name, clockRate);
if (bitRatePayload != null && bitRatePayload instanceof VPayload)
{
VPayload tmp = (VPayload) bitRatePayload;
VPayload p = new VPayload(codec, tmp.name, clockRate, tmp.bitRate, tmp.width, tmp.height, tmp.framerate);
if (offer)
{
offerVPayloads.add(p);
}
else
{
answerVPayloads.add(p);
}
}
}
}
}
}
else
{
int remotePort = media.getMediaPort();
String remoteParty = null;
if (md.getConnection() != null)
{
remoteParty = md.getConnection().getAddress();
}
else
{
remoteParty = sd.getConnection().getAddress();
}
InetAddress remoteAddr = InetAddress.getByName(remoteParty);
Log.info("CallSession parseSDP " + remoteAddr + " " + remoteParty + " " + remotePort);
MediaService mediaService = LibJitsi.getMediaService();
mediaStream.setTarget(new MediaStreamTarget(new InetSocketAddress(remoteAddr, remotePort),new InetSocketAddress(remoteAddr, remotePort + 1)));
mediaStream.addDynamicRTPPayloadType((byte)111, mediaService.getFormatFactory().createMediaFormat("opus", 48000, 2));
mediaStream.addDynamicRTPPayloadType((byte)0, mediaService.getFormatFactory().createMediaFormat("PCMU", 8000, 1));
mediaStream.setFormat(mediaService.getFormatFactory().createMediaFormat("PCMU", 8000, 1));
mediaStream.setDirection(MediaDirection.SENDRECV);
@SuppressWarnings("unchecked")
Vector<String> codecs = (Vector<String>) media.getMediaFormats(false);
for (String codec : codecs)
{
int id = Integer.parseInt(codec);
Log.debug("[[" + internalCallId + "]] Got a codec " + id);
if (id < 97)
{
Payload p = getById(id);
if (p != null)
{
if (offer)
{
offerPayloads.add(p);
}
else
{
answerPayloads.add(p);
}
}
}
}
@SuppressWarnings("unchecked")
Vector<Attribute> attributes = (Vector<Attribute>) md.getAttributes(false);
for (Attribute attrib : attributes)
{
if (attrib.getName().equals("rtpmap"))
{
Log.debug("[[" + internalCallId + "]] Got attribute value " + attrib.getValue());
String fields[] = attrib.getValue().split(" ", 2);
int codec = Integer.parseInt(fields[0]);
String name = fields[1].split("/")[0];
int clockRate = Integer.parseInt(fields[1].split("/")[1]);
Log.debug("[[" + internalCallId + "]] Payload " + codec + " rate " + clockRate + " is mapped to " + name);
if (codec >= 96)
{
Payload bitRatePayload = getByName(name, clockRate);
if (bitRatePayload != null)
{
Payload p = new Payload(codec, name, clockRate, bitRatePayload.bitRate);
if (offer)
{
offerPayloads.add(p);
}
else
{
answerPayloads.add(p);
}
}
}
}
}
}
}
}
catch (Exception e)
{
Log.error("Unable to parse SDP!", e);
}
}
public void parseInvite(Message message, Dialog d, ServerTransaction trans)
{
sipDialog = d;
inviteTransaction = trans;
parseSDP(new String(message.getRawContent()), true);
}
}
/**
* Copyright 2012 Voxbone SA/NV
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ifsoft.sip;
/**
* Class used to generate RFC2833 DTMF events
*
*/
public class DtmfEvent
{
byte event;
short duration;
byte [] rtpPacket = new byte[16];
public DtmfEvent(char dtmf, long startTime, byte [] ssrc)
{
if (dtmf == '*')
{
event = 10;
}
else if (dtmf == '#')
{
event = 11;
}
else if (dtmf == 'A')
{
event = 12;
}
else if (dtmf == 'B')
{
event = 13;
}
else if (dtmf == 'C')
{
event = 14;
}
else if (dtmf == 'D')
{
event = 15;
}
else
{
event = Byte.parseByte("" + dtmf);
}
RtpUtil.buildRtpHeader(rtpPacket, 101, (short) 0, startTime, ssrc);
rtpPacket[12] = this.event;
rtpPacket[13] = 10;
duration = 0;
}
public byte [] startPacket()
{
RtpUtil.setMarker(rtpPacket, true);
duration += 160;
rtpPacket[15] = (byte) (duration & 0xFF);
rtpPacket[14] = (byte) ((duration >> 8) & 0xFF);
return rtpPacket;
}
public byte [] continuationPacket()
{
RtpUtil.setMarker(rtpPacket, false);
duration += 160;
rtpPacket[15] = (byte) (duration & 0xFF);
rtpPacket[14] = (byte) ((duration >> 8) & 0xFF);
return rtpPacket;
}
public byte [] endPacket()
{
rtpPacket[13] |= (byte) (1<<7);
return rtpPacket;
}
}
/**
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ifsoft.sip;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* The class takes standard Http Authentication details and returns a response
* according to the MD5 algorithm
*
* @author Emil Ivov < emcho@dev.java.net >
* @version 1.0
*/
public class MessageDigestAlgorithm {
/**
* Calculates a response an http authentication response in accordance with
* rfc2617.
*
* @param algorithm MD5 or MD5-sess)
* @param username_value username_value (see rfc2617)
* @param realm_value realm_value
* @param passwd passwd
* @param nonce_value nonce_value
* @param cnonce_value cnonce_value
* @param Method method
* @param digest_uri_value uri_value
* @param entity_body entity_body
* @param qop_value qop
* @return a digest response as defined in rfc2617
* @throws NullPointerException in case of incorrectly null parameters.
*/
public static String calculateResponse(String algorithm, String username_value,
String realm_value, String passwd, String nonce_value,
String nc_value, String cnonce_value, String Method,
String digest_uri_value, String entity_body, String qop_value) {
if (username_value == null || realm_value == null || passwd == null
|| Method == null || digest_uri_value == null
|| nonce_value == null)
throw new NullPointerException(
"Null parameter to MessageDigestAlgorithm.calculateResponse()");
// The following follows closely the algorithm for generating a response
// digest as specified by rfc2617
String A1 = null;
if (algorithm == null || algorithm.trim().length() == 0
|| algorithm.trim().equalsIgnoreCase("MD5")) {
A1 = username_value + ":" + realm_value + ":" + passwd;
}
else {
if (cnonce_value == null || cnonce_value.length() == 0)
throw new NullPointerException(
"cnonce_value may not be absent for MD5-Sess algorithm.");
A1 = MessageDigestAlgorithm.H(username_value + ":" + realm_value + ":" + passwd) + ":"
+ nonce_value + ":" + cnonce_value;
}
String A2 = null;
if (qop_value == null || qop_value.trim().length() == 0
|| qop_value.trim().equalsIgnoreCase("auth")) {
A2 = Method + ":" + digest_uri_value;
}
else {
if (entity_body == null)
entity_body = "";
A2 = Method + ":" + digest_uri_value + ":" + MessageDigestAlgorithm.H(entity_body);
}
String request_digest = null;
if (cnonce_value != null && qop_value != null
&& (qop_value.equals("auth") || (qop_value.equals("auth-int")))) {
request_digest = MessageDigestAlgorithm.KD(MessageDigestAlgorithm.H(A1), nonce_value + ":" + nc_value + ":"
+ cnonce_value + ":" + qop_value + ":" + MessageDigestAlgorithm.H(A2));
}
else {
request_digest = MessageDigestAlgorithm.KD(MessageDigestAlgorithm.H(A1), nonce_value + ":" + MessageDigestAlgorithm.H(A2));
}
return request_digest;
}
/**
* Defined in rfc 2617 as H(data) = MD5(data);
*
* @param data data
* @return MD5(data)
*/
private static String H(String data) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
return MessageDigestAlgorithm.toHexString(digest.digest(data.getBytes()));
}
catch (NoSuchAlgorithmException ex) {
// shouldn't happen
return null;
}
}
/**
* Defined in rfc 2617 as KD(secret, data) = H(concat(secret, ":", data))
*
* @param data data
* @param secret secret
* @return H(concat(secret, ":", data));
*/
private static String KD(String secret, String data) {
return MessageDigestAlgorithm.H(secret + ":" + data);
}
// the following code was copied from the NIST-SIP instant
// messenger (its author is Olivier Deruelle). Thanks for making it public!
/**
* to hex converter
*/
private static final char[] toHex = {'0', '1', '2', '3', '4', '5', '6',
'7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
/**
* Converts b[] to hex string.
*
* @param b
* the bte array to convert
* @return a Hex representation of b.
*/
private static String toHexString(byte b[]) {
int pos = 0;
char[] c = new char[b.length * 2];
for (int i = 0; i < b.length; i++) {
c[pos++] = MessageDigestAlgorithm.toHex[(b[i] >> 4) & 0x0F];
c[pos++] = MessageDigestAlgorithm.toHex[b[i] & 0x0f];
}
return new String(c);
}
}
/**
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ifsoft.sip;
public class ProxyCredentials
{
private String xmppUserName = null;
private String userName = null;
private String userDisplay = null;
private char[] password = null;
private String authUserName = null;
private String realm = null;
private String proxy = null;
private String host = null;
private String name = null;
public void setUserName(String userName)
{
this.userName = userName;
}
public void setAuthUserName(String userName)
{
this.authUserName = userName;
}
public void setXmppUserName(String xmppUserName)
{
this.xmppUserName = xmppUserName;
}
public void setRealm(String realm)
{
this.realm = realm;
}
public void setProxy(String proxy)
{
this.proxy = proxy;
}
public void setHost(String host)
{
this.host = host;
}
public void setName(String name)
{
this.name = name;
}
public void setUserDisplay(String userDisplay)
{
this.userDisplay = userDisplay;
}
public void setPassword(char[] passwd) {
this.password = passwd;
}
public String getUserDisplay()
{
return userDisplay;
}
public String getUserName()
{
return this.userName;
}
public String getRealm()
{
return realm;
}
public String getProxy()
{
return proxy;
}
public String getHost()
{
return host;
}
public String getName()
{
return name;
}
public String getAuthUserName()
{
return this.authUserName;
}
public String getXmppUserName()
{
return this.xmppUserName;
}
public char[] getPassword() {
return password;
}
}
/**
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ifsoft.sip;
import java.io.IOException;
import java.text.*;
import java.util.*;
import javax.sip.*;
import javax.sip.address.*;
import javax.sip.header.*;
import javax.sip.message.*;
import org.slf4j.*;
import org.slf4j.Logger;
import org.jitsi.videobridge.openfire.PluginImpl;
class RegisterProcessing implements SipListener
{
private static final Logger Log = LoggerFactory.getLogger(RegisterProcessing.class);
private Request registerRequest = null;
private boolean isRegistered = false;
private Timer reRegisterTimer = new Timer();
private String registrar;
private String address;
private ProxyCredentials proxyCredentials;
private VideoBridgeSipListener.SipServerCallback sipServerCallback;
private String sipCallId;
int registrarPort = 5060;
int expires = 120;
private HeaderFactory headerFactory = SipService.getHeaderFactory();
private AddressFactory addressFactory = SipService.getAddressFactory();
private MessageFactory messageFactory = SipService.getMessageFactory();
private SipProvider sipProvider = SipService.getSipProvider();
public RegisterProcessing(String address, String registrar, ProxyCredentials proxyCredentials)
{
Log.info("Start registering...." + registrar);
this.registrar = registrar;
this.proxyCredentials = proxyCredentials;
this.address = address;
sipServerCallback = VideoBridgeSipListener.getSipServerCallback();
try {
register();
} catch (IOException e) {
Log.info(e.getMessage());
}
}
public void processRequest(RequestEvent requestReceivedEvent) {
Log.info("Request ignored: "
+ requestReceivedEvent.getRequest());
}
public void processResponse(ResponseEvent responseReceivedEvent) {
//Log.info("Registering response...." + sipCallId);
Response response = (Response)responseReceivedEvent.getResponse();
int statusCode = response.getStatusCode();
String method = ((CSeqHeader) response.getHeader(CSeqHeader.NAME)).getMethod();
Log.debug("Got response " + response);
if (statusCode == Response.OK) {
isRegistered = true;
Log.info("Voice bridge successfully registered with " + registrar + " for " + proxyCredentials.getXmppUserName());
PluginImpl.sipRegisterStatus = "Registered ok with " + proxyCredentials.getHost();
sipServerCallback.removeSipListener(sipCallId);
} else if (statusCode == Response.UNAUTHORIZED || statusCode == Response.PROXY_AUTHENTICATION_REQUIRED) {
if (method.equals(Request.REGISTER))
{
CSeqHeader cseq = (CSeqHeader) response.getHeader(CSeqHeader.NAME);
if (cseq.getSequenceNumber() < 2) {
ClientTransaction regTrans = SipService.handleChallenge(response, responseReceivedEvent.getClientTransaction(), proxyCredentials);
if (regTrans != null)
{
try {
regTrans.sendRequest();
} catch (Exception e) {
Log.info("Registration failed, cannot send transaction " + e);
PluginImpl.sipRegisterStatus = "Registration error " + e.toString();
}
} else {
Log.info("Registration failed, cannot create transaction");
PluginImpl.sipRegisterStatus = "Registration cannot create transaction";
}
} else {
Log.info("Registration failed " + responseReceivedEvent);
PluginImpl.sipRegisterStatus = "Registration failed";
}
}
} else {
Log.info("Unrecognized response: " + response);
}
}
public void processTimeout(TimeoutEvent timeoutEvent) {
Log.info("Timeout trying to register with " + registrar);
sipServerCallback.removeSipListener(sipCallId);
}
public void processDialogTerminated(DialogTerminatedEvent dte) {
Log.debug("processDialogTerminated called");
sipServerCallback.removeSipListener(sipCallId);
}
public void processTransactionTerminated(TransactionTerminatedEvent tte) {
Log.debug("processTransactionTerminated called");
sipServerCallback.removeSipListener(sipCallId);
}
public void processIOException(IOExceptionEvent ioee) {
Log.debug("processTransactionTerminated called");
sipServerCallback.removeSipListener(sipCallId);
}
private void register() throws IOException
{
Log.info("Registering with " + registrar);
FromHeader fromHeader = getFromHeader();
Address fromAddress = fromHeader.getAddress();
//Request URI
SipURI requestURI = null;
try {
requestURI = addressFactory.createSipURI(null, registrar);
} catch (ParseException e) {
throw new IOException("Bad registrar address:" + registrar + " " + e.getMessage());
}
//requestURI.setPort(registrarPort);
try {
requestURI.setTransportParam(
sipProvider.getListeningPoint().getTransport());
} catch (ParseException e) {
throw new IOException(sipProvider.getListeningPoint().getTransport() + " is not a valid transport! " + e.getMessage());
}
CallIdHeader callIdHeader = sipProvider.getNewCallId();
CSeqHeader cSeqHeader = null;
try {
cSeqHeader = headerFactory.createCSeqHeader(1, Request.REGISTER);
} catch (ParseException e) {
//Should never happen
throw new IOException("Corrupt Sip Stack " + e.getMessage());
} catch (InvalidArgumentException e) {
//Should never happen
throw new IOException("The application is corrupt " );
}
ToHeader toHeader = null;
try {
String proxyWorkAround =
System.getProperty("com.sun.mc.softphone.REGISTRAR_WORKAROUND");
if (proxyWorkAround != null &&
proxyWorkAround.toUpperCase().equals("TRUE")) {
SipURI toURI = (SipURI)(requestURI.clone());
toURI.setUser(System.getProperty("user.name"));
toHeader = headerFactory.createToHeader(
addressFactory.createAddress(toURI), null);
} else {
toHeader = headerFactory.createToHeader(fromAddress, null);
}
} catch (ParseException e) {
throw new IOException("Could not create a To header for address:"
+ fromHeader.getAddress() + " " + e.getMessage());
}
ArrayList viaHeaders = getLocalViaHeaders();
MaxForwardsHeader maxForwardsHeader = getMaxForwardsHeader();
Request request = null;
try {
request = messageFactory.createRequest(requestURI,
Request.REGISTER,
callIdHeader,
cSeqHeader, fromHeader, toHeader,
viaHeaders,
maxForwardsHeader);
} catch (ParseException e) {
throw new IOException("Could not create the register request! " + e.getMessage());
}
ExpiresHeader expHeader = null;
for (int retry = 0; retry < 2; retry++) {
try {
expHeader = headerFactory.createExpiresHeader(
expires);
} catch (InvalidArgumentException e) {
if (retry == 0) {
continue;
}
throw new IOException(
"Invalid registrations expiration parameter - "
+ expires + " " + e.getMessage());
}
}
request.addHeader(expHeader);
ContactHeader contactHeader = getRegistrationContactHeader();
request.addHeader(contactHeader);
try {
SipURI routeURI = (SipURI) addressFactory.createURI("sip:" + proxyCredentials.getProxy() + ";lr");
RouteHeader routeHeader = headerFactory.createRouteHeader(addressFactory.createAddress(routeURI));
request.addHeader(routeHeader);
} catch (Exception e) {
Log.error("Creating registration route error ", e);
}
ClientTransaction regTrans = null;
try {
regTrans = sipProvider.getNewClientTransaction(request);
} catch (TransactionUnavailableException e) {
throw new IOException("Could not create a register transaction!\n" + "Check that the Registrar address is correct! " + e.getMessage());
}
try {
sipCallId = callIdHeader.getCallId();
sipServerCallback.addSipListener(sipCallId, this);
registerRequest = request;
regTrans.sendRequest();
Log.debug("Sent register request " + registerRequest);
if (expires > 0) {
scheduleReRegistration();
}
} catch (Exception e) {
throw new IOException("Could not send out the register request! "+ e.getMessage());
}
this.registerRequest = request;
}
public void unregister() throws IOException
{
if (!isRegistered) {
return;
}
cancelPendingRegistrations();
isRegistered = false;
if (this.registerRequest == null) {
Log.info("Couldn't find the initial register request");
throw new IOException("Couldn't find the initial register request");
}
Request unregisterRequest = (Request) registerRequest.clone();
try {
unregisterRequest.getExpires().setExpires(0);
CSeqHeader cSeqHeader =
(CSeqHeader)unregisterRequest.getHeader(CSeqHeader.NAME);
//[issue 1] - increment registration cseq number
//reported by - Roberto Tealdi <roby.tea@tin.it>
cSeqHeader.setSequenceNumber(cSeqHeader.getSequenceNumber()+1);
} catch (InvalidArgumentException e) {
Log.info("Unable to set Expires Header " + e.getMessage());
return;
}
ClientTransaction unregisterTransaction = null;
try {
unregisterTransaction = sipProvider.getNewClientTransaction(
unregisterRequest);
} catch (TransactionUnavailableException e) {
throw new IOException("Unable to create a unregister transaction "
+ e.getMessage());
}
try {
unregisterTransaction.sendRequest();
} catch (SipException e) {
Log.info("Faied to send unregister request "
+ e.getMessage());
return;
}
}
public boolean isRegistered() {
return isRegistered;
}
private FromHeader fromHeader;
private FromHeader getFromHeader() throws IOException {
if (fromHeader != null) {
return fromHeader;
}
try {
SipURI fromURI = (SipURI) addressFactory.createURI("sip:" + proxyCredentials.getUserName() + "@" + registrar);
fromURI.setTransportParam(sipProvider.getListeningPoint().getTransport());
fromURI.setPort(sipProvider.getListeningPoint().getPort());
Address fromAddress = addressFactory.createAddress(fromURI);
fromAddress.setDisplayName(proxyCredentials.getUserDisplay());
fromHeader = headerFactory.createFromHeader(fromAddress, Integer.toString(hashCode()));
} catch (ParseException e) {
throw new IOException(
"A ParseException occurred while creating From Header! "
+ e.getMessage());
}
return fromHeader;
}
private ArrayList viaHeaders;
private ArrayList getLocalViaHeaders() throws IOException {
/*
* We can't keep a cached copy because the callers
* of this method change the viaHeaders. In particular
* a branch may be added which causes INVITES to fail.
*/
if (viaHeaders != null) {
return viaHeaders;
}
ListeningPoint lp = sipProvider.getListeningPoint();
viaHeaders = new ArrayList();
try {
String addr = lp.getIPAddress();
ViaHeader viaHeader = headerFactory.createViaHeader(
addr, lp.getPort(), lp.getTransport(), null);
viaHeader.setRPort();
viaHeaders.add(viaHeader);
return viaHeaders;
} catch (ParseException e) {
throw new IOException (
"A ParseException occurred while creating Via Headers! "
+ e.getMessage());
} catch (InvalidArgumentException e) {
throw new IOException(
"Unable to create a via header for port " + lp.getPort()
+ " " + e.getMessage());
}
}
private static final int MAX_FORWARDS = 70;
private MaxForwardsHeader maxForwardsHeader;
private MaxForwardsHeader getMaxForwardsHeader() throws IOException {
if (maxForwardsHeader != null) {
return maxForwardsHeader;
}
try {
maxForwardsHeader =
headerFactory.createMaxForwardsHeader(MAX_FORWARDS);
return maxForwardsHeader;
} catch (InvalidArgumentException e) {
throw new IOException(
"A problem occurred while creating MaxForwardsHeader "
+ e.getMessage());
}
}
private ContactHeader contactHeader;
private ContactHeader getRegistrationContactHeader() throws IOException {
if (contactHeader != null) {
return contactHeader;
}
try {
SipURI contactURI = (SipURI) addressFactory.createURI("sip:" + proxyCredentials.getUserName() + "@" + proxyCredentials.getHost());
contactURI.setTransportParam(
sipProvider.getListeningPoint().getTransport());
contactURI.setPort(sipProvider.getListeningPoint().getPort());
Address contactAddress = addressFactory.createAddress(contactURI);
contactAddress.setDisplayName(proxyCredentials.getUserDisplay());
contactHeader = headerFactory.createContactHeader(contactAddress);
return contactHeader;
} catch (ParseException e) {
throw new IOException(
"A ParseException occurred while creating From Header! "
+ " " + e.getMessage());
}
}
class ReRegisterTask extends TimerTask {
public ReRegisterTask() {
}
public void run() {
try {
if (isRegistered()) {
register();
}
} catch (IOException e) {
Log.info("Failed to reRegister " + e.getMessage());
}
}
}
private void cancelPendingRegistrations() {
reRegisterTimer.cancel();
reRegisterTimer = null;
reRegisterTimer = new Timer();
}
private void scheduleReRegistration() {
ReRegisterTask reRegisterTask = new ReRegisterTask();
reRegisterTimer.schedule(reRegisterTask, expires * 1000);
}
}
/**
* Copyright 2012 Voxbone SA/NV
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ifsoft.sip;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.spi.SelectorProvider;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.Properties;
import javax.sip.DialogState;
import org.slf4j.*;
import org.slf4j.Logger;
import de.javawi.jstun.attribute.MappedAddress;
import de.javawi.jstun.attribute.MessageAttribute;
import de.javawi.jstun.attribute.MessageAttributeException;
import de.javawi.jstun.attribute.MessageAttributeParsingException;
import de.javawi.jstun.attribute.SourceAddress;
import de.javawi.jstun.attribute.UnknownMessageAttributeException;
import de.javawi.jstun.attribute.Username;
import de.javawi.jstun.attribute.MessageAttributeInterface.MessageAttributeType;
import de.javawi.jstun.header.MessageHeader;
import de.javawi.jstun.header.MessageHeaderParsingException;
import de.javawi.jstun.header.MessageHeaderInterface.MessageHeaderType;
import de.javawi.jstun.util.Address;
import de.javawi.jstun.util.UtilityException;
/**
* This is the RTP Media relay thread, can be for video or audio.
* For the xmpp side it also takes care of the STUN signaling
*
*/
public class RtpRelay extends Thread
{
private static final Logger Log = LoggerFactory.getLogger(RtpRelay.class);
private static int RTP_MIN_PORT;
private static int RTP_MAX_PORT;
private static int nextPort = RTP_MIN_PORT;
private static boolean NAT_ENABLE = false;
private static boolean FIR_ENABLE = false;
private static boolean RTP_DEBUG = false;
private static boolean VUP_ENABLE = false;
private static int VUP_TIMER = 5000;
Timer retransTimer = new Timer("Stun Retransmit Thread");
private class StunTransmitter extends TimerTask
{
byte [] message;
SocketAddress dest;
DatagramChannel socket;
String remoteUser;
String localUser;
public StunTransmitter(byte [] message, String remoteUser, String localUser, SocketAddress dest, DatagramChannel socket)
{
this.message = message;
this.dest = dest;
this.socket = socket;
this.remoteUser = remoteUser;
this.localUser = localUser;
}
public void run()
{
if (RTP_DEBUG) {
Log.debug("[[" + cs.internalCallId + "]] Running RtpRelay::StunTransmitter ... : " + dest + " -- " + socket.socket().getLocalPort());
}
try
{
if (socket.isOpen())
{
socket.send(ByteBuffer.wrap(message), dest);
}
}
catch (IOException e)
{
Log.error("[[" + cs.internalCallId + "]] RtpRelay::StunTransmitter sending failed ==> " + dest + " -- " + socket.socket().getLocalPort());
}
}
public boolean cancel()
{
Log.debug("[[" + cs.internalCallId + "]] Cancelling RtpRelay::StunTransmitter ... : " + dest + " -- " + socket.socket().getLocalPort());
return super.cancel();
}
}
BlockingQueue<Character> dtmfQueue = new LinkedBlockingQueue<Character>();
private boolean video = false;
private class DtmfGenerator extends Thread
{
@Override
public void run()
{
while (sipSocket.isOpen())
{
char dtmf;
try
{
dtmf = dtmfQueue.take();
if (dtmf == '\0')
{
Log.debug("[[" + cs.internalCallId + "]] End flag detected in dtmf thread");
break;
}
long ts = 0;
synchronized (sipSocket)
{
ts = jabberTimestamp;
}
DtmfEvent de = new DtmfEvent(dtmf, ts, jabberSSRC);
Log.debug("[[" + cs.internalCallId + "]] Preparing to send dtmf " + dtmf);
synchronized (sipSocket)
{
ByteBuffer buffer = ByteBuffer.wrap(de.startPacket());
RtpUtil.setSequenceNumber(buffer.array(), ++jabberSequence);
try
{
sipSocket.send(buffer, sipDest);
}
catch (IOException e)
{
Log.error("Error sending dtmf start packet!", e);
}
}
for (int i = 0; i < 5; i++)
{
Thread.sleep(20);
synchronized (sipSocket)
{
ByteBuffer buffer = ByteBuffer.wrap(de.continuationPacket());
RtpUtil.setSequenceNumber(buffer.array(), ++jabberSequence);
try
{
sipSocket.send(buffer, sipDest);
}
catch (IOException e)
{
Log.error("Error sending dtmf continuation packet!", e);
}
}
}
for (int i = 0; i < 3; i++)
{
synchronized (sipSocket)
{
ByteBuffer buffer = ByteBuffer.wrap(de.endPacket());
RtpUtil.setSequenceNumber(buffer.array(), ++jabberSequence);
try
{
sipSocket.send(buffer, sipDest);
}
catch (IOException e)
{
Log.error("Error sending dtmf end packet!", e);
}
}
}
// Ensure at least 40 ms between dtmfs
Thread.sleep(40);
}
catch (InterruptedException e)
{
// do nothing
}
}
Log.debug("[[" + cs.internalCallId + "]] DtmfGenerator shut down");
}
}
public Hashtable<String, StunTransmitter> transmitters = new Hashtable<String, StunTransmitter>();
private class ID
{
byte [] id;
public ID(byte [] id)
{
this.id = id;
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(id);
return result;
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
final ID other = (ID) obj;
if (!Arrays.equals(id, other.id))
{
return false;
}
return true;
}
}
private DatagramChannel jabberSocket;
private DatagramChannel sipSocket;
private DatagramChannel jabberSocketRtcp;
private DatagramChannel sipSocketRtcp;
private SocketAddress jabberDest;
private SocketAddress jabberDestRtcp;
private SocketAddress sipDest;
private SocketAddress sipDestRtcp;
byte [] sipSSRC = null;
byte [] jabberSSRC = null;
long jabberTimestamp = 0;
short jabberSequence = 0;
long lastVUpate = 0;
int firSeq = 0;
private CallSession cs = null;
private DatagramChannel makeDatagramChannel(boolean any) throws IOException
{
DatagramChannel socket = DatagramChannel.open();
while (!socket.socket().isBound())
{
nextPort += 1;
if (nextPort > RTP_MAX_PORT)
{
nextPort = RTP_MIN_PORT;
}
Log.debug("[[" + cs.internalCallId + "]] trying to bind to port: " + nextPort);
try
{
if (!any)
{
socket.socket().bind(new InetSocketAddress(SipService.getLocalIP(), nextPort));
}
else
{
socket.socket().bind(new InetSocketAddress(nextPort));
}
}
catch (SocketException e)
{
Log.error("Unable to make RTP socket!", e);
}
}
return socket;
}
public void sendBind(String user, String me, String destIp, int destPort, boolean rtcp)
{
MessageHeader sendMH = new MessageHeader(MessageHeaderType.BindingRequest);
Username name = new Username(user + me);
try
{
sendMH.generateTransactionID();
}
catch (UtilityException e)
{
Log.error("Unable to make stun transaction id", e);
}
if(name.getUsername().length() > 0)
{
sendMH.addMessageAttribute(name);
}
try
{
byte [] data = sendMH.getBytes();
Log.debug("[[" + cs.internalCallId + "]] Sending: " + Arrays.toString(data));
DatagramChannel socket = null;
if (rtcp)
{
socket = jabberSocketRtcp;
}
else
{
socket = jabberSocket;
}
synchronized (transmitters)
{
if (jabberDest == null)
{
Log.debug("[[" + cs.internalCallId + "]] Sending Bind to: " + destIp + ":" + destPort);
StunTransmitter st = new StunTransmitter(data, user, me, new InetSocketAddress(destIp, destPort), socket);
String key = name.getUsername() + "_" + destIp + ":" + destPort;
if (transmitters.containsKey(key))
{
transmitters.get(key).cancel();
transmitters.remove(key);
}
transmitters.put(key, st);
Log.debug("[[" + cs.internalCallId + "]] RtpRelay::StunTransmitter scheduled (fast) [" + jabberSocket.socket().getLocalPort() + "][" + sipSocket.socket().getLocalPort() + "] ==> " + st.socket.socket().getLocalPort());
retransTimer.schedule(st, 50, 50);
}
}
}
catch (UtilityException e)
{
Log.error("Error in stun bind!", e);
}
}
public RtpRelay(CallSession cs, boolean video) throws IOException
{
this.video = video;
this.cs = cs;
jabberSocket = makeDatagramChannel(false);
jabberSocketRtcp = makeDatagramChannel(false);
sipSocket = makeDatagramChannel(false);
sipSocketRtcp = makeDatagramChannel(false);
Log.info("[[" + cs.internalCallId + "]] RtpRelay created [" + jabberSocket.socket().getLocalPort() + "][" + sipSocket.socket().getLocalPort() + "]");
if (!video)
{
(new DtmfGenerator()).start();
}
start();
}
protected void finalize() throws Throwable
{
Log.info("[[" + cs.internalCallId + "]] RtpRelay destroyed [" + jabberSocket.socket().getLocalPort() + "][" + sipSocket.socket().getLocalPort() + "]");
super.finalize(); // not necessary if extending Object.
}
private void processStun(SocketAddress src, byte [] origData, DatagramChannel socket)
{
try
{
MessageHeader receiveMH = MessageHeader.parseHeader(origData);
if (receiveMH.getType() == MessageHeaderType.BindingErrorResponse)
{
return;
}
receiveMH.parseAttributes(origData);
if (receiveMH.getType() == MessageHeaderType.BindingRequest)
{
MessageHeader sendMH = new MessageHeader(MessageHeaderType.BindingResponse);
sendMH.setTransactionID(receiveMH.getTransactionID());
// Mapped address attribute
MappedAddress ma = new MappedAddress();
ma.setAddress(new Address(((InetSocketAddress) src).getAddress().getAddress()));
ma.setPort(((InetSocketAddress) src).getPort());
sendMH.addMessageAttribute(ma);
SourceAddress sa = new SourceAddress();
sa.setAddress(new Address(SipService.getLocalIP()));
sa.setPort(socket.socket().getLocalPort());
sendMH.addMessageAttribute(sa);
MessageAttribute usernameMA = receiveMH.getMessageAttribute(MessageAttributeType.Username);
if (usernameMA != null) {
sendMH.addMessageAttribute(usernameMA);
}
byte [] data = sendMH.getBytes();
socket.send(ByteBuffer.wrap(data), src);
synchronized (transmitters)
{
boolean reflexive = true;
String me = null;
Username user = (Username) receiveMH.getMessageAttribute(MessageAttributeType.Username);
for (String key : transmitters.keySet())
{
if(user == null) break;
StunTransmitter st = transmitters.get(key);
if(user.getUsername().startsWith(st.localUser))
{
me = st.localUser;
if (RTP_DEBUG) {
Log.debug("Local User found " + me);
}
}
if(src.equals(st.dest))
{
reflexive = false;
break;
}
}
if(reflexive && me != null)
{
Log.info("Reflexive detected " + user.getUsername());
String remote = user.getUsername().substring(me.length());
Log.info("Remote = " + remote + " me = " + me);
sendBind(remote, me, ((InetSocketAddress) src).getAddress().getHostAddress(), ((InetSocketAddress) src).getPort(), socket == jabberSocket ? false : true);
}
}
}
else if (receiveMH.getType() == MessageHeaderType.BindingResponse)
{
synchronized (transmitters)
{
if ( (this.jabberDest == null && socket == jabberSocket)
|| (this.jabberDestRtcp == null && socket == jabberSocketRtcp))
{
if (socket == jabberSocket)
{
this.jabberDest = src;
}
else
{
this.jabberDestRtcp = src;
}
Username user = (Username) receiveMH.getMessageAttribute(MessageAttributeType.Username);
StunTransmitter newTimer = null;
String newKey = null;
String desired = user.getUsername() + "_" + ((InetSocketAddress)src).getAddress().getHostAddress() + ":" + ((InetSocketAddress)src).getPort();
for (String key : transmitters.keySet())
{
StunTransmitter st = transmitters.get(key);
if (st.socket == socket)
{
try
{
st.cancel();
}
catch (Exception e)
{
}
Log.debug("[[" + cs.internalCallId + "]] Comparing " + key + " to " + desired);
if (key.equals(desired))
{
newKey = key;
newTimer = new StunTransmitter(st.message, st.remoteUser, st.localUser, st.dest, st.socket);
}
}
}
if (newTimer != null && newKey != null)
{
Log.debug("[[" + cs.internalCallId + "]] ++++++++++++++++ slowing retransmission " + newKey + " ++++++++++++++");
transmitters.put(newKey, newTimer);
Log.debug("[[" + cs.internalCallId + "]] RtpRelay::StunTransmitter scheduled (slow) [" + jabberSocket.socket().getLocalPort() + "][" + sipSocket.socket().getLocalPort() + "] ==> " + newTimer.socket.socket().getLocalPort());
retransTimer.schedule(newTimer, 100, 5000);
}
}
}
}
}
catch (MessageHeaderParsingException e)
{
// ignore (problem occurred in stun code)
}
catch (UnknownMessageAttributeException e)
{
// ignore (problem occurred in stun code)
}
catch (MessageAttributeParsingException e)
{
// ignore (problem occurred in stun code)
}
catch (UtilityException e)
{
Log.error("Error in processStun", e);
}
catch (MessageAttributeException e)
{
Log.error("Error in processStun", e);
}
catch (IOException e)
{
Log.error("Error in processStun", e);
}
catch (ArrayIndexOutOfBoundsException e)
{
// ignore (problem occurred in stun code)
}
catch (Exception e)
{
Log.error("Error in processStun", e);
}
}
/*
* Experimental. I believe that google uses special rtcp packets to send fast video updates - this is an unsuccessful
* attempt at implementing this feature.
*/
public void sendFIR()
{
if (FIR_ENABLE) {
byte [] buffer = new byte[40];
RtpUtil.buildFIR(buffer, firSeq++, sipSSRC, jabberSSRC);
try
{
jabberSocketRtcp.send(ByteBuffer.wrap(buffer), jabberDestRtcp);
}
catch (Exception e)
{
Log.error("Error sending FIR packet!", e);
}
}
}
public void sendSipDTMF(char dtmf)
{
switch (dtmf)
{
case '0' :
case '1' :
case '2' :
case '3' :
case '4' :
case '5' :
case '6' :
case '7' :
case '8' :
case '9' :
case '*' :
case '#' :
case 'A' :
case 'B' :
case 'C' :
case 'D' :
Log.debug("[[" + cs.internalCallId + "]] Logging dtmf " + dtmf + " for generation");
try
{
dtmfQueue.put(dtmf);
}
catch (InterruptedException e)
{
Log.error("Interrupted why queueing a dtmf", e);
}
break;
default :
Log.warn("[[" + cs.internalCallId + "]] Ignoring invalid dtmf " + dtmf);
}
}
public void run()
{
Log.info("[[" + cs.internalCallId + "]] RtpRelay Thread Started");
Selector sel = null;
try
{
sel = SelectorProvider.provider().openSelector();
sipSocket.configureBlocking(false);
sipSocketRtcp.configureBlocking(false);
jabberSocket.configureBlocking(false);
jabberSocketRtcp.configureBlocking(false);
sipSocket.register(sel, SelectionKey.OP_READ);
jabberSocket.register(sel, SelectionKey.OP_READ);
sipSocketRtcp.register(sel, SelectionKey.OP_READ);
jabberSocketRtcp.register(sel, SelectionKey.OP_READ);
ByteBuffer inputBuffer = ByteBuffer.allocate(20000);
ByteBuffer outputBuffer = ByteBuffer.allocate(20000);
byte [] outputBytes = new byte[20000];
while (sipSocket.isOpen())
{
if (sel.select(1000) >= 0)
{
Iterator<SelectionKey> itr = sel.selectedKeys().iterator();
while (itr.hasNext())
{
SelectionKey key = itr.next();
itr.remove();
if (key.isValid() && key.isReadable())
{
DatagramChannel socket = (DatagramChannel) key.channel();
inputBuffer.clear();
if (!socket.isOpen())
{
Log.error("[[" + cs.internalCallId + "]] Socket is not open ... ignoring");
continue;
}
SocketAddress src = socket.receive(inputBuffer);
if (src == null)
{
Log.error("[[" + cs.internalCallId + "]] Src is null ... ignoring");
continue;
}
if ((inputBuffer.get(0) & 0x80) != 0)
{
DatagramChannel destSocket;
SocketAddress destAddr;
inputBuffer.flip();
if (socket == sipSocket)
{
if(NAT_ENABLE && !src.equals(sipDest))
{
Log.debug("Nat detected, updating sip rtp destination from " + sipDest + " to " + src);
sipDest = src;
}
destSocket = jabberSocket;
destAddr = jabberDest;
if (this.sipSSRC == null)
{
this.sipSSRC = RtpUtil.getSSRC(inputBuffer.array());
}
if (destSocket != null && destAddr != null)
{
destSocket.send(inputBuffer, destAddr);
}
}
else if (socket == sipSocketRtcp)
{
destSocket = jabberSocketRtcp;
destAddr = jabberDestRtcp;
if(NAT_ENABLE && !src.equals(sipDestRtcp))
{
Log.debug("Nat detected, updating sip rtcp destination from " + sipDestRtcp + " to " + src);
sipDestRtcp = src;
}
if (destSocket != null && destAddr != null)
{
destSocket.send(inputBuffer, destAddr);
}
}
else if (socket == jabberSocketRtcp)
{
destSocket = sipSocketRtcp;
destAddr = sipDestRtcp;
if (destSocket != null && destAddr != null)
{
destSocket.send(inputBuffer, destAddr);
}
if (video && VUP_ENABLE && System.currentTimeMillis() - lastVUpate > VUP_TIMER
&& (this.cs.sipDialog.getState() != null && this.cs.sipDialog.getState() != DialogState.EARLY) )
{
SipService.sendVideoUpdate(this.cs);
lastVUpate = System.currentTimeMillis();
}
}
else
{
destSocket = sipSocket;
destAddr = sipDest;
if (this.jabberSSRC == null)
{
this.jabberSSRC = RtpUtil.getSSRC(inputBuffer.array());
}
synchronized (destSocket)
{
if (destSocket != null && destAddr != null)
{
if (!video)
{
this.jabberTimestamp = RtpUtil.getTimeStamp(inputBuffer.array());
RtpUtil.setSequenceNumber(inputBuffer.array(), ++this.jabberSequence);
if (destSocket.isOpen())
{
destSocket.send(inputBuffer, destAddr);
}
}
else
{
// TODO: google uses H264 SVC, the rest of the world uses AVC, so convert
// google now supports AVC so we should adapt to that
/*
int length = RtpUtil.filterSVC(inputBuffer.array(), outputBytes, inputBuffer.remaining());
outputBuffer.clear();
outputBuffer.put(outputBytes, 0, length);
outputBuffer.flip();
*/
if (destSocket.isOpen())
{
destSocket.send(inputBuffer, destAddr);
}
}
}
}
}
}
else
{
this.processStun(src, inputBuffer.array(), socket);
}
}
}
}
}
}
catch (IOException e)
{
Log.error("Error in RTP relay thread!", e);
}
catch (Exception e)
{
Log.error("Error in RTP relay thread!", e);
}
finally
{
if (sel != null)
{
try
{
sel.close();
}
catch (IOException e)
{
// ignore (we're dead anyhow)
}
}
}
Log.info("[[" + cs.internalCallId + "]] RtpRelay Thread Stopped");
}
public void setSipDest(String host, int port)
{
this.sipDest = new InetSocketAddress(host, port);
this.sipDestRtcp = new InetSocketAddress(host, port + 1);
}
public int getSipPort()
{
return this.sipSocket.socket().getLocalPort();
}
public int getSipRtcpPort()
{
return this.sipSocketRtcp.socket().getLocalPort();
}
public int getJabberPort()
{
return this.jabberSocket.socket().getLocalPort();
}
public int getJabberRtcpPort()
{
return this.jabberSocketRtcp.socket().getLocalPort();
}
public void shutdown()
{
Log.debug("[[" + cs.internalCallId + "]] Shutdown of rtp thread requested");
synchronized (transmitters)
{
Log.debug("[[" + cs.internalCallId + "]] number of transmitters : " + transmitters.size());
for (String key : transmitters.keySet())
{
Log.debug("[[" + cs.internalCallId + "]] cancelling transmitter : " + key);
transmitters.get(key).cancel();
}
}
try
{
dtmfQueue.put('\0');
}
catch (InterruptedException e)
{
Log.error("unable to queue shutdown signal!", e);
}
try
{
sipSocket.close();
}
catch (IOException e)
{
Log.error("unable to close sip-side rtp socket!", e);
}
try
{
jabberSocket.close();
}
catch (IOException e)
{
Log.error("unable to close xmpp-side rtp socket!", e);
}
try
{
sipSocketRtcp.close();
}
catch (IOException e)
{
Log.error("Error in rtcp shutdown", e);
}
try
{
jabberSocketRtcp.close();
}
catch (IOException e)
{
Log.error("Error in rtcp shutdown", e);
}
}
public static void configure(Properties properties)
{
RTP_MIN_PORT = Integer.parseInt(properties.getProperty("com.voxbone.kelpie.rtp.min_port", "8000"));
RTP_MAX_PORT = Integer.parseInt(properties.getProperty("com.voxbone.kelpie.rtp.max_port", "10000"));
NAT_ENABLE = Boolean.parseBoolean(properties.getProperty("com.voxbone.kelpie.rtp.nat_enable", "false"));
FIR_ENABLE = Boolean.parseBoolean(properties.getProperty("com.voxbone.kelpie.rtp.fir_enable", "false"));
RTP_DEBUG = Boolean.parseBoolean(properties.getProperty("com.voxbone.kelpie.rtp.debug", "false"));
VUP_ENABLE = Boolean.parseBoolean(properties.getProperty("com.voxbone.kelpie.rtp.vup_enable", "true"));
VUP_TIMER = Integer.parseInt(properties.getProperty("com.voxbone.kelpie.rtp.vup_timer", "5000"));
nextPort = RTP_MIN_PORT;
}
}
/**
* Copyright 2012 Voxbone SA/NV
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ifsoft.sip;
/**
* Utility functions for Parsing information from RTP packets
*
*/
public class RtpUtil
{
/*
The RTP header has the following format:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X| CC |M| PT | sequence number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timestamp |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| synchronization source (SSRC) identifier |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
| contributing source (CSRC) identifiers |
| .... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
public static void buildRtpHeader(byte [] buffer, int payload, short seq, long timestamp, byte [] ssrc)
{
buffer[0] = (byte) 0x80;
buffer[1] = (byte) payload;
setSequenceNumber(buffer, seq);
setTimeStamp(buffer, timestamp);
setSSRC(buffer, ssrc);
}
/*
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P| FMT | PT | length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SSRC of packet sender |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SSRC of media source |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
: Feedback Control Information (FCI) :
: :
Name | Value | Brief Description
----------+-------+------------------------------------
RTPFB | 205 | Transport layer FB message
PSFB | 206 | Payload-specific FB message
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SSRC |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Seq nr. | Reserved |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
public static void buildFIR(byte [] buffer, int seq, byte [] senderSsrc, byte [] destSsrc)
{
byte [] sr ={(byte)0x80,(byte)0xc9,(byte)0x00,(byte)0x01,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x01,(byte)0x81,(byte)0xca,(byte)0x00,(byte)0x02,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x01,(byte)0x01,(byte)0x01,(byte)0x31,(byte)0x00};
System.arraycopy(sr, 0, buffer, 0, sr.length);
buffer[sr.length+0] = (byte) 0x84;
buffer[sr.length+1] = (byte) 206;
buffer[sr.length+2] = 0;
buffer[sr.length+3] = 4;
// packet sender 0001
//System.arraycopy(senderSsrc, 0, buffer, sr.length+4, 4);
buffer[sr.length +4+3] = (byte)0x01;
// media source keeps 0
//System.arraycopy(senderSsrc, 0, buffer, sr.length+8, 4);
System.arraycopy(destSsrc, 0, buffer, sr.length+12, 4);
buffer[sr.length+16] = (byte) seq;
}
public static int getFIRSequence(byte [] buffer, int start, int end)
{
if(start+1 >= end)
{
// no FIR
return -1;
}
if(buffer[start+1] == (byte)206 && (buffer[start]&0x0F) == (byte)0x04)
{
// fir found!
return (int)buffer[start+16]&0xFF;
}
else
{
// not fir, check next rtcp
int length = getShort(buffer, start+2)*4 + 4;
return getFIRSequence(buffer, start+length, end);
}
}
public static short getSequenceNumber(byte [] buffer)
{
return getShort(buffer, 2);
}
public static void setSequenceNumber(byte [] buffer, short seq)
{
buffer[3] = (byte) (seq & 0xFF);
buffer[2] = (byte) ((seq >> 8) & 0xFF);
}
public static byte [] getSSRC(byte [] buffer)
{
byte ssrc[] = new byte[4];
System.arraycopy(buffer, 8, ssrc, 0, 4);
return ssrc;
}
public static void setSSRC(byte [] buffer, byte [] ssrc)
{
System.arraycopy(ssrc, 0, buffer, 8, 4);
}
public static long getTimeStamp(byte [] buffer)
{
long timestamp = 0;
timestamp = 0xFF & buffer[4];
timestamp = timestamp << 8;
timestamp |= 0xFF & buffer[5];
timestamp = timestamp << 8;
timestamp |= 0xFF & buffer[6];
timestamp = timestamp << 8;
timestamp |= 0xFF & buffer[7];
return timestamp;
}
public static void setTimeStamp(byte [] buffer, long timestamp)
{
buffer[7] = (byte) (timestamp & 0xFF);
buffer[6] = (byte) ((timestamp >> 8) & 0xFF);
buffer[5] = (byte) ((timestamp >> 16) & 0xFF);
buffer[4] = (byte) ((timestamp >> 24) & 0xFF);
}
public static void setMarker(byte [] buffer, boolean set)
{
if (set)
{
buffer[1] |= (0x1 << 7);
}
else
{
buffer[1] &= (0x7F);
}
}
public static int getNALType(byte [] input, int start)
{
return (int) (0x1F & input[start]);
}
public static short getShort(byte [] buffer, int offset)
{
int seq = 0;
seq = 0xFF & buffer[offset];
seq = seq << 8;
seq |= 0xFF & buffer[offset + 1];
return (short) seq;
}
public static int filterSVC(byte [] input, byte [] output, int input_size)
{
int output_offset = 12;
int input_offset = 12 + 2 + 2 + 8;
System.arraycopy(input, 0, output, 0, 12);
output[0] &= 0xEF;
int nalType = getNALType(input, input_offset);
if (nalType == 24)
{
output[output_offset] = input[input_offset];
input_offset += 1;
output_offset += 1;
while (input_offset < input_size)
{
short length = getShort(input, input_offset);
nalType = getNALType(input, input_offset + 2);
if (nalType != 30 && nalType != 14 && nalType != 20)
{
System.arraycopy(input, input_offset, output, output_offset, length + 2);
input_offset += length + 2;
output_offset += length + 2;
}
else
{
input_offset += length + 2;
}
}
}
else if (nalType != 30 && nalType != 14 && nalType != 20)
{
System.arraycopy(input, input_offset, output, output_offset, input_size - input_offset);
output_offset += input_size - input_offset;
}
return output_offset;
}
}
/**
* Copyright 2012 Voxbone SA/NV
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ifsoft.sip;
import org.jivesoftware.util.*;
import java.text.*;
import java.util.*;
import javax.sip.*;
import javax.sip.address.*;
import javax.sip.header.*;
import javax.sip.message.*;
import org.slf4j.*;
import org.slf4j.Logger;
/**
* Functions useful for building SIP messages
*
*/
public class SipService
{
private static final Logger Log = LoggerFactory.getLogger(SipService.class);
public static AddressFactory addressFactory;
public static HeaderFactory headerFactory;
public static MessageFactory messageFactory;
public static VideoBridgeSipListener sipListener;
public static SipProvider sipProvider;
public static ProxyCredentials sipAccount;
private SipStack sipStack;
private SipFactory sipFactory;
private int localport;
private static String localip;
private static String remoteip;
private static String agentName;
private static String clientVersion;
public SipService(Properties properties)
{
localip = properties.getProperty("com.voxbone.kelpie.ip");
localport = Integer.parseInt(properties.getProperty("com.voxbone.kelpie.sip_port", "5070"));
remoteip = properties.getProperty("com.voxbone.kelpie.sip_gateway");
sipListener = new VideoBridgeSipListener(properties.getProperty("com.voxbone.kelpie.hostname"));
agentName = properties.getProperty("com.voxbone.kelpie.sip.user-agent", "Jitsi Videobridge");
clientVersion = properties.getProperty("com.voxbone.kelpie.version");
String nameOS = System.getProperty("os.name");
String versOS = System.getProperty("os.version");
agentName = agentName + " " + clientVersion + " (" + nameOS + "/" + versOS + ")";
sipFactory = SipFactory.getInstance();
sipFactory.setPathName("gov.nist");
properties.setProperty("javax.sip.STACK_NAME", "JitsiVideobridgeStack");
try
{
sipStack = sipFactory.createSipStack(properties);
headerFactory = sipFactory.createHeaderFactory();
addressFactory = sipFactory.createAddressFactory();
messageFactory = sipFactory.createMessageFactory();
}
catch (PeerUnavailableException e)
{
Log.error(e.toString(), e);
}
try
{
ListeningPoint udp = sipStack.createListeningPoint(localip, localport, "udp");
sipProvider = sipStack.createSipProvider(udp);
sipProvider.setAutomaticDialogSupportEnabled(false);
sipProvider.addSipListener(sipListener);
registerWithDefaultProxy();
}
catch (TransportNotSupportedException e)
{
Log.error(e.toString(), e);
}
catch (InvalidArgumentException e)
{
Log.error(e.toString(), e);
}
catch (ObjectInUseException e)
{
Log.error(e.toString(), e);
}
catch (TooManyListenersException e)
{
Log.error(e.toString(), e);
}
}
public static MessageFactory getMessageFactory()
{
return messageFactory;
}
public static SipProvider getSipProvider()
{
return sipProvider;
}
public static AddressFactory getAddressFactory()
{
return addressFactory;
}
public static HeaderFactory getHeaderFactory()
{
return headerFactory;
}
public static String getRemoteIP()
{
return remoteip;
}
public static String getLocalIP()
{
return localip;
}
private void registerWithDefaultProxy()
{
sipAccount = new ProxyCredentials();
try {
String name = JiveGlobals.getProperty("voicebridge.default.proxy.name", "admin");
String username = JiveGlobals.getProperty("voicebridge.default.proxy.username", name);
String sipusername = JiveGlobals.getProperty("voicebridge.default.proxy.sipusername", name);
String authusername = JiveGlobals.getProperty("voicebridge.default.proxy.sipauthuser", null);
String displayname = JiveGlobals.getProperty("voicebridge.default.proxy.sipdisplayname", name);
String password = JiveGlobals.getProperty("voicebridge.default.proxy.sippassword", name);
String server = JiveGlobals.getProperty("voicebridge.default.proxy.sipserver", null);
String stunServer = JiveGlobals.getProperty("voicebridge.default.proxy.stunserver", localip);
String stunPort = JiveGlobals.getProperty("voicebridge.default.proxy.stunport");
String voicemail = JiveGlobals.getProperty("voicebridge.default.proxy.voicemail", name);
String outboundproxy = JiveGlobals.getProperty("voicebridge.default.proxy.outboundproxy", localip);
sipAccount.setName(name);
sipAccount.setXmppUserName(username);
sipAccount.setUserName(sipusername);
sipAccount.setAuthUserName(authusername);
sipAccount.setUserDisplay(displayname);
sipAccount.setPassword(password.toCharArray());
sipAccount.setHost(server);
sipAccount.setProxy(outboundproxy);
sipAccount.setRealm(server);
if (authusername != null && server != null)
{
Log.info(String.format("VoiceBridge adding SIP registration: %s with user %s host %s", sipAccount.getXmppUserName(), sipAccount.getUserName(), sipAccount.getHost()));
new RegisterProcessing(localip, server, sipAccount);
}
} catch (Exception e) {
Log.error("registerWithDefaultProxy", e);
}
}
public static boolean acceptCall(CallSession cs)
{
try
{
Request req = cs.inviteTransaction.getRequest();
Response resp = messageFactory.createResponse(Response.OK, cs.inviteTransaction.getRequest());
ContentTypeHeader cth = headerFactory.createContentTypeHeader("application", "sdp");
Object sdp = cs.buildSDP(false);
ToHeader th = (ToHeader) req.getHeader("To");
String dest = ((SipURI) th.getAddress().getURI()).getUser();
ListeningPoint lp = sipProvider.getListeningPoint(ListeningPoint.UDP);
Address localAddress = addressFactory.createAddress("sip:" + dest + "@" + lp.getIPAddress() + ":" + lp.getPort());
ContactHeader ch = headerFactory.createContactHeader(localAddress);
AllowHeader allowHeader = headerFactory.createAllowHeader("INVITE, ACK, CANCEL, OPTIONS, BYE, UPDATE, NOTIFY, MESSAGE, SUBSCRIBE, INFO");
resp.addHeader(allowHeader);
resp.addHeader(ch);
UserAgentHeader userAgent = (UserAgentHeader) headerFactory.createHeader(UserAgentHeader.NAME, agentName);
resp.setHeader(userAgent);
resp.setContent(sdp, cth);
cs.inviteTransaction.sendResponse(resp);
}
catch (ParseException e)
{
Log.error("Error accepting call", e);
return false;
}
catch (SipException e)
{
Log.error("Error accepting call", e);
return false;
}
catch (InvalidArgumentException e)
{
Log.error("Error accepting call", e);
return false;
}
return true;
}
public static boolean sendBye(CallSession cs)
{
Request req;
try
{
if (cs.inviteOutTransaction != null && (cs.sipDialog.getState() == null || cs.sipDialog.getState() == DialogState.EARLY))
{
req = cs.inviteOutTransaction.createCancel();
ClientTransaction t = sipProvider.getNewClientTransaction(req);
t.sendRequest();
cs.sendBye();
return false;
}
else
{
req = cs.sipDialog.createRequest(Request.BYE);
ClientTransaction t = sipProvider.getNewClientTransaction(req);
cs.sipDialog.sendRequest(t);
cs.sendBye();
}
}
catch (SipException e)
{
Log.error("Error sending BYE", e);
}
return true;
}
public static boolean sendReject(CallSession cs)
{
try
{
Request req = cs.inviteTransaction.getRequest();
Response resp = messageFactory.createResponse(Response.TEMPORARILY_UNAVAILABLE, cs.inviteTransaction.getRequest());
ToHeader th = (ToHeader) req.getHeader("To");
String dest = ((SipURI) th.getAddress().getURI()).getUser();
ListeningPoint lp = sipProvider.getListeningPoint(ListeningPoint.UDP);
Address localAddress = addressFactory.createAddress("sip:" + dest + "@" + lp.getIPAddress() + ":" + lp.getPort());
ContactHeader ch = headerFactory.createContactHeader(localAddress);
resp.addHeader(ch);
cs.inviteTransaction.sendResponse(resp);
}
catch (Exception e)
{
Log.error("Error sending Reject", e);
}
return true;
}
public static boolean sendDTMFinfo(CallSession cs, char dtmf, int dtmfl)
{
Request req;
try
{
Log.debug("Sending SIP INFO DTMF - Signal: " + dtmf + " Duration:" + dtmfl);
ContentTypeHeader cth = headerFactory.createContentTypeHeader("application", "dtmf-relay");
String body = "Signal=" + dtmf + "\r\nDuration="+dtmfl;
req = cs.sipDialog.createRequest(Request.INFO);
ClientTransaction t = sipProvider.getNewClientTransaction(req);
req.setContent(body, cth);
cs.sipDialog.sendRequest(t);
}
catch (SipException e)
{
Log.error("Error sending DTMF INFO", e);
}
catch (ParseException e)
{
Log.error("Error sending DTMF INFO", e);
}
return true;
}
public static boolean sendVideoUpdate(CallSession cs)
{
Request req;
try
{
ContentTypeHeader cth = headerFactory.createContentTypeHeader("application", "media_control+xml");
String body = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
+ "<media_control>"
+ "<vc_primitive>"
+ "<to_encoder>"
+ "<picture_fast_update>"
+ "</picture_fast_update>"
+ "</to_encoder>"
+ "</vc_primitive>"
+ "</media_control>";
req = cs.sipDialog.createRequest(Request.INFO);
ClientTransaction t = sipProvider.getNewClientTransaction(req);
req.setContent(body, cth);
cs.sipDialog.sendRequest(t);
}
catch (SipException e)
{
Log.error("Error sending FVR INFO", e);
}
catch (ParseException e)
{
Log.error("Error sending FVR INFO", e);
}
return true;
}
public static boolean sendInvite(CallSession cs)
{
FromHeader fromHeader = null;
ToHeader toHeader = null;
URI requestURI = null;
URI fromURI = null;
try
{
ListeningPoint lp = sipProvider.getListeningPoint(ListeningPoint.UDP);
localip = lp.getIPAddress();
requestURI = addressFactory.createURI(cs.jabberRemote);
toHeader = headerFactory.createToHeader(addressFactory.createAddress(requestURI), null);
fromURI = addressFactory.createURI(cs.jabberLocal);
fromHeader = headerFactory.createFromHeader(addressFactory.createAddress(fromURI), null);
int tag = (int) (Math.random() * 100000);
fromHeader.setTag(Integer.toString(tag));
ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
ViaHeader viaHeader = null;
viaHeader = headerFactory.createViaHeader(lp.getIPAddress(), lp.getPort(), lp.getTransport(), null);
viaHeaders.add(viaHeader);
CallIdHeader callIdHeader = sipProvider.getNewCallId();
CSeqHeader cSeqHeader = headerFactory.createCSeqHeader(1L, Request.INVITE);
MaxForwardsHeader maxForwards = headerFactory.createMaxForwardsHeader(70);
ContentTypeHeader contentTypeHeader = headerFactory.createContentTypeHeader("application", "sdp");
Request request = messageFactory.createRequest(requestURI, Request.INVITE, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards, contentTypeHeader, cs.buildSDP(true));
Address localAddress = addressFactory.createAddress(requestURI);
ContactHeader ch = headerFactory.createContactHeader(localAddress);
request.addHeader(ch);
AllowHeader allowHeader = headerFactory.createAllowHeader("INVITE, ACK, CANCEL, OPTIONS, BYE, UPDATE, NOTIFY, MESSAGE, SUBSCRIBE, INFO");
request.addHeader(allowHeader);
UserAgentHeader userAgent = (UserAgentHeader) headerFactory.createHeader(UserAgentHeader.NAME, agentName);
request.setHeader(userAgent);
ClientTransaction t = sipProvider.getNewClientTransaction(request);
//t.setApplicationData(new ResponseInfo(listener, transaction));
Dialog d = SipService.sipProvider.getNewDialog(t);
cs.sipDialog = d;
d.setApplicationData(cs);
t.setApplicationData(cs);
cs.inviteOutTransaction = t;
t.sendRequest();
return true;
}
catch (ParseException e)
{
Log.error("Error on SIPTransmitter:deliverMessage", e);
}
catch (InvalidArgumentException e)
{
Log.error("Error on SIPTransmitter:deliverMessage", e);
}
catch (TransactionUnavailableException e)
{
Log.error("Error on SIPTransmitter:deliverMessage", e);
}
catch (SipException e)
{
Log.error("Error on SIPTransmitter:deliverMessage", e);
}
return false;
}
public static boolean sendMessageMessage(String to, String from, String body)
{
FromHeader fromHeader = null;
ToHeader toHeader = null;
URI requestURI = null;
URI fromURI = null;
try
{
requestURI = addressFactory.createURI(to);
toHeader = headerFactory.createToHeader(addressFactory.createAddress(requestURI), null);
fromURI = addressFactory.createURI(from);
fromHeader = headerFactory.createFromHeader(addressFactory.createAddress(fromURI), null);
int tag = (int) (Math.random() * 100000);
fromHeader.setTag(Integer.toString(tag));
ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
ViaHeader viaHeader = null;
ListeningPoint lp = sipProvider.getListeningPoint(ListeningPoint.UDP);
viaHeader = headerFactory.createViaHeader(lp.getIPAddress(), lp.getPort(), lp.getTransport(), null);
viaHeaders.add(viaHeader);
CallIdHeader callIdHeader = sipProvider.getNewCallId();
CSeqHeader cSeqHeader = headerFactory.createCSeqHeader(1L, Request.MESSAGE);
MaxForwardsHeader maxForwards = headerFactory.createMaxForwardsHeader(70);
ContentTypeHeader contentTypeHeader = headerFactory.createContentTypeHeader("text", "plain");
Request request = messageFactory.createRequest(requestURI, "MESSAGE", callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards, contentTypeHeader, body);
ClientTransaction t = sipProvider.getNewClientTransaction(request);
t.sendRequest();
return true;
}
catch (ParseException e)
{
Log.error("Error on SIPTransmitter:deliverMessage", e);
}
catch (InvalidArgumentException e)
{
Log.error("Error on SIPTransmitter:deliverMessage", e);
}
catch (TransactionUnavailableException e)
{
Log.error("Error on SIPTransmitter:deliverMessage", e);
}
catch (SipException e)
{
Log.error("Error on SIPTransmitter:deliverMessage", e);
}
return false;
}
public synchronized static ClientTransaction handleChallenge(Response challenge, ClientTransaction challengedTransaction, ProxyCredentials proxyCredentials)
{
try {
String branchID = challengedTransaction.getBranchId();
Request challengedRequest = challengedTransaction.getRequest();
Request reoriginatedRequest = (Request) challengedRequest.clone();
ListIterator authHeaders = null;
if (challenge == null || reoriginatedRequest == null)
throw new NullPointerException("A null argument was passed to handle challenge.");
// CallIdHeader callId =
// (CallIdHeader)challenge.getHeader(CallIdHeader.NAME);
if (challenge.getStatusCode() == Response.UNAUTHORIZED)
authHeaders = challenge.getHeaders(WWWAuthenticateHeader.NAME);
else if (challenge.getStatusCode() == Response.PROXY_AUTHENTICATION_REQUIRED)
authHeaders = challenge.getHeaders(ProxyAuthenticateHeader.NAME);
if (authHeaders == null)
throw new SecurityException("Could not find WWWAuthenticate or ProxyAuthenticate headers");
// Remove all authorization headers from the request (we'll re-add
// them
// from cache)
reoriginatedRequest.removeHeader(AuthorizationHeader.NAME);
reoriginatedRequest.removeHeader(ProxyAuthorizationHeader.NAME);
reoriginatedRequest.removeHeader(ViaHeader.NAME);
// rfc 3261 says that the cseq header should be augmented for the
// new
// request. do it here so that the new dialog (created together with
// the new client transaction) takes it into account.
// Bug report - Fredrik Wickstrom
CSeqHeader cSeq = (CSeqHeader) reoriginatedRequest.getHeader((CSeqHeader.NAME));
cSeq.setSequenceNumber(cSeq.getSequenceNumber() + 1);
reoriginatedRequest.setHeader(cSeq);
ClientTransaction retryTran = sipProvider.getNewClientTransaction(reoriginatedRequest);
WWWAuthenticateHeader authHeader = null;
while (authHeaders.hasNext()) {
authHeader = (WWWAuthenticateHeader) authHeaders.next();
String realm = authHeader.getRealm();
FromHeader from = (FromHeader) reoriginatedRequest.getHeader(FromHeader.NAME);
URI uri = from.getAddress().getURI();
AuthorizationHeader authorization = getAuthorization(reoriginatedRequest.getMethod(),
reoriginatedRequest.getRequestURI().toString(),
reoriginatedRequest.getContent() == null ? "" : reoriginatedRequest.getContent().toString(),
authHeader, proxyCredentials);
reoriginatedRequest.addHeader(authorization);
// if there was trouble with the user - make sure we fix it
if (uri.isSipURI()) {
((SipURI) uri).setUser(proxyCredentials.getUserName());
Address add = from.getAddress();
add.setURI(uri);
from.setAddress(add);
reoriginatedRequest.setHeader(from);
if (challengedRequest.getMethod().equals(Request.REGISTER))
{
ToHeader to = (ToHeader) reoriginatedRequest.getHeader(ToHeader.NAME);
add.setURI(uri);
to.setAddress(add);
reoriginatedRequest.setHeader(to);
}
Log.info("URI: " + uri.toString());
}
// if this is a register - fix to as well
}
return retryTran;
}
catch (Exception e) {
Log.error("ClientTransaction handleChallenge error", e);
return null;
}
}
private synchronized static AuthorizationHeader getAuthorization(String method, String uri,
String requestBody, WWWAuthenticateHeader authHeader,
ProxyCredentials proxyCredentials) throws SecurityException {
String response = null;
try {
Log.info("getAuthorization " + proxyCredentials.getAuthUserName());
response = MessageDigestAlgorithm.calculateResponse(authHeader
.getAlgorithm(), proxyCredentials.getAuthUserName(),
authHeader.getRealm(), new String(proxyCredentials
.getPassword()), authHeader.getNonce(),
// TODO we should one day implement those two null-s
null,// nc-value
null,// cnonce
method, uri, requestBody, authHeader.getQop());
}
catch (NullPointerException exc) {
throw new SecurityException(
"The authenticate header was malformatted");
}
AuthorizationHeader authorization = null;
try {
if (authHeader instanceof ProxyAuthenticateHeader) {
authorization = headerFactory
.createProxyAuthorizationHeader(authHeader.getScheme());
} else {
authorization = headerFactory
.createAuthorizationHeader(authHeader.getScheme());
}
authorization.setUsername(proxyCredentials.getAuthUserName());
authorization.setRealm(authHeader.getRealm());
authorization.setNonce(authHeader.getNonce());
authorization.setParameter("uri", uri);
authorization.setResponse(response);
if (authHeader.getAlgorithm() != null)
authorization.setAlgorithm(authHeader.getAlgorithm());
if (authHeader.getOpaque() != null)
authorization.setOpaque(authHeader.getOpaque());
authorization.setResponse(response);
}
catch (ParseException ex) {
throw new SecurityException("Failed to create an authorization header!");
}
return authorization;
}
}
/**
* Copyright 2012 Voxbone SA/NV
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ifsoft.sip;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Timer;
import java.util.TimerTask;
import javax.sip.ClientTransaction;
import javax.sip.InvalidArgumentException;
import javax.sip.ListeningPoint;
import javax.sip.SipException;
import javax.sip.TransactionUnavailableException;
import javax.sip.address.Address;
import javax.sip.address.SipURI;
import javax.sip.address.URI;
import javax.sip.header.CSeqHeader;
import javax.sip.header.CallIdHeader;
import javax.sip.header.ContactHeader;
import javax.sip.header.ContentTypeHeader;
import javax.sip.header.EventHeader;
import javax.sip.header.ExpiresHeader;
import javax.sip.header.FromHeader;
import javax.sip.header.MaxForwardsHeader;
import javax.sip.header.RecordRouteHeader;
import javax.sip.header.SubscriptionStateHeader;
import javax.sip.header.ToHeader;
import javax.sip.header.ViaHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.slf4j.*;
import org.slf4j.Logger;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
public class SipSubscription extends TimerTask
{
private static final Logger Log = LoggerFactory.getLogger(SipSubscription.class);
boolean active = false; // false=pending true=active
String localTag;
String remoteTag;
Address localParty;
Address remoteParty;
String callId;
long cseq;
String contact;
LinkedList<Address> rl;
long expires;
private static Timer timer = new Timer("Subscription Thread");
/*
* Creates a subscription from information in an xml file
*/
SipSubscription(String file)
{
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try
{
DocumentBuilder db = dbf.newDocumentBuilder();
Document dom = db.parse(file);
Element docEle = dom.getDocumentElement();
NodeList nl = docEle.getChildNodes();
if (nl != null && nl.getLength() > 0)
{
for (int i = 0 ; i < nl.getLength();i++)
{
Node n = nl.item(i);
if (n instanceof Element)
{
Element el = (Element) n;
Log.debug("Got a node of " + el.getNodeName() + ":" + el.getTextContent());
if (el.getNodeName().equals("active"))
{
this.active = Boolean.parseBoolean(el.getTextContent());
}
else if (el.getNodeName().equals("remote"))
{
this.remoteTag = el.getAttribute("tag");
this.remoteParty = SipService.addressFactory.createAddress(el.getTextContent());
}
else if (el.getNodeName().equals("local"))
{
this.localTag = el.getAttribute("tag");
this.localParty = SipService.addressFactory.createAddress(el.getTextContent());
}
else if (el.getNodeName().equals("callid"))
{
this.callId = el.getTextContent();
}
else if (el.getNodeName().equals("cseq"))
{
this.cseq = Long.parseLong(el.getTextContent());
}
else if (el.getNodeName().equals("contact"))
{
this.contact = el.getTextContent();
}
else if (el.getNodeName().equals("routeset"))
{
this.rl = new LinkedList<Address>();
NodeList routeList = el.getElementsByTagName("route");
for (int j = 0; j < routeList.getLength(); j++)
{
Element route = (Element) routeList.item(j);
Address addr = SipService.addressFactory.createAddress(route.getTextContent());
this.rl.add(addr);
}
}
else if (el.getNodeName().equals("expires"))
{
this.expires = Long.parseLong(el.getTextContent());
}
}
}
}
}
catch (ParserConfigurationException e)
{
Log.error("Error loading subscriptions from file", e);
}
catch (SAXException e)
{
Log.error("Error loading subscriptions from file", e);
}
catch (IOException e)
{
Log.error("Error loading subscriptions from file", e);
}
catch (DOMException e)
{
Log.error("Error loading subscriptions from file", e);
}
catch (ParseException e)
{
Log.error("Error loading subscriptions from file", e);
}
}
/*
* Creates an outbound subscription
*/
SipSubscription(String from, String to) throws ParseException
{
this.localParty = SipService.addressFactory.createAddress("sip:" + from + "@" + SipService.sipListener.host);
this.remoteParty = SipService.addressFactory.createAddress("sip:" + to + "@" + SipService.sipListener.host);
this.localTag = Integer.toString((int) (Math.random() * 100000));
this.remoteTag = null;
callId = SipService.sipProvider.getNewCallId().getCallId();
this.cseq = 1;
@SuppressWarnings("unused")
ListeningPoint listeningPoint = SipService.sipProvider.getListeningPoint(ListeningPoint.UDP);
this.contact = "sip:" + to + "@" + SipService.getRemoteIP();
rl = new LinkedList<Address>();
expires = System.currentTimeMillis() + (3600 * 1000);
}
public void schedule()
{
timer.schedule(this, 1800 * 1000, 1800 * 1000);
}
public void schedule(long nextCall)
{
timer.schedule(this, nextCall, 1800 * 1000);
}
/*
* Creates a Subscription Object based on a received subscribe
*/
SipSubscription(Request req)
{
CallIdHeader ch = (CallIdHeader) req.getHeader("Call-ID");
ToHeader th = (ToHeader) req.getHeader("To");
FromHeader fh = (FromHeader) req.getHeader("From");
rl = new LinkedList<Address>();
this.callId = ch.getCallId();
// incoming request from=remote
ContactHeader cont = (ContactHeader) req.getHeader(ContactHeader.NAME);
if (cont != null)
{
this.contact = cont.getAddress().getURI().toString();
}
// This is a server dialog. The top most record route
// header is the one that is closest to us. We extract the
// route list in the same order as the addresses in the
// incoming request.
if (rl.isEmpty())
{
ListIterator<?> rrl = req.getHeaders(RecordRouteHeader.NAME);
while (rrl.hasNext())
{
RecordRouteHeader rrh = (RecordRouteHeader) rrl.next();
rl.add(rrh.getAddress());
}
}
remoteTag = fh.getTag();
remoteParty = fh.getAddress();
localTag = th.getTag();
localParty = th.getAddress();
if (localTag == null)
{
localTag = Integer.toString((int) (Math.random() * 100000));
}
ExpiresHeader eh = (ExpiresHeader) req.getHeader(ExpiresHeader.NAME);
this.expires = System.currentTimeMillis() + (eh.getExpires() * 1000);
cseq = 1;
}
public void makeActive()
{
this.active = true;
}
public boolean isActive()
{
return this.active;
}
/*
public void sendNotify(boolean expire, Presence pres)
{
FromHeader fromHeader = null;
ToHeader toHeader = null;
URI requestURI = null;
try
{
requestURI = SipService.addressFactory.createURI(this.contact);
toHeader = SipService.headerFactory.createToHeader(this.remoteParty, this.remoteTag);
fromHeader = SipService.headerFactory.createFromHeader(this.localParty, this.localTag);
ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
ViaHeader viaHeader = null;
ListeningPoint lp = SipService.sipProvider.getListeningPoint(ListeningPoint.UDP);
viaHeader = SipService.headerFactory.createViaHeader(lp.getIPAddress(), lp.getPort(), lp.getTransport(), null);
viaHeaders.add(viaHeader);
CallIdHeader callIdHeader = SipService.headerFactory.createCallIdHeader(this.callId);
CSeqHeader cSeqHeader = SipService.headerFactory.createCSeqHeader(this.cseq++, Request.NOTIFY);
MaxForwardsHeader maxForwards = SipService.headerFactory.createMaxForwardsHeader(70);
Request request = null;
if (pres != null)
{
ContentTypeHeader ch = SipService.headerFactory.createContentTypeHeader("application", "pidf+xml");
request = SipService.messageFactory.createRequest(requestURI, Request.NOTIFY, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards, ch, pres.buildPidf(((SipURI) this.remoteParty.getURI()).getHost()));
}
else
{
request = SipService.messageFactory.createRequest(requestURI, Request.NOTIFY, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards);
}
EventHeader eph = SipService.headerFactory.createEventHeader("presence");
request.addHeader(eph);
if (expire)
{
SubscriptionStateHeader ssh = SipService.headerFactory.createSubscriptionStateHeader("terminated;reason=timeout");
request.addHeader(ssh);
}
else
{
long duration = (this.expires - System.currentTimeMillis()) / 1000;
SubscriptionStateHeader ssh = SipService.headerFactory.createSubscriptionStateHeader("active;expires=" + duration);
request.addHeader(ssh);
try
{
SipSubscriptionManager.saveWatcher(this);
}
catch (IOException e)
{
Log.error("Error persisting watcher", e);
}
catch (SAXException e)
{
Log.error("Error persisting watcher", e);
}
}
String fromUser = ((SipURI) this.localParty.getURI()).getUser();
Address localAddress = SipService.addressFactory.createAddress("sip:" + fromUser + "@" + lp.getIPAddress() + ":" + lp.getPort());
ContactHeader ch = SipService.headerFactory.createContactHeader(localAddress);
request.addHeader(ch);
if (this.rl != null && !this.rl.isEmpty())
{
ListIterator<Address> li = this.rl.listIterator();
while (li.hasNext())
{
request.addHeader(SipService.headerFactory.createRouteHeader(li.next()));
}
}
ClientTransaction t = SipService.sipProvider.getNewClientTransaction(request);
t.sendRequest();
}
catch (ParseException e)
{
Log.error("Error on SipSubscription:sendNotify", e);
}
catch (InvalidArgumentException e)
{
Log.error("Error on SipSubscription:sendNotify", e);
}
catch (TransactionUnavailableException e)
{
Log.error("Error on SipSubscription:sendNotify", e);
}
catch (SipException e)
{
Log.error("Error on SipSubscription:sendNotify", e);
}
}
*/
public void sendSubscribe(boolean expire)
{
FromHeader fromHeader = null;
ToHeader toHeader = null;
URI requestURI = null;
try
{
requestURI = SipService.addressFactory.createURI(this.contact);
toHeader = SipService.headerFactory.createToHeader(this.remoteParty, this.remoteTag);
fromHeader = SipService.headerFactory.createFromHeader(this.localParty, this.localTag);
ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
ViaHeader viaHeader = null;
ListeningPoint lp = SipService.sipProvider.getListeningPoint(ListeningPoint.UDP);
viaHeader = SipService.headerFactory.createViaHeader(lp.getIPAddress(), lp.getPort(), lp.getTransport(), null);
viaHeaders.add(viaHeader);
CallIdHeader callIdHeader = SipService.headerFactory.createCallIdHeader(this.callId);
CSeqHeader cSeqHeader = SipService.headerFactory.createCSeqHeader(this.cseq++, Request.SUBSCRIBE);
MaxForwardsHeader maxForwards = SipService.headerFactory.createMaxForwardsHeader(70);
Request request = SipService.messageFactory.createRequest(requestURI, "SUBSCRIBE", callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards);
EventHeader eph = SipService.headerFactory.createEventHeader("presence");
request.addHeader(eph);
if (expire)
{
ExpiresHeader eh = SipService.headerFactory.createExpiresHeader(0);
request.addHeader(eh);
this.cancel();
}
else
{
ExpiresHeader eh = SipService.headerFactory.createExpiresHeader(3600);
request.addHeader(eh);
this.expires = System.currentTimeMillis() + (3600 * 1000);
try
{
SipSubscriptionManager.saveSubscription(this);
}
catch (IOException e)
{
Log.error("Error persisting subscriber", e);
}
catch (SAXException e)
{
Log.error("Error persisting subscriber", e);
}
}
String fromUser = ((SipURI) this.localParty.getURI()).getUser();
Address localAddress = SipService.addressFactory.createAddress("sip:" + fromUser + "@" + lp.getIPAddress() + ":" + lp.getPort());
ContactHeader ch = SipService.headerFactory.createContactHeader(localAddress);
request.addHeader(ch);
if (this.rl != null && !this.rl.isEmpty())
{
ListIterator<Address> li = this.rl.listIterator();
while (li.hasNext())
{
request.addHeader(SipService.headerFactory.createRouteHeader(li.next()));
}
}
ClientTransaction t = SipService.sipProvider.getNewClientTransaction(request);
t.sendRequest();
}
catch (ParseException e)
{
Log.error("Error on SipSubscription:sendSubscribe", e);
}
catch (InvalidArgumentException e)
{
Log.error("Error on SipSubscription:sendSubscribe", e);
}
catch (TransactionUnavailableException e)
{
Log.error("Error on SipSubscription:sendSubscribe", e);
}
catch (SipException e)
{
Log.error("Error on SipSubscription:sendSubscribe", e);
}
}
// update subscribe on receipt of 200 Ok
public void updateSubscription(Response resp)
{
ContactHeader cont = (ContactHeader) resp.getHeader(ContactHeader.NAME);
ToHeader th = (ToHeader) resp.getHeader("To");
FromHeader fh = (FromHeader) resp.getHeader("From");
if (cont != null)
{
this.contact = cont.getAddress().getURI().toString();
}
// This is a client dialog so we extract the record
// route from the response and reverse its order to
// create a route list.
// ignore Record-Route in 1xx messages
if (this.rl.isEmpty() && resp.getStatusCode() >= 200)
{
ListIterator<?> rrl = resp.getHeaders(RecordRouteHeader.NAME);
while (rrl.hasNext())
{
RecordRouteHeader rrh = (RecordRouteHeader) rrl.next();
this.rl.addFirst(rrh.getAddress());
}
}
this.remoteTag = th.getTag();
this.localTag = fh.getTag();
try
{
SipSubscriptionManager.saveSubscription(this);
}
catch (IOException e)
{
Log.error("Error persisting subscriber", e);
}
catch (SAXException e)
{
Log.error("Error persisting subscriber", e);
}
}
// bug Fix for sip communicator, if the 200ok lacks a contact get it from the first notify
public void updateSubscription(Request req)
{
ContactHeader cont = (ContactHeader) req.getHeader(ContactHeader.NAME);
if (cont != null && this.contact.endsWith(SipService.getRemoteIP()))
{
this.contact = cont.getAddress().getURI().toString();
}
}
public void buildSubscriptionXML(ContentHandler hd) throws SAXException
{
AttributesImpl atts = new AttributesImpl();
String activeStr = Boolean.toString(this.active);
hd.startElement("", "", "active", atts);
hd.characters(activeStr.toCharArray(), 0, activeStr.length());
hd.endElement("", "", "active");
atts.addAttribute("", "", "tag", "", remoteTag);
hd.startElement("", "", "remote", atts);
String party = remoteParty.toString();
hd.characters(party.toCharArray(), 0, party.length());
hd.endElement("", "", "remote");
atts.clear();
atts.addAttribute("", "", "tag", "", localTag);
hd.startElement("", "", "local", atts);
party = localParty.toString();
hd.characters(party.toCharArray(), 0, party.length());
hd.endElement("", "", "local");
atts.clear();
hd.startElement("", "", "callid", atts);
hd.characters(callId.toCharArray(), 0, callId.length());
hd.endElement("", "", "callid");
String cseqStr = Long.toString(this.cseq);
hd.startElement("", "", "cseq", atts);
hd.characters(cseqStr.toCharArray(), 0, cseqStr.length());
hd.endElement("", "", "cseq");
hd.startElement("", "", "contact", atts);
hd.characters(contact.toCharArray(), 0, contact.length());
hd.endElement("", "", "contact");
hd.startElement("", "", "routeset", atts);
ListIterator<Address> li = rl.listIterator();
while(li.hasNext())
{
String addr = li.next().toString();
hd.startElement("", "", "route", atts);
hd.characters(addr.toCharArray(), 0, addr.length());
hd.endElement("", "", "route");
}
hd.endElement("", "", "routeset");
String expireStr = Long.toString(this.expires);
hd.startElement("", "", "expires", atts);
hd.characters(expireStr.toCharArray(), 0, expireStr.length());
hd.endElement("", "", "expires");
}
// This is a task to refresh subscriptions
public void run()
{
// Time's up, refresh the subscription
sendSubscribe(false);
}
}
/**
* Copyright 2012 Voxbone SA/NV
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ifsoft.sip;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import javax.sip.address.SipURI;
import org.slf4j.*;
import org.slf4j.Logger;
import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
/**
* Table to track all active SIP Events dialogs
*
*/
public class SipSubscriptionManager
{
private static final Logger Log = LoggerFactory.getLogger(SipSubscriptionManager.class);
// Outbound subscriptions
static Hashtable<String, List<SipSubscription>> subscriptions = new Hashtable<String, List<SipSubscription>>();
// Inbound subscriptions
static Hashtable<String, List<SipSubscription>> watchers = new Hashtable<String, List<SipSubscription>>();
private static String spoolPath = null;
public static void configure(Properties properties) {
spoolPath = properties.getProperty("com.voxbone.kelpie.spool_directory", "/var/spool/kelpie");
}
public static void addSubscriber(String user, SipSubscription subscription)
{
synchronized (subscriptions)
{
if (subscriptions.get(user) == null)
{
subscriptions.put(user, new LinkedList<SipSubscription>());
}
subscriptions.get(user).add(subscription);
subscription.schedule();
try
{
saveSubscription(subscription);
}
catch (IOException e)
{
Log.error("Error persisting new subscription", e);
}
catch (SAXException e)
{
Log.error("Error persisting new subscription", e);
}
}
}
public static void addWatcher(String user, SipSubscription subscription)
{
synchronized (watchers)
{
if (watchers.get(user) == null)
{
watchers.put(user, new LinkedList<SipSubscription>());
}
watchers.get(user).add(subscription);
try
{
saveWatcher(subscription);
}
catch (IOException e)
{
Log.error("Error persisting new watcher", e);
}
catch (SAXException e)
{
Log.error("Error persisting new watcher", e);
}
}
}
public static SipSubscription getWatcherByCallID(String user, String callId)
{
synchronized (watchers)
{
List<SipSubscription> subs = watchers.get(user);
if (subs == null)
{
return null;
}
for (SipSubscription sub : subs)
{
if (callId.equals(sub.callId))
{
return sub;
}
}
}
return null;
}
public static SipSubscription getSubscriptionByCallID(String user, String callId)
{
synchronized (subscriptions)
{
List<SipSubscription> subs = subscriptions.get(user);
if (subs == null)
{
return null;
}
for (SipSubscription sub : subs)
{
if (callId.equals(sub.callId))
{
return sub;
}
}
}
return null;
}
public static SipSubscription getSubscription(String user, String dest)
{
synchronized (subscriptions)
{
List<SipSubscription> subs = subscriptions.get(user);
if (subs!=null)
{
for (SipSubscription sub : subs)
{
String subDest = ((SipURI) sub.remoteParty.getURI()).getUser();
if (subDest.equals(dest))
{
return sub;
}
}
}
}
return null;
}
public static void removeWatcher(String user, SipSubscription subscription)
{
synchronized (watchers)
{
List<SipSubscription> subs = watchers.get(user);
subs.remove(subscription);
deleteWatcher(subscription);
}
}
public static SipSubscription getWatcher(String user, String dest)
{
synchronized (watchers)
{
List<SipSubscription> subs = watchers.get(user);
if (subs!=null)
{
for (SipSubscription sub : subs)
{
String subDest = ((SipURI) sub.remoteParty.getURI()).getUser();
if (subDest.equals(dest))
{
return sub;
}
}
}
}
return null;
}
public static SipSubscription removeSubscription(String user, String dest)
{
synchronized (subscriptions)
{
List<SipSubscription> subs = subscriptions.get(user);
if (subs!=null)
{
for (SipSubscription sub : subs)
{
String subDest = ((SipURI) sub.remoteParty.getURI()).getUser();
if (subDest.equals(dest))
{
sub.cancel();
subs.remove(sub);
deleteSubscription(sub);
return sub;
}
}
}
}
return null;
}
public static SipSubscription removeSubscriptionByCallID(String user, String callID)
{
synchronized (subscriptions)
{
List<SipSubscription> subs = subscriptions.get(user);
if (subs!=null)
{
for (SipSubscription sub : subs)
{
if (sub.callId.equals(callID))
{
sub.cancel();
subs.remove(sub);
deleteSubscription(sub);
return sub;
}
}
}
}
return null;
}
public static void saveSubscription(SipSubscription sub) throws IOException, SAXException
{
String filename = spoolPath + "/subscriptions/" + ((SipURI) sub.localParty.getURI()).getUser() + "_" + sub.callId;
FileOutputStream fs = new FileOutputStream(new File(filename));
OutputFormat of = new OutputFormat("XML", "ISO-8859-1", true);
of.setIndent(1);
of.setIndenting(true);
XMLSerializer serializer = new XMLSerializer(fs, of);
ContentHandler hd = serializer.asContentHandler();
hd.startDocument();
hd.startElement("", "", "SUBSCRIPTION", null);
sub.buildSubscriptionXML(hd);
hd.endElement("", "", "SUBSCRIPTION");
hd.endDocument();
fs.close();
}
public static void saveWatcher(SipSubscription sub) throws IOException, SAXException
{
String filename = spoolPath + "/watchers/" + ((SipURI) sub.localParty.getURI()).getUser() + "_" + sub.callId;
FileOutputStream fs = new FileOutputStream(new File(filename));
OutputFormat of = new OutputFormat("XML", "ISO-8859-1", true);
of.setIndent(1);
of.setIndenting(true);
XMLSerializer serializer = new XMLSerializer(fs, of);
ContentHandler hd = serializer.asContentHandler();
hd.startDocument();
hd.startElement("", "", "SUBSCRIPTION", null);
sub.buildSubscriptionXML(hd);
hd.endElement("", "", "SUBSCRIPTION");
hd.endDocument();
fs.close();
}
public static void deleteSubscription(SipSubscription sub)
{
try {
String filename = spoolPath + "/subscriptions/" + ((SipURI) sub.localParty.getURI()).getUser() + "_" + sub.callId;
File file = new File(filename);
file.delete();
} catch(Exception ioe) {
System.err.println( ioe.getMessage() + "Error deleting!");
}
}
public static void deleteWatcher(SipSubscription sub)
{
try {
String filename = spoolPath + "/watchers/" + ((SipURI) sub.localParty.getURI()).getUser() + "_" + sub.callId;
File file = new File(filename);
file.delete();
} catch(Exception ioe) {
System.err.println( ioe.getMessage() + "Error deleting!");
}
}
public static void loadData()
{
long now = System.currentTimeMillis();
String dirName = spoolPath + "/subscriptions/";
File dirFile = new File(dirName);
String [] children = dirFile.list();
for (String child : children)
{
SipSubscription sub = new SipSubscription(dirName + child);
if (sub != null)
{
if (now < sub.expires)
{
String user = ((SipURI) sub.localParty.getURI()).getUser();
Log.info("adding subscriber for " + user);
synchronized (subscriptions)
{
if (subscriptions.get(user) == null)
{
subscriptions.put(user, new LinkedList<SipSubscription>());
}
subscriptions.get(user).add(sub);
long nextCall = sub.expires - now - (1800 * 1000);
if (nextCall < 0)
{
nextCall = 0;
}
sub.schedule(nextCall);
}
}
else
{
Log.error("Skipping already expired subscription");
try
{
deleteSubscription(sub);
}
catch(Exception e)
{
Log.error("Error Deleting subscription");
}
}
}
}
dirName = spoolPath + "/watchers/";
dirFile = new File(dirName);
children = dirFile.list();
for (String child : children)
{
SipSubscription sub = new SipSubscription(dirName + child);
if (now < sub.expires)
{
String user = ((SipURI) sub.localParty.getURI()).getUser();
Log.info("adding watcher for " + user);
synchronized (watchers)
{
if (watchers.get(user) == null)
{
watchers.put(user, new LinkedList<SipSubscription>());
}
watchers.get(user).add(sub);
}
}
else
{
Log.error("Skipping already expired watcher");
deleteWatcher(sub);
}
}
}
}
/**
* Copyright 2012 Voxbone SA/NV
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ifsoft.sip;
import gov.nist.javax.sip.header.Require;
import java.io.*;
import java.net.*;
import java.util.*;
import java.text.*;
import javax.sip.ClientTransaction;
import javax.sip.Dialog;
import javax.sip.DialogTerminatedEvent;
import javax.sip.IOExceptionEvent;
import javax.sip.InvalidArgumentException;
import javax.sip.ListeningPoint;
import javax.sip.RequestEvent;
import javax.sip.ResponseEvent;
import javax.sip.ServerTransaction;
import javax.sip.SipException;
import javax.sip.SipListener;
import javax.sip.SipProvider;
import javax.sip.TimeoutEvent;
import javax.sip.TransactionAlreadyExistsException;
import javax.sip.TransactionTerminatedEvent;
import javax.sip.TransactionUnavailableException;
import javax.sip.address.Address;
import javax.sip.address.SipURI;
import javax.sip.address.URI;
import javax.sip.header.CSeqHeader;
import javax.sip.header.CallIdHeader;
import javax.sip.header.ContactHeader;
import javax.sip.header.ContentTypeHeader;
import javax.sip.header.ExpiresHeader;
import javax.sip.header.FromHeader;
import javax.sip.header.Header;
import javax.sip.header.ViaHeader;
import javax.sip.header.SubscriptionStateHeader;
import javax.sip.header.ToHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;
import org.slf4j.*;
import org.slf4j.Logger;
/**
* Handles incoming sip requests/responses
*
*/
public class VideoBridgeSipListener implements SipListener
{
private static final Logger Log = LoggerFactory.getLogger(SipService.class);
private static boolean optionsmode = false;
private static boolean subscribeRport = false;
private static boolean subscribeEmu = false;
private static SipServerCallback sipServerCallback;
private static Hashtable sipListenersTable;
public String host;
public static void configure(Properties properties)
{
optionsmode = Boolean.parseBoolean(properties.getProperty("com.voxbone.kelpie.feature.options.probe", "false"));
subscribeRport = Boolean.parseBoolean(properties.getProperty("com.voxbone.kelpie.feature.subscribe.rport", "false"));
subscribeEmu = Boolean.parseBoolean(properties.getProperty("com.voxbone.kelpie.feature.subscribe.force-emu", "false"));
}
public VideoBridgeSipListener(String host)
{
this.host = host;
sipServerCallback = new SipServerCallback();
sipListenersTable = new Hashtable();
}
public static SipServerCallback getSipServerCallback() {
return sipServerCallback;
}
public void processDialogTerminated(DialogTerminatedEvent evt)
{
}
public void processIOException(IOExceptionEvent evt)
{
}
public void processRequest(RequestEvent evt)
{
Request req = evt.getRequest();
Log.debug("[[SIP]] Got a request " + req.getMethod());
try
{
SipListener sipListener = findSipListener(evt);
if (sipListener != null)
{
sipListener.processRequest(evt);
return;
}
if (req.getMethod().equals(Request.MESSAGE))
{
Log.info("[[SIP]] Forwarding message");
/*
MessageMessage mm = new MessageMessage(req);
//JID destination = UriMappings.toJID(mm.to);
ContentTypeHeader cth = (ContentTypeHeader) req.getHeader(ContentTypeHeader.NAME);
if ( !cth.getContentType().equals("text")
&& !cth.getContentSubType().equals("plain"))
{
Log.warn("[[SIP]] Message isn't text, rejecting");
Response res = SipService.messageFactory.createResponse(Response.NOT_IMPLEMENTED, req);
if (evt.getServerTransaction() == null)
{
ServerTransaction tx = ((SipProvider) evt.getSource()).getNewServerTransaction(req);
tx.sendResponse(res);
}
else
{
evt.getServerTransaction().sendResponse(res);
}
return;
}
if (mm.body.startsWith("/echo")) {
mm.body = mm.body.substring(mm.body.lastIndexOf('/') + 5);
mm.to = mm.from;
String domain = host;
SipSubscription sub = SipSubscriptionManager.getWatcher(mm.from, mm.to);
if (sub != null)
{
domain = ((SipURI)sub.remoteParty.getURI()).getHost();
}
// Log.debug("[[SIP]] Echo message: " + mm.body);
SipService.sendMessageMessage(mm, domain);
return;
} else if (mm.body.startsWith("/me")) {
mm.body = mm.from + " " + mm.body.substring(mm.body.lastIndexOf('/') + 3);
}
Log.debug("[[SIP]] Jabber destination is " + destination);
Session sess = SessionManager.findCreateSession(host, destination);
if (sess != null)
{
if (sess.sendMessageMessage(mm))
{
Log.debug("[[SIP]] Message forwarded ok");
Response res = SipService.messageFactory.createResponse(Response.OK, req);
if (evt.getServerTransaction() == null)
{
ServerTransaction tx = ((SipProvider) evt.getSource()).getNewServerTransaction(req);
tx.sendResponse(res);
}
else
{
evt.getServerTransaction().sendResponse(res);
}
return;
}
}
*/
Log.error("[[SIP]] Forwarding failed!");
}
else if (req.getMethod().equals(Request.SUBSCRIBE))
{
Log.info("[[SIP]] Received a Subscribe message");
String callid = ((CallIdHeader) req.getHeader(CallIdHeader.NAME)).getCallId();
FromHeader fh = (FromHeader) req.getHeader("From");
URI ruri = req.getRequestURI();
String src = ((SipURI) fh.getAddress().getURI()).getUser();
String dest = ((SipURI) ruri).getUser();
SipSubscription sub = SipSubscriptionManager.getWatcherByCallID(dest, callid);
if (subscribeRport) {
// ContactHeader contact = (ContactHeader) req.getHeader(ContactHeader.NAME);
ViaHeader viaHeaderr = (ViaHeader)req.getHeader(ViaHeader.NAME);
int rport = Integer.parseInt( viaHeaderr.getParameter("rport") );
String received = viaHeaderr.getParameter("received");
Log.debug("[[SIP]] Forcing Contact RPORT: "+received+":"+rport);
Address localrAddress = SipService.addressFactory.createAddress("sip:" + src + "@" + received + ":" + rport );
ContactHeader nch = SipService.headerFactory.createContactHeader(localrAddress);
req.removeHeader("Contact");
req.addHeader(nch);
}
ToHeader th = (ToHeader) req.getHeader("To");
int expires = ((ExpiresHeader) req.getHeader(ExpiresHeader.NAME)).getExpires();
Response res = SipService.messageFactory.createResponse(Response.ACCEPTED, req);
if (expires > 0)
{
Log.debug("[[SIP]] New subscription or refresh");
if (sub == null)
{
if (th.getTag() == null)
{
Log.info("[[SIP]] New Subscription, sending add request to user");
sub = new SipSubscription(req);
//sub.localTag = ((ToHeader) res.getHeader(ToHeader.NAME)).getTag();
((ToHeader) res.getHeader(ToHeader.NAME)).setTag(sub.localTag);
SipSubscriptionManager.addWatcher(dest, sub);
/*
JID destination = UriMappings.toJID(dest);
JID source = new JID(src + "@" + host);
if (destination != null)
{
Session sess = SessionManager.findCreateSession(host, destination);
sess.sendSubscribeRequest(source, destination, "subscribe");
}
else
{
Log.warn("[[SIP]] Unknown Jabber user...");
res = SipService.messageFactory.createResponse(Response.NOT_FOUND, req);
}
*/
}
else
{
Log.warn("[[SIP]] Rejecting Unknown in-dialog subscribe for "+ ruri);
res = SipService.messageFactory.createResponse(481, req);
}
}
else
{
Log.debug("[[SIP]] Refresh subscribe, sending poll");
/*
JID destination = UriMappings.toJID(dest);
JID source = new JID(src + "@" + host);
if (destination != null)
{
Session sess = SessionManager.findCreateSession(host, destination);
sess.sendSubscribeRequest(source, destination, "probe");
}
else
{
res = SipService.messageFactory.createResponse(Response.NOT_FOUND, req);
Log.error("[[SIP]] Unknown destination!");
}
*/
}
}
else
{
Log.debug("[[SIP]] Expire subscribe");
if (sub != null)
{
Log.debug("[[SIP]] Subscription found, removing");
/*
sub.sendNotify(true, null);
SipSubscriptionManager.removeWatcher(dest, sub);
JID destination = UriMappings.toJID(dest);
JID source = new JID(src + "@" + host);
Session sess = SessionManager.findCreateSession(host, destination);
sess.sendSubscribeRequest(source, destination, "unsubscribe");
*/
}
}
res.addHeader(req.getHeader(ExpiresHeader.NAME));
ListeningPoint lp = SipService.sipProvider.getListeningPoint(ListeningPoint.UDP);
Address localAddress = SipService.addressFactory.createAddress("sip:" + dest + "@" + lp.getIPAddress() + ":" + lp.getPort());
ContactHeader ch = SipService.headerFactory.createContactHeader(localAddress);
res.addHeader(ch);
if (evt.getServerTransaction() == null)
{
ServerTransaction tx = ((SipProvider) evt.getSource()).getNewServerTransaction(req);
tx.sendResponse(res);
}
else
{
evt.getServerTransaction().sendResponse(res);
}
return;
}
else if (req.getMethod().equals(Request.NOTIFY))
{
Log.info("[[SIP]] Received a Notify message");
try
{
String callid = ((CallIdHeader) req.getHeader(CallIdHeader.NAME)).getCallId();
FromHeader fh = (FromHeader) req.getHeader("From");
URI ruri = req.getRequestURI();
String src = ((SipURI) fh.getAddress().getURI()).getUser();
String dest = ((SipURI) ruri).getUser();
SipSubscription sub = SipSubscriptionManager.getSubscriptionByCallID(dest, callid);
if (sub != null)
{
Log.debug("[[SIP]] Subscription found!");
SubscriptionStateHeader ssh = (SubscriptionStateHeader) req.getHeader(SubscriptionStateHeader.NAME);
if (ssh.getState().equalsIgnoreCase(SubscriptionStateHeader.PENDING))
{
Log.debug("[[SIP]] Subscription pending. Updating");
sub.updateSubscription(req);
}
else if ( ssh.getState().equalsIgnoreCase(SubscriptionStateHeader.ACTIVE)
&& !sub.isActive())
{
Log.debug("[[SIP]] Subscription accepted. Informing");
sub.updateSubscription(req);
/*
JID destination = UriMappings.toJID(dest);
JID source = new JID(src + "@" + host);
sub.makeActive();
Session sess = SessionManager.findCreateSession(host, destination);
sess.sendSubscribeRequest(source, destination, "subscribed");
*/
}
else if (ssh.getState().equalsIgnoreCase(SubscriptionStateHeader.TERMINATED))
{
Log.debug("[[SIP]] Subscription is over, removing");
SipSubscriptionManager.removeSubscriptionByCallID(dest, sub.callId);
/*
JID destination = UriMappings.toJID(dest);
@SuppressWarnings("unused")
JID source = new JID(src + "@" + host);
Session sess = SessionManager.findCreateSession(host, destination);
sess.sendPresence(Presence.buildOfflinePresence(src, dest));
*/
Log.debug("[[SIP]] Reason code is " + ssh.getReasonCode());
if ( ssh.getReasonCode() != null
&& ( ssh.getReasonCode().equalsIgnoreCase(SubscriptionStateHeader.TIMEOUT)
|| ssh.getReasonCode().equalsIgnoreCase(SubscriptionStateHeader.DEACTIVATED)))
{
Log.debug("[[SIP]] Reason is timeout, sending re-subscribe");
sub = new SipSubscription(dest, src);
SipSubscriptionManager.addSubscriber(dest, sub);
sub.sendSubscribe(false);
}
}
if (req.getRawContent() != null)
{
try
{
/*
Presence pres = new Presence(req);
JID destination = UriMappings.toJID(dest);
Session sess = SessionManager.findCreateSession(host, destination);
sess.sendPresence(pres);
*/
}
catch (Exception e)
{
Log.error("[[SIP]] Error parsing presence document!\n" + req.toString(), e);
}
}
else if (sub.isActive())
{
/*
Presence pres = Presence.buildUnknownPresence(src, dest, host);
JID destination = UriMappings.toJID(dest);
Session sess = SessionManager.findCreateSession(host, destination);
sess.sendPresence(pres);
*/
}
Response res = SipService.messageFactory.createResponse(Response.OK, req);
ListeningPoint lp = SipService.sipProvider.getListeningPoint(ListeningPoint.UDP);
Address localAddress = SipService.addressFactory.createAddress("sip:" + dest + "@" + lp.getIPAddress() + ":" + lp.getPort());
ContactHeader ch = SipService.headerFactory.createContactHeader(localAddress);
res.addHeader(ch);
if (evt.getServerTransaction() == null)
{
ServerTransaction tx = ((SipProvider) evt.getSource()).getNewServerTransaction(req);
tx.sendResponse(res);
}
else
{
evt.getServerTransaction().sendResponse(res);
}
}
else
{
Response res = SipService.messageFactory.createResponse(481, req);
ListeningPoint lp = SipService.sipProvider.getListeningPoint(ListeningPoint.UDP);
Address localAddress = SipService.addressFactory.createAddress("sip:" + dest + "@" + lp.getIPAddress() + ":" + lp.getPort());
ContactHeader ch = SipService.headerFactory.createContactHeader(localAddress);
res.addHeader(ch);
if (evt.getServerTransaction() == null)
{
ServerTransaction tx = ((SipProvider) evt.getSource()).getNewServerTransaction(req);
tx.sendResponse(res);
}
else
{
evt.getServerTransaction().sendResponse(res);
}
}
}
catch (Exception e)
{
Log.error("[[SIP]] failure while handling NOTIFY message", e);
}
return;
}
else if (req.getMethod().equals(Request.INVITE))
{
if (evt.getDialog() == null)
{
Log.info("[[SIP]] Got initial invite!");
FromHeader fh = (FromHeader) req.getHeader("From");
URI ruri = req.getRequestURI();
String src = ((SipURI) fh.getAddress().getURI()).getUser();
String dest = ((SipURI) ruri).getUser();
/*
JID destination = UriMappings.toJID(dest);
if (destination != null)
{
Log.debug("[[SIP]] Attempting to send to destination: " + destination.toString());
ServerTransaction trans;
if (evt.getServerTransaction() == null)
{
trans = ((SipProvider) evt.getSource()).getNewServerTransaction(req);
}
else
{
trans = evt.getServerTransaction();
}
Dialog dialog = SipService.sipProvider.getNewDialog(trans);
Session sess = SessionManager.findCreateSession(host, destination);
CallSession cs = new CallSession();
Log.info("[[SIP]] created call session : [[" + cs.internalCallId + "]]");
cs.parseInvite(req, dialog, trans);
dialog.setApplicationData(cs);
if (sess.startCall(cs, src, dest))
{
Response res = SipService.messageFactory.createResponse(Response.RINGING, req);
trans.sendResponse(res);
return;
}
}
*/
} else {
// SIP RE-INVITE (dumbstart implementation, ignores timers, etc)
Log.info("[[SIP]] Got a re-invite!");
CallSession cs = (CallSession) evt.getDialog().getApplicationData();
if (cs != null)
{
Response res = null;
/*
Session sess = SessionManager.findCreateSession(cs.jabberLocal.getDomain(), cs.jabberRemote);
if (sess == null)
{
res = SipService.messageFactory.createResponse(488, req);
} else {
res = SipService.messageFactory.createResponse(Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST, req);
}
*/
if (evt.getServerTransaction() == null)
{
ServerTransaction tx = ((SipProvider) evt.getSource()).getNewServerTransaction(req);
tx.sendResponse(res);
}
else
{
evt.getServerTransaction().sendResponse(res);
}
return;
}
}
}
else if (req.getMethod().equals(Request.BYE))
{
if (evt.getDialog() != null)
{
Log.info("[[SIP]] Got in dialog bye");
CallSession cs = (CallSession) evt.getDialog().getApplicationData();
if (cs != null)
{
/*
Session sess = SessionManager.findCreateSession(cs.jabberLocal.getDomain(), cs.jabberRemote);
if (sess != null)
{
sess.sendBye(cs);
}
*/
}
Response res = SipService.messageFactory.createResponse(Response.OK, req);
if (evt.getServerTransaction() == null)
{
ServerTransaction tx = ((SipProvider) evt.getSource()).getNewServerTransaction(req);
tx.sendResponse(res);
}
else
{
evt.getServerTransaction().sendResponse(res);
}
return;
}
}
else if (req.getMethod().equals(Request.CANCEL))
{
if (evt.getDialog() != null)
{
Log.info("[[SIP]] Got in dialog cancel");
Response res = SipService.messageFactory.createResponse(Response.OK, req);
if (evt.getServerTransaction() == null)
{
ServerTransaction tx = ((SipProvider) evt.getSource()).getNewServerTransaction(req);
tx.sendResponse(res);
}
else
{
evt.getServerTransaction().sendResponse(res);
}
CallSession cs = (CallSession) evt.getDialog().getApplicationData();
if (cs != null)
{
/*
Session sess = SessionManager.findCreateSession(cs.jabberLocal.getDomain(), cs.jabberRemote);
if (sess != null)
{
SipService.sendReject(cs);
sess.sendBye(cs);
}
*/
}
return;
}
}
else if (req.getMethod().equals(Request.ACK))
{
return;
}
else if (req.getMethod().equals(Request.OPTIONS))
{
int resp = Response.OK;
if (optionsmode) {
if (evt.getDialog() != null)
{
Log.info("[[SIP]] Got in dialog OPTIONS");
resp = Response.OK;
// temp: debug message to validate this OPTIONS scenario
CallSession cs = (CallSession) evt.getDialog().getApplicationData();
if (cs == null)
{
Log.error("[[SIP]] OPTIONS CallSession is null?");
}
} else {
Log.info("[[SIP]] Rejecting out-of-dialog OPTIONS");
resp = Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST;
}
}
try
{
DatagramSocket ds = new DatagramSocket();
ds.close();
}
catch (SocketException e)
{
Log.error("[[SIP]] No more sockets available", e);
resp = Response.SERVER_INTERNAL_ERROR;
}
Response res = SipService.messageFactory.createResponse(resp, req);
SipService.sipProvider.sendResponse(res);
return;
}
// SIP UPDATE (dumbstart, purposed as Jingle session-info counterpart)
else if (req.getMethod().equals(Request.UPDATE))
{
int resp = Response.OK;
if (optionsmode) {
if (evt.getDialog() != null)
{
Log.info("[[SIP]] Got UPDATE request");
resp = Response.OK;
// temp: debug message to validate this OPTIONS scenario
CallSession cs = (CallSession) evt.getDialog().getApplicationData();
if (cs == null)
{
Log.error("[[SIP]] UPDATE CallSession is null?");
}
Header require = (Header)req.getHeader(Require.NAME);
Header sessexp = (Header)req.getHeader("Session-Expires");
Log.debug("[[SIP]] SESSION-TIMER: "+require+":"+sessexp);
} else {
Log.info("[[SIP]] No Session - Rejecting UPDATE");
resp = Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST;
}
}
try
{
DatagramSocket ds = new DatagramSocket();
ds.close();
}
catch (SocketException e)
{
Log.error("[[SIP]] No more sockets available", e);
resp = Response.SERVER_INTERNAL_ERROR;
}
Response res = SipService.messageFactory.createResponse(resp, req);
SipService.sipProvider.sendResponse(res);
return;
}
else if (req.getMethod().equals(Request.INFO))
{
CallSession cs = (CallSession) evt.getDialog().getApplicationData();
if (cs != null && cs.vRelay != null)
{
cs.vRelay.sendFIR();
Response res = SipService.messageFactory.createResponse(Response.OK, req);
if (evt.getServerTransaction() == null)
{
ServerTransaction tx = ((SipProvider) evt.getSource()).getNewServerTransaction(req);
tx.sendResponse(res);
}
else
{
evt.getServerTransaction().sendResponse(res);
}
return;
}
}
Response res = SipService.messageFactory.createResponse(Response.FORBIDDEN, req);
if (evt.getServerTransaction() == null)
{
ServerTransaction tx = ((SipProvider) evt.getSource()).getNewServerTransaction(req);
tx.sendResponse(res);
}
else
{
evt.getServerTransaction().sendResponse(res);
}
Log.error("[[SIP]] Rejecting request");
}
catch (ParseException e)
{
Log.error("[[SIP]] Error processing sip Request!\n" + req.toString(), e);
}
catch (TransactionAlreadyExistsException e)
{
Log.error("[[SIP]] Error processing sip Request!\n" + req.toString(), e);
}
catch (TransactionUnavailableException e)
{
Log.error("[[SIP]] Error processing sip Request!\n" + req.toString(), e);
}
catch (SipException e)
{
Log.error("[[SIP]] Error processing sip Request!\n" + req.toString(), e);
}
catch (InvalidArgumentException e)
{
Log.error("[[SIP]] Error processing sip Request!\n" + req.toString(), e);
}
catch(Exception e)
{
Log.error("[[SIP]] Error processing sip Request!\n" + req.toString(), e);
}
}
public void processResponse(ResponseEvent evt)
{
Response resp = evt.getResponse();
String method = ((CSeqHeader) resp.getHeader(CSeqHeader.NAME)).getMethod();
int status = resp.getStatusCode();
Log.info("[[SIP]] Got a response to " + method);
try
{
SipListener sipListener = findSipListener(evt);
if (sipListener != null)
{
sipListener.processResponse(evt);
return;
}
if (method.equals(Request.SUBSCRIBE))
{
if (subscribeEmu) {
// force emulation regardless of reply
status = 500;
}
if (status == Response.PROXY_AUTHENTICATION_REQUIRED || status == Response.UNAUTHORIZED)
{
ClientTransaction clientTransaction = evt.getClientTransaction();
if (SipService.sipAccount != null)
{
try {
SipService.handleChallenge(resp, clientTransaction, SipService.sipAccount).sendRequest();
} catch (Exception e) {
Log.error("Proxy authentification failed", e);
}
}
return;
}
else if (status >= 200 && status < 300)
{
Log.info("[[SIP]] 200 OK to SUBSCRIBE, updating route info");
String callid = ((CallIdHeader) resp.getHeader(CallIdHeader.NAME)).getCallId();
FromHeader fh = (FromHeader) resp.getHeader("From");
String user = ((SipURI) fh.getAddress().getURI()).getUser();
SipSubscription sub = SipSubscriptionManager.getSubscriptionByCallID(user, callid);
// Subscription can be null if it's a response to Subscribe / Expires 0
if (sub != null)
{
sub.updateSubscription(resp);
}
}
else if (status >= 400)
{
Log.info("[[SIP]] Subscribe failed");
FromHeader fh = (FromHeader) resp.getHeader("From");
String dest = ((SipURI) fh.getAddress().getURI()).getUser();
ToHeader th = (ToHeader) resp.getHeader("To");
String src = ((SipURI) th.getAddress().getURI()).getUser();
String callid = ((CallIdHeader) resp.getHeader(CallIdHeader.NAME)).getCallId();
if (status != 404)
{
Log.info("[[SIP]] emulating presence");
/*
JID destination = UriMappings.toJID(dest);
Session sess = SessionManager.findCreateSession(host, destination);
sess.sendSubscribeRequest(new JID(src + "@" + host), destination, "subscribed");
sess.sendPresence(Presence.buildOnlinePresence(src, dest, host));
*/
}
@SuppressWarnings("unused")
SipSubscription sub = SipSubscriptionManager.removeSubscriptionByCallID(dest, callid);
}
}
else if (method.equals(Request.INVITE))
{
if (status >= 200 && status < 300)
{
Dialog d = evt.getDialog();
if (d == null)
{
Log.error("[[SIP]] Dialog is null");
return;
}
CallSession cs = (CallSession) d.getApplicationData();
if (cs == null)
{
Log.error("[[SIP]] CallSession is null");
ClientTransaction ct = evt.getClientTransaction();
if (ct == null)
{
Log.error("[[SIP]] Client transaction null!!!!");
return;
}
else if (ct.getApplicationData() == null)
{
Log.error("[[SIP]] Client transaction application data null!!!!");
return;
}
Log.debug("[[SIP]] Found CallSession in transaction, re-pairing");
d.setApplicationData(ct.getApplicationData());
cs = (CallSession) ct.getApplicationData();
cs.sipDialog = d;
}
d.sendAck(d.createAck(d.getLocalSeqNumber()));
FromHeader fh = (FromHeader) resp.getHeader("From");
String dest = ((SipURI) fh.getAddress().getURI()).getUser();
//JID destination = UriMappings.toJID(dest);
//Session sess = SessionManager.findCreateSession(host, destination);
if(!cs.callAccepted)
{
// RFC3261 says that all 200 OK to an invite get passed to UAC, even re-trans, so we need to filter
cs.parseSDP(new String(resp.getRawContent()), false);
//sess.sendAccept(cs);
cs.callAccepted = true;
}
}
else if (status >= 400)
{
Log.info("[[SIP]] Invite failed, ending call");
Dialog d = evt.getDialog();
if (d == null)
{
Log.error("[[SIP]] Dialog is null");
return;
}
CallSession cs = (CallSession) d.getApplicationData();
// terminate the jabber side if it hasn't been done already
/*
if (cs != null && CallManager.getSession(cs.jabberSessionId) != null)
{
FromHeader fh = (FromHeader) resp.getHeader("From");
String dest = ((SipURI) fh.getAddress().getURI()).getUser();
JID destination = UriMappings.toJID(dest);
Session sess = SessionManager.findCreateSession(host, destination);
sess.sendBye(cs);
}
*/
}
}
else if (method.equals(Request.NOTIFY))
{
if (status == 418)
{
Log.info("[[SIP]] Subcription is no longer known, removing");
FromHeader fh = (FromHeader) resp.getHeader("From");
String dest = ((SipURI) fh.getAddress().getURI()).getUser();
String callid = ((CallIdHeader) resp.getHeader(CallIdHeader.NAME)).getCallId();
SipSubscription sub = SipSubscriptionManager.getWatcherByCallID(dest, callid);
if (sub != null)
{
Log.debug("[[SIP]] Watcher removed ok");
SipSubscriptionManager.removeWatcher(dest, sub);
}
}
}
// Very basic MESSAGE handling for replies
else if (method.equals(Request.MESSAGE))
{
if (status >= 400) {
Log.info("[[SIP]] MESSAGE failed with status "+status);
}
else if (status == 200) {
Log.info("[[SIP]] MESSAGE delivered");
}
}
}
catch (Exception e)
{
Log.error("[[SIP]] Error processing sip Response!\n" + resp.toString(), e);
}
}
public void processTimeout(TimeoutEvent evt)
{
}
public void processTransactionTerminated(TransactionTerminatedEvent evt)
{
}
private SipListener findSipListener(EventObject event)
{
String sipCallId = null;
try {
CallIdHeader callIdHeader;
if (event instanceof RequestEvent) {
Request request = ((RequestEvent)event).getRequest();
callIdHeader = (CallIdHeader) request.getHeader(CallIdHeader.NAME);
} else if (event instanceof ResponseEvent) {
Response response = ((ResponseEvent)event).getResponse();
callIdHeader = (CallIdHeader) response.getHeader(CallIdHeader.NAME);
} else {
Log.error("Invalid event object " + event);
return null;
}
sipCallId = callIdHeader.getCallId();
synchronized (sipListenersTable)
{
return (SipListener)sipListenersTable.get(sipCallId);
}
} catch (NullPointerException e) {
if (sipCallId == null || "".equals(sipCallId))
{
Log.error("could not get SIP CallId from incoming message. Dropping message", e);
}
throw e;
}
}
class SipServerCallback {
public void addSipListener(String key, SipListener sipListener) {
synchronized (sipListenersTable) {
if (!sipListenersTable.containsKey(key)) {
sipListenersTable.put(key, sipListener);
} else {
Log.error("key: " + key + " already mapped!");
}
}
}
public void removeSipListener(String key) {
synchronized (sipListenersTable) {
if (sipListenersTable.containsKey(key)) {
sipListenersTable.remove(key);
} else {
Log.error("could not find a SipListener " + "entry to remove with the key:" + key);
}
}
}
}
}
...@@ -20,9 +20,6 @@ import javax.media.*; ...@@ -20,9 +20,6 @@ import javax.media.*;
import javax.media.protocol.*; import javax.media.protocol.*;
import javax.media.format.*; import javax.media.format.*;
import org.jitsi.service.neomedia.*;
import org.jitsi.util.*;
import org.jitsi.videobridge.*;
import org.jivesoftware.openfire.container.*; import org.jivesoftware.openfire.container.*;
import org.jivesoftware.openfire.muc.*; import org.jivesoftware.openfire.muc.*;
import org.jivesoftware.util.*; import org.jivesoftware.util.*;
...@@ -74,7 +71,9 @@ import org.jitsi.util.*; ...@@ -74,7 +71,9 @@ import org.jitsi.util.*;
import org.ifsoft.*; import org.ifsoft.*;
import org.ifsoft.rtp.*; import org.ifsoft.rtp.*;
import org.ifsoft.sip.*;
import net.sf.fmj.media.rtp.*;
/** /**
* Implements <tt>org.jivesoftware.openfire.container.Plugin</tt> to integrate * Implements <tt>org.jivesoftware.openfire.container.Plugin</tt> to integrate
...@@ -85,6 +84,10 @@ import org.ifsoft.rtp.*; ...@@ -85,6 +84,10 @@ import org.ifsoft.rtp.*;
*/ */
public class PluginImpl implements Plugin, PropertyEventListener public class PluginImpl implements Plugin, PropertyEventListener
{ {
/**
* SIP registration status.
*/
public static String sipRegisterStatus = "";
/** /**
* The logger. * The logger.
*/ */
...@@ -132,6 +135,11 @@ public class PluginImpl implements Plugin, PropertyEventListener ...@@ -132,6 +135,11 @@ public class PluginImpl implements Plugin, PropertyEventListener
*/ */
public static final String MIN_PORT_NUMBER_PROPERTY_NAME = "org.jitsi.videobridge.media.MIN_PORT_NUMBER"; public static final String MIN_PORT_NUMBER_PROPERTY_NAME = "org.jitsi.videobridge.media.MIN_PORT_NUMBER";
/**
* The name of the property that contains the default SIP port.
*/
public static final String SIP_PORT_PROPERTY_NAME = "org.jitsi.videobridge.sip.port.number";
/** /**
* The minimum port number default value. * The minimum port number default value.
*/ */
...@@ -142,6 +150,7 @@ public class PluginImpl implements Plugin, PropertyEventListener ...@@ -142,6 +150,7 @@ public class PluginImpl implements Plugin, PropertyEventListener
*/ */
public static final int MAX_PORT_DEFAULT_VALUE = 60000; public static final int MAX_PORT_DEFAULT_VALUE = 60000;
/** /**
* The Jabber component which has been added to {@link #componentManager} * The Jabber component which has been added to {@link #componentManager}
* i.e. Openfire. * i.e. Openfire.
...@@ -252,6 +261,25 @@ public class PluginImpl implements Plugin, PropertyEventListener ...@@ -252,6 +261,25 @@ public class PluginImpl implements Plugin, PropertyEventListener
createIQHandlers(); createIQHandlers();
Properties properties = new Properties();
String hostName = XMPPServer.getInstance().getServerInfo().getHostname();
String logDir = pluginDirectory.getAbsolutePath() + File.separator + ".." + File.separator + ".." + File.separator + "logs" + File.separator;
String port = JiveGlobals.getProperty(SIP_PORT_PROPERTY_NAME, "5060");
properties.setProperty("com.voxbone.kelpie.hostname", hostName);
properties.setProperty("com.voxbone.kelpie.ip", hostName);
properties.setProperty("com.voxbone.kelpie.sip_port", port);
properties.setProperty("javax.sip.IP_ADDRESS", hostName);
properties.setProperty("gov.nist.javax.sip.TRACE_LEVEL", "99");
properties.setProperty("gov.nist.javax.sip.SERVER_LOG", logDir + "sip_server.log");
properties.setProperty("gov.nist.javax.sip.DEBUG_LOG", logDir + "sip_debug.log");
new SipService(properties);
Log.info("Initialize SIP Stack at " + hostName + ":" + port);
} }
catch(Exception e) { catch(Exception e) {
Log.error( "Jitsi Videobridge web app initialize error", e); Log.error( "Jitsi Videobridge web app initialize error", e);
...@@ -717,6 +745,34 @@ public class PluginImpl implements Plugin, PropertyEventListener ...@@ -717,6 +745,34 @@ public class PluginImpl implements Plugin, PropertyEventListener
reply.setError(PacketError.Condition.not_allowed); reply.setError(PacketError.Condition.not_allowed);
} }
} else if (object instanceof UnInviteCommand) {
UnInviteCommand uninvite = (UnInviteCommand) object;
String focusAgentName = "jitsi.videobridge." + uninvite.getMuc().getNode();
if (sessions.containsKey(focusAgentName))
{
FocusAgent focusAgent = sessions.get(focusAgentName);
focusAgent.uninviteNewParticipant(uninvite.getCallId(), reply);
} else {
reply.setError(PacketError.Condition.not_allowed);
}
} else if (object instanceof InviteCommand) {
InviteCommand invite = (InviteCommand) object;
String focusAgentName = "jitsi.videobridge." + invite.getMuc().getNode();
if (sessions.containsKey(focusAgentName))
{
FocusAgent focusAgent = sessions.get(focusAgentName);
focusAgent.inviteNewParticipant(invite.getFrom(), invite.getTo(), reply);
} else {
reply.setError(PacketError.Condition.not_allowed);
}
} else { } else {
reply.setError(PacketError.Condition.not_allowed); reply.setError(PacketError.Condition.not_allowed);
} }
...@@ -1090,10 +1146,13 @@ public class PluginImpl implements Plugin, PropertyEventListener ...@@ -1090,10 +1146,13 @@ public class PluginImpl implements Plugin, PropertyEventListener
private String domainName = XMPPServer.getInstance().getServerInfo().getXMPPDomain(); private String domainName = XMPPServer.getInstance().getServerInfo().getXMPPDomain();
private Recorder recorder = null; private Recorder recorder = null;
private AudioMixingPushBufferDataSource outDataSource; private AudioMixingPushBufferDataSource outDataSource;
private SSRCFactoryImpl ssrcFactory = new SSRCFactoryImpl();
private long initialLocalSSRC;
public ConcurrentHashMap<String, Participant> users = new ConcurrentHashMap<String, Participant>(); public ConcurrentHashMap<String, Participant> users = new ConcurrentHashMap<String, Participant>();
public ConcurrentHashMap<String, Participant> ids = new ConcurrentHashMap<String, Participant>(); public ConcurrentHashMap<String, Participant> ids = new ConcurrentHashMap<String, Participant>();
public ConcurrentHashMap<String, Element> ssrcs = new ConcurrentHashMap<String, Element>(); public ConcurrentHashMap<String, Element> ssrcs = new ConcurrentHashMap<String, Element>();
public ConcurrentHashMap<String, CallSession> callSessions = new ConcurrentHashMap<String, CallSession>();
/** /**
* *
...@@ -1287,7 +1346,7 @@ public class PluginImpl implements Plugin, PropertyEventListener ...@@ -1287,7 +1346,7 @@ public class PluginImpl implements Plugin, PropertyEventListener
} }
Element audioContent = conferenceIq.addElement("content").addAttribute("name", "audio"); Element audioContent = conferenceIq.addElement("content").addAttribute("name", "audio");
audioContent.addElement("channel").addAttribute("initiator", "true").addAttribute("expire", "15"); audioContent.addElement("channel").addAttribute("initiator", "true").addAttribute("expire", "15").addAttribute("rtp-level-relay-type", "mixer");
Element videoContent = conferenceIq.addElement("content").addAttribute("name", "video"); Element videoContent = conferenceIq.addElement("content").addAttribute("name", "video");
videoContent.addElement("channel").addAttribute("initiator", "true").addAttribute("expire", "15"); videoContent.addElement("channel").addAttribute("initiator", "true").addAttribute("expire", "15");
...@@ -1345,6 +1404,100 @@ public class PluginImpl implements Plugin, PropertyEventListener ...@@ -1345,6 +1404,100 @@ public class PluginImpl implements Plugin, PropertyEventListener
} }
} }
/**
*
*
*/
public void uninviteNewParticipant(String callId, IQ reply)
{
Log.info("uninviteNewParticipant " + callId);
CallSession cs = callSessions.remove(callId);
if (cs != null)
{
SipService.sendBye(cs);
} else {
reply.setError(PacketError.Condition.not_allowed);
}
}
/**
*
*
*/
public void inviteNewParticipant(URI fromUri, URI toUri, IQ reply)
{
Log.info("inviteNewParticipant " + fromUri + " " + toUri);
String from = fromUri.toString();
String to = toUri.toString();
String hostname = XMPPServer.getInstance().getServerInfo().getHostname();
boolean toSip = to.indexOf("sip:") == 0 ;
boolean toPhone = to.indexOf("tel:") == 0;
boolean toXmpp = to.indexOf("xmpp:") == 0;
if (toSip)
{
from = "sip:jitsi-videobridge" + System.currentTimeMillis() + "@" + hostname;
} else if (toPhone){
String outboundProxy = JiveGlobals.getProperty("voicebridge.default.proxy.outboundproxy", null);
String sipUsername = JiveGlobals.getProperty("voicebridge.default.proxy.sipauthuser", null);
if (outboundProxy != null && sipUsername != null)
{
to = "sip:" + to.substring(4) + "@" + outboundProxy;
from = "sip:" + sipUsername + "@" + outboundProxy;
} else {
reply.setError(PacketError.Condition.not_allowed);
return;
}
} else if (toXmpp){
reply.setError(PacketError.Condition.not_allowed);
return;
} else {
reply.setError(PacketError.Condition.not_allowed);
return;
}
Conference conference = null;
String focusJid = XMPPServer.getInstance().createJID(focusName, focusName).toString();
if (focusId != null)
{
conference = getVideoBridge().getConference(focusId, focusJid);
}
if (conference != null)
{
MediaService mediaService = LibJitsi.getMediaService();
MediaStream mediaStream = mediaService.createMediaStream(null, org.jitsi.service.neomedia.MediaType.AUDIO, mediaService.createSrtpControl(SrtpControlType.MIKEY));
mediaStream.setName("rayo-" + System.currentTimeMillis());
mediaStream.setSSRCFactory(ssrcFactory);
mediaStream.setDevice(conference.getOrCreateContent("audio").getMixer());
initialLocalSSRC = ssrcFactory.doGenerateSSRC() & 0xFFFFFFFFL;
CallSession cs = new CallSession(mediaStream, hostname);
callSessions.put(toUri.toString(), cs);
cs.jabberRemote = to;
cs.jabberLocal = from;
SipService.sendInvite(cs);
} else {
reply.setError(PacketError.Condition.not_allowed);
}
}
/** /**
* *
* *
...@@ -1423,6 +1576,15 @@ public class PluginImpl implements Plugin, PropertyEventListener ...@@ -1423,6 +1576,15 @@ public class PluginImpl implements Plugin, PropertyEventListener
recorder.done(); recorder.done();
outDataSource.disconnect(); outDataSource.disconnect();
outDataSource.stop(); outDataSource.stop();
for (CallSession callSession : callSessions.values())
{
callSession.mediaStream.stop();
callSession.mediaStream.close();
}
callSessions.clear();
} catch (Exception e) {} } catch (Exception e) {}
recorder = null; recorder = null;
...@@ -1493,7 +1655,7 @@ public class PluginImpl implements Plugin, PropertyEventListener ...@@ -1493,7 +1655,7 @@ public class PluginImpl implements Plugin, PropertyEventListener
*/ */
public void deliver(Packet packet) throws UnauthorizedException public void deliver(Packet packet) throws UnauthorizedException
{ {
Log.info("FocusAgent deliver\n" + packet + " " + getVideoBridge()); Log.info("FocusAgent deliver\n" + packet);
IQ iq = (IQ) packet; IQ iq = (IQ) packet;
...@@ -1697,5 +1859,54 @@ public class PluginImpl implements Plugin, PropertyEventListener ...@@ -1697,5 +1859,54 @@ public class PluginImpl implements Plugin, PropertyEventListener
return null; return null;
} }
/**
* Generates a new synchronization source (SSRC) identifier.
*
* @param
* @param i the number of times that the method has been executed prior to
* the current invocation
* @return a randomly chosen <tt>int</tt> value which is to be utilized as a
* new synchronization source (SSRC) identifier should it be found to be
* globally unique within the associated RTP session or
* <tt>Long.MAX_VALUE</tt> to cancel the operation
*/
private long ssrcFactoryGenerateSSRC(String cause, int i)
{
if (initialLocalSSRC != -1)
{
if (i == 0)
return (int) initialLocalSSRC;
else if (cause.equals(GenerateSSRCCause.REMOVE_SEND_STREAM.name()))
return Long.MAX_VALUE;
}
return ssrcFactory.doGenerateSSRC();
}
private class SSRCFactoryImpl implements SSRCFactory
{
private int i = 0;
/**
* The <tt>Random</tt> instance used by this <tt>SSRCFactory</tt> to
* generate new synchronization source (SSRC) identifiers.
*/
private final Random random = new Random();
public int doGenerateSSRC()
{
return random.nextInt();
}
/**
* {@inheritDoc}
*/
@Override
public long generateSSRC(String cause)
{
return ssrcFactoryGenerateSSRC(cause, i++);
}
}
} }
} }
...@@ -70,7 +70,19 @@ ...@@ -70,7 +70,19 @@
JiveGlobals.setProperty(PluginImpl.PASSWORD_PROPERTY_NAME, password); JiveGlobals.setProperty(PluginImpl.PASSWORD_PROPERTY_NAME, password);
String enabled = request.getParameter("enabled"); String enabled = request.getParameter("enabled");
JiveGlobals.setProperty(PluginImpl.RECORD_PROPERTY_NAME, enabled); JiveGlobals.setProperty(PluginImpl.RECORD_PROPERTY_NAME, enabled);
String authusername = request.getParameter("authusername");
JiveGlobals.setProperty("voicebridge.default.proxy.sipauthuser", authusername);
String sippassword = request.getParameter("sippassword");
JiveGlobals.setProperty("voicebridge.default.proxy.sippassword", sippassword);
String server = request.getParameter("server");
JiveGlobals.setProperty("voicebridge.default.proxy.sipserver", server);
String outboundproxy = request.getParameter("outboundproxy");
JiveGlobals.setProperty("voicebridge.default.proxy.outboundproxy", outboundproxy);
} }
%> %>
...@@ -93,12 +105,12 @@ ...@@ -93,12 +105,12 @@
<table class="jive-table" cellpadding="0" cellspacing="0" border="0" width="50%"> <table class="jive-table" cellpadding="0" cellspacing="0" border="0" width="50%">
<thead> <thead>
<tr> <tr>
<th colspan="2"><fmt:message key="config.page.configuration.title"/></th> <th colspan="2"><fmt:message key="config.page.configuration.media.title"/></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td><label class="jive-label"><fmt:message key="config.page.configuration.min.port"/>:</label><br> <td><fmt:message key="config.page.configuration.min.port"/><br>
</td> </td>
<td align="left"> <td align="left">
<input name="minport" type="text" maxlength="5" size="5" <input name="minport" type="text" maxlength="5" size="5"
...@@ -106,15 +118,25 @@ ...@@ -106,15 +118,25 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td><label class="jive-label"><fmt:message key="config.page.configuration.max.port"/>:</label><br> <td><fmt:message key="config.page.configuration.max.port"/><br>
</td> </td>
<td align="left"> <td align="left">
<input name="maxport" type="text" maxlength="5" size="5" <input name="maxport" type="text" maxlength="5" size="5"
value="<%=plugin.getMaxPort()%>"/> value="<%=plugin.getMaxPort()%>"/>
</td> </td>
</tr> </tr>
</tbody>
</table>
<p/>
<table class="jive-table" cellpadding="0" cellspacing="0" border="0" width="50%">
<thead>
<tr>
<th colspan="2"><fmt:message key="config.page.configuration.security.title"/></th>
</tr>
</thead>
<tbody>
<tr> <tr>
<td><label class="jive-label"><fmt:message key="config.page.configuration.username"/>:</label><br> <td><fmt:message key="config.page.configuration.username"/><br>
</td> </td>
<td align="left"> <td align="left">
<input name="username" type="text" maxlength="16" size="16" <input name="username" type="text" maxlength="16" size="16"
...@@ -122,13 +144,23 @@ ...@@ -122,13 +144,23 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td><label class="jive-label"><fmt:message key="config.page.configuration.password"/>:</label><br> <td><fmt:message key="config.page.configuration.password"/><br>
</td> </td>
<td align="left"> <td align="left">
<input name="password" type="password" maxlength="16" size="16" <input name="password" type="password" maxlength="16" size="16"
value="<%=JiveGlobals.getProperty(PluginImpl.PASSWORD_PROPERTY_NAME, "jitsi")%>"/> value="<%=JiveGlobals.getProperty(PluginImpl.PASSWORD_PROPERTY_NAME, "jitsi")%>"/>
</td> </td>
</tr> </tr>
</tbody>
</table>
<p/>
<table class="jive-table" cellpadding="0" cellspacing="0" border="0" width="50%">
<thead>
<tr>
<th colspan="2"><fmt:message key="config.page.configuration.recording.title"/></th>
</tr>
</thead>
<tbody>
<tr> <tr>
<td nowrap colspan="2"> <td nowrap colspan="2">
<input type="radio" value="false" name="enabled" <%= ("false".equals(JiveGlobals.getProperty(PluginImpl.RECORD_PROPERTY_NAME, "false")) ? "checked" : "") %>> <input type="radio" value="false" name="enabled" <%= ("false".equals(JiveGlobals.getProperty(PluginImpl.RECORD_PROPERTY_NAME, "false")) ? "checked" : "") %>>
...@@ -141,12 +173,66 @@ ...@@ -141,12 +173,66 @@
<b><fmt:message key="config.page.configuration.record.enabled" /></b> - <fmt:message key="config.page.configuration.record.enabled_description" /> <b><fmt:message key="config.page.configuration.record.enabled" /></b> - <fmt:message key="config.page.configuration.record.enabled_description" />
</td> </td>
</tr> </tr>
</tbody>
</table>
<p/>
<table class="jive-table" cellpadding="0" cellspacing="0" border="0" width="50%">
<thead>
<tr> <tr>
<th colspan="2"><input type="submit" name="update" <th><fmt:message key="config.page.configuration.telephone.title"/></th>
value="<fmt:message key="config.page.configuration.submit" />"></th> <th><%= plugin.sipRegisterStatus %></th>
</tr> </tr>
</tbody> </thead>
<tbody>
<tr>
<td align="left" width="150">
<fmt:message key="config.page.configuration.authusername"/>
</td>
<td><input type="text" size="20" maxlength="100" name="authusername"
value="<%= JiveGlobals.getProperty("voicebridge.default.proxy.sipauthuser", "") %>">
</td>
</tr>
<tr>
<td align="left" width="150">
<fmt:message key="config.page.configuration.sippassword"/>
</td>
<td><input type="password" size="20" maxlength="100" name="sippassword"
value="<%= JiveGlobals.getProperty("voicebridge.default.proxy.sippassword", "") %>">
</td>
</tr>
<tr>
<td align="left" width="150">
<fmt:message key="config.page.configuration.server"/>
</td>
<td><input type="text" size="20" maxlength="100" name="server"
value="<%= JiveGlobals.getProperty("voicebridge.default.proxy.sipserver", "") %>">
</td>
</tr>
<tr>
<td align="left" width="150">
<fmt:message key="config.page.configuration.outboundproxy"/>
</td>
<td><input type="text" size="20" maxlength="100" name="outboundproxy"
value="<%= JiveGlobals.getProperty("voicebridge.default.proxy.outboundproxy", "") %>">
</td>
</tr>
</table> </table>
<p/>
<table class="jive-table" cellpadding="0" cellspacing="0" border="0" width="50%">
<thead>
<tr>
<th colspan="2"><fmt:message key="config.page.configuration.save.title"/></th>
</tr>
</thead>
<tbody>
<tr>
<th colspan="2"><input type="submit" name="update" value="<fmt:message key="config.page.configuration.submit" />"><fmt:message key="config.page.configuration.restart.warning"/></th>
</tr>
</tbody>
</table>
</form> </form>
</div> </div>
......
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