diff --git a/plugins/sip/src/rtp-utils.ts b/plugins/sip/src/rtp-utils.ts index b7bb0651b..1c48dcc17 100644 --- a/plugins/sip/src/rtp-utils.ts +++ b/plugins/sip/src/rtp-utils.ts @@ -11,6 +11,7 @@ export interface RtpStreamOptions { export interface RtpOptions { audio: RtpStreamOptions + video: RtpStreamOptions } export interface RtpStreamDescription extends RtpStreamOptions { @@ -22,6 +23,7 @@ export interface RtpStreamDescription extends RtpStreamOptions { export interface RtpDescription { address: string audio: RtpStreamDescription + video: RtpStreamDescription sdp: string } @@ -52,10 +54,12 @@ export function sendStunBindingRequest({ rtcpSplitter: dgram.Socket rtpDescription: RtpDescription localUfrag?: string - type: 'video' | 'audio' + type: 'audio' | 'video' }) { + const remoteDescription = rtpDescription[type]; + if( remoteDescription.port == 0 ) + return; const message = stun.createMessage(1), - remoteDescription = rtpDescription[type], { address } = rtpDescription, { iceUFrag, icePwd, port, rtcpPort } = remoteDescription diff --git a/plugins/sip/src/sip-call.ts b/plugins/sip/src/sip-call.ts index 4225a797f..a5e1e2346 100644 --- a/plugins/sip/src/sip-call.ts +++ b/plugins/sip/src/sip-call.ts @@ -10,6 +10,8 @@ export interface SipOptions { from: string localIp: string localPort: number + udp: boolean + tcp: boolean } interface UriOptions { @@ -61,17 +63,24 @@ function getRandomId() { function getRtpDescription( console: any, sections: string[], - mediaType: 'audio' + mediaType: 'audio' | 'video' ): 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')) + const section = sections.find((s) => s.startsWith('m=' + mediaType)); + if( section === undefined ) { + return { + port: 0, + rtcpPort: 0 + }; + } + + const { 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) { @@ -105,7 +114,8 @@ function parseRtpDescription(console: any, inviteResponse: { return { sdp: inviteResponse.content, address: cLine.match(/c=IN IP4 (\S*)/)![1], - audio: getRtpDescription(console, sections, 'audio') + audio: getRtpDescription(console, sections, 'audio'), + video: getRtpDescription(console, sections, 'video') } } @@ -131,7 +141,7 @@ export class SipCall { ) { this.console = console; - const { audio } = rtpOptions, + const { audio, video } = rtpOptions, { from } = this.sipOptions, host = this.sipOptions.localIp, port = this.sipOptions.localPort, @@ -143,13 +153,13 @@ export class SipCall { host, hostname: host, port: port, - udp: true, - tcp: false, + udp: this.sipOptions.udp, + tcp: this.sipOptions.tcp, tls: false, // tls_port: tlsPort, // tls: { // rejectUnauthorized: false, - // }, + // }, ws: false }, (request: SipRequest) => { @@ -179,6 +189,25 @@ export class SipCall { ] .filter((l) => l) .join('\r\n')) + '\r\n'; + + /* Example SDP for audio and video + this.sdp = ([ + 'v=0', + `o=${from.split(':')[1].split('@')[0]} 3747 461 IN IP4 ${host}`, + 's=ScryptedSipPlugin', + `c=IN IP4 ${host}`, + 't=0 0', + `m=audio ${audio.port} RTP/AVP 97`, + `a=rtpmap:97 speex/8000`, + `m=video ${video.port} RTP/AVP 97`, + `a=rtpmap:97 H264/90000`, + `a=fmtp:97 profile-level-id=42801F`, + `a=ssrc:${ssrc}`, + 'a=recvonly' + ] + .filter((l) => l) + .join('\r\n')) + '\r\n'; + */ } request({ @@ -299,7 +328,9 @@ export class SipCall { } destroy() { + this.console.debug("detroying sip-call") this.destroyed = true this.sipStack.destroy() + this.console.debug("detroying sip-call: done") } } diff --git a/plugins/sip/src/sip-session.ts b/plugins/sip/src/sip-session.ts index d9963e9f2..c65b6e9ac 100644 --- a/plugins/sip/src/sip-session.ts +++ b/plugins/sip/src/sip-session.ts @@ -1,5 +1,5 @@ import { reservePorts } from '@homebridge/camera-utils'; -import { createBindUdp } from '@scrypted/common/src/listen-cluster'; +import { createBindUdp, createBindZero } from '@scrypted/common/src/listen-cluster'; import dgram from 'dgram'; import { ReplaySubject, timer } from 'rxjs'; import { createStunResponder, RtpDescription, RtpOptions, sendStunBindingRequest } from './rtp-utils'; @@ -19,6 +19,8 @@ export class SipSession extends Subscribed { public readonly rtpOptions: RtpOptions, public readonly audioSplitter: dgram.Socket, public audioRtcpSplitter: dgram.Socket, + public readonly videoSplitter: dgram.Socket, + public videoRtcpSplitter: dgram.Socket, public readonly cameraName: string ) { super() @@ -27,13 +29,18 @@ export class SipSession extends Subscribed { } static async createSipSession(console: any, cameraName: string, sipOptions: SipOptions) { - const audioPort = 0, - audioSplitter = await createBindUdp(audioPort), + const audioSplitter = await createBindZero(), audioRtcpSplitter = await createBindUdp(audioSplitter.port + 1), + videoSplitter = await createBindZero(), + videoRtcpSplitter = await createBindUdp(videoSplitter.port + 1), rtpOptions = { audio: { port: audioSplitter.port, rtcpPort: audioRtcpSplitter.port + }, + video: { + port: videoSplitter.port, + rtcpPort: videoRtcpSplitter.port } } @@ -43,6 +50,8 @@ export class SipSession extends Subscribed { rtpOptions, audioSplitter.server, audioRtcpSplitter.server, + videoSplitter.server, + videoRtcpSplitter.server, cameraName ) } @@ -87,18 +96,35 @@ export class SipSession extends Subscribed { localUfrag: this.sipCall.audioUfrag, type: 'audio', }) + sendStunBindingRequest({ + rtpSplitter: this.videoSplitter, + rtcpSplitter: this.videoRtcpSplitter, + rtpDescription, + localUfrag: this.sipCall.videoUfrag, + type: 'video', + }) } // if rtcp-mux is supported, rtp splitter will be used for both rtp and rtcp - if (rtpDescription.audio.port === rtpDescription.audio.rtcpPort) { + if ( rtpDescription.audio.port > 0 && rtpDescription.audio.port === rtpDescription.audio.rtcpPort) { this.audioRtcpSplitter.close() this.audioRtcpSplitter = this.audioSplitter } - if (rtpDescription.audio.iceUFrag) { + if ( rtpDescription.video.port > 0 && rtpDescription.video.port === rtpDescription.video.rtcpPort) { + this.videoRtcpSplitter.close() + this.videoRtcpSplitter = this.videoSplitter + } + + if ( (rtpDescription.audio.port > 0 && rtpDescription.audio.iceUFrag)|| (rtpDescription.video.port > 0 && rtpDescription.video.iceUFrag ) ) { // ICE is supported this.console.log(`Connecting to ${this.cameraName} using ICE`) - createStunResponder(this.audioSplitter) + if( rtpDescription.audio.port > 0 ) { + createStunResponder(this.audioSplitter) + } + if( rtpDescription.video.port > 0 ) { + createStunResponder(this.videoSplitter) + } sendStunRequests() } else { @@ -111,7 +137,11 @@ export class SipSession extends Subscribed { } this.audioSplitter.once('message', () => { - this.console.log(`Audio stream latched for ${this.cameraName}`) + this.console.log(`Audio stream latched for ${this.cameraName}, port: ${this.rtpOptions.audio.port}`) + }) + + this.videoSplitter.once('message', () => { + this.console.log(`Video stream latched for ${this.cameraName}, port: ${this.rtpOptions.video.port}`) }) return rtpDescription @@ -123,7 +153,7 @@ export class SipSession extends Subscribed { } static async reserveRtpRtcpPorts() { - const ports = await reservePorts({ count: 2, type: 'udp' }) + const ports = await reservePorts({ count: 4, type: 'udp' }) return ports } @@ -134,15 +164,19 @@ export class SipSession extends Subscribed { this.hasCallEnded = true if (sendBye) { - await this.sipCall.sendBye().catch(this.console.log) + await this.sipCall.sendBye().catch(this.console.error) } // clean up + this.console.log("sip-session callEnded") this.onCallEndedSubject.next(null) this.sipCall.destroy() this.audioSplitter.close() this.audioRtcpSplitter.close() + this.videoSplitter.close() + this.videoRtcpSplitter.close() this.unsubscribe() + this.console.log("sip-session callEnded: done") } async stop() {