From 6b5c3f767742cb1f122a8e716839b8122609a2b2 Mon Sep 17 00:00:00 2001 From: Nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Sat, 15 Jan 2022 12:47:04 +0100 Subject: [PATCH 1/9] First working two-way audio SIP call --- package-lock.json | 73 +++++++++- package.json | 4 +- src/sip-call.ts | 291 +++++++++++++++++++++++++++++++++++++++ src/streamingDelegate.ts | 67 ++++++++- 4 files changed, 424 insertions(+), 11 deletions(-) create mode 100644 src/sip-call.ts diff --git a/package-lock.json b/package-lock.json index b9480960..9b7921b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,9 @@ "dependencies": { "ffmpeg-for-homebridge": "^0.0.9", "mqtt": "4.2.8", - "pick-port": "^1.0.0" + "pick-port": "^1.0.0", + "sdp": "3.0.3", + "sip": "0.0.6" }, "devDependencies": { "@types/node": "^17.0.5", @@ -158,9 +160,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.5.tgz", - "integrity": "sha512-w3mrvNXLeDYV1GKTZorGJQivK6XLCoGwpnyJFbJVK/aTBQUxOCaa/GlFAAN3OTDFcb7h5tiFG+YXCO2By+riZw==", + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz", + "integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==", "dev": true }, "node_modules/@types/ws": { @@ -420,6 +422,11 @@ "node": ">=8" } }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -2417,6 +2424,11 @@ } ] }, + "node_modules/sdp": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.0.3.tgz", + "integrity": "sha512-8EkfckS+XZQaPLyChu4ey7PghrdcraCVNpJe2Gfdi2ON1ylQ7OasuKX+b37R9slnRChwIAiQgt+oj8xXGD8x+A==" + }, "node_modules/semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", @@ -2496,6 +2508,25 @@ "simple-concat": "^1.0.0" } }, + "node_modules/sip": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/sip/-/sip-0.0.6.tgz", + "integrity": "sha512-t+FYic4EQ25GTsIRWFVvsq+GmVkoZhrcoghANlnN6CsWMHGcfjPDYMD+nTBNrHR/WnRykF4nqx4i+gahAnW5NA==", + "dependencies": { + "ws": "^6.1.0" + }, + "engines": { + "node": ">=0.2.2" + } + }, + "node_modules/sip/node_modules/ws": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -2974,9 +3005,9 @@ "dev": true }, "@types/node": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.5.tgz", - "integrity": "sha512-w3mrvNXLeDYV1GKTZorGJQivK6XLCoGwpnyJFbJVK/aTBQUxOCaa/GlFAAN3OTDFcb7h5tiFG+YXCO2By+riZw==", + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz", + "integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==", "dev": true }, "@types/ws": { @@ -3135,6 +3166,11 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -4577,6 +4613,11 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, + "sdp": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.0.3.tgz", + "integrity": "sha512-8EkfckS+XZQaPLyChu4ey7PghrdcraCVNpJe2Gfdi2ON1ylQ7OasuKX+b37R9slnRChwIAiQgt+oj8xXGD8x+A==" + }, "semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", @@ -4627,6 +4668,24 @@ "simple-concat": "^1.0.0" } }, + "sip": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/sip/-/sip-0.0.6.tgz", + "integrity": "sha512-t+FYic4EQ25GTsIRWFVvsq+GmVkoZhrcoghANlnN6CsWMHGcfjPDYMD+nTBNrHR/WnRykF4nqx4i+gahAnW5NA==", + "requires": { + "ws": "^6.1.0" + }, + "dependencies": { + "ws": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", diff --git a/package.json b/package.json index 72071e88..ce97118a 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,8 @@ "dependencies": { "ffmpeg-for-homebridge": "^0.0.9", "mqtt": "4.2.8", - "pick-port": "^1.0.0" + "pick-port": "^1.0.0", + "sdp": "3.0.3", + "sip": "0.0.6" } } diff --git a/src/sip-call.ts b/src/sip-call.ts new file mode 100644 index 00000000..a8942c15 --- /dev/null +++ b/src/sip-call.ts @@ -0,0 +1,291 @@ +//import { noop, Subject } from 'rxjs' +import { Logger } from './logger'; + +const sip = require('sip'), + sdp = require('sdp') + +export interface RtpStreamOptions { + port: number + rtcpPort: number + ssrc?: number +} + +export interface RtpOptions { + audio: RtpStreamOptions +} + +export interface RtpDescription { + address: string + audio: RtpStreamOptions +} + +interface UriOptions { + name?: string + uri: string + params?: { tag?: string } +} + +interface SipHeaders { + [name: string]: string | any + cseq: { seq: number; method: string } + to: UriOptions + from: UriOptions + contact?: UriOptions[] + via?: UriOptions[] +} + +export interface SipRequest { + uri: UriOptions | string + method: string + headers: SipHeaders + content: string +} + +export interface SipResponse { + status: number + reason: string + headers: SipHeaders + content: string +} + +export interface SipClient { + send: ( + request: SipRequest | SipResponse, + handler?: (response: SipResponse) => void + ) => void + destroy: () => void + makeResponse: ( + response: SipRequest, + status: number, + method: string + ) => SipResponse +} + +export interface SipOptions { + to: string + from: string + localIp: string +} + +function getRandomId() { + return Math.floor(Math.random() * 1e6).toString() +} + +function getRtpDescription( + log: Logger, + sections: string[], + mediaType: 'audio' +): RtpStreamOptions { + try { + const section = sections.find((s) => s.startsWith('m=' + mediaType)) + + const { port } = sdp.parseMLine(section), + lines: string[] = sdp.splitLines(section), + rtcpLine = lines.find((l: string) => l.startsWith('a=rtcp:')), + ssrcLine = lines.find((l: string) => l.startsWith('a=ssrc')) + + return { + port, + rtcpPort: (rtcpLine && Number(rtcpLine.match(/rtcp:(\S*)/)?.[1])) || port+1, + ssrc: (ssrcLine && Number(ssrcLine.match(/ssrc:(\S*)/)?.[1])) || undefined + } + } catch (e) { + log.error('Failed to parse SDP from remote end') + log.error(sections.join('\r\n')) + throw e + } +} + +function parseRtpDescription(log: Logger, inviteResponse: { + content: string +}): RtpDescription { + const sections: string[] = sdp.splitSections(inviteResponse.content), + lines: string[] = sdp.splitLines(sections[0]), + cLine = lines.find((line: string) => line.startsWith('c='))! + + return { + address: cLine.match(/c=IN IP4 (\S*)/)![1], + audio: getRtpDescription(log, sections, 'audio') + } +} + +export class SipCall { + private seq = 20 + private fromParams = { tag: getRandomId() } + private toParams: { tag?: string } = {} + private callId = getRandomId() + private sipClient: SipClient + //public readonly onEndedByRemote = new Subject() + private destroyed = false + private readonly log: Logger + + public readonly sdp: string + + constructor( + log: Logger, + private sipOptions: SipOptions, + rtpOptions: RtpOptions, + ) { + this.log = log; + const { audio } = rtpOptions, + { from } = this.sipOptions, + host = this.sipOptions.localIp + + this.sipClient = { + makeResponse: sip.makeResponse, + ...sip.create({ + host, + hostname: host, + tcp: false, + udp: true, + }, + (request: SipRequest) => { + if (request.method === 'BYE') { + this.log.info('received BYE from remote end') + this.sipClient.send(this.sipClient.makeResponse(request, 200, 'Ok')) + + if (this.destroyed) { + //this.onEndedByRemote.next(null) + } + } + } + )} + + this.sdp = [ + 'v=0', + `o=${from.split(':')[1].split('@')[0]} 3747 461 IN IP4 ${host}`, + 's=Talk', + `c=IN IP4 ${host}`, + 't=0 0', + `m=audio ${audio.port} RTP/AVP 0`, + 'a=rtpmap:0 PCMU/8000', + `a=rtcp:${audio.rtcpPort}`, + 'a=ssrc:2315747900', + 'a=sendrecv' + ] + .filter((l) => l) + .join('\r\n') + this.sdp = this.sdp += '\r\n' + } + + request({ + method, + headers, + content, + seq, + }: { + method: string + headers?: Partial + content?: string + seq?: number + }) { + if (this.destroyed) { + return Promise.reject( + new Error('SIP request made after call was destroyed') + ) + } + + return new Promise((resolve, reject) => { + seq = seq || this.seq++ + this.sipClient.send( + { + method, + uri: this.sipOptions.to, + headers: { + to: { + name: '"SIP doorbell client"', + uri: this.sipOptions.to, + params: this.toParams, + }, + from: { + uri: this.sipOptions.from, + params: this.fromParams, + }, + 'max-forwards': 70, + 'call-id': this.callId, + cseq: { seq, method }, + ...headers, + }, + content: content || '', + }, + (response: SipResponse) => { + if (response.headers.to.params && response.headers.to.params.tag) { + this.toParams.tag = response.headers.to.params.tag + } + + if (response.status >= 300) { + if (response.status !== 408 || method !== 'BYE') { + this.log.error( + `sip ${method} request failed with status ` + response.status + ) + } + reject( + new Error( + `sip ${method} request failed with status ` + response.status + ) + ) + } else if (response.status < 200) { + // call made progress, do nothing and wait for another response + // console.log('call progress status ' + response.status) + } else { + if (method === 'INVITE') { + // The ACK must be sent with every OK to keep the connection alive. + this.ackWithInfo(seq!).catch((e) => { + this.log.error('Failed to send SDP ACK and INFO') + this.log.error(e) + }) + } + resolve(response) + } + } + ) + }) + } + + private async ackWithInfo(seq: number) { + // Don't wait for ack, it won't ever come back. + this.request({ + method: 'ACK', + seq, // The ACK must have the original sequence number. + //}).catch(noop) + }).catch() + } + + sendDtmf(key: string) { + return this.request({ + method: 'INFO', + headers: { + 'Content-Type': 'application/dtmf-relay', + }, + content: `Signal=${key}\r\nDuration=250`, + }) + } + + async invite() { + const { from } = this.sipOptions, + inviteResponse = await this.request({ + method: 'INVITE', + headers: { + supported: 'replaces, outbound', + allow: + 'INVITE, ACK, CANCEL, OPTIONS, BYE, REFER, NOTIFY, MESSAGE, SUBSCRIBE, INFO, UPDATE', + 'content-type': 'application/sdp', + contact: [{ uri: from }], + }, + content: this.sdp, + }) + + return parseRtpDescription(this.log, inviteResponse) + } + + sendBye() { + return this.request({ method: 'BYE' }).catch(() => { + // Don't care if we get an exception here. + }) + } + + destroy() { + this.destroyed = true + this.sipClient.destroy() + } +} diff --git a/src/streamingDelegate.ts b/src/streamingDelegate.ts index 93f6727d..0a02d501 100644 --- a/src/streamingDelegate.ts +++ b/src/streamingDelegate.ts @@ -26,6 +26,7 @@ import pickPort, { pickPortOptions } from 'pick-port'; import { CameraConfig, VideoConfig } from './configTypes'; import { FfmpegProcess } from './ffmpeg'; import { Logger } from './logger'; +import { RtpDescription, RtpOptions, SipCall, SipOptions } from './sip-call' type SessionInfo = { address: string; // address of the HAP controller @@ -42,6 +43,12 @@ type SessionInfo = { audioCryptoSuite: SRTPCryptoSuites; audioSRTP: Buffer; audioSSRC: number; + + sipOptions: SipOptions; + rtpOptions: RtpOptions; + + sipCall?: SipCall; + rtpDescription?: RtpDescription; }; type ResolutionInfo = { @@ -57,6 +64,7 @@ type ActiveSession = { returnProcess?: FfmpegProcess; timeout?: NodeJS.Timeout; socket?: Socket; + sipCall?: SipCall; }; export class StreamingDelegate implements CameraStreamingDelegate { @@ -68,7 +76,7 @@ export class StreamingDelegate implements CameraStreamingDelegate { private readonly videoProcessor: string; readonly controller: CameraController; private snapshotPromise?: Promise; - + // keep track of sessions pendingSessions: Map = new Map(); ongoingSessions: Map = new Map(); @@ -114,7 +122,8 @@ export class StreamingDelegate implements CameraStreamingDelegate { } }, audio: { - twoWayAudio: !!this.videoConfig.returnAudioTarget, + //twoWayAudio: !!this.videoConfig.returnAudioTarget, + twoWayAudio: true, codecs: [ { type: AudioStreamingCodecType.AAC_ELD, @@ -285,6 +294,10 @@ export class StreamingDelegate implements CameraStreamingDelegate { const audioReturnPort = await pickPort(options); const audioSSRC = this.hap.CameraController.generateSynchronisationSource(); + //const sipAudioPort = await pickPort(options); + const sipAudioPort = 12346; + const sipAudioSSRC = this.hap.CameraController.generateSynchronisationSource(); + const sessionInfo: SessionInfo = { address: request.targetAddress, ipv6: ipv6, @@ -299,7 +312,21 @@ export class StreamingDelegate implements CameraStreamingDelegate { audioReturnPort: audioReturnPort, audioCryptoSuite: request.audio.srtpCryptoSuite, audioSRTP: Buffer.concat([request.audio.srtp_key, request.audio.srtp_salt]), - audioSSRC: audioSSRC + audioSSRC: audioSSRC, + + sipOptions: { + to: "sip:11@10.10.10.80", + from: "sip:user1@10.10.10.126", + localIp: "10.10.10.126", + }, + + rtpOptions: { + audio: { + port: sipAudioPort, + rtcpPort: sipAudioPort + 1, + ssrc: sipAudioSSRC + } + } }; const response: PrepareStreamResponse = { @@ -319,6 +346,9 @@ export class StreamingDelegate implements CameraStreamingDelegate { } }; + sessionInfo.sipCall = new SipCall(this.log, sessionInfo.sipOptions, sessionInfo.rtpOptions) + sessionInfo.rtpDescription = await sessionInfo.sipCall.invite() + this.pendingSessions.set(request.sessionID, sessionInfo); callback(undefined, response); } @@ -359,6 +389,13 @@ export class StreamingDelegate implements CameraStreamingDelegate { let ffmpegArgs = this.videoConfig.source!; + ffmpegArgs += // SIP audio via RTP + '-hide_banner' + + ' -protocol_whitelist pipe,udp,rtp,file,crypto' + + ' -f sdp' + + ' -c:a pcm_mulaw' + + ' -i pipe:'; + ffmpegArgs += // Video (this.videoConfig.mapvideo ? ' -map ' + this.videoConfig.mapvideo : ' -an -sn -dn') + ' -codec:v ' + vcodec + @@ -429,8 +466,29 @@ export class StreamingDelegate implements CameraStreamingDelegate { }); activeSession.socket.bind(sessionInfo.videoReturnPort); + if (sessionInfo.rtpDescription) + { + this.videoConfig.returnAudioTarget = + ' -acodec pcm_mulaw' + + ' -ac 1' + + ' -ar 8k' + + ' -f rtp' + + (sessionInfo.rtpDescription.audio.ssrc !== undefined ? ' -ssrc ' + sessionInfo.rtpDescription.audio.ssrc : '') + + ' -payload_type 0' + + ' rtp://' + sessionInfo.rtpDescription.address + ':' + sessionInfo.rtpDescription.audio.port + '?rtcpport=' + sessionInfo.rtpDescription.audio.rtcpPort; + } + + const sdpAudio = + 'v=0\r\n' + + 'o=- 0 0 IN IP4 ' + sessionInfo.address + '\r\n' + + 's=Talk\r\n' + + 'c=IN IP4 ' + sessionInfo.address + '\r\n' + + 't=0 0\r\n' + + 'm=audio ' + sessionInfo.rtpOptions.audio.port + ' RTP/AVP 0\r\n' + + 'a=rtpmap:0 PCMU/8000\r\n'; activeSession.mainProcess = new FfmpegProcess(this.cameraName, request.sessionID, this.videoProcessor, ffmpegArgs, this.log, this.videoConfig.debug, this, callback); + activeSession.mainProcess.stdin.end(sdpAudio); if (this.videoConfig.returnAudioTarget) { const ffmpegReturnArgs = @@ -463,6 +521,8 @@ export class StreamingDelegate implements CameraStreamingDelegate { activeSession.returnProcess.stdin.end(sdpReturnAudio); } + activeSession.sipCall = sessionInfo.sipCall; + this.ongoingSessions.set(request.sessionID, activeSession); this.pendingSessions.delete(request.sessionID); } else { @@ -509,6 +569,7 @@ export class StreamingDelegate implements CameraStreamingDelegate { } catch (err) { this.log.error('Error occurred terminating two-way FFmpeg process: ' + err, this.cameraName); } + session.sipCall?.destroy(); } this.ongoingSessions.delete(sessionId); this.log.info('Stopped video stream.', this.cameraName); From d3d5f2ec0f5d8387f78220e7c255e618bd7e70d1 Mon Sep 17 00:00:00 2001 From: Nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Tue, 18 Jan 2022 10:20:43 +0100 Subject: [PATCH 2/9] Cleanup --- src/sip-call.ts | 23 +++++++++++------------ src/streamingDelegate.ts | 27 ++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/sip-call.ts b/src/sip-call.ts index a8942c15..e961c22c 100644 --- a/src/sip-call.ts +++ b/src/sip-call.ts @@ -1,4 +1,3 @@ -//import { noop, Subject } from 'rxjs' import { Logger } from './logger'; const sip = require('sip'), @@ -115,10 +114,9 @@ export class SipCall { private toParams: { tag?: string } = {} private callId = getRandomId() private sipClient: SipClient - //public readonly onEndedByRemote = new Subject() + public onEndedByRemote?: () => void private destroyed = false private readonly log: Logger - public readonly sdp: string constructor( @@ -145,7 +143,9 @@ export class SipCall { this.sipClient.send(this.sipClient.makeResponse(request, 200, 'Ok')) if (this.destroyed) { - //this.onEndedByRemote.next(null) + if (this.onEndedByRemote) { + this.onEndedByRemote() + } } } } @@ -158,10 +158,10 @@ export class SipCall { `c=IN IP4 ${host}`, 't=0 0', `m=audio ${audio.port} RTP/AVP 0`, - 'a=rtpmap:0 PCMU/8000', - `a=rtcp:${audio.rtcpPort}`, - 'a=ssrc:2315747900', - 'a=sendrecv' + //'a=rtpmap:0 PCMU/8000', + //`a=rtcp:${audio.rtcpPort}`, + //'a=ssrc:2315747900', + //'a=sendrecv' ] .filter((l) => l) .join('\r\n') @@ -230,8 +230,8 @@ export class SipCall { } else { if (method === 'INVITE') { // The ACK must be sent with every OK to keep the connection alive. - this.ackWithInfo(seq!).catch((e) => { - this.log.error('Failed to send SDP ACK and INFO') + this.acknowledge(seq!).catch((e) => { + this.log.error('Failed to send SDP ACK') this.log.error(e) }) } @@ -242,12 +242,11 @@ export class SipCall { }) } - private async ackWithInfo(seq: number) { + private async acknowledge(seq: number) { // Don't wait for ack, it won't ever come back. this.request({ method: 'ACK', seq, // The ACK must have the original sequence number. - //}).catch(noop) }).catch() } diff --git a/src/streamingDelegate.ts b/src/streamingDelegate.ts index 0a02d501..440f050e 100644 --- a/src/streamingDelegate.ts +++ b/src/streamingDelegate.ts @@ -316,6 +316,7 @@ export class StreamingDelegate implements CameraStreamingDelegate { sipOptions: { to: "sip:11@10.10.10.80", + //to: "sip:11@10.10.10.22", from: "sip:user1@10.10.10.126", localIp: "10.10.10.126", }, @@ -347,7 +348,12 @@ export class StreamingDelegate implements CameraStreamingDelegate { }; sessionInfo.sipCall = new SipCall(this.log, sessionInfo.sipOptions, sessionInfo.rtpOptions) - sessionInfo.rtpDescription = await sessionInfo.sipCall.invite() + + try { + sessionInfo.rtpDescription = await sessionInfo.sipCall.invite(); + } catch(err) { + this.log.error('SIP INVITE failed: ' + err, this.cameraName); + } this.pendingSessions.set(request.sessionID, sessionInfo); callback(undefined, response); @@ -475,7 +481,9 @@ export class StreamingDelegate implements CameraStreamingDelegate { ' -f rtp' + (sessionInfo.rtpDescription.audio.ssrc !== undefined ? ' -ssrc ' + sessionInfo.rtpDescription.audio.ssrc : '') + ' -payload_type 0' + - ' rtp://' + sessionInfo.rtpDescription.address + ':' + sessionInfo.rtpDescription.audio.port + '?rtcpport=' + sessionInfo.rtpDescription.audio.rtcpPort; + ' rtp://' + sessionInfo.rtpDescription.address + ':' + sessionInfo.rtpDescription.audio.port + '?rtcpport=' + sessionInfo.rtpDescription.audio.rtcpPort; //+ + //'&localrtpport=' + sessionInfo.rtpOptions.audio.port + // bind failed: Address already in use: two-way issue + //'&localrtcpport=' + sessionInfo.rtpOptions.audio.rtcpPort; } const sdpAudio = @@ -569,7 +577,20 @@ export class StreamingDelegate implements CameraStreamingDelegate { } catch (err) { this.log.error('Error occurred terminating two-way FFmpeg process: ' + err, this.cameraName); } - session.sipCall?.destroy(); + + try { + session.sipCall?.sendBye(); + } catch(err) { + this.log.error('SIP BYE failed: ' + err, this.cameraName); + } + setTimeout(() => { + this.log.debug('Destroying SIP stack.', this.cameraName); + try { + session.sipCall?.destroy(); + } catch(err) { + this.log.error('Destroying SIP stack failed: ' + err, this.cameraName); + } + }, 500); } this.ongoingSessions.delete(sessionId); this.log.info('Stopped video stream.', this.cameraName); From 5f4dddd4d9006a3cbe2c31b9df10766be63525af Mon Sep 17 00:00:00 2001 From: Nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Tue, 18 Jan 2022 13:28:31 +0100 Subject: [PATCH 3/9] Cleanup and create SipCall instance at startup --- package-lock.json | 267 ++++++++++++++++++++++++++++++++++++--- package.json | 1 + src/sip-call.ts | 82 ++++++------ src/streamingDelegate.ts | 64 +++++----- 4 files changed, 321 insertions(+), 93 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9b7921b6..7356757e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ ], "license": "ISC", "dependencies": { + "@homebridge/camera-utils": "^2.0.4", "ffmpeg-for-homebridge": "^0.0.9", "mqtt": "4.2.8", "pick-port": "^1.0.0", @@ -77,6 +78,18 @@ "node": ">= 4" } }, + "node_modules/@homebridge/camera-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@homebridge/camera-utils/-/camera-utils-2.0.4.tgz", + "integrity": "sha512-uR9RfnFGptSiEZaSnkSsQ1LPJarKyjVvHERzSXlK+P6Jui5T2yYgPKYvM+HBIpS8Kx3cfN5NBmQxhvm/qbD0IA==", + "dependencies": { + "execa": "^5.1.1", + "ffmpeg-for-homebridge": "^0.0.9", + "pick-port": "^1.0.0", + "rxjs": "^7.3.0", + "systeminformation": "^5.8.0" + } + }, "node_modules/@homebridge/ciao": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.1.3.tgz", @@ -640,7 +653,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1098,6 +1110,28 @@ "node": ">=0.10.0" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1289,6 +1323,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -1485,6 +1530,14 @@ "node": ">=10.17.0" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1743,6 +1796,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -1835,8 +1899,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -1913,6 +1976,11 @@ "node": ">=10" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -1935,6 +2003,14 @@ "node": ">=8.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, "node_modules/mimic-response": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", @@ -2087,6 +2163,17 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/object-inspect": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", @@ -2147,6 +2234,20 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -2188,7 +2289,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -2405,6 +2505,14 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.2.tgz", + "integrity": "sha512-PwDt186XaL3QN5qXj/H9DGyHhP3/RYYgZZwqBv9Tv8rsAaiwFH1IsJJlcgD37J7UW5a6O67qX0KWKS3/pu0m4w==", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2448,7 +2556,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -2460,7 +2567,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -2479,6 +2585,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -2614,6 +2725,14 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "engines": { + "node": ">=6" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -2638,6 +2757,31 @@ "node": ">=8" } }, + "node_modules/systeminformation": { + "version": "5.10.3", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.10.3.tgz", + "integrity": "sha512-qHYF5YwmhomGpKCZxWLgSt2eui/g5NQsqOixBrGmq1hdXFmurjdH6R5AwmFo7gCpQQpPEjd6Og6I6QzVKDzhKA==", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, "node_modules/tar": { "version": "6.1.10", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.10.tgz", @@ -2681,8 +2825,7 @@ "node_modules/tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -2801,7 +2944,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -2937,6 +3079,18 @@ } } }, + "@homebridge/camera-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@homebridge/camera-utils/-/camera-utils-2.0.4.tgz", + "integrity": "sha512-uR9RfnFGptSiEZaSnkSsQ1LPJarKyjVvHERzSXlK+P6Jui5T2yYgPKYvM+HBIpS8Kx3cfN5NBmQxhvm/qbD0IA==", + "requires": { + "execa": "^5.1.1", + "ffmpeg-for-homebridge": "^0.0.9", + "pick-port": "^1.0.0", + "rxjs": "^7.3.0", + "systeminformation": "^5.8.0" + } + }, "@homebridge/ciao": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.1.3.tgz", @@ -3323,7 +3477,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3670,6 +3823,22 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3832,6 +4001,11 @@ "has-symbols": "^1.0.1" } }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" + }, "get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -3971,6 +4145,11 @@ "source-map-support": "^0.5.20" } }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -4137,6 +4316,11 @@ "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", "dev": true }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, "is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -4202,8 +4386,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "js-yaml": { "version": "4.1.0", @@ -4266,6 +4449,11 @@ "yallist": "^4.0.0" } }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4282,6 +4470,11 @@ "picomatch": "^2.2.3" } }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, "mimic-response": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", @@ -4401,6 +4594,14 @@ } } }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "requires": { + "path-key": "^3.0.0" + } + }, "object-inspect": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", @@ -4443,6 +4644,14 @@ "wrappy": "1" } }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -4474,8 +4683,7 @@ "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" }, "path-type": { "version": "4.0.0", @@ -4608,6 +4816,14 @@ "queue-microtask": "^1.2.2" } }, + "rxjs": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.2.tgz", + "integrity": "sha512-PwDt186XaL3QN5qXj/H9DGyHhP3/RYYgZZwqBv9Tv8rsAaiwFH1IsJJlcgD37J7UW5a6O67qX0KWKS3/pu0m4w==", + "requires": { + "tslib": "^2.1.0" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4631,7 +4847,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "requires": { "shebang-regex": "^3.0.0" } @@ -4639,8 +4854,7 @@ "shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, "side-channel": { "version": "1.0.4", @@ -4653,6 +4867,11 @@ "object-inspect": "^1.9.0" } }, + "signal-exit": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" + }, "simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -4758,6 +4977,11 @@ "ansi-regex": "^5.0.1" } }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -4773,6 +4997,11 @@ "has-flag": "^4.0.0" } }, + "systeminformation": { + "version": "5.10.3", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.10.3.tgz", + "integrity": "sha512-qHYF5YwmhomGpKCZxWLgSt2eui/g5NQsqOixBrGmq1hdXFmurjdH6R5AwmFo7gCpQQpPEjd6Og6I6QzVKDzhKA==" + }, "tar": { "version": "6.1.10", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.10.tgz", @@ -4810,8 +5039,7 @@ "tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "tsutils": { "version": "3.21.0", @@ -4904,7 +5132,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "requires": { "isexe": "^2.0.0" } diff --git a/package.json b/package.json index ce97118a..019ab76f 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "typescript": "^4.5.4" }, "dependencies": { + "@homebridge/camera-utils": "^2.0.4", "ffmpeg-for-homebridge": "^0.0.9", "mqtt": "4.2.8", "pick-port": "^1.0.0", diff --git a/src/sip-call.ts b/src/sip-call.ts index e961c22c..53eb21f3 100644 --- a/src/sip-call.ts +++ b/src/sip-call.ts @@ -18,6 +18,12 @@ export interface RtpDescription { audio: RtpStreamOptions } +export interface SipOptions { + to: string + from: string + localIp: string +} + interface UriOptions { name?: string uri: string @@ -33,21 +39,21 @@ interface SipHeaders { via?: UriOptions[] } -export interface SipRequest { +interface SipRequest { uri: UriOptions | string method: string headers: SipHeaders content: string } -export interface SipResponse { +interface SipResponse { status: number reason: string headers: SipHeaders content: string } -export interface SipClient { +interface SipStack { send: ( request: SipRequest | SipResponse, handler?: (response: SipResponse) => void @@ -60,12 +66,6 @@ export interface SipClient { ) => SipResponse } -export interface SipOptions { - to: string - from: string - localIp: string -} - function getRandomId() { return Math.floor(Math.random() * 1e6).toString() } @@ -113,34 +113,33 @@ export class SipCall { private fromParams = { tag: getRandomId() } private toParams: { tag?: string } = {} private callId = getRandomId() - private sipClient: SipClient + private sipStack: SipStack public onEndedByRemote?: () => void private destroyed = false private readonly log: Logger - public readonly sdp: string constructor( log: Logger, private sipOptions: SipOptions, - rtpOptions: RtpOptions, ) { this.log = log; - const { audio } = rtpOptions, - { from } = this.sipOptions, - host = this.sipOptions.localIp + const { from } = this.sipOptions, + host = this.sipOptions.localIp - this.sipClient = { + this.sipStack = { makeResponse: sip.makeResponse, ...sip.create({ host, hostname: host, - tcp: false, udp: true, + tcp: false, + tls: false, + ws: false }, (request: SipRequest) => { if (request.method === 'BYE') { this.log.info('received BYE from remote end') - this.sipClient.send(this.sipClient.makeResponse(request, 200, 'Ok')) + this.sipStack.send(this.sipStack.makeResponse(request, 200, 'Ok')) if (this.destroyed) { if (this.onEndedByRemote) { @@ -150,22 +149,6 @@ export class SipCall { } } )} - - this.sdp = [ - 'v=0', - `o=${from.split(':')[1].split('@')[0]} 3747 461 IN IP4 ${host}`, - 's=Talk', - `c=IN IP4 ${host}`, - 't=0 0', - `m=audio ${audio.port} RTP/AVP 0`, - //'a=rtpmap:0 PCMU/8000', - //`a=rtcp:${audio.rtcpPort}`, - //'a=ssrc:2315747900', - //'a=sendrecv' - ] - .filter((l) => l) - .join('\r\n') - this.sdp = this.sdp += '\r\n' } request({ @@ -187,7 +170,7 @@ export class SipCall { return new Promise((resolve, reject) => { seq = seq || this.seq++ - this.sipClient.send( + this.sipStack.send( { method, uri: this.sipOptions.to, @@ -260,9 +243,28 @@ export class SipCall { }) } - async invite() { - const { from } = this.sipOptions, - inviteResponse = await this.request({ + async invite(rtpOptions: RtpOptions) { + + const { audio } = rtpOptions, + { from } = this.sipOptions, + host = this.sipOptions.localIp; + + const sdp = ([ + 'v=0', + `o=${from.split(':')[1].split('@')[0]} 3747 461 IN IP4 ${host}`, + 's=Talk', + `c=IN IP4 ${host}`, + 't=0 0', + `m=audio ${audio.port} RTP/AVP 0`, + //'a=rtpmap:0 PCMU/8000', + //`a=rtcp:${audio.rtcpPort}`, + //'a=ssrc:2315747900', + //'a=sendrecv' + ] + .filter((l) => l) + .join('\r\n')) + '\r\n'; + + const inviteResponse = await this.request({ method: 'INVITE', headers: { supported: 'replaces, outbound', @@ -271,7 +273,7 @@ export class SipCall { 'content-type': 'application/sdp', contact: [{ uri: from }], }, - content: this.sdp, + content: sdp, }) return parseRtpDescription(this.log, inviteResponse) @@ -285,6 +287,6 @@ export class SipCall { destroy() { this.destroyed = true - this.sipClient.destroy() + this.sipStack.destroy() } } diff --git a/src/streamingDelegate.ts b/src/streamingDelegate.ts index 440f050e..95ce9ee2 100644 --- a/src/streamingDelegate.ts +++ b/src/streamingDelegate.ts @@ -26,7 +26,9 @@ import pickPort, { pickPortOptions } from 'pick-port'; import { CameraConfig, VideoConfig } from './configTypes'; import { FfmpegProcess } from './ffmpeg'; import { Logger } from './logger'; -import { RtpDescription, RtpOptions, SipCall, SipOptions } from './sip-call' +import { RtpDescription, RtpOptions, SipCall, SipOptions } from './sip-call'; +import { RtpDemuxer } from './rtp'; +import { reservePorts } from '@homebridge/camera-utils'; type SessionInfo = { address: string; // address of the HAP controller @@ -44,11 +46,8 @@ type SessionInfo = { audioSRTP: Buffer; audioSSRC: number; - sipOptions: SipOptions; - rtpOptions: RtpOptions; - - sipCall?: SipCall; - rtpDescription?: RtpDescription; + sipRtpOptions: RtpOptions; + sipRemoteRtpDescription?: RtpDescription; }; type ResolutionInfo = { @@ -64,7 +63,6 @@ type ActiveSession = { returnProcess?: FfmpegProcess; timeout?: NodeJS.Timeout; socket?: Socket; - sipCall?: SipCall; }; export class StreamingDelegate implements CameraStreamingDelegate { @@ -74,6 +72,8 @@ export class StreamingDelegate implements CameraStreamingDelegate { private readonly unbridge: boolean; private readonly videoConfig: VideoConfig; private readonly videoProcessor: string; + private readonly sipOptions: SipOptions; + private readonly sipCall: SipCall; readonly controller: CameraController; private snapshotPromise?: Promise; @@ -95,6 +95,13 @@ export class StreamingDelegate implements CameraStreamingDelegate { for (const session in this.ongoingSessions) { this.stopStream(session); } + this.log.debug('Destroying SIP stack.', this.cameraName); + try { + this.sipCall.destroy(); + } catch(err) { + this.log.error('Destroying SIP stack failed: ' + err, this.cameraName); + } + }); const options: CameraControllerOptions = { @@ -136,6 +143,15 @@ export class StreamingDelegate implements CameraStreamingDelegate { } }; + this.sipOptions = { + to: "sip:11@10.10.10.80", + //to: "sip:11@10.10.10.22", + from: "sip:user1@10.10.10.126", + localIp: "10.10.10.126", + }; + + this.sipCall = new SipCall(this.log, this.sipOptions); + this.controller = new hap.CameraController(options); } @@ -314,14 +330,7 @@ export class StreamingDelegate implements CameraStreamingDelegate { audioSRTP: Buffer.concat([request.audio.srtp_key, request.audio.srtp_salt]), audioSSRC: audioSSRC, - sipOptions: { - to: "sip:11@10.10.10.80", - //to: "sip:11@10.10.10.22", - from: "sip:user1@10.10.10.126", - localIp: "10.10.10.126", - }, - - rtpOptions: { + sipRtpOptions: { audio: { port: sipAudioPort, rtcpPort: sipAudioPort + 1, @@ -347,10 +356,8 @@ export class StreamingDelegate implements CameraStreamingDelegate { } }; - sessionInfo.sipCall = new SipCall(this.log, sessionInfo.sipOptions, sessionInfo.rtpOptions) - try { - sessionInfo.rtpDescription = await sessionInfo.sipCall.invite(); + sessionInfo.sipRemoteRtpDescription = await this.sipCall.invite(sessionInfo.sipRtpOptions); } catch(err) { this.log.error('SIP INVITE failed: ' + err, this.cameraName); } @@ -472,16 +479,16 @@ export class StreamingDelegate implements CameraStreamingDelegate { }); activeSession.socket.bind(sessionInfo.videoReturnPort); - if (sessionInfo.rtpDescription) + if (sessionInfo.sipRemoteRtpDescription) { this.videoConfig.returnAudioTarget = ' -acodec pcm_mulaw' + ' -ac 1' + ' -ar 8k' + ' -f rtp' + - (sessionInfo.rtpDescription.audio.ssrc !== undefined ? ' -ssrc ' + sessionInfo.rtpDescription.audio.ssrc : '') + + (sessionInfo.sipRemoteRtpDescription.audio.ssrc !== undefined ? ' -ssrc ' + sessionInfo.sipRemoteRtpDescription.audio.ssrc : '') + ' -payload_type 0' + - ' rtp://' + sessionInfo.rtpDescription.address + ':' + sessionInfo.rtpDescription.audio.port + '?rtcpport=' + sessionInfo.rtpDescription.audio.rtcpPort; //+ + ' rtp://' + sessionInfo.sipRemoteRtpDescription.address + ':' + sessionInfo.sipRemoteRtpDescription.audio.port + '?rtcpport=' + sessionInfo.sipRemoteRtpDescription.audio.rtcpPort; //+ //'&localrtpport=' + sessionInfo.rtpOptions.audio.port + // bind failed: Address already in use: two-way issue //'&localrtcpport=' + sessionInfo.rtpOptions.audio.rtcpPort; } @@ -492,7 +499,7 @@ export class StreamingDelegate implements CameraStreamingDelegate { 's=Talk\r\n' + 'c=IN IP4 ' + sessionInfo.address + '\r\n' + 't=0 0\r\n' + - 'm=audio ' + sessionInfo.rtpOptions.audio.port + ' RTP/AVP 0\r\n' + + 'm=audio ' + sessionInfo.sipRtpOptions.audio.port + ' RTP/AVP 0\r\n' + 'a=rtpmap:0 PCMU/8000\r\n'; activeSession.mainProcess = new FfmpegProcess(this.cameraName, request.sessionID, this.videoProcessor, ffmpegArgs, this.log, this.videoConfig.debug, this, callback); @@ -529,8 +536,6 @@ export class StreamingDelegate implements CameraStreamingDelegate { activeSession.returnProcess.stdin.end(sdpReturnAudio); } - activeSession.sipCall = sessionInfo.sipCall; - this.ongoingSessions.set(request.sessionID, activeSession); this.pendingSessions.delete(request.sessionID); } else { @@ -579,18 +584,11 @@ export class StreamingDelegate implements CameraStreamingDelegate { } try { - session.sipCall?.sendBye(); + this.log.debug('Sending SIP BYE to ' + this.cameraName); + this.sipCall.sendBye(); } catch(err) { this.log.error('SIP BYE failed: ' + err, this.cameraName); } - setTimeout(() => { - this.log.debug('Destroying SIP stack.', this.cameraName); - try { - session.sipCall?.destroy(); - } catch(err) { - this.log.error('Destroying SIP stack failed: ' + err, this.cameraName); - } - }, 500); } this.ongoingSessions.delete(sessionId); this.log.info('Stopped video stream.', this.cameraName); From 3b9ce58d16f7af810146a61a0c450d1c2559bfd1 Mon Sep 17 00:00:00 2001 From: Nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Tue, 18 Jan 2022 16:47:22 +0100 Subject: [PATCH 4/9] Added RtpHelper to fix problem of having two ffmpeg instances use the same port for TX and RX (symmetric RTP) --- src/rtp.ts | 147 +++++++++++++++++++++++++++++++++++++++ src/sip-call.ts | 10 +-- src/streamingDelegate.ts | 77 +++++++++++++------- 3 files changed, 206 insertions(+), 28 deletions(-) create mode 100644 src/rtp.ts diff --git a/src/rtp.ts b/src/rtp.ts new file mode 100644 index 00000000..86b29941 --- /dev/null +++ b/src/rtp.ts @@ -0,0 +1,147 @@ +import { Logger } from "./logger"; +import { createSocket, RemoteInfo, Socket } from "dgram"; + +export class RtpHelper { + private readonly logPrefix: string; + private readonly log: Logger; + private heartbeatTimer!: NodeJS.Timeout; + private heartbeatMsg!: Buffer; + private inputPort: number; + private inputRtcpPort: number; + public readonly rtpSocket: Socket; + public readonly rtcpSocket?: Socket; + + // Create an instance of RtpHelper. + constructor(log: Logger, logPrefix: string, ipFamily: ("ipv4" | "ipv6") , inputPort: number, inputRtcpPort: number, rtcpPort: number, rtpPort: number, + sendAddress: string, sendPort: number, sendRtcpPort: number) { + + this.log = log; + this.logPrefix = logPrefix; + this.inputPort = inputPort; + this.inputRtcpPort = inputRtcpPort; + this.rtpSocket = createSocket(ipFamily === "ipv6" ? "udp6" : "udp4" ); + this.rtcpSocket = (inputPort !== inputRtcpPort) ? createSocket(ipFamily === "ipv6" ? "udp6" : "udp4" ) : undefined; + + // Catch errors when they happen on our demuxer. + this.rtpSocket.on("error", (error) => { + this.log.error("RtpHelper (RTP) Error: " + error, this.logPrefix); + this.rtpSocket.close(); + }); + + // Catch errors when they happen on our demuxer. + this.rtcpSocket?.on("error", (error) => { + this.log.error("RtpHelper (RTCP) Error: " + error, this.logPrefix); + this.rtcpSocket?.close(); + }); + + // Split the message into RTP and RTCP packets. + this.rtpSocket.on("message", (msg: Buffer, rinfo: RemoteInfo) => { + + // Check if we have to forward a packet from ffmpeg to the external peer + if (rinfo.address === '127.0.0.1') + { + this.rtpSocket.send(msg, sendPort, sendAddress); + return; + } + + // Send RTP packets to the RTP port. + if(this.isRtpMessage(msg)) { + + this.rtpSocket.send(msg, rtpPort); + + } else { + + // Save this RTCP message for heartbeat purposes for the RTP port. This works because RTCP packets will be ignored + // by ffmpeg on the RTP port, effectively providing a heartbeat to ensure FFmpeg doesn't timeout if there's an + // extended delay between data transmission. + //this.heartbeatMsg = Buffer.from(msg); + + // Clear the old heartbeat timer. + //clearTimeout(this.heartbeatTimer); + //this.heartbeat(rtpPort); + + // RTCP control packets should go to the RTCP port. + this.rtpSocket.send(msg, rtcpPort); + + } + }); + + // Split the message into RTP and RTCP packets. + this.rtcpSocket?.on("message", (msg: Buffer, rinfo: RemoteInfo) => { + + // Check if we have to forward a packet from ffmpeg to the external peer + if (rinfo.address === '127.0.0.1') + { + this.rtcpSocket?.send(msg, sendRtcpPort, sendAddress); + return; + } + + // Send RTP packets to the RTP port. + if(this.isRtpMessage(msg)) { + + this.rtcpSocket?.send(msg, rtpPort); + + } else { + + // Save this RTCP message for heartbeat purposes for the RTP port. This works because RTCP packets will be ignored + // by ffmpeg on the RTP port, effectively providing a heartbeat to ensure FFmpeg doesn't timeout if there's an + // extended delay between data transmission. + //this.heartbeatMsg = Buffer.from(msg); + + // Clear the old heartbeat timer. + //clearTimeout(this.heartbeatTimer); + //this.heartbeat(rtpPort); + + // RTCP control packets should go to the RTCP port. + this.rtcpSocket?.send(msg, rtcpPort); + + } + }); + + this.log.debug("Creating RtpHelper instance - inbound port: " + this.inputPort + ", RTCP port: " + rtcpPort + ", RTP port: " + rtpPort, this.logPrefix); + + // Take the socket live. + this.rtpSocket.bind(this.inputPort); + this.rtcpSocket?.bind(this.inputRtcpPort); + } + + // Send a regular heartbeat to FFmpeg to ensure the pipe remains open and the process alive. + private heartbeat(port: number): 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(() => { + + this.log.debug("Sending ffmpeg a heartbeat.", this.logPrefix); + + this.rtpSocket.send(this.heartbeatMsg, port); + this.heartbeat(port); + + }, 3.5 * 1000); + } + + // Close the socket and cleanup. + public close(): void { + this.log.debug("Closing RtpHelper instance on port: " + this.inputPort, this.logPrefix); + + //clearTimeout(this.heartbeatTimer); + this.rtpSocket.close(); + this.rtcpSocket?.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); + } +} diff --git a/src/sip-call.ts b/src/sip-call.ts index 53eb21f3..1bf83e6c 100644 --- a/src/sip-call.ts +++ b/src/sip-call.ts @@ -141,10 +141,8 @@ export class SipCall { this.log.info('received BYE from remote end') this.sipStack.send(this.sipStack.makeResponse(request, 200, 'Ok')) - if (this.destroyed) { - if (this.onEndedByRemote) { - this.onEndedByRemote() - } + if (this.onEndedByRemote) { + this.onEndedByRemote() } } } @@ -245,6 +243,10 @@ export class SipCall { async invite(rtpOptions: RtpOptions) { + // As we keep the SIP stack alive, we havw to reset call-related properties for each new call + this.callId = getRandomId(); + this.toParams.tag = undefined; + const { audio } = rtpOptions, { from } = this.sipOptions, host = this.sipOptions.localIp; diff --git a/src/streamingDelegate.ts b/src/streamingDelegate.ts index 95ce9ee2..bb113393 100644 --- a/src/streamingDelegate.ts +++ b/src/streamingDelegate.ts @@ -27,7 +27,7 @@ import { CameraConfig, VideoConfig } from './configTypes'; import { FfmpegProcess } from './ffmpeg'; import { Logger } from './logger'; import { RtpDescription, RtpOptions, SipCall, SipOptions } from './sip-call'; -import { RtpDemuxer } from './rtp'; +import { RtpHelper } from './rtp'; import { reservePorts } from '@homebridge/camera-utils'; type SessionInfo = { @@ -48,6 +48,9 @@ type SessionInfo = { sipRtpOptions: RtpOptions; sipRemoteRtpDescription?: RtpDescription; + rtpHelper?: RtpHelper; + rtpLocalAudioPort: number; + rtpLocalAudioPortRtcp: number; }; type ResolutionInfo = { @@ -63,6 +66,7 @@ type ActiveSession = { returnProcess?: FfmpegProcess; timeout?: NodeJS.Timeout; socket?: Socket; + rtpHelper?: RtpHelper; }; export class StreamingDelegate implements CameraStreamingDelegate { @@ -101,7 +105,6 @@ export class StreamingDelegate implements CameraStreamingDelegate { } catch(err) { this.log.error('Destroying SIP stack failed: ' + err, this.cameraName); } - }); const options: CameraControllerOptions = { @@ -144,8 +147,8 @@ export class StreamingDelegate implements CameraStreamingDelegate { }; this.sipOptions = { - to: "sip:11@10.10.10.80", - //to: "sip:11@10.10.10.22", + //to: "sip:11@10.10.10.80", + to: "sip:11@10.10.10.22", from: "sip:user1@10.10.10.126", localIp: "10.10.10.126", }; @@ -310,11 +313,15 @@ export class StreamingDelegate implements CameraStreamingDelegate { const audioReturnPort = await pickPort(options); const audioSSRC = this.hap.CameraController.generateSynchronisationSource(); - //const sipAudioPort = await pickPort(options); - const sipAudioPort = 12346; - const sipAudioSSRC = this.hap.CameraController.generateSynchronisationSource(); + const [ + sipIncomingAudioPort, + sipIncomingAudioRtcpPort + ] = await reservePorts({count: 2, type: 'udp'}); + const sipAudioSSRC = this.hap.CameraController.generateSynchronisationSource(); // The current implementation just seems to generate a random number. No additional magic inside. So just use it. - const sessionInfo: SessionInfo = { + const [sipLocalAudioPort, sipLocalAudioRtcpPort] = await reservePorts({count: 2, type: 'udp'}); + + let sessionInfo: SessionInfo = { address: request.targetAddress, ipv6: ipv6, @@ -332,13 +339,39 @@ export class StreamingDelegate implements CameraStreamingDelegate { sipRtpOptions: { audio: { - port: sipAudioPort, - rtcpPort: sipAudioPort + 1, + port: sipIncomingAudioPort, + rtcpPort: sipIncomingAudioRtcpPort, ssrc: sipAudioSSRC } - } + }, + + rtpLocalAudioPort: sipLocalAudioPort, + rtpLocalAudioPortRtcp: sipLocalAudioRtcpPort }; + let sipRemoteRtpDescription: RtpDescription; + try { + sipRemoteRtpDescription = await this.sipCall.invite({ + audio: { + port: sipIncomingAudioPort, + rtcpPort: sipIncomingAudioRtcpPort, + ssrc: sipAudioSSRC + }}); + + sessionInfo.sipRemoteRtpDescription = sipRemoteRtpDescription; + sessionInfo.rtpHelper = new RtpHelper(this.log, this.cameraName, "ipv4", + sipIncomingAudioPort, + sipIncomingAudioRtcpPort, + sipLocalAudioPort, + sipLocalAudioRtcpPort, + sipRemoteRtpDescription.address, + sipRemoteRtpDescription.audio.port, + sipRemoteRtpDescription.audio.rtcpPort); + + } catch(err) { + this.log.error('SIP INVITE failed: ' + err, this.cameraName); + } + const response: PrepareStreamResponse = { video: { port: videoReturnPort, @@ -356,12 +389,6 @@ export class StreamingDelegate implements CameraStreamingDelegate { } }; - try { - sessionInfo.sipRemoteRtpDescription = await this.sipCall.invite(sessionInfo.sipRtpOptions); - } catch(err) { - this.log.error('SIP INVITE failed: ' + err, this.cameraName); - } - this.pendingSessions.set(request.sessionID, sessionInfo); callback(undefined, response); } @@ -486,20 +513,18 @@ export class StreamingDelegate implements CameraStreamingDelegate { ' -ac 1' + ' -ar 8k' + ' -f rtp' + - (sessionInfo.sipRemoteRtpDescription.audio.ssrc !== undefined ? ' -ssrc ' + sessionInfo.sipRemoteRtpDescription.audio.ssrc : '') + + //(sessionInfo.sipRemoteRtpDescription.audio.ssrc ? ' -ssrc ' + sessionInfo.sipRemoteRtpDescription.audio.ssrc : '') + // see ffmpeg bug: https://trac.ffmpeg.org/ticket/9080 ' -payload_type 0' + - ' rtp://' + sessionInfo.sipRemoteRtpDescription.address + ':' + sessionInfo.sipRemoteRtpDescription.audio.port + '?rtcpport=' + sessionInfo.sipRemoteRtpDescription.audio.rtcpPort; //+ - //'&localrtpport=' + sessionInfo.rtpOptions.audio.port + // bind failed: Address already in use: two-way issue - //'&localrtcpport=' + sessionInfo.rtpOptions.audio.rtcpPort; + ' rtp://127.0.0.1:' + sessionInfo.sipRtpOptions.audio.port + '?rtcpport=' + sessionInfo.sipRtpOptions.audio.rtcpPort; } const sdpAudio = 'v=0\r\n' + - 'o=- 0 0 IN IP4 ' + sessionInfo.address + '\r\n' + + 'o=- 0 0 IN IP4 127.0.0.1\r\n' + 's=Talk\r\n' + - 'c=IN IP4 ' + sessionInfo.address + '\r\n' + + 'c=IN IP4 127.0.0.1\r\n' + 't=0 0\r\n' + - 'm=audio ' + sessionInfo.sipRtpOptions.audio.port + ' RTP/AVP 0\r\n' + + 'm=audio ' + sessionInfo.rtpLocalAudioPort + ' RTP/AVP 0\r\n' + 'a=rtpmap:0 PCMU/8000\r\n'; activeSession.mainProcess = new FfmpegProcess(this.cameraName, request.sessionID, this.videoProcessor, ffmpegArgs, this.log, this.videoConfig.debug, this, callback); @@ -536,6 +561,8 @@ export class StreamingDelegate implements CameraStreamingDelegate { activeSession.returnProcess.stdin.end(sdpReturnAudio); } + activeSession.rtpHelper = sessionInfo.rtpHelper; + this.ongoingSessions.set(request.sessionID, activeSession); this.pendingSessions.delete(request.sessionID); } else { @@ -589,6 +616,8 @@ export class StreamingDelegate implements CameraStreamingDelegate { } catch(err) { this.log.error('SIP BYE failed: ' + err, this.cameraName); } + + session.rtpHelper?.close(); } this.ongoingSessions.delete(sessionId); this.log.info('Stopped video stream.', this.cameraName); From e6aebaf75edd087a9f84e3ee9e1551d6af2d2ae3 Mon Sep 17 00:00:00 2001 From: Nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Wed, 19 Jan 2022 14:52:36 +0100 Subject: [PATCH 5/9] Add configuration via config file and cleanup --- src/configTypes.ts | 6 ++ src/sip-call.ts | 16 ++-- src/streamingDelegate.ts | 185 +++++++++++++++++++++------------------ 3 files changed, 116 insertions(+), 91 deletions(-) diff --git a/src/configTypes.ts b/src/configTypes.ts index cd9874cb..5953d42b 100644 --- a/src/configTypes.ts +++ b/src/configTypes.ts @@ -28,6 +28,7 @@ export type CameraConfig = { mqtt?: MqttCameraConfig; unbridge?: boolean; videoConfig?: VideoConfig; + sipConfig?: SipConfig; }; export type VideoConfig = { @@ -59,3 +60,8 @@ export type MqttCameraConfig = { doorbellTopic?: string; doorbellMessage?: string; }; + +export type SipConfig = { + from: string; + to: string; +}; diff --git a/src/sip-call.ts b/src/sip-call.ts index 1bf83e6c..463c7d96 100644 --- a/src/sip-call.ts +++ b/src/sip-call.ts @@ -21,7 +21,7 @@ export interface RtpDescription { export interface SipOptions { to: string from: string - localIp: string + //localIp: string } interface UriOptions { @@ -85,7 +85,7 @@ function getRtpDescription( return { port, - rtcpPort: (rtcpLine && Number(rtcpLine.match(/rtcp:(\S*)/)?.[1])) || port+1, + rtcpPort: (rtcpLine && Number(rtcpLine.match(/rtcp:(\S*)/)?.[1])) || port + 1, // if there is no explicit RTCP port, then use RTP port + 1 ssrc: (ssrcLine && Number(ssrcLine.match(/ssrc:(\S*)/)?.[1])) || undefined } } catch (e) { @@ -123,9 +123,11 @@ export class SipCall { private sipOptions: SipOptions, ) { this.log = log; - const { from } = this.sipOptions, - host = this.sipOptions.localIp + const fromUri = sip.parseUri(this.sipOptions.from); + const host = fromUri.host; + const { from } = this.sipOptions + //host = this.sipOptions.localIp this.sipStack = { makeResponse: sip.makeResponse, ...sip.create({ @@ -247,9 +249,11 @@ export class SipCall { this.callId = getRandomId(); this.toParams.tag = undefined; + const fromUri = sip.parseUri(this.sipOptions.from); + const host = fromUri.host; const { audio } = rtpOptions, - { from } = this.sipOptions, - host = this.sipOptions.localIp; + { from } = this.sipOptions + //host = this.sipOptions.localIp; const sdp = ([ 'v=0', diff --git a/src/streamingDelegate.ts b/src/streamingDelegate.ts index bb113393..29f18a6b 100644 --- a/src/streamingDelegate.ts +++ b/src/streamingDelegate.ts @@ -23,10 +23,10 @@ import { spawn } from 'child_process'; import { createSocket, Socket } from 'dgram'; import ffmpegPath from 'ffmpeg-for-homebridge'; import pickPort, { pickPortOptions } from 'pick-port'; -import { CameraConfig, VideoConfig } from './configTypes'; +import { CameraConfig, VideoConfig, SipConfig } from './configTypes'; import { FfmpegProcess } from './ffmpeg'; import { Logger } from './logger'; -import { RtpDescription, RtpOptions, SipCall, SipOptions } from './sip-call'; +import { RtpDescription, RtpOptions, SipCall } from './sip-call'; import { RtpHelper } from './rtp'; import { reservePorts } from '@homebridge/camera-utils'; @@ -46,11 +46,11 @@ type SessionInfo = { audioSRTP: Buffer; audioSSRC: number; - sipRtpOptions: RtpOptions; + sipRtpOptions?: RtpOptions; sipRemoteRtpDescription?: RtpDescription; rtpHelper?: RtpHelper; - rtpLocalAudioPort: number; - rtpLocalAudioPortRtcp: number; + rtpLocalAudioPort?: number; + rtpLocalAudioPortRtcp?: number; }; type ResolutionInfo = { @@ -76,8 +76,8 @@ export class StreamingDelegate implements CameraStreamingDelegate { private readonly unbridge: boolean; private readonly videoConfig: VideoConfig; private readonly videoProcessor: string; - private readonly sipOptions: SipOptions; - private readonly sipCall: SipCall; + private readonly sipConfig?: SipConfig; + private readonly sipCall?: SipCall; readonly controller: CameraController; private snapshotPromise?: Promise; @@ -94,16 +94,20 @@ export class StreamingDelegate implements CameraStreamingDelegate { this.unbridge = cameraConfig.unbridge ?? false; this.videoConfig = cameraConfig.videoConfig!; this.videoProcessor = videoProcessor || ffmpegPath || 'ffmpeg'; + this.sipConfig = cameraConfig.sipConfig; api.on(APIEvent.SHUTDOWN, () => { for (const session in this.ongoingSessions) { this.stopStream(session); } - this.log.debug('Destroying SIP stack.', this.cameraName); - try { - this.sipCall.destroy(); - } catch(err) { - this.log.error('Destroying SIP stack failed: ' + err, this.cameraName); + + if (this.sipConfig) { + this.log.debug('Destroying SIP stack.', this.cameraName); + try { + this.sipCall?.destroy(); + } catch(err) { + this.log.error('Destroying SIP stack failed: ' + err, this.cameraName); + } } }); @@ -132,8 +136,7 @@ export class StreamingDelegate implements CameraStreamingDelegate { } }, audio: { - //twoWayAudio: !!this.videoConfig.returnAudioTarget, - twoWayAudio: true, + twoWayAudio: !!this.videoConfig.returnAudioTarget || !!this.sipConfig, codecs: [ { type: AudioStreamingCodecType.AAC_ELD, @@ -146,14 +149,16 @@ export class StreamingDelegate implements CameraStreamingDelegate { } }; - this.sipOptions = { - //to: "sip:11@10.10.10.80", - to: "sip:11@10.10.10.22", - from: "sip:user1@10.10.10.126", - localIp: "10.10.10.126", - }; - - this.sipCall = new SipCall(this.log, this.sipOptions); + if (this.sipConfig) { + this.sipCall = new SipCall(this.log, { + //to: "sip:11@10.10.10.80", + //to: "sip:11@10.10.10.22", + //from: "sip:user1@10.10.10.126", + from: this.sipConfig.from, + to: this.sipConfig.to + //localIp: "10.10.10.126", + }); + } this.controller = new hap.CameraController(options); } @@ -313,14 +318,6 @@ export class StreamingDelegate implements CameraStreamingDelegate { const audioReturnPort = await pickPort(options); const audioSSRC = this.hap.CameraController.generateSynchronisationSource(); - const [ - sipIncomingAudioPort, - sipIncomingAudioRtcpPort - ] = await reservePorts({count: 2, type: 'udp'}); - const sipAudioSSRC = this.hap.CameraController.generateSynchronisationSource(); // The current implementation just seems to generate a random number. No additional magic inside. So just use it. - - const [sipLocalAudioPort, sipLocalAudioRtcpPort] = await reservePorts({count: 2, type: 'udp'}); - let sessionInfo: SessionInfo = { address: request.targetAddress, ipv6: ipv6, @@ -336,42 +333,52 @@ export class StreamingDelegate implements CameraStreamingDelegate { audioCryptoSuite: request.audio.srtpCryptoSuite, audioSRTP: Buffer.concat([request.audio.srtp_key, request.audio.srtp_salt]), audioSSRC: audioSSRC, - - sipRtpOptions: { - audio: { - port: sipIncomingAudioPort, - rtcpPort: sipIncomingAudioRtcpPort, - ssrc: sipAudioSSRC + }; + + if (this.sipConfig && this.sipCall) { + const [ + sipIncomingAudioPort, + sipIncomingAudioRtcpPort + ] = await reservePorts({count: 2, type: 'udp'}); + const sipAudioSSRC = this.hap.CameraController.generateSynchronisationSource(); // The current implementation just seems to generate a random number. No additional magic inside. So just use it. + + const [sipLocalAudioPort, sipLocalAudioRtcpPort] = await reservePorts({count: 2, type: 'udp'}); + + sessionInfo = {...sessionInfo, ...{ + sipRtpOptions: { + audio: { + port: sipIncomingAudioPort, + rtcpPort: sipIncomingAudioRtcpPort, + ssrc: sipAudioSSRC + } + }, + rtpLocalAudioPort: sipLocalAudioPort, + rtpLocalAudioPortRtcp: sipLocalAudioRtcpPort } - }, + }; - rtpLocalAudioPort: sipLocalAudioPort, - rtpLocalAudioPortRtcp: sipLocalAudioRtcpPort - }; + try { + const sipRemoteRtpDescription = await this.sipCall?.invite({ + audio: { + port: sipIncomingAudioPort, + rtcpPort: sipIncomingAudioRtcpPort, + ssrc: sipAudioSSRC + }}); + + sessionInfo.sipRemoteRtpDescription = sipRemoteRtpDescription; + sessionInfo.rtpHelper = new RtpHelper(this.log, this.cameraName, "ipv4", + sipIncomingAudioPort, + sipIncomingAudioRtcpPort, + sipLocalAudioPort, + sipLocalAudioRtcpPort, + sipRemoteRtpDescription.address, + sipRemoteRtpDescription.audio.port, + sipRemoteRtpDescription.audio.rtcpPort); - let sipRemoteRtpDescription: RtpDescription; - try { - sipRemoteRtpDescription = await this.sipCall.invite({ - audio: { - port: sipIncomingAudioPort, - rtcpPort: sipIncomingAudioRtcpPort, - ssrc: sipAudioSSRC - }}); - - sessionInfo.sipRemoteRtpDescription = sipRemoteRtpDescription; - sessionInfo.rtpHelper = new RtpHelper(this.log, this.cameraName, "ipv4", - sipIncomingAudioPort, - sipIncomingAudioRtcpPort, - sipLocalAudioPort, - sipLocalAudioRtcpPort, - sipRemoteRtpDescription.address, - sipRemoteRtpDescription.audio.port, - sipRemoteRtpDescription.audio.rtcpPort); - - } catch(err) { - this.log.error('SIP INVITE failed: ' + err, this.cameraName); + } catch(err) { + this.log.error('SIP INVITE failed: ' + err, this.cameraName); + } } - const response: PrepareStreamResponse = { video: { port: videoReturnPort, @@ -429,12 +436,14 @@ export class StreamingDelegate implements CameraStreamingDelegate { let ffmpegArgs = this.videoConfig.source!; - ffmpegArgs += // SIP audio via RTP - '-hide_banner' + - ' -protocol_whitelist pipe,udp,rtp,file,crypto' + - ' -f sdp' + - ' -c:a pcm_mulaw' + - ' -i pipe:'; + if (this.sipConfig) { + ffmpegArgs += // SIP audio via RTP + '-hide_banner' + + ' -protocol_whitelist pipe,udp,rtp,file,crypto' + + ' -f sdp' + + ' -c:a pcm_mulaw' + + ' -i pipe:'; + } ffmpegArgs += // Video (this.videoConfig.mapvideo ? ' -map ' + this.videoConfig.mapvideo : ' -an -sn -dn') + @@ -506,8 +515,9 @@ export class StreamingDelegate implements CameraStreamingDelegate { }); activeSession.socket.bind(sessionInfo.videoReturnPort); - if (sessionInfo.sipRemoteRtpDescription) + if (this.sipConfig && sessionInfo.sipRemoteRtpDescription && sessionInfo.sipRtpOptions) { + // If we use SIP we setup the returnAudioTarget ourselves this.videoConfig.returnAudioTarget = ' -acodec pcm_mulaw' + ' -ac 1' + @@ -518,17 +528,21 @@ export class StreamingDelegate implements CameraStreamingDelegate { ' rtp://127.0.0.1:' + sessionInfo.sipRtpOptions.audio.port + '?rtcpport=' + sessionInfo.sipRtpOptions.audio.rtcpPort; } - const sdpAudio = - 'v=0\r\n' + - 'o=- 0 0 IN IP4 127.0.0.1\r\n' + - 's=Talk\r\n' + - 'c=IN IP4 127.0.0.1\r\n' + - 't=0 0\r\n' + - 'm=audio ' + sessionInfo.rtpLocalAudioPort + ' RTP/AVP 0\r\n' + - 'a=rtpmap:0 PCMU/8000\r\n'; activeSession.mainProcess = new FfmpegProcess(this.cameraName, request.sessionID, this.videoProcessor, ffmpegArgs, this.log, this.videoConfig.debug, this, callback); - activeSession.mainProcess.stdin.end(sdpAudio); + + if (this.sipConfig) { + const sdpAudio = + 'v=0\r\n' + + 'o=- 0 0 IN IP4 127.0.0.1\r\n' + + 's=Talk\r\n' + + 'c=IN IP4 127.0.0.1\r\n' + + 't=0 0\r\n' + + 'm=audio ' + sessionInfo.rtpLocalAudioPort + ' RTP/AVP 0\r\n' + + 'a=rtpmap:0 PCMU/8000\r\n'; + + activeSession.mainProcess.stdin.end(sdpAudio); + } if (this.videoConfig.returnAudioTarget) { const ffmpegReturnArgs = @@ -610,14 +624,15 @@ export class StreamingDelegate implements CameraStreamingDelegate { this.log.error('Error occurred terminating two-way FFmpeg process: ' + err, this.cameraName); } - try { - this.log.debug('Sending SIP BYE to ' + this.cameraName); - this.sipCall.sendBye(); - } catch(err) { - this.log.error('SIP BYE failed: ' + err, this.cameraName); + if (this.sipConfig) { + try { + this.log.debug('Sending SIP BYE to ' + this.cameraName); + this.sipCall?.sendBye(); + } catch(err) { + this.log.error('SIP BYE failed: ' + err, this.cameraName); + } + session.rtpHelper?.close(); } - - session.rtpHelper?.close(); } this.ongoingSessions.delete(sessionId); this.log.info('Stopped video stream.', this.cameraName); From bfa602600e9f9c8c7ef531a6a3e619d0e9f37cf4 Mon Sep 17 00:00:00 2001 From: Nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Wed, 19 Jan 2022 15:02:09 +0100 Subject: [PATCH 6/9] Cleanup --- src/streamingDelegate.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/streamingDelegate.ts b/src/streamingDelegate.ts index 29f18a6b..c289ba87 100644 --- a/src/streamingDelegate.ts +++ b/src/streamingDelegate.ts @@ -332,7 +332,7 @@ export class StreamingDelegate implements CameraStreamingDelegate { audioReturnPort: audioReturnPort, audioCryptoSuite: request.audio.srtpCryptoSuite, audioSRTP: Buffer.concat([request.audio.srtp_key, request.audio.srtp_salt]), - audioSSRC: audioSSRC, + audioSSRC: audioSSRC }; if (this.sipConfig && this.sipCall) { @@ -340,7 +340,9 @@ export class StreamingDelegate implements CameraStreamingDelegate { sipIncomingAudioPort, sipIncomingAudioRtcpPort ] = await reservePorts({count: 2, type: 'udp'}); - const sipAudioSSRC = this.hap.CameraController.generateSynchronisationSource(); // The current implementation just seems to generate a random number. No additional magic inside. So just use it. + // The current implementation of generateSynchronisationSource() just seems to generate a random number. + // No additional magic inside. So just use it for our SIP signaling too. + const sipAudioSSRC = this.hap.CameraController.generateSynchronisationSource(); const [sipLocalAudioPort, sipLocalAudioRtcpPort] = await reservePorts({count: 2, type: 'udp'}); From 180bdcf7a77c7d939e4b0afd250e21be201d0b7f Mon Sep 17 00:00:00 2001 From: Nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Thu, 20 Jan 2022 17:23:44 +0100 Subject: [PATCH 7/9] Cleanup and add possibility to specify SIP port for SIP client --- src/sip-call.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/sip-call.ts b/src/sip-call.ts index 463c7d96..2ab933cf 100644 --- a/src/sip-call.ts +++ b/src/sip-call.ts @@ -21,7 +21,6 @@ export interface RtpDescription { export interface SipOptions { to: string from: string - //localIp: string } interface UriOptions { @@ -125,14 +124,15 @@ export class SipCall { this.log = log; const fromUri = sip.parseUri(this.sipOptions.from); const host = fromUri.host; + const port = fromUri.port; const { from } = this.sipOptions - //host = this.sipOptions.localIp this.sipStack = { makeResponse: sip.makeResponse, ...sip.create({ host, hostname: host, + port: port, udp: true, tcp: false, tls: false, @@ -253,7 +253,6 @@ export class SipCall { const host = fromUri.host; const { audio } = rtpOptions, { from } = this.sipOptions - //host = this.sipOptions.localIp; const sdp = ([ 'v=0', From 0a506ba9d1a575ce7e40d2e0d0de8e2ae2cc5f25 Mon Sep 17 00:00:00 2001 From: Nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Thu, 20 Jan 2022 17:41:02 +0100 Subject: [PATCH 8/9] Cleanup --- src/rtp.ts | 47 ----------------------------------------------- 1 file changed, 47 deletions(-) diff --git a/src/rtp.ts b/src/rtp.ts index 86b29941..4738e63c 100644 --- a/src/rtp.ts +++ b/src/rtp.ts @@ -4,8 +4,6 @@ import { createSocket, RemoteInfo, Socket } from "dgram"; export class RtpHelper { private readonly logPrefix: string; private readonly log: Logger; - private heartbeatTimer!: NodeJS.Timeout; - private heartbeatMsg!: Buffer; private inputPort: number; private inputRtcpPort: number; public readonly rtpSocket: Socket; @@ -50,19 +48,7 @@ export class RtpHelper { this.rtpSocket.send(msg, rtpPort); } else { - - // Save this RTCP message for heartbeat purposes for the RTP port. This works because RTCP packets will be ignored - // by ffmpeg on the RTP port, effectively providing a heartbeat to ensure FFmpeg doesn't timeout if there's an - // extended delay between data transmission. - //this.heartbeatMsg = Buffer.from(msg); - - // Clear the old heartbeat timer. - //clearTimeout(this.heartbeatTimer); - //this.heartbeat(rtpPort); - - // RTCP control packets should go to the RTCP port. this.rtpSocket.send(msg, rtcpPort); - } }); @@ -78,21 +64,8 @@ export class RtpHelper { // Send RTP packets to the RTP port. if(this.isRtpMessage(msg)) { - this.rtcpSocket?.send(msg, rtpPort); - } else { - - // Save this RTCP message for heartbeat purposes for the RTP port. This works because RTCP packets will be ignored - // by ffmpeg on the RTP port, effectively providing a heartbeat to ensure FFmpeg doesn't timeout if there's an - // extended delay between data transmission. - //this.heartbeatMsg = Buffer.from(msg); - - // Clear the old heartbeat timer. - //clearTimeout(this.heartbeatTimer); - //this.heartbeat(rtpPort); - - // RTCP control packets should go to the RTCP port. this.rtcpSocket?.send(msg, rtcpPort); } @@ -105,30 +78,10 @@ export class RtpHelper { this.rtcpSocket?.bind(this.inputRtcpPort); } - // Send a regular heartbeat to FFmpeg to ensure the pipe remains open and the process alive. - private heartbeat(port: number): 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(() => { - - this.log.debug("Sending ffmpeg a heartbeat.", this.logPrefix); - - this.rtpSocket.send(this.heartbeatMsg, port); - this.heartbeat(port); - - }, 3.5 * 1000); - } - // Close the socket and cleanup. public close(): void { this.log.debug("Closing RtpHelper instance on port: " + this.inputPort, this.logPrefix); - //clearTimeout(this.heartbeatTimer); this.rtpSocket.close(); this.rtcpSocket?.close(); } From 5536895815fd22a3fd91506e660a6577cee3fd02 Mon Sep 17 00:00:00 2001 From: Nanosonde <2073569+nanosonde@users.noreply.github.com> Date: Thu, 20 Jan 2022 18:04:33 +0100 Subject: [PATCH 9/9] Cleanup --- src/streamingDelegate.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/streamingDelegate.ts b/src/streamingDelegate.ts index c289ba87..c050a589 100644 --- a/src/streamingDelegate.ts +++ b/src/streamingDelegate.ts @@ -151,12 +151,8 @@ export class StreamingDelegate implements CameraStreamingDelegate { if (this.sipConfig) { this.sipCall = new SipCall(this.log, { - //to: "sip:11@10.10.10.80", - //to: "sip:11@10.10.10.22", - //from: "sip:user1@10.10.10.126", from: this.sipConfig.from, to: this.sipConfig.to - //localIp: "10.10.10.126", }); }