|
|
/* eslint-disable */
|
|
|
|
|
|
/**
|
|
|
*
|
|
|
* @returns
|
|
|
*/
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
var adapter = require("./adapter");
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
|
var WebRTCAdaptor = function(initialValues) {
|
|
|
|
|
|
class PeerStats {
|
|
|
|
|
|
constructor(streamId) {
|
|
|
this.streamId = streamId;
|
|
|
this.totalBytesReceivedCount = 0;
|
|
|
this.totalBytesSent = 0;
|
|
|
this.packetsLost = 0;
|
|
|
this.fractionLost = 0;
|
|
|
this.startTime = 0;
|
|
|
this.lastBytesReceived = 0;
|
|
|
this.lastBytesSent = 0;
|
|
|
this.currentTimestamp = 0;
|
|
|
this.janusScreen = null;
|
|
|
this.lastTime = 0;
|
|
|
this.timerId = 0;
|
|
|
this.firstByteSentCount = 0;
|
|
|
this.firstBytesReceivedCount = 0;
|
|
|
}
|
|
|
|
|
|
//kbits/sec
|
|
|
get averageOutgoingBitrate() {
|
|
|
return Math.floor(8 * (this.totalBytesSentCount - this.firstByteSentCount) / (this.currentTimestamp - this.startTime));
|
|
|
}
|
|
|
|
|
|
//kbits/sec
|
|
|
get averageIncomingBitrate() {
|
|
|
return Math.floor(8 * (this.totalBytesReceivedCount - this.firstBytesReceivedCount) / (this.currentTimestamp - this.startTime));
|
|
|
}
|
|
|
|
|
|
//kbits/sec
|
|
|
get currentOutgoingBitrate() {
|
|
|
return Math.floor(8 * (this.totalBytesSentCount - this.lastBytesSent) / (this.currentTimestamp - this.lastTime));
|
|
|
}
|
|
|
|
|
|
//kbits/sec
|
|
|
get currentIncomingBitrate() {
|
|
|
return Math.floor(8 * (this.totalBytesReceivedCount - this.lastBytesReceived) / (this.currentTimestamp - this.lastTime));
|
|
|
}
|
|
|
|
|
|
set currentTime(timestamp) {
|
|
|
this.lastTime = this.currentTimestamp;
|
|
|
this.currentTimestamp = timestamp;
|
|
|
if (this.startTime == 0) {
|
|
|
this.startTime = timestamp - 1; // do not have zero division error
|
|
|
}
|
|
|
}
|
|
|
|
|
|
set totalBytesReceived(bytesReceived) {
|
|
|
this.lastBytesReceived = this.totalBytesReceivedCount;
|
|
|
this.totalBytesReceivedCount = bytesReceived;
|
|
|
if (this.firstBytesReceivedCount == 0) {
|
|
|
this.firstBytesReceivedCount = bytesReceived;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
set totalBytesSent(bytesSent) {
|
|
|
this.lastBytesSent = this.totalBytesSentCount;
|
|
|
this.totalBytesSentCount = bytesSent;
|
|
|
if (this.firstByteSentCount == 0) {
|
|
|
this.firstByteSentCount = bytesSent;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
var thiz = this;
|
|
|
thiz.peerconnection_config = null;
|
|
|
thiz.sdp_constraints = null;
|
|
|
thiz.remotePeerConnection = new Array();
|
|
|
thiz.remotePeerConnectionStats = new Array();
|
|
|
thiz.remoteDescriptionSet = new Array();
|
|
|
thiz.iceCandidateList = new Array();
|
|
|
thiz.webSocketAdaptor = null;
|
|
|
thiz.roomName = null;
|
|
|
thiz.videoTrackSender = null;
|
|
|
thiz.audioTrackSender = null;
|
|
|
thiz.playStreamId = new Array();
|
|
|
thiz.micGainNode = null;
|
|
|
thiz.localStream = null;
|
|
|
thiz.bandwidth = 512; //default bandwidth kbps
|
|
|
|
|
|
thiz.isPlayMode = false;
|
|
|
thiz.debug = false;
|
|
|
|
|
|
/**
|
|
|
* The cam_location below is effective when camera and screen is send at the same time.
|
|
|
* possible values are top and bottom. It's on right all the time
|
|
|
*/
|
|
|
thiz.camera_location = "top"
|
|
|
|
|
|
/**
|
|
|
* The cam_margin below is effective when camera and screen is send at the same time.
|
|
|
* This is the margin value in px from the edges
|
|
|
*/
|
|
|
thiz.camera_margin = 15;
|
|
|
|
|
|
/**
|
|
|
* Thiz camera_percent is how large the camera view appear on the screen. It's %15 by default.
|
|
|
*/
|
|
|
thiz.camera_percent = 100;
|
|
|
|
|
|
for (var key in initialValues) {
|
|
|
if (initialValues.hasOwnProperty(key)) {
|
|
|
this[key] = initialValues[key];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
thiz.localVideo = document.getElementById(thiz.localVideoId);
|
|
|
thiz.remoteVideo = document.getElementById(thiz.remoteVideoId);
|
|
|
|
|
|
if (!("WebSocket" in window)) {
|
|
|
console.log("WebSocket not supported.");
|
|
|
thiz.callbackError("WebSocketNotSupported");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (typeof navigator.mediaDevices == "undefined" && thiz.isPlayMode == false) {
|
|
|
console.log("Cannot open camera and mic because of unsecure context. Please Install SSL(https)");
|
|
|
thiz.callbackError("UnsecureContext");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Get user media
|
|
|
*/
|
|
|
this.getUserMedia = function (mediaConstraints, audioConstraint) {
|
|
|
navigator.mediaDevices.getUserMedia(mediaConstraints)
|
|
|
.then(function (stream) {
|
|
|
|
|
|
//this trick, getting audio and video separately, make us add or remove tracks on the fly
|
|
|
var audioTrack = stream.getAudioTracks();
|
|
|
if (audioTrack.length > 0) {
|
|
|
stream.removeTrack(audioTrack[0]);
|
|
|
}
|
|
|
|
|
|
//add callback if desktop is sharing
|
|
|
if (mediaConstraints.video != "undefined"
|
|
|
// eslint-disable-next-line no-mixed-spaces-and-tabs
|
|
|
&& typeof mediaConstraints.video.mandatory != "undefined"
|
|
|
// eslint-disable-next-line no-mixed-spaces-and-tabs
|
|
|
&& typeof mediaConstraints.video.mandatory.chromeMediaSource != "undefined"
|
|
|
// eslint-disable-next-line no-mixed-spaces-and-tabs
|
|
|
&& mediaConstraints.video.mandatory.chromeMediaSource == "desktop") {
|
|
|
|
|
|
stream.getVideoTracks()[0].onended = function (event) {
|
|
|
thiz.callback("screen_share_stopped");
|
|
|
}
|
|
|
}
|
|
|
if (thiz.janusScreen) {
|
|
|
thiz.gotStream(thiz.janusScreen);
|
|
|
} else if (audioConstraint != "undefined" && audioConstraint != false) {
|
|
|
var media_audio_constraint = {audio: audioConstraint};
|
|
|
navigator.mediaDevices.getUserMedia(media_audio_constraint)
|
|
|
.then(function (audioStream) {
|
|
|
|
|
|
|
|
|
if (thiz.mediaConstraints != "undefined" && thiz.mediaConstraints.video == "screen+camera") {
|
|
|
navigator.mediaDevices.getUserMedia({video: true, audio: false})
|
|
|
.then(function (cameraStream) {
|
|
|
//create a canvas element
|
|
|
var canvas = document.createElement("canvas");
|
|
|
var canvasContext = canvas.getContext("2d");
|
|
|
|
|
|
//create video element for screen
|
|
|
//var screenVideo = document.getElementById('sourceVideo');
|
|
|
var screenVideo = document.createElement('video');
|
|
|
//TODO: check audio track
|
|
|
screenVideo.srcObject = stream;
|
|
|
screenVideo.play();
|
|
|
|
|
|
|
|
|
//create video element for camera
|
|
|
var cameraVideo = document.createElement('video');
|
|
|
//TODO: check audio track
|
|
|
cameraVideo.srcObject = cameraStream;
|
|
|
cameraVideo.play();
|
|
|
|
|
|
|
|
|
var canvasStream = canvas.captureStream(15);
|
|
|
canvasStream.addTrack(audioStream.getAudioTracks()[0]);
|
|
|
//call gotStream
|
|
|
thiz.gotStream(canvasStream);
|
|
|
|
|
|
//update the canvas
|
|
|
setInterval(function () {
|
|
|
|
|
|
//draw screen to canvas
|
|
|
canvas.width = screenVideo.videoWidth;
|
|
|
canvas.height = screenVideo.videoHeight;
|
|
|
canvasContext.drawImage(screenVideo, 0, 0, canvas.width, canvas.height);
|
|
|
|
|
|
var cameraWidth = screenVideo.videoWidth * (thiz.camera_percent / 100);
|
|
|
var cameraHeight = (cameraVideo.videoHeight / cameraVideo.videoWidth) * cameraWidth
|
|
|
|
|
|
var positionX = (canvas.width - cameraWidth) - thiz.camera_margin;
|
|
|
var positionY;
|
|
|
|
|
|
if (thiz.camera_location == "top") {
|
|
|
positionY = thiz.camera_margin;
|
|
|
} else { //if not top, make it bottom
|
|
|
//draw camera on right bottom corner
|
|
|
positionY = (canvas.height - cameraHeight) - thiz.camera_margin;
|
|
|
}
|
|
|
|
|
|
canvasContext.drawImage(cameraVideo, positionX, positionY, cameraWidth, cameraHeight);
|
|
|
|
|
|
|
|
|
}, 66);
|
|
|
|
|
|
})
|
|
|
.catch(function (error) {
|
|
|
thiz.callbackError(error.name, error.message);
|
|
|
});
|
|
|
} else {
|
|
|
stream.addTrack(audioStream.getAudioTracks()[0]);
|
|
|
thiz.gotStream(stream);
|
|
|
}
|
|
|
})
|
|
|
.catch(function (error) {
|
|
|
thiz.callbackError(error.name, error.message);
|
|
|
});
|
|
|
} else {
|
|
|
stream.addTrack(audioStream.getAudioTracks()[0]);
|
|
|
thiz.gotStream(stream);
|
|
|
}
|
|
|
})
|
|
|
.catch(function (error) {
|
|
|
thiz.callbackError(error.name, error.message);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
this.openScreen = function (audioConstraint, openCamera) {
|
|
|
var callback = function (message) {
|
|
|
if (message.data == "rtcmulticonnection-extension-loaded") {
|
|
|
console.debug("rtcmulticonnection-extension-loaded parameter is received");
|
|
|
window.postMessage("get-sourceId", "*");
|
|
|
} else if (message.data == "PermissionDeniedError") {
|
|
|
console.debug("Permission denied error");
|
|
|
thiz.callbackError("screen_share_permission_denied");
|
|
|
} else if (message.data && message.data.sourceId) {
|
|
|
var mediaConstraints = {
|
|
|
audio: false,
|
|
|
video: {
|
|
|
mandatory: {
|
|
|
chromeMediaSource: 'desktop',
|
|
|
chromeMediaSourceId: message.data.sourceId,
|
|
|
},
|
|
|
optional: []
|
|
|
}
|
|
|
};
|
|
|
|
|
|
thiz.getUserMedia(mediaConstraints, audioConstraint);
|
|
|
|
|
|
//remove event listener
|
|
|
window.removeEventListener("message", callback);
|
|
|
}
|
|
|
|
|
|
}
|
|
|
window.addEventListener("message", callback, false);
|
|
|
|
|
|
window.postMessage("are-you-there", "*");
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Open media stream, it may be screen, camera or audio
|
|
|
*/
|
|
|
this.openStream = function (mediaConstraints) {
|
|
|
|
|
|
thiz.mediaConstraints = mediaConstraints;
|
|
|
var audioConstraint = false;
|
|
|
if (typeof mediaConstraints.audio != "undefined" && mediaConstraints.audio != false) {
|
|
|
audioConstraint = mediaConstraints.audio;
|
|
|
}
|
|
|
|
|
|
if (typeof mediaConstraints.video != "undefined") {
|
|
|
|
|
|
if (mediaConstraints.video == "screen+camera" || mediaConstraints.video == "screen") {
|
|
|
this.openScreen(audioConstraint);
|
|
|
} else {
|
|
|
thiz.getUserMedia(mediaConstraints, audioConstraint);
|
|
|
}
|
|
|
} else {
|
|
|
console.error("MediaConstraint video is not defined");
|
|
|
thiz.callbackError("media_constraint_video_not_defined");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Closes stream, if you want to stop peer connection, call stop(streamId)
|
|
|
*/
|
|
|
this.closeStream = function () {
|
|
|
|
|
|
thiz.localStream.getVideoTracks().forEach(function (track) {
|
|
|
track.onended = null;
|
|
|
track.stop();
|
|
|
});
|
|
|
|
|
|
thiz.localStream.getAudioTracks().forEach(function (track) {
|
|
|
track.onended = null;
|
|
|
track.stop();
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
/**
|
|
|
* Checks chrome screen share extension is avaiable
|
|
|
* if exists it call callback with "screen_share_extension_available"
|
|
|
*/
|
|
|
this.checkExtension = function () {
|
|
|
var callback = function (message) {
|
|
|
|
|
|
if (message.data == "rtcmulticonnection-extension-loaded") {
|
|
|
thiz.callback("screen_share_extension_available");
|
|
|
window.removeEventListener("message", callback);
|
|
|
}
|
|
|
|
|
|
};
|
|
|
//add event listener for desktop capture
|
|
|
window.addEventListener("message", callback, false);
|
|
|
|
|
|
window.postMessage("are-you-there", "*");
|
|
|
|
|
|
};
|
|
|
|
|
|
/*
|
|
|
* Call check extension. Below function is called when this class is created
|
|
|
*/
|
|
|
thiz.checkExtension();
|
|
|
|
|
|
/*
|
|
|
* Below lines are executed as well when this class is created
|
|
|
*/
|
|
|
if (!this.isPlayMode && typeof thiz.mediaConstraints != "undefined" && this.localStream == null) {
|
|
|
if (typeof thiz.mediaConstraints.video != "undefined" && thiz.mediaConstraints.video != false) {
|
|
|
// if it is not play mode and media constraint is defined, try to get user media
|
|
|
if (thiz.mediaConstraints.audio.mandatory) {
|
|
|
//this case captures mic and video(audio(screen audio) + video(screen)) and then provide mute/unmute mic with
|
|
|
//enableMicInMixedAudio
|
|
|
navigator.mediaDevices.getUserMedia({audio: true, video: false}).then(function (micStream) {
|
|
|
navigator.mediaDevices.getUserMedia(thiz.mediaConstraints)
|
|
|
.then(function (stream) {
|
|
|
//console.debug("audio stream track count: " + audioStream.getAudioTracks().length);
|
|
|
|
|
|
var audioContext = new AudioContext();
|
|
|
var desktopSoundGainNode = audioContext.createGain();
|
|
|
|
|
|
desktopSoundGainNode.gain.value = 1;
|
|
|
|
|
|
var audioDestionation = audioContext.createMediaStreamDestination();
|
|
|
var audioSource = audioContext.createMediaStreamSource(stream);
|
|
|
|
|
|
audioSource.connect(desktopSoundGainNode);
|
|
|
|
|
|
thiz.micGainNode = audioContext.createGain();
|
|
|
thiz.micGainNode.gain.value = 1;
|
|
|
var audioSource2 = audioContext.createMediaStreamSource(micStream);
|
|
|
audioSource2.connect(thiz.micGainNode);
|
|
|
|
|
|
desktopSoundGainNode.connect(audioDestionation);
|
|
|
thiz.micGainNode.connect(audioDestionation);
|
|
|
|
|
|
stream.removeTrack(stream.getAudioTracks()[0]);
|
|
|
audioDestionation.stream.getAudioTracks().forEach(function (track) {
|
|
|
stream.addTrack(track);
|
|
|
});
|
|
|
|
|
|
console.debug("Running gotStream");
|
|
|
thiz.gotStream(stream);
|
|
|
|
|
|
}).catch(function (error) {
|
|
|
thiz.callbackError(error.name, error.message);
|
|
|
});
|
|
|
}).catch(function (error) {
|
|
|
thiz.callbackError(error.name, error.message);
|
|
|
});
|
|
|
} else {
|
|
|
//most of the times, this statement runs
|
|
|
thiz.openStream(thiz.mediaConstraints, thiz.mode);
|
|
|
}
|
|
|
} else {
|
|
|
// get only audio
|
|
|
var media_audio_constraint = {audio: thiz.mediaConstraints.audio};
|
|
|
navigator.mediaDevices.getUserMedia(media_audio_constraint)
|
|
|
.then(function (stream) {
|
|
|
thiz.gotStream(stream);
|
|
|
})
|
|
|
.catch(function (error) {
|
|
|
thiz.callbackError(error.name, error.message);
|
|
|
});
|
|
|
}
|
|
|
} else {
|
|
|
|
|
|
//just playing, it does not open any stream
|
|
|
if (thiz.webSocketAdaptor == null || thiz.webSocketAdaptor.isConnected() == false) {
|
|
|
thiz.webSocketAdaptor = new WebSocketAdaptor();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
this.enableMicInMixedAudio = function (enable) {
|
|
|
if (thiz.micGainNode != null) {
|
|
|
if (enable) {
|
|
|
thiz.micGainNode.gain.value = 1;
|
|
|
} else {
|
|
|
thiz.micGainNode.gain.value = 0;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
this.publish = function (streamId, token) {
|
|
|
var jsCmd = {
|
|
|
command: "publish",
|
|
|
streamId: streamId,
|
|
|
token: token,
|
|
|
video: { width: 1280, height: 720 },
|
|
|
audio: true,
|
|
|
};
|
|
|
|
|
|
thiz.webSocketAdaptor.send(JSON.stringify(jsCmd));
|
|
|
}
|
|
|
|
|
|
|
|
|
this.joinRoom = function (roomName, streamId) {
|
|
|
thiz.roomName = roomName;
|
|
|
|
|
|
var jsCmd = {
|
|
|
command: "joinRoom",
|
|
|
room: roomName,
|
|
|
streamId: streamId,
|
|
|
}
|
|
|
|
|
|
thiz.webSocketAdaptor.send(JSON.stringify(jsCmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
this.play = function (streamId, token, roomId) {
|
|
|
thiz.playStreamId.push(streamId);
|
|
|
var jsCmd =
|
|
|
{
|
|
|
command: "play",
|
|
|
streamId: streamId,
|
|
|
token: token,
|
|
|
room: roomId,
|
|
|
}
|
|
|
|
|
|
thiz.webSocketAdaptor.send(JSON.stringify(jsCmd));
|
|
|
}
|
|
|
|
|
|
this.stop = function (streamId) {
|
|
|
thiz.closePeerConnection(streamId);
|
|
|
|
|
|
var jsCmd = {
|
|
|
command: "stop",
|
|
|
streamId: streamId,
|
|
|
};
|
|
|
|
|
|
thiz.webSocketAdaptor.send(JSON.stringify(jsCmd));
|
|
|
}
|
|
|
|
|
|
this.join = function (streamId) {
|
|
|
var jsCmd = {
|
|
|
command: "join",
|
|
|
streamId: streamId,
|
|
|
};
|
|
|
|
|
|
|
|
|
thiz.webSocketAdaptor.send(JSON.stringify(jsCmd));
|
|
|
}
|
|
|
|
|
|
this.leaveFromRoom = function (roomName) {
|
|
|
thiz.roomName = roomName;
|
|
|
var jsCmd = {
|
|
|
command: "leaveFromRoom",
|
|
|
room: roomName,
|
|
|
};
|
|
|
console.log("leave request is sent for " + roomName);
|
|
|
|
|
|
thiz.webSocketAdaptor.send(JSON.stringify(jsCmd));
|
|
|
}
|
|
|
|
|
|
this.leave = function (streamId) {
|
|
|
|
|
|
var jsCmd = {
|
|
|
command: "leave",
|
|
|
streamId: streamId,
|
|
|
};
|
|
|
|
|
|
thiz.webSocketAdaptor.send(JSON.stringify(jsCmd));
|
|
|
thiz.closePeerConnection(streamId);
|
|
|
}
|
|
|
|
|
|
this.getStreamInfo = function (streamId) {
|
|
|
var jsCmd = {
|
|
|
command: "getStreamInfo",
|
|
|
streamId: streamId,
|
|
|
};
|
|
|
this.webSocketAdaptor.send(JSON.stringify(jsCmd));
|
|
|
}
|
|
|
|
|
|
this.gotStream = function (stream) {
|
|
|
thiz.localStream = stream;
|
|
|
// thiz.localVideo.srcObject = stream;
|
|
|
if (thiz.webSocketAdaptor == null || thiz.webSocketAdaptor.isConnected() == false) {
|
|
|
thiz.webSocketAdaptor = new WebSocketAdaptor();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
this.switchVideoCapture = function (streamId) {
|
|
|
var mediaConstraints = {
|
|
|
video: true,
|
|
|
audio: true
|
|
|
};
|
|
|
|
|
|
thiz.switchVideoSource(streamId, mediaConstraints, null);
|
|
|
}
|
|
|
|
|
|
this.switchDesktopCapture = function (streamId) {
|
|
|
|
|
|
var screenShareExtensionCallback = function (message) {
|
|
|
|
|
|
if (message.data == "rtcmulticonnection-extension-loaded") {
|
|
|
console.debug("rtcmulticonnection-extension-loaded parameter is received");
|
|
|
window.postMessage("get-sourceId", "*");
|
|
|
} else if (message.data == "PermissionDeniedError") {
|
|
|
console.debug("Permission denied error");
|
|
|
thiz.callbackError("screen_share_permission_denied");
|
|
|
} else if (message.data && message.data.sourceId) {
|
|
|
var mediaConstraints = {
|
|
|
audio: true,
|
|
|
video: {
|
|
|
mandatory: {
|
|
|
chromeMediaSource: 'desktop',
|
|
|
chromeMediaSourceId: message.data.sourceId,
|
|
|
},
|
|
|
optional: []
|
|
|
}
|
|
|
};
|
|
|
|
|
|
thiz.switchVideoSource(streamId, mediaConstraints, function (event) {
|
|
|
thiz.callback("screen_share_stopped");
|
|
|
thiz.switchVideoCapture(streamId);
|
|
|
});
|
|
|
|
|
|
//remove event listener
|
|
|
window.removeEventListener("message", screenShareExtensionCallback);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
//add event listener for desktop capture
|
|
|
window.addEventListener("message", screenShareExtensionCallback, false);
|
|
|
|
|
|
window.postMessage("are-you-there", "*");
|
|
|
}
|
|
|
|
|
|
thiz.arrangeStreams = function (stream, onEndedCallback) {
|
|
|
var videoTrack = thiz.localStream.getVideoTracks()[0];
|
|
|
thiz.localStream.removeTrack(videoTrack);
|
|
|
videoTrack.stop();
|
|
|
thiz.localStream.addTrack(stream.getVideoTracks()[0]);
|
|
|
// thiz.localVideo.srcObject = thiz.localStream;
|
|
|
if (onEndedCallback != null) {
|
|
|
stream.getVideoTracks()[0].onended = function (event) {
|
|
|
onEndedCallback(event);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
this.switchVideoSource = function (streamId, mediaConstraints, onEndedCallback) {
|
|
|
|
|
|
navigator.mediaDevices.getUserMedia(mediaConstraints)
|
|
|
.then(function (stream) {
|
|
|
|
|
|
if (thiz.remotePeerConnection[streamId] != null) {
|
|
|
var videoTrackSender = thiz.remotePeerConnection[streamId].getSenders().find(function (s) {
|
|
|
return s.track.kind == "video";
|
|
|
});
|
|
|
|
|
|
videoTrackSender.replaceTrack(stream.getVideoTracks()[0]).then(function (result) {
|
|
|
thiz.arrangeStreams(stream, onEndedCallback);
|
|
|
|
|
|
}).catch(function (error) {
|
|
|
console.log(error.name);
|
|
|
});
|
|
|
} else {
|
|
|
thiz.arrangeStreams(stream, onEndedCallback);
|
|
|
}
|
|
|
|
|
|
})
|
|
|
.catch(function (error) {
|
|
|
thiz.callbackError(error.name);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
this.onTrack = function (event, streamId) {
|
|
|
console.log("onTrack");
|
|
|
if (thiz.remoteVideo != null) {
|
|
|
//thiz.remoteVideo.srcObject = event.streams[0];
|
|
|
if (thiz.remoteVideo.srcObject !== event.streams[0]) {
|
|
|
thiz.remoteVideo.srcObject = event.streams[0];
|
|
|
console.log('Received remote stream');
|
|
|
}
|
|
|
} else {
|
|
|
var dataObj = {
|
|
|
track: event.streams[0],
|
|
|
streamId: streamId
|
|
|
}
|
|
|
thiz.callback("newStreamAvailable", dataObj);
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
this.iceCandidateReceived = function (event, streamId) {
|
|
|
if (event.candidate) {
|
|
|
|
|
|
var jsCmd = {
|
|
|
command: "takeCandidate",
|
|
|
streamId: streamId,
|
|
|
label: event.candidate.sdpMLineIndex,
|
|
|
id: event.candidate.sdpMid,
|
|
|
candidate: event.candidate.candidate
|
|
|
};
|
|
|
|
|
|
if (thiz.debug) {
|
|
|
console.log("sending ice candiate for stream Id " + streamId);
|
|
|
console.log(JSON.stringify(event.candidate));
|
|
|
}
|
|
|
|
|
|
thiz.webSocketAdaptor.send(JSON.stringify(jsCmd));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
this.initPeerConnection = function (streamId) {
|
|
|
if (thiz.remotePeerConnection[streamId] == null) {
|
|
|
var closedStreamId = streamId;
|
|
|
console.log("stream id in init peer connection: " + streamId + " close dstream id: " + closedStreamId);
|
|
|
thiz.remotePeerConnection[streamId] = new RTCPeerConnection(thiz.peerconnection_config);
|
|
|
thiz.remoteDescriptionSet[streamId] = false;
|
|
|
thiz.iceCandidateList[streamId] = new Array();
|
|
|
if (!thiz.playStreamId.includes(streamId)) {
|
|
|
thiz.remotePeerConnection[streamId].addStream(thiz.localStream);
|
|
|
}
|
|
|
thiz.remotePeerConnection[streamId].onicecandidate = function (event) {
|
|
|
thiz.iceCandidateReceived(event, closedStreamId);
|
|
|
}
|
|
|
thiz.remotePeerConnection[streamId].ontrack = function (event) {
|
|
|
thiz.onTrack(event, closedStreamId);
|
|
|
}
|
|
|
|
|
|
if (!thiz.isPlayMode) {
|
|
|
thiz.remotePeerConnection[streamId].oniceconnectionstatechange = function (event) {
|
|
|
|
|
|
if (thiz.remotePeerConnection[streamId].iceConnectionState == "connected") {
|
|
|
|
|
|
thiz.changeBandwidth(thiz.bandwidth, streamId).then(() => {
|
|
|
console.log("Bandwidth is changed to " + thiz.bandwidth);
|
|
|
})
|
|
|
.catch(e => console.error(e));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
this.closePeerConnection = function (streamId) {
|
|
|
if (thiz.remotePeerConnection[streamId] != null
|
|
|
&& thiz.remotePeerConnection[streamId].signalingState != "closed") {
|
|
|
thiz.remotePeerConnection[streamId].close();
|
|
|
thiz.remotePeerConnection[streamId] = null;
|
|
|
delete thiz.remotePeerConnection[streamId];
|
|
|
var playStreamIndex = thiz.playStreamId.indexOf(streamId);
|
|
|
if (playStreamIndex != -1) {
|
|
|
thiz.playStreamId.splice(playStreamIndex, 1);
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (thiz.remotePeerConnectionStats[streamId] != null) {
|
|
|
clearInterval(thiz.remotePeerConnectionStats[streamId].timerId);
|
|
|
delete thiz.remotePeerConnectionStats[streamId];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
this.signallingState = function (streamId) {
|
|
|
if (thiz.remotePeerConnection[streamId] != null) {
|
|
|
return thiz.remotePeerConnection[streamId].signalingState;
|
|
|
}
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
this.iceConnectionState = function (streamId) {
|
|
|
if (thiz.remotePeerConnection[streamId] != null) {
|
|
|
return thiz.remotePeerConnection[streamId].iceConnectionState;
|
|
|
}
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
this.gotDescription = function (configuration, streamId) {
|
|
|
thiz.remotePeerConnection[streamId]
|
|
|
.setLocalDescription(configuration)
|
|
|
.then(function (responose) {
|
|
|
console.debug("Set local description successfully for stream Id " + streamId);
|
|
|
|
|
|
var jsCmd = {
|
|
|
command: "takeConfiguration",
|
|
|
streamId: streamId,
|
|
|
type: configuration.type,
|
|
|
sdp: configuration.sdp
|
|
|
|
|
|
};
|
|
|
|
|
|
if (thiz.debug) {
|
|
|
console.debug("local sdp: ");
|
|
|
console.debug(configuration.sdp);
|
|
|
}
|
|
|
|
|
|
thiz.webSocketAdaptor.send(JSON.stringify(jsCmd));
|
|
|
|
|
|
}).catch(function (error) {
|
|
|
console.error("Cannot set local description. Error is: " + error);
|
|
|
});
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
this.turnOffLocalCamera = function () {
|
|
|
if (thiz.remotePeerConnection != null) {
|
|
|
|
|
|
var track = thiz.localStream.getVideoTracks()[0];
|
|
|
track.enabled = false;
|
|
|
} else {
|
|
|
this.callbackError("NoActiveConnection");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
this.turnOnLocalCamera = function () {
|
|
|
if (thiz.remotePeerConnection != null) {
|
|
|
var track = thiz.localStream.getVideoTracks()[0];
|
|
|
track.enabled = true;
|
|
|
} else {
|
|
|
this.callbackError("NoActiveConnection");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
this.muteLocalMic = function () {
|
|
|
if (thiz.remotePeerConnection != null) {
|
|
|
var track = thiz.localStream.getAudioTracks()[0];
|
|
|
track.enabled = false;
|
|
|
} else {
|
|
|
this.callbackError("NoActiveConnection");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* if there is audio it calls callbackError with "AudioAlreadyActive" parameter
|
|
|
*/
|
|
|
this.unmuteLocalMic = function () {
|
|
|
if (thiz.remotePeerConnection != null) {
|
|
|
var track = thiz.localStream.getAudioTracks()[0];
|
|
|
track.enabled = true;
|
|
|
} else {
|
|
|
this.callbackError("NoActiveConnection");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
this.takeConfiguration = function (idOfStream, configuration, typeOfConfiguration) {
|
|
|
var streamId = idOfStream
|
|
|
var type = typeOfConfiguration;
|
|
|
var conf = configuration;
|
|
|
|
|
|
thiz.initPeerConnection(streamId);
|
|
|
|
|
|
thiz.remotePeerConnection[streamId].setRemoteDescription(new RTCSessionDescription({
|
|
|
sdp: conf,
|
|
|
type: type
|
|
|
})).then(function (response) {
|
|
|
|
|
|
if (thiz.debug) {
|
|
|
console.debug("set remote description is succesfull with response: " + response + " for stream : "
|
|
|
+ streamId + " and type: " + type);
|
|
|
console.debug(conf);
|
|
|
}
|
|
|
|
|
|
thiz.remoteDescriptionSet[streamId] = true;
|
|
|
var length = thiz.iceCandidateList[streamId].length;
|
|
|
console.debug("Ice candidate list size to be added: " + length);
|
|
|
for (var i = 0; i < length; i++) {
|
|
|
thiz.addIceCandidate(streamId, thiz.iceCandidateList[streamId][i]);
|
|
|
}
|
|
|
thiz.iceCandidateList[streamId] = [];
|
|
|
|
|
|
if (type == "offer") {
|
|
|
//SDP constraints may be different in play mode
|
|
|
console.log("try to create answer for stream id: " + streamId);
|
|
|
|
|
|
thiz.remotePeerConnection[streamId].createAnswer(thiz.sdp_constraints)
|
|
|
.then(function (configuration) {
|
|
|
console.log("created answer for stream id: " + streamId);
|
|
|
thiz.gotDescription(configuration, streamId);
|
|
|
})
|
|
|
.catch(function (error) {
|
|
|
console.error("create answer error :" + error);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
}).catch(function (error) {
|
|
|
if (thiz.debug) {
|
|
|
console.error("set remote description is failed with error: " + error);
|
|
|
}
|
|
|
if (error.toString().indexOf("InvalidAccessError") > -1 || error.toString().indexOf("setRemoteDescription") > -1) {
|
|
|
/**
|
|
|
* This error generally occurs in codec incompatibility.
|
|
|
* AMS for a now supports H.264 codec. This error happens when some browsers try to open it from VP8.
|
|
|
*/
|
|
|
thiz.callbackError("notSetRemoteDescription");
|
|
|
}
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
this.takeCandidate = function (idOfTheStream, tmpLabel, tmpCandidate) {
|
|
|
var streamId = idOfTheStream;
|
|
|
var label = tmpLabel;
|
|
|
var candidateSdp = tmpCandidate;
|
|
|
|
|
|
var candidate = new RTCIceCandidate({
|
|
|
sdpMLineIndex: label,
|
|
|
candidate: candidateSdp
|
|
|
});
|
|
|
|
|
|
thiz.initPeerConnection(streamId);
|
|
|
|
|
|
if (thiz.remoteDescriptionSet[streamId] == true) {
|
|
|
thiz.addIceCandidate(streamId, candidate);
|
|
|
} else {
|
|
|
console.debug("Ice candidate is added to list because remote description is not set yet");
|
|
|
thiz.iceCandidateList[streamId].push(candidate);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
this.addIceCandidate = function (streamId, candidate) {
|
|
|
thiz.remotePeerConnection[streamId].addIceCandidate(candidate)
|
|
|
.then(function (response) {
|
|
|
if (thiz.debug) {
|
|
|
console.log("Candidate is added for stream " + streamId);
|
|
|
}
|
|
|
})
|
|
|
.catch(function (error) {
|
|
|
console.error("ice candiate cannot be added for stream id: " + streamId + " error is: " + error);
|
|
|
console.error(candidate);
|
|
|
});
|
|
|
};
|
|
|
|
|
|
this.startPublishing = function (idOfStream) {
|
|
|
var streamId = idOfStream;
|
|
|
|
|
|
thiz.initPeerConnection(streamId);
|
|
|
|
|
|
thiz.remotePeerConnection[streamId].createOffer(thiz.sdp_constraints)
|
|
|
.then(function (configuration) {
|
|
|
thiz.gotDescription(configuration, streamId);
|
|
|
})
|
|
|
.catch(function (error) {
|
|
|
console.error("create offer error for stream id: " + streamId + " error: " + error);
|
|
|
});
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* If we have multiple video tracks in coming versions, this method may cause some issues
|
|
|
*/
|
|
|
this.getVideoSender = function (streamId) {
|
|
|
|
|
|
var videoSender = null;
|
|
|
if ((adapter.browserDetails.browser === 'chrome' ||
|
|
|
(adapter.browserDetails.browser === 'firefox' &&
|
|
|
adapter.browserDetails.version >= 64)) &&
|
|
|
'RTCRtpSender' in window &&
|
|
|
'setParameters' in window.RTCRtpSender.prototype) {
|
|
|
const senders = thiz.remotePeerConnection[streamId].getSenders();
|
|
|
|
|
|
for (let i = 0; i < senders.length; i++) {
|
|
|
if (senders[i].track != null && senders[i].track.kind == "video") {
|
|
|
videoSender = senders[i];
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
}
|
|
|
return videoSender;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* bandwidth is in kbps
|
|
|
*/
|
|
|
this.changeBandwidth = function (bandwidth, streamId) {
|
|
|
|
|
|
var errorDefinition = "";
|
|
|
|
|
|
var videoSender = thiz.getVideoSender(streamId);
|
|
|
|
|
|
if (videoSender != null) {
|
|
|
const parameters = videoSender.getParameters();
|
|
|
|
|
|
if (!parameters.encodings) {
|
|
|
parameters.encodings = [{}];
|
|
|
}
|
|
|
|
|
|
if (bandwidth === 'unlimited') {
|
|
|
delete parameters.encodings[0].maxBitrate;
|
|
|
} else {
|
|
|
parameters.encodings[0].maxBitrate = bandwidth * 1000;
|
|
|
}
|
|
|
|
|
|
return videoSender.setParameters(parameters)
|
|
|
} else {
|
|
|
errorDefinition = "Video sender not found to change bandwidth";
|
|
|
}
|
|
|
|
|
|
return Promise.reject(errorDefinition);
|
|
|
};
|
|
|
|
|
|
this.getStats = function (streamId) {
|
|
|
thiz.remotePeerConnection[streamId].getStats(null).then(stats => {
|
|
|
var bytesReceived = 0;
|
|
|
var packetsLost = 0;
|
|
|
var fractionLost = 0;
|
|
|
var currentTime = 0;
|
|
|
var bytesSent = 0;
|
|
|
|
|
|
stats.forEach(value => {
|
|
|
|
|
|
if (value.type == "inbound-rtp") {
|
|
|
bytesReceived += value.bytesReceived;
|
|
|
packetsLost += value.packetsLost;
|
|
|
fractionLost += value.fractionLost;
|
|
|
currentTime = value.timestamp;
|
|
|
} else if (value.type == "outbound-rtp") {
|
|
|
bytesSent += value.bytesSent
|
|
|
currentTime = value.timestamp
|
|
|
}
|
|
|
});
|
|
|
|
|
|
thiz.remotePeerConnectionStats[streamId].totalBytesReceived = bytesReceived;
|
|
|
thiz.remotePeerConnectionStats[streamId].packetsLost = packetsLost;
|
|
|
thiz.remotePeerConnectionStats[streamId].fractionLost = fractionLost;
|
|
|
thiz.remotePeerConnectionStats[streamId].currentTime = currentTime;
|
|
|
thiz.remotePeerConnectionStats[streamId].totalBytesSent = bytesSent;
|
|
|
|
|
|
thiz.callback("updated_stats", thiz.remotePeerConnectionStats[streamId]);
|
|
|
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
this.enableStats = function (streamId) {
|
|
|
thiz.remotePeerConnectionStats[streamId] = new PeerStats(streamId);
|
|
|
thiz.remotePeerConnectionStats[streamId].timerId = setInterval(() => {
|
|
|
thiz.getStats(streamId);
|
|
|
|
|
|
}, 5000);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* After calling this function, create new WebRTCAdaptor instance, don't use the the same objectone
|
|
|
* Because all streams are closed on server side as well when websocket connection is closed.
|
|
|
*/
|
|
|
this.closeWebSocket = function () {
|
|
|
for (var key in thiz.remotePeerConnection) {
|
|
|
thiz.remotePeerConnection[key].close();
|
|
|
}
|
|
|
//free the remote peer connection by initializing again
|
|
|
thiz.remotePeerConnection = new Array();
|
|
|
thiz.webSocketAdaptor.close();
|
|
|
}
|
|
|
|
|
|
function WebSocketAdaptor() {
|
|
|
var wsConn = new WebSocket(thiz.websocket_url);
|
|
|
|
|
|
var connected = false;
|
|
|
|
|
|
var pingTimerId = -1;
|
|
|
|
|
|
var clearPingTimer = function () {
|
|
|
if (pingTimerId != -1) {
|
|
|
if (thiz.debug) {
|
|
|
console.debug("Clearing ping message timer");
|
|
|
}
|
|
|
clearInterval(pingTimerId);
|
|
|
pingTimerId = -1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
var sendPing = function () {
|
|
|
var jsCmd = {
|
|
|
command: "ping"
|
|
|
};
|
|
|
wsConn.send(JSON.stringify(jsCmd));
|
|
|
}
|
|
|
|
|
|
this.close = function () {
|
|
|
wsConn.close();
|
|
|
}
|
|
|
|
|
|
wsConn.onopen = function () {
|
|
|
if (thiz.debug) {
|
|
|
console.log("websocket connected");
|
|
|
}
|
|
|
|
|
|
pingTimerId = setInterval(() => {
|
|
|
sendPing();
|
|
|
}, 3000);
|
|
|
|
|
|
connected = true;
|
|
|
thiz.callback("initialized");
|
|
|
}
|
|
|
|
|
|
this.send = function (text) {
|
|
|
|
|
|
if (wsConn.readyState == 0 || wsConn.readyState == 2 || wsConn.readyState == 3) {
|
|
|
thiz.callbackError("WebSocketNotConnected");
|
|
|
return;
|
|
|
}
|
|
|
wsConn.send(text);
|
|
|
console.log("sent message:" + text);
|
|
|
}
|
|
|
|
|
|
this.isConnected = function () {
|
|
|
return connected;
|
|
|
}
|
|
|
|
|
|
wsConn.onmessage = function (event) {
|
|
|
var obj = JSON.parse(event.data);
|
|
|
|
|
|
if (obj.command == "start") {
|
|
|
//this command is received first, when publishing so playmode is false
|
|
|
|
|
|
if (thiz.debug) {
|
|
|
console.debug("received start command");
|
|
|
}
|
|
|
|
|
|
thiz.startPublishing(obj.streamId);
|
|
|
} else if (obj.command == "takeCandidate") {
|
|
|
|
|
|
if (thiz.debug) {
|
|
|
console.debug("received ice candidate for stream id " + obj.streamId);
|
|
|
console.debug(obj.candidate);
|
|
|
}
|
|
|
|
|
|
thiz.takeCandidate(obj.streamId, obj.label, obj.candidate);
|
|
|
|
|
|
} else if (obj.command == "takeConfiguration") {
|
|
|
|
|
|
if (thiz.debug) {
|
|
|
console.log("received remote description type for stream id: " + obj.streamId + " type: " + obj.type);
|
|
|
}
|
|
|
thiz.takeConfiguration(obj.streamId, obj.sdp, obj.type);
|
|
|
|
|
|
} else if (obj.command == "stop") {
|
|
|
console.debug("Stop command received");
|
|
|
thiz.closePeerConnection(obj.streamId);
|
|
|
} else if (obj.command == "error") {
|
|
|
thiz.callbackError(obj.definition);
|
|
|
} else if (obj.command == "notification") {
|
|
|
thiz.callback(obj.definition, obj);
|
|
|
if (obj.definition == "play_finished" || obj.definition == "publish_finished") {
|
|
|
thiz.closePeerConnection(obj.streamId);
|
|
|
}
|
|
|
} else if (obj.command == "streamInformation") {
|
|
|
thiz.callback(obj.command, obj);
|
|
|
} else if (obj.command == "pong") {
|
|
|
thiz.callback(obj.command);
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
wsConn.onerror = function (error) {
|
|
|
console.log(" error occured: " + JSON.stringify(error));
|
|
|
clearPingTimer();
|
|
|
thiz.callbackError(error)
|
|
|
}
|
|
|
|
|
|
wsConn.onclose = function (event) {
|
|
|
connected = false;
|
|
|
console.log("connection closed.");
|
|
|
clearPingTimer();
|
|
|
thiz.callback("closed", event);
|
|
|
}
|
|
|
|
|
|
|
|
|
}
|
|
|
}
|
|
|
module.exports = WebRTCAdaptor
|
|
|
|