sip: publish plugin

This commit is contained in:
Koushik Dutta
2022-11-28 10:09:44 -08:00
parent 7679e3a550
commit 98b975594a
9 changed files with 150 additions and 177 deletions

View File

@@ -17,7 +17,7 @@
"sourceMaps": true,
"localRoot": "${workspaceFolder}/out",
"remoteRoot": "/plugin/",
"type": "pwa-node"
"type": "node"
}
]
}

View File

@@ -1,4 +1,4 @@
{
"scrypted.debugHost": "koushik-ubuntu",
"scrypted.debugHost": "127.0.0.1",
}

View File

@@ -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.

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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) {

View File

@@ -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
})
}

View File

@@ -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',

View File

@@ -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()
}