Library for comfortable using WebRTC technology.
Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyfork.org/scripts/348/1119/RTCMultiConnection.js
// ==UserScript==
// @name RTCMultiConnection
// @version 1.7
// @description Library for comfortable using WebRTC technology.
// ==/UserScript==
(function () {
// www.RTCMultiConnection.org/docs/constructor/
window.RTCMultiConnection = function (channel) {
// a reference to your constructor!
var connection = this;
// www.RTCMultiConnection.org/docs/channel-id/
connection.channel = channel || location.href.replace(/\/|:|#|%|\.|\[|\]/g, '');
var rtcMultiSession; // a reference to backbone object i.e. RTCMultiSession!
// to allow single user to join multiple rooms;
// you can change this property at runtime!
connection.isAcceptNewSession = true;
// www.RTCMultiConnection.org/docs/open/
connection.open = function (args) {
connection.isAcceptNewSession = false;
// www.RTCMultiConnection.org/docs/session-initiator/
// you can always use this property to determine room owner!
connection.isInitiator = true;
var dontTransmit = false;
// a channel can contain multiple rooms i.e. sessions
if (args) {
if (typeof args == 'string') {
connection.sessionid = args;
} else {
if (typeof args.transmitRoomOnce != 'undefined') {
connection.transmitRoomOnce = args.transmitRoomOnce;
}
if (typeof args.dontTransmit != 'undefined') {
dontTransmit = args.dontTransmit;
}
if (typeof args.sessionid != 'undefined') {
connection.sessionid = args.sessionid;
}
}
}
// if firebase && if session initiator
if (connection.socket && connection.socket.remove) {
connection.socket.remove();
}
if (!connection.sessionid) connection.sessionid = connection.channel;
var sessionDescription = {
sessionid: connection.sessionid,
userid: connection.userid,
session: connection.session,
extra: connection.extra
};
if (!connection.stats.sessions[sessionDescription.sessionid]) {
connection.stats.numberOfSessions++;
connection.stats.sessions[sessionDescription.sessionid] = sessionDescription;
}
// verify to see if "openSignalingChannel" exists!
prepareSignalingChannel(function () {
// connect with signaling channel
initRTCMultiSession(function () {
// for session-initiator, user-media is captured as soon as "open" is invoked.
captureUserMedia(function () {
rtcMultiSession.initSession({
sessionDescription: sessionDescription,
dontTransmit: dontTransmit
});
});
});
});
return sessionDescription;
};
// www.RTCMultiConnection.org/docs/connect/
this.connect = function (sessionid) {
// a channel can contain multiple rooms i.e. sessions
if (sessionid) {
connection.sessionid = sessionid;
}
// verify to see if "openSignalingChannel" exists!
prepareSignalingChannel(function () {
// connect with signaling channel
initRTCMultiSession();
});
return this;
};
// www.RTCMultiConnection.org/docs/join/
this.join = joinSession;
// www.RTCMultiConnection.org/docs/send/
this.send = function (data, _channel) {
// send file/data or /text
if (!data)
throw 'No file, data or text message to share.';
// connection.send([file1, file2, file3])
// you can share multiple files, strings or data objects using "send" method!
if (!!data.forEach) {
// todo: this mechanism can cause failure for subsequent packets/data
// on Firefox especially; and on chrome as well!
// todo: need to use setTimeout instead.
for (var i = 0; i < data.length; i++) {
connection.send(data[i], _channel);
}
return;
}
// File or Blob object MUST have "type" and "size" properties
if (typeof data.size != 'undefined' && typeof data.type != 'undefined') {
// to send multiple files concurrently!
// file of any size; maximum length: 1GB
FileSender.send({
file: data,
channel: rtcMultiSession,
_channel: _channel,
connection: connection
});
} else {
// to allow longest string messages
// and largest data objects
// or anything of any size!
// to send multiple data objects concurrently!
TextSender.send({
text: data,
channel: rtcMultiSession,
_channel: _channel,
connection: connection
});
}
};
// this method checks to verify "openSignalingChannel" method
// github.com/muaz-khan/WebRTC-Experiment/blob/master/Signaling.md
function prepareSignalingChannel(callback) {
if (connection.openSignalingChannel) return callback();
// make sure firebase.js is loaded before using their JavaScript API
if (!window.Firebase) {
return loadScript('https://www.webrtc-experiment.com/firebase.js', function () {
prepareSignalingChannel(callback);
});
}
// Single socket is a preferred solution!
var socketCallbacks = {};
var firebase = new Firebase('https://' + connection.firebase + '.firebaseio.com/' + connection.channel);
firebase.on('child_added', function (snap) {
var data = snap.val();
if (data.sender == connection.userid) return;
if (socketCallbacks[data.channel]) {
socketCallbacks[data.channel](data.message);
}
snap.ref().remove();
});
// www.RTCMultiConnection.org/docs/openSignalingChannel/
connection.openSignalingChannel = function (args) {
var callbackid = args.channel || connection.channel;
socketCallbacks[callbackid] = args.onmessage;
if (args.onopen) setTimeout(args.onopen, 1000);
return {
send: function (message) {
firebase.push({
sender: connection.userid,
channel: callbackid,
message: message
});
},
channel: channel // todo: remove this "channel" object
};
};
firebase.onDisconnect().remove();
callback();
}
function initRTCMultiSession(onSignalingReady) {
// RTCMultiSession is the backbone object;
// this object MUST be initialized once!
if (rtcMultiSession) return onSignalingReady();
// your everything is passed over RTCMultiSession constructor!
rtcMultiSession = new RTCMultiSession(connection, onSignalingReady);
}
function joinSession(session) {
if (!session || !session.userid || !session.sessionid)
throw 'invalid data passed over "join" method';
if (!rtcMultiSession) {
// verify to see if "openSignalingChannel" exists!
prepareSignalingChannel(function () {
// connect with signaling channel
initRTCMultiSession(function () {
joinSession(session);
});
});
return;
}
connection.session = session.session;
extra = connection.extra || session.extra || {};
// todo: need to verify that if-block statement works as expected.
// expectations: if it is oneway streaming; or if it is data-only connection
// then, it shouldn't capture user-media on participant's side.
if (session.oneway || isData(session)) {
rtcMultiSession.joinSession(session, extra);
} else {
captureUserMedia(function () {
rtcMultiSession.joinSession(session, extra);
});
}
}
var isFirstSession = true;
// www.RTCMultiConnection.org/docs/captureUserMedia/
function captureUserMedia(callback, _session) {
// capture user's media resources
var session = _session || connection.session;
if (isEmpty(session)) {
if (callback) callback();
return;
}
// you can force to skip media capturing!
if (connection.dontAttachStream)
return callback();
// if it is data-only connection
// if it is one-way connection and current user is participant
if (isData(session) || (!connection.isInitiator && session.oneway)) {
// www.RTCMultiConnection.org/docs/attachStreams/
connection.attachStreams = [];
return callback();
}
var constraints = {
audio: !!session.audio,
video: !!session.video
};
// if custom audio device is selected
if (connection._mediaSources.audio) {
constraints.audio = {
optional: [{
sourceId: connection._mediaSources.audio
}]
};
}
// if custom video device is selected
if (connection._mediaSources.video) {
constraints.video = {
optional: [{
sourceId: connection._mediaSources.video
}]
};
}
var screen_constraints = {
audio: false,
video: {
mandatory: {
chromeMediaSource: 'screen'
},
optional: []
}
};
// if screen is prompted
if (session.screen) {
var _isFirstSession = isFirstSession;
_captureUserMedia(screen_constraints, constraints.audio || constraints.video ? function () {
if (_isFirstSession) isFirstSession = true;
_captureUserMedia(constraints, callback);
} : callback);
} else _captureUserMedia(constraints, callback, session.audio && !session.video);
function _captureUserMedia(forcedConstraints, forcedCallback, isRemoveVideoTracks) {
var mediaConfig = {
onsuccess: function (stream, returnBack, idInstance, streamid) {
if (isRemoveVideoTracks && isChrome) {
stream = new window.webkitMediaStream(stream.getAudioTracks());
}
// var streamid = getRandomString();
connection.localStreamids.push(streamid);
stream.onended = function () {
connection.onstreamended(streamedObject);
// if user clicks "stop" button to close screen sharing
var _stream = connection.streams[streamid];
if (_stream && _stream.sockets.length) {
_stream.sockets.forEach(function (socket) {
socket.send({
streamid: _stream.streamid,
userid: _stream.rtcMultiConnection.userid,
extra: _stream.rtcMultiConnection.extra,
stopped: true
});
});
}
currentUserMediaRequest.mutex = false;
// to make sure same stream can be captured again!
if (currentUserMediaRequest.streams[idInstance]) {
delete currentUserMediaRequest.streams[idInstance];
}
};
var mediaElement = createMediaElement(stream, session);
mediaElement.muted = true;
stream.streamid = streamid;
var streamedObject = {
stream: stream,
streamid: streamid,
mediaElement: mediaElement,
blobURL: mediaElement.mozSrcObject || mediaElement.src,
type: 'local',
userid: connection.userid,
extra: connection.extra,
session: session,
isVideo: stream.getVideoTracks().length > 0,
isAudio: !stream.getVideoTracks().length && stream.getAudioTracks().length > 0,
isInitiator: !!connection.isInitiator
};
var sObject = {
stream: stream,
userid: connection.userid,
streamid: streamid,
session: session,
type: 'local',
streamObject: streamedObject,
mediaElement: mediaElement,
rtcMultiConnection: connection
};
if (isFirstSession) {
connection.attachStreams.push(stream);
}
isFirstSession = false;
connection.streams[streamid] = connection._getStream(sObject);
if (!returnBack) {
connection.onstream(streamedObject);
}
if (connection.setDefaultEventsForMediaElement) {
connection.setDefaultEventsForMediaElement(mediaElement, streamid);
}
if (forcedCallback) forcedCallback(stream, streamedObject);
if (connection.onspeaking) {
var soundMeter = new SoundMeter({
context: connection._audioContext,
connection: connection,
event: streamedObject
});
soundMeter.connectToSource(stream);
}
},
onerror: function (e, idInstance) {
connection.onMediaError(toStr(e));
if (session.audio) {
connection.onMediaError('Maybe microphone access is denied.');
}
if (session.video) {
connection.onMediaError('Maybe webcam access is denied.');
}
if (session.screen) {
if (isFirefox) {
connection.onMediaError('Firefox has not yet released their screen capturing modules. Still work in progress! Please try chrome for now!');
} else if (location.protocol !== 'https:') {
connection.onMediaError('<https> is mandatory to capture screen.');
} else {
connection.onMediaError('Unable to detect actual issue. Maybe "deprecated" screen capturing flag is not enabled or maybe you clicked "No" button.');
}
currentUserMediaRequest.mutex = false;
// to make sure same stream can be captured again!
if (currentUserMediaRequest.streams[idInstance]) {
delete currentUserMediaRequest.streams[idInstance];
}
}
},
mediaConstraints: connection.mediaConstraints || {}
};
mediaConfig.constraints = forcedConstraints || constraints;
mediaConfig.media = connection.media;
getUserMedia(mediaConfig);
}
}
// www.RTCMultiConnection.org/docs/captureUserMedia/
this.captureUserMedia = captureUserMedia;
// www.RTCMultiConnection.org/docs/leave/
this.leave = function (userid) {
// eject a user; or leave the session
rtcMultiSession.leave(userid);
if (!userid) {
var streams = connection.attachStreams;
for (var i = 0; i < streams.length; i++) {
stopTracks(streams[i]);
}
currentUserMediaRequest.streams = [];
connection.attachStreams = [];
}
// if firebase; remove data from firebase servers
if (connection.isInitiator && !!connection.socket && !!connection.socket.remove) {
connection.socket.remove();
}
};
// www.RTCMultiConnection.org/docs/eject/
this.eject = function (userid) {
if (!connection.isInitiator) throw 'Only session-initiator can eject a user.';
this.leave(userid);
};
// www.RTCMultiConnection.org/docs/close/
this.close = function () {
// close entire session
connection.autoCloseEntireSession = true;
rtcMultiSession.leave();
};
// www.RTCMultiConnection.org/docs/renegotiate/
this.renegotiate = function (stream, session) {
rtcMultiSession.addStream({
renegotiate: session || {
oneway: true,
audio: true,
video: true
},
stream: stream
});
};
// www.RTCMultiConnection.org/docs/addStream/
this.addStream = function (session, socket) {
// www.RTCMultiConnection.org/docs/renegotiation/
// renegotiate new media stream
if (session) {
var isOneWayStreamFromParticipant;
if (!connection.isInitiator && session.oneway) {
session.oneway = false;
isOneWayStreamFromParticipant = true;
}
captureUserMedia(function (stream) {
if (isOneWayStreamFromParticipant) {
session.oneway = true;
}
addStream(stream);
}, session);
} else addStream();
function addStream(stream) {
rtcMultiSession.addStream({
stream: stream,
renegotiate: session || connection.session,
socket: socket
});
}
};
// www.RTCMultiConnection.org/docs/removeStream/
this.removeStream = function (streamid) {
// detach pre-attached streams
if (!this.streams[streamid]) return warn('No such stream exists. Stream-id:', streamid);
// www.RTCMultiConnection.org/docs/detachStreams/
this.detachStreams.push(streamid);
this.renegotiate();
};
// set RTCMultiConnection defaults on constructor invocation
setDefaults(this);
};
function RTCMultiSession(connection, onSignalingReady) {
var fileReceiver = new FileReceiver(connection);
var textReceiver = new TextReceiver(connection);
function onDataChannelMessage(e) {
if (!e) return;
e = JSON.parse(e);
if (e.data.type === 'text') {
textReceiver.receive(e.data, e.userid, e.extra);
} else if (typeof e.data.maxChunks != 'undefined') {
fileReceiver.receive(e.data);
} else {
if (connection.autoTranslateText) {
e.original = e.data;
connection.Translator.TranslateText(e.data, function (translatedText) {
e.data = translatedText;
connection.onmessage(e);
});
} else connection.onmessage(e);
}
}
function onNewSession(session) {
// todo: make sure this works as expected.
// i.e. "onNewSession" should be fired only for
// sessionid that is passed over "connect" method.
if (connection.sessionid && session.sessionid != connection.sessionid) return;
if (connection.onNewSession) {
session.join = function (forceSession) {
if (!forceSession) return connection.join(session);
for (var f in forceSession) {
session.session[f] = forceSession[f];
}
// keeping previous state
var isDontAttachStream = connection.dontAttachStream;
connection.dontAttachStream = false;
connection.captureUserMedia(function () {
connection.dontAttachStream = true;
connection.join(session);
// returning back previous state
connection.dontAttachStream = isDontAttachStream;
}, forceSession);
};
if (!session.extra) session.extra = {};
return connection.onNewSession(session);
}
connection.join(session);
}
var socketObjects = {};
var sockets = [];
var rtcMultiSession = this;
var participants = {};
function updateSocketForLocalStreams(socket) {
for (var i = 0; i < connection.localStreamids.length; i++) {
var streamid = connection.localStreamids[i];
if (connection.streams[streamid]) {
// using "sockets" array to keep references of all sockets using
// this media stream; so we can fire "onstreamended" among all users.
connection.streams[streamid].sockets.push(socket);
}
}
}
function newPrivateSocket(_config) {
var socketConfig = {
channel: _config.channel,
onmessage: socketResponse,
onopen: function (_socket) {
if (_socket) socket = _socket;
if (isofferer && !peer) {
peerConfig.session = connection.session;
if (!peer) peer = new PeerConnection();
peer.create('offer', peerConfig);
}
_config.socketIndex = socket.index = sockets.length;
socketObjects[socketConfig.channel] = socket;
sockets[_config.socketIndex] = socket;
updateSocketForLocalStreams(socket);
}
};
socketConfig.callback = function (_socket) {
socket = _socket;
socketConfig.onopen();
};
var socket = connection.openSignalingChannel(socketConfig),
isofferer = _config.isofferer,
peer;
var peerConfig = {
onopen: onChannelOpened,
onicecandidate: function (candidate) {
if (!connection.candidates) throw 'ICE candidates are mandatory.';
if (!connection.candidates.host && candidate.candidate.indexOf('typ host') != -1) return;
if (!connection.candidates.relay && candidate.candidate.indexOf('relay') != -1) return;
if (!connection.candidates.reflexive && candidate.candidate.indexOf('srflx') != -1) return;
log(candidate.candidate);
socket && socket.send({
userid: connection.userid,
candidate: {
sdpMLineIndex: candidate.sdpMLineIndex,
candidate: JSON.stringify(candidate.candidate)
}
});
},
onmessage: onDataChannelMessage,
onaddstream: function (stream, session) {
session = session || _config.renegotiate || connection.session;
// if it is Firefox; then return.
if (isData(session)) return;
if (_config.streaminfo) {
var streaminfo = _config.streaminfo.split('----');
for (var i = 0; i < streaminfo.length; i++) {
stream.streamid = streaminfo[i];
}
_config.streaminfo = swap(streaminfo.pop()).join('----');
}
var mediaElement = createMediaElement(stream, merge({ remote: true }, session));
_config.stream = stream;
if (!stream.getVideoTracks().length)
mediaElement.addEventListener('play', function () {
setTimeout(function () {
mediaElement.muted = false;
afterRemoteStreamStartedFlowing(mediaElement, session);
}, 3000);
}, false);
else
waitUntilRemoteStreamStartsFlowing(mediaElement, session);
if (connection.setDefaultEventsForMediaElement) {
connection.setDefaultEventsForMediaElement(mediaElement, stream.streamid);
}
// to allow this user join all existing users!
if (connection.isInitiator && getLength(participants) > 1 && getLength(participants) <= connection.maxParticipantsAllowed) {
if (!connection.session.oneway && !connection.session.broadcast) {
defaultSocket.send({
joinUsers: participants,
userid: connection.userid,
extra: connection.extra
});
}
}
},
onremovestream: function (event) {
warn('onremovestream', event);
},
onclose: function (e) {
e.extra = _config.extra;
e.userid = _config.userid;
connection.onclose(e);
// suggested in #71 by "efaj"
if (connection.channels[e.userid])
delete connection.channels[e.userid];
},
onerror: function (e) {
e.extra = _config.extra;
e.userid = _config.userid;
connection.onerror(e);
},
oniceconnectionstatechange: function (event) {
log('oniceconnectionstatechange', toStr(event));
if (connection.peers[_config.userid] && connection.peers[_config.userid].oniceconnectionstatechange) {
connection.peers[_config.userid].oniceconnectionstatechange(event);
}
if (!connection.autoReDialOnFailure) return;
if (connection.peers[_config.userid]) {
if (connection.peers[_config.userid].peer.connection.iceConnectionState != 'disconnected') {
_config.redialing = false;
}
if (connection.peers[_config.userid].peer.connection.iceConnectionState == 'disconnected' && !_config.redialing) {
_config.redialing = true;
warn('Peer connection is closed.', toStr(connection.peers[_config.userid].peer.connection), 'ReDialing..');
connection.peers[_config.userid].socket.send({
userid: connection.userid,
extra: connection.extra || {},
redial: true
});
// to make sure all old "remote" streams are also removed!
for (var stream in connection.streams) {
stream = connection.streams[stream];
if (stream.userid == _config.userid && stream.type == 'remote') {
connection.onstreamended(stream.streamObject);
}
}
}
}
},
onsignalingstatechange: function (event) {
log('onsignalingstatechange', toStr(event));
},
attachStreams: connection.attachStreams,
iceServers: connection.iceServers,
bandwidth: connection.bandwidth,
sdpConstraints: connection.sdpConstraints,
optionalArgument: connection.optionalArgument,
disableDtlsSrtp: connection.disableDtlsSrtp,
dataChannelDict: connection.dataChannelDict,
preferSCTP: connection.preferSCTP,
onSessionDescription: function (sessionDescription, streaminfo) {
sendsdp({
sdp: sessionDescription,
socket: socket,
streaminfo: streaminfo
});
},
socket: socket,
selfUserid: connection.userid
};
function waitUntilRemoteStreamStartsFlowing(mediaElement, session, numberOfTimes) {
if (!numberOfTimes) numberOfTimes = 0;
numberOfTimes++;
if (!(mediaElement.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA || mediaElement.paused || mediaElement.currentTime <= 0)) {
afterRemoteStreamStartedFlowing(mediaElement, session);
} else {
if (numberOfTimes >= 100) {
socket.send({
userid: connection.userid,
extra: connection.extra,
failedToReceiveRemoteVideo: true,
streamid: _config.stream.streamid
});
} else
setTimeout(function () {
log('waiting for remote video to play: ' + numberOfTimes);
waitUntilRemoteStreamStartsFlowing(mediaElement, session, numberOfTimes);
}, 200);
}
}
function initFakeChannel() {
if (!connection.fakeDataChannels || connection.channels[_config.userid]) return;
// for non-data connections; allow fake data sender!
if (!connection.session.data) {
var fakeChannel = {
send: function (data) {
socket.send({
fakeData: data
});
},
readyState: 'open'
};
// connection.channels['user-id'].send(data);
connection.channels[_config.userid] = {
channel: fakeChannel,
send: function (data) {
this.channel.send(data);
}
};
peerConfig.onopen(fakeChannel);
}
}
function afterRemoteStreamStartedFlowing(mediaElement, session) {
var stream = _config.stream;
stream.onended = function () {
connection.onstreamended(streamedObject);
};
var streamedObject = {
mediaElement: mediaElement,
stream: stream,
streamid: stream.streamid,
session: session || connection.session,
blobURL: mediaElement.mozSrcObject || mediaElement.src,
type: 'remote',
extra: _config.extra,
userid: _config.userid,
isVideo: stream.getVideoTracks().length > 0,
isAudio: !stream.getVideoTracks().length && stream.getAudioTracks().length > 0,
isInitiator: !!_config.isInitiator
};
// connection.streams['stream-id'].mute({audio:true})
connection.streams[stream.streamid] = connection._getStream({
stream: stream,
userid: _config.userid,
streamid: stream.streamid,
socket: socket,
type: 'remote',
streamObject: streamedObject,
mediaElement: mediaElement,
rtcMultiConnection: connection,
session: session || connection.session
});
connection.onstream(streamedObject);
onSessionOpened();
if (connection.onspeaking) {
var soundMeter = new SoundMeter({
context: connection._audioContext,
connection: connection,
event: streamedObject
});
soundMeter.connectToSource(stream);
}
}
function onChannelOpened(channel) {
_config.channel = channel;
// connection.channels['user-id'].send(data);
connection.channels[_config.userid] = {
channel: _config.channel,
send: function (data) {
connection.send(data, this.channel);
}
};
connection.onopen({
extra: _config.extra,
userid: _config.userid
});
// fetch files from file-queue
for (var q in connection.fileQueue) {
connection.send(connection.fileQueue[q], channel);
}
if (isData(connection.session)) onSessionOpened();
}
function updateSocket() {
// todo: need to check following {if-block} MUST not affect "redial" process
if (socket.userid == _config.userid)
return;
socket.userid = _config.userid;
sockets[_config.socketIndex] = socket;
connection.stats.numberOfConnectedUsers++;
// connection.peers['user-id'].addStream({audio:true})
connection.peers[_config.userid] = {
socket: socket,
peer: peer,
userid: _config.userid,
extra: _config.extra,
addStream: function (session00) {
// connection.peers['user-id'].addStream({audio: true, video: true);
connection.addStream(session00, this.socket);
},
removeStream: function (streamid) {
if (!connection.streams[streamid])
return warn('No such stream exists. Stream-id:', streamid);
this.peer.connection.removeStream(connection.streams[streamid].stream);
this.renegotiate();
},
renegotiate: function (stream, session) {
// connection.peers['user-id'].renegotiate();
connection.renegotiate(stream, session);
},
changeBandwidth: function (bandwidth) {
// connection.peers['user-id'].changeBandwidth();
if (!bandwidth) throw 'You MUST pass bandwidth object.';
if (typeof bandwidth == 'string') throw 'Pass object for bandwidth instead of string; e.g. {audio:10, video:20}';
// set bandwidth for self
this.peer.bandwidth = bandwidth;
// ask remote user to synchronize bandwidth
this.socket.send({
userid: connection.userid,
extra: connection.extra || {},
changeBandwidth: true,
bandwidth: bandwidth
});
},
sendCustomMessage: function (message) {
// connection.peers['user-id'].sendCustomMessage();
this.socket.send({
userid: connection.userid,
extra: connection.extra || {},
customMessage: true,
message: message
});
},
onCustomMessage: function (message) {
log('Received "private" message from', this.userid,
typeof message == 'string' ? message : toStr(message));
},
drop: function (dontSendMessage) {
// connection.peers['user-id'].drop();
for (var stream in connection.streams) {
if (connection._skip.indexOf(stream) == -1) {
stream = connection.streams[stream];
if (stream.userid == connection.userid && stream.type == 'local') {
this.peer.connection.removeStream(stream.stream);
connection.onstreamended(stream.streamObject);
}
if (stream.type == 'remote' && stream.userid == this.userid) {
connection.onstreamended(stream.streamObject);
}
}
}
!dontSendMessage && this.socket.send({
userid: connection.userid,
extra: connection.extra || {},
drop: true
});
},
hold: function (holdMLine) {
// connection.peers['user-id'].hold();
this.socket.send({
userid: connection.userid,
extra: connection.extra || {},
hold: true,
holdMLine: holdMLine || 'both'
});
this.peer.hold = true;
this.fireHoldUnHoldEvents({
kind: holdMLine,
isHold: true,
userid: connection.userid,
remoteUser: this.userid
});
},
unhold: function (holdMLine) {
// connection.peers['user-id'].unhold();
this.socket.send({
userid: connection.userid,
extra: connection.extra || {},
unhold: true,
holdMLine: holdMLine || 'both'
});
this.peer.hold = false;
this.fireHoldUnHoldEvents({
kind: holdMLine,
isHold: false,
userid: connection.userid,
remoteUser: this.userid
});
},
fireHoldUnHoldEvents: function (e) {
// this method is for inner usages only!
var isHold = e.isHold;
var kind = e.kind;
var userid = e.remoteUser || e.userid;
// hold means inactive a specific media line!
// a media line can contain multiple synced sources (ssrc)
// i.e. a media line can reference multiple tracks!
// that's why hold will affect all relevant tracks in a specific media line!
for (var stream in connection.streams) {
if (connection._skip.indexOf(stream) == -1) {
stream = connection.streams[stream];
if (stream.userid == userid) {
// www.RTCMultiConnection.org/docs/onhold/
if (isHold)
connection.onhold(merge({
kind: kind
}, stream.streamObject));
// www.RTCMultiConnection.org/docs/onunhold/
if (!isHold)
connection.onunhold(merge({
kind: kind
}, stream.streamObject));
}
}
}
},
redial: function () {
// connection.peers['user-id'].redial();
// 1st of all; remove all relevant remote media streams
for (var stream in connection.streams) {
if (connection._skip.indexOf(stream) == -1) {
stream = connection.streams[stream];
if (stream.userid == this.userid && stream.type == 'remote') {
connection.onstreamended(stream.streamObject);
}
}
}
log('ReDialing...');
socket.send({
userid: connection.userid,
extra: connection.extra,
recreatePeer: true
});
peer = new PeerConnection();
peer.create('offer', peerConfig);
},
sharePartOfScreen: function (args) {
// www.RTCMultiConnection.org/docs/onpartofscreen/
var element = args.element;
var that = this;
if (!window.html2canvas) {
return loadScript('https://www.webrtc-experiment.com/screenshot.js', function () {
that.sharePartOfScreen(args);
});
}
if (typeof element == 'string') {
element = document.querySelector(element);
if (!element) element = document.getElementById(element);
}
if (!element) throw 'HTML Element is inaccessible!';
function partOfScreenCapturer() {
// if stopped
if (that.stopPartOfScreenSharing) {
that.stopPartOfScreenSharing = false;
if (connection.onpartofscreenstopped) {
connection.onpartofscreenstopped();
}
return;
}
// if paused
if (that.pausePartOfScreenSharing) {
if (connection.onpartofscreenpaused) {
connection.onpartofscreenpaused();
}
return setTimeout(partOfScreenCapturer, args.interval || 200);
}
// html2canvas.js is used to take screenshots
html2canvas(element, {
onrendered: function (canvas) {
var screenshot = canvas.toDataURL();
if (!connection.channels[that.userid]) {
throw 'No such data channel exists.';
}
connection.channels[that.userid].send({
userid: connection.userid,
extra: connection.extra,
screenshot: screenshot,
isPartOfScreen: true
});
// "once" can be used to share single screenshot
!args.once && setTimeout(partOfScreenCapturer, args.interval || 200);
}
});
}
partOfScreenCapturer();
}
};
}
function onSessionOpened() {
// admin/guest is one-to-one relationship
if (connection.userType && connection.direction !== 'many-to-many') return;
// original conferencing infrastructure!
if (connection.isInitiator && getLength(participants) > 1 && getLength(participants) <= connection.maxParticipantsAllowed) {
if (!connection.session.oneway && !connection.session.broadcast) {
defaultSocket.send({
sessionid: connection.sessionid,
newParticipant: _config.userid || socket.channel,
userid: connection.userid,
extra: connection.extra,
userData: {
userid: _config.userid,
extra: _config.extra
}
});
} else if (connection.interconnect) {
socket.send({
joinUsers: participants,
userid: connection.userid,
extra: connection.extra
});
}
}
if (connection.isInitiator) {
// this code snippet is added to make sure that "previously-renegotiated" streams are also
// renegotiated to this new user
// todo: currently renegotiating only one stream; need renegotiate all.
if (connection.renegotiatedSessions[0]) {
connection.peers[_config.userid].renegotiate(connection.renegotiatedSessions[0].stream, connection.renegotiatedSessions[0].session);
}
}
}
function socketResponse(response) {
if (response.userid == connection.userid)
return;
if (response.sdp) {
_config.userid = response.userid;
_config.extra = response.extra || {};
_config.renegotiate = response.renegotiate;
_config.streaminfo = response.streaminfo;
_config.isInitiator = response.isInitiator;
var sdp = JSON.parse(response.sdp);
if (sdp.type == 'offer') {
// to synchronize SCTP or RTP
peerConfig.preferSCTP = !!response.preferSCTP;
connection.fakeDataChannels = !!response.fakeDataChannels;
}
// initializing fake channel
initFakeChannel();
sdpInvoker(sdp, response.labels);
}
if (response.candidate) {
peer && peer.addIceCandidate({
sdpMLineIndex: response.candidate.sdpMLineIndex,
candidate: JSON.parse(response.candidate.candidate)
});
}
if (response.mute || response.unmute) {
if (response.promptMuteUnmute) {
if (connection.streams[response.streamid]) {
if (response.mute && !connection.streams[response.streamid].muted) {
connection.streams[response.streamid].mute(response.session);
}
if (response.unmute && connection.streams[response.streamid].muted) {
connection.streams[response.streamid].unmute(response.session);
}
}
} else {
var streamObject = {};
if (connection.streams[response.streamid]) {
streamObject = connection.streams[response.streamid].streamObject;
}
var session = response.session;
var fakeObject = merge({}, streamObject);
fakeObject.session = session;
fakeObject.isAudio = session.audio && !session.video;
fakeObject.isVideo = (!session.audio && session.video) || (session.audio && session.video);
if (response.mute) connection.onmute(fakeObject || response);
if (response.unmute) connection.onunmute(fakeObject || response);
}
}
if (response.isVolumeChanged) {
log('Volume of stream: ' + response.streamid + ' has changed to: ' + response.volume);
if (connection.streams[response.streamid]) {
var mediaElement = connection.streams[response.streamid].mediaElement;
if (mediaElement) mediaElement.volume = response.volume;
}
}
// to stop local stream
if (response.stopped) {
if (connection.streams[response.streamid]) {
connection.onstreamended(connection.streams[response.streamid].streamObject);
}
}
// to stop remote stream
if (response.promptStreamStop /* && !connection.isInitiator */) {
// var forceToStopRemoteStream = true;
// connection.streams['remote-stream-id'].stop( forceToStopRemoteStream );
warn('Remote stream has been manually stopped!');
if (connection.streams[response.streamid]) {
connection.streams[response.streamid].stop();
}
}
if (response.left) {
// firefox is unable to stop remote streams
// firefox doesn't auto stop streams when peer.close() is called.
if (isFirefox) {
var userLeft = response.userid;
for (var stream in connection.streams) {
stream = connection.streams[stream];
if (stream.userid == userLeft) {
stopTracks(stream);
stream.stream.onended(stream.streamObject);
}
}
}
if (peer && peer.connection) {
peer.connection.close();
peer.connection = null;
}
if (response.closeEntireSession) {
connection.close();
connection.refresh();
} else if (socket && response.ejected) {
// if user is ejected; his stream MUST be removed
// from all other users' side
socket.send({
left: true,
extra: connection.extra,
userid: connection.userid
});
if (sockets[_config.socketIndex])
delete sockets[_config.socketIndex];
if (socketObjects[socket.channel])
delete socketObjects[socket.channel];
socket = null;
}
connection.remove(response.userid);
if (participants[response.userid]) delete participants[response.userid];
connection.onleave({
userid: response.userid,
extra: response.extra,
entireSessionClosed: !!response.closeEntireSession
});
if (connection.userType) connection.busy = false;
}
// keeping session active even if initiator leaves
if (response.playRoleOfBroadcaster) {
if (response.extra) {
connection.extra = merge(connection.extra, response.extra);
}
setTimeout(connection.playRoleOfInitiator, 2000);
}
if (response.isCreateDataChannel) {
if (isFirefox) {
peer.createDataChannel();
}
}
if (response.changeBandwidth) {
if (!connection.peers[response.userid]) throw 'No such peer exists.';
// synchronize bandwidth
connection.peers[response.userid].peer.bandwidth = response.bandwidth;
// renegotiate to apply bandwidth
connection.peers[response.userid].renegotiate();
}
if (response.customMessage) {
if (!connection.peers[response.userid]) throw 'No such peer exists.';
connection.peers[response.userid].onCustomMessage(response.message);
}
if (response.drop) {
if (!connection.peers[response.userid]) throw 'No such peer exists.';
connection.peers[response.userid].drop(true);
connection.peers[response.userid].renegotiate();
connection.ondrop(response.userid);
}
if (response.hold) {
if (!connection.peers[response.userid]) throw 'No such peer exists.';
connection.peers[response.userid].peer.hold = true;
connection.peers[response.userid].peer.holdMLine = response.holdMLine;
connection.peers[response.userid].renegotiate();
connection.peers[response.userid].fireHoldUnHoldEvents({
kind: response.holdMLine,
isHold: true,
userid: response.userid
});
}
if (response.unhold) {
if (!connection.peers[response.userid]) throw 'No such peer exists.';
connection.peers[response.userid].peer.hold = false;
connection.peers[response.userid].peer.holdMLine = response.holdMLine;
connection.peers[response.userid].renegotiate();
connection.peers[response.userid].fireHoldUnHoldEvents({
kind: response.holdMLine,
isHold: false,
userid: response.userid
});
}
// fake data channels!
if (response.fakeData) {
peerConfig.onmessage(response.fakeData);
}
// sometimes we don't need to renegotiate e.g. when peers are disconnected
// or if it is firefox
if (response.recreatePeer) {
peer = new PeerConnection();
}
// remote video failed either out of ICE gathering process or ICE connectivity check-up
// or IceAgent was unable to locate valid candidates/ports.
if (response.failedToReceiveRemoteVideo) {
log('Remote peer hasn\'t received stream: ' + response.streamid + '. Renegotiating...');
if (connection.peers[response.userid]) {
connection.peers[response.userid].renegotiate();
}
}
if (response.joinUsers) {
for (var user in response.joinUsers) {
if (!participants[response.joinUsers[user]]) {
onNewParticipant({
sessionid: connection.sessionid,
newParticipant: response.joinUsers[user],
userid: connection.userid,
extra: connection.extra,
interconnect: true
});
}
}
}
if (response.redial) {
if (connection.peers[response.userid]) {
if (connection.peers[response.userid].peer.connection.iceConnectionState != 'disconnected') {
_config.redialing = false;
}
if (connection.peers[response.userid].peer.connection.iceConnectionState == 'disconnected' && !_config.redialing) {
_config.redialing = true;
warn('Peer connection is closed.', toStr(connection.peers[response.userid].peer.connection), 'ReDialing..');
connection.peers[response.userid].redial();
}
}
}
}
connection.playRoleOfInitiator = function () {
connection.dontAttachStream = true;
connection.open();
sockets = swap(sockets);
connection.dontAttachStream = false;
};
function sdpInvoker(sdp, labels) {
log(sdp.type, sdp.sdp);
if (sdp.type == 'answer') {
peer.setRemoteDescription(sdp);
updateSocket();
return;
}
if (!_config.renegotiate && sdp.type == 'offer') {
peerConfig.offerDescription = sdp;
peerConfig.session = connection.session;
if (!peer) peer = new PeerConnection();
peer.create('answer', peerConfig);
updateSocket();
return;
}
var session = _config.renegotiate;
// detach streams
detachMediaStream(labels, peer.connection);
if (session.oneway || isData(session)) {
createAnswer();
} else {
if (_config.capturing)
return;
_config.capturing = true;
connection.captureUserMedia(function (stream) {
_config.capturing = false;
if (isChrome || (isFirefox && !peer.connection.getLocalStreams().length)) {
peer.connection.addStream(stream);
}
createAnswer();
}, _config.renegotiate);
}
delete _config.renegotiate;
function createAnswer() {
if (isFirefox) {
if (connection.peers[_config.userid]) {
connection.peers[_config.userid].redial();
}
return;
}
peer.recreateAnswer(sdp, session, function (_sdp, streaminfo) {
sendsdp({
sdp: _sdp,
socket: socket,
streaminfo: streaminfo
});
});
}
}
}
function detachMediaStream(labels, peer) {
if (!labels) return;
for (var i = 0; i < labels.length; i++) {
var label = labels[i];
if (connection.streams[label]) {
peer.removeStream(connection.streams[label].stream);
}
}
}
function sendsdp(e) {
e.socket.send({
userid: connection.userid,
sdp: JSON.stringify(e.sdp),
extra: connection.extra,
renegotiate: !!e.renegotiate ? e.renegotiate : false,
streaminfo: e.streaminfo || '',
labels: e.labels || [],
preferSCTP: !!connection.preferSCTP,
fakeDataChannels: !!connection.fakeDataChannels,
isInitiator: !!connection.isInitiator
});
}
// sharing new user with existing participants
function onNewParticipant(response) {
if (response.interconnect && !connection.interconnect) return;
// todo: make sure this works as expected.
// if(connection.sessionid && response.sessionid != connection.sessionid) return;
var channel = response.newParticipant;
if (!channel || !!participants[channel] || channel == connection.userid)
return;
participants[channel] = channel;
var new_channel = connection.token();
newPrivateSocket({
channel: new_channel,
extra: response.userData ? response.userData.extra : response.extra,
userid: response.userData ? response.userData.userid : response.userid
});
defaultSocket.send({
participant: true,
userid: connection.userid,
targetUser: channel,
channel: new_channel,
extra: connection.extra
});
}
// if a user leaves
function clearSession(channel) {
connection.stats.numberOfConnectedUsers--;
var alert = {
left: true,
extra: connection.extra,
userid: connection.userid,
sessionid: connection.sessionid
};
if (connection.isInitiator) {
if (connection.autoCloseEntireSession) {
alert.closeEntireSession = true;
} else if (sockets[0]) {
sockets[0].send({
playRoleOfBroadcaster: true,
userid: connection.userid
});
}
}
if (!channel) {
var length = sockets.length;
for (var i = 0; i < length; i++) {
socket = sockets[i];
if (socket) {
socket.send(alert);
if (socketObjects[socket.channel])
delete socketObjects[socket.channel];
delete sockets[i];
}
}
}
// eject a specific user!
if (channel) {
socket = socketObjects[channel];
if (socket) {
alert.ejected = true;
socket.send(alert);
if (sockets[socket.index])
delete sockets[socket.index];
delete socketObjects[channel];
}
}
sockets = swap(sockets);
}
// www.RTCMultiConnection.org/docs/remove/
connection.remove = function (userid) {
if (rtcMultiSession.requestsFrom && rtcMultiSession.requestsFrom[userid]) delete rtcMultiSession.requestsFrom[userid];
if (connection.peers[userid]) {
if (connection.peers[userid].peer && connection.peers[userid].peer.connection) {
connection.peers[userid].peer.connection.close();
connection.peers[userid].peer.connection = null;
}
delete connection.peers[userid];
}
if (participants[userid]) {
delete participants[userid];
}
for (var stream in connection.streams) {
stream = connection.streams[stream];
if (stream.userid == userid) {
connection.onstreamended(stream.streamObject);
if (stream.stop) stream.stop();
delete connection.streams[stream];
}
}
if (socketObjects[userid]) {
delete socketObjects[userid];
}
};
// www.RTCMultiConnection.org/docs/refresh/
connection.refresh = function () {
participants = [];
connection.isAcceptNewSession = true;
connection.busy = false;
// to stop/remove self streams
for (var i = 0; i < connection.attachStreams.length; i++) {
stopTracks(connection.attachStreams[i]);
}
connection.attachStreams = [];
// to allow capturing of identical streams
currentUserMediaRequest = {
streams: [],
mutex: false,
queueRequests: []
};
rtcMultiSession.isOwnerLeaving = true;
connection.isInitiator = false;
};
// www.RTCMultiConnection.org/docs/reject/
connection.reject = function (userid) {
if (typeof userid != 'string') userid = userid.userid;
defaultSocket.send({
rejectedRequestOf: userid,
userid: connection.userid,
extra: connection.extra || {}
});
};
window.addEventListener('beforeunload', function () {
clearSession();
}, false);
window.addEventListener('keyup', function (e) {
if (e.keyCode == 116)
clearSession();
}, false);
function initDefaultSocket() {
defaultSocket = connection.openSignalingChannel({
onmessage: function (response) {
if (response.userid == connection.userid) return;
if (response.sessionid && response.userid) {
if (!connection.stats.sessions[response.sessionid]) {
connection.stats.numberOfSessions++;
connection.stats.sessions[response.sessionid] = response;
}
}
if (connection.isAcceptNewSession && response.sessionid && response.userid) {
connection.session = response.session;
onNewSession(response);
}
if (response.newParticipant && !connection.isAcceptNewSession && rtcMultiSession.broadcasterid === response.userid) {
onNewParticipant(response);
}
if (getLength(participants) < connection.maxParticipantsAllowed && response.userid && response.targetUser == connection.userid && response.participant && !participants[response.userid]) {
acceptRequest(response);
}
if (response.userType && response.userType != connection.userType) {
if (!connection.busy) {
if (response.userType == 'admin') {
if (connection.onAdmin) connection.onAdmin(response);
else connection.accept(response.userid);
}
if (response.userType == 'guest') {
if (connection.onGuest) connection.onGuest(response);
else connection.accept(response.userid);
}
} else {
if (response.userType != connection.userType) {
connection.reject(response.userid);
}
}
}
if (response.acceptedRequestOf == connection.userid) {
if (connection.onstats) connection.onstats('accepted', response);
}
if (response.rejectedRequestOf == connection.userid) {
if (connection.onstats) connection.onstats(connection.userType ? 'busy' : 'rejected', response);
sendRequest();
}
if (response.customMessage) {
if (response.message.drop) {
connection.ondrop(response.userid);
connection.attachStreams = [];
// "drop" should detach all local streams
for (var stream in connection.streams) {
if (connection._skip.indexOf(stream) == -1) {
stream = connection.streams[stream];
if (stream.type == 'local') {
connection.detachStreams.push(stream.streamid);
connection.onstreamended(stream.streamObject);
} else connection.onstreamended(stream.streamObject);
}
}
if (response.message.renegotiate) {
// renegotiate; so "peer.removeStream" happens.
connection.addStream();
}
} else if (connection.onCustomMessage) {
connection.onCustomMessage(response.message);
}
}
if (response.joinUsers) {
for (var user in response.joinUsers) {
if (!participants[response.joinUsers[user]]) {
onNewParticipant({
sessionid: connection.sessionid,
newParticipant: response.joinUsers[user],
userid: connection.userid,
extra: connection.extra,
interconnect: true
});
}
}
}
},
callback: function (socket) {
if (socket) defaultSocket = socket;
if (connection.userType) sendRequest(socket || defaultSocket);
if (onSignalingReady) onSignalingReady();
},
onopen: function (socket) {
if (socket) defaultSocket = socket;
if (connection.userType) sendRequest(socket || defaultSocket);
if (onSignalingReady) onSignalingReady();
}
});
}
var defaultSocket;
initDefaultSocket();
function sendRequest(socket) {
if (!socket) {
return setTimeout(function () {
sendRequest(defaultSocket);
}, 1000);
}
socket.send({
userType: connection.userType,
userid: connection.userid,
extra: connection.extra || {}
});
}
function setDirections() {
var userMaxParticipantsAllowed = 0;
// if user has set a custom max participant setting, remember it
if (connection.maxParticipantsAllowed != 256) {
userMaxParticipantsAllowed = connection.maxParticipantsAllowed;
}
if (connection.direction == 'one-way') connection.session.oneway = true;
if (connection.direction == 'one-to-one') connection.maxParticipantsAllowed = 1;
if (connection.direction == 'one-to-many') connection.session.broadcast = true;
if (connection.direction == 'many-to-many') {
if (!connection.maxParticipantsAllowed || connection.maxParticipantsAllowed == 1) {
connection.maxParticipantsAllowed = 256;
}
}
// if user has set a custom max participant setting, set it back
if (userMaxParticipantsAllowed && connection.maxParticipantsAllowed != 1) {
connection.maxParticipantsAllowed = userMaxParticipantsAllowed;
}
}
// open new session
this.initSession = function (args) {
rtcMultiSession.isOwnerLeaving = false;
setDirections();
participants = {};
rtcMultiSession.isOwnerLeaving = false;
if (typeof args.transmitRoomOnce != 'undefined') {
connection.transmitRoomOnce = args.transmitRoomOnce;
}
function transmit() {
if (getLength(participants) < connection.maxParticipantsAllowed && !rtcMultiSession.isOwnerLeaving) {
defaultSocket && defaultSocket.send(args.sessionDescription);
}
if (!connection.transmitRoomOnce && !rtcMultiSession.isOwnerLeaving)
setTimeout(transmit, connection.interval || 3000);
}
// todo: test and fix next line.
if (!args.dontTransmit /* || connection.transmitRoomOnce */) transmit();
};
// join existing session
this.joinSession = function (_config) {
if (!defaultSocket)
return setTimeout(function () {
warn('Default-Socket is not yet initialized.');
rtcMultiSession.joinSession(_config);
}, 1000);
_config = _config || {};
participants = {};
connection.session = _config.session || {};
rtcMultiSession.broadcasterid = _config.userid;
if (_config.sessionid) {
// used later to prevent external rooms messages to be used by this user!
connection.sessionid = _config.sessionid;
}
connection.isAcceptNewSession = false;
var channel = getRandomString();
newPrivateSocket({
channel: channel,
extra: _config.extra || {},
userid: _config.userid
});
defaultSocket.send({
participant: true,
userid: connection.userid,
channel: channel,
targetUser: _config.userid,
extra: connection.extra,
session: connection.session
});
};
// send file/data or text message
this.send = function (message, _channel) {
message = JSON.stringify({
extra: connection.extra,
userid: connection.userid,
data: message
});
if (_channel) {
if (_channel.readyState == 'open') {
_channel.send(message);
}
return;
}
for (var dataChannel in connection.channels) {
var channel = connection.channels[dataChannel].channel;
if (channel.readyState == 'open') {
channel.send(message);
}
}
};
// leave session
this.leave = function (userid) {
clearSession(userid);
if (connection.isInitiator) {
rtcMultiSession.isOwnerLeaving = true;
connection.isInitiator = false;
}
// to stop/remove self streams
for (var i = 0; i < connection.attachStreams.length; i++) {
stopTracks(connection.attachStreams[i]);
}
connection.attachStreams = [];
// to allow capturing of identical streams
currentUserMediaRequest = {
streams: [],
mutex: false,
queueRequests: []
};
if (!userid) {
connection.isAcceptNewSession = true;
}
connection.busy = false;
};
// renegotiate new stream
this.addStream = function (e) {
var session = e.renegotiate;
connection.renegotiatedSessions.push({
session: e.renegotiate,
stream: e.stream
});
if (e.socket) {
addStream(connection.peers[e.socket.userid]);
} else {
for (var peer in connection.peers) {
addStream(connection.peers[peer]);
}
}
function addStream(_peer) {
var socket = _peer.socket;
if (!socket) {
warn(_peer, 'doesn\'t has socket.');
return;
}
updateSocketForLocalStreams(socket);
if (!_peer || !_peer.peer) {
throw 'No peer to renegotiate.';
}
var peer = _peer.peer;
if (e.stream) {
peer.attachStreams = [e.stream];
}
// detaching old streams
detachMediaStream(connection.detachStreams, peer.connection);
if (e.stream && (session.audio || session.video || session.screen)) {
// removeStream is not yet implemented in Firefox
// if(isFirefox) peer.connection.removeStream(e.stream);
if (isChrome || (isFirefox && !peer.connection.getLocalStreams().length)) {
peer.connection.addStream(e.stream);
}
}
// if isFirefox, try to create peer connection again!
if (isFirefox) {
return _peer.redial();
}
peer.recreateOffer(session, function (sdp, streaminfo) {
sendsdp({
sdp: sdp,
socket: socket,
renegotiate: session,
labels: connection.detachStreams,
streaminfo: streaminfo
});
connection.detachStreams = [];
});
}
};
// www.RTCMultiConnection.org/docs/request/
connection.request = function (userid, extra) {
if (connection.direction === 'many-to-many') connection.busy = true;
connection.captureUserMedia(function () {
// open private socket that will be used to receive offer-sdp
newPrivateSocket({
channel: connection.userid,
extra: extra || {},
userid: userid
});
// ask other user to create offer-sdp
defaultSocket.send({
participant: true,
userid: connection.userid,
extra: connection.extra || {},
targetUser: userid
});
});
};
function acceptRequest(response) {
if (!rtcMultiSession.requestsFrom) rtcMultiSession.requestsFrom = {};
if (connection.busy || rtcMultiSession.requestsFrom[response.userid]) return;
var obj = {
userid: response.userid,
extra: response.extra,
channel: response.channel || response.userid,
session: response.session || connection.session
};
rtcMultiSession.requestsFrom[response.userid] = obj;
// www.RTCMultiConnection.org/docs/onRequest/
if (connection.onRequest && (!connection.userType && connection.isInitiator)) {
connection.onRequest(obj);
} else _accept(obj);
}
function _accept(e) {
if (connection.userType) {
if (connection.direction === 'many-to-many') connection.busy = true;
defaultSocket.send({
acceptedRequestOf: e.userid,
userid: connection.userid,
extra: connection.extra || {}
});
}
participants[e.userid] = e.userid;
newPrivateSocket({
isofferer: true,
userid: e.userid,
channel: e.channel,
extra: e.extra || {},
session: e.session || connection.session
});
}
// www.RTCMultiConnection.org/docs/sendMessage/
connection.sendCustomMessage = function (message) {
if (!defaultSocket) {
return setTimeout(function () {
connection.sendMessage(message);
}, 1000);
}
defaultSocket.send({
userid: connection.userid,
customMessage: true,
message: message
});
};
// www.RTCMultiConnection.org/docs/accept/
connection.accept = function (e) {
// for backward compatibility
if (arguments.length > 1 && typeof arguments[0] == 'string') {
e = {};
if (arguments[0]) e.userid = arguments[0];
if (arguments[1]) e.extra = arguments[1];
if (arguments[2]) e.channel = arguments[2];
}
connection.captureUserMedia(function () {
_accept(e);
});
};
}
var RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
var RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription;
var RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate;
function PeerConnection() {
return {
create: function (type, options) {
merge(this, options);
var self = this;
this.type = type;
this.init();
this.attachMediaStreams();
if (isData(this.session) && isFirefox) {
navigator.mozGetUserMedia({
audio: true,
fake: true
}, function (stream) {
self.connection.addStream(stream);
if (type == 'offer') {
self.createDataChannel();
}
self.getLocalDescription(type);
if (type == 'answer') {
self.createDataChannel();
}
}, this.onMediaError);
}
if (!isData(this.session) && isFirefox) {
if (this.session.data && type == 'offer') {
this.createDataChannel();
}
this.getLocalDescription(type);
if (this.session.data && type == 'answer') {
this.createDataChannel();
}
}
isChrome && self.getLocalDescription(type);
return this;
},
getLocalDescription: function (type) {
log('peer type is', type);
if (type == 'answer') {
this.setRemoteDescription(this.offerDescription);
}
var self = this;
this.connection[type == 'offer' ? 'createOffer' : 'createAnswer'](function (sessionDescription) {
sessionDescription.sdp = self.serializeSdp(sessionDescription.sdp);
self.connection.setLocalDescription(sessionDescription);
self.onSessionDescription(sessionDescription, self.streaminfo);
}, this.onSdpError, this.constraints);
},
serializeSdp: function (sdp) {
sdp = this.setBandwidth(sdp);
if (this.holdMLine == 'both') {
if (this.hold) {
this.prevSDP = sdp;
sdp = sdp.replace(/sendonly|recvonly|sendrecv/g, 'inactive');
} else if (this.prevSDP) {
// sdp = sdp.replace(/inactive/g, 'sendrecv');
sdp = this.prevSDP;
}
} else if (this.holdMLine == 'audio' || this.holdMLine == 'video') {
sdp = sdp.split('m=');
var audio = '';
var video = '';
if (sdp[1] && sdp[1].indexOf('audio') == 0) {
audio = 'm=' + sdp[1];
}
if (sdp[2] && sdp[2].indexOf('audio') == 0) {
audio = 'm=' + sdp[2];
}
if (sdp[1] && sdp[1].indexOf('video') == 0) {
video = 'm=' + sdp[1];
}
if (sdp[2] && sdp[2].indexOf('video') == 0) {
video = 'm=' + sdp[2];
}
if (this.holdMLine == 'audio') {
if (this.hold) {
this.prevSDP = sdp[0] + audio + video;
sdp = sdp[0] + audio.replace(/sendonly|recvonly|sendrecv/g, 'inactive') + video;
} else if (this.prevSDP) {
// sdp = sdp[0] + audio.replace(/inactive/g, 'sendrecv') + video;
sdp = this.prevSDP;
}
}
if (this.holdMLine == 'video') {
if (this.hold) {
this.prevSDP = sdp[0] + audio + video;
sdp = sdp[0] + audio + video.replace(/sendonly|recvonly|sendrecv/g, 'inactive');
} else if (this.prevSDP) {
// sdp = sdp[0] + audio + video.replace(/inactive/g, 'sendrecv');
sdp = this.prevSDP;
}
}
}
return sdp;
},
init: function () {
this.setConstraints();
this.connection = new RTCPeerConnection(this.iceServers, this.optionalArgument);
if (this.session.data && isChrome) {
this.createDataChannel();
}
this.connection.onicecandidate = function (event) {
if (event.candidate) {
self.onicecandidate(event.candidate);
}
};
this.connection.onaddstream = function (e) {
self.onaddstream(e.stream, self.session);
log('onaddstream', toStr(e.stream));
};
this.connection.onremovestream = function (e) {
self.onremovestream(e.stream);
};
this.connection.onsignalingstatechange = function () {
self.connection && self.oniceconnectionstatechange({
iceConnectionState: self.connection.iceConnectionState,
iceGatheringState: self.connection.iceGatheringState,
signalingState: self.connection.signalingState
});
};
this.connection.oniceconnectionstatechange = function () {
self.connection && self.oniceconnectionstatechange({
iceConnectionState: self.connection.iceConnectionState,
iceGatheringState: self.connection.iceGatheringState,
signalingState: self.connection.signalingState
});
};
var self = this;
},
setBandwidth: function (sdp) {
// sdp.replace( /a=sendrecv\r\n/g , 'a=sendrecv\r\nb=AS:50\r\n');
if (isMobileDevice || isFirefox || !this.bandwidth) return sdp;
var bandwidth = this.bandwidth;
// if screen; must use at least 300kbs
if (bandwidth.screen && this.session.screen && isEmpty(bandwidth)) {
sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, '');
sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + bandwidth.screen + '\r\n');
}
// remove existing bandwidth lines
if (bandwidth.audio || bandwidth.video || bandwidth.data) {
sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, '');
}
if (bandwidth.audio) {
sdp = sdp.replace(/a=mid:audio\r\n/g, 'a=mid:audio\r\nb=AS:' + bandwidth.audio + '\r\n');
}
if (bandwidth.video) {
sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + (this.session.screen ? '300' : bandwidth.video) + '\r\n');
}
if (bandwidth.data && !this.preferSCTP) {
sdp = sdp.replace(/a=mid:data\r\n/g, 'a=mid:data\r\nb=AS:' + bandwidth.data + '\r\n');
}
return sdp;
},
setConstraints: function () {
this.constraints = {
optional: this.sdpConstraints.optional || [],
mandatory: this.sdpConstraints.mandatory || {
OfferToReceiveAudio: !!this.session.audio,
OfferToReceiveVideo: !!this.session.video || !!this.session.screen
}
};
// workaround for older firefox
if (this.session.data && isFirefox && this.constraints.mandatory) {
this.constraints.mandatory.OfferToReceiveAudio = true;
}
log('sdp-constraints', toStr(this.constraints.mandatory));
this.optionalArgument = {
optional: this.optionalArgument.optional || [{
DtlsSrtpKeyAgreement: true
}],
mandatory: this.optionalArgument.mandatory || {}
};
if (isChrome && chromeVersion >= 32 && !isNodeWebkit) {
this.optionalArgument.optional.push({
googIPv6: true
});
this.optionalArgument.optional.push({ googDscp: true });
}
if (!this.preferSCTP) {
this.optionalArgument.optional.push({
RtpDataChannels: true
});
}
log('optional-argument', toStr(this.optionalArgument.optional));
this.iceServers = {
iceServers: this.iceServers
};
log('ice-servers', toStr(this.iceServers.iceServers));
},
onSdpError: function (e) {
var message = toStr(e);
if (message && message.indexOf('RTP/SAVPF Expects at least 4 fields') != -1) {
message = 'It seems that you are trying to interop RTP-datachannels with SCTP. It is not supported!';
}
error('onSdpError:', message);
},
onMediaError: function (err) {
error(toStr(err));
},
setRemoteDescription: function (sessionDescription) {
if (!sessionDescription) throw 'Remote session description should NOT be NULL.';
log('setting remote description', sessionDescription.type, sessionDescription.sdp);
this.connection.setRemoteDescription(
new RTCSessionDescription(sessionDescription)
);
},
addIceCandidate: function (candidate) {
var iceCandidate = new RTCIceCandidate({
sdpMLineIndex: candidate.sdpMLineIndex,
candidate: candidate.candidate
});
if (isNodeWebkit) {
this.connection.addIceCandidate(iceCandidate);
} else {
// landed in chrome M33
// node-webkit doesn't support this format yet!
this.connection.addIceCandidate(iceCandidate, this.onIceSuccess, this.onIceFailure);
}
},
onIceSuccess: function () {
log('ice success', toStr(arguments));
},
onIceFailure: function () {
warn('ice failure', toStr(arguments));
},
createDataChannel: function (channelIdentifier) {
if (!this.channels) this.channels = [];
// protocol: 'text/chat', preset: true, stream: 16
// maxRetransmits:0 && ordered:false
var dataChannelDict = {};
if (this.dataChannelDict) dataChannelDict = this.dataChannelDict;
if (isChrome && !this.preferSCTP) {
dataChannelDict.reliable = false; // Deprecated!
}
log('dataChannelDict', toStr(dataChannelDict));
if (isFirefox) {
this.connection.onconnection = function () {
self.socket.send({
userid: self.selfUserid,
isCreateDataChannel: true
});
};
}
if (this.type == 'answer' || isFirefox) {
this.connection.ondatachannel = function (event) {
self.setChannelEvents(event.channel);
};
}
if ((isChrome && this.type == 'offer') || isFirefox) {
this.setChannelEvents(
this.connection.createDataChannel(channelIdentifier || 'channel', dataChannelDict)
);
}
var self = this;
},
setChannelEvents: function (channel) {
var self = this;
channel.onmessage = function (event) {
self.onmessage(event.data);
};
var numberOfTimes = 0;
channel.onopen = function () {
channel.push = channel.send;
channel.send = function (data) {
if (channel.readyState != 'open') {
numberOfTimes++;
return setTimeout(function () {
if (numberOfTimes < 20) {
channel.send(data);
} else throw 'Number of times exceeded to wait for WebRTC data connection to be opened.';
}, 1000);
}
try {
channel.push(data);
} catch (e) {
numberOfTimes++;
warn('Data transmission failed. Re-transmitting..', numberOfTimes, toStr(e));
if (numberOfTimes >= 20) throw 'Number of times exceeded to resend data packets over WebRTC data channels.';
setTimeout(function () {
channel.send(data);
}, 100);
}
};
self.onopen(channel);
};
channel.onerror = function (event) {
self.onerror(event);
};
channel.onclose = function (event) {
self.onclose(event);
};
this.channels.push(channel);
},
attachMediaStreams: function () {
var streams = this.attachStreams;
for (var i = 0; i < streams.length; i++) {
log('attaching stream:', streams[i].streamid);
this.connection.addStream(streams[i]);
}
this.getStreamInfo();
},
getStreamInfo: function () {
this.streaminfo = '';
var streams = this.attachStreams;
for (var i = 0; i < streams.length; i++) {
if (i == 0) {
this.streaminfo = streams[i].streamid;
} else {
this.streaminfo += '----' + streams[i].streamid;
}
}
this.attachStreams = [];
},
recreateOffer: function (renegotiate, callback) {
// if(isFirefox) this.create(this.type, this);
log('recreating offer');
this.type = 'offer';
this.renegotiate = true;
this.session = renegotiate;
this.setConstraints();
this.onSessionDescription = callback;
this.getStreamInfo();
// one can renegotiate data connection in existing audio/video/screen connection!
if (this.session.data && isChrome) {
this.createDataChannel();
}
this.getLocalDescription('offer');
},
recreateAnswer: function (sdp, session, callback) {
// if(isFirefox) this.create(this.type, this);
log('recreating answer');
this.type = 'answer';
this.renegotiate = true;
this.session = session;
this.setConstraints();
this.onSessionDescription = callback;
this.offerDescription = sdp;
this.getStreamInfo();
// one can renegotiate data connection in existing audio/video/screen connection!
if (this.session.data && isChrome) {
this.createDataChannel();
}
this.getLocalDescription('answer');
}
};
}
var video_constraints = {
mandatory: {},
optional: []
};
/* by @FreCap pull request #41 */
var currentUserMediaRequest = {
streams: [],
mutex: false,
queueRequests: []
};
function getUserMedia(options) {
if (currentUserMediaRequest.mutex === true) {
currentUserMediaRequest.queueRequests.push(options);
return;
}
currentUserMediaRequest.mutex = true;
// tools.ietf.org/html/draft-alvestrand-constraints-resolution-00
var mediaConstraints = options.mediaConstraints || {};
var n = navigator,
hints = options.constraints || {
audio: true,
video: video_constraints
};
if (hints.video == true) hints.video = video_constraints;
// connection.mediaConstraints.audio = false;
if (typeof mediaConstraints.audio != 'undefined') {
hints.audio = mediaConstraints.audio;
}
// connection.media.min(320,180);
// connection.media.max(1920,1080);
var media = options.media;
if (isChrome) {
var mandatory = {
minWidth: media.minWidth,
minHeight: media.minHeight,
maxWidth: media.maxWidth,
maxHeight: media.maxHeight,
minAspectRatio: media.minAspectRatio
};
// code.google.com/p/chromium/issues/detail?id=143631#c9
var allowed = ['1920:1080', '1280:720', '960:720', '640:360', '640:480', '320:240', '320:180'];
if (allowed.indexOf(mandatory.minWidth + ':' + mandatory.minHeight) == -1 ||
allowed.indexOf(mandatory.maxWidth + ':' + mandatory.maxHeight) == -1) {
error('The min/max width/height constraints you passed "seems" NOT supported.', toStr(mandatory));
}
if (mandatory.minWidth > mandatory.maxWidth || mandatory.minHeight > mandatory.maxHeight) {
error('Minimum value must not exceed maximum value.', toStr(mandatory));
}
if (mandatory.minWidth >= 1280 && mandatory.minHeight >= 720) {
warn('Enjoy HD video! min/' + mandatory.minWidth + ':' + mandatory.minHeight + ', max/' + mandatory.maxWidth + ':' + mandatory.maxHeight);
}
hints.video.mandatory = merge(hints.video.mandatory, mandatory);
}
if (mediaConstraints.mandatory)
hints.video.mandatory = merge(hints.video.mandatory, mediaConstraints.mandatory);
// mediaConstraints.optional.bandwidth = 1638400;
if (mediaConstraints.optional)
hints.video.optional[0] = merge({}, mediaConstraints.optional);
log('media hints:', toStr(hints));
// easy way to match
var idInstance = JSON.stringify(hints);
function streaming(stream, returnBack, streamid) {
if (!streamid) streamid = getRandomString();
var video = options.video;
if (video) {
video[isFirefox ? 'mozSrcObject' : 'src'] = isFirefox ? stream : window.webkitURL.createObjectURL(stream);
video.play();
}
options.onsuccess(stream, returnBack, idInstance, streamid);
currentUserMediaRequest.streams[idInstance] = {
stream: stream,
streamid: streamid
};
currentUserMediaRequest.mutex = false;
if (currentUserMediaRequest.queueRequests.length)
getUserMedia(currentUserMediaRequest.queueRequests.shift());
}
if (currentUserMediaRequest.streams[idInstance]) {
streaming(currentUserMediaRequest.streams[idInstance].stream, true, currentUserMediaRequest.streams[idInstance].streamid);
} else {
n.getMedia = n.webkitGetUserMedia || n.mozGetUserMedia;
n.getMedia(hints, streaming, function (err) {
if (options.onerror) options.onerror(err, idInstance);
else error(toStr(err));
});
}
}
var FileSender = {
send: function (config) {
var connection = config.connection;
var channel = config.channel;
var privateChannel = config._channel;
var file = config.file;
if (!config.file) {
error('You must attach/select a file.');
return;
}
// max chunk sending limit on chrome is 64k
// max chunk receiving limit on firefox is 16k
var packetSize = (!!navigator.mozGetUserMedia || connection.preferSCTP) ? 15 * 1000 : 1 * 1000;
if (connection.chunkSize) {
packetSize = connection.chunkSize;
}
var textToTransfer = '';
var numberOfPackets = 0;
var packets = 0;
file.uuid = getRandomString();
function processInWebWorker() {
var blob = URL.createObjectURL(new Blob(['function readFile(_file) {postMessage(new FileReaderSync().readAsDataURL(_file));};this.onmessage = function (e) {readFile(e.data);}'], {
type: 'application/javascript'
}));
var worker = new Worker(blob);
URL.revokeObjectURL(blob);
return worker;
}
if (!!window.Worker && !isMobileDevice) {
var webWorker = processInWebWorker();
webWorker.onmessage = function (event) {
onReadAsDataURL(event.data);
};
webWorker.postMessage(file);
} else {
var reader = new FileReader();
reader.onload = function (e) {
onReadAsDataURL(e.target.result);
};
reader.readAsDataURL(file);
}
function onReadAsDataURL(dataURL, text) {
var data = {
type: 'file',
uuid: file.uuid,
maxChunks: numberOfPackets,
currentPosition: numberOfPackets - packets,
name: file.name,
fileType: file.type,
size: file.size,
userid: connection.userid,
extra: connection.extra
};
if (dataURL) {
text = dataURL;
numberOfPackets = packets = data.packets = parseInt(text.length / packetSize);
file.maxChunks = data.maxChunks = numberOfPackets;
data.currentPosition = numberOfPackets - packets;
file.userid = connection.userid;
file.extra = connection.extra;
file.sending = true;
connection.onFileStart(file);
}
connection.onFileProgress({
remaining: packets--,
length: numberOfPackets,
sent: numberOfPackets - packets,
maxChunks: numberOfPackets,
uuid: file.uuid,
currentPosition: numberOfPackets - packets,
sending: true
}, file.uuid);
if (text.length > packetSize) data.message = text.slice(0, packetSize);
else {
data.message = text;
data.last = true;
data.name = file.name;
file.url = URL.createObjectURL(file);
file.userid = connection.userid;
file.extra = connection.extra;
file.sending = true;
connection.onFileEnd(file);
}
channel.send(data, privateChannel);
textToTransfer = text.slice(data.message.length);
if (textToTransfer.length) {
setTimeout(function () {
onReadAsDataURL(null, textToTransfer);
}, connection.chunkInterval || 100);
}
}
}
};
function FileReceiver(connection) {
var content = {},
packets = {},
numberOfPackets = {};
function receive(data) {
var uuid = data.uuid;
if (typeof data.packets !== 'undefined') {
numberOfPackets[uuid] = packets[uuid] = parseInt(data.packets);
data.sending = false;
connection.onFileStart(data);
}
connection.onFileProgress({
remaining: packets[uuid]--,
length: numberOfPackets[uuid],
received: numberOfPackets[uuid] - packets[uuid],
maxChunks: numberOfPackets[uuid],
uuid: uuid,
currentPosition: numberOfPackets[uuid] - packets[uuid],
sending: false
}, uuid);
if (!content[uuid]) content[uuid] = [];
content[uuid].push(data.message);
if (data.last) {
var dataURL = content[uuid].join('');
FileConverter.DataURLToBlob(dataURL, data.fileType, function (blob) {
blob.uuid = uuid;
blob.name = data.name;
blob.type = data.fileType;
blob.url = (window.URL || window.webkitURL).createObjectURL(blob);
blob.sending = false;
blob.userid = data.userid || connection.userid;
blob.extra = data.extra || connection.extra;
connection.onFileEnd(blob);
if (connection.autoSaveToDisk) {
FileSaver.SaveToDisk(blob.url, data.name);
}
delete content[uuid];
});
}
}
return {
receive: receive
};
}
var FileSaver = {
SaveToDisk: function (fileUrl, fileName) {
var hyperlink = document.createElement('a');
hyperlink.href = fileUrl;
hyperlink.target = '_blank';
hyperlink.download = fileName || fileUrl;
var mouseEvent = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
hyperlink.dispatchEvent(mouseEvent);
// (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href);
}
};
var FileConverter = {
DataURLToBlob: function (dataURL, fileType, callback) {
function processInWebWorker() {
var blob = URL.createObjectURL(new Blob(['function getBlob(_dataURL, _fileType) {var binary = atob(_dataURL.substr(_dataURL.indexOf(",") + 1)),i = binary.length,view = new Uint8Array(i);while (i--) {view[i] = binary.charCodeAt(i);};postMessage(new Blob([view], {type: _fileType}));};this.onmessage = function (e) {var data = JSON.parse(e.data); getBlob(data.dataURL, data.fileType);}'], {
type: 'application/javascript'
}));
var worker = new Worker(blob);
URL.revokeObjectURL(blob);
return worker;
}
if (!!window.Worker && !isMobileDevice) {
var webWorker = processInWebWorker();
webWorker.onmessage = function (event) {
callback(event.data);
};
webWorker.postMessage(JSON.stringify({
dataURL: dataURL,
fileType: fileType
}));
} else {
var binary = atob(dataURL.substr(dataURL.indexOf(',') + 1)),
i = binary.length,
view = new Uint8Array(i);
while (i--) {
view[i] = binary.charCodeAt(i);
}
callback(new Blob([view]));
}
}
};
var TextSender = {
send: function (config) {
var connection = config.connection;
var channel = config.channel,
_channel = config._channel,
initialText = config.text,
packetSize = connection.chunkSize || 1000,
textToTransfer = '',
isobject = false;
if (typeof initialText !== 'string') {
isobject = true;
initialText = JSON.stringify(initialText);
}
// uuid is used to uniquely identify sending instance
var uuid = getRandomString();
var sendingTime = new Date().getTime();
sendText(initialText);
function sendText(textMessage, text) {
var data = {
type: 'text',
uuid: uuid,
sendingTime: sendingTime
};
if (textMessage) {
text = textMessage;
data.packets = parseInt(text.length / packetSize);
}
if (text.length > packetSize)
data.message = text.slice(0, packetSize);
else {
data.message = text;
data.last = true;
data.isobject = isobject;
}
channel.send(data, _channel);
textToTransfer = text.slice(data.message.length);
if (textToTransfer.length) {
setTimeout(function () {
sendText(null, textToTransfer);
}, connection.chunkInterval || 100);
}
}
}
};
// _______________
// TextReceiver.js
function TextReceiver(connection) {
var content = {};
function receive(data, userid, extra) {
// uuid is used to uniquely identify sending instance
var uuid = data.uuid;
if (!content[uuid]) content[uuid] = [];
content[uuid].push(data.message);
if (data.last) {
var message = content[uuid].join('');
if (data.isobject) message = JSON.parse(message);
// latency detection
var receivingTime = new Date().getTime();
var latency = receivingTime - data.sendingTime;
var e = {
data: message,
userid: userid,
extra: extra,
latency: latency
};
if (message.preRecordedMediaChunk) {
if (!connection.preRecordedMedias[message.streamerid]) {
connection.shareMediaFile(null, null, message.streamerid);
}
connection.preRecordedMedias[message.streamerid].onData(message.chunk);
} else if (connection.autoTranslateText) {
e.original = e.data;
connection.Translator.TranslateText(e.data, function (translatedText) {
e.data = translatedText;
connection.onmessage(e);
});
} else if (message.isPartOfScreen) {
connection.onpartofscreen(message);
} else connection.onmessage(e);
delete content[uuid];
}
}
return {
receive: receive
};
}
// Sound meter is used to detect speaker
// SoundMeter.js copyright goes to someone else!
function SoundMeter(config) {
var connection = config.connection;
var context = config.context;
this.context = context;
this.volume = 0.0;
this.slow_volume = 0.0;
this.clip = 0.0;
// Legal values are (256, 512, 1024, 2048, 4096, 8192, 16384)
this.script = context.createScriptProcessor(256, 1, 1);
that = this;
this.script.onaudioprocess = function (event) {
var input = event.inputBuffer.getChannelData(0);
var i;
var sum = 0.0;
var clipcount = 0;
for (i = 0; i < input.length; ++i) {
sum += input[i] * input[i];
if (Math.abs(input[i]) > 0.99) {
clipcount += 1;
}
}
that.volume = Math.sqrt(sum / input.length);
var volume = that.volume.toFixed(2);
if (volume >= .1 && connection.onspeaking) {
connection.onspeaking(config.event);
}
if (volume < .1 && connection.onsilence) {
connection.onsilence(config.event);
}
};
}
SoundMeter.prototype.connectToSource = function (stream) {
this.mic = this.context.createMediaStreamSource(stream);
this.mic.connect(this.script);
this.script.connect(this.context.destination);
};
SoundMeter.prototype.stop = function () {
this.mic.disconnect();
this.script.disconnect();
};
var isChrome = !!navigator.webkitGetUserMedia;
var isFirefox = !!navigator.mozGetUserMedia;
var isMobileDevice = navigator.userAgent.match(/Android|iPhone|iPad|iPod|BlackBerry|IEMobile/i);
// detect node-webkit
var isNodeWebkit = window.process && (typeof window.process == 'object') && window.process.versions && window.process.versions['node-webkit'];
window.MediaStream = window.MediaStream || window.webkitMediaStream;
window.AudioContext = window.AudioContext || window.webkitAudioContext;
function getRandomString() {
return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, '');
}
var chromeVersion = !!navigator.mozGetUserMedia ? 0 : parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2]);
function isData(session) {
return !session.audio && !session.video && !session.screen && session.data;
}
function isEmpty(session) {
var length = 0;
for (var s in session) {
length++;
}
return length == 0;
}
function swap(arr) {
var swapped = [],
length = arr.length;
for (var i = 0; i < length; i++)
if (arr[i] && arr[i] !== true)
swapped.push(arr[i]);
return swapped;
}
var log = console.log.bind(console);
var error = console.error.bind(console);
var warn = console.warn.bind(console);
function toStr(obj) {
return JSON.stringify(obj, function (key, value) {
if (value && value.sdp) {
log(value.sdp.type, '\t', value.sdp.sdp);
return '';
} else return value;
}, '\t');
}
function getLength(obj) {
var length = 0;
for (var o in obj)
if (o) length++;
return length;
}
// Get HTMLAudioElement/HTMLVideoElement accordingly
function createMediaElement(stream, session) {
var isAudio = session.audio && !session.video && !session.screen;
if (isChrome && stream.getAudioTracks && stream.getVideoTracks) {
isAudio = stream.getAudioTracks().length && !stream.getVideoTracks().length;
}
var mediaElement = document.createElement(isAudio ? 'audio' : 'video');
// "mozSrcObject" is always preferred over "src"!!
mediaElement[isFirefox ? 'mozSrcObject' : 'src'] = isFirefox ? stream : window.webkitURL.createObjectURL(stream);
mediaElement.controls = true;
mediaElement.autoplay = !!session.remote;
mediaElement.muted = session.remote ? false : true;
mediaElement.play();
return mediaElement;
}
function merge(mergein, mergeto) {
if (!mergein) mergein = {};
if (!mergeto) return mergein;
for (var item in mergeto) {
mergein[item] = mergeto[item];
}
return mergein;
}
function loadScript(src, onload) {
var script = document.createElement('script');
script.src = src;
if (onload) script.onload = onload;
document.documentElement.appendChild(script);
}
function muteOrUnmute(e) {
var stream = e.stream,
root = e.root,
session = e.session || {},
enabled = e.enabled;
if (!session.audio && !session.video) {
if (typeof session != 'string') {
session = merge(session, {
audio: true,
video: true
});
} else {
session = {
audio: true,
video: true
};
}
}
// implementation from #68
if (session.type) {
if (session.type == 'remote' && root.type != 'remote') return;
if (session.type == 'local' && root.type != 'local') return;
}
log(enabled ? 'mute' : 'unmute', 'session', session);
// enable/disable audio/video tracks
if (session.audio) {
var audioTracks = stream.getAudioTracks()[0];
if (audioTracks)
audioTracks.enabled = !enabled;
}
if (session.video) {
var videoTracks = stream.getVideoTracks()[0];
if (videoTracks)
videoTracks.enabled = !enabled;
}
root.sockets.forEach(function (socket) {
if (root.type == 'local')
socket.send({
userid: root.rtcMultiConnection.userid,
streamid: root.streamid,
mute: !!enabled,
unmute: !enabled,
session: session
});
if (root.type == 'remote')
socket.send({
userid: root.rtcMultiConnection.userid,
promptMuteUnmute: true,
streamid: root.streamid,
mute: !!enabled,
unmute: !enabled,
session: session
});
});
// According to issue #135, onmute/onumute must be fired for self
// "fakeObject" is used because we need to keep session for renegotiated streams;
// and MUST pass accurate session over "onstreamended" event.
var fakeObject = merge({}, root.streamObject);
fakeObject.session = session;
fakeObject.isAudio = session.audio && !session.video;
fakeObject.isVideo = (!session.audio && session.video) || (session.audio && session.video);
if (!!enabled) {
root.rtcMultiConnection.onmute(fakeObject);
}
if (!enabled) {
root.rtcMultiConnection.onunmute(fakeObject);
}
}
function stopTracks(mediaStream) {
// if getAudioTracks is not implemented
if ((!mediaStream.getAudioTracks || !mediaStream.getVideoTracks) && mediaStream.stop) {
mediaStream.stop();
return;
}
var fallback = false,
i;
// MediaStream.stop should be avoided. It still exist and works but
// it is removed from the spec and instead MediaStreamTrack.stop should be used
var audioTracks = mediaStream.getAudioTracks();
var videoTracks = mediaStream.getVideoTracks();
for (i = 0; i < audioTracks.length; i++) {
if (audioTracks[i].stop) {
// for chrome canary; which has "stop" method; however not functional yet!
try {
audioTracks[i].stop();
} catch (e) {
fallback = true;
continue;
}
} else {
fallback = true;
continue;
}
}
for (i = 0; i < videoTracks.length; i++) {
if (videoTracks[i].stop) {
// for chrome canary; which has "stop" method; however not functional yet!
try {
videoTracks[i].stop();
} catch (e) {
fallback = true;
continue;
}
} else {
fallback = true;
continue;
}
}
if (fallback && mediaStream.stop) mediaStream.stop();
}
// this object is used for pre-recorded media streaming!
function Streamer(connection) {
var prefix = !!navigator.webkitGetUserMedia ? '' : 'moz';
var self = this;
self.stream = streamPreRecordedMedia;
window.MediaSource = window.MediaSource || window.WebKitMediaSource;
if (!window.MediaSource) throw 'Chrome >=M28 (or Firefox with flag "media.mediasource.enabled=true") is mandatory to test this experiment.';
function streamPreRecordedMedia(file) {
if (!self.push) throw '<push> method is mandatory.';
var reader = new window.FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function (e) {
startStreaming(new window.Blob([new window.Uint8Array(e.target.result)]));
};
var sourceBuffer, mediaSource = new MediaSource();
mediaSource.addEventListener(prefix + 'sourceopen', function () {
sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vorbis,vp8"');
log('MediaSource readyState: <', this.readyState, '>');
}, false);
mediaSource.addEventListener(prefix + 'sourceended', function () {
log('MediaSource readyState: <', this.readyState, '>');
}, false);
function startStreaming(blob) {
if (!blob) return;
var size = blob.size,
startIndex = 0,
plus = 3000;
log('one chunk size: <', plus, '>');
function inner_streamer() {
reader = new window.FileReader();
reader.onload = function (e) {
self.push(new window.Uint8Array(e.target.result));
startIndex += plus;
if (startIndex <= size) {
setTimeout(inner_streamer, connection.chunkInterval || 100);
} else {
self.push({
end: true
});
}
};
reader.readAsArrayBuffer(blob.slice(startIndex, startIndex + plus));
}
inner_streamer();
}
startStreaming();
}
self.receive = receive;
function receive() {
var mediaSource = new MediaSource();
self.video.src = window.URL.createObjectURL(mediaSource);
mediaSource.addEventListener(prefix + 'sourceopen', function () {
self.receiver = mediaSource.addSourceBuffer('video/webm; codecs="vorbis,vp8"');
self.mediaSource = mediaSource;
log('MediaSource readyState: <', this.readyState, '>');
}, false);
mediaSource.addEventListener(prefix + 'sourceended', function () {
warn('MediaSource readyState: <', this.readyState, '>');
}, false);
}
this.append = function (data) {
var that = this;
if (!self.receiver)
return setTimeout(function () {
that.append(data);
});
try {
var uint8array = new window.Uint8Array(data);
self.receiver.appendBuffer(uint8array);
} catch (e) {
error('Pre-recorded media streaming:', e);
}
};
this.end = function () {
self.mediaSource.endOfStream();
};
}
function setDefaults(connection) {
// www.RTCMultiConnection.org/docs/onmessage/
connection.onmessage = function (e) {
log('onmessage', toStr(e));
};
// www.RTCMultiConnection.org/docs/onopen/
connection.onopen = function (e) {
log('Data connection is opened between you and', e.userid);
};
// www.RTCMultiConnection.org/docs/onerror/
connection.onerror = function (e) {
error(onerror, toStr(e));
};
// www.RTCMultiConnection.org/docs/onclose/
connection.onclose = function (e) {
warn('onclose', toStr(e));
};
var progressHelper = {};
// www.RTCMultiConnection.org/docs/body/
connection.body = document.body || document.documentElement;
// www.RTCMultiConnection.org/docs/autoSaveToDisk/
// to make sure file-saver dialog is not invoked.
connection.autoSaveToDisk = false;
// www.RTCMultiConnection.org/docs/onFileStart/
connection.onFileStart = function (file) {
var div = document.createElement('div');
div.title = file.name;
div.innerHTML = '<label>0%</label> <progress></progress>';
connection.body.insertBefore(div, connection.body.firstChild);
progressHelper[file.uuid] = {
div: div,
progress: div.querySelector('progress'),
label: div.querySelector('label')
};
progressHelper[file.uuid].progress.max = file.maxChunks;
};
// www.RTCMultiConnection.org/docs/onFileProgress/
connection.onFileProgress = function (chunk) {
var helper = progressHelper[chunk.uuid];
if (!helper) return;
helper.progress.value = chunk.currentPosition || chunk.maxChunks || helper.progress.max;
updateLabel(helper.progress, helper.label);
};
// www.RTCMultiConnection.org/docs/onFileEnd/
connection.onFileEnd = function (file) {
if (progressHelper[file.uuid]) progressHelper[file.uuid].div.innerHTML = '<a href="' + file.url + '" target="_blank" download="' + file.name + '">' + file.name + '</a>';
// for backward compatibility
if (connection.onFileSent || connection.onFileReceived) {
warn('Now, "autoSaveToDisk" is false. Read more here: http://www.RTCMultiConnection.org/docs/autoSaveToDisk/');
if (connection.onFileSent) connection.onFileSent(file, file.uuid);
if (connection.onFileReceived) connection.onFileReceived(file.name, file);
}
};
function updateLabel(progress, label) {
if (progress.position == -1) return;
var position = +progress.position.toFixed(2).split('.')[1] || 100;
label.innerHTML = position + '%';
}
// www.RTCMultiConnection.org/docs/dontAttachStream/
connection.dontAttachStream = false;
// www.RTCMultiConnection.org/docs/onstream/
connection.onstream = function (e) {
connection.body.insertBefore(e.mediaElement, connection.body.firstChild);
};
// www.RTCMultiConnection.org/docs/onstreamended/
connection.onstreamended = function (e) {
if (e.mediaElement && e.mediaElement.parentNode) {
e.mediaElement.parentNode.removeChild(e.mediaElement);
}
};
// www.RTCMultiConnection.org/docs/onmute/
connection.onmute = function (e) {
log('onmute', e);
if (e.isVideo && e.mediaElement) {
e.mediaElement.pause();
e.mediaElement.setAttribute('poster', e.snapshot || 'https://www.webrtc-experiment.com/images/muted.png');
}
if (e.isAudio && e.mediaElement) {
e.mediaElement.muted = true;
}
};
// www.RTCMultiConnection.org/docs/onunmute/
connection.onunmute = function (e) {
log('onunmute', e);
if (e.isVideo && e.mediaElement) {
e.mediaElement.play();
e.mediaElement.removeAttribute('poster');
}
if (e.isAudio && e.mediaElement) {
e.mediaElement.muted = false;
}
};
// www.RTCMultiConnection.org/docs/onleave/
connection.onleave = function (e) {
log('onleave', toStr(e));
};
connection.token = function () {
// suggested by @rvulpescu from #154
if (window.crypto) {
var a = window.crypto.getRandomValues(new Uint32Array(3)),
token = '';
for (var i = 0, l = a.length; i < l; i++) token += a[i].toString(36);
return token;
} else {
return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, '');
}
};
// www.RTCMultiConnection.org/docs/userid/
connection.userid = connection.token();
// www.RTCMultiConnection.org/docs/peers/
connection.peers = {};
connection.peers[connection.userid] = {
drop: function () {
connection.drop();
},
renegotiate: function () {
},
addStream: function () {
},
hold: function () {
},
unhold: function () {
},
changeBandwidth: function () {
},
sharePartOfScreen: function () {
}
};
connection._skip = ['stop', 'mute', 'unmute', '_private'];
// www.RTCMultiConnection.org/docs/streams/
connection.streams = {
mute: function (session) {
this._private(session, true);
},
unmute: function (session) {
this._private(session, false);
},
_private: function (session, enabled) {
// implementation from #68
for (var stream in this) {
if (connection._skip.indexOf(stream) == -1) {
this[stream]._private(session, enabled);
}
}
},
stop: function (type) {
// connection.streams.stop('local');
var _stream;
for (var stream in this) {
if (stream != 'stop' && stream != 'mute' && stream != 'unmute' && stream != '_private') {
_stream = this[stream];
if (!type) _stream.stop();
if (type == 'local' && _stream.type == 'local')
_stream.stop();
if (type == 'remote' && _stream.type == 'remote')
_stream.stop();
}
}
}
};
// this array is aimed to store all renegotiated streams' session-types
connection.renegotiatedSessions = [];
// www.RTCMultiConnection.org/docs/channels/
connection.channels = {};
// www.RTCMultiConnection.org/docs/extra/
connection.extra = {};
// www.RTCMultiConnection.org/docs/session/
connection.session = {
audio: true,
video: true
};
// www.RTCMultiConnection.org/docs/bandwidth/
connection.bandwidth = {
screen: 300 // 300kbps (old workaround!)
};
connection.sdpConstraints = {};
connection.mediaConstraints = {};
connection.optionalArgument = {};
connection.dataChannelDict = {};
var iceServers = [];
if (isFirefox) {
iceServers.push({
url: 'stun:23.21.150.121'
});
iceServers.push({
url: 'stun:stun.services.mozilla.com'
});
}
if (isChrome) {
iceServers.push({
url: 'stun:stun.l.google.com:19302'
});
iceServers.push({
url: 'stun:stun.anyfirewall.com:3478'
});
}
if (isChrome && chromeVersion < 28) {
iceServers.push({
url: 'turn:[email protected]:80?transport=udp',
credential: 'homeo'
});
iceServers.push({
url: 'turn:[email protected]:80?transport=tcp',
credential: 'homeo'
});
}
if (isChrome && chromeVersion >= 28) {
iceServers.push({
url: 'turn:turn.bistri.com:80?transport=udp',
credential: 'homeo',
username: 'homeo'
});
iceServers.push({
url: 'turn:turn.bistri.com:80?transport=tcp',
credential: 'homeo',
username: 'homeo'
});
iceServers.push({
url: 'turn:turn.anyfirewall.com:443?transport=tcp',
credential: 'webrtc',
username: 'webrtc'
});
}
connection.iceServers = iceServers;
// www.RTCMultiConnection.org/docs/preferSCTP/
connection.preferSCTP = isFirefox || chromeVersion >= 32 ? true : false;
connection.chunkInterval = isFirefox || chromeVersion >= 32 ? 100 : 500; // 500ms for RTP and 100ms for SCTP
connection.chunkSize = isFirefox || chromeVersion >= 32 ? 13 * 1000 : 1000; // 1000 chars for RTP and 13000 chars for SCTP
if (isFirefox) {
connection.preferSCTP = true; // FF supports only SCTP!
}
// www.RTCMultiConnection.org/docs/fakeDataChannels/
connection.fakeDataChannels = false;
// www.RTCMultiConnection.org/docs/UA/
connection.UA = {
Firefox: isFirefox,
Chrome: isChrome,
Mobile: isMobileDevice,
Version: chromeVersion,
NodeWebkit: isNodeWebkit
};
// file queue: to store previous file objects in memory;
// and stream over newly connected peers
// www.RTCMultiConnection.org/docs/fileQueue/
connection.fileQueue = {};
// www.RTCMultiConnection.org/docs/media/
connection.media = {
min: function (width, height) {
this.minWidth = width;
this.minHeight = height;
},
minWidth: 640,
minHeight: 360,
max: function (width, height) {
this.maxWidth = width;
this.maxHeight = height;
},
maxWidth: 1280,
maxHeight: 720,
bandwidth: 256,
minFrameRate: 1,
maxFrameRate: 30,
minAspectRatio: 1.77
};
// www.RTCMultiConnection.org/docs/candidates/
connection.candidates = {
host: true,
relay: true,
reflexive: true
};
// www.RTCMultiConnection.org/docs/attachStreams/
connection.attachStreams = [];
// www.RTCMultiConnection.org/docs/detachStreams/
connection.detachStreams = [];
// www.RTCMultiConnection.org/docs/maxParticipantsAllowed/
connection.maxParticipantsAllowed = 256;
// www.RTCMultiConnection.org/docs/direction/
// 'many-to-many' / 'one-to-many' / 'one-to-one' / 'one-way'
connection.direction = 'many-to-many';
connection._getStream = function (e) {
return {
rtcMultiConnection: e.rtcMultiConnection,
streamObject: e.streamObject,
stream: e.stream,
session: e.session,
userid: e.userid,
streamid: e.streamid,
sockets: e.socket ? [e.socket] : [],
type: e.type,
mediaElement: e.mediaElement,
stop: function (forceToStopRemoteStream) {
this.sockets.forEach(function (socket) {
if (this.type == 'local') {
socket.send({
userid: this.rtcMultiConnection.userid,
extra: this.rtcMultiConnection.extra,
streamid: this.streamid,
stopped: true
});
}
if (this.type == 'remote' && !!forceToStopRemoteStream) {
socket.send({
userid: this.rtcMultiConnection.userid,
promptStreamStop: true,
streamid: this.streamid
});
}
});
var stream = this.stream;
if (stream && stream.stop) {
stopTracks(stream);
}
},
mute: function (session) {
this.muted = true;
this._private(session, true);
},
unmute: function (session) {
this.muted = false;
this._private(session, false);
},
_private: function (session, enabled) {
muteOrUnmute({
root: this,
session: session,
enabled: enabled,
stream: this.stream
});
},
startRecording: function (session) {
if (!session)
session = {
audio: true,
video: true
};
if (isFirefox) {
// https://www.webrtc-experiment.com/RecordRTC/AudioVideo-on-Firefox.html
session = { audio: true };
}
if (!window.RecordRTC) {
var self = this;
return loadScript('https://www.webrtc-experiment.com/RecordRTC.js', function () {
self.startRecording(session);
});
}
this.recorder = new MRecordRTC();
this.recorder.mediaType = session;
this.recorder.addStream(this.stream);
this.recorder.startRecording();
},
stopRecording: function (callback) {
this.recorder.stopRecording();
this.recorder.getBlob(function (blob) {
callback(blob.audio || blob.video, blob.video);
});
}
};
};
// new RTCMultiConnection().set({properties}).connect()
connection.set = function (properties) {
for (var property in properties) {
this[property] = properties[property];
}
return this;
};
// www.RTCMultiConnection.org/docs/firebase/
connection.firebase = 'chat';
// www.RTCMultiConnection.org/docs/onMediaError/
connection.onMediaError = function (_error) {
error(_error);
};
// www.RTCMultiConnection.org/docs/stats/
connection.stats = {
numberOfConnectedUsers: 0,
numberOfSessions: 0,
sessions: {}
};
// www.RTCMultiConnection.org/docs/getStats/
connection.getStats = function (callback) {
var numberOfConnectedUsers = 0;
for (var peer in connection.peers) {
numberOfConnectedUsers++;
}
connection.stats.numberOfConnectedUsers = numberOfConnectedUsers;
// numberOfSessions
if (callback) callback(connection.stats);
};
// www.RTCMultiConnection.org/docs/caniuse/
connection.caniuse = {
RTCPeerConnection: !!RTCPeerConnection,
getUserMedia: !!getUserMedia,
AudioContext: !!AudioContext,
// there is no way to check whether "getUserMedia" flag is enabled or not!
ScreenSharing: isChrome && chromeVersion >= 26 && location.protocol == 'https:',
checkIfScreenSharingFlagEnabled: function (callback) {
var warning;
if (isFirefox) {
warning = 'Screen sharing is NOT supported on Firefox.';
error(warning);
if (callback) callback(false);
}
if (location.protocol !== 'https:') {
warning = 'Screen sharing is NOT supported on ' + location.protocol + ' Try https!';
error(warning);
if (callback) return callback(false);
}
if (chromeVersion < 26) {
warning = 'Screen sharing support is suspicious!';
warn(warning);
}
var screen_constraints = {
video: {
mandatory: {
chromeMediaSource: 'screen'
}
}
};
var invocationInterval = 0,
stop;
(function selfInvoker() {
invocationInterval++;
if (!stop) setTimeout(selfInvoker, 10);
})();
navigator.webkitGetUserMedia(screen_constraints, onsuccess, onfailure);
function onsuccess(stream) {
if (stream.stop) {
stream.stop();
}
if (callback) {
callback(true);
}
}
function onfailure() {
stop = true;
if (callback) callback(invocationInterval > 5, warning);
}
},
RtpDataChannels: isChrome && chromeVersion >= 25,
SctpDataChannels: isChrome && chromeVersion >= 31
};
// www.RTCMultiConnection.org/docs/snapshots/
connection.snapshots = {};
// www.RTCMultiConnection.org/docs/takeSnapshot/
connection.takeSnapshot = function (userid, callback) {
for (var stream in connection.streams) {
stream = connection.streams[stream];
if (stream.userid == userid && stream.stream && stream.stream.getVideoTracks && stream.stream.getVideoTracks().length) {
var video = stream.streamObject.mediaElement;
var canvas = document.createElement('canvas');
canvas.width = video.videoWidth || video.clientWidth;
canvas.height = video.videoHeight || video.clientHeight;
var context = canvas.getContext('2d');
context.drawImage(video, 0, 0, canvas.width, canvas.height);
connection.snapshots[userid] = canvas.toDataURL();
callback && callback(connection.snapshots[userid]);
continue;
}
}
};
connection.saveToDisk = function (blob, fileName) {
if (blob.size && blob.type) FileSaver.SaveToDisk(URL.createObjectURL(blob), fileName || blob.name || blob.type.replace('/', '-') + blob.type.split('/')[1]);
else FileSaver.SaveToDisk(blob, fileName);
};
// www.WebRTC-Experiment.com/demos/MediaStreamTrack.getSources.html
connection._mediaSources = {};
// www.RTCMultiConnection.org/docs/selectDevices/
connection.selectDevices = function (device1, device2) {
if (device1) select(this.devices[device1]);
if (device2) select(this.devices[device2]);
function select(device) {
if (!device) return;
connection._mediaSources[device.kind] = device.id;
}
};
// www.RTCMultiConnection.org/docs/devices/
connection.devices = {};
// www.RTCMultiConnection.org/docs/getDevices/
connection.getDevices = function (callback) {
if (!!window.MediaStreamTrack && !!MediaStreamTrack.getSources) {
MediaStreamTrack.getSources(function (media_sources) {
var sources = [];
for (var i = 0; i < media_sources.length; i++) {
sources.push(media_sources[i]);
}
getAllUserMedias(sources);
if (callback) callback(connection.devices);
});
var index = 0;
var devicesFetched = {};
function getAllUserMedias(media_sources) {
var media_source = media_sources[index];
if (!media_source) return;
// to prevent duplicated devices to be fetched.
if (devicesFetched[media_source.id]) {
index++;
return getAllUserMedias(media_sources);
}
devicesFetched[media_source.id] = media_source;
connection.devices[media_source.id] = media_source;
index++;
getAllUserMedias(media_sources);
}
}
};
// www.RTCMultiConnection.org/docs/onCustomMessage/
connection.onCustomMessage = function (message) {
log('Custom message', message);
};
// www.RTCMultiConnection.org/docs/ondrop/
connection.ondrop = function (droppedBy) {
log('Media connection is dropped by ' + droppedBy);
};
// www.RTCMultiConnection.org/docs/drop/
connection.drop = function (config) {
config = config || {};
this.attachStreams = [];
// "drop" should detach all local streams
for (var stream in this.streams) {
if (this._skip.indexOf(stream) == -1) {
stream = this.streams[stream];
if (stream.type == 'local') {
this.detachStreams.push(stream.streamid);
this.onstreamended(stream.streamObject);
} else this.onstreamended(stream.streamObject);
}
}
// www.RTCMultiConnection.org/docs/sendCustomMessage/
this.sendCustomMessage({
drop: true,
dontRenegotiate: typeof config.renegotiate == 'undefined' ? true : config.renegotiate
});
};
// used for SoundMeter
if (!!window.AudioContext) {
connection._audioContext = new AudioContext();
}
// www.RTCMultiConnection.org/docs/language/ (to see list of all supported languages)
connection.language = 'en';
// www.RTCMultiConnection.org/docs/autoTranslateText/
connection.autoTranslateText = false;
// please use your own API key; if possible
connection.googKey = 'AIzaSyCUmCjvKRb-kOYrnoL2xaXb8I-_JJeKpf0';
// www.RTCMultiConnection.org/docs/Translator/
connection.Translator = {
TranslateText: function (text, callback) {
// if(location.protocol === 'https:') return callback(text);
var newScript = document.createElement('script');
newScript.type = 'text/javascript';
var sourceText = encodeURIComponent(text); // escape
var randomNumber = 'method' + connection.token();
window[randomNumber] = function (response) {
if (response.data && response.data.translations[0] && callback) {
callback(response.data.translations[0].translatedText);
}
};
var source = 'https://www.googleapis.com/language/translate/v2?key=' + connection.googKey + '&target=' + (connection.language || 'en-US') + '&callback=window.' + randomNumber + '&q=' + sourceText;
newScript.src = source;
document.getElementsByTagName('head')[0].appendChild(newScript);
}
};
// you can easily override it by setting it NULL!
connection.setDefaultEventsForMediaElement = function (mediaElement, streamid) {
mediaElement.onpause = function () {
if (connection.streams[streamid] && !connection.streams[streamid].muted) {
connection.streams[streamid].mute();
}
};
// todo: need to make sure that "onplay" EVENT doesn't play self-voice!
mediaElement.onplay = function () {
if (connection.streams[streamid] && connection.streams[streamid].muted) {
connection.streams[streamid].unmute();
}
};
var volumeChangeEventFired = false;
mediaElement.onvolumechange = function () {
if (!volumeChangeEventFired) {
volumeChangeEventFired = true;
setTimeout(function () {
var root = connection.streams[streamid];
connection.streams[streamid].sockets.forEach(function (socket) {
socket.send({
userid: connection.userid,
streamid: root.streamid,
isVolumeChanged: true,
volume: mediaElement.volume
});
});
volumeChangeEventFired = false;
}, 2000);
}
};
};
connection.localStreamids = [];
// www.RTCMultiConnection.org/docs/onMediaFile/
connection.onMediaFile = function (e) {
log('onMediaFile', e);
connection.body.appendChild(e.mediaElement);
};
// this object stores pre-recorded media streaming uids
// multiple pre-recorded media files can be streamed concurrently.
connection.preRecordedMedias = {};
// www.RTCMultiConnection.org/docs/shareMediaFile/
// this method handles pre-recorded media streaming
connection.shareMediaFile = function (file, video, streamerid) {
if (file && (typeof file.size == 'undefined' || typeof file.type == 'undefined')) throw 'You MUST attach file using input[type=file] or pass a Blob.';
warn('Pre-recorded media streaming is added as experimental feature.');
video = video || document.createElement('video');
video.autoplay = true;
video.controls = true;
streamerid = streamerid || connection.token();
var streamer = new Streamer(this);
streamer.push = function (chunk) {
connection.send({
preRecordedMediaChunk: true,
chunk: chunk,
streamerid: streamerid
});
};
if (file) {
streamer.stream(file);
}
streamer.video = video;
streamer.receive();
connection.preRecordedMedias[streamerid] = {
video: video,
streamer: streamer,
onData: function (data) {
if (data.end) this.streamer.end();
else this.streamer.append(data);
}
};
connection.onMediaFile({
mediaElement: video,
userid: connection.userid,
extra: connection.extra
});
return streamerid;
};
// www.RTCMultiConnection.org/docs/onpartofscreen/
connection.onpartofscreen = function (e) {
var image = document.createElement('img');
image.src = e.screenshot;
connection.body.appendChild(image);
};
connection.skipLogs = function () {
log = error = warn = function () {
};
};
// www.RTCMultiConnection.org/docs/hold/
connection.hold = function (mLine) {
for (var peer in connection.peers) {
connection.peers[peer].hold(mLine);
}
};
// www.RTCMultiConnection.org/docs/onhold/
connection.onhold = function (track) {
log('onhold', track);
if (track.kind != 'audio') {
track.mediaElement.pause();
track.mediaElement.setAttribute('poster', track.screenshot || 'https://www.webrtc-experiment.com/images/muted.png');
}
if (track.kind == 'audio') {
track.mediaElement.muted = true;
}
};
// www.RTCMultiConnection.org/docs/unhold/
connection.unhold = function (mLine) {
for (var peer in connection.peers) {
connection.peers[peer].unhold(mLine);
}
};
// www.RTCMultiConnection.org/docs/onunhold/
connection.onunhold = function (track) {
log('onunhold', track);
if (track.kind != 'audio') {
track.mediaElement.play();
track.mediaElement.removeAttribute('poster');
}
if (track.kind != 'audio') {
track.mediaElement.muted = false;
}
};
connection.sharePartOfScreen = function (args) {
for (var peer in connection.peers) {
connection.peers[peer].sharePartOfScreen(args);
}
};
connection.pausePartOfScreenSharing = function () {
for (var peer in connection.peers) {
connection.peers[peer].pausePartOfScreenSharing = true;
}
};
connection.stopPartOfScreenSharing = function () {
for (var peer in connection.peers) {
connection.peers[peer].stopPartOfScreenSharing = true;
}
};
// it is false because workaround that is used to capture connections' failures
// affects renegotiation scenarios!
// todo: fix it!
connection.autoReDialOnFailure = false;
connection.isInitiator = false;
}
})();