Merge pull request #16 from hakanekin/fix/pw-record-raw-compat

Handle pw-record versions without --raw support
This commit is contained in:
Yusuf İpek
2026-08-05 18:42:58 +03:00
committed by GitHub
2 changed files with 67 additions and 1 deletions
+23 -1
View File
@@ -445,7 +445,7 @@ def _pulse_record(target):
return cmd return cmd
if shutil.which("pw-record"): if shutil.which("pw-record"):
cmd = [ cmd = [
"pw-record", "--raw", f"--rate={RATE}", "pw-record", *_pw_record_raw_option(), f"--rate={RATE}",
f"--channels={CHANNELS}", "--format=s16", f"--channels={CHANNELS}", "--format=s16",
] ]
if target: if target:
@@ -455,6 +455,28 @@ def _pulse_record(target):
return [] return []
def _pw_record_raw_option():
"""Use --raw only on pw-record releases that provide it.
PipeWire gained --raw in 1.4, and in the same release stopped treating a
filename of "-" as raw on its own: before it, the option is refused and the
recorder dies before any sound arrives; after it, leaving the option out
wraps the stream in a container the rest of this file would read as noise.
Ubuntu 24.04 and anything else still on 1.0 or 1.2 sit on the near side of
that line, so ask the installed binary which form it understands.
"""
try:
result = subprocess.run(
["pw-record", "--help"], capture_output=True, text=True, timeout=2
)
help_text = (result.stdout or "") + (result.stderr or "")
except (subprocess.SubprocessError, OSError):
return ["--raw"] # preserve the existing command when probing itself fails
if not help_text.strip():
return ["--raw"]
return ["--raw"] if "--raw" in help_text else []
def _pulse_meeting(mic_target, system_target): def _pulse_meeting(mic_target, system_target):
return [ return [
"ffmpeg", "-hide_banner", "-nostdin", "-loglevel", "error", "ffmpeg", "-hide_banner", "-nostdin", "-loglevel", "error",
+44
View File
@@ -238,6 +238,14 @@ class FakeProcess:
class RecordingCommand(OnLinux, DikteTest): class RecordingCommand(OnLinux, DikteTest):
"""Which program captures the microphone, and how it is asked to.""" """Which program captures the microphone, and how it is asked to."""
def setUp(self):
super().setUp()
# Whether pw-record takes --raw is read off the installed binary, and
# what is being tested here is the command rather than the machine the
# test is running on. PwRecordRawOption covers the reading itself.
self.enterContext(mock.patch.object(
audio, "_pw_record_raw_option", return_value=["--raw"]))
def test_parec_is_preferred(self): def test_parec_is_preferred(self):
"""It speaks to PulseAudio and to PipeWire's compatibility service, so """It speaks to PulseAudio and to PipeWire's compatibility service, so
it is the one that works on both desktops.""" it is the one that works on both desktops."""
@@ -288,9 +296,45 @@ class RecordingCommand(OnLinux, DikteTest):
if arg.startswith(flag)]) if arg.startswith(flag)])
class PwRecordRawOption(DikteTest):
"""Two pw-record generations want opposite commands for the same stream.
PipeWire 1.4 added --raw and stopped treating a filename of "-" as raw on
its own, so the option is refused by everything older and needed by
everything newer. The help text is the only thing that tells them apart.
"""
def option(self, **run):
with mock.patch.object(audio.subprocess, "run", **run):
return audio._pw_record_raw_option()
def test_a_version_that_offers_raw_is_asked_for_it(self):
self.assertEqual(["--raw"], self.option(
return_value=FakeCompleted(stdout=" -a, --raw RAW mode\n")))
def test_a_version_without_it_is_not(self):
self.assertEqual([], self.option(
return_value=FakeCompleted(stdout=" --rate Sample rate\n")))
def test_help_that_could_not_be_read_keeps_the_option(self):
"""Whatever is installed, the command that worked before this check
existed is the safer guess."""
self.assertEqual(["--raw"], self.option(side_effect=OSError))
self.assertEqual(["--raw"], self.option(
side_effect=subprocess.TimeoutExpired("pw-record", 2)))
def test_help_that_said_nothing_keeps_it_too(self):
self.assertEqual(["--raw"], self.option(return_value=FakeCompleted()))
class RecorderChain(OnLinux, DikteTest): class RecorderChain(OnLinux, DikteTest):
"""Start to WAV, with pw-record faked out.""" """Start to WAV, with pw-record faked out."""
def setUp(self):
super().setUp()
self.enterContext(mock.patch.object(
audio, "_pw_record_raw_option", return_value=["--raw"]))
def record(self, data, target="", max_seconds=300): def record(self, data, target="", max_seconds=300):
recorder = audio.Recorder() recorder = audio.Recorder()
results = [] results = []