Let the Device field be picked, not typed blind
Diagnosing capture on a real TV meant reading pactlSources off the screen and typing an exact PulseAudio source name back in through the same remote-driven text field — no way to copy-paste, easy to mistype, and the one piece of information (which source, if any, is actually RUNNING) was buried in a JSON dump. Added two choice() pickers bound to the same capture.device setting: one built from pactlSources (pulse/auto backends), one built from alsaCapturePcms (alsa backend), both parsed from diagnostics the service already collects — no new Luna method needed. Diagnostics already run once at boot, so the picker is populated immediately, before the user ever presses "Run diagnostics" by hand. Picking a value writes straight into capture.device, and the plain text field stays as the fallback for anything the parser misses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f3a4cddfd6
commit
0759cc00aa
@@ -64,6 +64,21 @@ echo "== Capture pipeline end to end"
|
||||
"$CC" "${CFLAGS[@]}" -o "$OUT/engine_smoke" test/engine_smoke.c "${SOURCES[@]}" -lpthread -lm
|
||||
"$OUT/engine_smoke"
|
||||
|
||||
echo
|
||||
echo "== Unraid plugin"
|
||||
python3 test/verify_unraid_plugin.py
|
||||
|
||||
echo
|
||||
echo "== Receiver container"
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
docker build -f docker/Dockerfile -t lgtv-audiocap-receiver:test-run . >/dev/null 2>&1
|
||||
docker run --rm lgtv-audiocap-receiver:test-run --help >/dev/null
|
||||
echo " ok image builds and forwards --help"
|
||||
docker rmi lgtv-audiocap-receiver:test-run >/dev/null 2>&1
|
||||
else
|
||||
echo " SKIP: docker is not installed"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "== Frontend"
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
|
||||
@@ -191,6 +191,10 @@ async function main() {
|
||||
eq('stops again', $('state-pill').textContent, 'Stopped');
|
||||
|
||||
console.log('diagnostics');
|
||||
// Diagnostics run once automatically at boot, so the picker is already
|
||||
// there — the user should not have to press the button first.
|
||||
check('device picker already present from the boot-time diagnostics run',
|
||||
!!doc.querySelector('[data-path="capture.device"].choice'));
|
||||
click($('run-diagnostics'));
|
||||
await wait(200);
|
||||
check('diagnostics output shown',
|
||||
@@ -200,6 +204,17 @@ async function main() {
|
||||
await wait(200);
|
||||
check('log output shown', $('output').textContent.indexOf('browser mock') >= 0);
|
||||
|
||||
console.log('device picker');
|
||||
const picker = doc.querySelector('[data-path="capture.device"].choice');
|
||||
check('device picker appears once sources are known', !!picker);
|
||||
eq('picker starts on Automatic', picker.textContent, 'Automatic (@DEFAULT_MONITOR@)');
|
||||
click(picker); // Automatic -> tpcm_output.monitor
|
||||
await wait(600);
|
||||
eq('picking a source reaches settings',
|
||||
window.App.state.settings.capture.device, 'tpcm_output.monitor');
|
||||
eq('the plain device field reflects the pick',
|
||||
doc.querySelector('input[data-path="capture.device"]').value, 'tpcm_output.monitor');
|
||||
|
||||
console.log('navigation');
|
||||
fakeLayout(window);
|
||||
const tabs = doc.querySelectorAll('.tab');
|
||||
|
||||
Executable
+108
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Checks unraid/lgtv-audiocap-loopback.plg without needing an Unraid box.
|
||||
|
||||
Verifies the plugin is well-formed XML (a CDATA-free bash script anywhere in
|
||||
it means a stray "&" or "<" one edit away from breaking the DOCTYPE entity
|
||||
expansion Unraid's installer relies on), that entities substitute the way
|
||||
Unraid's installer would substitute them, that both embedded scripts are
|
||||
syntactically valid bash, and that the install/remove pair is idempotent and
|
||||
symmetric against a scratch go-file.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import os
|
||||
import xml.dom.minidom as minidom
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
PLG = os.path.join(HERE, os.pardir, "unraid", "lgtv-audiocap-loopback.plg")
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
|
||||
def check(condition, description):
|
||||
global passed, failed
|
||||
if condition:
|
||||
print(" ok %s" % description)
|
||||
passed += 1
|
||||
else:
|
||||
print(" FAIL %s" % description)
|
||||
failed += 1
|
||||
|
||||
|
||||
def bash_syntax_ok(script):
|
||||
result = subprocess.run(["bash", "-n"], input=script, text=True,
|
||||
capture_output=True)
|
||||
return result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
def main():
|
||||
doc = minidom.parse(PLG)
|
||||
|
||||
plugin = doc.getElementsByTagName("PLUGIN")
|
||||
check(len(plugin) == 1, "exactly one PLUGIN element")
|
||||
attrs = dict(plugin[0].attributes.items()) if plugin else {}
|
||||
for key in ("name", "author", "version", "pluginURL", "min"):
|
||||
check(bool(attrs.get(key)), "PLUGIN has a non-empty %s attribute" % key)
|
||||
check(attrs.get("name") == "lgtv-audiocap-loopback", "name matches the filename's stem")
|
||||
check(attrs.get("pluginURL", "").endswith(attrs.get("name", "\0") + ".plg"),
|
||||
"pluginURL points at this same file's name")
|
||||
|
||||
files = doc.getElementsByTagName("FILE")
|
||||
check(len(files) == 2, "exactly two FILE blocks (install + remove)")
|
||||
|
||||
install_script = remove_script = None
|
||||
for f in files:
|
||||
inline = f.getElementsByTagName("INLINE")
|
||||
check(len(inline) == 1, "FILE (Method=%s) has one INLINE child" % (f.getAttribute("Method") or "install"))
|
||||
script = inline[0].firstChild.data if inline and inline[0].firstChild else ""
|
||||
ok, stderr = bash_syntax_ok(script)
|
||||
check(ok, "FILE (Method=%s) script is valid bash%s" % (
|
||||
f.getAttribute("Method") or "install", "" if ok else ": " + stderr.strip()))
|
||||
if f.getAttribute("Method") == "remove":
|
||||
remove_script = script
|
||||
else:
|
||||
install_script = script
|
||||
|
||||
check(install_script is not None, "found the install script")
|
||||
check(remove_script is not None, "found the remove script")
|
||||
check("lgtv-audiocap-loopback" in (install_script or ""),
|
||||
"&name; entity actually expanded inside the install script (not left literal)")
|
||||
|
||||
# The plugin appends to /boot/config/go; redirect that at a scratch file
|
||||
# to exercise the real install/remove logic end to end, not just parse it.
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
go = os.path.join(tmp, "go")
|
||||
with open(go, "w") as fh:
|
||||
fh.write("#!/bin/bash\n/usr/local/sbin/emhttp\n")
|
||||
original = open(go).read()
|
||||
|
||||
# modprobe isn't run for real here; the script already tolerates that
|
||||
# (it warns and continues), so there's nothing to stub out beyond
|
||||
# keeping its stderr out of /tmp.
|
||||
env_script = install_script.replace("GO=/boot/config/go", "GO=%s" % go)
|
||||
env_script = env_script.replace("/tmp/${NAME}.err", os.path.join(tmp, "err"))
|
||||
subprocess.run(["bash", "-c", env_script], check=True)
|
||||
after_install = open(go).read()
|
||||
check(after_install != original, "install actually appended something to go")
|
||||
check("modprobe snd-aloop" in after_install, "the modprobe line ended up in go")
|
||||
|
||||
subprocess.run(["bash", "-c", env_script], check=True)
|
||||
after_second_install = open(go).read()
|
||||
check(after_second_install == after_install, "installing twice does not duplicate the block")
|
||||
|
||||
env_remove = remove_script.replace("GO=/boot/config/go", "GO=%s" % go)
|
||||
env_remove = env_remove.replace("/sbin/rmmod snd_aloop 2>/dev/null || true", "true")
|
||||
subprocess.run(["bash", "-c", env_remove], check=True)
|
||||
after_remove = open(go).read()
|
||||
check(after_remove == original, "remove restores go to its original contents exactly")
|
||||
|
||||
print()
|
||||
print("%d/%d checks passed" % (passed, passed + failed))
|
||||
return 0 if failed == 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user