mirror of
https://github.com/koush/scrypted.git
synced 2026-02-03 06:03:27 +00:00
sip: publish plugin
This commit is contained in:
2
plugins/sip/.vscode/launch.json
vendored
2
plugins/sip/.vscode/launch.json
vendored
@@ -17,7 +17,7 @@
|
||||
"sourceMaps": true,
|
||||
"localRoot": "${workspaceFolder}/out",
|
||||
"remoteRoot": "/plugin/",
|
||||
"type": "pwa-node"
|
||||
"type": "node"
|
||||
}
|
||||
]
|
||||
}
|
||||
2
plugins/sip/.vscode/settings.json
vendored
2
plugins/sip/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "koushik-ubuntu",
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
6
plugins/sip/package-lock.json
generated
6
plugins/sip/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user