UNPKG

hap-nodejs

Version:

HAP-NodeJS is a Node.js implementation of HomeKit Accessory Server.

267 lines 8.68 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const dgram_1 = tslib_1.__importDefault(require("dgram")); /** * RTPProxy to proxy unencrypted RTP and RTCP * * At early days of HomeKit camera support, HomeKit allowed for unencrypted RTP stream. * The proxy was created to deal with RTCP and SSRC related stuff from external streams back in that days. * Later HomeKit removed support for unencrypted stream so it’s mostly no longer useful anymore, only really for testing * with a custom HAP controller. * @group Camera */ class RTPProxy { options; startingPort = 10000; type; outgoingAddress; outgoingPort; incomingPayloadType; outgoingSSRC; incomingSSRC; outgoingPayloadType; disabled; incomingRTPSocket; incomingRTCPSocket; outgoingSocket; serverAddress; serverRTPPort; serverRTCPPort; constructor(options) { this.options = options; this.type = options.isIPV6 ? "udp6" : "udp4"; this.startingPort = 10000; this.outgoingAddress = options.outgoingAddress; this.outgoingPort = options.outgoingPort; this.incomingPayloadType = 0; this.outgoingSSRC = options.outgoingSSRC; this.disabled = options.disabled; this.incomingSSRC = null; this.outgoingPayloadType = null; } setup() { return this.createSocketPair(this.type) .then((sockets) => { this.incomingRTPSocket = sockets[0]; this.incomingRTCPSocket = sockets[1]; return this.createSocket(this.type); }).then((socket) => { this.outgoingSocket = socket; this.onBound(); }); } destroy() { if (this.incomingRTPSocket) { this.incomingRTPSocket.close(); } if (this.incomingRTCPSocket) { this.incomingRTCPSocket.close(); } if (this.outgoingSocket) { this.outgoingSocket.close(); } } incomingRTPPort() { const address = this.incomingRTPSocket.address(); if (typeof address !== "string") { return address.port; } throw new Error("Unsupported socket!"); } incomingRTCPPort() { const address = this.incomingRTCPSocket.address(); if (typeof address !== "string") { return address.port; } throw new Error("Unsupported socket!"); } outgoingLocalPort() { const address = this.outgoingSocket.address(); if (typeof address !== "string") { return address.port; } throw new Error("Unsupported socket!"); } setServerAddress(address) { this.serverAddress = address; } setServerRTPPort(port) { this.serverRTPPort = port; } setServerRTCPPort(port) { this.serverRTCPPort = port; } setIncomingPayloadType(pt) { this.incomingPayloadType = pt; } setOutgoingPayloadType(pt) { this.outgoingPayloadType = pt; } sendOut(msg) { // Just drop it if we're not setup yet, I guess. if (!this.outgoingAddress || !this.outgoingPort) { return; } this.outgoingSocket.send(msg, this.outgoingPort, this.outgoingAddress); } sendBack(msg) { // Just drop it if we're not setup yet, I guess. if (!this.serverAddress || !this.serverRTCPPort) { return; } this.outgoingSocket.send(msg, this.serverRTCPPort, this.serverAddress); } onBound() { if (this.disabled) { return; } this.incomingRTPSocket.on("message", msg => { this.rtpMessage(msg); }); this.incomingRTCPSocket.on("message", msg => { this.rtcpMessage(msg); }); this.outgoingSocket.on("message", msg => { this.rtcpReply(msg); }); } rtpMessage(msg) { if (msg.length < 12) { // Not a proper RTP packet. Just forward it. this.sendOut(msg); return; } let mpt = msg.readUInt8(1); const pt = mpt & 0x7F; if (pt === this.incomingPayloadType) { mpt = (mpt & 0x80) | this.outgoingPayloadType; msg.writeUInt8(mpt, 1); } if (this.incomingSSRC === null) { this.incomingSSRC = msg.readUInt32BE(4); } msg.writeUInt32BE(this.outgoingSSRC, 8); this.sendOut(msg); } processRTCPMessage(msg, transform) { const rtcpPackets = []; let offset = 0; while ((offset + 4) <= msg.length) { const pt = msg.readUInt8(offset + 1); const len = msg.readUInt16BE(offset + 2) * 4; if ((offset + 4 + len) > msg.length) { break; } let packet = msg.slice(offset, offset + 4 + len); packet = transform(pt, packet); if (packet) { rtcpPackets.push(packet); } offset += 4 + len; } if (rtcpPackets.length > 0) { return Buffer.concat(rtcpPackets); } return null; } rtcpMessage(msg) { const processed = this.processRTCPMessage(msg, (pt, packet) => { if (pt !== 200 || packet.length < 8) { return packet; } if (this.incomingSSRC === null) { this.incomingSSRC = packet.readUInt32BE(4); } packet.writeUInt32BE(this.outgoingSSRC, 4); return packet; }); if (processed) { this.sendOut(processed); } } rtcpReply(msg) { const processed = this.processRTCPMessage(msg, (pt, packet) => { if (pt !== 201 || packet.length < 12) { return packet; } // Assume source 1 is the one we want to edit. packet.writeUInt32BE(this.incomingSSRC, 8); return packet; }); if (processed) { this.sendOut(processed); } } createSocket(type) { return new Promise(resolve => { const retry = () => { const socket = dgram_1.default.createSocket(type); const bindErrorHandler = () => { if (this.startingPort === 65535) { this.startingPort = 10000; } else { ++this.startingPort; } socket.close(); retry(); }; socket.once("error", bindErrorHandler); socket.on("listening", () => { resolve(socket); }); socket.bind(this.startingPort); }; retry(); }); } createSocketPair(type) { return new Promise(resolve => { const retry = () => { const socket1 = dgram_1.default.createSocket(type); const socket2 = dgram_1.default.createSocket(type); const state = { socket1: 0, socket2: 0 }; const recheck = () => { if (state.socket1 === 0 || state.socket2 === 0) { return; } if (state.socket1 === 2 && state.socket2 === 2) { resolve([socket1, socket2]); return; } if (this.startingPort === 65534) { this.startingPort = 10000; } else { ++this.startingPort; } socket1.close(); socket2.close(); retry(); }; socket1.once("error", () => { state.socket1 = 1; recheck(); }); socket2.once("error", () => { state.socket2 = 1; recheck(); }); socket1.once("listening", () => { state.socket1 = 2; recheck(); }); socket2.once("listening", () => { state.socket2 = 2; recheck(); }); socket1.bind(this.startingPort); socket2.bind(this.startingPort + 1); }; retry(); }); } } exports.default = RTPProxy; //# sourceMappingURL=RTPProxy.js.map