Tätä skriptiä ei tulisi asentaa suoraan. Se on kirjasto muita skriptejä varten sisällytettäväksi metadirektiivillä // @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:homeo@turn.bistri.com:80?transport=udp',
credential: 'homeo'
});
iceServers.push({
url: 'turn:homeo@turn.bistri.com: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;
}
})();