feat: decide the classroom's audio output from what is plugged in

The room that prompted this had a television on HDMI with working PCM
audio and nothing in the analog jack, and WirePlumber chose analog. A
teacher who corrected it by hand then had to restart the browser before
any sound came out.

The policy is a pure function so it can be tested against that room
rather than against a machine: an unavailable port is not a candidate,
an analog port merely existing is not evidence of a cable, and an
explicit AUDIO_OUTPUT overrides the lot. Twelve cases, the real room
among them.

The daemon follows PipeWire's own event stream rather than polling, and
moves streams that are already playing, which is the part that stops a
teacher having to close and reopen the browser.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
ekklesia
2026-09-10 08:47:28 -05:00
co-authored by Claude Opus 5
parent 87b55761c6
commit 2ff9d47be9
27 changed files with 626 additions and 40 deletions
+8 -3
View File
@@ -32,7 +32,12 @@ action/help_report_bug=false
action/help_about_kde=false
action/help_contents=false
[General][$i]
# Not immutable. Marking this group [$i] froze the colour scheme so hard
# that `classroom-theme light` changed the running session and then lost
# the change at the next start — measured. These are defaults for a room
# that has no settings application anyway; the lock belongs on the
# restrictions above, not on the palette.
[General]
ColorScheme=EkklesiaDark
AccentColor=79,179,168
# The teacher's own words are not decoration: keep text rendering plain
@@ -41,13 +46,13 @@ XftAntialias=true
XftHintStyle=hintslight
XftSubPixel=rgb
[KDE][$i]
[KDE]
widgetStyle=Breeze
LookAndFeelPackage=org.ekklesia.classroom.desktop
SingleClick=false
AnimationDurationFactor=0.5
[Icons][$i]
[Icons]
Theme=breeze-dark
[KFileDialog Settings][$i]
+21 -37
View File
@@ -1,48 +1,32 @@
# Window rules.
#
# There are three, they are all about size, and none of them is about
# stacking. Dialogs staying in front of the window that opened them,
# secondary windows floating over the main one instead of resizing it, and
# a print dialog being the window in front are all ordinary window-manager
# behaviour here, so there is nothing to write down.
# There is one, and it is about video.
#
# `types=1` is NET::Normal only. Without it a rule that maximises would
# also maximise the dialogs, which is exactly the behaviour the classroom
# is moving away from.
# Everything else the classroom used to arrange by hand is ordinary window
# manager behaviour and needs no rule: a dialog stays in front of the window
# that opened it, a window a page opens floats over the browser instead of
# resizing it, and a print dialog is the window in front.
#
# There is deliberately no rule that maximises the browser. A rule can only
# match a window class, and Chrome gives its main window and every window a
# page opens the same one — matching on it maximised the lesson's popup to
# the whole display, which is the behaviour this architecture exists to
# stop. Chrome can tell them apart, so `--start-maximized` in the launcher
# does it: the first window fills the screen, later ones keep the size the
# page asked for. Arranging that from outside the browser is what the old
# 314-line window arbiter was doing.
[General]
count=3
rules=classroom-browser,classroom-wonderink,classroom-video
[classroom-browser]
Description=The classroom browser fills the screen
wmclass=google-chrome
wmclassmatch=1
wmclasscomplete=false
types=1
maximizehoriz=true
maximizehorizrule=3
maximizevert=true
maximizevertrule=3
[classroom-wonderink]
Description=Wonder Ink fills the screen
# Chrome derives this from the --app URL. Measured on a running session
# rather than guessed; it is also what the launcher's StartupWMClass says,
# and the two have to agree or the panel shows two buttons for one app.
wmclass=chrome-app.wonderink.org__-Default
wmclassmatch=1
wmclasscomplete=false
types=1
maximizehoriz=true
maximizehorizrule=3
maximizevert=true
maximizevertrule=3
count=1
rules=classroom-video
[classroom-video]
Description=Video is an appliance screen, not a window
wmclass=vlc
wmclassmatch=2
# VLC reports its class as "Vlc", and KWin's matching is case sensitive —
# measured on a running session, where a lower-case substring rule matched
# nothing at all and said so nowhere.
wmclass=(?i)vlc
wmclassmatch=3
wmclasscomplete=false
noborder=true
noborderrule=2