mirror of
https://github.com/koush/scrypted.git
synced 2026-09-17 09:10:38 +01:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user