From 8a3ce5118d9747f98fc82893af55f88fa9be5eab Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Thu, 3 Mar 2022 13:03:16 -0800 Subject: [PATCH] common: fix rtsp track selection, path suffixing, udp/tcp sdp port setup. --- common/src/rtsp-server.ts | 26 +++++++++++++------------- common/src/sdp-utils.ts | 26 +++++++++++++++++++------- common/src/wrtc-to-rtsp.ts | 17 +++++++++++------ 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/common/src/rtsp-server.ts b/common/src/rtsp-server.ts index fa82cb20e..8c1f7c964 100644 --- a/common/src/rtsp-server.ts +++ b/common/src/rtsp-server.ts @@ -14,10 +14,6 @@ interface Headers { [header: string]: string } -function findSyncFrame(streamChunks: StreamChunk[]): StreamChunk[] { - return streamChunks; -} - export interface RtspStreamParser extends StreamParser { sdp: Promise; } @@ -141,14 +137,16 @@ export class RtspClient extends RtspBase { } } - writeRequest(method: string, headers?: Headers, path?: string, body?: Buffer, authenticating?: boolean) { + writeRequest(method: string, headers?: Headers, path?: string, body?: Buffer) { headers = headers || {}; - let fullUrl: string; - if (path) - fullUrl = new URL(path, this.url).toString(); - else - fullUrl = this.url; + let fullUrl = this.url; + if (path) { + // strangely, RTSP urls do not behave like expected from an HTTP-ish server. + // ffmpeg will happily suffix path segments after query strings: + // SETUP rtsp://localhost:5554/cam/realmonitor?channel=1&subtype=0/trackID=0 RTSP/1.0 + fullUrl += '/' + path; + } const sanitized = new URL(fullUrl); sanitized.username = ''; @@ -172,7 +170,7 @@ export class RtspClient extends RtspBase { headers: Headers, body: Buffer }> { - this.writeRequest(method, headers, path, body, authenticating); + this.writeRequest(method, headers, path, body); const message = await this.readMessage(); const status = message[0]; @@ -381,6 +379,8 @@ export class RtspServer { this.respond(200, 'OK', requestHeaders, headers, Buffer.from(this.sdp)) } + // todo: use the sdp itself to determine the audio/video track ids so + // rewriting is not necessary. setup(url: string, requestHeaders: Headers) { const headers: Headers = {}; const transport = requestHeaders['transport']; @@ -391,7 +391,7 @@ export class RtspServer { const [_, rtp, rtcp] = match; if (url.includes('audio')) this.udpPorts.audio = parseInt(rtp); - else + else if (url.includes('video')) this.udpPorts.video = parseInt(rtp); } else if (transport.includes('TCP')) { @@ -402,7 +402,7 @@ export class RtspServer { if (url.includes('audio')) { this.audioChannel = low; } - else { + else if (url.includes('video')) { this.videoChannel = low; } } diff --git a/common/src/sdp-utils.ts b/common/src/sdp-utils.ts index 61bc4725b..e435a7aa0 100644 --- a/common/src/sdp-utils.ts +++ b/common/src/sdp-utils.ts @@ -18,21 +18,33 @@ export function parsePayloadTypes(sdp: string) { export function findTrack(sdp: string, type: string, directions: TrackDirection[] = ['recvonly']) { const tracks = sdp.split('m=').filter(track => track.startsWith(type)); + for (const track of tracks) { + const returnTrack = () => { + const lines = track.split('\n').map(line => line.trim()); + const control = lines.find(line => line.startsWith('a=control:')); + return { + section: 'm=' + track, + trackId: control?.split('a=control:')?.[1], + }; + } + for (const dir of directions) { if (track.includes(`a=${dir}`)) { - const lines = track.split('\n').map(line => line.trim()); - const control = lines.find(line => line.startsWith('a=control:')); - return { - section: 'm=' + track, - trackId: control?.split('a=control:')?.[1], - }; + return returnTrack(); } } + + // some sdp do not advertise a media flow direction. i think recvonly is the default? + if ((directions.includes('recvonly')) + && !track.includes('sendonly') + && !track.includes('inactive')) { + return returnTrack(); + } } } -type TrackDirection = 'sendonly' | 'sendrecv' | 'recvonly'; +type TrackDirection = 'sendonly' | 'sendrecv' | 'recvonly' | 'inactive'; export function parseTrackIds(sdp: string, directions: TrackDirection[] = ['recvonly', 'sendrecv']) { return { diff --git a/common/src/wrtc-to-rtsp.ts b/common/src/wrtc-to-rtsp.ts index fb8159f15..ba9352c6e 100644 --- a/common/src/wrtc-to-rtsp.ts +++ b/common/src/wrtc-to-rtsp.ts @@ -10,10 +10,15 @@ import { RTCSessionControl, ScryptedDeviceBase } from "@scrypted/sdk"; // h264 baseline and opus are required codecs that all webrtc implementations must provide. function createSdpInput(audioPort: number, videoPort: number, sdp: string) { let outputSdp = sdp + .replace(/c=IN .*?/, `c=IN IP4 127.0.0.1`) .replace(/m=audio \d+/, `m=audio ${audioPort}`) .replace(/m=video \d+/, `m=video ${videoPort}`); - const lines = outputSdp.split('\n').map(line => line.trim()); + let lines = outputSdp.split('\n').map(line => line.trim()); + lines = lines + .filter(line => !line.includes('a=candidate')) + .filter(line => !line.includes('a=ice')); + const vindex = lines.findIndex(line => line.startsWith('m=video')); lines.splice(vindex + 1, 0, 'a=control:trackID=video'); const aindex = lines.findIndex(line => line.startsWith('m=audio')); @@ -43,8 +48,8 @@ export function getRTCMediaStreamOptions(id: string, name: string): MediaStreamO export async function createRTCPeerConnectionSource(channel: ScryptedDeviceBase & RTCSignalingChannel, id: string): Promise { const { console, name } = channel; - const videoPort = Math.round(Math.random() * 10000 + 30000); - const audioPort = Math.round(Math.random() * 10000 + 30000); + const videoPort = useUdp ? Math.round(Math.random() * 10000 + 30000) : 0; + const audioPort = useUdp ? Math.round(Math.random() * 10000 + 30000) : 0; const { clientPromise, port } = await listenZeroSingleClient(); @@ -65,13 +70,13 @@ export async function createRTCPeerConnectionSource(channel: ScryptedDeviceBase } catch (e) { } - sessionControl?.endSession().catch(() => {}); + sessionControl?.endSession().catch(() => { }); }; clientPromise.then(async (client) => { socket = client; const rtspServer = new RtspServer(socket, undefined, udp); - // rtspServer.console = console; + rtspServer.console = console; rtspServer.audioChannel = 0; rtspServer.videoChannel = 2; @@ -203,7 +208,7 @@ export async function createRTCPeerConnectionSource(channel: ScryptedDeviceBase }; rtspServer.client.write(rtspServer.sdp + '\r\n'); rtspServer.client.end(); - rtspServer.client.on('data', () => {}); + rtspServer.client.on('data', () => { }); // rtspServer.client.destroy(); console.log('sdp sent'); }