Added some to parse optional video streams from the SIP call (#457)

* Added some to parse optional video streams from the SIP call

* Added some to parse optional video streams from the SIP call
This commit is contained in:
slyoldfox
2022-12-06 17:04:01 +01:00
committed by GitHub
parent bf5b90dc2c
commit a5bae93813
3 changed files with 94 additions and 25 deletions

View File

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

View File

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

View File

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