@mattkrick/fast-rtc-peer
Version:
a small RTC client for connecting 2 peers
486 lines (473 loc) • 19.9 kB
JavaScript
module.exports =
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./src/FastRTCPeer.ts");
/******/ })
/************************************************************************/
/******/ ({
/***/ "./src/FastRTCPeer.ts":
/*!****************************!*\
!*** ./src/FastRTCPeer.ts ***!
\****************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var eventemitter3__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! eventemitter3 */ "eventemitter3");
/* harmony import */ var eventemitter3__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(eventemitter3__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var shortid__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! shortid */ "shortid");
/* harmony import */ var shortid__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(shortid__WEBPACK_IMPORTED_MODULE_1__);
const replyWithTrack = async (transceiver, trackConfigOrKind) => {
if (trackConfigOrKind && typeof trackConfigOrKind !== 'string') {
const { track, setParameters } = trackConfigOrKind;
transceiver.direction = 'sendrecv';
if (setParameters) {
await setParameters(transceiver.sender);
}
await transceiver.sender.replaceTrack(track);
}
};
class FastRTCPeer extends eventemitter3__WEBPACK_IMPORTED_MODULE_0___default.a {
constructor(userConfig = {}) {
super();
this.dataChannelQueue = [];
this.remoteStreams = {};
this.streamConfig = [];
this.midsWithoutNames = new Set();
this.pendingTransceivers = [];
this.negotiationCount = 0;
this.midLookup = {};
this.dataChannel = null;
this.onIceCandidate = (event) => {
const payload = { type: 'candidate', candidate: event.candidate };
this.emit('signal', payload, this);
};
this.onIceConnectionStateChange = () => {
const { iceConnectionState } = this.peerConnection;
switch (iceConnectionState) {
case 'closed':
case 'failed':
this.close();
break;
}
this.emit('connection', iceConnectionState, this);
};
this.onNegotiationNeeded = async () => {
const neg = ++this.negotiationCount;
const offer = await this.peerConnection.createOffer();
if (neg !== this.negotiationCount)
return;
await this.peerConnection.setLocalDescription(offer).catch((e) => {
this.emit('error', e, this);
});
this.pendingTransceivers.slice().forEach(({ transceiver: { mid }, transceiverName }, idx) => {
if (!mid)
return;
this.midLookup[transceiverName] = mid;
this.sendInternal({ type: 'midOffer', mid, transceiverName });
this.pendingTransceivers.splice(idx, 1);
});
const { sdp, type } = offer;
this.emit('signal', { sdp, type }, this);
};
this.addTrackToStream = (transceiverName, track) => {
const streamNames = new Set(this.streamConfig
.filter((config) => config.transceiverName === transceiverName)
.map(({ streamName }) => streamName));
streamNames.forEach((streamName) => {
let stream = this.remoteStreams[streamName];
if (!stream) {
stream = this.remoteStreams[streamName] = new MediaStream([track]);
}
else {
stream.addTrack(track);
}
const streamCount = this.streamConfig.filter((config) => config.streamName === streamName)
.length;
if (streamCount === stream.getTracks().length) {
this.emit('stream', stream, streamName, this);
}
});
};
this.onTrack = async (e) => {
const { track, transceiver } = e;
const transceiverName = Object.keys(this.midLookup).find((name) => this.midLookup[name] === transceiver.mid);
if (transceiverName) {
this.addTrackToStream(transceiverName, track);
}
else {
if (!transceiver.mid)
throw new Error('No mid in onTrack');
this.midsWithoutNames.add(transceiver.mid);
}
};
this.handleOffer = async (initSdp) => {
const remoteSdp = new this.wrtc.RTCSessionDescription(initSdp);
await this.peerConnection.setRemoteDescription(remoteSdp).catch((e) => {
this.emit('error', e, this);
});
const answer = await this.peerConnection.createAnswer();
const { sdp, type } = answer;
this.emit('signal', { type, sdp }, this);
await this.peerConnection.setLocalDescription(answer).catch((e) => {
this.emit('error', e, this);
});
};
this.handleInternalMessage = (data) => {
if (typeof data !== 'string' || !data.startsWith('@fast'))
return false;
const payload = JSON.parse(data.substring(6));
switch (payload.type) {
case 'close':
this.close();
break;
case 'midOffer':
this.midLookup[payload.transceiverName] = payload.mid;
if (this.midsWithoutNames.has(payload.mid)) {
this.midsWithoutNames.delete(payload.mid);
const transceiver = this.peerConnection
.getTransceivers()
.find(({ mid }) => mid === payload.mid);
if (!transceiver)
throw new Error(`No transceiver exists with mid: ${payload.mid}`);
this.addTrackToStream(payload.transceiverName, transceiver.receiver.track);
const { trackConfigOrKind } = this.streamConfig.find(({ transceiverName }) => transceiverName === payload.transceiverName);
replyWithTrack(transceiver, trackConfigOrKind).catch();
}
break;
case 'transceiverRequest':
if (!this.midLookup[payload.transceiverName] &&
!this.pendingTransceivers.some(({ transceiverName }) => transceiverName === payload.transceiverName)) {
this.setupTransceiver(payload.transceiverName, payload.kind);
}
break;
case 'stream':
const { streamName, transceiverNames } = payload;
transceiverNames.forEach((transceiverName) => {
const existingConfig = this.streamConfig.find((config) => config.streamName === streamName && config.transceiverName === transceiverName);
if (!existingConfig) {
this.streamConfig.push({ streamName, transceiverName, trackConfigOrKind: null });
}
});
break;
}
return true;
};
this.setChannelEvents = (channel) => {
channel.onmessage = (event) => {
if (!this.handleInternalMessage(event.data)) {
this.emit('data', event.data, this);
}
};
channel.onopen = () => {
this.dataChannel = channel;
this.dataChannelQueue.forEach((payload) => this.sendInternal(payload));
this.dataChannelQueue.length = 0;
this.emit('open', this);
};
channel.onclose = () => {
this.emit('close', this);
};
};
this.addStreams = (streams) => {
if (!this.peerConnection)
return;
Object.keys(streams).forEach((streamName) => {
const trackDict = streams[streamName];
const transceiverNames = Object.keys(trackDict);
this.sendInternal({ type: 'stream', streamName, transceiverNames });
transceiverNames.forEach((transceiverName) => {
const trackConfigOrKind = trackDict[transceiverName];
this.upsertStreamConfig(streamName, transceiverName, trackConfigOrKind);
if (trackConfigOrKind) {
this.setTrack(transceiverName, trackConfigOrKind);
}
});
});
};
this.send = (data) => {
var _a;
try {
(_a = this.dataChannel) === null || _a === void 0 ? void 0 : _a.send(data);
}
catch (e) {
this.emit('error', e, this);
}
};
const { id, isOfferer, userId, streams, wrtc, rtcConfig = {} } = userConfig;
this.id = id || FastRTCPeer.generateID();
this.isOfferer = isOfferer || false;
this.userId = userId || null;
this.wrtc = wrtc || window;
const { RTCPeerConnection } = this.wrtc;
if (!RTCPeerConnection)
throw new Error('Client does not support WebRTC');
this.peerConnection = new RTCPeerConnection(Object.assign(Object.assign({}, FastRTCPeer.defaultConfig), rtcConfig));
this.setupPeer();
this.addStreams(FastRTCPeer.fromStreamShorthand(streams));
}
setupPeer() {
if (!this.peerConnection)
return;
this.peerConnection.onicecandidate = this.onIceCandidate;
this.peerConnection.oniceconnectionstatechange = this.onIceConnectionStateChange;
this.peerConnection.onnegotiationneeded = this.onNegotiationNeeded;
this.peerConnection.ontrack = this.onTrack;
this.addDataChannel('fast');
}
handleAnswer(initSDP) {
const desc = new this.wrtc.RTCSessionDescription(initSDP);
this.peerConnection.setRemoteDescription(desc).catch((e) => {
this.emit('error', e, this);
});
}
handleCandidate(candidateObj) {
if (!candidateObj)
return;
const candidate = new this.wrtc.RTCIceCandidate(candidateObj);
this.peerConnection.addIceCandidate(candidate).catch((e) => {
this.emit('error', e, this);
});
}
sendInternal(payload) {
if (!this.dataChannel || this.dataChannel.readyState !== 'open') {
this.dataChannelQueue.push(payload);
}
else {
try {
this.dataChannel.send(` /${JSON.stringify(payload)}`);
}
catch (e) {
this.dataChannelQueue.push(payload);
}
}
}
addDataChannel(label, dataChannelDict) {
if (this.isOfferer) {
const dataChannel = this.peerConnection.createDataChannel(label, dataChannelDict);
this.setChannelEvents(dataChannel);
}
else {
this.peerConnection.ondatachannel = (e) => {
this.peerConnection.ondatachannel = null;
this.setChannelEvents(e.channel);
};
}
}
setupTransceiver(transceiverName, defaultKind = 'video') {
const { trackConfigOrKind } = this.streamConfig.find((config) => config.transceiverName === transceiverName);
const trackOrKind = typeof trackConfigOrKind === 'string'
? trackConfigOrKind
: trackConfigOrKind
? trackConfigOrKind.track
: defaultKind;
this.negotiationCount++;
const transceiver = this.peerConnection.addTransceiver(trackOrKind);
this.pendingTransceivers.push({ transceiver, transceiverName });
}
getTransceiver(transceiverName) {
if (!this.peerConnection)
return;
const mid = this.midLookup[transceiverName];
return this.peerConnection.getTransceivers().find((transceiver) => transceiver.mid === mid);
}
setTrack(transceiverName, trackConfigOrKind) {
const existingTransceiver = this.getTransceiver(transceiverName);
if (existingTransceiver) {
replyWithTrack(existingTransceiver, trackConfigOrKind).catch();
}
else {
if (this.isOfferer) {
this.setupTransceiver(transceiverName);
}
else {
const kind = typeof trackConfigOrKind === 'string'
? trackConfigOrKind
: trackConfigOrKind.track.kind;
this.sendInternal({ type: 'transceiverRequest', transceiverName, kind });
}
}
}
upsertStreamConfig(streamName, transceiverName, trackConfigOrKind) {
const existingConfig = this.streamConfig.find((config) => config.streamName === streamName && config.transceiverName === transceiverName);
if (!existingConfig) {
this.streamConfig.push({ streamName, transceiverName, trackConfigOrKind });
}
else {
existingConfig.trackConfigOrKind = trackConfigOrKind;
}
}
muteTrack(transceiverName) {
const transceiver = this.getTransceiver(transceiverName);
if (!transceiver) {
throw new Error(`Invalid track name: ${name}`);
}
const { track } = transceiver.sender;
transceiver.sender.replaceTrack(null).catch();
transceiver.direction = 'recvonly';
if (track) {
track.enabled = false;
track.stop();
}
}
close() {
if (!this.peerConnection)
return;
this.peerConnection.ontrack = null;
this.peerConnection.onicecandidate = null;
this.peerConnection.oniceconnectionstatechange = null;
this.peerConnection.onnegotiationneeded = null;
this.peerConnection.ondatachannel = null;
if (this.dataChannel) {
this.sendInternal({ type: 'close' });
this.dataChannel.onmessage = null;
this.dataChannel = null;
}
this.peerConnection.close();
this.peerConnection = null;
}
dispatch(payload) {
switch (payload.type) {
case 'offer':
this.handleOffer(payload).catch();
break;
case 'candidate':
this.handleCandidate(payload.candidate);
break;
case 'answer':
this.handleAnswer(payload);
}
}
}
FastRTCPeer.defaultICEServers = [
{
urls: 'stun:stun.l.google.com:19302'
},
{
urls: 'stun:global.stun.twilio.com:3478?transport=udp'
}
];
FastRTCPeer.defaultConfig = {
iceServers: FastRTCPeer.defaultICEServers,
sdpSemantics: 'unified-plan'
};
FastRTCPeer.generateID = () => {
return shortid__WEBPACK_IMPORTED_MODULE_1___default.a.generate();
};
FastRTCPeer.fromStreamShorthand = (streams) => {
const returnStreams = {};
if (streams) {
Object.keys(streams).forEach((streamName) => {
const streamOrConfig = streams[streamName];
returnStreams[streamName] =
streamOrConfig instanceof MediaStream
? {
audio: { track: streamOrConfig.getAudioTracks()[0] },
video: { track: streamOrConfig.getVideoTracks()[0] }
}
: streamOrConfig || {};
});
}
return returnStreams;
};
/* harmony default export */ __webpack_exports__["default"] = (FastRTCPeer);
/***/ }),
/***/ "eventemitter3":
/*!********************************!*\
!*** external "eventemitter3" ***!
\********************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = require("eventemitter3");
/***/ }),
/***/ "shortid":
/*!**************************!*\
!*** external "shortid" ***!
\**************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = require("shortid");
/***/ })
/******/ });
//# sourceMappingURL=FastRTCPeer.js.map