diff --git a/common/src/rtsp-server.ts b/common/src/rtsp-server.ts index 92ff26a762..4040527b88 100644 --- a/common/src/rtsp-server.ts +++ b/common/src/rtsp-server.ts @@ -681,7 +681,7 @@ export class RtspClient extends RtspBase { }); } - async setup(options: RtspClientTcpSetupOptions | RtspClientUdpSetupOptions) { + async setup(options: RtspClientTcpSetupOptions | RtspClientUdpSetupOptions, headers?: Headers) { const protocol = options.type === 'udp' ? '' : '/TCP'; const client = options.type === 'udp' ? 'client_port' : 'interleaved'; let port: number; @@ -697,9 +697,9 @@ export class RtspClient extends RtspBase { port = options.dgram.address().port; options.dgram.on('message', data => options.onRtp(undefined, data)); } - const headers: any = { + headers = Object.assign({ Transport: `RTP/AVP${protocol};unicast;${client}=${port}-${port + 1}`, - }; + }, headers); const response = await this.request('SETUP', headers, options.path); let interleaved: { begin: number; diff --git a/plugins/amcrest/package-lock.json b/plugins/amcrest/package-lock.json index dbfb118b2c..3840bd39a3 100644 --- a/plugins/amcrest/package-lock.json +++ b/plugins/amcrest/package-lock.json @@ -16,7 +16,7 @@ "multiparty": "^4.2.2" }, "devDependencies": { - "@types/node": "^16.11.0" + "@types/node": "^18.15.11" } }, "../../common": { @@ -36,7 +36,7 @@ }, "../../sdk": { "name": "@scrypted/sdk", - "version": "0.2.68", + "version": "0.2.87", "license": "ISC", "dependencies": { "@babel/preset-typescript": "^7.18.6", @@ -100,9 +100,9 @@ } }, "node_modules/@types/node": { - "version": "16.11.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.0.tgz", - "integrity": "sha512-8MLkBIYQMuhRBQzGN9875bYsOhPnf/0rgXGo66S2FemHkhbn9qtsz9ywV1iCG+vbjigE4WUNVvw37Dx+L0qsPg==" + "version": "18.15.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" }, "node_modules/auth-header": { "version": "1.0.0", @@ -291,9 +291,9 @@ } }, "@types/node": { - "version": "16.11.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.0.tgz", - "integrity": "sha512-8MLkBIYQMuhRBQzGN9875bYsOhPnf/0rgXGo66S2FemHkhbn9qtsz9ywV1iCG+vbjigE4WUNVvw37Dx+L0qsPg==" + "version": "18.15.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" }, "auth-header": { "version": "1.0.0", diff --git a/plugins/amcrest/package.json b/plugins/amcrest/package.json index 7c0729a859..c1a5a844db 100644 --- a/plugins/amcrest/package.json +++ b/plugins/amcrest/package.json @@ -36,12 +36,12 @@ }, "dependencies": { "@koush/axios-digest-auth": "^0.8.5", - "@scrypted/sdk": "file:../../sdk", "@scrypted/common": "file:../../common", + "@scrypted/sdk": "file:../../sdk", "@types/multiparty": "^0.0.33", "multiparty": "^4.2.2" }, "devDependencies": { - "@types/node": "^16.11.0" + "@types/node": "^18.15.11" } } diff --git a/plugins/hikvision/package-lock.json b/plugins/hikvision/package-lock.json index 7560289db1..c1723539ff 100644 --- a/plugins/hikvision/package-lock.json +++ b/plugins/hikvision/package-lock.json @@ -12,11 +12,13 @@ "@koush/axios-digest-auth": "^0.8.5", "@scrypted/common": "file:../../common", "@scrypted/sdk": "file:../../sdk", - "@types/node": "^16.9.1", "@types/xml2js": "^0.4.9", "axios": "^0.23.0", "lodash": "^4.17.21", "xml2js": "^0.4.23" + }, + "devDependencies": { + "@types/node": "^18.15.11" } }, "../../common": { @@ -36,7 +38,7 @@ }, "../../sdk": { "name": "@scrypted/sdk", - "version": "0.2.86", + "version": "0.2.87", "license": "ISC", "dependencies": { "@babel/preset-typescript": "^7.18.6", @@ -100,9 +102,9 @@ "link": true }, "node_modules/@types/node": { - "version": "16.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", - "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==" + "version": "18.15.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" }, "node_modules/@types/xml2js": { "version": "0.4.9", @@ -231,9 +233,9 @@ } }, "@types/node": { - "version": "16.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", - "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==" + "version": "18.15.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" }, "@types/xml2js": { "version": "0.4.9", diff --git a/plugins/hikvision/package.json b/plugins/hikvision/package.json index 0751e938af..71f387a9cf 100644 --- a/plugins/hikvision/package.json +++ b/plugins/hikvision/package.json @@ -38,10 +38,12 @@ "@koush/axios-digest-auth": "^0.8.5", "@scrypted/common": "file:../../common", "@scrypted/sdk": "file:../../sdk", - "@types/node": "^16.9.1", "@types/xml2js": "^0.4.9", "axios": "^0.23.0", "lodash": "^4.17.21", "xml2js": "^0.4.23" + }, + "devDependencies": { + "@types/node": "^18.15.11" } } diff --git a/plugins/hikvision/src/main.ts b/plugins/hikvision/src/main.ts index a8dada3b9a..ba2510fda9 100644 --- a/plugins/hikvision/src/main.ts +++ b/plugins/hikvision/src/main.ts @@ -8,6 +8,8 @@ import { OnvifIntercom } from "../../onvif/src/onvif-intercom"; import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp"; import { HikvisionCameraAPI, HikvisionCameraEvent } from "./hikvision-camera-api"; import { hikvisionHttpsAgent } from './probe'; +import { startRtpForwarderProcess } from '../../webrtc/src/rtp-forwarders'; +import { RtpPacket } from '../../../external/werift/packages/rtp/src/rtp/rtp'; const { mediaManager } = sdk; @@ -21,8 +23,8 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom { detectedChannels: Promise>; client: HikvisionCameraAPI; onvifIntercom = new OnvifIntercom(this); - cp: ChildProcess; - + activeIntercom: Awaited>; + constructor(nativeId: string, provider: RtspProvider) { super(nativeId, provider); @@ -360,13 +362,11 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom { async startIntercom(media: MediaObject): Promise { if (this.storage.getItem('twoWayAudio') === 'ONVIF') { + this.activeIntercom?.kill(); + this.activeIntercom = undefined; const options = await this.getConstructedVideoStreamOptions(); const stream = options[0]; - const url = new URL(stream.url); - // amcrest onvif requires this proto query parameter, or onvif two way - // will not activate. - url.searchParams.set('proto', 'Onvif'); - this.onvifIntercom.url = url.toString(); + this.onvifIntercom.url = stream.url; return this.onvifIntercom.startIntercom(media); } @@ -390,7 +390,7 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom { } } catch (e) { - this.console.error('Fialure while determining two way audio codec', e); + this.console.error('Failure while determining two way audio codec', e); } if (codec === 'G.711ulaw') { @@ -415,76 +415,64 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom { const buffer = await mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput); const ffmpegInput = JSON.parse(buffer.toString()) as FFmpegInput; - const args = ffmpegInput.inputArguments.slice(); - args.unshift('-hide_banner'); - - args.push( - "-vn", - '-ar', '8000', - '-ac', '1', - '-acodec', codec, - '-f', format, - 'pipe:3', - ); + const passthrough = new PassThrough(); + const open = `http://${this.getHttpAddress()}/ISAPI/System/TwoWayAudio/channels/${channel}/open`; + const { data } = await this.getClient().digestAuth.request({ + httpsAgent: hikvisionHttpsAgent, + method: 'PUT', + url: open, + }); + this.console.log('two way audio opened', data); - this.console.log('ffmpeg intercom', args); + const url = `http://${this.getHttpAddress()}/ISAPI/System/TwoWayAudio/channels/${channel}/audioData`; + this.console.log('posting audio data to', url); - const ffmpeg = await mediaManager.getFFmpegPath(); - this.cp = child_process.spawn(ffmpeg, args, { - stdio: ['pipe', 'pipe', 'pipe', 'pipe'], + const put = this.getClient().digestAuth.request({ + httpsAgent: hikvisionHttpsAgent, + method: 'PUT', + url, + headers: { + 'Content-Type': 'application/octet-stream', + // 'Connection': 'close', + 'Content-Length': '0' + }, + data: passthrough, }); - this.cp.on('exit', () => this.cp = undefined); - ffmpegLogInitialOutput(this.console, this.cp); - const socket = this.cp.stdio[3] as Readable; - (async () => { - const passthrough = new PassThrough(); - - try { - const open = `http://${this.getHttpAddress()}/ISAPI/System/TwoWayAudio/channels/${channel}/open`; - const { data } = await this.getClient().digestAuth.request({ - httpsAgent: hikvisionHttpsAgent, - method: 'PUT', - url: open, - }); - this.console.log('two way audio opened', data); - - const url = `http://${this.getHttpAddress()}/ISAPI/System/TwoWayAudio/channels/${channel}/audioData`; - this.console.log('posting audio data to', url); - - // seems the dahua doorbells preferred 1024 chunks. should investigate adts - // parsing and sending multipart chunks instead. - this.getClient().digestAuth.request({ - httpsAgent: hikvisionHttpsAgent, - method: 'PUT', - url, - headers: { - 'Content-Type': 'application/octet-stream', - // 'Connection': 'close', - 'Content-Length': '0' - }, - data: passthrough, - }); - - - while (true) { - const data = await readLength(socket, 1024); - passthrough.push(data); - } - } - catch (e) { - } - finally { - this.console.log('audio finished'); - passthrough.end(); + let available = Buffer.alloc(0); + this.activeIntercom?.kill(); + const forwarder = this.activeIntercom = await startRtpForwarderProcess(this.console, ffmpegInput, { + audio: { + onRtp: rtp => { + const parsed = RtpPacket.deSerialize(rtp); + available = Buffer.concat([available, parsed.payload]); + if (available.length > 1024) { + passthrough.push(available.subarray(0, 1024)); + available = available.subarray(1024); + } + }, + codecCopy: codec, + encoderArguments: [ + '-ar', '8000', + '-ac', '1', + '-acodec', codec, + ] } + }); + forwarder.killPromise.finally(() => { + this.console.log('audio finished'); + passthrough.end(); this.stopIntercom(); - })(); - } + }); + put.finally(() => forwarder.kill()); + } async stopIntercom(): Promise { + this.activeIntercom?.kill(); + this.activeIntercom = undefined; + if (this.storage.getItem('twoWayAudio') === 'ONVIF') { return this.onvifIntercom.stopIntercom(); } diff --git a/plugins/onvif/package-lock.json b/plugins/onvif/package-lock.json index 7654b70c85..446b47a421 100644 --- a/plugins/onvif/package-lock.json +++ b/plugins/onvif/package-lock.json @@ -1,18 +1,17 @@ { "name": "@scrypted/onvif", - "version": "0.0.119", + "version": "0.0.120", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/onvif", - "version": "0.0.119", + "version": "0.0.120", "license": "Apache", "dependencies": { "@koush/axios-digest-auth": "^0.8.5", "@scrypted/common": "file:../../common", "@scrypted/sdk": "file:../../sdk", - "@types/node": "^16.9.1", "base-64": "^1.0.0", "http-auth-utils": "^3.0.2", "md5": "^2.3.0", @@ -21,6 +20,7 @@ }, "devDependencies": { "@types/md5": "^2.3.1", + "@types/node": "^18.15.11", "@types/xml2js": "^0.4.9" } }, @@ -65,7 +65,7 @@ }, "../../sdk": { "name": "@scrypted/sdk", - "version": "0.2.68", + "version": "0.2.87", "license": "ISC", "dependencies": { "@babel/preset-typescript": "^7.18.6", @@ -130,9 +130,10 @@ } }, "node_modules/@types/node": { - "version": "16.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", - "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==" + "version": "18.15.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", + "dev": true }, "node_modules/@types/xml2js": { "version": "0.4.9", @@ -328,9 +329,10 @@ } }, "@types/node": { - "version": "16.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", - "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==" + "version": "18.15.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", + "dev": true }, "@types/xml2js": { "version": "0.4.9", diff --git a/plugins/onvif/package.json b/plugins/onvif/package.json index 0555f456c6..cfdb70591b 100644 --- a/plugins/onvif/package.json +++ b/plugins/onvif/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/onvif", - "version": "0.0.119", + "version": "0.0.120", "description": "ONVIF Camera Plugin for Scrypted", "author": "Scrypted", "license": "Apache", @@ -39,7 +39,6 @@ "@koush/axios-digest-auth": "^0.8.5", "@scrypted/common": "file:../../common", "@scrypted/sdk": "file:../../sdk", - "@types/node": "^16.9.1", "base-64": "^1.0.0", "http-auth-utils": "^3.0.2", "md5": "^2.3.0", @@ -48,6 +47,7 @@ }, "devDependencies": { "@types/md5": "^2.3.1", + "@types/node": "^18.15.11", "@types/xml2js": "^0.4.9" } } diff --git a/plugins/onvif/src/onvif-api.ts b/plugins/onvif/src/onvif-api.ts index b9d4178bf8..466cad3110 100644 --- a/plugins/onvif/src/onvif-api.ts +++ b/plugins/onvif/src/onvif-api.ts @@ -35,7 +35,7 @@ function stripNamespaces(topic: string) { let parts = topic.split('/') for (let index = 0; index < parts.length; index++) { let stringNoNamespace = parts[index].split(':').pop() // split on :, then return the last item in the array - if (output.length == 0) { + if (output.length === 0) { output += stringNoNamespace } else { output += '/' + stringNoNamespace @@ -92,9 +92,18 @@ export class OnvifCameraAPI { else ret.emit('event', OnvifEvent.AudioStop) } + // Reolink + else if (eventTopic.includes('Visitor') && (dataValue === true || dataValue === false)) { + if (dataValue) { + ret.emit('event', OnvifEvent.BinaryStart) + } + else { + ret.emit('event', OnvifEvent.BinaryStop) + } + } // Mobotix T26 else if (eventTopic.includes('VideoSource/Alarm')) { - if (dataValue == "Ring" || dataValue == "CameraBellButton") { + if (dataValue === "Ring" || dataValue === "CameraBellButton") { ret.emit('event', OnvifEvent.BinaryRingEvent); } } @@ -155,7 +164,7 @@ export class OnvifCameraAPI { this.console.log('supportsEvents error', err); return reject(err); } - if (!err && data.events && data.events.WSPullPointSupport && data.events.WSPullPointSupport == true) { + if (!err && data.events && data.events.WSPullPointSupport && data.events.WSPullPointSupport === true) { this.console.log('Camera supports WSPullPoint', xml); } else { this.console.log('Camera does not show WSPullPoint support, but trying anyway', xml); diff --git a/plugins/onvif/src/onvif-intercom.ts b/plugins/onvif/src/onvif-intercom.ts index 32ec7cc29c..a859c946ad 100644 --- a/plugins/onvif/src/onvif-intercom.ts +++ b/plugins/onvif/src/onvif-intercom.ts @@ -1,11 +1,13 @@ -import sdk, { MediaObject, Intercom, FFmpegInput, ScryptedMimeTypes } from "@scrypted/sdk"; -import { RtspSmartCamera } from "../../rtsp/src/rtsp"; -import { parseSemicolonDelimited, RtspClient } from "@scrypted/common/src/rtsp-server"; +import { createBindZero } from "@scrypted/common/src/listen-cluster"; +import { RtspClient, parseSemicolonDelimited } from "@scrypted/common/src/rtsp-server"; import { parseSdp } from "@scrypted/common/src/sdp-utils"; -import { ffmpegLogInitialOutput, safePrintFFmpegArguments } from "@scrypted/common/src/media-helpers"; -import child_process from 'child_process'; -import { createBindZero, reserveUdpPort } from "@scrypted/common/src/listen-cluster"; +import sdk, { FFmpegInput, Intercom, MediaObject, ScryptedMimeTypes } from "@scrypted/sdk"; import crypto from 'crypto'; +import { RtpPacket } from '../../../external/werift/packages/rtp/src/rtp/rtp'; +import { nextSequenceNumber } from "../../homekit/src/types/camera/jitter-buffer"; +import { RtspSmartCamera } from "../../rtsp/src/rtsp"; +import { startRtpForwarderProcess } from '../../webrtc/src/rtp-forwarders'; + const { mediaManager } = sdk; @@ -80,11 +82,11 @@ export class OnvifIntercom implements Intercom { const url = new URL(this.url); url.username = username; url.password = password; - this.intercomClient = new RtspClient(url.toString()); - this.intercomClient.console = this.camera.console; - await this.intercomClient.options(); + const intercomClient = this.intercomClient = new RtspClient(url.toString()); + intercomClient.console = this.camera.console; + await intercomClient.options(); - const describe = await this.intercomClient.describe({ + const describe = await intercomClient.describe({ Require, }); this.camera.console.log('ONVIF Backchannel SDP:'); @@ -94,31 +96,35 @@ export class OnvifIntercom implements Intercom { if (!audioBackchannel) throw new Error('ONVIF audio backchannel not found'); - return audioBackchannel; + return { audioBackchannel, intercomClient }; } async startIntercom(media: MediaObject) { + const ffmpegInput = await mediaManager.convertMediaObjectToJSON(media, ScryptedMimeTypes.FFmpegInput); + await this.stopIntercom(); - const audioBackchannel = await this.checkIntercom(); + const { audioBackchannel, intercomClient } = await this.checkIntercom(); if (!audioBackchannel) throw new Error('ONVIF audio backchannel not found'); - const rtp = await reserveUdpPort(); + const rtpServer = await createBindZero('udp4'); + const rtp = rtpServer.port; const rtcp = rtp + 1; let ip: string; let serverRtp: number; let transportDict: ReturnType; + let tcp = false; try { const headers: any = { Require, Transport: `RTP/AVP;unicast;client_port=${rtp}-${rtcp}`, }; - const response = await this.intercomClient.request('SETUP', headers, audioBackchannel.control); + const response = await intercomClient.request('SETUP', headers, audioBackchannel.control); transportDict = parseSemicolonDelimited(response.headers.transport); - this.intercomClient.session = response.headers.session.split(';')[0]; + intercomClient.session = response.headers.session.split(';')[0]; ip = this.camera.getIPAddress(); const { server_port } = transportDict; @@ -126,6 +132,7 @@ export class OnvifIntercom implements Intercom { serverRtp = parseInt(serverPorts[0]); } catch (e) { + tcp = true; this.camera.console.error('onvif udp backchannel failed, falling back to tcp', e); const headers: any = { @@ -133,21 +140,19 @@ export class OnvifIntercom implements Intercom { Transport: `RTP/AVP/TCP;unicast;interleaved=0-1`, }; - const response = await this.intercomClient.request('SETUP', headers, audioBackchannel.control); + const response = await intercomClient.request('SETUP', headers, audioBackchannel.control); transportDict = parseSemicolonDelimited(response.headers.transport); - this.intercomClient.session = response.headers.session.split(';')[0]; + intercomClient.session = response.headers.session.split(';')[0]; ip = '127.0.0.1'; const server = await createBindZero('udp4'); - this.intercomClient.client.on('close', () => server.server.close()); + intercomClient.client.on('close', () => server.server.close()); serverRtp = server.port; server.server.on('message', data => { - this.intercomClient.send(data, 0); + intercomClient.send(data, 0); }); } this.camera.console.log('backchannel transport', transportDict); - const ffmpegInput = await mediaManager.convertMediaObjectToJSON(media, ScryptedMimeTypes.FFmpegInput); - const availableCodecs = [...parseCodecs(audioBackchannel.contents)]; let match: CodecMatch; let codec: SupportedCodec; @@ -171,27 +176,69 @@ export class OnvifIntercom implements Intercom { } // ffmpeg expects ssrc as signed int32. const ssrc = ssrcBuffer.readInt32BE(0); + const ssrcUnsigned = ssrcBuffer.readUint32BE(0); - const args = [ - '-hide_banner', - ...ffmpegInput.inputArguments, - '-vn', - '-acodec', codec.ffmpegCodec, - '-ar', match.sampleRate, - '-ac', match.channels || '1', - "-payload_type", match.payloadType, - "-ssrc", ssrc.toString(), - '-f', 'rtp', - `rtp://${ip}:${serverRtp}?localrtpport=${rtp}&localrtcpport=${rtcp}`, - ]; - safePrintFFmpegArguments(this.camera.console, args); - const cp = child_process.spawn(await mediaManager.getFFmpegPath(), args); - - ffmpegLogInitialOutput(this.camera.console, cp); - - await this.intercomClient.play({ + const payloadType = parseInt(match.payloadType); + + await intercomClient.play({ Require, }); + + let pending: RtpPacket; + let seqNumber = 0; + + const forwarder = await startRtpForwarderProcess(console, ffmpegInput, { + audio: { + onRtp: (rtp) => { + // if (true) { + // const p = RtpPacket.deSerialize(rtp); + // p.header.payloadType = payloadType; + // p.header.ssrc = ssrcUnsigned; + // p.header.marker = true; + // rtpServer.server.send(p.serialize(), serverRtp, ip); + // return; + // } + + const p = RtpPacket.deSerialize(rtp); + + if (!pending) { + pending = p; + return; + } + + if (pending.payload.length + p.payload.length < 1024) { + pending.payload = Buffer.concat([pending.payload, p.payload]); + return; + } + + pending.header.payloadType = payloadType; + pending.header.ssrc = ssrcUnsigned; + pending.header.sequenceNumber = seqNumber; + seqNumber = nextSequenceNumber(seqNumber); + pending.header.marker = true; + + if (!tcp) + rtpServer.server.send(pending.serialize(), serverRtp, ip); + else + intercomClient.send(pending.serialize(), 0); + + pending = p; + }, + codecCopy: codec.ffmpegCodec, + payloadType, + ssrc, + packetSize: 1024, + encoderArguments: [ + '-acodec', codec.ffmpegCodec, + '-ar', match.sampleRate, + '-ac', match.channels || '1', + ], + } + }); + + intercomClient.client.on('close', () => forwarder.kill()); + forwarder.killPromise.finally(() => intercomClient?.client.destroy()); + this.camera.console.log('intercom playing'); } diff --git a/plugins/reolink/README.md b/plugins/reolink/README.md index 74625da36d..e9a3bc8a71 100644 --- a/plugins/reolink/README.md +++ b/plugins/reolink/README.md @@ -1,5 +1,9 @@ # Reolink Plugin for Scrypted +Reolink cameras should use the ONVIF plugin. This plugin is for older Reolink cameras that do not have ONVIF support or the ONVIF implementation is buggy. + +Reolink doorbells MUST use the ONVIF plugin. This plugin does not support two way audio or the doorbell button event. + Reolink Cameras offer both RTMP and RTSP streams. RTMP streams are more reliable than RTSP on Reolink Cameras, but Scrypted highly recommends using RTSP streams if they are stable on your Reolink hardware. RTMP streams will be preferred by default. The defaults can be changed in the camera's Rebroadcast `Stream Management` settings. Reolink Two Way Audio is not supported. It is a proprietary and undocumented protocol. diff --git a/plugins/reolink/package-lock.json b/plugins/reolink/package-lock.json index 0a1c94ca10..255e75e5ab 100644 --- a/plugins/reolink/package-lock.json +++ b/plugins/reolink/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/reolink", - "version": "0.0.21", + "version": "0.0.22", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/reolink", - "version": "0.0.21", + "version": "0.0.22", "license": "Apache", "dependencies": { "@koush/axios-digest-auth": "^0.8.5", diff --git a/plugins/reolink/package.json b/plugins/reolink/package.json index 43f4e754b4..37fea53958 100644 --- a/plugins/reolink/package.json +++ b/plugins/reolink/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/reolink", - "version": "0.0.21", + "version": "0.0.22", "description": "Reolink Plugin for Scrypted", "author": "Scrypted", "license": "Apache", diff --git a/plugins/reolink/src/main.ts b/plugins/reolink/src/main.ts index b95ca6b5c6..5c2e5cc3be 100644 --- a/plugins/reolink/src/main.ts +++ b/plugins/reolink/src/main.ts @@ -51,9 +51,11 @@ class ReolinkCamera extends RtspSmartCamera implements Camera { (async () => { while (!killed) { try { + // const ai = await client.getAiState(); + // ret.emit('data', JSON.stringify(ai)); const { value, data } = await client.getMotionState(); this.motionDetected = value; - ret.emit('data', data); + ret.emit('data', JSON.stringify(data)); } catch (e) { ret.emit('error', e); @@ -116,14 +118,17 @@ class ReolinkCamera extends RtspSmartCamera implements Camera { } // rough guesses for rebroadcast stream selection. + ret[0].container = 'rtmp'; ret[0].video = { width: 2560, height: 1920, } + ret[1].container = 'rtmp'; ret[1].video = { width: 896, height: 672, } + ret[2].container = 'rtmp'; ret[2].video = { width: 640, height: 480, @@ -147,6 +152,28 @@ class ReolinkCamera extends RtspSmartCamera implements Camera { }); } + // rough guesses for h264 + ret[3].container = 'rtsp'; + ret[3].video = { + codec: 'h264', + width: 2560, + height: 1920, + } + ret[4].container = 'rtsp'; + ret[4].video = { + codec: 'h264', + width: 896, + height: 672, + } + + ret[5].container = 'rtsp'; + ret[5].video = { + codec: 'h265', + width: 896, + height: 672, + } + + return ret; } diff --git a/plugins/reolink/src/probe.ts b/plugins/reolink/src/probe.ts index 5614c3008a..1e252ccdc1 100644 --- a/plugins/reolink/src/probe.ts +++ b/plugins/reolink/src/probe.ts @@ -6,7 +6,7 @@ export const reolinkHttpsAgent = new https.Agent({ }); export async function getMotionState(digestAuth: AxiosDigestAuth, username: string, password: string, address: string, channelId: number) { - const url = new URL(`http://${address}/cgi-bin/api.cgi`); + const url = new URL(`http://${address}/api.cgi`); const params = url.searchParams; params.set('cmd', 'GetMdState'); params.set('channel', channelId.toString()); diff --git a/plugins/reolink/src/reolink-api.ts b/plugins/reolink/src/reolink-api.ts index 9b9a01f3c7..563c8b7b9c 100644 --- a/plugins/reolink/src/reolink-api.ts +++ b/plugins/reolink/src/reolink-api.ts @@ -26,6 +26,23 @@ export class ReolinkCameraClient { return getMotionState(this.digestAuth, this.username, this.password, this.host, this.channelId); } + async getAiState() { + const url = new URL(`http://${this.host}/api.cgi`); + const params = url.searchParams; + params.set('cmd', 'GetAiState'); + params.set('channel', this.channelId.toString()); + params.set('user', this.username); + params.set('password', this.password); + const response = await this.digestAuth.request({ + url: url.toString(), + httpsAgent: reolinkHttpsAgent, + }); + return { + value: !!response.data?.[0]?.value?.state, + data: response.data, + }; + } + async jpegSnapshot() { const url = new URL(`http://${this.host}/cgi-bin/api.cgi`); const params = url.searchParams; diff --git a/server/package-lock.json b/server/package-lock.json index 7294da3c52..ca0476b085 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/server", - "version": "0.7.57", + "version": "0.7.58", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@scrypted/server", - "version": "0.7.57", + "version": "0.7.58", "license": "ISC", "dependencies": { "@mapbox/node-pre-gyp": "^1.0.10",