From 89d0c57ec752aacc4a2ee0a181d009b91c48bba8 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Tue, 14 Sep 2021 02:11:33 -0700 Subject: [PATCH] missing files --- plugins/homekit/src/intercom.ts | 109 +++++++++++++++++++++++++++++ plugins/homekit/src/rtp-demuxer.ts | 72 +++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 plugins/homekit/src/intercom.ts create mode 100644 plugins/homekit/src/rtp-demuxer.ts diff --git a/plugins/homekit/src/intercom.ts b/plugins/homekit/src/intercom.ts new file mode 100644 index 000000000..94b8586dd --- /dev/null +++ b/plugins/homekit/src/intercom.ts @@ -0,0 +1,109 @@ +import sdk from "@scrypted/sdk"; +import { listenZeroCluster } from "@scrypted/common/src/listen-cluster"; +import { FFMpegInput, Intercom, ScryptedDevice } from "@scrypted/sdk"; +import { createSocket, Socket, SocketType } from "dgram"; +import { createServer, Server } from "net"; +import child_process from "child_process"; +import { ffmpegLogInitialOutput } from "@scrypted/common/src/ffmpeg-helper"; +import { FFMpegRebroadcastSession, startRebroadcastSession } from "@scrypted/common/src/ffmpeg-rebroadcast"; + +const { mediaManager } = sdk; + +async function pickPort(socketType: SocketType) { + // const socket = createSocket(socketType); + // return await new Promise(resolve => socket.bind(0, () => { + // const { port } = socket.address(); + // socket.close(() => resolve(port)); + // })); + return Math.round(Math.abs(Math.random()) * 40000 + 10000); +} + +export class IntercomSession { + sdpReturnAudio: string; + sdpServer: Server; + session: FFMpegRebroadcastSession; + port: number; + heartbeatTimer: NodeJS.Timeout; + + constructor(public device: ScryptedDevice & Intercom, public socketType: SocketType, public address: string, public srtp: Buffer) { + + } + + async start(): Promise { + const sdpIpVersion = this.socketType === "udp6" ? "IP6 " : "IP4"; + this.port = await pickPort(this.socketType); + + // Session description protocol message that FFmpeg will share with HomeKit. + // SDP messages tell the other side of the connection what we're expecting to receive. + // + // Parameters are: + // v protocol version - always 0. + // o originator and session identifier. + // s session description. + // c connection information. + // t timestamps for the start and end of the session. + // m media type - audio, adhering to RTP/AVP, payload type 110. + // b bandwidth information - application specific, 24k. + // a=rtpmap payload type 110 corresponds to an MP4 stream. + // a=fmtp for payload type 110, use these format parameters. + // a=crypto crypto suite to use for this session. + this.sdpReturnAudio = [ + "v=0", + "o=- 0 0 IN " + sdpIpVersion + " 127.0.0.1", + "s=" + this.device.name + " Audio Talkback", + "c=IN " + sdpIpVersion + " " + this.address, + "t=0 0", + "m=audio " + this.port + " RTP/AVP 110", + "b=AS:24", + "a=rtpmap:110 MPEG4-GENERIC/16000/1", + "a=fmtp:110 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; config=F8F0212C00BC00", + "a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:" + this.srtp.toString("base64") + ].join("\n"); + + this.sdpServer = createServer(socket => { + this.sdpServer.close(); + socket.write(this.sdpReturnAudio); + socket.end(); + }); + const sdpPort = await listenZeroCluster(this.sdpServer); + console.log('sdp port', sdpPort); + + const ffmpegInput: FFMpegInput = { + inputArguments: [ + "-f", "sdp", + "-acodec", "libfdk_aac", + "-ac", '1', + "-i", `tcp://127.0.0.1:${sdpPort}`, + ] + }; + + this.session = await startRebroadcastSession(ffmpegInput, { + vcodec: ['-vn'], + acodec: ['-acodec', 'libfdk_aac', '-ac', '1'], + outputFormat: 'adts', + }); + + return this.session; + } + + // Send a regular heartbeat to FFmpeg to ensure the pipe remains open and the process alive. + heartbeat(socket: Socket, heartbeat: Buffer): void { + + // Clear the old heartbeat timer. + clearTimeout(this.heartbeatTimer); + + // Send a heartbeat to FFmpeg every few seconds to keep things open. FFmpeg has a five-second timeout + // in reading input, and we want to be comfortably within the margin for error to ensure the process + // continues to run. + this.heartbeatTimer = setTimeout(() => { + socket.send(heartbeat, this.port); + this.heartbeat(socket, heartbeat); + + }, 3.5 * 1000); + } + + destroy() { + this.sdpServer?.close(); + this.session?.kill(); + } +} \ No newline at end of file diff --git a/plugins/homekit/src/rtp-demuxer.ts b/plugins/homekit/src/rtp-demuxer.ts new file mode 100644 index 000000000..d1d411081 --- /dev/null +++ b/plugins/homekit/src/rtp-demuxer.ts @@ -0,0 +1,72 @@ +/* Copyright(C) 2017-2021, HJD (https://github.com/hjdhjd). All rights reserved. + * + * protect-rtp.ts: RTP-related utilities to slice and dice RTP streams. + * + * This module is heavily inspired by the homebridge and homebridge-camera-ffmpeg source code and + * borrows heavily from both. Thank you for your contributions to the HomeKit world. + */ +import { Socket } from "dgram"; +import { EventEmitter } from "stream"; + +// How often, in seconds, should we heartbeat FFmpeg in two-way audio sessions. This should be less than 5 seconds, which is +// FFmpeg's input timeout interval. +export const PROTECT_TWOWAY_HEARTBEAT_INTERVAL = 3.5; + +/* + * Here's the problem this class solves: FFmpeg doesn't support multiplexing RTP and RTCP data on a single UDP port (RFC 5761). + * If it did, we wouldn't need this workaround for HomeKit compatibility, which does multiplex RTP and RTCP over a single UDP port. + * + * This class inspects all packets coming in from inputPort and demultiplexes RTP and RTCP traffic to rtpPort and rtcpPort, respectively. + * + * Credit to @dgreif and @brandawg93 who graciously shared their code as a starting point, and their collaboration + * in answering the questions needed to bring all this together. A special thank you to @Sunoo for the many hours of + * discussion and brainstorming on this and other topics. + */ +export class RtpDemuxer extends EventEmitter { + private heartbeatTimer!: NodeJS.Timeout; + private heartbeatMsg!: Buffer; + + // Create an instance of RtpDemuxer. + constructor(public deviceName: string, public console: Console, public socket: Socket) { + super(); + // Catch errors when they happen on our demuxer. + this.socket.on("error", (error) => { + this.console.error("%s: RtpDemuxer Error: %s", this.deviceName, error); + this.socket.close(); + }); + + // Split the message into RTP and RTCP packets. + this.socket.on("message", (msg) => { + + // Send RTP packets to the RTP port. + if (this.isRtpMessage(msg)) { + this.emit('rtp', msg); + } else { + this.emit('rtcp', msg); + } + }); + + this.console.log("%s: Creating an RtpDemuxer instance - inbound port: %s, RTCP port: %s, RTP port: %s.", + this.deviceName); + } + + // Close the socket and cleanup. + public close(): void { + this.console.log("%s: Closing the RtpDemuxer instance on port %s.", this.deviceName); + + clearTimeout(this.heartbeatTimer); + this.socket.close(); + } + + // Retrieve the payload information from a packet to discern what the packet payload is. + private getPayloadType(message: Buffer): number { + return message.readUInt8(1) & 0x7f; + } + + // Return whether or not a packet is RTP (or not). + private isRtpMessage(message: Buffer): boolean { + const payloadType = this.getPayloadType(message); + + return (payloadType > 90) || (payloadType === 0); + } +}