From 98b975594accce7a3cd32a514fdd67b286cea1f3 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 28 Nov 2022 10:09:44 -0800 Subject: [PATCH] sip: publish plugin --- plugins/sip/.vscode/launch.json | 2 +- plugins/sip/.vscode/settings.json | 2 +- plugins/sip/README.md | 2 +- plugins/sip/package-lock.json | 6 +- plugins/sip/package.json | 2 +- plugins/sip/src/main.ts | 34 +++--- plugins/sip/src/rtp-utils.ts | 178 +++++++++++++++--------------- plugins/sip/src/sip-call.ts | 41 +++---- plugins/sip/src/sip-session.ts | 60 +++------- 9 files changed, 150 insertions(+), 177 deletions(-) diff --git a/plugins/sip/.vscode/launch.json b/plugins/sip/.vscode/launch.json index 03660e3c2..0c868d0cf 100644 --- a/plugins/sip/.vscode/launch.json +++ b/plugins/sip/.vscode/launch.json @@ -17,7 +17,7 @@ "sourceMaps": true, "localRoot": "${workspaceFolder}/out", "remoteRoot": "/plugin/", - "type": "pwa-node" + "type": "node" } ] } \ No newline at end of file diff --git a/plugins/sip/.vscode/settings.json b/plugins/sip/.vscode/settings.json index 44d4d203f..77ccdbd6d 100644 --- a/plugins/sip/.vscode/settings.json +++ b/plugins/sip/.vscode/settings.json @@ -1,4 +1,4 @@ { - "scrypted.debugHost": "koushik-ubuntu", + "scrypted.debugHost": "127.0.0.1", } \ No newline at end of file diff --git a/plugins/sip/README.md b/plugins/sip/README.md index 0d32b52aa..990d1b770 100644 --- a/plugins/sip/README.md +++ b/plugins/sip/README.md @@ -1,3 +1,3 @@ # SIP Plugin for Scrypted -The SIP Plugin bridges compatible SIP Cameras in Scrypted to HomeKit. +The SIP Plugin bridges compatible SIP Cameras to Scrypted. diff --git a/plugins/sip/package-lock.json b/plugins/sip/package-lock.json index fbf8cbc95..b3de2f84e 100644 --- a/plugins/sip/package-lock.json +++ b/plugins/sip/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/sip", - "version": "0.0.1", + "version": "0.0.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/sip", - "version": "0.0.1", + "version": "0.0.3", "dependencies": { "@homebridge/camera-utils": "^2.0.4", "rxjs": "^7.5.5", @@ -113,7 +113,7 @@ }, "../../sdk": { "name": "@scrypted/sdk", - "version": "0.2.12", + "version": "0.2.22", "dev": true, "license": "ISC", "dependencies": { diff --git a/plugins/sip/package.json b/plugins/sip/package.json index fc6cd5633..d627a4e16 100644 --- a/plugins/sip/package.json +++ b/plugins/sip/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/sip", - "version": "0.0.1", + "version": "0.0.3", "scripts": { "scrypted-setup-project": "scrypted-setup-project", "prescrypted-setup-project": "scrypted-package-json", diff --git a/plugins/sip/src/main.ts b/plugins/sip/src/main.ts index 7891c0143..93f9094fb 100644 --- a/plugins/sip/src/main.ts +++ b/plugins/sip/src/main.ts @@ -235,24 +235,24 @@ class SipCamera extends ScryptedDeviceBase implements Intercom, Camera, VideoCam let alost = 0; sip.audioSplitter.on('message', message => { - if (!isStunMessage(message)) { - const isRtpMessage = isRtpMessagePayloadType(getPayloadType(message)); - if (!isRtpMessage) - return; - aseen++; - sip.audioSplitter.send(message, rtpPort, "127.0.0.1"); - const seq = getSequenceNumber(message); - if (seq !== (aseq + 1) % 0x0FFFF) - alost++; - aseq = seq; - } - }); + if (!isStunMessage(message)) { + const isRtpMessage = isRtpMessagePayloadType(getPayloadType(message)); + if (!isRtpMessage) + return; + aseen++; + sip.audioSplitter.send(message, rtpPort, "127.0.0.1"); + const seq = getSequenceNumber(message); + if (seq !== (aseq + 1) % 0x0FFFF) + alost++; + aseq = seq; + } + }); - sip.audioRtcpSplitter.on('message', message => { - sip.audioRtcpSplitter.send(message, rtcpPort, "127.0.0.1"); - }); + sip.audioRtcpSplitter.on('message', message => { + sip.audioRtcpSplitter.send(message, rtcpPort, "127.0.0.1"); + }); - this.session = sip; + this.session = sip; } getRawVideoStreamOptions(): ResponseMediaStreamOptions[] { @@ -285,7 +285,7 @@ class SipCamera extends ScryptedDeviceBase implements Intercom, Camera, VideoCam } - createFFmpegMediaStreamOptions(ffmpegInput: string, index: number){ + createFFmpegMediaStreamOptions(ffmpegInput: string, index: number) { try { } catch (e) { diff --git a/plugins/sip/src/rtp-utils.ts b/plugins/sip/src/rtp-utils.ts index ad7f9e361..b7bb0651b 100644 --- a/plugins/sip/src/rtp-utils.ts +++ b/plugins/sip/src/rtp-utils.ts @@ -5,117 +5,117 @@ const stun = require('stun') const stunMagicCookie = 0x2112a442 // https://tools.ietf.org/html/rfc5389#section-6 export interface RtpStreamOptions { - port: number - rtcpPort: number - } - + port: number + rtcpPort: number +} + export interface RtpOptions { - audio: RtpStreamOptions + audio: RtpStreamOptions } export interface RtpStreamDescription extends RtpStreamOptions { - ssrc?: number - iceUFrag?: string - icePwd?: string - } + ssrc?: number + iceUFrag?: string + icePwd?: string +} export interface RtpDescription { - address: string - audio: RtpStreamDescription - sdp: string + address: string + audio: RtpStreamDescription + sdp: string } export function isRtpMessagePayloadType(payloadType: number) { - return payloadType > 90 || payloadType === 0 + return payloadType > 90 || payloadType === 0 } export function getPayloadType(message: Buffer) { - return message.readUInt8(1) & 0x7f + return message.readUInt8(1) & 0x7f } export function getSequenceNumber(message: Buffer) { - return message.readUInt16BE(2) + return message.readUInt16BE(2) } export function isStunMessage(message: Buffer) { - return message.length > 8 && message.readInt32BE(4) === stunMagicCookie + return message.length > 8 && message.readInt32BE(4) === stunMagicCookie } export function sendStunBindingRequest({ - rtpDescription, - rtpSplitter, - rtcpSplitter, - localUfrag, - type, - }: { - rtpSplitter: dgram.Socket - rtcpSplitter: dgram.Socket - rtpDescription: RtpDescription - localUfrag?: string - type: 'video' | 'audio' - }) { - const message = stun.createMessage(1), - remoteDescription = rtpDescription[type], - { address } = rtpDescription, - { iceUFrag, icePwd, port, rtcpPort } = remoteDescription - - if (iceUFrag && icePwd && localUfrag) { - // Full ICE supported. Send as formal stun request - message.addUsername(iceUFrag + ':' + localUfrag) - message.addMessageIntegrity(icePwd) - - stun - .request(`${address}:${port}`, { - socket: rtpSplitter, - message, - }) - .then(() => console.debug(`${type} stun complete`)) - .catch((e: Error) => { - console.error(`${type} stun error`) - console.error(e) - }) - } else { - // ICE not supported. Fire and forget the stun request for RTP and RTCP - const encodedMessage = stun.encode(message) - try { - rtpSplitter.send(encodedMessage, port, address) - } catch (e) { + rtpDescription, + rtpSplitter, + rtcpSplitter, + localUfrag, + type, +}: { + rtpSplitter: dgram.Socket + rtcpSplitter: dgram.Socket + rtpDescription: RtpDescription + localUfrag?: string + type: 'video' | 'audio' +}) { + const message = stun.createMessage(1), + remoteDescription = rtpDescription[type], + { address } = rtpDescription, + { iceUFrag, icePwd, port, rtcpPort } = remoteDescription + + if (iceUFrag && icePwd && localUfrag) { + // Full ICE supported. Send as formal stun request + message.addUsername(iceUFrag + ':' + localUfrag) + message.addMessageIntegrity(icePwd) + + stun + .request(`${address}:${port}`, { + socket: rtpSplitter, + message, + }) + .then(() => console.debug(`${type} stun complete`)) + .catch((e: Error) => { + console.error(`${type} stun error`) console.error(e) - } - - try { - rtcpSplitter.send(encodedMessage, rtcpPort, address) - } catch (e) { - console.error(e) - } + }) + } else { + // ICE not supported. Fire and forget the stun request for RTP and RTCP + const encodedMessage = stun.encode(message) + try { + rtpSplitter.send(encodedMessage, port, address) + } catch (e) { + console.error(e) + } + + try { + rtcpSplitter.send(encodedMessage, rtcpPort, address) + } catch (e) { + console.error(e) } } - - export function createStunResponder(rtpSplitter: dgram.Socket) { - return rtpSplitter.on('message', (message, info) => { - if (!isStunMessage(message)) { - return null - } - - try { - const decodedMessage = stun.decode(message), - response = stun.createMessage( - stun.constants.STUN_BINDING_RESPONSE, - decodedMessage.transactionId - ) - - response.addXorAddress(info.address, info.port) - try { - rtpSplitter.send(stun.encode(response), info.port, info.address) - } catch (e) { - console.error(e) - } - } catch (e) { - console.debug('Failed to Decode STUN Message') - console.debug(message.toString('hex')) - console.debug(e) - } - +} + +export function createStunResponder(rtpSplitter: dgram.Socket) { + return rtpSplitter.on('message', (message, info) => { + if (!isStunMessage(message)) { return null - }) - } + } + + try { + const decodedMessage = stun.decode(message), + response = stun.createMessage( + stun.constants.STUN_BINDING_RESPONSE, + decodedMessage.transactionId + ) + + response.addXorAddress(info.address, info.port) + try { + rtpSplitter.send(stun.encode(response), info.port, info.address) + } catch (e) { + console.error(e) + } + } catch (e) { + console.debug('Failed to Decode STUN Message') + console.debug(message.toString('hex')) + console.debug(e) + } + + return null + }) +} diff --git a/plugins/sip/src/sip-call.ts b/plugins/sip/src/sip-call.ts index d23d259f0..4225a797f 100644 --- a/plugins/sip/src/sip-call.ts +++ b/plugins/sip/src/sip-call.ts @@ -3,7 +3,7 @@ import { randomInteger, randomString } from './util' import { RtpDescription, RtpOptions, RtpStreamDescription } from './rtp-utils' const sip = require('sip'), - sdp = require('sdp') + sdp = require('sdp') export interface SipOptions { to: string @@ -65,13 +65,13 @@ function getRtpDescription( ): RtpStreamDescription { try { const section = sections.find((s) => s.startsWith('m=' + mediaType)), - { port } = sdp.parseMLine(section), - lines: string[] = sdp.splitLines(section), - rtcpLine = lines.find((l: string) => l.startsWith('a=rtcp:')), - rtcpMuxLine = lines.find((l: string) => l.startsWith('a=rtcp-mux')), - ssrcLine = lines.find((l: string) => l.startsWith('a=ssrc')), - iceUFragLine = lines.find((l: string) => l.startsWith('a=ice-ufrag')), - icePwdLine = lines.find((l: string) => l.startsWith('a=ice-pwd')) + { port } = sdp.parseMLine(section), + lines: string[] = sdp.splitLines(section), + rtcpLine = lines.find((l: string) => l.startsWith('a=rtcp:')), + rtcpMuxLine = lines.find((l: string) => l.startsWith('a=rtcp-mux')), + ssrcLine = lines.find((l: string) => l.startsWith('a=ssrc')), + iceUFragLine = lines.find((l: string) => l.startsWith('a=ice-ufrag')), + icePwdLine = lines.find((l: string) => l.startsWith('a=ice-pwd')) let rtcpPort: number; if (rtcpMuxLine) { @@ -84,9 +84,9 @@ function getRtpDescription( return { port, rtcpPort, - ssrc: (ssrcLine && Number(ssrcLine.match(/ssrc:(\S*)/)?.[1])) || undefined, + ssrc: (ssrcLine && Number(ssrcLine.match(/ssrc:(\S*)/)?.[1])) || undefined, iceUFrag: (iceUFragLine && iceUFragLine.match(/ice-ufrag:(\S*)/)?.[1]) || undefined, - icePwd: (icePwdLine && icePwdLine.match(/ice-pwd:(\S*)/)?.[1]) || undefined, + icePwd: (icePwdLine && icePwdLine.match(/ice-pwd:(\S*)/)?.[1]) || undefined, } } catch (e) { console.error('Failed to parse SDP from remote end') @@ -138,7 +138,7 @@ export class SipCall { ssrc = randomInteger(); this.sipStack = { - makeResponse: sip.makeResponse, + makeResponse: sip.makeResponse, ...sip.create({ host, hostname: host, @@ -152,17 +152,18 @@ export class SipCall { // }, ws: false }, - (request: SipRequest) => { - if (request.method === 'BYE') { - this.console.info('received BYE from remote end') - this.sipStack.send(this.sipStack.makeResponse(request, 200, 'Ok')) + (request: SipRequest) => { + if (request.method === 'BYE') { + this.console.info('received BYE from remote end') + this.sipStack.send(this.sipStack.makeResponse(request, 200, 'Ok')) - if (this.destroyed) { - this.onEndedByRemote.next(null) + if (this.destroyed) { + this.onEndedByRemote.next(null) + } } - } - } - )} + } + ) + } this.sdp = ([ 'v=0', diff --git a/plugins/sip/src/sip-session.ts b/plugins/sip/src/sip-session.ts index 672c81726..d9963e9f2 100644 --- a/plugins/sip/src/sip-session.ts +++ b/plugins/sip/src/sip-session.ts @@ -1,38 +1,10 @@ -import { ReplaySubject, timer } from 'rxjs' -import { once } from 'events' -import { createStunResponder, RtpDescription, RtpOptions, sendStunBindingRequest } from './rtp-utils' import { reservePorts } from '@homebridge/camera-utils'; -import { SipCall, SipOptions } from './sip-call' -import { Subscribed } from './subscribed' -import dgram from 'dgram' - -export async function bindUdp(server: dgram.Socket, usePort: number) { - server.bind({ - port: usePort, - // exclusive: false, - // address: '0.0.0.0', - }) - await once(server, 'listening') - server.setRecvBufferSize(1024 * 1024) - const port = server.address().port - return { - port, - url: `udp://'0.0.0.0':${port}`, - } -} - -export async function createBindUdp(usePort: number) { - const server = dgram.createSocket({ - type: 'udp4', - // reuseAddr: true, - }), - { port, url } = await bindUdp(server, usePort) - return { - server, - port, - url, - } -} +import { createBindUdp } from '@scrypted/common/src/listen-cluster'; +import dgram from 'dgram'; +import { ReplaySubject, timer } from 'rxjs'; +import { createStunResponder, RtpDescription, RtpOptions, sendStunBindingRequest } from './rtp-utils'; +import { SipCall, SipOptions } from './sip-call'; +import { Subscribed } from './subscribed'; export class SipSession extends Subscribed { private hasStarted = false @@ -107,15 +79,15 @@ export class SipSession extends Subscribed { try { const rtpDescription = await this.sipCall.invite(), - sendStunRequests = () => { - sendStunBindingRequest({ - rtpSplitter: this.audioSplitter, - rtcpSplitter: this.audioRtcpSplitter, - rtpDescription, - localUfrag: this.sipCall.audioUfrag, - type: 'audio', - }) - } + sendStunRequests = () => { + sendStunBindingRequest({ + rtpSplitter: this.audioSplitter, + rtcpSplitter: this.audioRtcpSplitter, + rtpDescription, + localUfrag: this.sipCall.audioUfrag, + type: 'audio', + }) + } // if rtcp-mux is supported, rtp splitter will be used for both rtp and rtcp if (rtpDescription.audio.port === rtpDescription.audio.rtcpPort) { @@ -169,7 +141,7 @@ export class SipSession extends Subscribed { this.onCallEndedSubject.next(null) this.sipCall.destroy() this.audioSplitter.close() - this.audioRtcpSplitter.close() + this.audioRtcpSplitter.close() this.unsubscribe() }