[GH-ISSUE #465] POST /api/v1/apps/add drops start_audio_server: apps added through the API get no audio #285

Closed
opened 2026-10-15 12:59:20 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @WMP on GitHub (Jul 25, 2026).
Original GitHub issue: https://github.com/games-on-whales/wolf/issues/465

Summary

POST /api/v1/apps/add silently discards start_audio_server. The field is
required by the endpoint's own schema, but it never reaches the stored App:
reading the app back always reports false, and that false is what gets
persisted to config.toml. Apps created through the API therefore run without
an audio server — the virtual compositor works, but there is no sound.

Apps defined directly in config.toml are unaffected (that path reads the
field correctly, defaulting to true when absent), so this only bites anything
that manages the app list through the API.

Reproduce

Send POST /api/v1/apps/add over the API socket (WOLF_SOCKET_PATH, in my case
/tmp/sockets/wolf.sock) with a payload copied from an existing app, changing
only title, id and start_audio_server: true. Wolf answers
{"success":true}.

Then GET /api/v1/apps reports the new app as:

{
  "title": "audio-probe",
  "start_virtual_compositor": true,
  "start_audio_server": false
}

start_audio_server was true in the request, and config.toml gets
start_audio_server = false for that app too. start_virtual_compositor in the
very same request survives, which is what makes this look like a field-mapping
slip rather than a validation problem.

What it looks like from the user side

Launching such an app: the session starts, the Wayland socket appears, video
streams fine. Inside the app PulseAudio has only auto_null, no
virtual_sink_<session_id>, so every stream lands in the null sink:

$ pactl list short sinks
0  auto_null  module-null-sink.c  s16le 2ch 48000Hz  RUNNING

$ pactl list sink-inputs | grep -E 'Sink:|application.name'
        Sink: 0
                application.name = "The Witcher 3"

Moonlight gets silence. Flipping start_audio_server = true by hand in
config.toml and restarting Wolf fixes it immediately, which confirms only the
API path is affected.

Root cause

Reflector<events::App>::to() in src/moonlight-server/events/reflectors.hpp
builds the events::App without start_audio_server, so the aggregate
initialisation leaves it false:

  static events::App to(const ReflType &app, const std::shared_ptr<events::EventBusType> &ev_bus) {
    auto runner = Reflector<events::Runner>::to(app.runner, ev_bus);
    return events::App{
        .base = {...},
        ...
        .start_virtual_compositor = app.start_virtual_compositor,
        .runner = runner,                    // <- .start_audio_server never assigned
    };
  }

The opposite direction is fine — from() (same file) copies
start_audio_server, which is why the value round-trips correctly for apps that
came from config.toml, and why the API reports the wrong value rather than
dropping the key. configTOML.cpp handles it in both directions too
(.start_audio_server = app.start_audio_server.value_or(true) on read,
serialised back on write).

Fix

One line in to():

        .start_virtual_compositor = app.start_virtual_compositor,
        .start_audio_server = app.start_audio_server,
        .runner = runner,

Happy to open a PR with that.

Environment

  • ghcr.io/games-on-whales/wolf:stable, sha256:ff82c125c9b79b2e9443de2b0eaec40c904edb03291680d408cccd57c1d59c76 (stable @ d6d41de)
  • GStreamer 1.26.7-0, NVENC, Talos Linux + NVIDIA GPU Operator
  • Wolf runs on Kubernetes; the app list is reconciled through the API by a sidecar, which is how this surfaced
Originally created by @WMP on GitHub (Jul 25, 2026). Original GitHub issue: https://github.com/games-on-whales/wolf/issues/465 ### Summary `POST /api/v1/apps/add` silently discards `start_audio_server`. The field is required by the endpoint's own schema, but it never reaches the stored `App`: reading the app back always reports `false`, and that `false` is what gets persisted to `config.toml`. Apps created through the API therefore run without an audio server — the virtual compositor works, but there is no sound. Apps defined directly in `config.toml` are unaffected (that path reads the field correctly, defaulting to `true` when absent), so this only bites anything that manages the app list through the API. ### Reproduce Send `POST /api/v1/apps/add` over the API socket (`WOLF_SOCKET_PATH`, in my case `/tmp/sockets/wolf.sock`) with a payload copied from an existing app, changing only `title`, `id` and `start_audio_server: true`. Wolf answers `{"success":true}`. Then `GET /api/v1/apps` reports the new app as: ```json { "title": "audio-probe", "start_virtual_compositor": true, "start_audio_server": false } ``` `start_audio_server` was `true` in the request, and `config.toml` gets `start_audio_server = false` for that app too. `start_virtual_compositor` in the very same request survives, which is what makes this look like a field-mapping slip rather than a validation problem. ### What it looks like from the user side Launching such an app: the session starts, the Wayland socket appears, video streams fine. Inside the app PulseAudio has only `auto_null`, no `virtual_sink_<session_id>`, so every stream lands in the null sink: ```console $ pactl list short sinks 0 auto_null module-null-sink.c s16le 2ch 48000Hz RUNNING $ pactl list sink-inputs | grep -E 'Sink:|application.name' Sink: 0 application.name = "The Witcher 3" ``` Moonlight gets silence. Flipping `start_audio_server = true` by hand in `config.toml` and restarting Wolf fixes it immediately, which confirms only the API path is affected. ### Root cause `Reflector<events::App>::to()` in `src/moonlight-server/events/reflectors.hpp` builds the `events::App` without `start_audio_server`, so the aggregate initialisation leaves it `false`: ```cpp static events::App to(const ReflType &app, const std::shared_ptr<events::EventBusType> &ev_bus) { auto runner = Reflector<events::Runner>::to(app.runner, ev_bus); return events::App{ .base = {...}, ... .start_virtual_compositor = app.start_virtual_compositor, .runner = runner, // <- .start_audio_server never assigned }; } ``` The opposite direction is fine — `from()` (same file) copies `start_audio_server`, which is why the value round-trips correctly for apps that came from `config.toml`, and why the API *reports* the wrong value rather than dropping the key. `configTOML.cpp` handles it in both directions too (`.start_audio_server = app.start_audio_server.value_or(true)` on read, serialised back on write). ### Fix One line in `to()`: ```cpp .start_virtual_compositor = app.start_virtual_compositor, .start_audio_server = app.start_audio_server, .runner = runner, ``` Happy to open a PR with that. ### Environment - `ghcr.io/games-on-whales/wolf:stable`, `sha256:ff82c125c9b79b2e9443de2b0eaec40c904edb03291680d408cccd57c1d59c76` (`stable` @ d6d41de) - GStreamer 1.26.7-0, NVENC, Talos Linux + NVIDIA GPU Operator - Wolf runs on Kubernetes; the app list is reconciled through the API by a sidecar, which is how this surfaced
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
games-on-whales/wolf#285
No description provided.