From 4f1c75c60e726a3648fc8afe9544834fefa56281 Mon Sep 17 00:00:00 2001 From: Stefaan Vanderheyden Date: Mon, 23 Mar 2026 11:01:15 +0100 Subject: [PATCH] feat(doorbird): add continuous audio for live view and HKSV recordings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Doorbird does not include audio in its RTSP video stream. Audio is only available via the separate HTTP audio-receive.cgi endpoint. The plugin previously fed silence (anullsrc) into the audio track, only connecting to the real microphone during two-way intercom. Add a "Continuous Audio" setting (enabled by default) that keeps a persistent connection to the Doorbird microphone. This provides real audio for HomeKit live view and HKSV recordings without requiring the intercom to be active. The implementation reuses the existing startSilenceGenerator method, replacing the anullsrc input with audio-receive.cgi when continuous audio is enabled. FFmpeg reconnect flags and auto-restart on exit provide resilience against connection drops. The existing intercom flow is unchanged — the doorbellAudioActive guard ensures intercom audio takes priority when two-way talk is active. Fixes #934, fixes #996 --- plugins/doorbird/src/main.ts | 99 +++++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 29 deletions(-) diff --git a/plugins/doorbird/src/main.ts b/plugins/doorbird/src/main.ts index 246d39428e..b89987dc6a 100644 --- a/plugins/doorbird/src/main.ts +++ b/plugins/doorbird/src/main.ts @@ -179,6 +179,16 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid title: 'Speech Enhancement', value: this.storage.getItem('audioSpeechEnhancement') === 'true', description: 'Apply band filtering and dynamic normalization to both audio streams.', + }, + { + key: 'continuousAudio', + type: 'boolean', + subgroup: 'Advanced', + title: 'Continuous Audio', + value: (this.storage.getItem('continuousAudio') ?? 'true') === 'true', + description: 'Continuously stream audio from the Doorbird microphone. ' + + 'When disabled, audio is only available during two-way intercom. ' + + 'Enable this for HomeKit Secure Video recording audio and live view audio.', } ]; } @@ -476,35 +486,56 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid if (this.audioSilenceProcess) return; - this.console.log('Doorbird: starting audio silence generator...') - const ffmpegPath = await mediaManager.getFFmpegPath(); - const ffmpegArgs = [ - // Suppress printing the FFmpeg banner. - '-hide_banner', - // Disable periodic progress/statistics logging. - '-nostats', - // Read input at its native frame rate to ensure real-time processing. - '-re', - // Use the lavfi (libavfilter) virtual input device. - '-f', 'lavfi', - // Specify the input source as a null audio source (silence) with a sample rate of 8000 Hz and mono channel layout. - '-i', 'anullsrc=r=8000:cl=mono', - - // --- Low-latency Output Flags --- - // Bypass FFmpeg's internal I/O buffering, writing directly to the output pipe. - '-avioflags', 'direct', - // Force flushing packets to the output immediately after encoding. - '-flush_packets', '1', - // Set the maximum demux-decode delay to zero, preventing buffering in the muxer. - '-muxdelay', '0', - - // --- Output Format Specification --- - // Force the output container format to raw µ-law. - '-f', 'mulaw', - // Output the processed audio to file descriptor 3 (the pipe). - 'pipe:3' - ]; + let ffmpegArgs: string[]; + + if (this.getContinuousAudio()) { + // Connect to the Doorbird's microphone for continuous audio. + // The Doorbird does not include audio in its RTSP video stream — audio is only + // available via the separate HTTP audio-receive.cgi endpoint. Without this, + // HomeKit live view and HKSV recordings have no audio. + const audioRxUrl = `${this.getHttpBaseAddress()}/bha-api/audio-receive.cgi`; + this.console.log('Doorbird: starting continuous audio receiver...'); + ffmpegArgs = [ + '-hide_banner', + '-nostats', + '-fflags', '+flush_packets+nobuffer', + '-analyzeduration', '0', + '-probesize', '32', + // Auto-reconnect if the HTTP stream drops or the Doorbird reboots. + '-reconnect', '1', + '-reconnect_at_eof', '1', + '-reconnect_streamed', '1', + '-reconnect_delay_max', '5', + '-re', + '-ar', '8000', + '-ac', '1', + '-f', 'mulaw', + '-i', audioRxUrl, + '-flags', '+global_header+low_delay', + '-avioflags', 'direct', + '-flush_packets', '1', + '-muxdelay', '0', + '-acodec', 'copy', + '-f', 'mulaw', + 'pipe:3' + ]; + } else { + // Generate silence as a placeholder audio track. + this.console.log('Doorbird: starting audio silence generator...'); + ffmpegArgs = [ + '-hide_banner', + '-nostats', + '-re', + '-f', 'lavfi', + '-i', 'anullsrc=r=8000:cl=mono', + '-avioflags', 'direct', + '-flush_packets', '1', + '-muxdelay', '0', + '-f', 'mulaw', + 'pipe:3' + ]; + } safePrintFFmpegArguments(console, ffmpegArgs); const cp = child_process.spawn(ffmpegPath, ffmpegArgs, { @@ -514,8 +545,14 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid ffmpegLogInitialOutput(console, cp); cp.on('exit', () => { - this.console.log('Doorbird: audio silence generator ended.') this.audioSilenceProcess = undefined; + if (this.getContinuousAudio() && this.audioRXClientSocket) { + // Auto-restart if the connection drops and the socket is still active. + this.console.log('Doorbird: continuous audio receiver ended, restarting...'); + setTimeout(() => this.startSilenceGenerator(), 2000); + } else { + this.console.log('Doorbird: audio silence generator ended.'); + } }); cp.stdout.on('data', data => this.console.log(data.toString())); cp.stderr.on('data', data => this.console.log(data.toString())); @@ -620,6 +657,10 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid return this.storage.getItem('audioSpeechEnhancement') === 'true'; } + getContinuousAudio(): boolean { + return (this.storage.getItem('continuousAudio') ?? 'true') === 'true'; + } + private getAudioFilter() { const filters: string[] = []; if (this.getAudioDenoise()) {