removed xmpp and RTC modules
This commit is contained in:
parent
6ee6b6e9e5
commit
c36001f861
|
@ -1,211 +0,0 @@
|
||||||
/* global config, APP, Strophe */
|
|
||||||
/* jshint -W101 */
|
|
||||||
|
|
||||||
// cache datachannels to avoid garbage collection
|
|
||||||
// https://code.google.com/p/chromium/issues/detail?id=405545
|
|
||||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
|
||||||
|
|
||||||
var _dataChannels = [];
|
|
||||||
var eventEmitter = null;
|
|
||||||
|
|
||||||
var DataChannels = {
|
|
||||||
/**
|
|
||||||
* Callback triggered by PeerConnection when new data channel is opened
|
|
||||||
* on the bridge.
|
|
||||||
* @param event the event info object.
|
|
||||||
*/
|
|
||||||
onDataChannel: function (event) {
|
|
||||||
var dataChannel = event.channel;
|
|
||||||
|
|
||||||
dataChannel.onopen = function () {
|
|
||||||
console.info("Data channel opened by the Videobridge!", dataChannel);
|
|
||||||
|
|
||||||
// Code sample for sending string and/or binary data
|
|
||||||
// Sends String message to the bridge
|
|
||||||
//dataChannel.send("Hello bridge!");
|
|
||||||
// Sends 12 bytes binary message to the bridge
|
|
||||||
//dataChannel.send(new ArrayBuffer(12));
|
|
||||||
|
|
||||||
eventEmitter.emit(RTCEvents.DATA_CHANNEL_OPEN);
|
|
||||||
};
|
|
||||||
|
|
||||||
dataChannel.onerror = function (error) {
|
|
||||||
console.error("Data Channel Error:", error, dataChannel);
|
|
||||||
};
|
|
||||||
|
|
||||||
dataChannel.onmessage = function (event) {
|
|
||||||
var data = event.data;
|
|
||||||
// JSON
|
|
||||||
var obj;
|
|
||||||
|
|
||||||
try {
|
|
||||||
obj = JSON.parse(data);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error(
|
|
||||||
"Failed to parse data channel message as JSON: ",
|
|
||||||
data,
|
|
||||||
dataChannel);
|
|
||||||
}
|
|
||||||
if (('undefined' !== typeof(obj)) && (null !== obj)) {
|
|
||||||
var colibriClass = obj.colibriClass;
|
|
||||||
|
|
||||||
if ("DominantSpeakerEndpointChangeEvent" === colibriClass) {
|
|
||||||
// Endpoint ID from the Videobridge.
|
|
||||||
var dominantSpeakerEndpoint = obj.dominantSpeakerEndpoint;
|
|
||||||
|
|
||||||
console.info(
|
|
||||||
"Data channel new dominant speaker event: ",
|
|
||||||
dominantSpeakerEndpoint);
|
|
||||||
eventEmitter.emit(RTCEvents.DOMINANTSPEAKER_CHANGED, dominantSpeakerEndpoint);
|
|
||||||
}
|
|
||||||
else if ("InLastNChangeEvent" === colibriClass) {
|
|
||||||
var oldValue = obj.oldValue;
|
|
||||||
var newValue = obj.newValue;
|
|
||||||
// Make sure that oldValue and newValue are of type boolean.
|
|
||||||
var type;
|
|
||||||
|
|
||||||
if ((type = typeof oldValue) !== 'boolean') {
|
|
||||||
if (type === 'string') {
|
|
||||||
oldValue = (oldValue == "true");
|
|
||||||
} else {
|
|
||||||
oldValue = Boolean(oldValue).valueOf();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((type = typeof newValue) !== 'boolean') {
|
|
||||||
if (type === 'string') {
|
|
||||||
newValue = (newValue == "true");
|
|
||||||
} else {
|
|
||||||
newValue = Boolean(newValue).valueOf();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eventEmitter.emit(RTCEvents.LASTN_CHANGED, oldValue, newValue);
|
|
||||||
}
|
|
||||||
else if ("LastNEndpointsChangeEvent" === colibriClass) {
|
|
||||||
// The new/latest list of last-n endpoint IDs.
|
|
||||||
var lastNEndpoints = obj.lastNEndpoints;
|
|
||||||
// The list of endpoint IDs which are entering the list of
|
|
||||||
// last-n at this time i.e. were not in the old list of last-n
|
|
||||||
// endpoint IDs.
|
|
||||||
var endpointsEnteringLastN = obj.endpointsEnteringLastN;
|
|
||||||
|
|
||||||
console.info(
|
|
||||||
"Data channel new last-n event: ",
|
|
||||||
lastNEndpoints, endpointsEnteringLastN, obj);
|
|
||||||
eventEmitter.emit(RTCEvents.LASTN_ENDPOINT_CHANGED,
|
|
||||||
lastNEndpoints, endpointsEnteringLastN, obj);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.debug("Data channel JSON-formatted message: ", obj);
|
|
||||||
// The received message appears to be appropriately
|
|
||||||
// formatted (i.e. is a JSON object which assigns a value to
|
|
||||||
// the mandatory property colibriClass) so don't just
|
|
||||||
// swallow it, expose it to public consumption.
|
|
||||||
eventEmitter.emit("rtc.datachannel." + colibriClass, obj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
dataChannel.onclose = function () {
|
|
||||||
console.info("The Data Channel closed", dataChannel);
|
|
||||||
var idx = _dataChannels.indexOf(dataChannel);
|
|
||||||
if (idx > -1)
|
|
||||||
_dataChannels = _dataChannels.splice(idx, 1);
|
|
||||||
};
|
|
||||||
_dataChannels.push(dataChannel);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds "ondatachannel" event listener to given PeerConnection instance.
|
|
||||||
* @param peerConnection WebRTC peer connection instance.
|
|
||||||
*/
|
|
||||||
init: function (peerConnection, emitter) {
|
|
||||||
if(!config.openSctp)
|
|
||||||
return;
|
|
||||||
|
|
||||||
peerConnection.ondatachannel = this.onDataChannel;
|
|
||||||
eventEmitter = emitter;
|
|
||||||
|
|
||||||
// Sample code for opening new data channel from Jitsi Meet to the bridge.
|
|
||||||
// Although it's not a requirement to open separate channels from both bridge
|
|
||||||
// and peer as single channel can be used for sending and receiving data.
|
|
||||||
// So either channel opened by the bridge or the one opened here is enough
|
|
||||||
// for communication with the bridge.
|
|
||||||
/*var dataChannelOptions =
|
|
||||||
{
|
|
||||||
reliable: true
|
|
||||||
};
|
|
||||||
var dataChannel
|
|
||||||
= peerConnection.createDataChannel("myChannel", dataChannelOptions);
|
|
||||||
|
|
||||||
// Can be used only when is in open state
|
|
||||||
dataChannel.onopen = function ()
|
|
||||||
{
|
|
||||||
dataChannel.send("My channel !!!");
|
|
||||||
};
|
|
||||||
dataChannel.onmessage = function (event)
|
|
||||||
{
|
|
||||||
var msgData = event.data;
|
|
||||||
console.info("Got My Data Channel Message:", msgData, dataChannel);
|
|
||||||
};*/
|
|
||||||
},
|
|
||||||
|
|
||||||
handleSelectedEndpointEvent: function (userResource) {
|
|
||||||
onXXXEndpointChanged("selected", userResource);
|
|
||||||
},
|
|
||||||
handlePinnedEndpointEvent: function (userResource) {
|
|
||||||
onXXXEndpointChanged("pinned", userResource);
|
|
||||||
},
|
|
||||||
|
|
||||||
some: function (callback, thisArg) {
|
|
||||||
if (_dataChannels && _dataChannels.length !== 0) {
|
|
||||||
if (thisArg)
|
|
||||||
return _dataChannels.some(callback, thisArg);
|
|
||||||
else
|
|
||||||
return _dataChannels.some(callback);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies Videobridge about a change in the value of a specific
|
|
||||||
* endpoint-related property such as selected endpoint and pinnned endpoint.
|
|
||||||
*
|
|
||||||
* @param xxx the name of the endpoint-related property whose value changed
|
|
||||||
* @param userResource the new value of the endpoint-related property after the
|
|
||||||
* change
|
|
||||||
*/
|
|
||||||
function onXXXEndpointChanged(xxx, userResource) {
|
|
||||||
// Derive the correct words from xxx such as selected and Selected, pinned
|
|
||||||
// and Pinned.
|
|
||||||
var head = xxx.charAt(0);
|
|
||||||
var tail = xxx.substring(1);
|
|
||||||
var lower = head.toLowerCase() + tail;
|
|
||||||
var upper = head.toUpperCase() + tail;
|
|
||||||
|
|
||||||
// Notify Videobridge about the specified endpoint change.
|
|
||||||
console.log(lower + ' endpoint changed: ', userResource);
|
|
||||||
DataChannels.some(function (dataChannel) {
|
|
||||||
if (dataChannel.readyState == 'open') {
|
|
||||||
console.log(
|
|
||||||
'sending ' + lower
|
|
||||||
+ ' endpoint changed notification to the bridge: ',
|
|
||||||
userResource);
|
|
||||||
|
|
||||||
var jsonObject = {};
|
|
||||||
|
|
||||||
jsonObject.colibriClass = (upper + 'EndpointChangedEvent');
|
|
||||||
jsonObject[lower + "Endpoint"]
|
|
||||||
= (userResource ? userResource : null);
|
|
||||||
dataChannel.send(JSON.stringify(jsonObject));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = DataChannels;
|
|
||||||
|
|
|
@ -1,145 +0,0 @@
|
||||||
/* global APP */
|
|
||||||
var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
|
|
||||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
|
||||||
var RTCBrowserType = require("./RTCBrowserType");
|
|
||||||
var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This implements 'onended' callback normally fired by WebRTC after the stream
|
|
||||||
* is stopped. There is no such behaviour yet in FF, so we have to add it.
|
|
||||||
* @param stream original WebRTC stream object to which 'onended' handling
|
|
||||||
* will be added.
|
|
||||||
*/
|
|
||||||
function implementOnEndedHandling(localStream) {
|
|
||||||
var stream = localStream.getOriginalStream();
|
|
||||||
var originalStop = stream.stop;
|
|
||||||
stream.stop = function () {
|
|
||||||
originalStop.apply(stream);
|
|
||||||
if (localStream.isActive()) {
|
|
||||||
stream.onended();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function LocalStream(stream, type, eventEmitter, videoType, isGUMStream) {
|
|
||||||
this.stream = stream;
|
|
||||||
this.eventEmitter = eventEmitter;
|
|
||||||
this.type = type;
|
|
||||||
this.videoType = videoType;
|
|
||||||
this.isGUMStream = true;
|
|
||||||
if(isGUMStream === false)
|
|
||||||
this.isGUMStream = isGUMStream;
|
|
||||||
var self = this;
|
|
||||||
if (MediaStreamType.AUDIO_TYPE === type) {
|
|
||||||
this.getTracks = function () {
|
|
||||||
return self.stream.getAudioTracks();
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
this.getTracks = function () {
|
|
||||||
return self.stream.getVideoTracks();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
APP.RTC.addMediaStreamInactiveHandler(
|
|
||||||
this.stream,
|
|
||||||
function () {
|
|
||||||
self.streamEnded();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (RTCBrowserType.isFirefox()) {
|
|
||||||
implementOnEndedHandling(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalStream.prototype.streamEnded = function () {
|
|
||||||
this.eventEmitter.emit(StreamEventTypes.EVENT_TYPE_LOCAL_ENDED, this);
|
|
||||||
};
|
|
||||||
|
|
||||||
LocalStream.prototype.getOriginalStream = function()
|
|
||||||
{
|
|
||||||
return this.stream;
|
|
||||||
};
|
|
||||||
|
|
||||||
LocalStream.prototype.isAudioStream = function () {
|
|
||||||
return MediaStreamType.AUDIO_TYPE === this.type;
|
|
||||||
};
|
|
||||||
|
|
||||||
LocalStream.prototype.isVideoStream = function () {
|
|
||||||
return MediaStreamType.VIDEO_TYPE === this.type;
|
|
||||||
};
|
|
||||||
|
|
||||||
LocalStream.prototype.setMute = function (mute)
|
|
||||||
{
|
|
||||||
var isAudio = this.isAudioStream();
|
|
||||||
var eventType = isAudio ? RTCEvents.AUDIO_MUTE : RTCEvents.VIDEO_MUTE;
|
|
||||||
|
|
||||||
if ((window.location.protocol != "https:" && this.isGUMStream) ||
|
|
||||||
(isAudio && this.isGUMStream) || this.videoType === "screen" ||
|
|
||||||
// FIXME FF does not support 'removeStream' method used to mute
|
|
||||||
RTCBrowserType.isFirefox()) {
|
|
||||||
|
|
||||||
var tracks = this.getTracks();
|
|
||||||
for (var idx = 0; idx < tracks.length; idx++) {
|
|
||||||
tracks[idx].enabled = !mute;
|
|
||||||
}
|
|
||||||
this.eventEmitter.emit(eventType, mute);
|
|
||||||
} else {
|
|
||||||
if (mute) {
|
|
||||||
APP.xmpp.removeStream(this.stream);
|
|
||||||
APP.RTC.stopMediaStream(this.stream);
|
|
||||||
this.eventEmitter.emit(eventType, true);
|
|
||||||
} else {
|
|
||||||
var self = this;
|
|
||||||
APP.RTC.rtcUtils.obtainAudioAndVideoPermissions(
|
|
||||||
(this.isAudioStream() ? ["audio"] : ["video"]),
|
|
||||||
function (stream) {
|
|
||||||
if (isAudio) {
|
|
||||||
APP.RTC.changeLocalAudio(stream,
|
|
||||||
function () {
|
|
||||||
self.eventEmitter.emit(eventType, false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
APP.RTC.changeLocalVideo(stream, false,
|
|
||||||
function () {
|
|
||||||
self.eventEmitter.emit(eventType, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
LocalStream.prototype.isMuted = function () {
|
|
||||||
var tracks = [];
|
|
||||||
if (this.isAudioStream()) {
|
|
||||||
tracks = this.stream.getAudioTracks();
|
|
||||||
} else {
|
|
||||||
if (!this.isActive())
|
|
||||||
return true;
|
|
||||||
tracks = this.stream.getVideoTracks();
|
|
||||||
}
|
|
||||||
for (var idx = 0; idx < tracks.length; idx++) {
|
|
||||||
if(tracks[idx].enabled)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
LocalStream.prototype.getId = function () {
|
|
||||||
return this.stream.getTracks()[0].id;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the MediaStream is avtive/not ended.
|
|
||||||
* When there is no check for active we don't have information and so
|
|
||||||
* will return that stream is active (in case of FF).
|
|
||||||
* @returns {boolean} whether MediaStream is active.
|
|
||||||
*/
|
|
||||||
LocalStream.prototype.isActive = function () {
|
|
||||||
if((typeof this.stream.active !== "undefined"))
|
|
||||||
return this.stream.active;
|
|
||||||
else
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = LocalStream;
|
|
|
@ -1,57 +0,0 @@
|
||||||
var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a MediaStream object for the given data, session id and ssrc.
|
|
||||||
* It is a wrapper class for the MediaStream.
|
|
||||||
*
|
|
||||||
* @param data the data object from which we obtain the stream,
|
|
||||||
* the peerjid, etc.
|
|
||||||
* @param ssrc the ssrc corresponding to this MediaStream
|
|
||||||
* @param mute the whether this MediaStream is muted
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
function MediaStream(data, ssrc, browser, eventEmitter, muted, type) {
|
|
||||||
|
|
||||||
// XXX(gp) to minimize headaches in the future, we should build our
|
|
||||||
// abstractions around tracks and not streams. ORTC is track based API.
|
|
||||||
// Mozilla expects m-lines to represent media tracks.
|
|
||||||
//
|
|
||||||
// Practically, what I'm saying is that we should have a MediaTrack class
|
|
||||||
// and not a MediaStream class.
|
|
||||||
//
|
|
||||||
// Also, we should be able to associate multiple SSRCs with a MediaTrack as
|
|
||||||
// a track might have an associated RTX and FEC sources.
|
|
||||||
|
|
||||||
if (!type) {
|
|
||||||
console.log("Errrm...some code needs an update...");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.stream = data.stream;
|
|
||||||
this.peerjid = data.peerjid;
|
|
||||||
this.videoType = data.videoType;
|
|
||||||
this.ssrc = ssrc;
|
|
||||||
this.type = type;
|
|
||||||
this.muted = muted;
|
|
||||||
this.eventEmitter = eventEmitter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME duplicated with LocalStream methods - extract base class
|
|
||||||
MediaStream.prototype.isAudioStream = function () {
|
|
||||||
return MediaStreamType.AUDIO_TYPE === this.type;
|
|
||||||
};
|
|
||||||
|
|
||||||
MediaStream.prototype.isVideoStream = function () {
|
|
||||||
return MediaStreamType.VIDEO_TYPE === this.type;
|
|
||||||
};
|
|
||||||
|
|
||||||
MediaStream.prototype.getOriginalStream = function () {
|
|
||||||
return this.stream;
|
|
||||||
};
|
|
||||||
|
|
||||||
MediaStream.prototype.setMute = function (value) {
|
|
||||||
this.stream.muted = value;
|
|
||||||
this.muted = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = MediaStream;
|
|
|
@ -1,335 +0,0 @@
|
||||||
/* global APP */
|
|
||||||
var EventEmitter = require("events");
|
|
||||||
var RTCBrowserType = require("./RTCBrowserType");
|
|
||||||
var RTCUtils = require("./RTCUtils.js");
|
|
||||||
var LocalStream = require("./LocalStream.js");
|
|
||||||
var DataChannels = require("./DataChannels");
|
|
||||||
var MediaStream = require("./MediaStream.js");
|
|
||||||
var DesktopSharingEventTypes
|
|
||||||
= require("../../service/desktopsharing/DesktopSharingEventTypes");
|
|
||||||
var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
|
|
||||||
var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js");
|
|
||||||
var RTCEvents = require("../../service/RTC/RTCEvents.js");
|
|
||||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
|
||||||
var UIEvents = require("../../service/UI/UIEvents");
|
|
||||||
|
|
||||||
var eventEmitter = new EventEmitter();
|
|
||||||
|
|
||||||
|
|
||||||
function getMediaStreamUsage()
|
|
||||||
{
|
|
||||||
var result = {
|
|
||||||
audio: true,
|
|
||||||
video: true
|
|
||||||
};
|
|
||||||
|
|
||||||
/** There are some issues with the desktop sharing
|
|
||||||
* when this property is enabled.
|
|
||||||
* WARNING: We must change the implementation to start video/audio if we
|
|
||||||
* receive from the focus that the peer is not muted.
|
|
||||||
|
|
||||||
var isSecureConnection = window.location.protocol == "https:";
|
|
||||||
|
|
||||||
if(config.disableEarlyMediaPermissionRequests || !isSecureConnection)
|
|
||||||
{
|
|
||||||
result = {
|
|
||||||
audio: false,
|
|
||||||
video: false
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
**/
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
var RTC = {
|
|
||||||
// Exposes DataChannels to public consumption (e.g. jitsi-meet-torture)
|
|
||||||
// without the necessity to require the module.
|
|
||||||
"DataChannels": DataChannels,
|
|
||||||
|
|
||||||
rtcUtils: null,
|
|
||||||
devices: {
|
|
||||||
audio: true,
|
|
||||||
video: true
|
|
||||||
},
|
|
||||||
remoteStreams: {},
|
|
||||||
localAudio: null,
|
|
||||||
localVideo: null,
|
|
||||||
addStreamListener: function (listener, eventType) {
|
|
||||||
eventEmitter.on(eventType, listener);
|
|
||||||
},
|
|
||||||
addListener: function (type, listener) {
|
|
||||||
eventEmitter.on(type, listener);
|
|
||||||
},
|
|
||||||
removeStreamListener: function (listener, eventType) {
|
|
||||||
if(!(eventType instanceof StreamEventTypes))
|
|
||||||
throw "Illegal argument";
|
|
||||||
|
|
||||||
eventEmitter.removeListener(eventType, listener);
|
|
||||||
},
|
|
||||||
createLocalStream: function (stream, type, change, videoType,
|
|
||||||
isMuted, isGUMStream) {
|
|
||||||
|
|
||||||
var localStream =
|
|
||||||
new LocalStream(stream, type, eventEmitter, videoType, isGUMStream);
|
|
||||||
if(isMuted === true)
|
|
||||||
localStream.setMute(true);
|
|
||||||
|
|
||||||
if (MediaStreamType.AUDIO_TYPE === type) {
|
|
||||||
this.localAudio = localStream;
|
|
||||||
} else {
|
|
||||||
this.localVideo = localStream;
|
|
||||||
}
|
|
||||||
var eventType = StreamEventTypes.EVENT_TYPE_LOCAL_CREATED;
|
|
||||||
if(change)
|
|
||||||
eventType = StreamEventTypes.EVENT_TYPE_LOCAL_CHANGED;
|
|
||||||
|
|
||||||
eventEmitter.emit(eventType, localStream, isMuted);
|
|
||||||
return localStream;
|
|
||||||
},
|
|
||||||
createRemoteStream: function (data, ssrc) {
|
|
||||||
var jid = data.peerjid || APP.xmpp.myJid();
|
|
||||||
|
|
||||||
// check the video muted state from last stored presence if any
|
|
||||||
var muted = false;
|
|
||||||
var pres = APP.xmpp.getLastPresence(jid);
|
|
||||||
if (pres && pres.videoMuted) {
|
|
||||||
muted = pres.videoMuted;
|
|
||||||
}
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
[MediaStreamType.AUDIO_TYPE, MediaStreamType.VIDEO_TYPE].forEach(
|
|
||||||
function (type) {
|
|
||||||
var tracks =
|
|
||||||
type == MediaStreamType.AUDIO_TYPE
|
|
||||||
? data.stream.getAudioTracks() : data.stream.getVideoTracks();
|
|
||||||
if (!tracks || !Array.isArray(tracks) || !tracks.length) {
|
|
||||||
console.log("Not creating a(n) " + type + " stream: no tracks");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var remoteStream = new MediaStream(data, ssrc,
|
|
||||||
RTCBrowserType.getBrowserType(), eventEmitter, muted, type);
|
|
||||||
|
|
||||||
if (!self.remoteStreams[jid]) {
|
|
||||||
self.remoteStreams[jid] = {};
|
|
||||||
}
|
|
||||||
self.remoteStreams[jid][type] = remoteStream;
|
|
||||||
eventEmitter.emit(StreamEventTypes.EVENT_TYPE_REMOTE_CREATED,
|
|
||||||
remoteStream);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getPCConstraints: function () {
|
|
||||||
return this.rtcUtils.pc_constraints;
|
|
||||||
},
|
|
||||||
getUserMediaWithConstraints:function(um, success_callback,
|
|
||||||
failure_callback, resolution,
|
|
||||||
bandwidth, fps, desktopStream)
|
|
||||||
{
|
|
||||||
return this.rtcUtils.getUserMediaWithConstraints(um, success_callback,
|
|
||||||
failure_callback, resolution, bandwidth, fps, desktopStream);
|
|
||||||
},
|
|
||||||
attachMediaStream: function (elSelector, stream) {
|
|
||||||
this.rtcUtils.attachMediaStream(elSelector, stream);
|
|
||||||
},
|
|
||||||
getStreamID: function (stream) {
|
|
||||||
return this.rtcUtils.getStreamID(stream);
|
|
||||||
},
|
|
||||||
getVideoSrc: function (element) {
|
|
||||||
return this.rtcUtils.getVideoSrc(element);
|
|
||||||
},
|
|
||||||
setVideoSrc: function (element, src) {
|
|
||||||
this.rtcUtils.setVideoSrc(element, src);
|
|
||||||
},
|
|
||||||
getVideoElementName: function () {
|
|
||||||
return RTCBrowserType.isTemasysPluginUsed() ? 'object' : 'video';
|
|
||||||
},
|
|
||||||
dispose: function() {
|
|
||||||
if (this.rtcUtils) {
|
|
||||||
this.rtcUtils = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
stop: function () {
|
|
||||||
this.dispose();
|
|
||||||
},
|
|
||||||
start: function () {
|
|
||||||
var self = this;
|
|
||||||
APP.desktopsharing.addListener(
|
|
||||||
DesktopSharingEventTypes.NEW_STREAM_CREATED,
|
|
||||||
function (stream, isUsingScreenStream, callback) {
|
|
||||||
self.changeLocalVideo(stream, isUsingScreenStream, callback);
|
|
||||||
});
|
|
||||||
APP.xmpp.addListener(XMPPEvents.CALL_INCOMING, function(event) {
|
|
||||||
DataChannels.init(event.peerconnection, eventEmitter);
|
|
||||||
});
|
|
||||||
APP.UI.addListener(UIEvents.SELECTED_ENDPOINT,
|
|
||||||
DataChannels.handleSelectedEndpointEvent);
|
|
||||||
APP.UI.addListener(UIEvents.PINNED_ENDPOINT,
|
|
||||||
DataChannels.handlePinnedEndpointEvent);
|
|
||||||
|
|
||||||
// In case of IE we continue from 'onReady' callback
|
|
||||||
// passed to RTCUtils constructor. It will be invoked by Temasys plugin
|
|
||||||
// once it is initialized.
|
|
||||||
var onReady = function () {
|
|
||||||
eventEmitter.emit(RTCEvents.RTC_READY, true);
|
|
||||||
self.rtcUtils.obtainAudioAndVideoPermissions(
|
|
||||||
null, null, getMediaStreamUsage());
|
|
||||||
};
|
|
||||||
|
|
||||||
this.rtcUtils = new RTCUtils(this, eventEmitter, onReady);
|
|
||||||
|
|
||||||
// Call onReady() if Temasys plugin is not used
|
|
||||||
if (!RTCBrowserType.isTemasysPluginUsed()) {
|
|
||||||
onReady();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
muteRemoteVideoStream: function (jid, value) {
|
|
||||||
var stream;
|
|
||||||
|
|
||||||
if(this.remoteStreams[jid] &&
|
|
||||||
this.remoteStreams[jid][MediaStreamType.VIDEO_TYPE]) {
|
|
||||||
stream = this.remoteStreams[jid][MediaStreamType.VIDEO_TYPE];
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!stream)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (value != stream.muted) {
|
|
||||||
stream.setMute(value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
changeLocalVideo: function (stream, isUsingScreenStream, callback) {
|
|
||||||
var oldStream = this.localVideo.getOriginalStream();
|
|
||||||
var type = (isUsingScreenStream ? "screen" : "camera");
|
|
||||||
var localCallback = callback;
|
|
||||||
if(this.localVideo.isMuted() && this.localVideo.videoType !== type) {
|
|
||||||
localCallback = function() {
|
|
||||||
APP.xmpp.setVideoMute(false, function(mute) {
|
|
||||||
eventEmitter.emit(RTCEvents.VIDEO_MUTE, mute);
|
|
||||||
});
|
|
||||||
|
|
||||||
callback();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// FIXME: Workaround for FF/IE/Safari
|
|
||||||
if (stream && stream.videoStream) {
|
|
||||||
stream = stream.videoStream;
|
|
||||||
}
|
|
||||||
var videoStream = this.rtcUtils.createStream(stream, true);
|
|
||||||
this.localVideo =
|
|
||||||
this.createLocalStream(videoStream, "video", true, type);
|
|
||||||
// Stop the stream
|
|
||||||
this.stopMediaStream(oldStream);
|
|
||||||
|
|
||||||
APP.xmpp.switchStreams(videoStream, oldStream,localCallback);
|
|
||||||
},
|
|
||||||
changeLocalAudio: function (stream, callback) {
|
|
||||||
var oldStream = this.localAudio.getOriginalStream();
|
|
||||||
var newStream = this.rtcUtils.createStream(stream);
|
|
||||||
this.localAudio
|
|
||||||
= this.createLocalStream(
|
|
||||||
newStream, MediaStreamType.AUDIO_TYPE, true);
|
|
||||||
// Stop the stream
|
|
||||||
this.stopMediaStream(oldStream);
|
|
||||||
APP.xmpp.switchStreams(newStream, oldStream, callback, true);
|
|
||||||
},
|
|
||||||
isVideoMuted: function (jid) {
|
|
||||||
if (jid === APP.xmpp.myJid()) {
|
|
||||||
var localVideo = APP.RTC.localVideo;
|
|
||||||
return (!localVideo || localVideo.isMuted());
|
|
||||||
} else {
|
|
||||||
if (!APP.RTC.remoteStreams[jid] ||
|
|
||||||
!APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE]) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE].muted;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setVideoMute: function (mute, callback, options) {
|
|
||||||
if (!this.localVideo)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (mute == APP.RTC.localVideo.isMuted())
|
|
||||||
{
|
|
||||||
APP.xmpp.sendVideoInfoPresence(mute);
|
|
||||||
if (callback)
|
|
||||||
callback(mute);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
APP.RTC.localVideo.setMute(mute);
|
|
||||||
APP.xmpp.setVideoMute(
|
|
||||||
mute,
|
|
||||||
callback,
|
|
||||||
options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setDeviceAvailability: function (devices) {
|
|
||||||
if(!devices)
|
|
||||||
return;
|
|
||||||
if(devices.audio === true || devices.audio === false)
|
|
||||||
this.devices.audio = devices.audio;
|
|
||||||
if(devices.video === true || devices.video === false)
|
|
||||||
this.devices.video = devices.video;
|
|
||||||
eventEmitter.emit(RTCEvents.AVAILABLE_DEVICES_CHANGED, this.devices);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* A method to handle stopping of the stream.
|
|
||||||
* One point to handle the differences in various implementations.
|
|
||||||
* @param mediaStream MediaStream object to stop.
|
|
||||||
*/
|
|
||||||
stopMediaStream: function (mediaStream) {
|
|
||||||
mediaStream.getTracks().forEach(function (track) {
|
|
||||||
// stop() not supported with IE
|
|
||||||
if (track.stop) {
|
|
||||||
track.stop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// leave stop for implementation still using it
|
|
||||||
if (mediaStream.stop) {
|
|
||||||
mediaStream.stop();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Adds onended/inactive handler to a MediaStream.
|
|
||||||
* @param mediaStream a MediaStream to attach onended/inactive handler
|
|
||||||
* @param handler the handler
|
|
||||||
*/
|
|
||||||
addMediaStreamInactiveHandler: function (mediaStream, handler) {
|
|
||||||
if (mediaStream.addEventListener) {
|
|
||||||
// chrome
|
|
||||||
if(typeof mediaStream.active !== "undefined")
|
|
||||||
mediaStream.oninactive = handler;
|
|
||||||
else
|
|
||||||
mediaStream.onended = handler;
|
|
||||||
} else {
|
|
||||||
// themasys
|
|
||||||
mediaStream.attachEvent('ended', function () {
|
|
||||||
handler(mediaStream);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Removes onended/inactive handler.
|
|
||||||
* @param mediaStream the MediaStream to remove the handler from.
|
|
||||||
* @param handler the handler to remove.
|
|
||||||
*/
|
|
||||||
removeMediaStreamInactiveHandler: function (mediaStream, handler) {
|
|
||||||
if (mediaStream.removeEventListener) {
|
|
||||||
// chrome
|
|
||||||
if(typeof mediaStream.active !== "undefined")
|
|
||||||
mediaStream.oninactive = null;
|
|
||||||
else
|
|
||||||
mediaStream.onended = null;
|
|
||||||
} else {
|
|
||||||
// themasys
|
|
||||||
mediaStream.detachEvent('ended', handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = RTC;
|
|
|
@ -1,574 +0,0 @@
|
||||||
/* global APP, config, require, attachMediaStream, getUserMedia,
|
|
||||||
RTCPeerConnection, webkitMediaStream, webkitURL, webkitRTCPeerConnection,
|
|
||||||
mozRTCIceCandidate, mozRTCSessionDescription, mozRTCPeerConnection */
|
|
||||||
/* jshint -W101 */
|
|
||||||
var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
|
|
||||||
var RTCBrowserType = require("./RTCBrowserType");
|
|
||||||
var Resolutions = require("../../service/RTC/Resolutions");
|
|
||||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
|
||||||
var AdapterJS = require("./adapter.screenshare");
|
|
||||||
|
|
||||||
var currentResolution = null;
|
|
||||||
|
|
||||||
function getPreviousResolution(resolution) {
|
|
||||||
if(!Resolutions[resolution])
|
|
||||||
return null;
|
|
||||||
var order = Resolutions[resolution].order;
|
|
||||||
var res = null;
|
|
||||||
var resName = null;
|
|
||||||
for(var i in Resolutions) {
|
|
||||||
var tmp = Resolutions[i];
|
|
||||||
if (!res || (res.order < tmp.order && tmp.order < order)) {
|
|
||||||
resName = i;
|
|
||||||
res = tmp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resName;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setResolutionConstraints(constraints, resolution) {
|
|
||||||
var isAndroid = RTCBrowserType.isAndroid();
|
|
||||||
|
|
||||||
if (Resolutions[resolution]) {
|
|
||||||
constraints.video.mandatory.minWidth = Resolutions[resolution].width;
|
|
||||||
constraints.video.mandatory.minHeight = Resolutions[resolution].height;
|
|
||||||
}
|
|
||||||
else if (isAndroid) {
|
|
||||||
// FIXME can't remember if the purpose of this was to always request
|
|
||||||
// low resolution on Android ? if yes it should be moved up front
|
|
||||||
constraints.video.mandatory.minWidth = 320;
|
|
||||||
constraints.video.mandatory.minHeight = 240;
|
|
||||||
constraints.video.mandatory.maxFrameRate = 15;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (constraints.video.mandatory.minWidth)
|
|
||||||
constraints.video.mandatory.maxWidth =
|
|
||||||
constraints.video.mandatory.minWidth;
|
|
||||||
if (constraints.video.mandatory.minHeight)
|
|
||||||
constraints.video.mandatory.maxHeight =
|
|
||||||
constraints.video.mandatory.minHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getConstraints(um, resolution, bandwidth, fps, desktopStream) {
|
|
||||||
var constraints = {audio: false, video: false};
|
|
||||||
|
|
||||||
if (um.indexOf('video') >= 0) {
|
|
||||||
// same behaviour as true
|
|
||||||
constraints.video = { mandatory: {}, optional: [] };
|
|
||||||
|
|
||||||
constraints.video.optional.push({ googLeakyBucket: true });
|
|
||||||
|
|
||||||
setResolutionConstraints(constraints, resolution);
|
|
||||||
}
|
|
||||||
if (um.indexOf('audio') >= 0) {
|
|
||||||
if (!RTCBrowserType.isFirefox()) {
|
|
||||||
// same behaviour as true
|
|
||||||
constraints.audio = { mandatory: {}, optional: []};
|
|
||||||
// if it is good enough for hangouts...
|
|
||||||
constraints.audio.optional.push(
|
|
||||||
{googEchoCancellation: true},
|
|
||||||
{googAutoGainControl: true},
|
|
||||||
{googNoiseSupression: true},
|
|
||||||
{googHighpassFilter: true},
|
|
||||||
{googNoisesuppression2: true},
|
|
||||||
{googEchoCancellation2: true},
|
|
||||||
{googAutoGainControl2: true}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
constraints.audio = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (um.indexOf('screen') >= 0) {
|
|
||||||
if (RTCBrowserType.isChrome()) {
|
|
||||||
constraints.video = {
|
|
||||||
mandatory: {
|
|
||||||
chromeMediaSource: "screen",
|
|
||||||
googLeakyBucket: true,
|
|
||||||
maxWidth: window.screen.width,
|
|
||||||
maxHeight: window.screen.height,
|
|
||||||
maxFrameRate: 3
|
|
||||||
},
|
|
||||||
optional: []
|
|
||||||
};
|
|
||||||
} else if (RTCBrowserType.isTemasysPluginUsed()) {
|
|
||||||
constraints.video = {
|
|
||||||
optional: [
|
|
||||||
{
|
|
||||||
sourceId: AdapterJS.WebRTCPlugin.plugin.screensharingKey
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
} else if (RTCBrowserType.isFirefox()) {
|
|
||||||
constraints.video = {
|
|
||||||
mozMediaSource: "window",
|
|
||||||
mediaSource: "window"
|
|
||||||
};
|
|
||||||
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
"'screen' WebRTC media source is supported only in Chrome" +
|
|
||||||
" and with Temasys plugin");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (um.indexOf('desktop') >= 0) {
|
|
||||||
constraints.video = {
|
|
||||||
mandatory: {
|
|
||||||
chromeMediaSource: "desktop",
|
|
||||||
chromeMediaSourceId: desktopStream,
|
|
||||||
googLeakyBucket: true,
|
|
||||||
maxWidth: window.screen.width,
|
|
||||||
maxHeight: window.screen.height,
|
|
||||||
maxFrameRate: 3
|
|
||||||
},
|
|
||||||
optional: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bandwidth) {
|
|
||||||
if (!constraints.video) {
|
|
||||||
//same behaviour as true
|
|
||||||
constraints.video = {mandatory: {}, optional: []};
|
|
||||||
}
|
|
||||||
constraints.video.optional.push({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) {
|
|
||||||
// same behaviour as true;
|
|
||||||
constraints.video = {mandatory: {}, optional: []};
|
|
||||||
}
|
|
||||||
constraints.video.mandatory.minFrameRate = fps;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we turn audio for both audio and video tracks, the fake audio & video seems to work
|
|
||||||
// only when enabled in one getUserMedia call, we cannot get fake audio separate by fake video
|
|
||||||
// this later can be a problem with some of the tests
|
|
||||||
if(RTCBrowserType.isFirefox() && config.firefox_fake_device)
|
|
||||||
{
|
|
||||||
constraints.audio = true;
|
|
||||||
constraints.fake = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return constraints;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function RTCUtils(RTCService, eventEmitter, onTemasysPluginReady)
|
|
||||||
{
|
|
||||||
var self = this;
|
|
||||||
this.service = RTCService;
|
|
||||||
this.eventEmitter = eventEmitter;
|
|
||||||
if (RTCBrowserType.isFirefox()) {
|
|
||||||
var FFversion = RTCBrowserType.getFirefoxVersion();
|
|
||||||
if (FFversion >= 40) {
|
|
||||||
this.peerconnection = mozRTCPeerConnection;
|
|
||||||
this.getUserMedia = navigator.mozGetUserMedia.bind(navigator);
|
|
||||||
this.pc_constraints = {};
|
|
||||||
this.attachMediaStream = function (element, stream) {
|
|
||||||
// srcObject is being standardized and FF will eventually
|
|
||||||
// support that unprefixed. FF also supports the
|
|
||||||
// "element.src = URL.createObjectURL(...)" combo, but that
|
|
||||||
// will be deprecated in favour of srcObject.
|
|
||||||
//
|
|
||||||
// https://groups.google.com/forum/#!topic/mozilla.dev.media/pKOiioXonJg
|
|
||||||
// https://github.com/webrtc/samples/issues/302
|
|
||||||
if(!element[0])
|
|
||||||
return;
|
|
||||||
element[0].mozSrcObject = stream;
|
|
||||||
element[0].play();
|
|
||||||
};
|
|
||||||
this.getStreamID = function (stream) {
|
|
||||||
var id = stream.id;
|
|
||||||
if (!id) {
|
|
||||||
var tracks = stream.getVideoTracks();
|
|
||||||
if (!tracks || tracks.length === 0) {
|
|
||||||
tracks = stream.getAudioTracks();
|
|
||||||
}
|
|
||||||
id = tracks[0].id;
|
|
||||||
}
|
|
||||||
return APP.xmpp.filter_special_chars(id);
|
|
||||||
};
|
|
||||||
this.getVideoSrc = function (element) {
|
|
||||||
if(!element)
|
|
||||||
return null;
|
|
||||||
return element.mozSrcObject;
|
|
||||||
};
|
|
||||||
this.setVideoSrc = function (element, src) {
|
|
||||||
if(element)
|
|
||||||
element.mozSrcObject = src;
|
|
||||||
};
|
|
||||||
window.RTCSessionDescription = mozRTCSessionDescription;
|
|
||||||
window.RTCIceCandidate = mozRTCIceCandidate;
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
"Firefox version too old: " + FFversion + ". Required >= 40.");
|
|
||||||
window.location.href = 'unsupported_browser.html';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (RTCBrowserType.isChrome() || RTCBrowserType.isOpera()) {
|
|
||||||
this.peerconnection = webkitRTCPeerConnection;
|
|
||||||
this.getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
|
|
||||||
this.attachMediaStream = function (element, stream) {
|
|
||||||
element.attr('src', webkitURL.createObjectURL(stream));
|
|
||||||
};
|
|
||||||
this.getStreamID = function (stream) {
|
|
||||||
// streams from FF endpoints have the characters '{' and '}'
|
|
||||||
// that make jQuery choke.
|
|
||||||
return APP.xmpp.filter_special_chars(stream.id);
|
|
||||||
};
|
|
||||||
this.getVideoSrc = function (element) {
|
|
||||||
if(!element)
|
|
||||||
return null;
|
|
||||||
return element.getAttribute("src");
|
|
||||||
};
|
|
||||||
this.setVideoSrc = function (element, src) {
|
|
||||||
if(element)
|
|
||||||
element.setAttribute("src", src);
|
|
||||||
};
|
|
||||||
// DTLS should now be enabled by default but..
|
|
||||||
this.pc_constraints = {'optional': [{'DtlsSrtpKeyAgreement': 'true'}]};
|
|
||||||
if (RTCBrowserType.isAndroid()) {
|
|
||||||
this.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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Detect IE/Safari
|
|
||||||
else if (RTCBrowserType.isTemasysPluginUsed()) {
|
|
||||||
|
|
||||||
//AdapterJS.WebRTCPlugin.setLogLevel(
|
|
||||||
// AdapterJS.WebRTCPlugin.PLUGIN_LOG_LEVELS.VERBOSE);
|
|
||||||
|
|
||||||
AdapterJS.webRTCReady(function (isPlugin) {
|
|
||||||
|
|
||||||
self.peerconnection = RTCPeerConnection;
|
|
||||||
self.getUserMedia = getUserMedia;
|
|
||||||
self.attachMediaStream = function (elSel, stream) {
|
|
||||||
|
|
||||||
if (stream.id === "dummyAudio" || stream.id === "dummyVideo") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
attachMediaStream(elSel[0], stream);
|
|
||||||
};
|
|
||||||
self.getStreamID = function (stream) {
|
|
||||||
return APP.xmpp.filter_special_chars(stream.label);
|
|
||||||
};
|
|
||||||
self.getVideoSrc = function (element) {
|
|
||||||
if (!element) {
|
|
||||||
console.warn("Attempt to get video SRC of null element");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var children = element.children;
|
|
||||||
for (var i = 0; i !== children.length; ++i) {
|
|
||||||
if (children[i].name === 'streamId') {
|
|
||||||
return children[i].value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//console.info(element.id + " SRC: " + src);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
self.setVideoSrc = function (element, src) {
|
|
||||||
//console.info("Set video src: ", element, src);
|
|
||||||
if (!src) {
|
|
||||||
console.warn("Not attaching video stream, 'src' is null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
AdapterJS.WebRTCPlugin.WaitForPluginReady();
|
|
||||||
var stream = AdapterJS.WebRTCPlugin.plugin
|
|
||||||
.getStreamWithId(AdapterJS.WebRTCPlugin.pageId, src);
|
|
||||||
attachMediaStream(element, stream);
|
|
||||||
};
|
|
||||||
|
|
||||||
onTemasysPluginReady(isPlugin);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
console.log('Browser does not appear to be WebRTC-capable');
|
|
||||||
} catch (e) { }
|
|
||||||
window.location.href = 'unsupported_browser.html';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
RTCUtils.prototype.getUserMediaWithConstraints = function(
|
|
||||||
um, success_callback, failure_callback, resolution,bandwidth, fps,
|
|
||||||
desktopStream) {
|
|
||||||
currentResolution = resolution;
|
|
||||||
|
|
||||||
var constraints = getConstraints(
|
|
||||||
um, resolution, bandwidth, fps, desktopStream);
|
|
||||||
|
|
||||||
console.info("Get media constraints", constraints);
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.getUserMedia(constraints,
|
|
||||||
function (stream) {
|
|
||||||
console.log('onUserMediaSuccess');
|
|
||||||
self.setAvailableDevices(um, true);
|
|
||||||
success_callback(stream);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
self.setAvailableDevices(um, false);
|
|
||||||
console.warn('Failed to get access to local media. Error ',
|
|
||||||
error, constraints);
|
|
||||||
self.eventEmitter.emit(RTCEvents.GET_USER_MEDIA_FAILED, error);
|
|
||||||
if (failure_callback) {
|
|
||||||
failure_callback(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('GUM failed: ', e);
|
|
||||||
self.eventEmitter.emit(RTCEvents.GET_USER_MEDIA_FAILED, e);
|
|
||||||
if(failure_callback) {
|
|
||||||
failure_callback(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
RTCUtils.prototype.setAvailableDevices = function (um, available) {
|
|
||||||
var devices = {};
|
|
||||||
if(um.indexOf("video") != -1) {
|
|
||||||
devices.video = available;
|
|
||||||
}
|
|
||||||
if(um.indexOf("audio") != -1) {
|
|
||||||
devices.audio = available;
|
|
||||||
}
|
|
||||||
this.service.setDeviceAvailability(devices);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We ask for audio and video combined stream in order to get permissions and
|
|
||||||
* not to ask twice.
|
|
||||||
*/
|
|
||||||
RTCUtils.prototype.obtainAudioAndVideoPermissions =
|
|
||||||
function(devices, callback, usageOptions)
|
|
||||||
{
|
|
||||||
var self = this;
|
|
||||||
// Get AV
|
|
||||||
|
|
||||||
var successCallback = function (stream) {
|
|
||||||
if(callback)
|
|
||||||
callback(stream, usageOptions);
|
|
||||||
else
|
|
||||||
self.successCallback(stream, usageOptions);
|
|
||||||
};
|
|
||||||
|
|
||||||
if(!devices)
|
|
||||||
devices = ['audio', 'video'];
|
|
||||||
|
|
||||||
var newDevices = [];
|
|
||||||
|
|
||||||
|
|
||||||
if(usageOptions)
|
|
||||||
for(var i = 0; i < devices.length; i++) {
|
|
||||||
var device = devices[i];
|
|
||||||
if(usageOptions[device] === true)
|
|
||||||
newDevices.push(device);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
newDevices = devices;
|
|
||||||
|
|
||||||
if(newDevices.length === 0) {
|
|
||||||
successCallback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RTCBrowserType.isFirefox() || RTCBrowserType.isTemasysPluginUsed()) {
|
|
||||||
|
|
||||||
// With FF/IE we can't split the stream into audio and video because FF
|
|
||||||
// doesn't support media stream constructors. So, we need to get the
|
|
||||||
// audio stream separately from the video stream using two distinct GUM
|
|
||||||
// calls. Not very user friendly :-( but we don't have many other
|
|
||||||
// options neither.
|
|
||||||
//
|
|
||||||
// Note that we pack those 2 streams in a single object and pass it to
|
|
||||||
// the successCallback method.
|
|
||||||
var obtainVideo = function (audioStream) {
|
|
||||||
self.getUserMediaWithConstraints(
|
|
||||||
['video'],
|
|
||||||
function (videoStream) {
|
|
||||||
return successCallback({
|
|
||||||
audioStream: audioStream,
|
|
||||||
videoStream: videoStream
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.error(
|
|
||||||
'failed to obtain video stream - stop', error);
|
|
||||||
self.errorCallback(error);
|
|
||||||
},
|
|
||||||
config.resolution || '360');
|
|
||||||
};
|
|
||||||
var obtainAudio = function () {
|
|
||||||
self.getUserMediaWithConstraints(
|
|
||||||
['audio'],
|
|
||||||
function (audioStream) {
|
|
||||||
if (newDevices.indexOf('video') !== -1)
|
|
||||||
obtainVideo(audioStream);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.error(
|
|
||||||
'failed to obtain audio stream - stop', error);
|
|
||||||
self.errorCallback(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
if (newDevices.indexOf('audio') !== -1) {
|
|
||||||
obtainAudio();
|
|
||||||
} else {
|
|
||||||
obtainVideo(null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.getUserMediaWithConstraints(
|
|
||||||
newDevices,
|
|
||||||
function (stream) {
|
|
||||||
successCallback(stream);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
self.errorCallback(error);
|
|
||||||
},
|
|
||||||
config.resolution || '360');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
RTCUtils.prototype.successCallback = function (stream, usageOptions) {
|
|
||||||
// If this is FF or IE, the stream parameter is *not* a MediaStream object,
|
|
||||||
// it's an object with two properties: audioStream, videoStream.
|
|
||||||
if (stream && stream.getAudioTracks && stream.getVideoTracks)
|
|
||||||
console.log('got', stream, stream.getAudioTracks().length,
|
|
||||||
stream.getVideoTracks().length);
|
|
||||||
this.handleLocalStream(stream, usageOptions);
|
|
||||||
};
|
|
||||||
|
|
||||||
RTCUtils.prototype.errorCallback = function (error) {
|
|
||||||
var self = this;
|
|
||||||
console.error('failed to obtain audio/video stream - trying audio only', error);
|
|
||||||
var resolution = getPreviousResolution(currentResolution);
|
|
||||||
if(typeof error == "object" && error.constraintName && error.name
|
|
||||||
&& (error.name == "ConstraintNotSatisfiedError" ||
|
|
||||||
error.name == "OverconstrainedError") &&
|
|
||||||
(error.constraintName == "minWidth" || error.constraintName == "maxWidth" ||
|
|
||||||
error.constraintName == "minHeight" || error.constraintName == "maxHeight")
|
|
||||||
&& resolution)
|
|
||||||
{
|
|
||||||
self.getUserMediaWithConstraints(['audio', 'video'],
|
|
||||||
function (stream) {
|
|
||||||
return self.successCallback(stream);
|
|
||||||
}, function (error) {
|
|
||||||
return self.errorCallback(error);
|
|
||||||
}, resolution);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
self.getUserMediaWithConstraints(
|
|
||||||
['audio'],
|
|
||||||
function (stream) {
|
|
||||||
return self.successCallback(stream);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.error('failed to obtain audio/video stream - stop',
|
|
||||||
error);
|
|
||||||
return self.successCallback(null);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
RTCUtils.prototype.handleLocalStream = function(stream, usageOptions) {
|
|
||||||
// If this is FF, the stream parameter is *not* a MediaStream object, it's
|
|
||||||
// an object with two properties: audioStream, videoStream.
|
|
||||||
var audioStream, videoStream;
|
|
||||||
if(window.webkitMediaStream)
|
|
||||||
{
|
|
||||||
audioStream = new webkitMediaStream();
|
|
||||||
videoStream = new webkitMediaStream();
|
|
||||||
if(stream) {
|
|
||||||
var audioTracks = stream.getAudioTracks();
|
|
||||||
|
|
||||||
for (var i = 0; i < audioTracks.length; i++) {
|
|
||||||
audioStream.addTrack(audioTracks[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
var videoTracks = stream.getVideoTracks();
|
|
||||||
|
|
||||||
for (i = 0; i < videoTracks.length; i++) {
|
|
||||||
videoStream.addTrack(videoTracks[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (RTCBrowserType.isFirefox() || RTCBrowserType.isTemasysPluginUsed())
|
|
||||||
{ // Firefox and Temasys plugin
|
|
||||||
if (stream && stream.audioStream)
|
|
||||||
audioStream = stream.audioStream;
|
|
||||||
else
|
|
||||||
audioStream = new DummyMediaStream("dummyAudio");
|
|
||||||
|
|
||||||
if (stream && stream.videoStream)
|
|
||||||
videoStream = stream.videoStream;
|
|
||||||
else
|
|
||||||
videoStream = new DummyMediaStream("dummyVideo");
|
|
||||||
}
|
|
||||||
|
|
||||||
var audioMuted = (usageOptions && usageOptions.audio === false),
|
|
||||||
videoMuted = (usageOptions && usageOptions.video === false);
|
|
||||||
|
|
||||||
var audioGUM = (!usageOptions || usageOptions.audio !== false),
|
|
||||||
videoGUM = (!usageOptions || usageOptions.video !== false);
|
|
||||||
|
|
||||||
|
|
||||||
this.service.createLocalStream(
|
|
||||||
audioStream, MediaStreamType.AUDIO_TYPE, null, null,
|
|
||||||
audioMuted, audioGUM);
|
|
||||||
|
|
||||||
this.service.createLocalStream(
|
|
||||||
videoStream, MediaStreamType.VIDEO_TYPE, null, 'camera',
|
|
||||||
videoMuted, videoGUM);
|
|
||||||
};
|
|
||||||
|
|
||||||
function DummyMediaStream(id) {
|
|
||||||
this.id = id;
|
|
||||||
this.label = id;
|
|
||||||
this.stop = function() { };
|
|
||||||
this.getAudioTracks = function() { return []; };
|
|
||||||
this.getVideoTracks = function() { return []; };
|
|
||||||
}
|
|
||||||
|
|
||||||
RTCUtils.prototype.createStream = function(stream, isVideo) {
|
|
||||||
var newStream = null;
|
|
||||||
if (window.webkitMediaStream) {
|
|
||||||
newStream = new webkitMediaStream();
|
|
||||||
if (newStream) {
|
|
||||||
var tracks = (isVideo ? stream.getVideoTracks() : stream.getAudioTracks());
|
|
||||||
|
|
||||||
for (var i = 0; i < tracks.length; i++) {
|
|
||||||
newStream.addTrack(tracks[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// FIXME: this is duplicated with 'handleLocalStream' !!!
|
|
||||||
if (stream) {
|
|
||||||
newStream = stream;
|
|
||||||
} else {
|
|
||||||
newStream =
|
|
||||||
new DummyMediaStream(isVideo ? "dummyVideo" : "dummyAudio");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newStream;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = RTCUtils;
|
|
File diff suppressed because it is too large
Load Diff
|
@ -12,7 +12,6 @@ import LargeVideoManager, {VideoContainerType} from "./LargeVideo";
|
||||||
import {PreziContainerType} from '../prezi/Prezi';
|
import {PreziContainerType} from '../prezi/Prezi';
|
||||||
import LocalVideo from "./LocalVideo";
|
import LocalVideo from "./LocalVideo";
|
||||||
|
|
||||||
var MediaStreamType = require("../../../service/RTC/MediaStreamTypes");
|
|
||||||
var RTCBrowserType = require('../../RTC/RTCBrowserType');
|
var RTCBrowserType = require('../../RTC/RTCBrowserType');
|
||||||
|
|
||||||
var remoteVideos = {};
|
var remoteVideos = {};
|
||||||
|
@ -674,7 +673,7 @@ var VideoLayout = {
|
||||||
|
|
||||||
var jid = APP.xmpp.findJidFromResource(resourceJid);
|
var jid = APP.xmpp.findJidFromResource(resourceJid);
|
||||||
var mediaStream =
|
var mediaStream =
|
||||||
APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE];
|
APP.RTC.remoteStreams[jid]['video'];
|
||||||
var sel = remoteVideo.selectVideoElement();
|
var sel = remoteVideo.selectVideoElement();
|
||||||
|
|
||||||
APP.RTC.attachMediaStream(sel, mediaStream.stream);
|
APP.RTC.attachMediaStream(sel, mediaStream.stream);
|
||||||
|
|
|
@ -1,127 +0,0 @@
|
||||||
/*
|
|
||||||
* JingleSession provides an API to manage a single Jingle session. We will
|
|
||||||
* have different implementations depending on the underlying interface used
|
|
||||||
* (i.e. WebRTC and ORTC) and here we hold the code common to all of them.
|
|
||||||
*/
|
|
||||||
function JingleSession(me, sid, connection, service, eventEmitter) {
|
|
||||||
/**
|
|
||||||
* Our JID.
|
|
||||||
*/
|
|
||||||
this.me = me;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Jingle session identifier.
|
|
||||||
*/
|
|
||||||
this.sid = sid;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The XMPP connection.
|
|
||||||
*/
|
|
||||||
this.connection = connection;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The XMPP service.
|
|
||||||
*/
|
|
||||||
this.service = service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The event emitter.
|
|
||||||
*/
|
|
||||||
this.eventEmitter = eventEmitter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to use dripping or not. Dripping is sending trickle candidates
|
|
||||||
* not one-by-one.
|
|
||||||
* Note: currently we do not support 'false'.
|
|
||||||
*/
|
|
||||||
this.usedrip = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When dripping is used, stores ICE candidates which are to be sent.
|
|
||||||
*/
|
|
||||||
this.drip_container = [];
|
|
||||||
|
|
||||||
// Media constraints. Is this WebRTC only?
|
|
||||||
this.media_constraints = null;
|
|
||||||
|
|
||||||
// ICE servers config (RTCConfiguration?).
|
|
||||||
this.ice_config = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepares this object to initiate a session.
|
|
||||||
* @param peerjid the JID of the remote peer.
|
|
||||||
* @param isInitiator whether we will be the Jingle initiator.
|
|
||||||
* @param media_constraints
|
|
||||||
* @param ice_config
|
|
||||||
*/
|
|
||||||
JingleSession.prototype.initialize = function(peerjid, isInitiator,
|
|
||||||
media_constraints, ice_config) {
|
|
||||||
this.media_constraints = media_constraints;
|
|
||||||
this.ice_config = ice_config;
|
|
||||||
|
|
||||||
if (this.state !== null) {
|
|
||||||
console.error('attempt to initiate on session ' + this.sid +
|
|
||||||
'in state ' + this.state);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.state = 'pending';
|
|
||||||
this.initiator = isInitiator ? this.me : peerjid;
|
|
||||||
this.responder = !isInitiator ? this.me : peerjid;
|
|
||||||
this.peerjid = peerjid;
|
|
||||||
|
|
||||||
this.doInitialize();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finishes initialization.
|
|
||||||
*/
|
|
||||||
JingleSession.prototype.doInitialize = function() {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the ICE candidates found in the 'contents' array as remote candidates?
|
|
||||||
* Note: currently only used on transport-info
|
|
||||||
*/
|
|
||||||
JingleSession.prototype.addIceCandidates = function(contents) {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles an 'add-source' event.
|
|
||||||
*
|
|
||||||
* @param contents an array of Jingle 'content' elements.
|
|
||||||
*/
|
|
||||||
JingleSession.prototype.addSources = function(contents) {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles a 'remove-source' event.
|
|
||||||
*
|
|
||||||
* @param contents an array of Jingle 'content' elements.
|
|
||||||
*/
|
|
||||||
JingleSession.prototype.removeSources = function(contents) {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Terminates this Jingle session (stops sending media and closes the streams?)
|
|
||||||
*/
|
|
||||||
JingleSession.prototype.terminate = function() {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a Jingle session-terminate message to the peer and terminates the
|
|
||||||
* session.
|
|
||||||
* @param reason
|
|
||||||
* @param text
|
|
||||||
*/
|
|
||||||
JingleSession.prototype.sendTerminate = function(reason, text) {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles an offer from the remote peer (prepares to accept a session).
|
|
||||||
* @param jingle the 'jingle' XML element.
|
|
||||||
*/
|
|
||||||
JingleSession.prototype.setOffer = function(jingle) {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles an answer from the remote peer (prepares to accept a session).
|
|
||||||
* @param jingle the 'jingle' XML element.
|
|
||||||
*/
|
|
||||||
JingleSession.prototype.setAnswer = function(jingle) {};
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = JingleSession;
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,268 +0,0 @@
|
||||||
/* global $ */
|
|
||||||
|
|
||||||
/*
|
|
||||||
Here we do modifications of local video SSRCs. There are 2 situations we have
|
|
||||||
to handle:
|
|
||||||
|
|
||||||
1. We generate SSRC for local recvonly video stream. This is the case when we
|
|
||||||
have no local camera and it is not generated automatically, but SSRC=1 is
|
|
||||||
used implicitly. If that happens RTCP packets will be dropped by the JVB
|
|
||||||
and we won't be able to request video key frames correctly.
|
|
||||||
|
|
||||||
2. A hack to re-use SSRC of the first video stream for any new stream created
|
|
||||||
in future. It turned out that Chrome may keep on using the SSRC of removed
|
|
||||||
video stream in RTCP even though a new one has been created. So we just
|
|
||||||
want to avoid that by re-using it. Jingle 'source-remove'/'source-add'
|
|
||||||
notifications are blocked once first video SSRC is advertised to the focus.
|
|
||||||
|
|
||||||
What this hack does:
|
|
||||||
|
|
||||||
1. Stores the SSRC of the first video stream created by
|
|
||||||
a) scanning Jingle session-accept/session-invite for existing video SSRC
|
|
||||||
b) watching for 'source-add' for new video stream if it has not been
|
|
||||||
created in step a)
|
|
||||||
2. Exposes method 'mungeLocalVideoSSRC' which replaces any new video SSRC with
|
|
||||||
the stored one. It is called by 'TracablePeerConnection' before local SDP is
|
|
||||||
returned to the other parts of the application.
|
|
||||||
3. Scans 'source-remove'/'source-add' notifications for stored video SSRC and
|
|
||||||
blocks those notifications. This makes Jicofo and all participants think
|
|
||||||
that it exists all the time even if the video stream has been removed or
|
|
||||||
replaced locally. Thanks to that there is no additional signaling activity
|
|
||||||
on video mute or when switching to the desktop stream.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var SDP = require('./SDP');
|
|
||||||
var RTCBrowserType = require('../RTC/RTCBrowserType');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The hack is enabled on all browsers except FF by default
|
|
||||||
* FIXME finish the hack once removeStream method is implemented in FF
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
var isEnabled = !RTCBrowserType.isFirefox();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stored SSRC of local video stream.
|
|
||||||
*/
|
|
||||||
var localVideoSSRC;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SSRC used for recvonly video stream when we have no local camera.
|
|
||||||
* This is in order to tell Chrome what SSRC should be used in RTCP requests
|
|
||||||
* instead of 1.
|
|
||||||
*/
|
|
||||||
var localRecvOnlySSRC;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* cname for <tt>localRecvOnlySSRC</tt>
|
|
||||||
*/
|
|
||||||
var localRecvOnlyCName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method removes <source> element which describes <tt>localVideoSSRC</tt>
|
|
||||||
* from given Jingle IQ.
|
|
||||||
* @param modifyIq 'source-add' or 'source-remove' Jingle IQ.
|
|
||||||
* @param actionName display name of the action which will be printed in log
|
|
||||||
* messages.
|
|
||||||
* @returns {*} modified Jingle IQ, so that it does not contain <source> element
|
|
||||||
* corresponding to <tt>localVideoSSRC</tt> or <tt>null</tt> if no
|
|
||||||
* other SSRCs left to be signaled after removing it.
|
|
||||||
*/
|
|
||||||
var filterOutSource = function (modifyIq, actionName) {
|
|
||||||
var modifyIqTree = $(modifyIq.tree());
|
|
||||||
|
|
||||||
if (!localVideoSSRC)
|
|
||||||
return modifyIqTree[0];
|
|
||||||
|
|
||||||
var videoSSRC = modifyIqTree.find(
|
|
||||||
'>jingle>content[name="video"]' +
|
|
||||||
'>description>source[ssrc="' + localVideoSSRC + '"]');
|
|
||||||
|
|
||||||
if (!videoSSRC.length) {
|
|
||||||
return modifyIqTree[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
console.info(
|
|
||||||
'Blocking ' + actionName + ' for local video SSRC: ' + localVideoSSRC);
|
|
||||||
|
|
||||||
videoSSRC.remove();
|
|
||||||
|
|
||||||
// Check if any sources still left to be added/removed
|
|
||||||
if (modifyIqTree.find('>jingle>content>description>source').length) {
|
|
||||||
return modifyIqTree[0];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scans given Jingle IQ for video SSRC and stores it.
|
|
||||||
* @param jingleIq the Jingle IQ to be scanned for video SSRC.
|
|
||||||
*/
|
|
||||||
var storeLocalVideoSSRC = function (jingleIq) {
|
|
||||||
var videoSSRCs =
|
|
||||||
$(jingleIq.tree())
|
|
||||||
.find('>jingle>content[name="video"]>description>source');
|
|
||||||
|
|
||||||
videoSSRCs.each(function (idx, ssrcElem) {
|
|
||||||
if (localVideoSSRC)
|
|
||||||
return;
|
|
||||||
// We consider SSRC real only if it has msid attribute
|
|
||||||
// recvonly streams in FF do not have it as well as local SSRCs
|
|
||||||
// we generate for recvonly streams in Chrome
|
|
||||||
var ssrSel = $(ssrcElem);
|
|
||||||
var msid = ssrSel.find('>parameter[name="msid"]');
|
|
||||||
if (msid.length) {
|
|
||||||
var ssrcVal = ssrSel.attr('ssrc');
|
|
||||||
if (ssrcVal) {
|
|
||||||
localVideoSSRC = ssrcVal;
|
|
||||||
console.info('Stored local video SSRC' +
|
|
||||||
' for future re-use: ' + localVideoSSRC);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates new SSRC for local video recvonly stream.
|
|
||||||
* FIXME what about eventual SSRC collision ?
|
|
||||||
*/
|
|
||||||
function generateRecvonlySSRC() {
|
|
||||||
//
|
|
||||||
localRecvOnlySSRC =
|
|
||||||
Math.random().toString(10).substring(2, 11);
|
|
||||||
localRecvOnlyCName =
|
|
||||||
Math.random().toString(36).substring(2);
|
|
||||||
console.info(
|
|
||||||
"Generated local recvonly SSRC: " + localRecvOnlySSRC +
|
|
||||||
", cname: " + localRecvOnlyCName);
|
|
||||||
}
|
|
||||||
|
|
||||||
var LocalSSRCReplacement = {
|
|
||||||
/**
|
|
||||||
* Method must be called before 'session-initiate' or 'session-invite' is
|
|
||||||
* sent. Scans the IQ for local video SSRC and stores it if detected.
|
|
||||||
*
|
|
||||||
* @param sessionInit our 'session-initiate' or 'session-accept' Jingle IQ
|
|
||||||
* which will be scanned for local video SSRC.
|
|
||||||
*/
|
|
||||||
processSessionInit: function (sessionInit) {
|
|
||||||
if (!isEnabled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (localVideoSSRC) {
|
|
||||||
console.error("Local SSRC stored already: " + localVideoSSRC);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
storeLocalVideoSSRC(sessionInit);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* If we have local video SSRC stored searched given
|
|
||||||
* <tt>localDescription</tt> for video SSRC and makes sure it is replaced
|
|
||||||
* with the stored one.
|
|
||||||
* @param localDescription local description object that will have local
|
|
||||||
* video SSRC replaced with the stored one
|
|
||||||
* @returns modified <tt>localDescription</tt> object.
|
|
||||||
*/
|
|
||||||
mungeLocalVideoSSRC: function (localDescription) {
|
|
||||||
if (!isEnabled)
|
|
||||||
return localDescription;
|
|
||||||
|
|
||||||
if (!localDescription) {
|
|
||||||
console.warn("localDescription is null or undefined");
|
|
||||||
return localDescription;
|
|
||||||
}
|
|
||||||
|
|
||||||
// IF we have local video SSRC stored make sure it is replaced
|
|
||||||
// with old SSRC
|
|
||||||
if (localVideoSSRC) {
|
|
||||||
var newSdp = new SDP(localDescription.sdp);
|
|
||||||
if (newSdp.media[1].indexOf("a=ssrc:") !== -1 &&
|
|
||||||
!newSdp.containsSSRC(localVideoSSRC)) {
|
|
||||||
// Get new video SSRC
|
|
||||||
var map = newSdp.getMediaSsrcMap();
|
|
||||||
var videoPart = map[1];
|
|
||||||
var videoSSRCs = videoPart.ssrcs;
|
|
||||||
var newSSRC = Object.keys(videoSSRCs)[0];
|
|
||||||
|
|
||||||
console.info(
|
|
||||||
"Replacing new video SSRC: " + newSSRC +
|
|
||||||
" with " + localVideoSSRC);
|
|
||||||
|
|
||||||
localDescription.sdp =
|
|
||||||
newSdp.raw.replace(
|
|
||||||
new RegExp('a=ssrc:' + newSSRC, 'g'),
|
|
||||||
'a=ssrc:' + localVideoSSRC);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Make sure we have any SSRC for recvonly video stream
|
|
||||||
var sdp = new SDP(localDescription.sdp);
|
|
||||||
|
|
||||||
if (sdp.media[1] && sdp.media[1].indexOf('a=ssrc:') === -1 &&
|
|
||||||
sdp.media[1].indexOf('a=recvonly') !== -1) {
|
|
||||||
|
|
||||||
if (!localRecvOnlySSRC) {
|
|
||||||
generateRecvonlySSRC();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.info('No SSRC in video recvonly stream' +
|
|
||||||
' - adding SSRC: ' + localRecvOnlySSRC);
|
|
||||||
|
|
||||||
sdp.media[1] += 'a=ssrc:' + localRecvOnlySSRC +
|
|
||||||
' cname:' + localRecvOnlyCName + '\r\n';
|
|
||||||
|
|
||||||
localDescription.sdp = sdp.session + sdp.media.join('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return localDescription;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Method must be called before 'source-add' notification is sent. In case
|
|
||||||
* we have local video SSRC advertised already it will be removed from the
|
|
||||||
* notification. If no other SSRCs are described by given IQ null will be
|
|
||||||
* returned which means that there is no point in sending the notification.
|
|
||||||
* @param sourceAdd 'source-add' Jingle IQ to be processed
|
|
||||||
* @returns modified 'source-add' IQ which can be sent to the focus or
|
|
||||||
* <tt>null</tt> if no notification shall be sent. It is no longer
|
|
||||||
* a Strophe IQ Builder instance, but DOM element tree.
|
|
||||||
*/
|
|
||||||
processSourceAdd: function (sourceAdd) {
|
|
||||||
if (!isEnabled)
|
|
||||||
return sourceAdd;
|
|
||||||
|
|
||||||
if (!localVideoSSRC) {
|
|
||||||
// Store local SSRC if available
|
|
||||||
storeLocalVideoSSRC(sourceAdd);
|
|
||||||
return sourceAdd;
|
|
||||||
} else {
|
|
||||||
return filterOutSource(sourceAdd, 'source-add');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Method must be called before 'source-remove' notification is sent.
|
|
||||||
* Removes local video SSRC from the notification. If there are no other
|
|
||||||
* SSRCs described in the given IQ <tt>null</tt> will be returned which
|
|
||||||
* means that there is no point in sending the notification.
|
|
||||||
* @param sourceRemove 'source-remove' Jingle IQ to be processed
|
|
||||||
* @returns modified 'source-remove' IQ which can be sent to the focus or
|
|
||||||
* <tt>null</tt> if no notification shall be sent. It is no longer
|
|
||||||
* a Strophe IQ Builder instance, but DOM element tree.
|
|
||||||
*/
|
|
||||||
processSourceRemove: function (sourceRemove) {
|
|
||||||
if (!isEnabled)
|
|
||||||
return sourceRemove;
|
|
||||||
|
|
||||||
return filterOutSource(sourceRemove, 'source-remove');
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turns the hack on or off
|
|
||||||
* @param enabled <tt>true</tt> to enable the hack or <tt>false</tt>
|
|
||||||
* to disable it
|
|
||||||
*/
|
|
||||||
setEnabled: function (enabled) {
|
|
||||||
isEnabled = enabled;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = LocalSSRCReplacement;
|
|
|
@ -1,645 +0,0 @@
|
||||||
/* jshint -W101 */
|
|
||||||
/* jshint -W117 */
|
|
||||||
var SDPUtil = require("./SDPUtil");
|
|
||||||
|
|
||||||
// SDP STUFF
|
|
||||||
function SDP(sdp) {
|
|
||||||
/**
|
|
||||||
* Whether or not to remove TCP ice candidates when translating from/to jingle.
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this.removeTcpCandidates = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not to remove UDP ice candidates when translating from/to jingle.
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this.removeUdpCandidates = false;
|
|
||||||
|
|
||||||
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('');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns map of MediaChannel mapped per channel idx.
|
|
||||||
*/
|
|
||||||
SDP.prototype.getMediaSsrcMap = function() {
|
|
||||||
var self = this;
|
|
||||||
var media_ssrcs = {};
|
|
||||||
var tmp;
|
|
||||||
for (var mediaindex = 0; mediaindex < self.media.length; mediaindex++) {
|
|
||||||
tmp = SDPUtil.find_lines(self.media[mediaindex], 'a=ssrc:');
|
|
||||||
var mid = SDPUtil.parse_mid(SDPUtil.find_line(self.media[mediaindex], 'a=mid:'));
|
|
||||||
var media = {
|
|
||||||
mediaindex: mediaindex,
|
|
||||||
mid: mid,
|
|
||||||
ssrcs: {},
|
|
||||||
ssrcGroups: []
|
|
||||||
};
|
|
||||||
media_ssrcs[mediaindex] = media;
|
|
||||||
tmp.forEach(function (line) {
|
|
||||||
var linessrc = line.substring(7).split(' ')[0];
|
|
||||||
// allocate new ChannelSsrc
|
|
||||||
if(!media.ssrcs[linessrc]) {
|
|
||||||
media.ssrcs[linessrc] = {
|
|
||||||
ssrc: linessrc,
|
|
||||||
lines: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
media.ssrcs[linessrc].lines.push(line);
|
|
||||||
});
|
|
||||||
tmp = SDPUtil.find_lines(self.media[mediaindex], 'a=ssrc-group:');
|
|
||||||
tmp.forEach(function(line){
|
|
||||||
var idx = line.indexOf(' ');
|
|
||||||
var semantics = line.substr(0, idx).substr(13);
|
|
||||||
var ssrcs = line.substr(14 + semantics.length).split(' ');
|
|
||||||
if (ssrcs.length) {
|
|
||||||
media.ssrcGroups.push({
|
|
||||||
semantics: semantics,
|
|
||||||
ssrcs: ssrcs
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return media_ssrcs;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Returns <tt>true</tt> if this SDP contains given SSRC.
|
|
||||||
* @param ssrc the ssrc to check.
|
|
||||||
* @returns {boolean} <tt>true</tt> if this SDP contains given SSRC.
|
|
||||||
*/
|
|
||||||
SDP.prototype.containsSSRC = function(ssrc) {
|
|
||||||
var medias = this.getMediaSsrcMap();
|
|
||||||
Object.keys(medias).forEach(function(mediaindex){
|
|
||||||
var media = medias[mediaindex];
|
|
||||||
//console.log("Check", channel, ssrc);
|
|
||||||
if(Object.keys(media.ssrcs).indexOf(ssrc) != -1){
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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, ssrcs) {
|
|
||||||
// console.log("SSRC" + ssrcs["audio"] + " - " + ssrcs["video"]);
|
|
||||||
var i, j, k, mline, ssrc, rtpmap, tmp, lines;
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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' ||
|
|
||||||
mline.media === 'application'))
|
|
||||||
{
|
|
||||||
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 {
|
|
||||||
if(ssrcs && ssrcs[mline.media]) {
|
|
||||||
ssrc = ssrcs[mline.media];
|
|
||||||
} 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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
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:');
|
|
||||||
if(ssrclines.length > 0) {
|
|
||||||
ssrclines.forEach(function (line) {
|
|
||||||
var 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 {
|
|
||||||
var k = kv.split(':', 2)[0];
|
|
||||||
elem.attrs({ name: k });
|
|
||||||
|
|
||||||
var v = kv.split(':', 2)[1];
|
|
||||||
v = SDPUtil.filter_special_chars(v);
|
|
||||||
elem.attrs({ value: v });
|
|
||||||
}
|
|
||||||
elem.up();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
elem.up();
|
|
||||||
elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
|
|
||||||
elem.c('parameter');
|
|
||||||
elem.attrs({name: "cname", value:Math.random().toString(36).substring(7)});
|
|
||||||
elem.up();
|
|
||||||
var msid = null;
|
|
||||||
if(mline.media == "audio") {
|
|
||||||
msid = APP.RTC.localAudio.getId();
|
|
||||||
} else {
|
|
||||||
msid = APP.RTC.localVideo.getId();
|
|
||||||
}
|
|
||||||
if(msid !== null) {
|
|
||||||
msid = SDPUtil.filter_special_chars(msid);
|
|
||||||
elem.c('parameter');
|
|
||||||
elem.attrs({name: "msid", value:msid});
|
|
||||||
elem.up();
|
|
||||||
elem.c('parameter');
|
|
||||||
elem.attrs({name: "mslabel", value:msid});
|
|
||||||
elem.up();
|
|
||||||
elem.c('parameter');
|
|
||||||
elem.attrs({name: "label", value:msid});
|
|
||||||
elem.up();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elem.up();
|
|
||||||
|
|
||||||
// XEP-0339 handle ssrc-group attributes
|
|
||||||
var ssrc_group_lines = SDPUtil.find_lines(this.media[i], 'a=ssrc-group:');
|
|
||||||
ssrc_group_lines.forEach(function(line) {
|
|
||||||
var idx = line.indexOf(' ');
|
|
||||||
var semantics = line.substr(0, idx).substr(13);
|
|
||||||
var ssrcs = line.substr(14 + semantics.length).split(' ');
|
|
||||||
if (ssrcs.length) {
|
|
||||||
elem.c('ssrc-group', { semantics: semantics, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
|
|
||||||
ssrcs.forEach(function(ssrc) {
|
|
||||||
elem.c('source', { ssrc: ssrc })
|
|
||||||
.up();
|
|
||||||
});
|
|
||||||
elem.up();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 tmp, sctpmap, sctpAttrs, fingerprints;
|
|
||||||
var self = this;
|
|
||||||
elem.c('transport');
|
|
||||||
|
|
||||||
// XEP-0343 DTLS/SCTP
|
|
||||||
if (SDPUtil.find_line(this.media[mediaindex], 'a=sctpmap:').length)
|
|
||||||
{
|
|
||||||
sctpmap = SDPUtil.find_line(
|
|
||||||
this.media[mediaindex], 'a=sctpmap:', self.session);
|
|
||||||
if (sctpmap)
|
|
||||||
{
|
|
||||||
sctpAttrs = SDPUtil.parse_sctpmap(sctpmap);
|
|
||||||
elem.c('sctpmap',
|
|
||||||
{
|
|
||||||
xmlns: 'urn:xmpp:jingle:transports:dtls-sctp:1',
|
|
||||||
number: sctpAttrs[0], /* SCTP port */
|
|
||||||
protocol: sctpAttrs[1] /* protocol */
|
|
||||||
});
|
|
||||||
// Optional stream count attribute
|
|
||||||
if (sctpAttrs.length > 2)
|
|
||||||
elem.attrs({ streams: sctpAttrs[2]});
|
|
||||||
elem.up();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// XEP-0320
|
|
||||||
fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session);
|
|
||||||
fingerprints.forEach(function(line) {
|
|
||||||
tmp = SDPUtil.parse_fingerprint(line);
|
|
||||||
tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0';
|
|
||||||
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) {
|
|
||||||
var candidate = SDPUtil.candidateToJingle(line);
|
|
||||||
var protocol = (candidate &&
|
|
||||||
typeof candidate.protocol === 'string')
|
|
||||||
? candidate.protocol.toLowerCase() : '';
|
|
||||||
if ((self.removeTcpCandidates && protocol === 'tcp') ||
|
|
||||||
(self.removeUdpCandidates && protocol === 'udp')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
elem.c('candidate', candidate).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';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
var sctp = content.find(
|
|
||||||
'>transport>sctpmap[xmlns="urn:xmpp:jingle:transports:dtls-sctp:1"]');
|
|
||||||
|
|
||||||
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) {
|
|
||||||
if (sctp.length)
|
|
||||||
tmp.proto = 'DTLS/SCTP';
|
|
||||||
else
|
|
||||||
tmp.proto = 'RTP/SAVPF';
|
|
||||||
} else {
|
|
||||||
tmp.proto = 'RTP/AVPF';
|
|
||||||
}
|
|
||||||
if (!sctp.length) {
|
|
||||||
tmp.fmt = desc.find('payload-type').map(
|
|
||||||
function () { return this.getAttribute('id'); }).get();
|
|
||||||
media += SDPUtil.build_mline(tmp) + '\r\n';
|
|
||||||
} else {
|
|
||||||
media += 'm=application 1 DTLS/SCTP ' + sctp.attr('number') + '\r\n';
|
|
||||||
media += 'a=sctpmap:' + sctp.attr('number') +
|
|
||||||
' ' + sctp.attr('protocol');
|
|
||||||
|
|
||||||
var streamCount = sctp.attr('streams');
|
|
||||||
if (streamCount)
|
|
||||||
media += ' ' + streamCount + '\r\n';
|
|
||||||
else
|
|
||||||
media += '\r\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
media += 'c=IN IP4 0.0.0.0\r\n';
|
|
||||||
if (!sctp.length)
|
|
||||||
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 () {
|
|
||||||
var protocol = this.getAttribute('protocol');
|
|
||||||
protocol = (typeof protocol === 'string') ? protocol.toLowerCase(): '';
|
|
||||||
|
|
||||||
if ((self.removeTcpCandidates && protocol === 'tcp') ||
|
|
||||||
(self.removeUdpCandidates && protocol === 'udp')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
media += SDPUtil.candidateFromJingle(this);
|
|
||||||
});
|
|
||||||
|
|
||||||
// XEP-0339 handle ssrc-group attributes
|
|
||||||
content.find('description>ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
|
|
||||||
var semantics = this.getAttribute('semantics');
|
|
||||||
var ssrcs = $(this).find('>source').map(function() {
|
|
||||||
return this.getAttribute('ssrc');
|
|
||||||
}).get();
|
|
||||||
|
|
||||||
if (ssrcs.length) {
|
|
||||||
media += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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 () {
|
|
||||||
var name = this.getAttribute('name');
|
|
||||||
var value = this.getAttribute('value');
|
|
||||||
value = SDPUtil.filter_special_chars(value);
|
|
||||||
media += 'a=ssrc:' + ssrc + ' ' + name;
|
|
||||||
if (value && value.length)
|
|
||||||
media += ':' + value;
|
|
||||||
media += '\r\n';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return media;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = SDP;
|
|
||||||
|
|
|
@ -1,168 +0,0 @@
|
||||||
var SDPUtil = require("./SDPUtil");
|
|
||||||
|
|
||||||
function SDPDiffer(mySDP, otherSDP)
|
|
||||||
{
|
|
||||||
this.mySDP = mySDP;
|
|
||||||
this.otherSDP = otherSDP;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns map of MediaChannel that contains media contained in
|
|
||||||
* 'mySDP', but not contained in 'otherSdp'. Mapped by channel idx.
|
|
||||||
*/
|
|
||||||
SDPDiffer.prototype.getNewMedia = function() {
|
|
||||||
|
|
||||||
// this could be useful in Array.prototype.
|
|
||||||
function arrayEquals(array) {
|
|
||||||
// if the other array is a falsy value, return
|
|
||||||
if (!array)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// compare lengths - can save a lot of time
|
|
||||||
if (this.length != array.length)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
for (var i = 0, l=this.length; i < l; i++) {
|
|
||||||
// Check if we have nested arrays
|
|
||||||
if (this[i] instanceof Array && array[i] instanceof Array) {
|
|
||||||
// recurse into the nested arrays
|
|
||||||
if (!this[i].equals(array[i]))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else if (this[i] != array[i]) {
|
|
||||||
// Warning - two different object instances will never be
|
|
||||||
// equal: {x:20} != {x:20}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var myMedias = this.mySDP.getMediaSsrcMap();
|
|
||||||
var othersMedias = this.otherSDP.getMediaSsrcMap();
|
|
||||||
var newMedia = {};
|
|
||||||
Object.keys(othersMedias).forEach(function(othersMediaIdx) {
|
|
||||||
var myMedia = myMedias[othersMediaIdx];
|
|
||||||
var othersMedia = othersMedias[othersMediaIdx];
|
|
||||||
if(!myMedia && othersMedia) {
|
|
||||||
// Add whole channel
|
|
||||||
newMedia[othersMediaIdx] = othersMedia;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Look for new ssrcs across the channel
|
|
||||||
Object.keys(othersMedia.ssrcs).forEach(function(ssrc) {
|
|
||||||
if(Object.keys(myMedia.ssrcs).indexOf(ssrc) === -1) {
|
|
||||||
// Allocate channel if we've found ssrc that doesn't exist in
|
|
||||||
// our channel
|
|
||||||
if(!newMedia[othersMediaIdx]){
|
|
||||||
newMedia[othersMediaIdx] = {
|
|
||||||
mediaindex: othersMedia.mediaindex,
|
|
||||||
mid: othersMedia.mid,
|
|
||||||
ssrcs: {},
|
|
||||||
ssrcGroups: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
newMedia[othersMediaIdx].ssrcs[ssrc] = othersMedia.ssrcs[ssrc];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Look for new ssrc groups across the channels
|
|
||||||
othersMedia.ssrcGroups.forEach(function(otherSsrcGroup){
|
|
||||||
|
|
||||||
// try to match the other ssrc-group with an ssrc-group of ours
|
|
||||||
var matched = false;
|
|
||||||
for (var i = 0; i < myMedia.ssrcGroups.length; i++) {
|
|
||||||
var mySsrcGroup = myMedia.ssrcGroups[i];
|
|
||||||
if (otherSsrcGroup.semantics == mySsrcGroup.semantics &&
|
|
||||||
arrayEquals.apply(otherSsrcGroup.ssrcs,
|
|
||||||
[mySsrcGroup.ssrcs])) {
|
|
||||||
|
|
||||||
matched = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!matched) {
|
|
||||||
// Allocate channel if we've found an ssrc-group that doesn't
|
|
||||||
// exist in our channel
|
|
||||||
|
|
||||||
if(!newMedia[othersMediaIdx]){
|
|
||||||
newMedia[othersMediaIdx] = {
|
|
||||||
mediaindex: othersMedia.mediaindex,
|
|
||||||
mid: othersMedia.mid,
|
|
||||||
ssrcs: {},
|
|
||||||
ssrcGroups: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
newMedia[othersMediaIdx].ssrcGroups.push(otherSsrcGroup);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return newMedia;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: document!
|
|
||||||
*/
|
|
||||||
SDPDiffer.prototype.toJingle = function(modify) {
|
|
||||||
var sdpMediaSsrcs = this.getNewMedia();
|
|
||||||
|
|
||||||
var modified = false;
|
|
||||||
Object.keys(sdpMediaSsrcs).forEach(function(mediaindex){
|
|
||||||
modified = true;
|
|
||||||
var media = sdpMediaSsrcs[mediaindex];
|
|
||||||
modify.c('content', {name: media.mid});
|
|
||||||
|
|
||||||
modify.c('description',
|
|
||||||
{xmlns:'urn:xmpp:jingle:apps:rtp:1', media: media.mid});
|
|
||||||
// FIXME: not completely sure this operates on blocks and / or handles
|
|
||||||
// different ssrcs correctly
|
|
||||||
// generate sources from lines
|
|
||||||
Object.keys(media.ssrcs).forEach(function(ssrcNum) {
|
|
||||||
var mediaSsrc = media.ssrcs[ssrcNum];
|
|
||||||
modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
|
|
||||||
modify.attrs({ssrc: mediaSsrc.ssrc});
|
|
||||||
// iterate over ssrc lines
|
|
||||||
mediaSsrc.lines.forEach(function (line) {
|
|
||||||
var idx = line.indexOf(' ');
|
|
||||||
var kv = line.substr(idx + 1);
|
|
||||||
modify.c('parameter');
|
|
||||||
if (kv.indexOf(':') == -1) {
|
|
||||||
modify.attrs({ name: kv });
|
|
||||||
} else {
|
|
||||||
var nv = kv.split(':', 2);
|
|
||||||
var name = nv[0];
|
|
||||||
var value = SDPUtil.filter_special_chars(nv[1]);
|
|
||||||
modify.attrs({ name: name });
|
|
||||||
modify.attrs({ value: value });
|
|
||||||
}
|
|
||||||
modify.up(); // end of parameter
|
|
||||||
});
|
|
||||||
modify.up(); // end of source
|
|
||||||
});
|
|
||||||
|
|
||||||
// generate source groups from lines
|
|
||||||
media.ssrcGroups.forEach(function(ssrcGroup) {
|
|
||||||
if (ssrcGroup.ssrcs.length) {
|
|
||||||
|
|
||||||
modify.c('ssrc-group', {
|
|
||||||
semantics: ssrcGroup.semantics,
|
|
||||||
xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0'
|
|
||||||
});
|
|
||||||
|
|
||||||
ssrcGroup.ssrcs.forEach(function (ssrc) {
|
|
||||||
modify.c('source', { ssrc: ssrc })
|
|
||||||
.up(); // end of source
|
|
||||||
});
|
|
||||||
modify.up(); // end of ssrc-group
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
modify.up(); // end of description
|
|
||||||
modify.up(); // end of content
|
|
||||||
});
|
|
||||||
|
|
||||||
return modified;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = SDPDiffer;
|
|
|
@ -1,361 +0,0 @@
|
||||||
/* jshint -W101 */
|
|
||||||
var RTCBrowserType = require('../RTC/RTCBrowserType');
|
|
||||||
|
|
||||||
var SDPUtil = {
|
|
||||||
filter_special_chars: function (text) {
|
|
||||||
return text.replace(/[\\\/\{,\}\+]/g, "");
|
|
||||||
},
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Parses SDP line "a=sctpmap:..." and extracts SCTP port from it.
|
|
||||||
* @param line eg. "a=sctpmap:5000 webrtc-datachannel"
|
|
||||||
* @returns [SCTP port number, protocol, streams]
|
|
||||||
*/
|
|
||||||
parse_sctpmap: function (line)
|
|
||||||
{
|
|
||||||
var parts = line.substring(10).split(' ');
|
|
||||||
var sctpPort = parts[0];
|
|
||||||
var protocol = parts[1];
|
|
||||||
// Stream count is optional
|
|
||||||
var streamCount = parts.length > 2 ? parts[2] : null;
|
|
||||||
return [sctpPort, protocol, streamCount];// SCTP port
|
|
||||||
},
|
|
||||||
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;
|
|
||||||
case 'tcptype':
|
|
||||||
candidate.tcptype = 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;
|
|
||||||
}
|
|
||||||
if (cand.hasOwnAttribute('tcptype')) {
|
|
||||||
line += 'tcptype';
|
|
||||||
line += ' ';
|
|
||||||
line += cand.tcptype;
|
|
||||||
line += ' ';
|
|
||||||
}
|
|
||||||
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.indexOf('candidate:') === 0) {
|
|
||||||
line = 'a=' + line;
|
|
||||||
} else 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];
|
|
||||||
|
|
||||||
candidate.generation = '0'; // default, may be overwritten below
|
|
||||||
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;
|
|
||||||
case 'tcptype':
|
|
||||||
candidate.tcptype = 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 += ' ';
|
|
||||||
|
|
||||||
var protocol = cand.getAttribute('protocol');
|
|
||||||
// use tcp candidates for FF
|
|
||||||
if (RTCBrowserType.isFirefox() && protocol.toLowerCase() == 'ssltcp') {
|
|
||||||
protocol = 'tcp';
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
if (protocol.toLowerCase() == 'tcp') {
|
|
||||||
line += 'tcptype';
|
|
||||||
line += ' ';
|
|
||||||
line += cand.getAttribute('tcptype');
|
|
||||||
line += ' ';
|
|
||||||
}
|
|
||||||
line += 'generation';
|
|
||||||
line += ' ';
|
|
||||||
line += cand.getAttribute('generation') || '0';
|
|
||||||
return line + '\r\n';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
module.exports = SDPUtil;
|
|
|
@ -1,449 +0,0 @@
|
||||||
/* global $, config, mozRTCPeerConnection, RTCPeerConnection,
|
|
||||||
webkitRTCPeerConnection, RTCSessionDescription */
|
|
||||||
/* jshint -W101 */
|
|
||||||
var RTC = require('../RTC/RTC');
|
|
||||||
var RTCBrowserType = require("../RTC/RTCBrowserType");
|
|
||||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
|
||||||
var SSRCReplacement = require("./LocalSSRCReplacement");
|
|
||||||
|
|
||||||
function TraceablePeerConnection(ice_config, constraints, session) {
|
|
||||||
var self = this;
|
|
||||||
var RTCPeerConnectionType = null;
|
|
||||||
if (RTCBrowserType.isFirefox()) {
|
|
||||||
RTCPeerConnectionType = mozRTCPeerConnection;
|
|
||||||
} else if (RTCBrowserType.isTemasysPluginUsed()) {
|
|
||||||
RTCPeerConnectionType = RTCPeerConnection;
|
|
||||||
} else {
|
|
||||||
RTCPeerConnectionType = webkitRTCPeerConnection;
|
|
||||||
}
|
|
||||||
self.eventEmitter = session.eventEmitter;
|
|
||||||
this.peerconnection = new RTCPeerConnectionType(ice_config, constraints);
|
|
||||||
this.updateLog = [];
|
|
||||||
this.stats = {};
|
|
||||||
this.statsinterval = null;
|
|
||||||
this.maxstats = 0; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
|
|
||||||
var Interop = require('sdp-interop').Interop;
|
|
||||||
this.interop = new Interop();
|
|
||||||
var Simulcast = require('sdp-simulcast');
|
|
||||||
this.simulcast = new Simulcast({numOfLayers: 3, explodeRemoteSimulcast: false});
|
|
||||||
|
|
||||||
// override as desired
|
|
||||||
this.trace = function (what, info) {
|
|
||||||
/*console.warn('WTRACE', what, info);
|
|
||||||
if (info && RTCBrowserType.isIExplorer()) {
|
|
||||||
if (info.length > 1024) {
|
|
||||||
console.warn('WTRACE', what, info.substr(1024));
|
|
||||||
}
|
|
||||||
if (info.length > 2048) {
|
|
||||||
console.warn('WTRACE', what, info.substr(2048));
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
self.updateLog.push({
|
|
||||||
time: new Date(),
|
|
||||||
type: what,
|
|
||||||
value: info || ""
|
|
||||||
});
|
|
||||||
};
|
|
||||||
this.onicecandidate = null;
|
|
||||||
this.peerconnection.onicecandidate = function (event) {
|
|
||||||
// FIXME: this causes stack overflow with Temasys Plugin
|
|
||||||
if (!RTCBrowserType.isTemasysPluginUsed())
|
|
||||||
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', self.signalingState);
|
|
||||||
if (self.onsignalingstatechange !== null) {
|
|
||||||
self.onsignalingstatechange(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.oniceconnectionstatechange = null;
|
|
||||||
this.peerconnection.oniceconnectionstatechange = function (event) {
|
|
||||||
self.trace('oniceconnectionstatechange', self.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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// XXX: do all non-firefox browsers which we support also support this?
|
|
||||||
if (!RTCBrowserType.isFirefox() && this.maxstats) {
|
|
||||||
this.statsinterval = window.setInterval(function() {
|
|
||||||
self.peerconnection.getStats(function(stats) {
|
|
||||||
var results = stats.result();
|
|
||||||
var now = new Date();
|
|
||||||
for (var i = 0; i < results.length; ++i) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a string representation of a SessionDescription object.
|
|
||||||
*/
|
|
||||||
var dumpSDP = function(description) {
|
|
||||||
if (typeof description === 'undefined' || description === null) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'type: ' + description.type + '\r\n' + description.sdp;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes a SessionDescription object and returns a "normalized" version.
|
|
||||||
* Currently it only takes care of ordering the a=ssrc lines.
|
|
||||||
*/
|
|
||||||
var normalizePlanB = function(desc) {
|
|
||||||
if (typeof desc !== 'object' || desc === null ||
|
|
||||||
typeof desc.sdp !== 'string') {
|
|
||||||
console.warn('An empty description was passed as an argument.');
|
|
||||||
return desc;
|
|
||||||
}
|
|
||||||
|
|
||||||
var transform = require('sdp-transform');
|
|
||||||
var session = transform.parse(desc.sdp);
|
|
||||||
|
|
||||||
if (typeof session !== 'undefined' && typeof session.media !== 'undefined' &&
|
|
||||||
Array.isArray(session.media)) {
|
|
||||||
session.media.forEach(function (mLine) {
|
|
||||||
|
|
||||||
// Chrome appears to be picky about the order in which a=ssrc lines
|
|
||||||
// are listed in an m-line when rtx is enabled (and thus there are
|
|
||||||
// a=ssrc-group lines with FID semantics). Specifically if we have
|
|
||||||
// "a=ssrc-group:FID S1 S2" and the "a=ssrc:S2" lines appear before
|
|
||||||
// the "a=ssrc:S1" lines, SRD fails.
|
|
||||||
// So, put SSRC which appear as the first SSRC in an FID ssrc-group
|
|
||||||
// first.
|
|
||||||
var firstSsrcs = [];
|
|
||||||
var newSsrcLines = [];
|
|
||||||
|
|
||||||
if (typeof mLine.ssrcGroups !== 'undefined' && Array.isArray(mLine.ssrcGroups)) {
|
|
||||||
mLine.ssrcGroups.forEach(function (group) {
|
|
||||||
if (typeof group.semantics !== 'undefined' &&
|
|
||||||
group.semantics === 'FID') {
|
|
||||||
if (typeof group.ssrcs !== 'undefined') {
|
|
||||||
firstSsrcs.push(Number(group.ssrcs.split(' ')[0]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) {
|
|
||||||
var i;
|
|
||||||
for (i = 0; i<mLine.ssrcs.length; i++){
|
|
||||||
if (typeof mLine.ssrcs[i] === 'object'
|
|
||||||
&& typeof mLine.ssrcs[i].id !== 'undefined'
|
|
||||||
&& !$.inArray(mLine.ssrcs[i].id, firstSsrcs)) {
|
|
||||||
newSsrcLines.push(mLine.ssrcs[i]);
|
|
||||||
delete mLine.ssrcs[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i = 0; i<mLine.ssrcs.length; i++){
|
|
||||||
if (typeof mLine.ssrcs[i] !== 'undefined') {
|
|
||||||
newSsrcLines.push(mLine.ssrcs[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mLine.ssrcs = newSsrcLines;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var resStr = transform.write(session);
|
|
||||||
return new RTCSessionDescription({
|
|
||||||
type: desc.type,
|
|
||||||
sdp: resStr
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
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() {
|
|
||||||
var desc = this.peerconnection.localDescription;
|
|
||||||
|
|
||||||
// FIXME this should probably be after the Unified Plan -> Plan B
|
|
||||||
// transformation.
|
|
||||||
desc = SSRCReplacement.mungeLocalVideoSSRC(desc);
|
|
||||||
|
|
||||||
this.trace('getLocalDescription::preTransform', dumpSDP(desc));
|
|
||||||
|
|
||||||
// if we're running on FF, transform to Plan B first.
|
|
||||||
if (RTCBrowserType.usesUnifiedPlan()) {
|
|
||||||
desc = this.interop.toPlanB(desc);
|
|
||||||
this.trace('getLocalDescription::postTransform (Plan B)', dumpSDP(desc));
|
|
||||||
}
|
|
||||||
return desc;
|
|
||||||
});
|
|
||||||
TraceablePeerConnection.prototype.__defineGetter__(
|
|
||||||
'remoteDescription',
|
|
||||||
function() {
|
|
||||||
var desc = this.peerconnection.remoteDescription;
|
|
||||||
this.trace('getRemoteDescription::preTransform', dumpSDP(desc));
|
|
||||||
|
|
||||||
// if we're running on FF, transform to Plan B first.
|
|
||||||
if (RTCBrowserType.usesUnifiedPlan()) {
|
|
||||||
desc = this.interop.toPlanB(desc);
|
|
||||||
this.trace('getRemoteDescription::postTransform (Plan B)', dumpSDP(desc));
|
|
||||||
}
|
|
||||||
return desc;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceablePeerConnection.prototype.addStream = function (stream) {
|
|
||||||
this.trace('addStream', stream.id);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
this.peerconnection.addStream(stream);
|
|
||||||
}
|
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TraceablePeerConnection.prototype.removeStream = function (stream, stopStreams) {
|
|
||||||
this.trace('removeStream', stream.id);
|
|
||||||
if(stopStreams) {
|
|
||||||
RTC.stopMediaStream(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// FF doesn't support this yet.
|
|
||||||
if (this.peerconnection.removeStream)
|
|
||||||
this.peerconnection.removeStream(stream);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
|
|
||||||
this.trace('createDataChannel', label, opts);
|
|
||||||
return this.peerconnection.createDataChannel(label, opts);
|
|
||||||
};
|
|
||||||
|
|
||||||
TraceablePeerConnection.prototype.setLocalDescription
|
|
||||||
= function (description, successCallback, failureCallback) {
|
|
||||||
this.trace('setLocalDescription::preTransform', dumpSDP(description));
|
|
||||||
// if we're running on FF, transform to Plan A first.
|
|
||||||
if (RTCBrowserType.usesUnifiedPlan()) {
|
|
||||||
description = this.interop.toUnifiedPlan(description);
|
|
||||||
this.trace('setLocalDescription::postTransform (Plan A)', dumpSDP(description));
|
|
||||||
}
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
this.peerconnection.setLocalDescription(description,
|
|
||||||
function () {
|
|
||||||
self.trace('setLocalDescriptionOnSuccess');
|
|
||||||
successCallback();
|
|
||||||
},
|
|
||||||
function (err) {
|
|
||||||
self.trace('setLocalDescriptionOnFailure', err);
|
|
||||||
self.eventEmitter.emit(RTCEvents.SET_LOCAL_DESCRIPTION_FAILED, err, self.peerconnection);
|
|
||||||
failureCallback(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
/*
|
|
||||||
if (this.statsinterval === null && this.maxstats > 0) {
|
|
||||||
// start gathering stats
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
};
|
|
||||||
|
|
||||||
TraceablePeerConnection.prototype.setRemoteDescription
|
|
||||||
= function (description, successCallback, failureCallback) {
|
|
||||||
this.trace('setRemoteDescription::preTransform', dumpSDP(description));
|
|
||||||
// TODO the focus should squeze or explode the remote simulcast
|
|
||||||
description = this.simulcast.mungeRemoteDescription(description);
|
|
||||||
this.trace('setRemoteDescription::postTransform (simulcast)', dumpSDP(description));
|
|
||||||
|
|
||||||
// if we're running on FF, transform to Plan A first.
|
|
||||||
if (RTCBrowserType.usesUnifiedPlan()) {
|
|
||||||
description = this.interop.toUnifiedPlan(description);
|
|
||||||
this.trace('setRemoteDescription::postTransform (Plan A)', dumpSDP(description));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RTCBrowserType.usesPlanB()) {
|
|
||||||
description = normalizePlanB(description);
|
|
||||||
}
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
this.peerconnection.setRemoteDescription(description,
|
|
||||||
function () {
|
|
||||||
self.trace('setRemoteDescriptionOnSuccess');
|
|
||||||
successCallback();
|
|
||||||
},
|
|
||||||
function (err) {
|
|
||||||
self.trace('setRemoteDescriptionOnFailure', err);
|
|
||||||
self.eventEmitter.emit(RTCEvents.SET_REMOTE_DESCRIPTION_FAILED, err, self.peerconnection);
|
|
||||||
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::preTransform', dumpSDP(offer));
|
|
||||||
// NOTE this is not tested because in meet the focus generates the
|
|
||||||
// offer.
|
|
||||||
|
|
||||||
// if we're running on FF, transform to Plan B first.
|
|
||||||
if (RTCBrowserType.usesUnifiedPlan()) {
|
|
||||||
offer = self.interop.toPlanB(offer);
|
|
||||||
self.trace('createOfferOnSuccess::postTransform (Plan B)', dumpSDP(offer));
|
|
||||||
}
|
|
||||||
|
|
||||||
offer = SSRCReplacement.mungeLocalVideoSSRC(offer);
|
|
||||||
|
|
||||||
if (config.enableSimulcast && self.simulcast.isSupported()) {
|
|
||||||
offer = self.simulcast.mungeLocalDescription(offer);
|
|
||||||
self.trace('createOfferOnSuccess::postTransform (simulcast)', dumpSDP(offer));
|
|
||||||
}
|
|
||||||
successCallback(offer);
|
|
||||||
},
|
|
||||||
function(err) {
|
|
||||||
self.trace('createOfferOnFailure', err);
|
|
||||||
self.eventEmitter.emit(RTCEvents.CREATE_OFFER_FAILED, err, self.peerconnection);
|
|
||||||
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::preTransform', dumpSDP(answer));
|
|
||||||
// if we're running on FF, transform to Plan A first.
|
|
||||||
if (RTCBrowserType.usesUnifiedPlan()) {
|
|
||||||
answer = self.interop.toPlanB(answer);
|
|
||||||
self.trace('createAnswerOnSuccess::postTransform (Plan B)', dumpSDP(answer));
|
|
||||||
}
|
|
||||||
|
|
||||||
// munge local video SSRC
|
|
||||||
answer = SSRCReplacement.mungeLocalVideoSSRC(answer);
|
|
||||||
|
|
||||||
if (config.enableSimulcast && self.simulcast.isSupported()) {
|
|
||||||
answer = self.simulcast.mungeLocalDescription(answer);
|
|
||||||
self.trace('createAnswerOnSuccess::postTransform (simulcast)', dumpSDP(answer));
|
|
||||||
}
|
|
||||||
successCallback(answer);
|
|
||||||
},
|
|
||||||
function(err) {
|
|
||||||
self.trace('createAnswerOnFailure', err);
|
|
||||||
self.eventEmitter.emit(RTCEvents.CREATE_ANSWER_FAILED, err, self.peerconnection);
|
|
||||||
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) {
|
|
||||||
// TODO: Is this the correct way to handle Opera, Temasys?
|
|
||||||
if (RTCBrowserType.isFirefox()) {
|
|
||||||
// ignore for now...
|
|
||||||
if(!errback)
|
|
||||||
errback = function () {};
|
|
||||||
this.peerconnection.getStats(null, callback, errback);
|
|
||||||
} else {
|
|
||||||
this.peerconnection.getStats(callback);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = TraceablePeerConnection;
|
|
|
@ -1,434 +0,0 @@
|
||||||
/* global $, $iq, APP, config, messageHandler,
|
|
||||||
roomName, sessionTerminated, Strophe, Util */
|
|
||||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
|
||||||
var Settings = require("../settings/Settings");
|
|
||||||
|
|
||||||
var AuthenticationEvents
|
|
||||||
= require("../../service/authentication/AuthenticationEvents");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contains logic responsible for enabling/disabling functionality available
|
|
||||||
* only to moderator users.
|
|
||||||
*/
|
|
||||||
var connection = null;
|
|
||||||
var focusUserJid;
|
|
||||||
|
|
||||||
function createExpBackoffTimer(step) {
|
|
||||||
var count = 1;
|
|
||||||
return function (reset) {
|
|
||||||
// Reset call
|
|
||||||
if (reset) {
|
|
||||||
count = 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Calculate next timeout
|
|
||||||
var timeout = Math.pow(2, count - 1);
|
|
||||||
count += 1;
|
|
||||||
return timeout * step;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var getNextTimeout = createExpBackoffTimer(1000);
|
|
||||||
var getNextErrorTimeout = createExpBackoffTimer(1000);
|
|
||||||
// External authentication stuff
|
|
||||||
var externalAuthEnabled = false;
|
|
||||||
// Sip gateway can be enabled by configuring Jigasi host in config.js or
|
|
||||||
// it will be enabled automatically if focus detects the component through
|
|
||||||
// service discovery.
|
|
||||||
var sipGatewayEnabled;
|
|
||||||
|
|
||||||
var eventEmitter = null;
|
|
||||||
|
|
||||||
var Moderator = {
|
|
||||||
isModerator: function () {
|
|
||||||
return connection && connection.emuc.isModerator();
|
|
||||||
},
|
|
||||||
|
|
||||||
isPeerModerator: function (peerJid) {
|
|
||||||
return connection &&
|
|
||||||
connection.emuc.getMemberRole(peerJid) === 'moderator';
|
|
||||||
},
|
|
||||||
|
|
||||||
isExternalAuthEnabled: function () {
|
|
||||||
return externalAuthEnabled;
|
|
||||||
},
|
|
||||||
|
|
||||||
isSipGatewayEnabled: function () {
|
|
||||||
return sipGatewayEnabled;
|
|
||||||
},
|
|
||||||
|
|
||||||
setConnection: function (con) {
|
|
||||||
connection = con;
|
|
||||||
},
|
|
||||||
|
|
||||||
init: function (xmpp, emitter) {
|
|
||||||
this.xmppService = xmpp;
|
|
||||||
eventEmitter = emitter;
|
|
||||||
|
|
||||||
sipGatewayEnabled =
|
|
||||||
config.hosts && config.hosts.call_control !== undefined;
|
|
||||||
|
|
||||||
// Message listener that talks to POPUP window
|
|
||||||
function listener(event) {
|
|
||||||
if (event.data && event.data.sessionId) {
|
|
||||||
if (event.origin !== window.location.origin) {
|
|
||||||
console.warn("Ignoring sessionId from different origin: " +
|
|
||||||
event.origin);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
localStorage.setItem('sessionId', event.data.sessionId);
|
|
||||||
// After popup is closed we will authenticate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Register
|
|
||||||
if (window.addEventListener) {
|
|
||||||
window.addEventListener("message", listener, false);
|
|
||||||
} else {
|
|
||||||
window.attachEvent("onmessage", listener);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onMucMemberLeft: function (jid) {
|
|
||||||
console.info("Someone left is it focus ? " + jid);
|
|
||||||
var resource = Strophe.getResourceFromJid(jid);
|
|
||||||
if (resource === 'focus' && !this.xmppService.sessionTerminated) {
|
|
||||||
console.info(
|
|
||||||
"Focus has left the room - leaving conference");
|
|
||||||
//hangUp();
|
|
||||||
// We'd rather reload to have everything re-initialized
|
|
||||||
// FIXME: show some message before reload
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setFocusUserJid: function (focusJid) {
|
|
||||||
if (!focusUserJid) {
|
|
||||||
focusUserJid = focusJid;
|
|
||||||
console.info("Focus jid set to: " + focusUserJid);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getFocusUserJid: function () {
|
|
||||||
return focusUserJid;
|
|
||||||
},
|
|
||||||
|
|
||||||
getFocusComponent: function () {
|
|
||||||
// Get focus component address
|
|
||||||
var focusComponent = config.hosts.focus;
|
|
||||||
// If not specified use default: 'focus.domain'
|
|
||||||
if (!focusComponent) {
|
|
||||||
focusComponent = 'focus.' + config.hosts.domain;
|
|
||||||
}
|
|
||||||
return focusComponent;
|
|
||||||
},
|
|
||||||
|
|
||||||
createConferenceIq: function (roomName) {
|
|
||||||
// Generate create conference IQ
|
|
||||||
var elem = $iq({to: Moderator.getFocusComponent(), type: 'set'});
|
|
||||||
|
|
||||||
// Session Id used for authentication
|
|
||||||
var sessionId = localStorage.getItem('sessionId');
|
|
||||||
var machineUID = Settings.getSettings().uid;
|
|
||||||
|
|
||||||
console.info(
|
|
||||||
"Session ID: " + sessionId + " machine UID: " + machineUID);
|
|
||||||
|
|
||||||
elem.c('conference', {
|
|
||||||
xmlns: 'http://jitsi.org/protocol/focus',
|
|
||||||
room: roomName,
|
|
||||||
'machine-uid': machineUID
|
|
||||||
});
|
|
||||||
|
|
||||||
if (sessionId) {
|
|
||||||
elem.attrs({ 'session-id': sessionId});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.hosts.bridge !== undefined) {
|
|
||||||
elem.c(
|
|
||||||
'property',
|
|
||||||
{ name: 'bridge', value: config.hosts.bridge})
|
|
||||||
.up();
|
|
||||||
}
|
|
||||||
// Tell the focus we have Jigasi configured
|
|
||||||
if (config.hosts.call_control !== undefined) {
|
|
||||||
elem.c(
|
|
||||||
'property',
|
|
||||||
{ name: 'call_control', value: config.hosts.call_control})
|
|
||||||
.up();
|
|
||||||
}
|
|
||||||
if (config.channelLastN !== undefined) {
|
|
||||||
elem.c(
|
|
||||||
'property',
|
|
||||||
{ name: 'channelLastN', value: config.channelLastN})
|
|
||||||
.up();
|
|
||||||
}
|
|
||||||
if (config.adaptiveLastN !== undefined) {
|
|
||||||
elem.c(
|
|
||||||
'property',
|
|
||||||
{ name: 'adaptiveLastN', value: config.adaptiveLastN})
|
|
||||||
.up();
|
|
||||||
}
|
|
||||||
if (config.adaptiveSimulcast !== undefined) {
|
|
||||||
elem.c(
|
|
||||||
'property',
|
|
||||||
{ name: 'adaptiveSimulcast', value: config.adaptiveSimulcast})
|
|
||||||
.up();
|
|
||||||
}
|
|
||||||
if (config.openSctp !== undefined) {
|
|
||||||
elem.c(
|
|
||||||
'property',
|
|
||||||
{ name: 'openSctp', value: config.openSctp})
|
|
||||||
.up();
|
|
||||||
}
|
|
||||||
if(config.startAudioMuted !== undefined)
|
|
||||||
{
|
|
||||||
elem.c(
|
|
||||||
'property',
|
|
||||||
{ name: 'startAudioMuted', value: config.startAudioMuted})
|
|
||||||
.up();
|
|
||||||
}
|
|
||||||
if(config.startVideoMuted !== undefined)
|
|
||||||
{
|
|
||||||
elem.c(
|
|
||||||
'property',
|
|
||||||
{ name: 'startVideoMuted', value: config.startVideoMuted})
|
|
||||||
.up();
|
|
||||||
}
|
|
||||||
elem.c(
|
|
||||||
'property',
|
|
||||||
{ name: 'simulcastMode', value: 'rewriting'})
|
|
||||||
.up();
|
|
||||||
elem.up();
|
|
||||||
return elem;
|
|
||||||
},
|
|
||||||
|
|
||||||
parseSessionId: function (resultIq) {
|
|
||||||
var sessionId = $(resultIq).find('conference').attr('session-id');
|
|
||||||
if (sessionId) {
|
|
||||||
console.info('Received sessionId: ' + sessionId);
|
|
||||||
localStorage.setItem('sessionId', sessionId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
parseConfigOptions: function (resultIq) {
|
|
||||||
|
|
||||||
Moderator.setFocusUserJid(
|
|
||||||
$(resultIq).find('conference').attr('focusjid'));
|
|
||||||
|
|
||||||
var authenticationEnabled
|
|
||||||
= $(resultIq).find(
|
|
||||||
'>conference>property' +
|
|
||||||
'[name=\'authentication\'][value=\'true\']').length > 0;
|
|
||||||
|
|
||||||
console.info("Authentication enabled: " + authenticationEnabled);
|
|
||||||
|
|
||||||
externalAuthEnabled = $(resultIq).find(
|
|
||||||
'>conference>property' +
|
|
||||||
'[name=\'externalAuth\'][value=\'true\']').length > 0;
|
|
||||||
|
|
||||||
console.info('External authentication enabled: ' + externalAuthEnabled);
|
|
||||||
|
|
||||||
if (!externalAuthEnabled) {
|
|
||||||
// We expect to receive sessionId in 'internal' authentication mode
|
|
||||||
Moderator.parseSessionId(resultIq);
|
|
||||||
}
|
|
||||||
|
|
||||||
var authIdentity = $(resultIq).find('>conference').attr('identity');
|
|
||||||
|
|
||||||
eventEmitter.emit(AuthenticationEvents.IDENTITY_UPDATED,
|
|
||||||
authenticationEnabled, authIdentity);
|
|
||||||
|
|
||||||
// Check if focus has auto-detected Jigasi component(this will be also
|
|
||||||
// included if we have passed our host from the config)
|
|
||||||
if ($(resultIq).find(
|
|
||||||
'>conference>property' +
|
|
||||||
'[name=\'sipGatewayEnabled\'][value=\'true\']').length) {
|
|
||||||
sipGatewayEnabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.info("Sip gateway enabled: " + sipGatewayEnabled);
|
|
||||||
},
|
|
||||||
|
|
||||||
// FIXME: we need to show the fact that we're waiting for the focus
|
|
||||||
// to the user(or that focus is not available)
|
|
||||||
allocateConferenceFocus: function (roomName, callback) {
|
|
||||||
// Try to use focus user JID from the config
|
|
||||||
Moderator.setFocusUserJid(config.focusUserJid);
|
|
||||||
// Send create conference IQ
|
|
||||||
var iq = Moderator.createConferenceIq(roomName);
|
|
||||||
var self = this;
|
|
||||||
connection.sendIQ(
|
|
||||||
iq,
|
|
||||||
function (result) {
|
|
||||||
|
|
||||||
// Setup config options
|
|
||||||
Moderator.parseConfigOptions(result);
|
|
||||||
|
|
||||||
if ('true' === $(result).find('conference').attr('ready')) {
|
|
||||||
// Reset both timers
|
|
||||||
getNextTimeout(true);
|
|
||||||
getNextErrorTimeout(true);
|
|
||||||
// Exec callback
|
|
||||||
callback();
|
|
||||||
} else {
|
|
||||||
var waitMs = getNextTimeout();
|
|
||||||
console.info("Waiting for the focus... " + waitMs);
|
|
||||||
// Reset error timeout
|
|
||||||
getNextErrorTimeout(true);
|
|
||||||
window.setTimeout(
|
|
||||||
function () {
|
|
||||||
Moderator.allocateConferenceFocus(
|
|
||||||
roomName, callback);
|
|
||||||
}, waitMs);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
// Invalid session ? remove and try again
|
|
||||||
// without session ID to get a new one
|
|
||||||
var invalidSession
|
|
||||||
= $(error).find('>error>session-invalid').length;
|
|
||||||
if (invalidSession) {
|
|
||||||
console.info("Session expired! - removing");
|
|
||||||
localStorage.removeItem("sessionId");
|
|
||||||
}
|
|
||||||
if ($(error).find('>error>graceful-shutdown').length) {
|
|
||||||
eventEmitter.emit(XMPPEvents.GRACEFUL_SHUTDOWN);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Check for error returned by the reservation system
|
|
||||||
var reservationErr = $(error).find('>error>reservation-error');
|
|
||||||
if (reservationErr.length) {
|
|
||||||
// Trigger error event
|
|
||||||
var errorCode = reservationErr.attr('error-code');
|
|
||||||
var errorMsg;
|
|
||||||
if ($(error).find('>error>text')) {
|
|
||||||
errorMsg = $(error).find('>error>text').text();
|
|
||||||
}
|
|
||||||
eventEmitter.emit(
|
|
||||||
XMPPEvents.RESERVATION_ERROR, errorCode, errorMsg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Not authorized to create new room
|
|
||||||
if ($(error).find('>error>not-authorized').length) {
|
|
||||||
console.warn("Unauthorized to start the conference", error);
|
|
||||||
var toDomain
|
|
||||||
= Strophe.getDomainFromJid(error.getAttribute('to'));
|
|
||||||
if (toDomain !== config.hosts.anonymousdomain) {
|
|
||||||
// FIXME: "is external" should come either from
|
|
||||||
// the focus or config.js
|
|
||||||
externalAuthEnabled = true;
|
|
||||||
}
|
|
||||||
eventEmitter.emit(
|
|
||||||
XMPPEvents.AUTHENTICATION_REQUIRED,
|
|
||||||
function () {
|
|
||||||
Moderator.allocateConferenceFocus(
|
|
||||||
roomName, callback);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var waitMs = getNextErrorTimeout();
|
|
||||||
console.error("Focus error, retry after " + waitMs, error);
|
|
||||||
// Show message
|
|
||||||
var focusComponent = Moderator.getFocusComponent();
|
|
||||||
var retrySec = waitMs / 1000;
|
|
||||||
// FIXME: message is duplicated ?
|
|
||||||
// Do not show in case of session invalid
|
|
||||||
// which means just a retry
|
|
||||||
if (!invalidSession) {
|
|
||||||
eventEmitter.emit(XMPPEvents.FOCUS_DISCONNECTED,
|
|
||||||
focusComponent, retrySec);
|
|
||||||
}
|
|
||||||
// Reset response timeout
|
|
||||||
getNextTimeout(true);
|
|
||||||
window.setTimeout(
|
|
||||||
function () {
|
|
||||||
Moderator.allocateConferenceFocus(roomName, callback);
|
|
||||||
}, waitMs);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
getLoginUrl: function (roomName, urlCallback) {
|
|
||||||
var iq = $iq({to: Moderator.getFocusComponent(), type: 'get'});
|
|
||||||
iq.c('login-url', {
|
|
||||||
xmlns: 'http://jitsi.org/protocol/focus',
|
|
||||||
room: roomName,
|
|
||||||
'machine-uid': Settings.getSettings().uid
|
|
||||||
});
|
|
||||||
connection.sendIQ(
|
|
||||||
iq,
|
|
||||||
function (result) {
|
|
||||||
var url = $(result).find('login-url').attr('url');
|
|
||||||
url = url = decodeURIComponent(url);
|
|
||||||
if (url) {
|
|
||||||
console.info("Got auth url: " + url);
|
|
||||||
urlCallback(url);
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
"Failed to get auth url from the focus", result);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.error("Get auth url error", error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
getPopupLoginUrl: function (roomName, urlCallback) {
|
|
||||||
var iq = $iq({to: Moderator.getFocusComponent(), type: 'get'});
|
|
||||||
iq.c('login-url', {
|
|
||||||
xmlns: 'http://jitsi.org/protocol/focus',
|
|
||||||
room: roomName,
|
|
||||||
'machine-uid': Settings.getSettings().uid,
|
|
||||||
popup: true
|
|
||||||
});
|
|
||||||
connection.sendIQ(
|
|
||||||
iq,
|
|
||||||
function (result) {
|
|
||||||
var url = $(result).find('login-url').attr('url');
|
|
||||||
url = url = decodeURIComponent(url);
|
|
||||||
if (url) {
|
|
||||||
console.info("Got POPUP auth url: " + url);
|
|
||||||
urlCallback(url);
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
"Failed to get POPUP auth url from the focus", result);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.error('Get POPUP auth url error', error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
logout: function (callback) {
|
|
||||||
var iq = $iq({to: Moderator.getFocusComponent(), type: 'set'});
|
|
||||||
var sessionId = localStorage.getItem('sessionId');
|
|
||||||
if (!sessionId) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
iq.c('logout', {
|
|
||||||
xmlns: 'http://jitsi.org/protocol/focus',
|
|
||||||
'session-id': sessionId
|
|
||||||
});
|
|
||||||
connection.sendIQ(
|
|
||||||
iq,
|
|
||||||
function (result) {
|
|
||||||
var logoutUrl = $(result).find('logout').attr('logout-url');
|
|
||||||
if (logoutUrl) {
|
|
||||||
logoutUrl = decodeURIComponent(logoutUrl);
|
|
||||||
}
|
|
||||||
console.info("Log out OK, url: " + logoutUrl, result);
|
|
||||||
localStorage.removeItem('sessionId');
|
|
||||||
callback(logoutUrl);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.error("Logout error", error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = Moderator;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,178 +0,0 @@
|
||||||
/* global $, $iq, config, connection, focusMucJid, messageHandler,
|
|
||||||
Toolbar, Util */
|
|
||||||
var Moderator = require("./moderator");
|
|
||||||
|
|
||||||
|
|
||||||
var recordingToken = null;
|
|
||||||
var recordingEnabled;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to use a jirecon component for recording, or use the videobridge
|
|
||||||
* through COLIBRI.
|
|
||||||
*/
|
|
||||||
var useJirecon;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ID of the jirecon recording session. Jirecon generates it when we
|
|
||||||
* initially start recording, and it needs to be used in subsequent requests
|
|
||||||
* to jirecon.
|
|
||||||
*/
|
|
||||||
var jireconRid = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The callback to update the recording button. Currently used from colibri
|
|
||||||
* after receiving a pending status.
|
|
||||||
*/
|
|
||||||
var recordingStateChangeCallback = null;
|
|
||||||
|
|
||||||
function setRecordingToken(token) {
|
|
||||||
recordingToken = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setRecordingJirecon(state, token, callback, connection) {
|
|
||||||
if (state == recordingEnabled){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var iq = $iq({to: config.hosts.jirecon, type: 'set'})
|
|
||||||
.c('recording', {xmlns: 'http://jitsi.org/protocol/jirecon',
|
|
||||||
action: (state === 'on') ? 'start' : 'stop',
|
|
||||||
mucjid: connection.emuc.roomjid});
|
|
||||||
if (state === 'off'){
|
|
||||||
iq.attrs({rid: jireconRid});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Start recording');
|
|
||||||
|
|
||||||
connection.sendIQ(
|
|
||||||
iq,
|
|
||||||
function (result) {
|
|
||||||
// TODO wait for an IQ with the real status, since this is
|
|
||||||
// provisional?
|
|
||||||
jireconRid = $(result).find('recording').attr('rid');
|
|
||||||
console.log('Recording ' +
|
|
||||||
((state === 'on') ? 'started' : 'stopped') +
|
|
||||||
'(jirecon)' + result);
|
|
||||||
recordingEnabled = state;
|
|
||||||
if (state === 'off'){
|
|
||||||
jireconRid = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(state);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.log('Failed to start recording, error: ', error);
|
|
||||||
callback(recordingEnabled);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends a COLIBRI message which enables or disables (according to 'state')
|
|
||||||
// the recording on the bridge. Waits for the result IQ and calls 'callback'
|
|
||||||
// with the new recording state, according to the IQ.
|
|
||||||
function setRecordingColibri(state, token, callback, connection) {
|
|
||||||
var elem = $iq({to: connection.emuc.focusMucJid, type: 'set'});
|
|
||||||
elem.c('conference', {
|
|
||||||
xmlns: 'http://jitsi.org/protocol/colibri'
|
|
||||||
});
|
|
||||||
elem.c('recording', {state: state, token: token});
|
|
||||||
|
|
||||||
connection.sendIQ(elem,
|
|
||||||
function (result) {
|
|
||||||
console.log('Set recording "', state, '". Result:', result);
|
|
||||||
var recordingElem = $(result).find('>conference>recording');
|
|
||||||
var newState = recordingElem.attr('state');
|
|
||||||
|
|
||||||
recordingEnabled = newState;
|
|
||||||
callback(newState);
|
|
||||||
|
|
||||||
if (newState === 'pending' && !recordingStateChangeCallback) {
|
|
||||||
recordingStateChangeCallback = callback;
|
|
||||||
connection.addHandler(function(iq){
|
|
||||||
var state = $(iq).find('recording').attr('state');
|
|
||||||
if (state)
|
|
||||||
recordingStateChangeCallback(state);
|
|
||||||
}, 'http://jitsi.org/protocol/colibri', 'iq', null, null, null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.warn(error);
|
|
||||||
callback(recordingEnabled);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setRecording(state, token, callback, connection) {
|
|
||||||
if (useJirecon){
|
|
||||||
setRecordingJirecon(state, token, callback, connection);
|
|
||||||
} else {
|
|
||||||
setRecordingColibri(state, token, callback, connection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var Recording = {
|
|
||||||
init: function () {
|
|
||||||
useJirecon = config.hosts &&
|
|
||||||
(typeof config.hosts.jirecon != "undefined");
|
|
||||||
},
|
|
||||||
toggleRecording: function (tokenEmptyCallback,
|
|
||||||
recordingStateChangeCallback,
|
|
||||||
connection) {
|
|
||||||
if (!Moderator.isModerator()) {
|
|
||||||
console.log(
|
|
||||||
'non-focus, or conference not yet organized:' +
|
|
||||||
' not enabling recording');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
// Jirecon does not (currently) support a token.
|
|
||||||
if (!recordingToken && !useJirecon) {
|
|
||||||
tokenEmptyCallback(function (value) {
|
|
||||||
setRecordingToken(value);
|
|
||||||
self.toggleRecording(tokenEmptyCallback,
|
|
||||||
recordingStateChangeCallback,
|
|
||||||
connection);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var oldState = recordingEnabled;
|
|
||||||
var newState = (oldState === 'off' || !oldState) ? 'on' : 'off';
|
|
||||||
|
|
||||||
setRecording(newState,
|
|
||||||
recordingToken,
|
|
||||||
function (state) {
|
|
||||||
console.log("New recording state: ", state);
|
|
||||||
if (state === oldState) {
|
|
||||||
// FIXME: new focus:
|
|
||||||
// this will not work when moderator changes
|
|
||||||
// during active session. Then it will assume that
|
|
||||||
// recording status has changed to true, but it might have
|
|
||||||
// been already true(and we only received actual status from
|
|
||||||
// the focus).
|
|
||||||
//
|
|
||||||
// SO we start with status null, so that it is initialized
|
|
||||||
// here and will fail only after second click, so if invalid
|
|
||||||
// token was used we have to press the button twice before
|
|
||||||
// current status will be fetched and token will be reset.
|
|
||||||
//
|
|
||||||
// Reliable way would be to return authentication error.
|
|
||||||
// Or status update when moderator connects.
|
|
||||||
// Or we have to stop recording session when current
|
|
||||||
// moderator leaves the room.
|
|
||||||
|
|
||||||
// Failed to change, reset the token because it might
|
|
||||||
// have been wrong
|
|
||||||
setRecordingToken(null);
|
|
||||||
}
|
|
||||||
recordingStateChangeCallback(state);
|
|
||||||
|
|
||||||
},
|
|
||||||
connection
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = Recording;
|
|
|
@ -1,706 +0,0 @@
|
||||||
/* jshint -W117 */
|
|
||||||
/* a simple MUC connection plugin
|
|
||||||
* can only handle a single MUC room
|
|
||||||
*/
|
|
||||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
|
||||||
var Moderator = require("./moderator");
|
|
||||||
|
|
||||||
module.exports = function(XMPP, eventEmitter) {
|
|
||||||
Strophe.addConnectionPlugin('emuc', {
|
|
||||||
connection: null,
|
|
||||||
roomjid: null,
|
|
||||||
myroomjid: null,
|
|
||||||
members: {},
|
|
||||||
list_members: [], // so we can elect a new focus
|
|
||||||
presMap: {},
|
|
||||||
preziMap: {},
|
|
||||||
lastPresenceMap: {},
|
|
||||||
joined: false,
|
|
||||||
isOwner: false,
|
|
||||||
role: null,
|
|
||||||
focusMucJid: null,
|
|
||||||
bridgeIsDown: false,
|
|
||||||
init: function (conn) {
|
|
||||||
this.connection = conn;
|
|
||||||
},
|
|
||||||
initPresenceMap: function (myroomjid) {
|
|
||||||
this.presMap['to'] = myroomjid;
|
|
||||||
this.presMap['xns'] = 'http://jabber.org/protocol/muc';
|
|
||||||
if (APP.RTC.localAudio && APP.RTC.localAudio.isMuted()) {
|
|
||||||
this.addAudioInfoToPresence(true);
|
|
||||||
}
|
|
||||||
if (APP.RTC.localVideo && APP.RTC.localVideo.isMuted()) {
|
|
||||||
this.addVideoInfoToPresence(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
doJoin: function (jid, password) {
|
|
||||||
this.myroomjid = jid;
|
|
||||||
|
|
||||||
console.info("Joined MUC as " + this.myroomjid);
|
|
||||||
|
|
||||||
this.initPresenceMap(this.myroomjid);
|
|
||||||
|
|
||||||
if (!this.roomjid) {
|
|
||||||
this.roomjid = Strophe.getBareJidFromJid(jid);
|
|
||||||
// add handlers (just once)
|
|
||||||
this.connection.addHandler(this.onPresence.bind(this), null, 'presence', null, null, this.roomjid, {matchBare: true});
|
|
||||||
this.connection.addHandler(this.onPresenceUnavailable.bind(this), null, 'presence', 'unavailable', null, this.roomjid, {matchBare: true});
|
|
||||||
this.connection.addHandler(this.onPresenceError.bind(this), null, 'presence', 'error', null, this.roomjid, {matchBare: true});
|
|
||||||
this.connection.addHandler(this.onMessage.bind(this), null, 'message', null, null, this.roomjid, {matchBare: true});
|
|
||||||
}
|
|
||||||
if (password !== undefined) {
|
|
||||||
this.presMap['password'] = password;
|
|
||||||
}
|
|
||||||
this.sendPresence();
|
|
||||||
},
|
|
||||||
doLeave: function () {
|
|
||||||
console.log("do leave", this.myroomjid);
|
|
||||||
var pres = $pres({to: this.myroomjid, type: 'unavailable' });
|
|
||||||
this.presMap.length = 0;
|
|
||||||
this.connection.send(pres);
|
|
||||||
},
|
|
||||||
createNonAnonymousRoom: function () {
|
|
||||||
// http://xmpp.org/extensions/xep-0045.html#createroom-reserved
|
|
||||||
|
|
||||||
var getForm = $iq({type: 'get', to: this.roomjid})
|
|
||||||
.c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'})
|
|
||||||
.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
this.connection.sendIQ(getForm, function (form) {
|
|
||||||
|
|
||||||
if (!$(form).find(
|
|
||||||
'>query>x[xmlns="jabber:x:data"]' +
|
|
||||||
'>field[var="muc#roomconfig_whois"]').length) {
|
|
||||||
|
|
||||||
console.error('non-anonymous rooms not supported');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var formSubmit = $iq({to: this.roomjid, type: 'set'})
|
|
||||||
.c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'});
|
|
||||||
|
|
||||||
formSubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
|
|
||||||
|
|
||||||
formSubmit.c('field', {'var': 'FORM_TYPE'})
|
|
||||||
.c('value')
|
|
||||||
.t('http://jabber.org/protocol/muc#roomconfig').up().up();
|
|
||||||
|
|
||||||
formSubmit.c('field', {'var': 'muc#roomconfig_whois'})
|
|
||||||
.c('value').t('anyone').up().up();
|
|
||||||
|
|
||||||
self.connection.sendIQ(formSubmit);
|
|
||||||
|
|
||||||
}, function (error) {
|
|
||||||
console.error("Error getting room configuration form");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onPresence: function (pres) {
|
|
||||||
var from = pres.getAttribute('from');
|
|
||||||
|
|
||||||
// What is this for? A workaround for something?
|
|
||||||
if (pres.getAttribute('type')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse etherpad tag.
|
|
||||||
var etherpad = $(pres).find('>etherpad');
|
|
||||||
if (etherpad.length) {
|
|
||||||
if (config.etherpad_base) {
|
|
||||||
eventEmitter.emit(XMPPEvents.ETHERPAD, etherpad.text());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var url;
|
|
||||||
// Parse prezi tag.
|
|
||||||
var presentation = $(pres).find('>prezi');
|
|
||||||
if (presentation.length) {
|
|
||||||
url = presentation.attr('url');
|
|
||||||
var current = presentation.find('>current').text();
|
|
||||||
|
|
||||||
console.log('presentation info received from', from, url);
|
|
||||||
|
|
||||||
if (this.preziMap[from] == null) {
|
|
||||||
this.preziMap[from] = url;
|
|
||||||
|
|
||||||
$(document).trigger('presentationadded.muc', [from, url, current]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$(document).trigger('gotoslide.muc', [from, url, current]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (this.preziMap[from] != null) {
|
|
||||||
url = this.preziMap[from];
|
|
||||||
delete this.preziMap[from];
|
|
||||||
$(document).trigger('presentationremoved.muc', [from, url]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// store the last presence for participant
|
|
||||||
this.lastPresenceMap[from] = {};
|
|
||||||
|
|
||||||
// Parse audio info tag.
|
|
||||||
var audioMuted = $(pres).find('>audiomuted');
|
|
||||||
if (audioMuted.length) {
|
|
||||||
eventEmitter.emit(XMPPEvents.PARTICIPANT_AUDIO_MUTED,
|
|
||||||
from, (audioMuted.text() === "true"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse video info tag.
|
|
||||||
var videoMuted = $(pres).find('>videomuted');
|
|
||||||
if (videoMuted.length) {
|
|
||||||
var value = (videoMuted.text() === "true");
|
|
||||||
this.lastPresenceMap[from].videoMuted = value;
|
|
||||||
eventEmitter.emit(XMPPEvents.PARTICIPANT_VIDEO_MUTED, from, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
var startMuted = $(pres).find('>startmuted');
|
|
||||||
if (startMuted.length && Moderator.isPeerModerator(from)) {
|
|
||||||
eventEmitter.emit(XMPPEvents.START_MUTED_SETTING_CHANGED,
|
|
||||||
startMuted.attr("audio") === "true",
|
|
||||||
startMuted.attr("video") === "true");
|
|
||||||
}
|
|
||||||
|
|
||||||
var devices = $(pres).find('>devices');
|
|
||||||
if(devices.length)
|
|
||||||
{
|
|
||||||
var audio = devices.find('>audio');
|
|
||||||
var video = devices.find('>video');
|
|
||||||
var devicesValues = {audio: false, video: false};
|
|
||||||
if(audio.length && audio.text() === "true")
|
|
||||||
{
|
|
||||||
devicesValues.audio = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(video.length && video.text() === "true")
|
|
||||||
{
|
|
||||||
devicesValues.video = true;
|
|
||||||
}
|
|
||||||
eventEmitter.emit(XMPPEvents.DEVICE_AVAILABLE,
|
|
||||||
Strophe.getResourceFromJid(from), devicesValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
var videoType = $(pres).find('>videoType');
|
|
||||||
if (videoType.length)
|
|
||||||
{
|
|
||||||
if (videoType.text().length)
|
|
||||||
{
|
|
||||||
eventEmitter.emit(XMPPEvents.PARTICIPANT_VIDEO_TYPE_CHANGED,
|
|
||||||
Strophe.getResourceFromJid(from), videoType.text());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var stats = $(pres).find('>stats');
|
|
||||||
if (stats.length) {
|
|
||||||
var statsObj = {};
|
|
||||||
Strophe.forEachChild(stats[0], "stat", function (el) {
|
|
||||||
statsObj[el.getAttribute("name")] = el.getAttribute("value");
|
|
||||||
});
|
|
||||||
eventEmitter.emit(XMPPEvents.REMOTE_STATS, from, statsObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse status.
|
|
||||||
if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="201"]').length) {
|
|
||||||
this.isOwner = true;
|
|
||||||
this.createNonAnonymousRoom();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse roles.
|
|
||||||
var member = {};
|
|
||||||
member.show = $(pres).find('>show').text();
|
|
||||||
member.status = $(pres).find('>status').text();
|
|
||||||
var tmp = $(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>item');
|
|
||||||
member.affiliation = tmp.attr('affiliation');
|
|
||||||
member.role = tmp.attr('role');
|
|
||||||
|
|
||||||
// Focus recognition
|
|
||||||
member.jid = tmp.attr('jid');
|
|
||||||
member.isFocus = false;
|
|
||||||
if (member.jid
|
|
||||||
&& member.jid.indexOf(Moderator.getFocusUserJid() + "/") == 0) {
|
|
||||||
member.isFocus = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var nicktag = $(pres).find('>nick[xmlns="http://jabber.org/protocol/nick"]');
|
|
||||||
member.displayName = (nicktag.length > 0 ? nicktag.text() : null);
|
|
||||||
|
|
||||||
if (from == this.myroomjid) {
|
|
||||||
if (member.affiliation == 'owner') this.isOwner = true;
|
|
||||||
if (this.role !== member.role) {
|
|
||||||
this.role = member.role;
|
|
||||||
|
|
||||||
eventEmitter.emit(XMPPEvents.LOCAL_ROLE_CHANGED,
|
|
||||||
from, member, pres, Moderator.isModerator());
|
|
||||||
}
|
|
||||||
if (!this.joined) {
|
|
||||||
this.joined = true;
|
|
||||||
console.log("(TIME) MUC joined:\t",
|
|
||||||
window.performance.now());
|
|
||||||
eventEmitter.emit(XMPPEvents.MUC_JOINED, from, member);
|
|
||||||
this.list_members.push(from);
|
|
||||||
}
|
|
||||||
} else if (this.members[from] === undefined) {
|
|
||||||
// new participant
|
|
||||||
this.members[from] = member;
|
|
||||||
this.list_members.push(from);
|
|
||||||
console.log('entered', from, member);
|
|
||||||
if (member.isFocus) {
|
|
||||||
this.focusMucJid = from;
|
|
||||||
console.info("Ignore focus: " + from + ", real JID: " + member.jid);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var id = $(pres).find('>userId').text();
|
|
||||||
var email = $(pres).find('>email');
|
|
||||||
if (email.length > 0) {
|
|
||||||
id = email.text();
|
|
||||||
}
|
|
||||||
eventEmitter.emit(XMPPEvents.MUC_MEMBER_JOINED, from, id, member.displayName);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Presence update for existing participant
|
|
||||||
// Watch role change:
|
|
||||||
if (this.members[from].role != member.role) {
|
|
||||||
this.members[from].role = member.role;
|
|
||||||
eventEmitter.emit(XMPPEvents.MUC_ROLE_CHANGED,
|
|
||||||
member.role, member.displayName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// store the new
|
|
||||||
if(member.displayName)
|
|
||||||
this.members[from].displayName = member.displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always trigger presence to update bindings
|
|
||||||
this.parsePresence(from, member, pres);
|
|
||||||
|
|
||||||
// Trigger status message update
|
|
||||||
if (member.status) {
|
|
||||||
eventEmitter.emit(XMPPEvents.PRESENCE_STATUS, from, member);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
onPresenceUnavailable: function (pres) {
|
|
||||||
var from = pres.getAttribute('from');
|
|
||||||
// room destroyed ?
|
|
||||||
if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]' +
|
|
||||||
'>destroy').length) {
|
|
||||||
var reason;
|
|
||||||
var reasonSelect = $(pres).find(
|
|
||||||
'>x[xmlns="http://jabber.org/protocol/muc#user"]' +
|
|
||||||
'>destroy>reason');
|
|
||||||
if (reasonSelect.length) {
|
|
||||||
reason = reasonSelect.text();
|
|
||||||
}
|
|
||||||
XMPP.disposeConference(false);
|
|
||||||
eventEmitter.emit(XMPPEvents.MUC_DESTROYED, reason);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status code 110 indicates that this notification is "self-presence".
|
|
||||||
if (!$(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="110"]').length) {
|
|
||||||
delete this.members[from];
|
|
||||||
this.list_members.splice(this.list_members.indexOf(from), 1);
|
|
||||||
this.onParticipantLeft(from);
|
|
||||||
}
|
|
||||||
// If the status code is 110 this means we're leaving and we would like
|
|
||||||
// to remove everyone else from our view, so we trigger the event.
|
|
||||||
else if (this.list_members.length > 1) {
|
|
||||||
for (var i = 0; i < this.list_members.length; i++) {
|
|
||||||
var member = this.list_members[i];
|
|
||||||
delete this.members[i];
|
|
||||||
this.list_members.splice(i, 1);
|
|
||||||
this.onParticipantLeft(member);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="307"]').length) {
|
|
||||||
$(document).trigger('kicked.muc', [from]);
|
|
||||||
if (this.myroomjid === from) {
|
|
||||||
XMPP.disposeConference(false);
|
|
||||||
eventEmitter.emit(XMPPEvents.KICKED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.lastPresenceMap[from] != null) {
|
|
||||||
delete this.lastPresenceMap[from];
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
onPresenceError: function (pres) {
|
|
||||||
var from = pres.getAttribute('from');
|
|
||||||
if ($(pres).find('>error[type="auth"]>not-authorized[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
|
|
||||||
console.log('on password required', from);
|
|
||||||
var self = this;
|
|
||||||
eventEmitter.emit(XMPPEvents.PASSWORD_REQUIRED, function (value) {
|
|
||||||
self.doJoin(from, value);
|
|
||||||
});
|
|
||||||
} else if ($(pres).find(
|
|
||||||
'>error[type="cancel"]>not-allowed[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
|
|
||||||
var toDomain = Strophe.getDomainFromJid(pres.getAttribute('to'));
|
|
||||||
if (toDomain === config.hosts.anonymousdomain) {
|
|
||||||
// enter the room by replying with 'not-authorized'. This would
|
|
||||||
// result in reconnection from authorized domain.
|
|
||||||
// We're either missing Jicofo/Prosody config for anonymous
|
|
||||||
// domains or something is wrong.
|
|
||||||
// XMPP.promptLogin();
|
|
||||||
eventEmitter.emit(XMPPEvents.ROOM_JOIN_ERROR, pres);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
console.warn('onPresError ', pres);
|
|
||||||
eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR, pres);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('onPresError ', pres);
|
|
||||||
eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR, pres);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
sendMessage: function (body, nickname) {
|
|
||||||
var msg = $msg({to: this.roomjid, type: 'groupchat'});
|
|
||||||
msg.c('body', body).up();
|
|
||||||
if (nickname) {
|
|
||||||
msg.c('nick', {xmlns: 'http://jabber.org/protocol/nick'}).t(nickname).up().up();
|
|
||||||
}
|
|
||||||
this.connection.send(msg);
|
|
||||||
eventEmitter.emit(XMPPEvents.SENDING_CHAT_MESSAGE, body);
|
|
||||||
},
|
|
||||||
setSubject: function (subject) {
|
|
||||||
var msg = $msg({to: this.roomjid, type: 'groupchat'});
|
|
||||||
msg.c('subject', subject);
|
|
||||||
this.connection.send(msg);
|
|
||||||
console.log("topic changed to " + subject);
|
|
||||||
},
|
|
||||||
onMessage: function (msg) {
|
|
||||||
// FIXME: this is a hack. but jingle on muc makes nickchanges hard
|
|
||||||
var from = msg.getAttribute('from');
|
|
||||||
var nick =
|
|
||||||
$(msg).find('>nick[xmlns="http://jabber.org/protocol/nick"]')
|
|
||||||
.text() ||
|
|
||||||
Strophe.getResourceFromJid(from);
|
|
||||||
|
|
||||||
var txt = $(msg).find('>body').text();
|
|
||||||
var type = msg.getAttribute("type");
|
|
||||||
if (type == "error") {
|
|
||||||
eventEmitter.emit(XMPPEvents.CHAT_ERROR_RECEIVED,
|
|
||||||
$(msg).find('>text').text(), txt);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var subject = $(msg).find('>subject');
|
|
||||||
if (subject.length) {
|
|
||||||
var subjectText = subject.text();
|
|
||||||
if (subjectText || subjectText == "") {
|
|
||||||
eventEmitter.emit(XMPPEvents.SUBJECT_CHANGED, subjectText);
|
|
||||||
console.log("Subject is changed to " + subjectText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// xep-0203 delay
|
|
||||||
var stamp = $(msg).find('>delay').attr('stamp');
|
|
||||||
|
|
||||||
if (!stamp) {
|
|
||||||
// or xep-0091 delay, UTC timestamp
|
|
||||||
stamp = $(msg).find('>[xmlns="jabber:x:delay"]').attr('stamp');
|
|
||||||
|
|
||||||
if (stamp) {
|
|
||||||
// the format is CCYYMMDDThh:mm:ss
|
|
||||||
var dateParts = stamp.match(/(\d{4})(\d{2})(\d{2}T\d{2}:\d{2}:\d{2})/);
|
|
||||||
stamp = dateParts[1] + "-" + dateParts[2] + "-" + dateParts[3] + "Z";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (txt) {
|
|
||||||
console.log('chat', nick, txt);
|
|
||||||
eventEmitter.emit(XMPPEvents.MESSAGE_RECEIVED,
|
|
||||||
from, nick, txt, this.myroomjid, stamp);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
lockRoom: function (key, onSuccess, onError, onNotSupported) {
|
|
||||||
//http://xmpp.org/extensions/xep-0045.html#roomconfig
|
|
||||||
var ob = this;
|
|
||||||
this.connection.sendIQ($iq({to: this.roomjid, type: 'get'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'}),
|
|
||||||
function (res) {
|
|
||||||
if ($(res).find('>query>x[xmlns="jabber:x:data"]>field[var="muc#roomconfig_roomsecret"]').length) {
|
|
||||||
var formsubmit = $iq({to: ob.roomjid, type: 'set'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'});
|
|
||||||
formsubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
|
|
||||||
formsubmit.c('field', {'var': 'FORM_TYPE'}).c('value').t('http://jabber.org/protocol/muc#roomconfig').up().up();
|
|
||||||
formsubmit.c('field', {'var': 'muc#roomconfig_roomsecret'}).c('value').t(key).up().up();
|
|
||||||
// Fixes a bug in prosody 0.9.+ https://code.google.com/p/lxmppd/issues/detail?id=373
|
|
||||||
formsubmit.c('field', {'var': 'muc#roomconfig_whois'}).c('value').t('anyone').up().up();
|
|
||||||
// FIXME: is muc#roomconfig_passwordprotectedroom required?
|
|
||||||
ob.connection.sendIQ(formsubmit,
|
|
||||||
onSuccess,
|
|
||||||
onError);
|
|
||||||
} else {
|
|
||||||
onNotSupported();
|
|
||||||
}
|
|
||||||
}, onError);
|
|
||||||
},
|
|
||||||
kick: function (jid) {
|
|
||||||
var kickIQ = $iq({to: this.roomjid, type: 'set'})
|
|
||||||
.c('query', {xmlns: 'http://jabber.org/protocol/muc#admin'})
|
|
||||||
.c('item', {nick: Strophe.getResourceFromJid(jid), role: 'none'})
|
|
||||||
.c('reason').t('You have been kicked.').up().up().up();
|
|
||||||
|
|
||||||
this.connection.sendIQ(
|
|
||||||
kickIQ,
|
|
||||||
function (result) {
|
|
||||||
console.log('Kick participant with jid: ', jid, result);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.log('Kick participant error: ', error);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
sendPresence: function () {
|
|
||||||
if (!this.presMap['to']) {
|
|
||||||
// Too early to send presence - not initialized
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var pres = $pres({to: this.presMap['to'] });
|
|
||||||
pres.c('x', {xmlns: this.presMap['xns']});
|
|
||||||
|
|
||||||
if (this.presMap['password']) {
|
|
||||||
pres.c('password').t(this.presMap['password']).up();
|
|
||||||
}
|
|
||||||
|
|
||||||
pres.up();
|
|
||||||
|
|
||||||
// Send XEP-0115 'c' stanza that contains our capabilities info
|
|
||||||
if (this.connection.caps) {
|
|
||||||
this.connection.caps.node = config.clientNode;
|
|
||||||
pres.c('c', this.connection.caps.generateCapsAttrs()).up();
|
|
||||||
}
|
|
||||||
|
|
||||||
pres.c('user-agent', {xmlns: 'http://jitsi.org/jitmeet/user-agent'})
|
|
||||||
.t(navigator.userAgent).up();
|
|
||||||
|
|
||||||
if (this.presMap['bridgeIsDown']) {
|
|
||||||
pres.c('bridgeIsDown').up();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.presMap['email']) {
|
|
||||||
pres.c('email').t(this.presMap['email']).up();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.presMap['userId']) {
|
|
||||||
pres.c('userId').t(this.presMap['userId']).up();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.presMap['displayName']) {
|
|
||||||
// XEP-0172
|
|
||||||
pres.c('nick', {xmlns: 'http://jabber.org/protocol/nick'})
|
|
||||||
.t(this.presMap['displayName']).up();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.presMap["devices"])
|
|
||||||
{
|
|
||||||
pres.c('devices').c('audio').t(this.presMap['devices'].audio).up()
|
|
||||||
.c('video').t(this.presMap['devices'].video).up().up();
|
|
||||||
}
|
|
||||||
if (this.presMap['audions']) {
|
|
||||||
pres.c('audiomuted', {xmlns: this.presMap['audions']})
|
|
||||||
.t(this.presMap['audiomuted']).up();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.presMap['videons']) {
|
|
||||||
pres.c('videomuted', {xmlns: this.presMap['videons']})
|
|
||||||
.t(this.presMap['videomuted']).up();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.presMap['videoTypeNs']) {
|
|
||||||
pres.c('videoType', { xmlns: this.presMap['videoTypeNs'] })
|
|
||||||
.t(this.presMap['videoType']).up();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.presMap['statsns']) {
|
|
||||||
var stats = pres.c('stats', {xmlns: this.presMap['statsns']});
|
|
||||||
for (var stat in this.presMap["stats"])
|
|
||||||
if (this.presMap["stats"][stat] != null)
|
|
||||||
stats.c("stat", {name: stat, value: this.presMap["stats"][stat]}).up();
|
|
||||||
pres.up();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.presMap['prezins']) {
|
|
||||||
pres.c('prezi',
|
|
||||||
{xmlns: this.presMap['prezins'],
|
|
||||||
'url': this.presMap['preziurl']})
|
|
||||||
.c('current').t(this.presMap['prezicurrent']).up().up();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is only for backward compatibility with clients which
|
|
||||||
// don't support getting sources from Jingle (i.e. jirecon).
|
|
||||||
if (this.presMap['medians']) {
|
|
||||||
pres.c('media', {xmlns: this.presMap['medians']});
|
|
||||||
var sourceNumber = 0;
|
|
||||||
Object.keys(this.presMap).forEach(function (key) {
|
|
||||||
if (key.indexOf('source') >= 0) {
|
|
||||||
sourceNumber++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (sourceNumber > 0) {
|
|
||||||
for (var i = 1; i <= sourceNumber / 3; i++) {
|
|
||||||
pres.c('source',
|
|
||||||
{
|
|
||||||
type: this.presMap['source' + i + '_type'],
|
|
||||||
ssrc: this.presMap['source' + i + '_ssrc'],
|
|
||||||
direction: this.presMap['source' + i + '_direction']
|
|
||||||
|| 'sendrecv'
|
|
||||||
}
|
|
||||||
).up();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pres.up();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.presMap["startMuted"] !== undefined)
|
|
||||||
{
|
|
||||||
pres.c("startmuted", {audio: this.presMap["startMuted"].audio,
|
|
||||||
video: this.presMap["startMuted"].video,
|
|
||||||
xmlns: "http://jitsi.org/jitmeet/start-muted"});
|
|
||||||
delete this.presMap["startMuted"];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.token) {
|
|
||||||
pres.c('token', { xmlns: 'http://jitsi.org/jitmeet/auth-token'}).t(config.token).up();
|
|
||||||
}
|
|
||||||
|
|
||||||
pres.up();
|
|
||||||
this.connection.send(pres);
|
|
||||||
},
|
|
||||||
addDisplayNameToPresence: function (displayName) {
|
|
||||||
this.presMap['displayName'] = displayName;
|
|
||||||
},
|
|
||||||
// This is only for backward compatibility with clients which
|
|
||||||
// don't support getting sources from Jingle (i.e. jirecon).
|
|
||||||
addMediaToPresence: function (sourceNumber, mtype, ssrcs, direction) {
|
|
||||||
if (!this.presMap['medians'])
|
|
||||||
this.presMap['medians'] = 'http://estos.de/ns/mjs';
|
|
||||||
|
|
||||||
this.presMap['source' + sourceNumber + '_type'] = mtype;
|
|
||||||
this.presMap['source' + sourceNumber + '_ssrc'] = ssrcs;
|
|
||||||
this.presMap['source' + sourceNumber + '_direction'] = direction;
|
|
||||||
},
|
|
||||||
// This is only for backward compatibility with clients which
|
|
||||||
// don't support getting sources from Jingle (i.e. jirecon).
|
|
||||||
clearPresenceMedia: function () {
|
|
||||||
var self = this;
|
|
||||||
Object.keys(this.presMap).forEach(function (key) {
|
|
||||||
if (key.indexOf('source') != -1) {
|
|
||||||
delete self.presMap[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
addDevicesToPresence: function (devices) {
|
|
||||||
this.presMap['devices'] = devices;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Adds the info about the type of our video stream.
|
|
||||||
* @param videoType 'camera' or 'screen'
|
|
||||||
*/
|
|
||||||
addVideoTypeToPresence: function (videoType) {
|
|
||||||
this.presMap['videoTypeNs'] = 'http://jitsi.org/jitmeet/video';
|
|
||||||
this.presMap['videoType'] = videoType;
|
|
||||||
},
|
|
||||||
addPreziToPresence: function (url, currentSlide) {
|
|
||||||
this.presMap['prezins'] = 'http://jitsi.org/jitmeet/prezi';
|
|
||||||
this.presMap['preziurl'] = url;
|
|
||||||
this.presMap['prezicurrent'] = currentSlide;
|
|
||||||
},
|
|
||||||
removePreziFromPresence: function () {
|
|
||||||
delete this.presMap['prezins'];
|
|
||||||
delete this.presMap['preziurl'];
|
|
||||||
delete this.presMap['prezicurrent'];
|
|
||||||
},
|
|
||||||
addCurrentSlideToPresence: function (currentSlide) {
|
|
||||||
this.presMap['prezicurrent'] = currentSlide;
|
|
||||||
},
|
|
||||||
getPrezi: function (roomjid) {
|
|
||||||
return this.preziMap[roomjid];
|
|
||||||
},
|
|
||||||
addAudioInfoToPresence: function (isMuted) {
|
|
||||||
this.presMap['audions'] = 'http://jitsi.org/jitmeet/audio';
|
|
||||||
this.presMap['audiomuted'] = isMuted.toString();
|
|
||||||
},
|
|
||||||
addVideoInfoToPresence: function (isMuted) {
|
|
||||||
this.presMap['videons'] = 'http://jitsi.org/jitmeet/video';
|
|
||||||
this.presMap['videomuted'] = isMuted.toString();
|
|
||||||
},
|
|
||||||
addConnectionInfoToPresence: function (stats) {
|
|
||||||
this.presMap['statsns'] = 'http://jitsi.org/jitmeet/stats';
|
|
||||||
this.presMap['stats'] = stats;
|
|
||||||
},
|
|
||||||
findJidFromResource: function (resourceJid) {
|
|
||||||
if (resourceJid &&
|
|
||||||
resourceJid === Strophe.getResourceFromJid(this.myroomjid)) {
|
|
||||||
return this.myroomjid;
|
|
||||||
}
|
|
||||||
var peerJid = null;
|
|
||||||
Object.keys(this.members).some(function (jid) {
|
|
||||||
peerJid = jid;
|
|
||||||
return Strophe.getResourceFromJid(jid) === resourceJid;
|
|
||||||
});
|
|
||||||
return peerJid;
|
|
||||||
},
|
|
||||||
addBridgeIsDownToPresence: function () {
|
|
||||||
this.presMap['bridgeIsDown'] = true;
|
|
||||||
},
|
|
||||||
addEmailToPresence: function (email) {
|
|
||||||
this.presMap['email'] = email;
|
|
||||||
},
|
|
||||||
addUserIdToPresence: function (userId) {
|
|
||||||
this.presMap['userId'] = userId;
|
|
||||||
},
|
|
||||||
addStartMutedToPresence: function (audio, video) {
|
|
||||||
this.presMap["startMuted"] = {audio: audio, video: video};
|
|
||||||
},
|
|
||||||
isModerator: function () {
|
|
||||||
return this.role === 'moderator';
|
|
||||||
},
|
|
||||||
getMemberRole: function (peerJid) {
|
|
||||||
if (this.members[peerJid]) {
|
|
||||||
return this.members[peerJid].role;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
onParticipantLeft: function (jid) {
|
|
||||||
|
|
||||||
eventEmitter.emit(XMPPEvents.MUC_MEMBER_LEFT, jid);
|
|
||||||
|
|
||||||
this.connection.jingle.terminateByJid(jid);
|
|
||||||
|
|
||||||
if (this.getPrezi(jid)) {
|
|
||||||
$(document).trigger('presentationremoved.muc',
|
|
||||||
[jid, this.getPrezi(jid)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Moderator.onMucMemberLeft(jid);
|
|
||||||
},
|
|
||||||
parsePresence: function (from, member, pres) {
|
|
||||||
if($(pres).find(">bridgeIsDown").length > 0 && !this.bridgeIsDown) {
|
|
||||||
this.bridgeIsDown = true;
|
|
||||||
eventEmitter.emit(XMPPEvents.BRIDGE_DOWN);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(member.isFocus)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var displayName = !config.displayJids
|
|
||||||
? member.displayName : Strophe.getResourceFromJid(from);
|
|
||||||
|
|
||||||
if (displayName && displayName.length > 0) {
|
|
||||||
eventEmitter.emit(XMPPEvents.DISPLAY_NAME_CHANGED, from, displayName);
|
|
||||||
}
|
|
||||||
|
|
||||||
var id = $(pres).find('>userID').text();
|
|
||||||
var email = $(pres).find('>email');
|
|
||||||
if (email.length > 0) {
|
|
||||||
id = email.text();
|
|
||||||
}
|
|
||||||
|
|
||||||
eventEmitter.emit(XMPPEvents.USER_ID_CHANGED, from, id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,341 +0,0 @@
|
||||||
/* jshint -W117 */
|
|
||||||
/* jshint -W101 */
|
|
||||||
|
|
||||||
var JingleSession = require("./JingleSessionPC");
|
|
||||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
|
||||||
var RTCBrowserType = require("../RTC/RTCBrowserType");
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = function(XMPP, eventEmitter) {
|
|
||||||
Strophe.addConnectionPlugin('jingle', {
|
|
||||||
connection: null,
|
|
||||||
sessions: {},
|
|
||||||
jid2session: {},
|
|
||||||
ice_config: {iceServers: []},
|
|
||||||
pc_constraints: {},
|
|
||||||
activecall: null,
|
|
||||||
media_constraints: {
|
|
||||||
mandatory: {
|
|
||||||
'OfferToReceiveAudio': true,
|
|
||||||
'OfferToReceiveVideo': true
|
|
||||||
}
|
|
||||||
// MozDontOfferDataChannel: true when this is firefox
|
|
||||||
},
|
|
||||||
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:dtls:0');
|
|
||||||
this.connection.disco.addFeature('urn:xmpp:jingle:transports:dtls-sctp:1');
|
|
||||||
this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:audio');
|
|
||||||
this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:video');
|
|
||||||
|
|
||||||
if (RTCBrowserType.isChrome() || RTCBrowserType.isOpera() ||
|
|
||||||
RTCBrowserType.isTemasysPluginUsed()) {
|
|
||||||
this.connection.disco.addFeature('urn:ietf:rfc:4588');
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is dealt with by SDP O/A so we don't need to announce 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');
|
|
||||||
var fromJid = iq.getAttribute('from');
|
|
||||||
// send ack first
|
|
||||||
var ack = $iq({type: 'result',
|
|
||||||
to: fromJid,
|
|
||||||
id: iq.getAttribute('id')
|
|
||||||
});
|
|
||||||
console.log('on jingle ' + action + ' from ' + fromJid, iq);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
// local jid is not checked
|
|
||||||
if (fromJid != sess.peerjid) {
|
|
||||||
console.warn('jid mismatch for session id', sid, fromJid, 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':
|
|
||||||
console.log("(TIME) received session-initiate:\t",
|
|
||||||
window.performance.now(), iq);
|
|
||||||
var startMuted = $(iq).find('jingle>startmuted');
|
|
||||||
if (startMuted && startMuted.length > 0) {
|
|
||||||
var audioMuted = startMuted.attr("audio");
|
|
||||||
var videoMuted = startMuted.attr("video");
|
|
||||||
eventEmitter.emit(XMPPEvents.START_MUTED_FROM_FOCUS,
|
|
||||||
audioMuted === "true", videoMuted === "true");
|
|
||||||
}
|
|
||||||
sess = new JingleSession(
|
|
||||||
$(iq).attr('to'), $(iq).find('jingle').attr('sid'),
|
|
||||||
this.connection, XMPP, eventEmitter);
|
|
||||||
// configure session
|
|
||||||
|
|
||||||
sess.media_constraints = this.media_constraints;
|
|
||||||
sess.pc_constraints = this.pc_constraints;
|
|
||||||
sess.ice_config = this.ice_config;
|
|
||||||
|
|
||||||
sess.initialize(fromJid, false);
|
|
||||||
// FIXME: setRemoteDescription should only be done when this call is to be accepted
|
|
||||||
sess.setOffer($(iq).find('>jingle'));
|
|
||||||
|
|
||||||
this.sessions[sess.sid] = sess;
|
|
||||||
this.jid2session[sess.peerjid] = sess;
|
|
||||||
|
|
||||||
// the callback should either
|
|
||||||
// .sendAnswer and .accept
|
|
||||||
// or .sendTerminate -- not necessarily synchronous
|
|
||||||
|
|
||||||
// TODO: do we check activecall == null?
|
|
||||||
this.connection.jingle.activecall = sess;
|
|
||||||
|
|
||||||
eventEmitter.emit(XMPPEvents.CALL_INCOMING, sess);
|
|
||||||
|
|
||||||
// TODO: check affiliation and/or role
|
|
||||||
console.log('emuc data for', sess.peerjid,
|
|
||||||
this.connection.emuc.members[sess.peerjid]);
|
|
||||||
sess.sendAnswer();
|
|
||||||
sess.accept();
|
|
||||||
break;
|
|
||||||
case 'session-accept':
|
|
||||||
sess.setAnswer($(iq).find('>jingle'));
|
|
||||||
sess.accept();
|
|
||||||
$(document).trigger('callaccepted.jingle', [sess.sid]);
|
|
||||||
break;
|
|
||||||
case 'session-terminate':
|
|
||||||
if (!sess) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
console.log('terminating...', sess.sid);
|
|
||||||
sess.terminate();
|
|
||||||
this.terminate(sess.sid);
|
|
||||||
if ($(iq).find('>jingle>reason').length) {
|
|
||||||
$(document).trigger('callterminated.jingle', [
|
|
||||||
sess.sid,
|
|
||||||
sess.peerjid,
|
|
||||||
$(iq).find('>jingle>reason>:first')[0].tagName,
|
|
||||||
$(iq).find('>jingle>reason>text').text()
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
$(document).trigger('callterminated.jingle',
|
|
||||||
[sess.sid, sess.peerjid]);
|
|
||||||
}
|
|
||||||
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, un-jingleish
|
|
||||||
case 'source-add': // FIXME: proprietary
|
|
||||||
console.info("source-add", iq);
|
|
||||||
sess.addSource($(iq).find('>jingle>content'));
|
|
||||||
break;
|
|
||||||
case 'removesource': // FIXME: proprietary, un-jingleish
|
|
||||||
case 'source-remove': // FIXME: proprietary
|
|
||||||
console.info("source-remove", iq);
|
|
||||||
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, XMPP, eventEmitter);
|
|
||||||
// configure session
|
|
||||||
|
|
||||||
sess.media_constraints = this.media_constraints;
|
|
||||||
sess.pc_constraints = this.pc_constraints;
|
|
||||||
sess.ice_config = this.ice_config;
|
|
||||||
|
|
||||||
sess.initialize(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];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Used to terminate a session when an unavailable presence is received.
|
|
||||||
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, jid], 'gone');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
terminateRemoteByJid: function (jid, reason) {
|
|
||||||
if (this.jid2session.hasOwnProperty(jid)) {
|
|
||||||
var sess = this.jid2session[jid];
|
|
||||||
if (sess) {
|
|
||||||
sess.sendTerminate(reason || (!sess.active()) ? 'kick' : null);
|
|
||||||
sess.terminate();
|
|
||||||
console.log('terminate peer with jid', sess.sid, jid);
|
|
||||||
delete this.sessions[sess.sid];
|
|
||||||
delete this.jid2session[jid];
|
|
||||||
$(document).trigger('callterminated.jingle',
|
|
||||||
[sess.sid, jid, 'kicked']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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 = {};
|
|
||||||
var type = el.attr('type');
|
|
||||||
switch (type) {
|
|
||||||
case 'stun':
|
|
||||||
dict.url = 'stun:' + el.attr('host');
|
|
||||||
if (el.attr('port')) {
|
|
||||||
dict.url += ':' + el.attr('port');
|
|
||||||
}
|
|
||||||
iceservers.push(dict);
|
|
||||||
break;
|
|
||||||
case 'turn':
|
|
||||||
case 'turns':
|
|
||||||
dict.url = type + ':';
|
|
||||||
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?
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the data saved in 'updateLog' in a format to be logged.
|
|
||||||
*/
|
|
||||||
getLog: function () {
|
|
||||||
var data = {};
|
|
||||||
var self = this;
|
|
||||||
Object.keys(this.sessions).forEach(function (sid) {
|
|
||||||
var session = self.sessions[sid];
|
|
||||||
if (session.peerconnection && session.peerconnection.updateLog) {
|
|
||||||
// FIXME: should probably be a .dump call
|
|
||||||
data["jingle_" + session.sid] = {
|
|
||||||
updateLog: session.peerconnection.updateLog,
|
|
||||||
stats: session.peerconnection.stats,
|
|
||||||
url: window.location.href
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
/* global Strophe */
|
|
||||||
module.exports = function () {
|
|
||||||
|
|
||||||
Strophe.addConnectionPlugin('logger', {
|
|
||||||
// logs raw stanzas and makes them available for download as JSON
|
|
||||||
connection: null,
|
|
||||||
log: [],
|
|
||||||
init: function (conn) {
|
|
||||||
this.connection = conn;
|
|
||||||
this.connection.rawInput = this.log_incoming.bind(this);
|
|
||||||
this.connection.rawOutput = this.log_outgoing.bind(this);
|
|
||||||
},
|
|
||||||
log_incoming: function (stanza) {
|
|
||||||
this.log.push([new Date().getTime(), 'incoming', stanza]);
|
|
||||||
},
|
|
||||||
log_outgoing: function (stanza) {
|
|
||||||
this.log.push([new Date().getTime(), 'outgoing', stanza]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1,60 +0,0 @@
|
||||||
/* global $, $iq, config, connection, focusMucJid, forceMuted, Strophe */
|
|
||||||
/**
|
|
||||||
* Moderate connection plugin.
|
|
||||||
*/
|
|
||||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
|
||||||
|
|
||||||
module.exports = function (XMPP, eventEmitter) {
|
|
||||||
Strophe.addConnectionPlugin('moderate', {
|
|
||||||
connection: null,
|
|
||||||
init: function (conn) {
|
|
||||||
this.connection = conn;
|
|
||||||
|
|
||||||
this.connection.addHandler(this.onMute.bind(this),
|
|
||||||
'http://jitsi.org/jitmeet/audio',
|
|
||||||
'iq',
|
|
||||||
'set',
|
|
||||||
null,
|
|
||||||
null);
|
|
||||||
},
|
|
||||||
setMute: function (jid, mute) {
|
|
||||||
console.info("set mute", mute);
|
|
||||||
var iqToFocus =
|
|
||||||
$iq({to: this.connection.emuc.focusMucJid, type: 'set'})
|
|
||||||
.c('mute', {
|
|
||||||
xmlns: 'http://jitsi.org/jitmeet/audio',
|
|
||||||
jid: jid
|
|
||||||
})
|
|
||||||
.t(mute.toString())
|
|
||||||
.up();
|
|
||||||
|
|
||||||
this.connection.sendIQ(
|
|
||||||
iqToFocus,
|
|
||||||
function (result) {
|
|
||||||
console.log('set mute', result);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.log('set mute error', error);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onMute: function (iq) {
|
|
||||||
var from = iq.getAttribute('from');
|
|
||||||
if (from !== this.connection.emuc.focusMucJid) {
|
|
||||||
console.warn("Ignored mute from non focus peer");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var mute = $(iq).find('mute');
|
|
||||||
if (mute.length) {
|
|
||||||
var doMuteAudio = mute.text() === "true";
|
|
||||||
eventEmitter.emit(XMPPEvents.AUDIO_MUTED_BY_FOCUS, doMuteAudio);
|
|
||||||
XMPP.forceMuted = doMuteAudio;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
eject: function (jid) {
|
|
||||||
// We're not the focus, so can't terminate
|
|
||||||
//connection.jingle.terminateRemoteByJid(jid, 'kick');
|
|
||||||
this.connection.emuc.kick(jid);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1,121 +0,0 @@
|
||||||
/* global $, $iq, Strophe */
|
|
||||||
|
|
||||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ping every 20 sec
|
|
||||||
*/
|
|
||||||
var PING_INTERVAL = 20000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ping timeout error after 15 sec of waiting.
|
|
||||||
*/
|
|
||||||
var PING_TIMEOUT = 15000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will close the connection after 3 consecutive ping errors.
|
|
||||||
*/
|
|
||||||
var PING_THRESHOLD = 3;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* XEP-0199 ping plugin.
|
|
||||||
*
|
|
||||||
* Registers "urn:xmpp:ping" namespace under Strophe.NS.PING.
|
|
||||||
*/
|
|
||||||
module.exports = function (XMPP, eventEmitter) {
|
|
||||||
Strophe.addConnectionPlugin('ping', {
|
|
||||||
|
|
||||||
connection: null,
|
|
||||||
|
|
||||||
failedPings: 0,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the plugin. Method called by Strophe.
|
|
||||||
* @param connection Strophe connection instance.
|
|
||||||
*/
|
|
||||||
init: function (connection) {
|
|
||||||
this.connection = connection;
|
|
||||||
Strophe.addNamespace('PING', "urn:xmpp:ping");
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends "ping" to given <tt>jid</tt>
|
|
||||||
* @param jid the JID to which ping request will be sent.
|
|
||||||
* @param success callback called on success.
|
|
||||||
* @param error callback called on error.
|
|
||||||
* @param timeout ms how long are we going to wait for the response. On
|
|
||||||
* timeout <tt>error<//t> callback is called with undefined error
|
|
||||||
* argument.
|
|
||||||
*/
|
|
||||||
ping: function (jid, success, error, timeout) {
|
|
||||||
var iq = $iq({type: 'get', to: jid});
|
|
||||||
iq.c('ping', {xmlns: Strophe.NS.PING});
|
|
||||||
this.connection.sendIQ(iq, success, error, timeout);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if given <tt>jid</tt> has XEP-0199 ping support.
|
|
||||||
* @param jid the JID to be checked for ping support.
|
|
||||||
* @param callback function with boolean argument which will be
|
|
||||||
* <tt>true</tt> if XEP-0199 ping is supported by given <tt>jid</tt>
|
|
||||||
*/
|
|
||||||
hasPingSupport: function (jid, callback) {
|
|
||||||
this.connection.disco.info(
|
|
||||||
jid, null,
|
|
||||||
function (result) {
|
|
||||||
var ping = $(result).find('>>feature[var="urn:xmpp:ping"]');
|
|
||||||
callback(ping.length > 0);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.error("Ping feature discovery error", error);
|
|
||||||
callback(false);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts to send ping in given interval to specified remote JID.
|
|
||||||
* This plugin supports only one such task and <tt>stopInterval</tt>
|
|
||||||
* must be called before starting a new one.
|
|
||||||
* @param remoteJid remote JID to which ping requests will be sent to.
|
|
||||||
* @param interval task interval in ms.
|
|
||||||
*/
|
|
||||||
startInterval: function (remoteJid, interval) {
|
|
||||||
if (this.intervalId) {
|
|
||||||
console.error("Ping task scheduled already");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!interval)
|
|
||||||
interval = PING_INTERVAL;
|
|
||||||
var self = this;
|
|
||||||
this.intervalId = window.setInterval(function () {
|
|
||||||
self.ping(remoteJid,
|
|
||||||
function (result) {
|
|
||||||
// Ping OK
|
|
||||||
self.failedPings = 0;
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
self.failedPings += 1;
|
|
||||||
console.error(
|
|
||||||
"Ping " + (error ? "error" : "timeout"), error);
|
|
||||||
if (self.failedPings >= PING_THRESHOLD) {
|
|
||||||
self.connection.disconnect();
|
|
||||||
}
|
|
||||||
}, PING_TIMEOUT);
|
|
||||||
}, interval);
|
|
||||||
console.info("XMPP pings will be sent every " + interval + " ms");
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops current "ping" interval task.
|
|
||||||
*/
|
|
||||||
stopInterval: function () {
|
|
||||||
if (this.intervalId) {
|
|
||||||
window.clearInterval(this.intervalId);
|
|
||||||
this.intervalId = null;
|
|
||||||
this.failedPings = 0;
|
|
||||||
console.info("Ping interval cleared");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1,96 +0,0 @@
|
||||||
/* jshint -W117 */
|
|
||||||
module.exports = function() {
|
|
||||||
Strophe.addConnectionPlugin('rayo',
|
|
||||||
{
|
|
||||||
RAYO_XMLNS: 'urn:xmpp:rayo:1',
|
|
||||||
connection: null,
|
|
||||||
init: function (conn) {
|
|
||||||
this.connection = conn;
|
|
||||||
if (this.connection.disco) {
|
|
||||||
this.connection.disco.addFeature('urn:xmpp:rayo:client:1');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.connection.addHandler(
|
|
||||||
this.onRayo.bind(this), this.RAYO_XMLNS, 'iq', 'set',
|
|
||||||
null, null);
|
|
||||||
},
|
|
||||||
onRayo: function (iq) {
|
|
||||||
console.info("Rayo IQ", iq);
|
|
||||||
},
|
|
||||||
dial: function (to, from, roomName, roomPass) {
|
|
||||||
var self = this;
|
|
||||||
var req = $iq(
|
|
||||||
{
|
|
||||||
type: 'set',
|
|
||||||
to: this.connection.emuc.focusMucJid
|
|
||||||
}
|
|
||||||
);
|
|
||||||
req.c('dial',
|
|
||||||
{
|
|
||||||
xmlns: this.RAYO_XMLNS,
|
|
||||||
to: to,
|
|
||||||
from: from
|
|
||||||
});
|
|
||||||
req.c('header',
|
|
||||||
{
|
|
||||||
name: 'JvbRoomName',
|
|
||||||
value: roomName
|
|
||||||
}).up();
|
|
||||||
|
|
||||||
if (roomPass !== null && roomPass.length) {
|
|
||||||
|
|
||||||
req.c('header',
|
|
||||||
{
|
|
||||||
name: 'JvbRoomPassword',
|
|
||||||
value: roomPass
|
|
||||||
}).up();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.connection.sendIQ(
|
|
||||||
req,
|
|
||||||
function (result) {
|
|
||||||
console.info('Dial result ', result);
|
|
||||||
|
|
||||||
var resource = $(result).find('ref').attr('uri');
|
|
||||||
self.call_resource = resource.substr('xmpp:'.length);
|
|
||||||
console.info(
|
|
||||||
"Received call resource: " + self.call_resource);
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.info('Dial error ', error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
hang_up: function () {
|
|
||||||
if (!this.call_resource) {
|
|
||||||
console.warn("No call in progress");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
var req = $iq(
|
|
||||||
{
|
|
||||||
type: 'set',
|
|
||||||
to: this.call_resource
|
|
||||||
}
|
|
||||||
);
|
|
||||||
req.c('hangup',
|
|
||||||
{
|
|
||||||
xmlns: this.RAYO_XMLNS
|
|
||||||
});
|
|
||||||
|
|
||||||
this.connection.sendIQ(
|
|
||||||
req,
|
|
||||||
function (result) {
|
|
||||||
console.info('Hangup result ', result);
|
|
||||||
self.call_resource = null;
|
|
||||||
},
|
|
||||||
function (error) {
|
|
||||||
console.info('Hangup error ', error);
|
|
||||||
self.call_resource = null;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,43 +0,0 @@
|
||||||
/* global Strophe */
|
|
||||||
/**
|
|
||||||
* Strophe logger implementation. Logs from level WARN and above.
|
|
||||||
*/
|
|
||||||
module.exports = function () {
|
|
||||||
|
|
||||||
Strophe.log = function (level, msg) {
|
|
||||||
switch (level) {
|
|
||||||
case Strophe.LogLevel.WARN:
|
|
||||||
console.warn("Strophe: " + msg);
|
|
||||||
break;
|
|
||||||
case Strophe.LogLevel.ERROR:
|
|
||||||
case Strophe.LogLevel.FATAL:
|
|
||||||
console.error("Strophe: " + msg);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Strophe.getStatusString = function (status) {
|
|
||||||
switch (status) {
|
|
||||||
case Strophe.Status.ERROR:
|
|
||||||
return "ERROR";
|
|
||||||
case Strophe.Status.CONNECTING:
|
|
||||||
return "CONNECTING";
|
|
||||||
case Strophe.Status.CONNFAIL:
|
|
||||||
return "CONNFAIL";
|
|
||||||
case Strophe.Status.AUTHENTICATING:
|
|
||||||
return "AUTHENTICATING";
|
|
||||||
case Strophe.Status.AUTHFAIL:
|
|
||||||
return "AUTHFAIL";
|
|
||||||
case Strophe.Status.CONNECTED:
|
|
||||||
return "CONNECTED";
|
|
||||||
case Strophe.Status.DISCONNECTED:
|
|
||||||
return "DISCONNECTED";
|
|
||||||
case Strophe.Status.DISCONNECTING:
|
|
||||||
return "DISCONNECTING";
|
|
||||||
case Strophe.Status.ATTACHED:
|
|
||||||
return "ATTACHED";
|
|
||||||
default:
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,616 +0,0 @@
|
||||||
/* global $, APP, config, Strophe, Base64, $msg */
|
|
||||||
/* jshint -W101 */
|
|
||||||
var Moderator = require("./moderator");
|
|
||||||
var EventEmitter = require("events");
|
|
||||||
var Recording = require("./recording");
|
|
||||||
var SDP = require("./SDP");
|
|
||||||
var SDPUtil = require("./SDPUtil");
|
|
||||||
var Settings = require("../settings/Settings");
|
|
||||||
var Pako = require("pako");
|
|
||||||
var StreamEventTypes = require("../../service/RTC/StreamEventTypes");
|
|
||||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
|
||||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
|
||||||
var retry = require('retry');
|
|
||||||
var RandomUtil = require("../util/RandomUtil");
|
|
||||||
|
|
||||||
var eventEmitter = new EventEmitter();
|
|
||||||
var connection = null;
|
|
||||||
var authenticatedUser = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method that generates user name based on random hex values.
|
|
||||||
* Eg. 12345678-1234-1234-12345678
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function generateUserName() {
|
|
||||||
return RandomUtil.randomHexString(8) + "-" + RandomUtil.randomHexString(4)
|
|
||||||
+ "-" + RandomUtil.randomHexString(4) + "-"
|
|
||||||
+ RandomUtil.randomHexString(8);
|
|
||||||
}
|
|
||||||
|
|
||||||
function connect(jid, password) {
|
|
||||||
|
|
||||||
var faultTolerantConnect = retry.operation({
|
|
||||||
retries: 3
|
|
||||||
});
|
|
||||||
|
|
||||||
// fault tolerant connect
|
|
||||||
faultTolerantConnect.attempt(function () {
|
|
||||||
|
|
||||||
connection = XMPP.createConnection();
|
|
||||||
Moderator.setConnection(connection);
|
|
||||||
|
|
||||||
connection.jingle.pc_constraints = APP.RTC.getPCConstraints();
|
|
||||||
if (config.useIPv6) {
|
|
||||||
// https://code.google.com/p/webrtc/issues/detail?id=2828
|
|
||||||
if (!connection.jingle.pc_constraints.optional)
|
|
||||||
connection.jingle.pc_constraints.optional = [];
|
|
||||||
connection.jingle.pc_constraints.optional.push({googIPv6: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include user info in MUC presence
|
|
||||||
var settings = Settings.getSettings();
|
|
||||||
if (settings.email) {
|
|
||||||
connection.emuc.addEmailToPresence(settings.email);
|
|
||||||
}
|
|
||||||
if (settings.uid) {
|
|
||||||
connection.emuc.addUserIdToPresence(settings.uid);
|
|
||||||
}
|
|
||||||
if (settings.displayName) {
|
|
||||||
connection.emuc.addDisplayNameToPresence(settings.displayName);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// connection.connect() starts the connection process.
|
|
||||||
//
|
|
||||||
// As the connection process proceeds, the user supplied callback will
|
|
||||||
// be triggered multiple times with status updates. The callback should
|
|
||||||
// take two arguments - the status code and the error condition.
|
|
||||||
//
|
|
||||||
// The status code will be one of the values in the Strophe.Status
|
|
||||||
// constants. The error condition will be one of the conditions defined
|
|
||||||
// in RFC 3920 or the condition ‘strophe-parsererror’.
|
|
||||||
//
|
|
||||||
// The Parameters wait, hold and route are optional and only relevant
|
|
||||||
// for BOSH connections. Please see XEP 124 for a more detailed
|
|
||||||
// explanation of the optional parameters.
|
|
||||||
//
|
|
||||||
// Connection status constants for use by the connection handler
|
|
||||||
// callback.
|
|
||||||
//
|
|
||||||
// Status.ERROR - An error has occurred (websockets specific)
|
|
||||||
// Status.CONNECTING - The connection is currently being made
|
|
||||||
// Status.CONNFAIL - The connection attempt failed
|
|
||||||
// Status.AUTHENTICATING - The connection is authenticating
|
|
||||||
// Status.AUTHFAIL - The authentication attempt failed
|
|
||||||
// Status.CONNECTED - The connection has succeeded
|
|
||||||
// Status.DISCONNECTED - The connection has been terminated
|
|
||||||
// Status.DISCONNECTING - The connection is currently being terminated
|
|
||||||
// Status.ATTACHED - The connection has been attached
|
|
||||||
|
|
||||||
var anonymousConnectionFailed = false;
|
|
||||||
var connectionFailed = false;
|
|
||||||
var lastErrorMsg;
|
|
||||||
connection.connect(jid, password, function (status, msg) {
|
|
||||||
console.log("(TIME) Strophe " + Strophe.getStatusString(status) +
|
|
||||||
(msg ? "[" + msg + "]" : "") +
|
|
||||||
"\t:" + window.performance.now());
|
|
||||||
if (status === Strophe.Status.CONNECTED) {
|
|
||||||
if (config.useStunTurn) {
|
|
||||||
connection.jingle.getStunAndTurnCredentials();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.info("My Jabber ID: " + connection.jid);
|
|
||||||
|
|
||||||
// Schedule ping ?
|
|
||||||
var pingJid = connection.domain;
|
|
||||||
connection.ping.hasPingSupport(
|
|
||||||
pingJid,
|
|
||||||
function (hasPing) {
|
|
||||||
if (hasPing)
|
|
||||||
connection.ping.startInterval(pingJid);
|
|
||||||
else
|
|
||||||
console.warn("Ping NOT supported by " + pingJid);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (password)
|
|
||||||
authenticatedUser = true;
|
|
||||||
maybeDoJoin();
|
|
||||||
} else if (status === Strophe.Status.CONNFAIL) {
|
|
||||||
if (msg === 'x-strophe-bad-non-anon-jid') {
|
|
||||||
anonymousConnectionFailed = true;
|
|
||||||
} else {
|
|
||||||
connectionFailed = true;
|
|
||||||
}
|
|
||||||
lastErrorMsg = msg;
|
|
||||||
} else if (status === Strophe.Status.DISCONNECTED) {
|
|
||||||
// Stop ping interval
|
|
||||||
connection.ping.stopInterval();
|
|
||||||
if (anonymousConnectionFailed) {
|
|
||||||
// prompt user for username and password
|
|
||||||
XMPP.promptLogin();
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Strophe already has built-in HTTP/BOSH error handling and
|
|
||||||
// request retry logic. Requests are resent automatically
|
|
||||||
// until their error count reaches 5. Strophe.js disconnects
|
|
||||||
// if the error count is > 5. We are not replicating this
|
|
||||||
// here.
|
|
||||||
//
|
|
||||||
// The "problem" is that failed HTTP/BOSH requests don't
|
|
||||||
// trigger a callback with a status update, so when a
|
|
||||||
// callback with status Strophe.Status.DISCONNECTED arrives,
|
|
||||||
// we can't be sure if it's a graceful disconnect or if it's
|
|
||||||
// triggered by some HTTP/BOSH error.
|
|
||||||
//
|
|
||||||
// But that's a minor issue in Jitsi Meet as we never
|
|
||||||
// disconnect anyway, not even when the user closes the
|
|
||||||
// browser window (which is kind of wrong, but the point is
|
|
||||||
// that we should never ever get disconnected).
|
|
||||||
//
|
|
||||||
// On the other hand, failed connections due to XMPP layer
|
|
||||||
// errors, trigger a callback with status Strophe.Status.CONNFAIL.
|
|
||||||
//
|
|
||||||
// Here we implement retry logic for failed connections due
|
|
||||||
// to XMPP layer errors and we display an error to the user
|
|
||||||
// if we get disconnected from the XMPP server permanently.
|
|
||||||
|
|
||||||
// If the connection failed, retry.
|
|
||||||
if (connectionFailed &&
|
|
||||||
faultTolerantConnect.retry("connection-failed")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we failed to connect to the XMPP server, fire an event
|
|
||||||
// to let all the interested module now about it.
|
|
||||||
eventEmitter.emit(XMPPEvents.CONNECTION_FAILED,
|
|
||||||
msg ? msg : lastErrorMsg);
|
|
||||||
}
|
|
||||||
} else if (status === Strophe.Status.AUTHFAIL) {
|
|
||||||
// wrong password or username, prompt user
|
|
||||||
XMPP.promptLogin();
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function maybeDoJoin() {
|
|
||||||
if (connection && connection.connected &&
|
|
||||||
Strophe.getResourceFromJid(connection.jid) &&
|
|
||||||
(APP.RTC.localAudio || APP.RTC.localVideo)) {
|
|
||||||
// .connected is true while connecting?
|
|
||||||
doJoin();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function doJoin() {
|
|
||||||
eventEmitter.emit(XMPPEvents.READY_TO_JOIN);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initStrophePlugins()
|
|
||||||
{
|
|
||||||
require("./strophe.emuc")(XMPP, eventEmitter);
|
|
||||||
require("./strophe.jingle")(XMPP, eventEmitter);
|
|
||||||
require("./strophe.moderate")(XMPP, eventEmitter);
|
|
||||||
require("./strophe.util")();
|
|
||||||
require("./strophe.ping")(XMPP, eventEmitter);
|
|
||||||
require("./strophe.rayo")();
|
|
||||||
require("./strophe.logger")();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If given <tt>localStream</tt> is video one this method will advertise it's
|
|
||||||
* video type in MUC presence.
|
|
||||||
* @param localStream new or modified <tt>LocalStream</tt>.
|
|
||||||
*/
|
|
||||||
function broadcastLocalVideoType(localStream) {
|
|
||||||
if (localStream.videoType)
|
|
||||||
XMPP.addToPresence('videoType', localStream.videoType);
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerListeners() {
|
|
||||||
APP.RTC.addStreamListener(
|
|
||||||
function (localStream) {
|
|
||||||
maybeDoJoin();
|
|
||||||
broadcastLocalVideoType(localStream);
|
|
||||||
},
|
|
||||||
StreamEventTypes.EVENT_TYPE_LOCAL_CREATED
|
|
||||||
);
|
|
||||||
APP.RTC.addStreamListener(
|
|
||||||
broadcastLocalVideoType,
|
|
||||||
StreamEventTypes.EVENT_TYPE_LOCAL_CHANGED
|
|
||||||
);
|
|
||||||
APP.RTC.addListener(RTCEvents.AVAILABLE_DEVICES_CHANGED, function (devices) {
|
|
||||||
XMPP.addToPresence("devices", devices);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var unload = (function () {
|
|
||||||
var unloaded = false;
|
|
||||||
|
|
||||||
return function () {
|
|
||||||
if (unloaded) { return; }
|
|
||||||
unloaded = true;
|
|
||||||
|
|
||||||
if (connection && connection.connected) {
|
|
||||||
// ensure signout
|
|
||||||
$.ajax({
|
|
||||||
type: 'POST',
|
|
||||||
url: config.bosh,
|
|
||||||
async: false,
|
|
||||||
cache: false,
|
|
||||||
contentType: 'application/xml',
|
|
||||||
data: "<body rid='" +
|
|
||||||
(connection.rid || connection._proto.rid) +
|
|
||||||
"' xmlns='http://jabber.org/protocol/httpbind' sid='" +
|
|
||||||
(connection.sid || connection._proto.sid) +
|
|
||||||
"' type='terminate'>" +
|
|
||||||
"<presence xmlns='jabber:client' type='unavailable'/>" +
|
|
||||||
"</body>",
|
|
||||||
success: function (data) {
|
|
||||||
console.log('signed out');
|
|
||||||
console.log(data);
|
|
||||||
},
|
|
||||||
error: function (XMLHttpRequest, textStatus, errorThrown) {
|
|
||||||
console.log('signout error',
|
|
||||||
textStatus + ' (' + errorThrown + ')');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
XMPP.disposeConference(true);
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
function setupEvents() {
|
|
||||||
// In recent versions of FF the 'beforeunload' event is not fired when the
|
|
||||||
// window or the tab is closed. It is only fired when we leave the page
|
|
||||||
// (change URL). If this participant doesn't unload properly, then it
|
|
||||||
// becomes a ghost for the rest of the participants that stay in the
|
|
||||||
// conference. Thankfully handling the 'unload' event in addition to the
|
|
||||||
// 'beforeunload' event seems to guarantee the execution of the 'unload'
|
|
||||||
// method at least once.
|
|
||||||
//
|
|
||||||
// The 'unload' method can safely be run multiple times, it will actually do
|
|
||||||
// something only the first time that it's run, so we're don't have to worry
|
|
||||||
// about browsers that fire both events.
|
|
||||||
|
|
||||||
$(window).bind('beforeunload', unload);
|
|
||||||
$(window).bind('unload', unload);
|
|
||||||
}
|
|
||||||
|
|
||||||
var XMPP = {
|
|
||||||
getConnection: function(){ return connection; },
|
|
||||||
sessionTerminated: false,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* XMPP connection status
|
|
||||||
*/
|
|
||||||
Status: Strophe.Status,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remembers if we were muted by the focus.
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
forceMuted: false,
|
|
||||||
start: function () {
|
|
||||||
setupEvents();
|
|
||||||
initStrophePlugins();
|
|
||||||
registerListeners();
|
|
||||||
Moderator.init(this, eventEmitter);
|
|
||||||
Recording.init();
|
|
||||||
var configDomain = config.hosts.anonymousdomain || config.hosts.domain;
|
|
||||||
// Force authenticated domain if room is appended with '?login=true'
|
|
||||||
if (config.hosts.anonymousdomain &&
|
|
||||||
window.location.search.indexOf("login=true") !== -1) {
|
|
||||||
configDomain = config.hosts.domain;
|
|
||||||
}
|
|
||||||
var jid = configDomain || window.location.hostname;
|
|
||||||
var password = null;
|
|
||||||
if (config.token) {
|
|
||||||
password = config.token;
|
|
||||||
if (config.id) {
|
|
||||||
jid = config.id + "@" + jid;
|
|
||||||
} else {
|
|
||||||
jid = generateUserName() + "@" + jid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
connect(jid, password);
|
|
||||||
},
|
|
||||||
createConnection: function () {
|
|
||||||
var bosh = config.bosh || '/http-bind';
|
|
||||||
// adds the room name used to the bosh connection
|
|
||||||
return new Strophe.Connection(bosh + '?room=' + APP.UI.getRoomNode());
|
|
||||||
},
|
|
||||||
getStatusString: function (status) {
|
|
||||||
return Strophe.getStatusString(status);
|
|
||||||
},
|
|
||||||
promptLogin: function () {
|
|
||||||
eventEmitter.emit(XMPPEvents.PROMPT_FOR_LOGIN, connect);
|
|
||||||
},
|
|
||||||
joinRoom: function(roomName, useNicks, nick) {
|
|
||||||
var roomjid = roomName;
|
|
||||||
|
|
||||||
if (useNicks) {
|
|
||||||
if (nick) {
|
|
||||||
roomjid += '/' + nick;
|
|
||||||
} else {
|
|
||||||
roomjid += '/' + Strophe.getNodeFromJid(connection.jid);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var tmpJid = Strophe.getNodeFromJid(connection.jid);
|
|
||||||
|
|
||||||
if(!authenticatedUser)
|
|
||||||
tmpJid = tmpJid.substr(0, 8);
|
|
||||||
|
|
||||||
roomjid += '/' + tmpJid;
|
|
||||||
}
|
|
||||||
connection.emuc.doJoin(roomjid);
|
|
||||||
},
|
|
||||||
myJid: function () {
|
|
||||||
if(!connection)
|
|
||||||
return null;
|
|
||||||
return connection.emuc.myroomjid;
|
|
||||||
},
|
|
||||||
myResource: function () {
|
|
||||||
if(!connection || ! connection.emuc.myroomjid)
|
|
||||||
return null;
|
|
||||||
return Strophe.getResourceFromJid(connection.emuc.myroomjid);
|
|
||||||
},
|
|
||||||
getLastPresence: function (from) {
|
|
||||||
if(!connection)
|
|
||||||
return null;
|
|
||||||
return connection.emuc.lastPresenceMap[from];
|
|
||||||
},
|
|
||||||
disposeConference: function (onUnload) {
|
|
||||||
var handler = connection.jingle.activecall;
|
|
||||||
if (handler && handler.peerconnection) {
|
|
||||||
// FIXME: probably removing streams is not required and close() should
|
|
||||||
// be enough
|
|
||||||
if (APP.RTC.localAudio) {
|
|
||||||
handler.peerconnection.removeStream(
|
|
||||||
APP.RTC.localAudio.getOriginalStream(), onUnload);
|
|
||||||
}
|
|
||||||
if (APP.RTC.localVideo) {
|
|
||||||
handler.peerconnection.removeStream(
|
|
||||||
APP.RTC.localVideo.getOriginalStream(), onUnload);
|
|
||||||
}
|
|
||||||
handler.peerconnection.close();
|
|
||||||
}
|
|
||||||
eventEmitter.emit(XMPPEvents.DISPOSE_CONFERENCE, onUnload);
|
|
||||||
connection.jingle.activecall = null;
|
|
||||||
if (!onUnload) {
|
|
||||||
this.sessionTerminated = true;
|
|
||||||
connection.emuc.doLeave();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addListener: function(type, listener) {
|
|
||||||
eventEmitter.on(type, listener);
|
|
||||||
},
|
|
||||||
removeListener: function (type, listener) {
|
|
||||||
eventEmitter.removeListener(type, listener);
|
|
||||||
},
|
|
||||||
allocateConferenceFocus: function(roomName, callback) {
|
|
||||||
Moderator.allocateConferenceFocus(roomName, callback);
|
|
||||||
},
|
|
||||||
getLoginUrl: function (roomName, callback) {
|
|
||||||
Moderator.getLoginUrl(roomName, callback);
|
|
||||||
},
|
|
||||||
getPopupLoginUrl: function (roomName, callback) {
|
|
||||||
Moderator.getPopupLoginUrl(roomName, callback);
|
|
||||||
},
|
|
||||||
isModerator: function () {
|
|
||||||
return Moderator.isModerator();
|
|
||||||
},
|
|
||||||
isSipGatewayEnabled: function () {
|
|
||||||
return Moderator.isSipGatewayEnabled();
|
|
||||||
},
|
|
||||||
isExternalAuthEnabled: function () {
|
|
||||||
return Moderator.isExternalAuthEnabled();
|
|
||||||
},
|
|
||||||
isConferenceInProgress: function () {
|
|
||||||
return connection && connection.jingle.activecall &&
|
|
||||||
connection.jingle.activecall.peerconnection;
|
|
||||||
},
|
|
||||||
switchStreams: function (stream, oldStream, callback, isAudio) {
|
|
||||||
if (this.isConferenceInProgress()) {
|
|
||||||
// FIXME: will block switchInProgress on true value in case of exception
|
|
||||||
connection.jingle.activecall.switchStreams(stream, oldStream, callback, isAudio);
|
|
||||||
} else {
|
|
||||||
// We are done immediately
|
|
||||||
console.warn("No conference handler or conference not started yet");
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sendVideoInfoPresence: function (mute) {
|
|
||||||
if(!connection)
|
|
||||||
return;
|
|
||||||
connection.emuc.addVideoInfoToPresence(mute);
|
|
||||||
connection.emuc.sendPresence();
|
|
||||||
},
|
|
||||||
setVideoMute: function (mute, callback, options) {
|
|
||||||
if(!connection)
|
|
||||||
return;
|
|
||||||
var self = this;
|
|
||||||
var localCallback = function (mute) {
|
|
||||||
self.sendVideoInfoPresence(mute);
|
|
||||||
return callback(mute);
|
|
||||||
};
|
|
||||||
|
|
||||||
if(connection.jingle.activecall)
|
|
||||||
{
|
|
||||||
connection.jingle.activecall.setVideoMute(
|
|
||||||
mute, localCallback, options);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
localCallback(mute);
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
setAudioMute: function (mute, callback) {
|
|
||||||
if (!(connection && APP.RTC.localAudio)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.forceMuted && !mute) {
|
|
||||||
console.info("Asking focus for unmute");
|
|
||||||
connection.moderate.setMute(connection.emuc.myroomjid, mute);
|
|
||||||
// FIXME: wait for result before resetting muted status
|
|
||||||
this.forceMuted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mute == APP.RTC.localAudio.isMuted()) {
|
|
||||||
// Nothing to do
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
APP.RTC.localAudio.setMute(mute);
|
|
||||||
this.sendAudioInfoPresence(mute, callback);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
sendAudioInfoPresence: function(mute, callback) {
|
|
||||||
if(connection) {
|
|
||||||
connection.emuc.addAudioInfoToPresence(mute);
|
|
||||||
connection.emuc.sendPresence();
|
|
||||||
}
|
|
||||||
callback();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
toggleRecording: function (tokenEmptyCallback,
|
|
||||||
recordingStateChangeCallback) {
|
|
||||||
Recording.toggleRecording(tokenEmptyCallback,
|
|
||||||
recordingStateChangeCallback, connection);
|
|
||||||
},
|
|
||||||
addToPresence: function (name, value, dontSend) {
|
|
||||||
switch (name) {
|
|
||||||
case "displayName":
|
|
||||||
connection.emuc.addDisplayNameToPresence(value);
|
|
||||||
break;
|
|
||||||
case "prezi":
|
|
||||||
connection.emuc.addPreziToPresence(value, 0);
|
|
||||||
break;
|
|
||||||
case "preziSlide":
|
|
||||||
connection.emuc.addCurrentSlideToPresence(value);
|
|
||||||
break;
|
|
||||||
case "connectionQuality":
|
|
||||||
connection.emuc.addConnectionInfoToPresence(value);
|
|
||||||
break;
|
|
||||||
case "email":
|
|
||||||
connection.emuc.addEmailToPresence(value);
|
|
||||||
break;
|
|
||||||
case "devices":
|
|
||||||
connection.emuc.addDevicesToPresence(value);
|
|
||||||
break;
|
|
||||||
case "videoType":
|
|
||||||
connection.emuc.addVideoTypeToPresence(value);
|
|
||||||
break;
|
|
||||||
case "startMuted":
|
|
||||||
if(!Moderator.isModerator())
|
|
||||||
return;
|
|
||||||
connection.emuc.addStartMutedToPresence(value[0],
|
|
||||||
value[1]);
|
|
||||||
break;
|
|
||||||
default :
|
|
||||||
console.log("Unknown tag for presence: " + name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!dontSend)
|
|
||||||
connection.emuc.sendPresence();
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Sends 'data' as a log message to the focus. Returns true iff a message
|
|
||||||
* was sent.
|
|
||||||
* @param data
|
|
||||||
* @returns {boolean} true iff a message was sent.
|
|
||||||
*/
|
|
||||||
sendLogs: function (data) {
|
|
||||||
if(!connection.emuc.focusMucJid)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var deflate = true;
|
|
||||||
|
|
||||||
var content = JSON.stringify(data);
|
|
||||||
if (deflate) {
|
|
||||||
content = String.fromCharCode.apply(null, Pako.deflateRaw(content));
|
|
||||||
}
|
|
||||||
content = Base64.encode(content);
|
|
||||||
// XEP-0337-ish
|
|
||||||
var message = $msg({to: connection.emuc.focusMucJid, type: 'normal'});
|
|
||||||
message.c('log', { xmlns: 'urn:xmpp:eventlog',
|
|
||||||
id: 'PeerConnectionStats'});
|
|
||||||
message.c('message').t(content).up();
|
|
||||||
if (deflate) {
|
|
||||||
message.c('tag', {name: "deflated", value: "true"}).up();
|
|
||||||
}
|
|
||||||
message.up();
|
|
||||||
|
|
||||||
connection.send(message);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
// Gets the logs from strophe.jingle.
|
|
||||||
getJingleLog: function () {
|
|
||||||
return connection.jingle ? connection.jingle.getLog() : {};
|
|
||||||
},
|
|
||||||
// Gets the logs from strophe.
|
|
||||||
getXmppLog: function () {
|
|
||||||
return connection.logger ? connection.logger.log : null;
|
|
||||||
},
|
|
||||||
getPrezi: function () {
|
|
||||||
return connection.emuc.getPrezi(this.myJid());
|
|
||||||
},
|
|
||||||
removePreziFromPresence: function () {
|
|
||||||
connection.emuc.removePreziFromPresence();
|
|
||||||
connection.emuc.sendPresence();
|
|
||||||
},
|
|
||||||
sendChatMessage: function (message, nickname) {
|
|
||||||
connection.emuc.sendMessage(message, nickname);
|
|
||||||
},
|
|
||||||
setSubject: function (topic) {
|
|
||||||
connection.emuc.setSubject(topic);
|
|
||||||
},
|
|
||||||
lockRoom: function (key, onSuccess, onError, onNotSupported) {
|
|
||||||
connection.emuc.lockRoom(key, onSuccess, onError, onNotSupported);
|
|
||||||
},
|
|
||||||
dial: function (to, from, roomName,roomPass) {
|
|
||||||
connection.rayo.dial(to, from, roomName,roomPass);
|
|
||||||
},
|
|
||||||
setMute: function (jid, mute) {
|
|
||||||
connection.moderate.setMute(jid, mute);
|
|
||||||
},
|
|
||||||
eject: function (jid) {
|
|
||||||
connection.moderate.eject(jid);
|
|
||||||
},
|
|
||||||
logout: function (callback) {
|
|
||||||
Moderator.logout(callback);
|
|
||||||
},
|
|
||||||
findJidFromResource: function (resource) {
|
|
||||||
return connection.emuc.findJidFromResource(resource);
|
|
||||||
},
|
|
||||||
getMembers: function () {
|
|
||||||
return connection.emuc.members;
|
|
||||||
},
|
|
||||||
getJidFromSSRC: function (ssrc) {
|
|
||||||
if (!this.isConferenceInProgress())
|
|
||||||
return null;
|
|
||||||
return connection.jingle.activecall.getSsrcOwner(ssrc);
|
|
||||||
},
|
|
||||||
// Returns true iff we have joined the MUC.
|
|
||||||
isMUCJoined: function () {
|
|
||||||
return connection === null ? false : connection.emuc.joined;
|
|
||||||
},
|
|
||||||
getSessions: function () {
|
|
||||||
return connection.jingle.sessions;
|
|
||||||
},
|
|
||||||
removeStream: function (stream) {
|
|
||||||
if (!this.isConferenceInProgress())
|
|
||||||
return;
|
|
||||||
connection.jingle.activecall.peerconnection.removeStream(stream);
|
|
||||||
},
|
|
||||||
filter_special_chars: function (text) {
|
|
||||||
return SDPUtil.filter_special_chars(text);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = XMPP;
|
|
Loading…
Reference in New Issue