feat(gamestream): AV1 negotiation + 5.1/7.1 surround audio
Codec negotiation (M2 polish):
- ServerCodecModeSupport now advertises what we encode: H264|HEVC|AV1_MAIN8
= 65793 (flags verified against moonlight-common-c Limelight.h). The old
placeholder 3843 wrongly claimed HEVC Main10 + 4:4:4 and no AV1. Main10
bits stay off on purpose: Moonlight ties 10-bit to HDR, and capture is
8-bit SDR BGRx with no HDR metadata path (av1_nvenc -highbitdepth was
validated working for later).
- RTSP ANNOUNCE: bitStreamFormat 0/1/2 -> H264/HEVC/AV1 (already plumbed to
av1_nvenc; validated e2e via `m0 --codec av1` + ffprobe av01), and a
dynamicRangeMode!=0 request now logs + falls back to 8-bit SDR.
Surround audio (M2 polish):
- ANNOUNCE x-nv-audio.surround.{numChannels,AudioQuality} +
x-nv-aqos.packetDuration -> per-session AudioParams; DESCRIBE advertises
all six Opus configs (normal before HQ per channel count). Normal-quality
mappings are pre-rotated for the client's GFE-order LFE swap
(RtspConnection.c, verified verbatim) so its derived decoder mapping
equals our encoder mapping — including 7.1, where Sunshine's rotate only
covers [3,6) and scrambles LFE/SL/SR.
- 5.1/7.1 encode via libopus multistream (audiopus_sys, the sys layer the
opus crate already links) with Sunshine's layouts/bitrates, RAII wrapper;
the live-validated stereo wire is byte-identical (plain Opus, no FEC).
- Surround sessions add Sunshine-style RS(4,2) audio FEC (packetType 127 +
AUDIO_FEC_HEADER, the OpenFEC parity matrix both ends hardcode, nanors
gemm semantics verified from nanors/rs.c).
- PipeWire capture generalized to the negotiated channel count with explicit
FL FR FC LFE RL RR [SL SR] positions; missing sink channels are zero-
filled by the channel-mixer. PwAudioCapturer now tears down cleanly on
Drop (pipewire channel -> loop quit), so a channel-count change can
reopen without leaking a capture stream.
Tests: serverinfo mask, RTSP codec/audio param parsing, DESCRIBE contents,
surround-params strings + client-swap round trip, FEC parity self-recovery
and packet layout, real-codec 5.1 channel-identity round trip, and an
ignored live test (ran green against a 6ch null sink monitor).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -105,7 +105,7 @@ async fn serve(opts: M3Options) -> Result<()> {
|
||||
);
|
||||
|
||||
// One audio capturer for the whole host lifetime, handed from session to session
|
||||
// (PipeWire streams have no cheap teardown — see AudioCapSlot).
|
||||
// (avoids a PipeWire stream setup per session — see AudioCapSlot).
|
||||
let audio_cap: AudioCapSlot = Arc::new(std::sync::Mutex::new(None));
|
||||
|
||||
let mut served = 0u32;
|
||||
@@ -143,8 +143,8 @@ async fn serve(opts: M3Options) -> Result<()> {
|
||||
const HANDSHAKE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
/// Persistent audio-capturer slot, reused across sessions (same pattern as the GameStream
|
||||
/// path): `PwAudioCapturer` has no teardown — dropping one per session would leak its
|
||||
/// PipeWire thread + core connection + live capture node on the daemon every session.
|
||||
/// path): keeps one warm PipeWire capture stream instead of a connect/negotiate cycle —
|
||||
/// and a daemon-side node churn — per session. (Drop now tears a capturer down cleanly.)
|
||||
type AudioCapSlot = Arc<std::sync::Mutex<Option<Box<dyn crate::audio::AudioCapturer>>>>;
|
||||
|
||||
/// One client session: handshake → input/audio planes → data plane until done/disconnect.
|
||||
@@ -459,7 +459,7 @@ fn audio_thread(conn: quinn::Connection, stop: Arc<AtomicBool>, audio_cap: Audio
|
||||
c.drain(); // discard audio captured between sessions
|
||||
c
|
||||
}
|
||||
None => match crate::audio::open_audio_capture() {
|
||||
None => match crate::audio::open_audio_capture(CHANNELS as u32) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %format!("{e:#}"), "punktfunk/1 audio unavailable — session continues without it");
|
||||
|
||||
Reference in New Issue
Block a user