Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(native): add SRT live streaming to OBS/TikTok over WiFi

Adds an opt-in ac-native-stream systemd service that captures the DRM
framebuffer (kmsgrab) + ALSA audio (via snd_aloop tee) and pushes an
SRT stream to OBS on a Windows host for TikTok Live broadcasting.

- ffmpeg-full with VAAPI hw encode for low-latency h264
- asound.conf tee routes audio to both speakers + loopback
- AC_AUDIO_DEVICE env var in audio.c for clean ALSA routing
- Controlled via /mnt/stream.conf on USB config partition

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+155
+18
fedac/native/src/audio.c
··· 1053 1053 1054 1054 // Open ALSA — try multiple cards and devices, with retries for race conditions. 1055 1055 // On fast NVMe boots the HDA codec may not be fully probed when we first try. 1056 + // If AC_AUDIO_DEVICE is set, try it first (used for stream tee via asound.conf). 1056 1057 snd_pcm_t *pcm = NULL; 1057 1058 const char *devices[] = { 1058 1059 "hw:0,0", "hw:1,0", "hw:0,1", "hw:1,1", ··· 1062 1063 }; 1063 1064 int err = -1; 1064 1065 int card_idx = 0; 1066 + 1067 + // AC_AUDIO_DEVICE override — try the env var device before the hardcoded list. 1068 + const char *env_dev = getenv("AC_AUDIO_DEVICE"); 1069 + if (env_dev && env_dev[0]) { 1070 + err = snd_pcm_open(&pcm, env_dev, SND_PCM_STREAM_PLAYBACK, 0); 1071 + if (err >= 0) { 1072 + fprintf(stderr, "[audio] Opened AC_AUDIO_DEVICE=%s\n", env_dev); 1073 + snprintf(audio->audio_device, sizeof(audio->audio_device), "%s", env_dev); 1074 + if (sscanf(env_dev, "hw:%d", &card_idx) != 1 && 1075 + sscanf(env_dev, "plughw:%d", &card_idx) != 1) 1076 + card_idx = 0; 1077 + } else { 1078 + fprintf(stderr, "[audio] AC_AUDIO_DEVICE=%s failed: %s — falling back\n", 1079 + env_dev, snd_strerror(err)); 1080 + } 1081 + } 1082 + 1065 1083 for (int attempt = 0; attempt < 5 && err < 0; attempt++) { 1066 1084 if (attempt > 0) { 1067 1085 fprintf(alog, "[audio] Retry %d/4 — waiting 2s for codec probe...\n", attempt);
+133
fedac/nixos/configuration.nix
··· 69 69 dosfstools # mkfs.vfat 70 70 efibootmgr 71 71 parted 72 + ffmpeg-full # SRT streaming + VAAPI hardware encode 73 + libva-utils # vainfo for debugging GPU encode 72 74 ]; 73 75 74 76 # zram swap ··· 130 132 echo "No ACDATA partition found; /mnt is temporary" 131 133 ${pkgs.util-linux}/bin/lsblk -o NAME,SIZE,TYPE,FSTYPE,LABEL,MOUNTPOINTS || true 132 134 ${pkgs.util-linux}/bin/blkid || true 135 + ''; 136 + }; 137 + }; 138 + 139 + # ALSA tee: "default" sends audio to both physical speakers and the loopback 140 + # device. The stream service captures from the loopback's read side. 141 + # snd_aloop is always loaded (see hardware.nix kernelModules). 142 + environment.etc."asound.conf".text = '' 143 + # AC Native audio tee — duplicate playback to speakers + ALSA loopback. 144 + # Loopback capture side (hw:Loopback,1,0) is read by the stream service. 145 + 146 + pcm.!default { 147 + type plug 148 + slave.pcm "ac_tee" 149 + } 150 + 151 + pcm.ac_tee { 152 + type route 153 + slave.pcm { 154 + type multi 155 + slaves { 156 + a { pcm "plughw:0,0" channels 2 } 157 + b { pcm "plughw:Loopback,0,0" channels 2 } 158 + } 159 + bindings { 160 + 0 { slave a channel 0 } 161 + 1 { slave a channel 1 } 162 + 2 { slave b channel 0 } 163 + 3 { slave b channel 1 } 164 + } 165 + } 166 + ttable.0.0 1 167 + ttable.1.1 1 168 + ttable.0.2 1 169 + ttable.1.3 1 170 + } 171 + 172 + ctl.!default { 173 + type hw 174 + card 0 175 + } 176 + ''; 177 + 178 + # SRT streamer: pushes notepat framebuffer over WiFi to OBS on Windows, 179 + # which then forwards to TikTok Live. 180 + # 181 + # Opt-in: only starts if /mnt/stream.conf exists on the USB config partition. 182 + # Create /mnt/stream.conf with: 183 + # HOST=192.168.1.100 # Windows host IP on LAN 184 + # PORT=9000 # SRT port (OBS Media Source listens here) 185 + # BITRATE=6M # encode bitrate 186 + # FRAMERATE=60 # capture fps 187 + # SRT_LATENCY=40 # ms — lower = less latency, more fragile on bad WiFi 188 + # WIDTH=1280 # output width (scale from native) 189 + # HEIGHT=720 # output height 190 + systemd.services.ac-native-stream = { 191 + description = "AC Native SRT streamer (to OBS → TikTok)"; 192 + after = [ "ac-native-kiosk.service" "mount-usb-config.service" ]; 193 + wants = [ "ac-native-kiosk.service" ]; 194 + wantedBy = [ "multi-user.target" ]; 195 + 196 + unitConfig = { 197 + ConditionPathExists = "/mnt/stream.conf"; 198 + }; 199 + 200 + environment = { 201 + LIBVA_DRIVER_NAME = "iHD"; 202 + ALSA_PLUGIN_DIR = "${pkgs.alsa-plugins}/lib/alsa-lib"; 203 + ALSA_CONFIG_PATH = "${pkgs.alsa-lib}/share/alsa/alsa.conf"; 204 + }; 205 + 206 + serviceConfig = { 207 + Type = "simple"; 208 + Restart = "on-failure"; 209 + RestartSec = 3; 210 + EnvironmentFile = "/mnt/stream.conf"; 211 + 212 + ExecStartPre = pkgs.writeShellScript "ac-native-stream-wait" '' 213 + # Give the kiosk a moment to grab DRM master and start rendering 214 + sleep 2 215 + 216 + for _ in $(seq 1 40); do 217 + ls /dev/dri/card* >/dev/null 2>&1 && break 218 + sleep 0.25 219 + done 220 + 221 + if ! ls /dev/dri/card* >/dev/null 2>&1; then 222 + echo "[stream] no DRM card found" 223 + exit 1 224 + fi 225 + ''; 226 + 227 + ExecStart = pkgs.writeShellScript "ac-native-stream" '' 228 + set -u 229 + 230 + HOST="''${HOST:?HOST not set in /mnt/stream.conf}" 231 + PORT="''${PORT:-9000}" 232 + BITRATE="''${BITRATE:-6M}" 233 + FRAMERATE="''${FRAMERATE:-60}" 234 + SRT_LATENCY="''${SRT_LATENCY:-40}" 235 + WIDTH="''${WIDTH:-1280}" 236 + HEIGHT="''${HEIGHT:-720}" 237 + 238 + CARD="$(ls /dev/dri/card* 2>/dev/null | head -n1)" 239 + [ -n "$CARD" ] || { echo "[stream] no DRM card"; exit 1; } 240 + 241 + URL="srt://''${HOST}:''${PORT}?mode=caller&latency=''${SRT_LATENCY}&pkt_size=1316&tlpktdrop=1" 242 + echo "[stream] capturing $CARD at ''${FRAMERATE}fps → $URL" 243 + echo "[stream] encode: h264_vaapi ''${WIDTH}x''${HEIGHT} @ ''${BITRATE}" 244 + 245 + AUDIO_BITRATE="''${AUDIO_BITRATE:-128k}" 246 + 247 + exec ${pkgs.ffmpeg-full}/bin/ffmpeg \ 248 + -hide_banner -loglevel warning \ 249 + -threads 2 \ 250 + -fflags nobuffer -flags low_delay \ 251 + -device "$CARD" \ 252 + -framerate "$FRAMERATE" \ 253 + -f kmsgrab -i - \ 254 + -f alsa -thread_queue_size 512 -channels 2 \ 255 + -sample_rate 48000 -audio_buffer_size 10000 \ 256 + -i hw:Loopback,1,0 \ 257 + -vf "hwmap=derive_device=vaapi,scale_vaapi=w=''${WIDTH}:h=''${HEIGHT}:format=nv12" \ 258 + -c:v h264_vaapi \ 259 + -b:v "$BITRATE" -maxrate "$BITRATE" -bufsize "$BITRATE" \ 260 + -g "$FRAMERATE" -bf 0 \ 261 + -quality 1 \ 262 + -compression_level 1 \ 263 + -sei 0 \ 264 + -c:a aac -b:a "$AUDIO_BITRATE" \ 265 + -f mpegts "$URL" 133 266 ''; 134 267 }; 135 268 };
+1
fedac/nixos/modules/hardware.nix
··· 37 37 # Audio 38 38 "snd_hda_intel" "snd_hda_codec_realtek" "snd_hda_codec_hdmi" 39 39 "snd_usb_audio" 40 + "snd_aloop" # ALSA loopback — lets ac-native-stream tee audio to ffmpeg 40 41 # Bluetooth 41 42 "btusb" "btintel" 42 43 ];
+3
fedac/nixos/modules/kiosk.nix
··· 151 151 SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; 152 152 ALSA_PLUGIN_DIR = "${pkgs.alsa-plugins}/lib/alsa-lib"; 153 153 ALSA_CONFIG_PATH = "${pkgs.alsa-lib}/share/alsa/alsa.conf"; 154 + # Route audio through asound.conf tee (speakers + loopback for streaming). 155 + # Falls back to hw:0,0 automatically if the tee device fails. 156 + AC_AUDIO_DEVICE = "default"; 154 157 }; 155 158 156 159 serviceConfig = {