roomrtc
Version:
RoomRTC enables quick development of webRTC
374 lines (328 loc) • 12.4 kB
JavaScript
const events = require("eventemitter2");
const adapter = require("webrtc-adapter");
const EventEmitter = events.EventEmitter2;
// TODO: Change class name PeerConnection to Peer
module.exports = class PeerConnection extends EventEmitter {
constructor(config, constraints) {
super();
this.logger = console;
this.config = config || {};
this.config.iceServers = this.config.iceServers || [];
this.config.constraints = this.config.constraints || {
offerToReceiveAudio: 1,
offerToReceiveVideo: 1
}
this.config.mediaAnswerConstraints = {
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
}
}
this.id = this.config.id;
this.sid = this.config.sid || Date.now().toString();
this.parent = this.config.parent;
this.localStream = this.parent.localStream;
this.stream = null;
this.pc = this.createRTCPeerConnection(this.parent.config.peerConnectionConfig, this.parent.config.peerConnectionConstraints);
// this.pc.addStream(this.localStream);
this.addStream(this.localStream);
// bind event to handle peer message
this.getLocalStreams = this.pc.getLocalStreams.bind(this.pc);
this.getRemoteStreams = this.pc.getRemoteStreams.bind(this.pc);
// expose event from peer connection
this.pc.onaddstream = this.emit.bind(this, "addStream");
this.pc.onremovestream = this.emit.bind(this, "removeStream");
this.pc.onnegotiationneeded = this.emit.bind(this, "negotiationNeeded");
// private event handler
this.pc.onicecandidate = this._onIceCandidate.bind(this);
this.pc.ondatachannel = this._onDataChannel.bind(this);
// proxy events to parent
this.onAny((event, value) => {
this.logger.debug("PeerConnection onAny event:", event, value);
this.parent.emit.call(this.parent, event, value);
});
// own events processing
// send offerMsg to signaling server
this.on("offer", offerMsg => {
this.logger.info('send msg offer', offerMsg);
this.send("offer", offerMsg);
});
// send answerMsg to signaling server
this.on("answer", answerMsg => {
this.send("answer", answerMsg);
});
// send candidateMsg
this.on("iceCandidate", candidate => {
this.send("iceCandidate", candidate);
});
this.on('iceEnd', offerMsg => {
this.parent.emit('iceEnd', this, offerMsg);
});
this.on("addStream", (event) => {
// TODO: Support multiple streams, save them to Set ?
let stream = event.stream;
this.stream = stream;
this.stream.psid = `${this.id || this.sid}_${this.stream.id}`;
for (let track of this.stream.getTracks()) {
track.addEventListener("ended", () => {
if (this.isAllTracksEnded(this.stream)) {
this.logger.debug("stream ended, id:", this.id);
this.end(stream);
}
// notify
this.parent.emit('removetrack', track);
});
}
this.stream.addEventListener('addtrack', (event) =>
{
let track = event.track;
this.logger.debug('stream "addtrack" event [track:%o]', track);
this.parent.emit('addtrack', track);
// Firefox does not implement 'stream.onremovetrack' so let's use 'track.ended'.
// But... track "ended" is neither fired.
// https://bugzilla.mozilla.org/show_bug.cgi?id=1347578
track.addEventListener('ended', () =>
{
if (this.isAllTracksEnded(this.stream)) {
this.logger.debug("stream ended, id:", this.id, track);
this.end(stream);
}
// notify
this.parent.emit('removetrack', track);
});
});
this.parent.emit("peerStreamAdded", this, stream);
});
}
/**
* check browser supports webrtc
*/
isSupportsPeerConnections() {
return typeof RTCPeerConnection !== 'undefined';
};
isAllTracksEnded(stream) {
for (let track of stream.getTracks()) {
if (track.readyState !== 'ended') {
return false;
}
}
return true;
}
createRTCPeerConnection(config, constraints) {
if (this.isSupportsPeerConnections()) {
return new RTCPeerConnection(config, constraints);
} else {
throw "The browser does not support WebRTC (RTCPeerConnection)";
}
}
start() {
this.offer(this.config.constraints);
}
end(stream) {
if (this.isClosed) return;
let hasListener = this.parent.emit("peerStreamRemoved", this, stream);
if (!hasListener) {
this.close();
this.isClosed = true;
this.parent.removePeerConnection(this);
}
}
/**
* Process local messages
*/
offer(constraints, cb) {
var callback = this._safeCallback(cb);
var mediaConstraints = constraints || this.config.constraints;
if (this.pc.signalingState === 'closed') return callback("Signaling state is closed");
// create offer
this.pc.createOffer(description => {
let offerMsg = {
type: "offer",
sdp: description.sdp
}
if (this.config.connectMediaServer) {
return this.emit("offer", offerMsg);
}
// set local sdp
this.pc.setLocalDescription(description, () => {
this.logger.debug("create offer success");
if (!cb) {
this.emit("offer", offerMsg);
} else {
callback(null, offerMsg);
}
}, (err) => {
this.emit("error", err);
callback(err);
});
}, (err) => {
this.emit("error", err);
callback(err);
}, mediaConstraints);
}
/**
* Create sdp answer
*/
answer(constraints, callback) {
callback = this._safeCallback(callback);
var mediaConstraints = constraints || this.config.mediaAnswerConstraints;
if (this.pc.signalingState === 'closed') return callback("Signaling state is closed");
// create an answer
this.pc.createAnswer(description => {
let answerMsg = {
type: "answer",
sdp: description.sdp
}
this.pc.setLocalDescription(description, () => {
this.logger.debug("create answer success");
this.emit("answer", answerMsg);
callback(null, answerMsg);
}, (err) => {
this.emit("error", err);
callback(err);
});
}, (err) => {
this.emit("error", err);
callback(err);
}, mediaConstraints);
}
/**
* Process remote messages
*/
processMessage(msg) {
this.logger.debug("Preparing proccess peer message", msg.type, msg);
if (msg.type === "offer") {
this.processMsgOffer(msg.payload, err => {
if (!err) {
this.answer(this.config.mediaAnswerConstraints, err => {
if (err) {
this.logger.error("Cannot create an answer message", err, msg);
} else {
this.logger.info("Sent the answer message to ", msg);
}
});
} else {
this.logger.error("Cannot process msgOffer:", err);
}
});
} else if (msg.type === "answer") {
this.processMsgAnswer(msg.payload, err => {
if (err) {
this.logger.error('Cannot process msgAnswer:', err, msg);
}
});
} else if (msg.type === "iceCandidate") {
this.processMsgCandidate(msg.payload, err => {
if (err) {
this.logger.error('Cannot process msgCandidate:', err, msg);
}
});
} else if (msg.type === "welcome") {
this.emit("welcome", msg);
} else {
this.logger.warn("Unknow message", msg);
}
}
processMsgOffer(msgOffer, callback) {
callback = this._safeCallback(callback);
let description = new RTCSessionDescription(msgOffer);
this.pc.setRemoteDescription(description, () => {
callback(null);
}, err => {
callback(err);
});
}
processMsgAnswer(msgAnswer, callback) {
callback = this._safeCallback(callback);
let description = new RTCSessionDescription(msgAnswer);
this.pc.setRemoteDescription(description, () => {
callback(null);
}, err => {
callback(err);
})
}
processMsgCandidate(msgIce, callback) {
callback = this._safeCallback(callback);
// IPv6 candidates are only accepted with a= syntax in addIceCandidate
// https://bugs.chromium.org/p/webrtc/issues/detail?id=3669
if (msgIce.candidate && msgIce.candidate.candidate.indexOf("a=") !== 0) {
msgIce.candidate.candidate = "a=" + msgIce.candidate.candidate;
}
let iceCandidate = new RTCIceCandidate(msgIce.candidate);
this.pc.addIceCandidate(iceCandidate, () => {}, (err) => {
this.emit("error", err);
});
callback(null);
}
/**
* Close the peer connection
*/
close() {
this.pc.close();
this.emit("close");
}
/**
* Add localStream to the peer connection
*/
addStream(stream) {
if (!stream) {
return this.logger.warn('Stream must not be null');
}
this.logger.debug("Got the stream!");
this.localStream = stream;
this.pc.addStream(stream);
}
/**
* Internal methods
*/
_safeCallback(cb) {
return cb || (() => 1);
}
_onIceCandidate(event) {
if (event.candidate) {
if (this.config.connectMediaServer) {
// do nothing.
this.logger.debug('Dont send ice candidate: ', this.id);
return;
}
let ice = event.candidate;
// let iceCandidate = new RTCIceCandidate(ice.candidate);
// this.pc.addIceCandidate(iceCandidate);
let iceCandidate = {
candidate: {
candidate: ice.candidate,
sdpMid: ice.sdpMid,
sdpMLineIndex: ice.sdpMLineIndex
}
}
// this.logger.debug("Got an ICE candidate: ", iceCandidate);
this.emit("iceCandidate", iceCandidate);
} else {
this.logger.debug("iceEnd_onIceCandidate", event);
let offerMsg = {
type: "offer",
sdp: this.pc.localDescription.sdp
}
this.emit("iceEnd", offerMsg);
}
}
_onDataChannel(event) {
let channel = event.channel;
this.logger.debug("add new channel:", channel);
this.emit("addChannel", channel);
}
/**
* Handle message
*/
// send via signaling channel
send(msgType, payload) {
let msg = {
to: this.id,
sid: this.sid,
type: msgType,
payload: payload
}
this.logger.debug("sending", msgType, msg);
this.parent.emit("message", msg);
}
}