aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-24 04:04:59 -0500
committerCraig Jennings <c@cjennings.net>2026-05-24 04:04:59 -0500
commit39795e850f2027a88021307a37de0381996df8a5 (patch)
tree7b0c6a011ca25de0e0a42303008db3c41aa8bb85
parent746e770ade412c8e7992b2a98441aebde000db3c (diff)
downloaddotemacs-39795e850f2027a88021307a37de0381996df8a5.tar.gz
dotemacs-39795e850f2027a88021307a37de0381996df8a5.zip
fix(recording): shell-quote device names and output paths in ffmpeg commands
The X11 video path and the audio path interpolated the mic device, system device, and output filename straight into the shell command, so a device name or recording directory with a space (or other shell metacharacter) would break the command or mishandle the path. The Wayland video branch already quoted these; the other two did not. I wrapped all three in shell-quote-argument on both paths. To make the audio command testable, I extracted it into cj/recording--build-audio-command mirroring the existing cj/recording--build-video-command, then quoted there. Tests cover device names and filenames with spaces on both the X11 and audio builders.
-rw-r--r--modules/video-audio-recording.el44
-rw-r--r--tests/test-video-audio-recording--build-audio-command.el76
-rw-r--r--tests/test-video-audio-recording--build-video-command.el16
3 files changed, 117 insertions, 19 deletions
diff --git a/modules/video-audio-recording.el b/modules/video-audio-recording.el
index 94f90099..6c00a2df 100644
--- a/modules/video-audio-recording.el
+++ b/modules/video-audio-recording.el
@@ -843,11 +843,32 @@ On X11: ffmpeg captures screen directly via x11grab with PulseAudio audio."
"-filter_complex \"[1:a]volume=%.1f[mic];[2:a]volume=%.1f[sys];[mic][sys]amerge=inputs=2[out]\" "
"-map 0:v -map \"[out]\" "
"%s")
- mic-device
- system-device
+ (shell-quote-argument mic-device)
+ (shell-quote-argument system-device)
cj/recording-mic-boost
cj/recording-system-volume
- filename)))
+ (shell-quote-argument filename))))
+
+(defun cj/recording--build-audio-command (mic-device system-device filename)
+ "Build the ffmpeg shell command string for audio-only recording.
+MIC-DEVICE and SYSTEM-DEVICE are PulseAudio device names. FILENAME is
+the output .m4a path. Mixes mic + system monitor into a single AAC file."
+ (format (concat "ffmpeg "
+ "-f pulse -i %s " ; Input 0: microphone
+ "-f pulse -i %s " ; Input 1: system audio monitor
+ "-filter_complex \""
+ "[0:a]volume=%.1f[mic];"
+ "[1:a]volume=%.1f[sys];"
+ "[mic][sys]amix=inputs=2:duration=longest[out]\" "
+ "-map \"[out]\" "
+ "-c:a aac "
+ "-b:a 64k "
+ "%s")
+ (shell-quote-argument mic-device)
+ (shell-quote-argument system-device)
+ cj/recording-mic-boost
+ cj/recording-system-volume
+ (shell-quote-argument filename)))
(defun cj/ffmpeg-record-video (directory)
"Start a video recording, saving output to DIRECTORY.
@@ -898,22 +919,7 @@ The filter graph mixes two PulseAudio inputs:
(name (format-time-string "%Y-%m-%d-%H-%M-%S"))
(filename (expand-file-name (concat name ".m4a") location))
(ffmpeg-command
- (format (concat "ffmpeg "
- "-f pulse -i %s " ; Input 0: microphone
- "-f pulse -i %s " ; Input 1: system audio monitor
- "-filter_complex \""
- "[0:a]volume=%.1f[mic];"
- "[1:a]volume=%.1f[sys];"
- "[mic][sys]amix=inputs=2:duration=longest[out]\" "
- "-map \"[out]\" "
- "-c:a aac "
- "-b:a 64k "
- "%s")
- mic-device
- system-device
- cj/recording-mic-boost
- cj/recording-system-volume
- filename)))
+ (cj/recording--build-audio-command mic-device system-device filename)))
(message "Recording from mic: %s + ALL system outputs" mic-device)
(cj/log-silently "Audio recording ffmpeg command: %s" ffmpeg-command)
(setq cj/audio-recording-ffmpeg-process
diff --git a/tests/test-video-audio-recording--build-audio-command.el b/tests/test-video-audio-recording--build-audio-command.el
new file mode 100644
index 00000000..54e5f56c
--- /dev/null
+++ b/tests/test-video-audio-recording--build-audio-command.el
@@ -0,0 +1,76 @@
+;;; test-video-audio-recording--build-audio-command.el --- Tests for audio command builder -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for cj/recording--build-audio-command.
+;; Verifies correct ffmpeg command construction for audio-only recording
+;; (mic + system monitor mixed to M4A/AAC), including shell quoting of
+;; device names and output paths.
+
+;;; Code:
+
+(require 'ert)
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+(require 'video-audio-recording)
+
+;;; Normal Cases
+
+(ert-deftest test-video-audio-recording--build-audio-command-normal-uses-ffmpeg-pulse ()
+ "Normal: audio command uses ffmpeg with two PulseAudio inputs mixed to AAC."
+ (let ((cj/recording-mic-boost 2.0)
+ (cj/recording-system-volume 1.0))
+ (let ((cmd (cj/recording--build-audio-command "mic" "sys" "/tmp/out.m4a")))
+ (should (string-match-p "ffmpeg" cmd))
+ (should (string-match-p "-f pulse -i" cmd))
+ (should (string-match-p "amix=inputs=2" cmd))
+ (should (string-match-p "-c:a aac" cmd)))))
+
+(ert-deftest test-video-audio-recording--build-audio-command-normal-devices-in-command ()
+ "Normal: both mic and system device names appear in the command."
+ (let ((cj/recording-mic-boost 1.0)
+ (cj/recording-system-volume 1.0))
+ (let ((cmd (cj/recording--build-audio-command
+ "alsa_input.usb-Jabra-00.mono"
+ "alsa_output.usb-JDS-00.monitor"
+ "/tmp/out.m4a")))
+ (should (string-match-p "alsa_input.usb-Jabra-00.mono" cmd))
+ (should (string-match-p "alsa_output.usb-JDS-00.monitor" cmd)))))
+
+(ert-deftest test-video-audio-recording--build-audio-command-normal-volume-in-filter ()
+ "Normal: volume settings appear in the filter_complex expression."
+ (let ((cj/recording-mic-boost 1.5)
+ (cj/recording-system-volume 0.7))
+ (let ((cmd (cj/recording--build-audio-command "mic" "sys" "/tmp/out.m4a")))
+ (should (string-match-p "volume=1\\.5" cmd))
+ (should (string-match-p "volume=0\\.7" cmd)))))
+
+;;; Boundary Cases
+
+(ert-deftest test-video-audio-recording--build-audio-command-boundary-device-quoted ()
+ "Boundary: device names with spaces are shell-quoted."
+ (let ((cj/recording-mic-boost 1.0)
+ (cj/recording-system-volume 1.0))
+ (let ((cmd (cj/recording--build-audio-command
+ "device with spaces" "sys" "/tmp/out.m4a")))
+ (should (string-match-p "device\\\\ with\\\\ spaces" cmd)))))
+
+(ert-deftest test-video-audio-recording--build-audio-command-boundary-filename-quoted ()
+ "Boundary: output filename with spaces is shell-quoted."
+ (let ((cj/recording-mic-boost 1.0)
+ (cj/recording-system-volume 1.0))
+ (let ((cmd (cj/recording--build-audio-command
+ "mic" "sys" "/tmp/my recording.m4a")))
+ (should (string-match-p "my\\\\ recording\\.m4a" cmd)))))
+
+(ert-deftest test-video-audio-recording--build-audio-command-boundary-zero-volume ()
+ "Boundary: zero volume values produce 0.0 in the command."
+ (let ((cj/recording-mic-boost 0.0)
+ (cj/recording-system-volume 0.0))
+ (let ((cmd (cj/recording--build-audio-command "mic" "sys" "/tmp/out.m4a")))
+ (should (string-match-p "volume=0\\.0" cmd)))))
+
+(provide 'test-video-audio-recording--build-audio-command)
+;;; test-video-audio-recording--build-audio-command.el ends here
diff --git a/tests/test-video-audio-recording--build-video-command.el b/tests/test-video-audio-recording--build-video-command.el
index 7f54f053..3b79c9ec 100644
--- a/tests/test-video-audio-recording--build-video-command.el
+++ b/tests/test-video-audio-recording--build-video-command.el
@@ -76,6 +76,22 @@
;; Filename should be quoted/escaped
(should (string-match-p "recording" cmd))))))
+(ert-deftest test-video-audio-recording--build-video-command-boundary-x11-device-quoted ()
+ "Boundary: X11 device names with spaces are shell-quoted."
+ (let ((cj/recording-mic-boost 1.0)
+ (cj/recording-system-volume 1.0))
+ (let ((cmd (cj/recording--build-video-command
+ "device with spaces" "sys" "/tmp/out.mkv" nil)))
+ (should (string-match-p "device\\\\ with\\\\ spaces" cmd)))))
+
+(ert-deftest test-video-audio-recording--build-video-command-boundary-x11-filename-quoted ()
+ "Boundary: X11 output filename with spaces is shell-quoted."
+ (let ((cj/recording-mic-boost 1.0)
+ (cj/recording-system-volume 1.0))
+ (let ((cmd (cj/recording--build-video-command
+ "mic" "sys" "/tmp/my recording.mkv" nil)))
+ (should (string-match-p "my\\\\ recording\\.mkv" cmd)))))
+
(ert-deftest test-video-audio-recording--build-video-command-boundary-zero-volume ()
"Zero volume values produce 0.0 in the command."
(let ((cj/recording-mic-boost 0.0)