UNPKG

@mattkrick/fast-rtc-peer

Version:

a small RTC client for connecting 2 peers

486 lines (473 loc) 19.9 kB
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(`@fast/${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