diff --git a/clients/deck/CMakeLists.txt b/clients/deck/CMakeLists.txt index 58a7f9c5..7de4a814 100644 --- a/clients/deck/CMakeLists.txt +++ b/clients/deck/CMakeLists.txt @@ -1,20 +1,54 @@ cmake_minimum_required(VERSION 3.24) -project(NovaDeck LANGUAGES CXX) +project(NovaDeck LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +find_package(PkgConfig REQUIRED) +pkg_check_modules(NOVA_DECK_FFMPEG_VAAPI REQUIRED IMPORTED_TARGET + libavcodec + libavutil + libva + libva-drm +) +pkg_check_modules(NOVA_DECK_LINUX_AUDIO REQUIRED IMPORTED_TARGET + libpipewire-0.3 + libpulse +) +find_package(Qt6 REQUIRED COMPONENTS Quick) + add_library(nova_deck_core src/deck_gamepad.cpp src/deck_layout.cpp src/polaris_game_fixture.cpp + src/stream/deck_stream_core.cpp + src/stream/deck_stream_media_adapters.cpp ) + +set(NOVA_DECK_MOONLIGHT_COMMON_C_DIR + ${CMAKE_CURRENT_SOURCE_DIR}/../../app/src/main/jni/moonlight-core/moonlight-common-c + CACHE PATH + "Path to the checked-out moonlight-common-c source tree used by the Deck stream skeleton" +) +if(EXISTS "${NOVA_DECK_MOONLIGHT_COMMON_C_DIR}/src/Limelight.h") + add_subdirectory("${NOVA_DECK_MOONLIGHT_COMMON_C_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/moonlight-common-c") + target_link_libraries(nova_deck_core PUBLIC moonlight-common-c) +else() + message(FATAL_ERROR "moonlight-common-c is required for the Deck stream skeleton; expected ${NOVA_DECK_MOONLIGHT_COMMON_C_DIR}/src/Limelight.h") +endif() + target_include_directories(nova_deck_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src ) +target_link_libraries(nova_deck_core + PUBLIC + PkgConfig::NOVA_DECK_FFMPEG_VAAPI + PkgConfig::NOVA_DECK_LINUX_AUDIO + Qt6::Quick +) target_compile_definitions(nova_deck_core PUBLIC NOVA_DECK_SAMPLE_GAME_FIXTURE=\"${CMAKE_CURRENT_SOURCE_DIR}/fixtures/sample_polaris_game.json\" @@ -32,6 +66,17 @@ if(BUILD_TESTING) NOVA_DECK_MAIN_QML_SOURCE="${CMAKE_CURRENT_SOURCE_DIR}/qml/Main.qml" ) add_test(NAME nova_deck_controller_library_smoke COMMAND nova_deck_layout_test) + add_executable(nova_deck_stream_core_test + tests/deck_stream_core_test.cpp + ) + target_link_libraries(nova_deck_stream_core_test PRIVATE nova_deck_core) + add_test(NAME nova_deck_stream_core_test COMMAND nova_deck_stream_core_test) + + add_executable(nova_deck_stream_media_adapters_test + tests/deck_stream_media_adapters_test.cpp + ) + target_link_libraries(nova_deck_stream_media_adapters_test PRIVATE nova_deck_core) + add_test(NAME nova_deck_stream_media_adapters_test COMMAND nova_deck_stream_media_adapters_test) endif() option(NOVA_DECK_BUILD_QT_SHELL "Build the experimental Qt/QML Steam Deck shell" ON) diff --git a/clients/deck/README.md b/clients/deck/README.md index 773a81d6..b939b12f 100644 --- a/clients/deck/README.md +++ b/clients/deck/README.md @@ -44,9 +44,15 @@ Native C++ cannot include Kotlin source directly. For this first slice, fixtures Keep this boundary explicit until the shared contract is exported through a real native-consumable API. Do not fake Kotlin/C++ interop by including .kt files. +## Stream core skeleton boundary + +clients/deck/src/stream/deck_stream_core.h is the first no-network native stream-core seam for the direct moonlight-common-c path. The CMake target links the real app/src/main/jni/moonlight-core/moonlight-common-c tree and the focused CTest includes Limelight.h, initializes STREAM_CONFIGURATION plus listener/video/audio callback structs, and verifies that the Deck lifecycle can move through idle, preparing, starting, active, stopping, stopped, cancelled, and failed states without opening sockets or calling LiStartConnection. + +The skeleton intentionally exposes adapter seams for renderer/presentation, audio, input, and session events, but ships only inert Linux-facing interfaces. Next backend work should add a hardware-backed Linux renderer/audio/input spike behind those seams while keeping host pairing, credentials, and real network start disabled until the lifecycle contract is reviewed. + ## Fedora or SteamOS dependency notes -The fallback smoke only needs CMake and a C++20 compiler. +The fallback smoke needs CMake, C/C++ compilers, OpenSSL crypto development headers, and the checked-out moonlight-common-c submodule. For the Qt shell on Fedora, install the Qt 6 development packages if CMake warns that Qt6 Quick or QuickControls2 is missing: diff --git a/clients/deck/spikes/streaming-backend-notes.md b/clients/deck/spikes/streaming-backend-notes.md new file mode 100644 index 00000000..e81618ec --- /dev/null +++ b/clients/deck/spikes/streaming-backend-notes.md @@ -0,0 +1,262 @@ +# Deck-T4 Streaming Backend Decision Notes + +Status: decision spike, not an implementation claim. No network calls, host probes, copied native assets, or Steam Deck support claims were added for this spike. + +## Recommendation + +Build the first real Deck streaming vertical slice as a Nova-owned native Linux client that links `moonlight-common-c` directly, then supplies Nova's own Linux backend adapters for decode/presentation, audio, input, discovery, pairing, persistence, and Polaris session orchestration. + +Do not fork an existing Linux Moonlight client as the product base for Deck-T4. Existing Linux clients remain useful references for backend behavior, dependency choices, and SteamOS quirks, but the first Nova slice should keep ownership of the shell, state machine, Polaris surfaces, and stream lifecycle boundary inside `clients/deck/` / `shared/stream-core/`. + +## Evidence from current repo + +- The existing Deck client is intentionally a preview shell. `clients/deck/README.md` says it validates layout, fake host/library states, inert launch preview, clipboard copy feedback, and controller primary-action routing only. It explicitly does not perform backend launch, streaming, discovery, pairing, persistence, network calls, shell execution, or real game launch behavior. +- The current Deck scaffold is already native CMake/C++20 with optional Qt/QML. The no-Qt path builds `nova_deck_core` and tests; the Qt path builds `nova-deck` only when Qt 6 Quick/QuickControls2 is present. +- Android streaming starts through `NvConnection`, which performs host/app/session negotiation, then calls `MoonBridge.startConnection(...)` after installing renderer/audio/listener callbacks. +- The JNI bridge is not portable as-is. `MoonBridge` owns Android/JVM static callback slots and loads `moonlight-core`; `callbacks.c` attaches native threads to the JVM, logs via Android logcat, uses Android CPU feature helpers, and forwards video/audio/listener callbacks back into Kotlin. +- The portable seam is lower: `moonlight-common-c` exposes `LiStartConnection(...)` with `CONNECTION_LISTENER_CALLBACKS`, `DECODER_RENDERER_CALLBACKS`, and `AUDIO_RENDERER_CALLBACKS`. That is the right contract to wrap for Deck, not the Android JNI bridge. +- `moonlight-common-c/README.md` says the C library is the shared GameStream client core used by Moonlight clients and is the right code to use when implementing a client that can use a C library. Its CMake also bundles the specific ENet variant required by the library, so the Deck build should consume that tree deliberately rather than accidentally linking a system ENet. +- Android-specific pieces are heavy: `MediaCodecDecoderRenderer`, `AndroidAudioRenderer`, Android discovery services/NSD/JmDNS wrappers, Android `Activity`/`Service` lifecycle, notification/keep-alive behavior, Android input capture, touch, USB controller drivers, and accessibility/key handling. These are behavior references, not source to port. +- Polaris product value currently lives above the stream core: capabilities, library, session truth, launch/watch modes, client settings, sync status, tuning, capture/encoder metadata, and NovaHUD/quick-menu state. A Deck backend that starts from a generic client shell would need to carve those surfaces back into someone else's navigation and state model. + +## Compared paths + +### A. Direct `moonlight-common-c` integration with Nova-owned Linux backends + +Use this for the next vertical slice. + +What gets reused: + +- GameStream/Moonlight transport core, RTSP/control/input/audio/video stream machinery, port-stage callbacks, network connectivity helpers where portable. +- Existing Android behavior as a reference for host negotiation, paired/owned/watch-only handling, current-game/session-token decisions, launch/quit/resume behavior, error copy, and Polaris session truth. +- Shared Polaris DTOs and future `shared/stream-core` contracts. + +What Nova must own on Deck: + +- A `DeckStreamingSession` boundary that wraps `LiStartConnection`, cancellation, stage transitions, and reconnect/suspend outcomes. +- Linux decode/presentation adapter that implements `DECODER_RENDERER_CALLBACKS` without involving Android `MediaCodec` or JNI. +- Linux audio adapter that implements `AUDIO_RENDERER_CALLBACKS` without `AudioTrack`. +- Input adapter that maps Steam Deck controls to Moonlight controller packets and keeps Nova UI shortcuts separate from in-stream input. +- Linux host discovery and pairing storage with certificate pinning semantics matching Nova's Android security posture. +- Polaris-aware library/launch/session/tuning surfaces in the native shell. + +Why this preserves Nova: + +- Nova keeps the controller-first Deck shell and stream state machine instead of inheriting a generic Moonlight client UI. +- Polaris surfaces can be first-class: Continue, launch mode, watch/owned-session status, tuning/sync truth, NovaHUD, reconnect copy, and safe diagnostic copy are product primitives rather than add-ons. +- Standard Moonlight-compatible hosts still work because the transport remains `moonlight-common-c`; Polaris simply enriches the host and session model. + +Risks: + +- More backend code up front. +- Need to prove the Deck decode/presentation path before investing in more UI. +- Need a clean Linux input path that does not fight Steam Input or Game Mode focus. +- Need a suspend/resume policy because `LiInterruptConnection()` is asynchronous and Android currently serializes connection start/stop through `NvConnection`/`MoonBridge` locks. + +### B. Adapt/fork an existing Linux Moonlight client + +Do not use this as the product base for Deck-T4. + +What it could reuse: + +- A working Linux streamer stack around decode, audio, input, fullscreen, and packaging. +- Known Moonlight-compatible host behavior and SteamOS-ish runtime knowledge if verified in that client. +- Potentially faster first picture on screen. + +Why it fights Nova: + +- Nova's differentiator is not the generic app list/stream button; it is Polaris-aware handheld behavior. Forking another Linux client means either grafting Polaris into its screens/state model or replacing those screens anyway. +- The current Deck scaffold already has a Nova-owned preview shell and product boundary. Forking now would duplicate or discard that work. +- Another client may encode assumptions about settings, app launch, session ownership, overlays, shortcuts, and host records that differ from Nova's Android reference behavior. +- Maintenance would follow another upstream's UI/runtime architecture instead of a shared Nova `stream-core` boundary that Android, Deck, and future clients can use. + +When to revisit: + +- If the first decode/presentation spike cannot produce stable low-latency video on SteamOS after testing VA-API/FFmpeg/GStreamer-style candidates. +- If a small, separable backend component can be reused without importing the whole client shell and state model. +- If license/compliance review says adapting a specific client source is simpler than building equivalent adapters. Nova is already GPLv3-lineage, but imported code still needs provenance and attribution review before copying. + +## Backend candidates for the direct path + +### Video decode and presentation + +First candidates to test: + +1. Hardware decode through a Linux media stack such as FFmpeg/libavcodec with VA-API on Steam Deck-class AMD hardware, then present into the Deck shell via an explicit renderer surface. +2. GStreamer only if it shortens VA-API + presentation integration without hiding frame pacing/control details Nova needs. +3. Software decode only as a fallback/diagnostic path, not the target. + +Decision for next test: prove H.264 first, then HEVC/AV1 and HDR after the control/audio/input skeleton works. Validate 1280x800 fullscreen, frame pacing, resize/fullscreen transitions, and overlay composition hooks before polishing library UI. + +### Audio + +First candidates: + +- PipeWire as the preferred SteamOS-era target if it is straightforward to feed decoded PCM with acceptable latency. +- PulseAudio compatibility path if PipeWire integration adds too much first-slice friction. +- SDL audio is acceptable as a temporary spike adapter only if it gets decoded PCM playing quickly while the session boundary stabilizes. + +The adapter must handle `AudioRenderer`-equivalent setup/start/stop/cleanup and arbitrary audio duration support because the Android native bridge advertises that capability today. + +### Controller and Steam Input + +First slice should handle the built-in Deck controls as a normal gamepad path first, using SDL/GameController-style mapping or a minimal evdev adapter if SDL is not introduced yet. Keep two routes distinct: + +- Shell navigation shortcuts: focus movement, command center, NovaHUD, copy/diagnostics, stream stop confirmation. +- In-stream packets: Moonlight controller arrival, button/stick/trigger packets, mouse/keyboard escape hatches later. + +Steam Input should be treated as an integration surface to test in Game Mode, not as an excuse to hardcode host-specific profiles into Nova. + +### Discovery and pairing + +Minimum next-slice surface: + +- Manual host add first, because it avoids a network discovery matrix while the stream backend is still proving itself. +- Pairing/session credential storage that matches Nova's security posture: local-only, no backup/export by accident, pinned server cert after pairing where available. +- LAN discovery via Linux mDNS/zeroconf only after manual add + pairing contracts are stable. + +### Fullscreen, Game Mode, and suspend/resume + +The first real stream test must run in a fullscreen Game Mode-like path, not only a Desktop Mode window. It should record: + +- Whether the shell can enter/exit fullscreen cleanly. +- Whether focus/controller input returns after overlay or stream transitions. +- What happens when the system suspends during connecting, active streaming, and disconnecting. +- Whether reconnect copy matches Android's owned/watch/session failure semantics. + +## Minimum Polaris surface for the next Deck vertical slice + +Required to feel like Nova rather than Moonlight cosplay: + +- Host capability probe: detect Polaris vs standard Moonlight-compatible host. +- Library row/card model using Polaris game metadata when available, with standard app-list fallback. +- Launch intent model containing host id, game id/UUID, requested launch mode, stream display mode/headless/virtual-display hint, and safe copy for UI/debugging. +- Session truth: active/inactive, owned-by-this-client, owner device/name when available, watch-only eligibility, quit/resume/replace permissions, session token plumbing where exposed. +- Client presentation/tuning summary: target fps/bitrate/codec/display mode, source of truth, sync state, and relaunch-required message. +- HUD/reconnect event stream: connection stage, transient warnings, poor connection, no video/no frame/protected content/early termination, suspend/reconnect status. + +Defer advanced Polaris surfaces until after first real stream: rich optimizer controls, profile editing, capture diagnostics UI, full NovaHUD parity, gyro/haptics polish, and every Android quick-menu action. + +## First technical risks to test next + +1. Video presentation: can a Deck-native adapter consume `moonlight-common-c` decode units and present low-latency fullscreen frames with overlay hooks? +2. Audio: can decoded PCM play through PipeWire/PulseAudio/temporary SDL without drift or bad teardown? +3. Controller input: can Deck controls be separated cleanly between shell shortcuts and in-stream Moonlight controller packets in Game Mode? +4. Host discovery/pairing: can manual add + pairing + pinned cert storage be represented without Android `Context`, NSD, or Android keystore assumptions? +5. Suspend/resume: can the session boundary interrupt, stop, reconnect, and report recovery states without corrupting `moonlight-common-c` lifecycle? +6. Game Mode fullscreen: can the Qt shell enter stream fullscreen, keep focus, and exit/recover with readable Nova copy? + +## Deck-T7 hardware-backed video/audio prototype decision + +Status: accepted follow-up decision for Deck-T7. This is still a local/offline +prototype plan: no `LiStartConnection`, sockets, host discovery, pairing, +credentials, native asset import, generated blobs, Android changes, or fake +"first stream" UI claims are part of this decision. + +### Recommended first path + +Use a Nova-owned Linux adapter pair behind the existing +`DeckStreamRenderer`/`DeckStreamAudio` seams: + +1. **Renderer/presentation:** FFmpeg/libavcodec H.264 decode with VA-API + hardware acceleration on Steam Deck-class AMD GPUs, exported toward a + Qt Quick/QRhi scene-graph item in the existing `nova-deck` Wayland window. + The first harness should prove the adapter can accept Annex-B decode units + from the `DECODER_RENDERER_CALLBACKS::submitDecodeUnit` shape, keep the + renderer non-blocking enough for `moonlight-common-c`, and present a + fullscreen 1280x800 surface with a future NovaHUD/overlay composition hook. +2. **Audio:** PipeWire native output fed by the Moonlight Opus/audio callback + path, with PulseAudio compatibility as the first fallback because SteamOS + exposes Pulse-compatible clients through PipeWire. The adapter must honor + `OPUS_MULTISTREAM_CONFIGURATION.samplesPerFrame` and advertise arbitrary + duration support only when the implementation really sizes decoded buffers + from that field. +3. **Shell/runtime boundary:** stay inside the Qt Deck shell for the first + prototype instead of taking over DRM/KMS. Game Mode should see a normal + fullscreen application surface through gamescope, letting Nova keep QML + focus, overlays, copy, stream-stop confirmation, and suspend/resume UI in + the same process. + +This path is grounded in the code already in-tree: `clients/deck/CMakeLists.txt` +links the real `moonlight-common-c` tree, and +`clients/deck/src/stream/deck_stream_core.h` exposes Linux-facing renderer, +audio, input, and session-event interfaces around real +`CONNECTION_LISTENER_CALLBACKS`, `DECODER_RENDERER_CALLBACKS`, +`AUDIO_RENDERER_CALLBACKS`, and `STREAM_CONFIGURATION` structs. The focused +CTest already proves those callback structs can be initialized and routed +without starting the network. Deck-T7's job is to choose the concrete +hardware-backed adapter technology for the next proof, not to start a host +stream. + +### Why FFmpeg + VA-API + Qt/QRhi first + +- `moonlight-common-c` submits Annex-B elementary stream decode units and asks + the renderer to return `DR_NEED_IDR` when it cannot process a unit. FFmpeg's + parser/decoder APIs fit that input shape directly and keep H.264 first while + leaving HEVC, AV1, 10-bit, HDR, and reference-frame invalidation for later + capability gates. +- VA-API is the shortest hardware-decode path for Steam Deck-class AMD Linux + systems and avoids committing Nova to a vendor-specific Vulkan decode stack + before the lifecycle, timing, and overlay contracts are known. +- Presenting through a Qt Quick/QRhi item preserves the existing controller-first + shell and gives Nova an overlay lane. A raw DRM/KMS renderer might be useful + for a narrow benchmark later, but in Steam Deck Game Mode it risks fighting + gamescope, focus restore, suspend/resume, and NovaHUD composition before the + stream lifecycle is proven. +- The Fedora development host already has the relevant development packages + discoverable by `pkg-config` (`libavcodec`, `libavutil`, `libva`, + `libva-drm`, `libdrm`, `egl`, `wayland-client`, `Qt6Quick`, and + `libpipewire-0.3`). Those probes are not product guarantees, but they show the + next local CMake probe can test real headers/libraries instead of inventing a + paper backend. + +### Why PipeWire first for audio + +- SteamOS-era desktops route audio through PipeWire, and PipeWire also covers + PulseAudio-compatible apps. Starting with PipeWire keeps the target modern + while preserving a fallback if the first slice needs compatibility glue. +- The Moonlight audio contract is small: `init`, `start`, `stop`, `cleanup`, and + `decodeAndPlaySample(char*, int)`. A dedicated PipeWire stream can own latency, + queue draining, and teardown explicitly instead of hiding it behind a game + framework introduced only for audio. +- SDL audio remains acceptable as a throwaway emergency spike, but not the + recommended product path. If Nova brings SDL in, it should be for a deliberate + gamepad/window/input decision rather than as an incidental audio-only + dependency. + +### Rejected alternatives + +- **Raw DRM/KMS/EGL primary renderer:** rejected for the first product slice. + It could be useful for a dedicated benchmarking harness, but it bypasses the + Qt shell that currently owns layout, focus, copy, and future NovaHUD surface. + It also risks conflicting with gamescope and suspend/resume expectations in + Steam Deck Game Mode. +- **SDL2 window/audio/input as the main streaming runtime:** rejected for now. + SDL2 is installed and useful for isolated probes, but replacing or embedding a + second window/input model would split shell navigation from stream focus before + Nova has proven the renderer/audio lifecycle. +- **GStreamer:** defer. It may simplify a complete media pipeline, but it can + hide frame pacing, decode-unit error handling, and overlay timing decisions + that Nova needs to own around `moonlight-common-c` callbacks. +- **Vulkan decode:** defer until after H.264/VA-API works. It may become the + right long-term zero-copy path, but it is too much API surface for the first + no-network hardware proof. +- **Software decode:** keep only as a diagnostic fallback. It does not answer + the Steam Deck hardware-backed question. +- **PulseAudio-only or ALSA-only audio:** reject as first choice. PulseAudio is a + compatibility fallback through PipeWire; ALSA is too low-level for the first + handheld lifecycle proof and makes device routing/suspend rougher than needed. + +### Deck-T8 implementation card + +Deck-T8 first hardware-backed Linux renderer/audio harness: add a local/offline +prototype under `clients/deck` that builds only when the required development +packages are present. It should connect the existing no-network stream-core +callbacks to an FFmpeg+VA-API H.264 renderer adapter and a PipeWire audio adapter, +feed them from deterministic test data created at test time or checked-in source +code only when licensing/provenance is explicit, and prove setup/start/submit or +decode/play/stop/cleanup boundaries without `LiStartConnection`, sockets, host +discovery, pairing, credentials, native asset blobs, or Android changes. Required +T8 verification: core Deck CMake/CTest, Qt smoke when available, adapter CTest or +probe skip with a clear dependency message, fullscreen/offscreen shell boundary +notes, `git diff --check`, and independent review before commit. diff --git a/clients/deck/src/stream/deck_stream_core.cpp b/clients/deck/src/stream/deck_stream_core.cpp new file mode 100644 index 00000000..7ebcf7c6 --- /dev/null +++ b/clients/deck/src/stream/deck_stream_core.cpp @@ -0,0 +1,870 @@ +#include "stream/deck_stream_core.h" + +namespace nova::deck::stream { + +namespace { + +constexpr std::size_t kMaxCallbackSlots = 16; +DeckStreamSession* callbackOwners[kMaxCallbackSlots] = {}; +bool callbackSlotReserved[kMaxCallbackSlots] = {}; +std::uintptr_t nextCallbackToken = 1; + +std::size_t reserveCallbackSlot() { + for (std::size_t slot = 0; slot < kMaxCallbackSlots; ++slot) { + if (!callbackSlotReserved[slot]) { + callbackSlotReserved[slot] = true; + return slot; + } + } + return kMaxCallbackSlots; +} + +void releaseCallbackSlot(const std::size_t slot) { + if (slot < kMaxCallbackSlots) { + callbackOwners[slot] = nullptr; + callbackSlotReserved[slot] = false; + } +} + +void* nextOpaqueCallbackContext() { + const auto token = nextCallbackToken++; + if (nextCallbackToken == 0) { + nextCallbackToken = 1; + } + return reinterpret_cast(token); +} + +bool isValidStreamRequest(const DeckStreamRequest& request) { + return request.width > 0 && request.height > 0 && request.fps > 0 && request.bitrateKbps > 0; +} + +} // namespace + +DeckStreamSession* DeckStreamSession::ownerFromContext(void* context) { + if (context == nullptr) { + return nullptr; + } + for (auto* owner : callbackOwners) { + if (owner != nullptr && owner->callbackContext_ == context) { + return owner; + } + } + return nullptr; +} + +void* DeckStreamSession::contextForSlot(const std::size_t slot) { + if (slot >= kMaxCallbackSlots) { + return nullptr; + } + return reinterpret_cast(slot + 1); +} + +DeckStreamSession* DeckStreamSession::ownerForSlot(const std::size_t slot) { + if (slot >= kMaxCallbackSlots) { + return nullptr; + } + return callbackOwners[slot]; +} + +bool DeckStreamSession::hasActiveOwnerOtherThan(const DeckStreamSession& session) { + for (auto* owner : callbackOwners) { + if (owner != nullptr && owner != &session) { + return true; + } + } + return false; +} + +void DeckStreamSession::setCallbackOwner(DeckStreamSession& session) { + if (session.hasCallbackSlot()) { + callbackOwners[session.callbackSlot_] = &session; + } +} + +void DeckStreamSession::clearCallbackOwner(DeckStreamSession& session) { + if (session.hasCallbackSlot() && callbackOwners[session.callbackSlot_] == &session) { + callbackOwners[session.callbackSlot_] = nullptr; + } +} + +int DeckStreamSession::videoSetupForSlot(const std::size_t slot, const int videoFormat, const int width, const int height, const int redrawRate, void* context, const int drFlags) { + auto* owner = ownerFromContext(context); + if (owner == nullptr || owner != ownerForSlot(slot)) { + return DR_NEED_IDR; + } + return owner->renderer_.setup(videoFormat, width, height, redrawRate, context, drFlags); +} + +void DeckStreamSession::videoStartForSlot(const std::size_t slot) { if (auto* owner = ownerForSlot(slot)) { owner->renderer_.start(); } } +void DeckStreamSession::videoStopForSlot(const std::size_t slot) { if (auto* owner = ownerForSlot(slot)) { owner->renderer_.stop(); } } +void DeckStreamSession::videoCleanupForSlot(const std::size_t slot) { if (auto* owner = ownerForSlot(slot)) { owner->renderer_.cleanup(); } } +int DeckStreamSession::videoSubmitDecodeUnitForSlot(const std::size_t slot, PDECODE_UNIT decodeUnit) { if (auto* owner = ownerForSlot(slot)) { return owner->renderer_.submitDecodeUnit(decodeUnit); } return DR_NEED_IDR; } + +int DeckStreamSession::audioInitForSlot(const std::size_t slot, const int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, const int arFlags) { + auto* owner = ownerFromContext(context); + if (owner == nullptr || owner != ownerForSlot(slot)) { + return -1; + } + return owner->audio_.init(audioConfiguration, opusConfig, context, arFlags); +} + +void DeckStreamSession::audioStartForSlot(const std::size_t slot) { if (auto* owner = ownerForSlot(slot)) { owner->audio_.start(); } } +void DeckStreamSession::audioStopForSlot(const std::size_t slot) { if (auto* owner = ownerForSlot(slot)) { owner->audio_.stop(); } } +void DeckStreamSession::audioCleanupForSlot(const std::size_t slot) { if (auto* owner = ownerForSlot(slot)) { owner->audio_.cleanup(); } } +void DeckStreamSession::audioDecodeAndPlaySampleForSlot(const std::size_t slot, char* sampleData, const int sampleLength) { if (auto* owner = ownerForSlot(slot)) { owner->audio_.decodeAndPlaySample(sampleData, sampleLength); } } + +void DeckStreamSession::listenerStageStartingForSlot(const std::size_t slot, const int stage) { (void)stage; if (auto* owner = ownerForSlot(slot)) { owner->noteSessionEvent("moonlight stage starting"); } } +void DeckStreamSession::listenerStageCompleteForSlot(const std::size_t slot, const int stage) { (void)stage; if (auto* owner = ownerForSlot(slot)) { owner->noteSessionEvent("moonlight stage complete"); } } +void DeckStreamSession::listenerStageFailedForSlot(const std::size_t slot, const int stage, const int errorCode) { (void)stage; (void)errorCode; if (auto* owner = ownerForSlot(slot)) { owner->noteSessionEvent("moonlight stage failed"); } } +void DeckStreamSession::listenerConnectionStartedForSlot(const std::size_t slot) { if (auto* owner = ownerForSlot(slot)) { owner->noteSessionEvent("moonlight connection started callback received in no-network adapter"); } } +void DeckStreamSession::listenerConnectionTerminatedForSlot(const std::size_t slot, const int errorCode) { (void)errorCode; if (auto* owner = ownerForSlot(slot)) { owner->noteSessionEvent("moonlight connection terminated callback received in no-network adapter"); } } +void DeckStreamSession::listenerRumbleForSlot(const std::size_t slot, const unsigned short controllerNumber, const unsigned short lowFreqMotor, const unsigned short highFreqMotor) { if (auto* owner = ownerForSlot(slot)) { owner->input_.rumble(controllerNumber, lowFreqMotor, highFreqMotor); } } +void DeckStreamSession::listenerSetMotionEventStateForSlot(const std::size_t slot, const uint16_t controllerNumber, const uint8_t motionType, const uint16_t reportRateHz) { if (auto* owner = ownerForSlot(slot)) { owner->input_.setMotionEventState(controllerNumber, motionType, reportRateHz); } } +void DeckStreamSession::listenerSetControllerLedForSlot(const std::size_t slot, const uint16_t controllerNumber, const uint8_t r, const uint8_t g, const uint8_t b) { if (auto* owner = ownerForSlot(slot)) { owner->input_.setControllerLed(controllerNumber, r, g, b); } } + +int DeckStreamSession::videoSetup0(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { return videoSetupForSlot(0, videoFormat, width, height, redrawRate, context, drFlags); } +void DeckStreamSession::videoStart0() { videoStartForSlot(0); } +void DeckStreamSession::videoStop0() { videoStopForSlot(0); } +void DeckStreamSession::videoCleanup0() { videoCleanupForSlot(0); } +int DeckStreamSession::videoSubmitDecodeUnit0(PDECODE_UNIT decodeUnit) { return videoSubmitDecodeUnitForSlot(0, decodeUnit); } +int DeckStreamSession::audioInit0(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags) { return audioInitForSlot(0, audioConfiguration, opusConfig, context, arFlags); } +void DeckStreamSession::audioStart0() { audioStartForSlot(0); } +void DeckStreamSession::audioStop0() { audioStopForSlot(0); } +void DeckStreamSession::audioCleanup0() { audioCleanupForSlot(0); } +void DeckStreamSession::audioDecodeAndPlaySample0(char* sampleData, int sampleLength) { audioDecodeAndPlaySampleForSlot(0, sampleData, sampleLength); } +void DeckStreamSession::listenerStageStarting0(int stage) { listenerStageStartingForSlot(0, stage); } +void DeckStreamSession::listenerStageComplete0(int stage) { listenerStageCompleteForSlot(0, stage); } +void DeckStreamSession::listenerStageFailed0(int stage, int errorCode) { listenerStageFailedForSlot(0, stage, errorCode); } +void DeckStreamSession::listenerConnectionStarted0() { listenerConnectionStartedForSlot(0); } +void DeckStreamSession::listenerConnectionTerminated0(int errorCode) { listenerConnectionTerminatedForSlot(0, errorCode); } +void DeckStreamSession::listenerRumble0(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) { listenerRumbleForSlot(0, controllerNumber, lowFreqMotor, highFreqMotor); } +void DeckStreamSession::listenerSetMotionEventState0(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) { listenerSetMotionEventStateForSlot(0, controllerNumber, motionType, reportRateHz); } +void DeckStreamSession::listenerSetControllerLed0(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) { listenerSetControllerLedForSlot(0, controllerNumber, r, g, b); } +int DeckStreamSession::videoSetup1(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { return videoSetupForSlot(1, videoFormat, width, height, redrawRate, context, drFlags); } +void DeckStreamSession::videoStart1() { videoStartForSlot(1); } +void DeckStreamSession::videoStop1() { videoStopForSlot(1); } +void DeckStreamSession::videoCleanup1() { videoCleanupForSlot(1); } +int DeckStreamSession::videoSubmitDecodeUnit1(PDECODE_UNIT decodeUnit) { return videoSubmitDecodeUnitForSlot(1, decodeUnit); } +int DeckStreamSession::audioInit1(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags) { return audioInitForSlot(1, audioConfiguration, opusConfig, context, arFlags); } +void DeckStreamSession::audioStart1() { audioStartForSlot(1); } +void DeckStreamSession::audioStop1() { audioStopForSlot(1); } +void DeckStreamSession::audioCleanup1() { audioCleanupForSlot(1); } +void DeckStreamSession::audioDecodeAndPlaySample1(char* sampleData, int sampleLength) { audioDecodeAndPlaySampleForSlot(1, sampleData, sampleLength); } +void DeckStreamSession::listenerStageStarting1(int stage) { listenerStageStartingForSlot(1, stage); } +void DeckStreamSession::listenerStageComplete1(int stage) { listenerStageCompleteForSlot(1, stage); } +void DeckStreamSession::listenerStageFailed1(int stage, int errorCode) { listenerStageFailedForSlot(1, stage, errorCode); } +void DeckStreamSession::listenerConnectionStarted1() { listenerConnectionStartedForSlot(1); } +void DeckStreamSession::listenerConnectionTerminated1(int errorCode) { listenerConnectionTerminatedForSlot(1, errorCode); } +void DeckStreamSession::listenerRumble1(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) { listenerRumbleForSlot(1, controllerNumber, lowFreqMotor, highFreqMotor); } +void DeckStreamSession::listenerSetMotionEventState1(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) { listenerSetMotionEventStateForSlot(1, controllerNumber, motionType, reportRateHz); } +void DeckStreamSession::listenerSetControllerLed1(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) { listenerSetControllerLedForSlot(1, controllerNumber, r, g, b); } +int DeckStreamSession::videoSetup2(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { return videoSetupForSlot(2, videoFormat, width, height, redrawRate, context, drFlags); } +void DeckStreamSession::videoStart2() { videoStartForSlot(2); } +void DeckStreamSession::videoStop2() { videoStopForSlot(2); } +void DeckStreamSession::videoCleanup2() { videoCleanupForSlot(2); } +int DeckStreamSession::videoSubmitDecodeUnit2(PDECODE_UNIT decodeUnit) { return videoSubmitDecodeUnitForSlot(2, decodeUnit); } +int DeckStreamSession::audioInit2(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags) { return audioInitForSlot(2, audioConfiguration, opusConfig, context, arFlags); } +void DeckStreamSession::audioStart2() { audioStartForSlot(2); } +void DeckStreamSession::audioStop2() { audioStopForSlot(2); } +void DeckStreamSession::audioCleanup2() { audioCleanupForSlot(2); } +void DeckStreamSession::audioDecodeAndPlaySample2(char* sampleData, int sampleLength) { audioDecodeAndPlaySampleForSlot(2, sampleData, sampleLength); } +void DeckStreamSession::listenerStageStarting2(int stage) { listenerStageStartingForSlot(2, stage); } +void DeckStreamSession::listenerStageComplete2(int stage) { listenerStageCompleteForSlot(2, stage); } +void DeckStreamSession::listenerStageFailed2(int stage, int errorCode) { listenerStageFailedForSlot(2, stage, errorCode); } +void DeckStreamSession::listenerConnectionStarted2() { listenerConnectionStartedForSlot(2); } +void DeckStreamSession::listenerConnectionTerminated2(int errorCode) { listenerConnectionTerminatedForSlot(2, errorCode); } +void DeckStreamSession::listenerRumble2(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) { listenerRumbleForSlot(2, controllerNumber, lowFreqMotor, highFreqMotor); } +void DeckStreamSession::listenerSetMotionEventState2(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) { listenerSetMotionEventStateForSlot(2, controllerNumber, motionType, reportRateHz); } +void DeckStreamSession::listenerSetControllerLed2(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) { listenerSetControllerLedForSlot(2, controllerNumber, r, g, b); } +int DeckStreamSession::videoSetup3(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { return videoSetupForSlot(3, videoFormat, width, height, redrawRate, context, drFlags); } +void DeckStreamSession::videoStart3() { videoStartForSlot(3); } +void DeckStreamSession::videoStop3() { videoStopForSlot(3); } +void DeckStreamSession::videoCleanup3() { videoCleanupForSlot(3); } +int DeckStreamSession::videoSubmitDecodeUnit3(PDECODE_UNIT decodeUnit) { return videoSubmitDecodeUnitForSlot(3, decodeUnit); } +int DeckStreamSession::audioInit3(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags) { return audioInitForSlot(3, audioConfiguration, opusConfig, context, arFlags); } +void DeckStreamSession::audioStart3() { audioStartForSlot(3); } +void DeckStreamSession::audioStop3() { audioStopForSlot(3); } +void DeckStreamSession::audioCleanup3() { audioCleanupForSlot(3); } +void DeckStreamSession::audioDecodeAndPlaySample3(char* sampleData, int sampleLength) { audioDecodeAndPlaySampleForSlot(3, sampleData, sampleLength); } +void DeckStreamSession::listenerStageStarting3(int stage) { listenerStageStartingForSlot(3, stage); } +void DeckStreamSession::listenerStageComplete3(int stage) { listenerStageCompleteForSlot(3, stage); } +void DeckStreamSession::listenerStageFailed3(int stage, int errorCode) { listenerStageFailedForSlot(3, stage, errorCode); } +void DeckStreamSession::listenerConnectionStarted3() { listenerConnectionStartedForSlot(3); } +void DeckStreamSession::listenerConnectionTerminated3(int errorCode) { listenerConnectionTerminatedForSlot(3, errorCode); } +void DeckStreamSession::listenerRumble3(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) { listenerRumbleForSlot(3, controllerNumber, lowFreqMotor, highFreqMotor); } +void DeckStreamSession::listenerSetMotionEventState3(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) { listenerSetMotionEventStateForSlot(3, controllerNumber, motionType, reportRateHz); } +void DeckStreamSession::listenerSetControllerLed3(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) { listenerSetControllerLedForSlot(3, controllerNumber, r, g, b); } +int DeckStreamSession::videoSetup4(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { return videoSetupForSlot(4, videoFormat, width, height, redrawRate, context, drFlags); } +void DeckStreamSession::videoStart4() { videoStartForSlot(4); } +void DeckStreamSession::videoStop4() { videoStopForSlot(4); } +void DeckStreamSession::videoCleanup4() { videoCleanupForSlot(4); } +int DeckStreamSession::videoSubmitDecodeUnit4(PDECODE_UNIT decodeUnit) { return videoSubmitDecodeUnitForSlot(4, decodeUnit); } +int DeckStreamSession::audioInit4(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags) { return audioInitForSlot(4, audioConfiguration, opusConfig, context, arFlags); } +void DeckStreamSession::audioStart4() { audioStartForSlot(4); } +void DeckStreamSession::audioStop4() { audioStopForSlot(4); } +void DeckStreamSession::audioCleanup4() { audioCleanupForSlot(4); } +void DeckStreamSession::audioDecodeAndPlaySample4(char* sampleData, int sampleLength) { audioDecodeAndPlaySampleForSlot(4, sampleData, sampleLength); } +void DeckStreamSession::listenerStageStarting4(int stage) { listenerStageStartingForSlot(4, stage); } +void DeckStreamSession::listenerStageComplete4(int stage) { listenerStageCompleteForSlot(4, stage); } +void DeckStreamSession::listenerStageFailed4(int stage, int errorCode) { listenerStageFailedForSlot(4, stage, errorCode); } +void DeckStreamSession::listenerConnectionStarted4() { listenerConnectionStartedForSlot(4); } +void DeckStreamSession::listenerConnectionTerminated4(int errorCode) { listenerConnectionTerminatedForSlot(4, errorCode); } +void DeckStreamSession::listenerRumble4(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) { listenerRumbleForSlot(4, controllerNumber, lowFreqMotor, highFreqMotor); } +void DeckStreamSession::listenerSetMotionEventState4(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) { listenerSetMotionEventStateForSlot(4, controllerNumber, motionType, reportRateHz); } +void DeckStreamSession::listenerSetControllerLed4(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) { listenerSetControllerLedForSlot(4, controllerNumber, r, g, b); } +int DeckStreamSession::videoSetup5(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { return videoSetupForSlot(5, videoFormat, width, height, redrawRate, context, drFlags); } +void DeckStreamSession::videoStart5() { videoStartForSlot(5); } +void DeckStreamSession::videoStop5() { videoStopForSlot(5); } +void DeckStreamSession::videoCleanup5() { videoCleanupForSlot(5); } +int DeckStreamSession::videoSubmitDecodeUnit5(PDECODE_UNIT decodeUnit) { return videoSubmitDecodeUnitForSlot(5, decodeUnit); } +int DeckStreamSession::audioInit5(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags) { return audioInitForSlot(5, audioConfiguration, opusConfig, context, arFlags); } +void DeckStreamSession::audioStart5() { audioStartForSlot(5); } +void DeckStreamSession::audioStop5() { audioStopForSlot(5); } +void DeckStreamSession::audioCleanup5() { audioCleanupForSlot(5); } +void DeckStreamSession::audioDecodeAndPlaySample5(char* sampleData, int sampleLength) { audioDecodeAndPlaySampleForSlot(5, sampleData, sampleLength); } +void DeckStreamSession::listenerStageStarting5(int stage) { listenerStageStartingForSlot(5, stage); } +void DeckStreamSession::listenerStageComplete5(int stage) { listenerStageCompleteForSlot(5, stage); } +void DeckStreamSession::listenerStageFailed5(int stage, int errorCode) { listenerStageFailedForSlot(5, stage, errorCode); } +void DeckStreamSession::listenerConnectionStarted5() { listenerConnectionStartedForSlot(5); } +void DeckStreamSession::listenerConnectionTerminated5(int errorCode) { listenerConnectionTerminatedForSlot(5, errorCode); } +void DeckStreamSession::listenerRumble5(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) { listenerRumbleForSlot(5, controllerNumber, lowFreqMotor, highFreqMotor); } +void DeckStreamSession::listenerSetMotionEventState5(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) { listenerSetMotionEventStateForSlot(5, controllerNumber, motionType, reportRateHz); } +void DeckStreamSession::listenerSetControllerLed5(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) { listenerSetControllerLedForSlot(5, controllerNumber, r, g, b); } +int DeckStreamSession::videoSetup6(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { return videoSetupForSlot(6, videoFormat, width, height, redrawRate, context, drFlags); } +void DeckStreamSession::videoStart6() { videoStartForSlot(6); } +void DeckStreamSession::videoStop6() { videoStopForSlot(6); } +void DeckStreamSession::videoCleanup6() { videoCleanupForSlot(6); } +int DeckStreamSession::videoSubmitDecodeUnit6(PDECODE_UNIT decodeUnit) { return videoSubmitDecodeUnitForSlot(6, decodeUnit); } +int DeckStreamSession::audioInit6(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags) { return audioInitForSlot(6, audioConfiguration, opusConfig, context, arFlags); } +void DeckStreamSession::audioStart6() { audioStartForSlot(6); } +void DeckStreamSession::audioStop6() { audioStopForSlot(6); } +void DeckStreamSession::audioCleanup6() { audioCleanupForSlot(6); } +void DeckStreamSession::audioDecodeAndPlaySample6(char* sampleData, int sampleLength) { audioDecodeAndPlaySampleForSlot(6, sampleData, sampleLength); } +void DeckStreamSession::listenerStageStarting6(int stage) { listenerStageStartingForSlot(6, stage); } +void DeckStreamSession::listenerStageComplete6(int stage) { listenerStageCompleteForSlot(6, stage); } +void DeckStreamSession::listenerStageFailed6(int stage, int errorCode) { listenerStageFailedForSlot(6, stage, errorCode); } +void DeckStreamSession::listenerConnectionStarted6() { listenerConnectionStartedForSlot(6); } +void DeckStreamSession::listenerConnectionTerminated6(int errorCode) { listenerConnectionTerminatedForSlot(6, errorCode); } +void DeckStreamSession::listenerRumble6(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) { listenerRumbleForSlot(6, controllerNumber, lowFreqMotor, highFreqMotor); } +void DeckStreamSession::listenerSetMotionEventState6(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) { listenerSetMotionEventStateForSlot(6, controllerNumber, motionType, reportRateHz); } +void DeckStreamSession::listenerSetControllerLed6(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) { listenerSetControllerLedForSlot(6, controllerNumber, r, g, b); } +int DeckStreamSession::videoSetup7(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { return videoSetupForSlot(7, videoFormat, width, height, redrawRate, context, drFlags); } +void DeckStreamSession::videoStart7() { videoStartForSlot(7); } +void DeckStreamSession::videoStop7() { videoStopForSlot(7); } +void DeckStreamSession::videoCleanup7() { videoCleanupForSlot(7); } +int DeckStreamSession::videoSubmitDecodeUnit7(PDECODE_UNIT decodeUnit) { return videoSubmitDecodeUnitForSlot(7, decodeUnit); } +int DeckStreamSession::audioInit7(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags) { return audioInitForSlot(7, audioConfiguration, opusConfig, context, arFlags); } +void DeckStreamSession::audioStart7() { audioStartForSlot(7); } +void DeckStreamSession::audioStop7() { audioStopForSlot(7); } +void DeckStreamSession::audioCleanup7() { audioCleanupForSlot(7); } +void DeckStreamSession::audioDecodeAndPlaySample7(char* sampleData, int sampleLength) { audioDecodeAndPlaySampleForSlot(7, sampleData, sampleLength); } +void DeckStreamSession::listenerStageStarting7(int stage) { listenerStageStartingForSlot(7, stage); } +void DeckStreamSession::listenerStageComplete7(int stage) { listenerStageCompleteForSlot(7, stage); } +void DeckStreamSession::listenerStageFailed7(int stage, int errorCode) { listenerStageFailedForSlot(7, stage, errorCode); } +void DeckStreamSession::listenerConnectionStarted7() { listenerConnectionStartedForSlot(7); } +void DeckStreamSession::listenerConnectionTerminated7(int errorCode) { listenerConnectionTerminatedForSlot(7, errorCode); } +void DeckStreamSession::listenerRumble7(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) { listenerRumbleForSlot(7, controllerNumber, lowFreqMotor, highFreqMotor); } +void DeckStreamSession::listenerSetMotionEventState7(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) { listenerSetMotionEventStateForSlot(7, controllerNumber, motionType, reportRateHz); } +void DeckStreamSession::listenerSetControllerLed7(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) { listenerSetControllerLedForSlot(7, controllerNumber, r, g, b); } +int DeckStreamSession::videoSetup8(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { return videoSetupForSlot(8, videoFormat, width, height, redrawRate, context, drFlags); } +void DeckStreamSession::videoStart8() { videoStartForSlot(8); } +void DeckStreamSession::videoStop8() { videoStopForSlot(8); } +void DeckStreamSession::videoCleanup8() { videoCleanupForSlot(8); } +int DeckStreamSession::videoSubmitDecodeUnit8(PDECODE_UNIT decodeUnit) { return videoSubmitDecodeUnitForSlot(8, decodeUnit); } +int DeckStreamSession::audioInit8(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags) { return audioInitForSlot(8, audioConfiguration, opusConfig, context, arFlags); } +void DeckStreamSession::audioStart8() { audioStartForSlot(8); } +void DeckStreamSession::audioStop8() { audioStopForSlot(8); } +void DeckStreamSession::audioCleanup8() { audioCleanupForSlot(8); } +void DeckStreamSession::audioDecodeAndPlaySample8(char* sampleData, int sampleLength) { audioDecodeAndPlaySampleForSlot(8, sampleData, sampleLength); } +void DeckStreamSession::listenerStageStarting8(int stage) { listenerStageStartingForSlot(8, stage); } +void DeckStreamSession::listenerStageComplete8(int stage) { listenerStageCompleteForSlot(8, stage); } +void DeckStreamSession::listenerStageFailed8(int stage, int errorCode) { listenerStageFailedForSlot(8, stage, errorCode); } +void DeckStreamSession::listenerConnectionStarted8() { listenerConnectionStartedForSlot(8); } +void DeckStreamSession::listenerConnectionTerminated8(int errorCode) { listenerConnectionTerminatedForSlot(8, errorCode); } +void DeckStreamSession::listenerRumble8(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) { listenerRumbleForSlot(8, controllerNumber, lowFreqMotor, highFreqMotor); } +void DeckStreamSession::listenerSetMotionEventState8(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) { listenerSetMotionEventStateForSlot(8, controllerNumber, motionType, reportRateHz); } +void DeckStreamSession::listenerSetControllerLed8(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) { listenerSetControllerLedForSlot(8, controllerNumber, r, g, b); } +int DeckStreamSession::videoSetup9(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { return videoSetupForSlot(9, videoFormat, width, height, redrawRate, context, drFlags); } +void DeckStreamSession::videoStart9() { videoStartForSlot(9); } +void DeckStreamSession::videoStop9() { videoStopForSlot(9); } +void DeckStreamSession::videoCleanup9() { videoCleanupForSlot(9); } +int DeckStreamSession::videoSubmitDecodeUnit9(PDECODE_UNIT decodeUnit) { return videoSubmitDecodeUnitForSlot(9, decodeUnit); } +int DeckStreamSession::audioInit9(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags) { return audioInitForSlot(9, audioConfiguration, opusConfig, context, arFlags); } +void DeckStreamSession::audioStart9() { audioStartForSlot(9); } +void DeckStreamSession::audioStop9() { audioStopForSlot(9); } +void DeckStreamSession::audioCleanup9() { audioCleanupForSlot(9); } +void DeckStreamSession::audioDecodeAndPlaySample9(char* sampleData, int sampleLength) { audioDecodeAndPlaySampleForSlot(9, sampleData, sampleLength); } +void DeckStreamSession::listenerStageStarting9(int stage) { listenerStageStartingForSlot(9, stage); } +void DeckStreamSession::listenerStageComplete9(int stage) { listenerStageCompleteForSlot(9, stage); } +void DeckStreamSession::listenerStageFailed9(int stage, int errorCode) { listenerStageFailedForSlot(9, stage, errorCode); } +void DeckStreamSession::listenerConnectionStarted9() { listenerConnectionStartedForSlot(9); } +void DeckStreamSession::listenerConnectionTerminated9(int errorCode) { listenerConnectionTerminatedForSlot(9, errorCode); } +void DeckStreamSession::listenerRumble9(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) { listenerRumbleForSlot(9, controllerNumber, lowFreqMotor, highFreqMotor); } +void DeckStreamSession::listenerSetMotionEventState9(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) { listenerSetMotionEventStateForSlot(9, controllerNumber, motionType, reportRateHz); } +void DeckStreamSession::listenerSetControllerLed9(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) { listenerSetControllerLedForSlot(9, controllerNumber, r, g, b); } +int DeckStreamSession::videoSetup10(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { return videoSetupForSlot(10, videoFormat, width, height, redrawRate, context, drFlags); } +void DeckStreamSession::videoStart10() { videoStartForSlot(10); } +void DeckStreamSession::videoStop10() { videoStopForSlot(10); } +void DeckStreamSession::videoCleanup10() { videoCleanupForSlot(10); } +int DeckStreamSession::videoSubmitDecodeUnit10(PDECODE_UNIT decodeUnit) { return videoSubmitDecodeUnitForSlot(10, decodeUnit); } +int DeckStreamSession::audioInit10(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags) { return audioInitForSlot(10, audioConfiguration, opusConfig, context, arFlags); } +void DeckStreamSession::audioStart10() { audioStartForSlot(10); } +void DeckStreamSession::audioStop10() { audioStopForSlot(10); } +void DeckStreamSession::audioCleanup10() { audioCleanupForSlot(10); } +void DeckStreamSession::audioDecodeAndPlaySample10(char* sampleData, int sampleLength) { audioDecodeAndPlaySampleForSlot(10, sampleData, sampleLength); } +void DeckStreamSession::listenerStageStarting10(int stage) { listenerStageStartingForSlot(10, stage); } +void DeckStreamSession::listenerStageComplete10(int stage) { listenerStageCompleteForSlot(10, stage); } +void DeckStreamSession::listenerStageFailed10(int stage, int errorCode) { listenerStageFailedForSlot(10, stage, errorCode); } +void DeckStreamSession::listenerConnectionStarted10() { listenerConnectionStartedForSlot(10); } +void DeckStreamSession::listenerConnectionTerminated10(int errorCode) { listenerConnectionTerminatedForSlot(10, errorCode); } +void DeckStreamSession::listenerRumble10(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) { listenerRumbleForSlot(10, controllerNumber, lowFreqMotor, highFreqMotor); } +void DeckStreamSession::listenerSetMotionEventState10(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) { listenerSetMotionEventStateForSlot(10, controllerNumber, motionType, reportRateHz); } +void DeckStreamSession::listenerSetControllerLed10(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) { listenerSetControllerLedForSlot(10, controllerNumber, r, g, b); } +int DeckStreamSession::videoSetup11(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { return videoSetupForSlot(11, videoFormat, width, height, redrawRate, context, drFlags); } +void DeckStreamSession::videoStart11() { videoStartForSlot(11); } +void DeckStreamSession::videoStop11() { videoStopForSlot(11); } +void DeckStreamSession::videoCleanup11() { videoCleanupForSlot(11); } +int DeckStreamSession::videoSubmitDecodeUnit11(PDECODE_UNIT decodeUnit) { return videoSubmitDecodeUnitForSlot(11, decodeUnit); } +int DeckStreamSession::audioInit11(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags) { return audioInitForSlot(11, audioConfiguration, opusConfig, context, arFlags); } +void DeckStreamSession::audioStart11() { audioStartForSlot(11); } +void DeckStreamSession::audioStop11() { audioStopForSlot(11); } +void DeckStreamSession::audioCleanup11() { audioCleanupForSlot(11); } +void DeckStreamSession::audioDecodeAndPlaySample11(char* sampleData, int sampleLength) { audioDecodeAndPlaySampleForSlot(11, sampleData, sampleLength); } +void DeckStreamSession::listenerStageStarting11(int stage) { listenerStageStartingForSlot(11, stage); } +void DeckStreamSession::listenerStageComplete11(int stage) { listenerStageCompleteForSlot(11, stage); } +void DeckStreamSession::listenerStageFailed11(int stage, int errorCode) { listenerStageFailedForSlot(11, stage, errorCode); } +void DeckStreamSession::listenerConnectionStarted11() { listenerConnectionStartedForSlot(11); } +void DeckStreamSession::listenerConnectionTerminated11(int errorCode) { listenerConnectionTerminatedForSlot(11, errorCode); } +void DeckStreamSession::listenerRumble11(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) { listenerRumbleForSlot(11, controllerNumber, lowFreqMotor, highFreqMotor); } +void DeckStreamSession::listenerSetMotionEventState11(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) { listenerSetMotionEventStateForSlot(11, controllerNumber, motionType, reportRateHz); } +void DeckStreamSession::listenerSetControllerLed11(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) { listenerSetControllerLedForSlot(11, controllerNumber, r, g, b); } +int DeckStreamSession::videoSetup12(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { return videoSetupForSlot(12, videoFormat, width, height, redrawRate, context, drFlags); } +void DeckStreamSession::videoStart12() { videoStartForSlot(12); } +void DeckStreamSession::videoStop12() { videoStopForSlot(12); } +void DeckStreamSession::videoCleanup12() { videoCleanupForSlot(12); } +int DeckStreamSession::videoSubmitDecodeUnit12(PDECODE_UNIT decodeUnit) { return videoSubmitDecodeUnitForSlot(12, decodeUnit); } +int DeckStreamSession::audioInit12(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags) { return audioInitForSlot(12, audioConfiguration, opusConfig, context, arFlags); } +void DeckStreamSession::audioStart12() { audioStartForSlot(12); } +void DeckStreamSession::audioStop12() { audioStopForSlot(12); } +void DeckStreamSession::audioCleanup12() { audioCleanupForSlot(12); } +void DeckStreamSession::audioDecodeAndPlaySample12(char* sampleData, int sampleLength) { audioDecodeAndPlaySampleForSlot(12, sampleData, sampleLength); } +void DeckStreamSession::listenerStageStarting12(int stage) { listenerStageStartingForSlot(12, stage); } +void DeckStreamSession::listenerStageComplete12(int stage) { listenerStageCompleteForSlot(12, stage); } +void DeckStreamSession::listenerStageFailed12(int stage, int errorCode) { listenerStageFailedForSlot(12, stage, errorCode); } +void DeckStreamSession::listenerConnectionStarted12() { listenerConnectionStartedForSlot(12); } +void DeckStreamSession::listenerConnectionTerminated12(int errorCode) { listenerConnectionTerminatedForSlot(12, errorCode); } +void DeckStreamSession::listenerRumble12(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) { listenerRumbleForSlot(12, controllerNumber, lowFreqMotor, highFreqMotor); } +void DeckStreamSession::listenerSetMotionEventState12(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) { listenerSetMotionEventStateForSlot(12, controllerNumber, motionType, reportRateHz); } +void DeckStreamSession::listenerSetControllerLed12(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) { listenerSetControllerLedForSlot(12, controllerNumber, r, g, b); } +int DeckStreamSession::videoSetup13(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { return videoSetupForSlot(13, videoFormat, width, height, redrawRate, context, drFlags); } +void DeckStreamSession::videoStart13() { videoStartForSlot(13); } +void DeckStreamSession::videoStop13() { videoStopForSlot(13); } +void DeckStreamSession::videoCleanup13() { videoCleanupForSlot(13); } +int DeckStreamSession::videoSubmitDecodeUnit13(PDECODE_UNIT decodeUnit) { return videoSubmitDecodeUnitForSlot(13, decodeUnit); } +int DeckStreamSession::audioInit13(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags) { return audioInitForSlot(13, audioConfiguration, opusConfig, context, arFlags); } +void DeckStreamSession::audioStart13() { audioStartForSlot(13); } +void DeckStreamSession::audioStop13() { audioStopForSlot(13); } +void DeckStreamSession::audioCleanup13() { audioCleanupForSlot(13); } +void DeckStreamSession::audioDecodeAndPlaySample13(char* sampleData, int sampleLength) { audioDecodeAndPlaySampleForSlot(13, sampleData, sampleLength); } +void DeckStreamSession::listenerStageStarting13(int stage) { listenerStageStartingForSlot(13, stage); } +void DeckStreamSession::listenerStageComplete13(int stage) { listenerStageCompleteForSlot(13, stage); } +void DeckStreamSession::listenerStageFailed13(int stage, int errorCode) { listenerStageFailedForSlot(13, stage, errorCode); } +void DeckStreamSession::listenerConnectionStarted13() { listenerConnectionStartedForSlot(13); } +void DeckStreamSession::listenerConnectionTerminated13(int errorCode) { listenerConnectionTerminatedForSlot(13, errorCode); } +void DeckStreamSession::listenerRumble13(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) { listenerRumbleForSlot(13, controllerNumber, lowFreqMotor, highFreqMotor); } +void DeckStreamSession::listenerSetMotionEventState13(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) { listenerSetMotionEventStateForSlot(13, controllerNumber, motionType, reportRateHz); } +void DeckStreamSession::listenerSetControllerLed13(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) { listenerSetControllerLedForSlot(13, controllerNumber, r, g, b); } +int DeckStreamSession::videoSetup14(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { return videoSetupForSlot(14, videoFormat, width, height, redrawRate, context, drFlags); } +void DeckStreamSession::videoStart14() { videoStartForSlot(14); } +void DeckStreamSession::videoStop14() { videoStopForSlot(14); } +void DeckStreamSession::videoCleanup14() { videoCleanupForSlot(14); } +int DeckStreamSession::videoSubmitDecodeUnit14(PDECODE_UNIT decodeUnit) { return videoSubmitDecodeUnitForSlot(14, decodeUnit); } +int DeckStreamSession::audioInit14(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags) { return audioInitForSlot(14, audioConfiguration, opusConfig, context, arFlags); } +void DeckStreamSession::audioStart14() { audioStartForSlot(14); } +void DeckStreamSession::audioStop14() { audioStopForSlot(14); } +void DeckStreamSession::audioCleanup14() { audioCleanupForSlot(14); } +void DeckStreamSession::audioDecodeAndPlaySample14(char* sampleData, int sampleLength) { audioDecodeAndPlaySampleForSlot(14, sampleData, sampleLength); } +void DeckStreamSession::listenerStageStarting14(int stage) { listenerStageStartingForSlot(14, stage); } +void DeckStreamSession::listenerStageComplete14(int stage) { listenerStageCompleteForSlot(14, stage); } +void DeckStreamSession::listenerStageFailed14(int stage, int errorCode) { listenerStageFailedForSlot(14, stage, errorCode); } +void DeckStreamSession::listenerConnectionStarted14() { listenerConnectionStartedForSlot(14); } +void DeckStreamSession::listenerConnectionTerminated14(int errorCode) { listenerConnectionTerminatedForSlot(14, errorCode); } +void DeckStreamSession::listenerRumble14(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) { listenerRumbleForSlot(14, controllerNumber, lowFreqMotor, highFreqMotor); } +void DeckStreamSession::listenerSetMotionEventState14(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) { listenerSetMotionEventStateForSlot(14, controllerNumber, motionType, reportRateHz); } +void DeckStreamSession::listenerSetControllerLed14(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) { listenerSetControllerLedForSlot(14, controllerNumber, r, g, b); } +int DeckStreamSession::videoSetup15(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { return videoSetupForSlot(15, videoFormat, width, height, redrawRate, context, drFlags); } +void DeckStreamSession::videoStart15() { videoStartForSlot(15); } +void DeckStreamSession::videoStop15() { videoStopForSlot(15); } +void DeckStreamSession::videoCleanup15() { videoCleanupForSlot(15); } +int DeckStreamSession::videoSubmitDecodeUnit15(PDECODE_UNIT decodeUnit) { return videoSubmitDecodeUnitForSlot(15, decodeUnit); } +int DeckStreamSession::audioInit15(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags) { return audioInitForSlot(15, audioConfiguration, opusConfig, context, arFlags); } +void DeckStreamSession::audioStart15() { audioStartForSlot(15); } +void DeckStreamSession::audioStop15() { audioStopForSlot(15); } +void DeckStreamSession::audioCleanup15() { audioCleanupForSlot(15); } +void DeckStreamSession::audioDecodeAndPlaySample15(char* sampleData, int sampleLength) { audioDecodeAndPlaySampleForSlot(15, sampleData, sampleLength); } +void DeckStreamSession::listenerStageStarting15(int stage) { listenerStageStartingForSlot(15, stage); } +void DeckStreamSession::listenerStageComplete15(int stage) { listenerStageCompleteForSlot(15, stage); } +void DeckStreamSession::listenerStageFailed15(int stage, int errorCode) { listenerStageFailedForSlot(15, stage, errorCode); } +void DeckStreamSession::listenerConnectionStarted15() { listenerConnectionStartedForSlot(15); } +void DeckStreamSession::listenerConnectionTerminated15(int errorCode) { listenerConnectionTerminatedForSlot(15, errorCode); } +void DeckStreamSession::listenerRumble15(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) { listenerRumbleForSlot(15, controllerNumber, lowFreqMotor, highFreqMotor); } +void DeckStreamSession::listenerSetMotionEventState15(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) { listenerSetMotionEventStateForSlot(15, controllerNumber, motionType, reportRateHz); } +void DeckStreamSession::listenerSetControllerLed15(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) { listenerSetControllerLedForSlot(15, controllerNumber, r, g, b); } + +bool DeckStreamSession::hasCallbackSlot() const { + return callbackSlot_ != kInvalidCallbackSlot && callbackSlot_ < kMaxCallbackSlots; +} + +void DeckStreamSession::installCallbackThunks() { + callbackSlot_ = reserveCallbackSlot(); + if (callbackSlot_ >= kMaxCallbackSlots) { + callbackSlot_ = kInvalidCallbackSlot; + return; + } + switch (callbackSlot_) { + case 0: + videoCallbacks_.setup = &DeckStreamSession::videoSetup0; + videoCallbacks_.start = &DeckStreamSession::videoStart0; + videoCallbacks_.stop = &DeckStreamSession::videoStop0; + videoCallbacks_.cleanup = &DeckStreamSession::videoCleanup0; + videoCallbacks_.submitDecodeUnit = &DeckStreamSession::videoSubmitDecodeUnit0; + audioCallbacks_.init = &DeckStreamSession::audioInit0; + audioCallbacks_.start = &DeckStreamSession::audioStart0; + audioCallbacks_.stop = &DeckStreamSession::audioStop0; + audioCallbacks_.cleanup = &DeckStreamSession::audioCleanup0; + audioCallbacks_.decodeAndPlaySample = &DeckStreamSession::audioDecodeAndPlaySample0; + listenerCallbacks_.stageStarting = &DeckStreamSession::listenerStageStarting0; + listenerCallbacks_.stageComplete = &DeckStreamSession::listenerStageComplete0; + listenerCallbacks_.stageFailed = &DeckStreamSession::listenerStageFailed0; + listenerCallbacks_.connectionStarted = &DeckStreamSession::listenerConnectionStarted0; + listenerCallbacks_.connectionTerminated = &DeckStreamSession::listenerConnectionTerminated0; + listenerCallbacks_.rumble = &DeckStreamSession::listenerRumble0; + listenerCallbacks_.setMotionEventState = &DeckStreamSession::listenerSetMotionEventState0; + listenerCallbacks_.setControllerLED = &DeckStreamSession::listenerSetControllerLed0; + break; + case 1: + videoCallbacks_.setup = &DeckStreamSession::videoSetup1; + videoCallbacks_.start = &DeckStreamSession::videoStart1; + videoCallbacks_.stop = &DeckStreamSession::videoStop1; + videoCallbacks_.cleanup = &DeckStreamSession::videoCleanup1; + videoCallbacks_.submitDecodeUnit = &DeckStreamSession::videoSubmitDecodeUnit1; + audioCallbacks_.init = &DeckStreamSession::audioInit1; + audioCallbacks_.start = &DeckStreamSession::audioStart1; + audioCallbacks_.stop = &DeckStreamSession::audioStop1; + audioCallbacks_.cleanup = &DeckStreamSession::audioCleanup1; + audioCallbacks_.decodeAndPlaySample = &DeckStreamSession::audioDecodeAndPlaySample1; + listenerCallbacks_.stageStarting = &DeckStreamSession::listenerStageStarting1; + listenerCallbacks_.stageComplete = &DeckStreamSession::listenerStageComplete1; + listenerCallbacks_.stageFailed = &DeckStreamSession::listenerStageFailed1; + listenerCallbacks_.connectionStarted = &DeckStreamSession::listenerConnectionStarted1; + listenerCallbacks_.connectionTerminated = &DeckStreamSession::listenerConnectionTerminated1; + listenerCallbacks_.rumble = &DeckStreamSession::listenerRumble1; + listenerCallbacks_.setMotionEventState = &DeckStreamSession::listenerSetMotionEventState1; + listenerCallbacks_.setControllerLED = &DeckStreamSession::listenerSetControllerLed1; + break; + case 2: + videoCallbacks_.setup = &DeckStreamSession::videoSetup2; + videoCallbacks_.start = &DeckStreamSession::videoStart2; + videoCallbacks_.stop = &DeckStreamSession::videoStop2; + videoCallbacks_.cleanup = &DeckStreamSession::videoCleanup2; + videoCallbacks_.submitDecodeUnit = &DeckStreamSession::videoSubmitDecodeUnit2; + audioCallbacks_.init = &DeckStreamSession::audioInit2; + audioCallbacks_.start = &DeckStreamSession::audioStart2; + audioCallbacks_.stop = &DeckStreamSession::audioStop2; + audioCallbacks_.cleanup = &DeckStreamSession::audioCleanup2; + audioCallbacks_.decodeAndPlaySample = &DeckStreamSession::audioDecodeAndPlaySample2; + listenerCallbacks_.stageStarting = &DeckStreamSession::listenerStageStarting2; + listenerCallbacks_.stageComplete = &DeckStreamSession::listenerStageComplete2; + listenerCallbacks_.stageFailed = &DeckStreamSession::listenerStageFailed2; + listenerCallbacks_.connectionStarted = &DeckStreamSession::listenerConnectionStarted2; + listenerCallbacks_.connectionTerminated = &DeckStreamSession::listenerConnectionTerminated2; + listenerCallbacks_.rumble = &DeckStreamSession::listenerRumble2; + listenerCallbacks_.setMotionEventState = &DeckStreamSession::listenerSetMotionEventState2; + listenerCallbacks_.setControllerLED = &DeckStreamSession::listenerSetControllerLed2; + break; + case 3: + videoCallbacks_.setup = &DeckStreamSession::videoSetup3; + videoCallbacks_.start = &DeckStreamSession::videoStart3; + videoCallbacks_.stop = &DeckStreamSession::videoStop3; + videoCallbacks_.cleanup = &DeckStreamSession::videoCleanup3; + videoCallbacks_.submitDecodeUnit = &DeckStreamSession::videoSubmitDecodeUnit3; + audioCallbacks_.init = &DeckStreamSession::audioInit3; + audioCallbacks_.start = &DeckStreamSession::audioStart3; + audioCallbacks_.stop = &DeckStreamSession::audioStop3; + audioCallbacks_.cleanup = &DeckStreamSession::audioCleanup3; + audioCallbacks_.decodeAndPlaySample = &DeckStreamSession::audioDecodeAndPlaySample3; + listenerCallbacks_.stageStarting = &DeckStreamSession::listenerStageStarting3; + listenerCallbacks_.stageComplete = &DeckStreamSession::listenerStageComplete3; + listenerCallbacks_.stageFailed = &DeckStreamSession::listenerStageFailed3; + listenerCallbacks_.connectionStarted = &DeckStreamSession::listenerConnectionStarted3; + listenerCallbacks_.connectionTerminated = &DeckStreamSession::listenerConnectionTerminated3; + listenerCallbacks_.rumble = &DeckStreamSession::listenerRumble3; + listenerCallbacks_.setMotionEventState = &DeckStreamSession::listenerSetMotionEventState3; + listenerCallbacks_.setControllerLED = &DeckStreamSession::listenerSetControllerLed3; + break; + case 4: + videoCallbacks_.setup = &DeckStreamSession::videoSetup4; + videoCallbacks_.start = &DeckStreamSession::videoStart4; + videoCallbacks_.stop = &DeckStreamSession::videoStop4; + videoCallbacks_.cleanup = &DeckStreamSession::videoCleanup4; + videoCallbacks_.submitDecodeUnit = &DeckStreamSession::videoSubmitDecodeUnit4; + audioCallbacks_.init = &DeckStreamSession::audioInit4; + audioCallbacks_.start = &DeckStreamSession::audioStart4; + audioCallbacks_.stop = &DeckStreamSession::audioStop4; + audioCallbacks_.cleanup = &DeckStreamSession::audioCleanup4; + audioCallbacks_.decodeAndPlaySample = &DeckStreamSession::audioDecodeAndPlaySample4; + listenerCallbacks_.stageStarting = &DeckStreamSession::listenerStageStarting4; + listenerCallbacks_.stageComplete = &DeckStreamSession::listenerStageComplete4; + listenerCallbacks_.stageFailed = &DeckStreamSession::listenerStageFailed4; + listenerCallbacks_.connectionStarted = &DeckStreamSession::listenerConnectionStarted4; + listenerCallbacks_.connectionTerminated = &DeckStreamSession::listenerConnectionTerminated4; + listenerCallbacks_.rumble = &DeckStreamSession::listenerRumble4; + listenerCallbacks_.setMotionEventState = &DeckStreamSession::listenerSetMotionEventState4; + listenerCallbacks_.setControllerLED = &DeckStreamSession::listenerSetControllerLed4; + break; + case 5: + videoCallbacks_.setup = &DeckStreamSession::videoSetup5; + videoCallbacks_.start = &DeckStreamSession::videoStart5; + videoCallbacks_.stop = &DeckStreamSession::videoStop5; + videoCallbacks_.cleanup = &DeckStreamSession::videoCleanup5; + videoCallbacks_.submitDecodeUnit = &DeckStreamSession::videoSubmitDecodeUnit5; + audioCallbacks_.init = &DeckStreamSession::audioInit5; + audioCallbacks_.start = &DeckStreamSession::audioStart5; + audioCallbacks_.stop = &DeckStreamSession::audioStop5; + audioCallbacks_.cleanup = &DeckStreamSession::audioCleanup5; + audioCallbacks_.decodeAndPlaySample = &DeckStreamSession::audioDecodeAndPlaySample5; + listenerCallbacks_.stageStarting = &DeckStreamSession::listenerStageStarting5; + listenerCallbacks_.stageComplete = &DeckStreamSession::listenerStageComplete5; + listenerCallbacks_.stageFailed = &DeckStreamSession::listenerStageFailed5; + listenerCallbacks_.connectionStarted = &DeckStreamSession::listenerConnectionStarted5; + listenerCallbacks_.connectionTerminated = &DeckStreamSession::listenerConnectionTerminated5; + listenerCallbacks_.rumble = &DeckStreamSession::listenerRumble5; + listenerCallbacks_.setMotionEventState = &DeckStreamSession::listenerSetMotionEventState5; + listenerCallbacks_.setControllerLED = &DeckStreamSession::listenerSetControllerLed5; + break; + case 6: + videoCallbacks_.setup = &DeckStreamSession::videoSetup6; + videoCallbacks_.start = &DeckStreamSession::videoStart6; + videoCallbacks_.stop = &DeckStreamSession::videoStop6; + videoCallbacks_.cleanup = &DeckStreamSession::videoCleanup6; + videoCallbacks_.submitDecodeUnit = &DeckStreamSession::videoSubmitDecodeUnit6; + audioCallbacks_.init = &DeckStreamSession::audioInit6; + audioCallbacks_.start = &DeckStreamSession::audioStart6; + audioCallbacks_.stop = &DeckStreamSession::audioStop6; + audioCallbacks_.cleanup = &DeckStreamSession::audioCleanup6; + audioCallbacks_.decodeAndPlaySample = &DeckStreamSession::audioDecodeAndPlaySample6; + listenerCallbacks_.stageStarting = &DeckStreamSession::listenerStageStarting6; + listenerCallbacks_.stageComplete = &DeckStreamSession::listenerStageComplete6; + listenerCallbacks_.stageFailed = &DeckStreamSession::listenerStageFailed6; + listenerCallbacks_.connectionStarted = &DeckStreamSession::listenerConnectionStarted6; + listenerCallbacks_.connectionTerminated = &DeckStreamSession::listenerConnectionTerminated6; + listenerCallbacks_.rumble = &DeckStreamSession::listenerRumble6; + listenerCallbacks_.setMotionEventState = &DeckStreamSession::listenerSetMotionEventState6; + listenerCallbacks_.setControllerLED = &DeckStreamSession::listenerSetControllerLed6; + break; + case 7: + videoCallbacks_.setup = &DeckStreamSession::videoSetup7; + videoCallbacks_.start = &DeckStreamSession::videoStart7; + videoCallbacks_.stop = &DeckStreamSession::videoStop7; + videoCallbacks_.cleanup = &DeckStreamSession::videoCleanup7; + videoCallbacks_.submitDecodeUnit = &DeckStreamSession::videoSubmitDecodeUnit7; + audioCallbacks_.init = &DeckStreamSession::audioInit7; + audioCallbacks_.start = &DeckStreamSession::audioStart7; + audioCallbacks_.stop = &DeckStreamSession::audioStop7; + audioCallbacks_.cleanup = &DeckStreamSession::audioCleanup7; + audioCallbacks_.decodeAndPlaySample = &DeckStreamSession::audioDecodeAndPlaySample7; + listenerCallbacks_.stageStarting = &DeckStreamSession::listenerStageStarting7; + listenerCallbacks_.stageComplete = &DeckStreamSession::listenerStageComplete7; + listenerCallbacks_.stageFailed = &DeckStreamSession::listenerStageFailed7; + listenerCallbacks_.connectionStarted = &DeckStreamSession::listenerConnectionStarted7; + listenerCallbacks_.connectionTerminated = &DeckStreamSession::listenerConnectionTerminated7; + listenerCallbacks_.rumble = &DeckStreamSession::listenerRumble7; + listenerCallbacks_.setMotionEventState = &DeckStreamSession::listenerSetMotionEventState7; + listenerCallbacks_.setControllerLED = &DeckStreamSession::listenerSetControllerLed7; + break; + case 8: + videoCallbacks_.setup = &DeckStreamSession::videoSetup8; + videoCallbacks_.start = &DeckStreamSession::videoStart8; + videoCallbacks_.stop = &DeckStreamSession::videoStop8; + videoCallbacks_.cleanup = &DeckStreamSession::videoCleanup8; + videoCallbacks_.submitDecodeUnit = &DeckStreamSession::videoSubmitDecodeUnit8; + audioCallbacks_.init = &DeckStreamSession::audioInit8; + audioCallbacks_.start = &DeckStreamSession::audioStart8; + audioCallbacks_.stop = &DeckStreamSession::audioStop8; + audioCallbacks_.cleanup = &DeckStreamSession::audioCleanup8; + audioCallbacks_.decodeAndPlaySample = &DeckStreamSession::audioDecodeAndPlaySample8; + listenerCallbacks_.stageStarting = &DeckStreamSession::listenerStageStarting8; + listenerCallbacks_.stageComplete = &DeckStreamSession::listenerStageComplete8; + listenerCallbacks_.stageFailed = &DeckStreamSession::listenerStageFailed8; + listenerCallbacks_.connectionStarted = &DeckStreamSession::listenerConnectionStarted8; + listenerCallbacks_.connectionTerminated = &DeckStreamSession::listenerConnectionTerminated8; + listenerCallbacks_.rumble = &DeckStreamSession::listenerRumble8; + listenerCallbacks_.setMotionEventState = &DeckStreamSession::listenerSetMotionEventState8; + listenerCallbacks_.setControllerLED = &DeckStreamSession::listenerSetControllerLed8; + break; + case 9: + videoCallbacks_.setup = &DeckStreamSession::videoSetup9; + videoCallbacks_.start = &DeckStreamSession::videoStart9; + videoCallbacks_.stop = &DeckStreamSession::videoStop9; + videoCallbacks_.cleanup = &DeckStreamSession::videoCleanup9; + videoCallbacks_.submitDecodeUnit = &DeckStreamSession::videoSubmitDecodeUnit9; + audioCallbacks_.init = &DeckStreamSession::audioInit9; + audioCallbacks_.start = &DeckStreamSession::audioStart9; + audioCallbacks_.stop = &DeckStreamSession::audioStop9; + audioCallbacks_.cleanup = &DeckStreamSession::audioCleanup9; + audioCallbacks_.decodeAndPlaySample = &DeckStreamSession::audioDecodeAndPlaySample9; + listenerCallbacks_.stageStarting = &DeckStreamSession::listenerStageStarting9; + listenerCallbacks_.stageComplete = &DeckStreamSession::listenerStageComplete9; + listenerCallbacks_.stageFailed = &DeckStreamSession::listenerStageFailed9; + listenerCallbacks_.connectionStarted = &DeckStreamSession::listenerConnectionStarted9; + listenerCallbacks_.connectionTerminated = &DeckStreamSession::listenerConnectionTerminated9; + listenerCallbacks_.rumble = &DeckStreamSession::listenerRumble9; + listenerCallbacks_.setMotionEventState = &DeckStreamSession::listenerSetMotionEventState9; + listenerCallbacks_.setControllerLED = &DeckStreamSession::listenerSetControllerLed9; + break; + case 10: + videoCallbacks_.setup = &DeckStreamSession::videoSetup10; + videoCallbacks_.start = &DeckStreamSession::videoStart10; + videoCallbacks_.stop = &DeckStreamSession::videoStop10; + videoCallbacks_.cleanup = &DeckStreamSession::videoCleanup10; + videoCallbacks_.submitDecodeUnit = &DeckStreamSession::videoSubmitDecodeUnit10; + audioCallbacks_.init = &DeckStreamSession::audioInit10; + audioCallbacks_.start = &DeckStreamSession::audioStart10; + audioCallbacks_.stop = &DeckStreamSession::audioStop10; + audioCallbacks_.cleanup = &DeckStreamSession::audioCleanup10; + audioCallbacks_.decodeAndPlaySample = &DeckStreamSession::audioDecodeAndPlaySample10; + listenerCallbacks_.stageStarting = &DeckStreamSession::listenerStageStarting10; + listenerCallbacks_.stageComplete = &DeckStreamSession::listenerStageComplete10; + listenerCallbacks_.stageFailed = &DeckStreamSession::listenerStageFailed10; + listenerCallbacks_.connectionStarted = &DeckStreamSession::listenerConnectionStarted10; + listenerCallbacks_.connectionTerminated = &DeckStreamSession::listenerConnectionTerminated10; + listenerCallbacks_.rumble = &DeckStreamSession::listenerRumble10; + listenerCallbacks_.setMotionEventState = &DeckStreamSession::listenerSetMotionEventState10; + listenerCallbacks_.setControllerLED = &DeckStreamSession::listenerSetControllerLed10; + break; + case 11: + videoCallbacks_.setup = &DeckStreamSession::videoSetup11; + videoCallbacks_.start = &DeckStreamSession::videoStart11; + videoCallbacks_.stop = &DeckStreamSession::videoStop11; + videoCallbacks_.cleanup = &DeckStreamSession::videoCleanup11; + videoCallbacks_.submitDecodeUnit = &DeckStreamSession::videoSubmitDecodeUnit11; + audioCallbacks_.init = &DeckStreamSession::audioInit11; + audioCallbacks_.start = &DeckStreamSession::audioStart11; + audioCallbacks_.stop = &DeckStreamSession::audioStop11; + audioCallbacks_.cleanup = &DeckStreamSession::audioCleanup11; + audioCallbacks_.decodeAndPlaySample = &DeckStreamSession::audioDecodeAndPlaySample11; + listenerCallbacks_.stageStarting = &DeckStreamSession::listenerStageStarting11; + listenerCallbacks_.stageComplete = &DeckStreamSession::listenerStageComplete11; + listenerCallbacks_.stageFailed = &DeckStreamSession::listenerStageFailed11; + listenerCallbacks_.connectionStarted = &DeckStreamSession::listenerConnectionStarted11; + listenerCallbacks_.connectionTerminated = &DeckStreamSession::listenerConnectionTerminated11; + listenerCallbacks_.rumble = &DeckStreamSession::listenerRumble11; + listenerCallbacks_.setMotionEventState = &DeckStreamSession::listenerSetMotionEventState11; + listenerCallbacks_.setControllerLED = &DeckStreamSession::listenerSetControllerLed11; + break; + case 12: + videoCallbacks_.setup = &DeckStreamSession::videoSetup12; + videoCallbacks_.start = &DeckStreamSession::videoStart12; + videoCallbacks_.stop = &DeckStreamSession::videoStop12; + videoCallbacks_.cleanup = &DeckStreamSession::videoCleanup12; + videoCallbacks_.submitDecodeUnit = &DeckStreamSession::videoSubmitDecodeUnit12; + audioCallbacks_.init = &DeckStreamSession::audioInit12; + audioCallbacks_.start = &DeckStreamSession::audioStart12; + audioCallbacks_.stop = &DeckStreamSession::audioStop12; + audioCallbacks_.cleanup = &DeckStreamSession::audioCleanup12; + audioCallbacks_.decodeAndPlaySample = &DeckStreamSession::audioDecodeAndPlaySample12; + listenerCallbacks_.stageStarting = &DeckStreamSession::listenerStageStarting12; + listenerCallbacks_.stageComplete = &DeckStreamSession::listenerStageComplete12; + listenerCallbacks_.stageFailed = &DeckStreamSession::listenerStageFailed12; + listenerCallbacks_.connectionStarted = &DeckStreamSession::listenerConnectionStarted12; + listenerCallbacks_.connectionTerminated = &DeckStreamSession::listenerConnectionTerminated12; + listenerCallbacks_.rumble = &DeckStreamSession::listenerRumble12; + listenerCallbacks_.setMotionEventState = &DeckStreamSession::listenerSetMotionEventState12; + listenerCallbacks_.setControllerLED = &DeckStreamSession::listenerSetControllerLed12; + break; + case 13: + videoCallbacks_.setup = &DeckStreamSession::videoSetup13; + videoCallbacks_.start = &DeckStreamSession::videoStart13; + videoCallbacks_.stop = &DeckStreamSession::videoStop13; + videoCallbacks_.cleanup = &DeckStreamSession::videoCleanup13; + videoCallbacks_.submitDecodeUnit = &DeckStreamSession::videoSubmitDecodeUnit13; + audioCallbacks_.init = &DeckStreamSession::audioInit13; + audioCallbacks_.start = &DeckStreamSession::audioStart13; + audioCallbacks_.stop = &DeckStreamSession::audioStop13; + audioCallbacks_.cleanup = &DeckStreamSession::audioCleanup13; + audioCallbacks_.decodeAndPlaySample = &DeckStreamSession::audioDecodeAndPlaySample13; + listenerCallbacks_.stageStarting = &DeckStreamSession::listenerStageStarting13; + listenerCallbacks_.stageComplete = &DeckStreamSession::listenerStageComplete13; + listenerCallbacks_.stageFailed = &DeckStreamSession::listenerStageFailed13; + listenerCallbacks_.connectionStarted = &DeckStreamSession::listenerConnectionStarted13; + listenerCallbacks_.connectionTerminated = &DeckStreamSession::listenerConnectionTerminated13; + listenerCallbacks_.rumble = &DeckStreamSession::listenerRumble13; + listenerCallbacks_.setMotionEventState = &DeckStreamSession::listenerSetMotionEventState13; + listenerCallbacks_.setControllerLED = &DeckStreamSession::listenerSetControllerLed13; + break; + case 14: + videoCallbacks_.setup = &DeckStreamSession::videoSetup14; + videoCallbacks_.start = &DeckStreamSession::videoStart14; + videoCallbacks_.stop = &DeckStreamSession::videoStop14; + videoCallbacks_.cleanup = &DeckStreamSession::videoCleanup14; + videoCallbacks_.submitDecodeUnit = &DeckStreamSession::videoSubmitDecodeUnit14; + audioCallbacks_.init = &DeckStreamSession::audioInit14; + audioCallbacks_.start = &DeckStreamSession::audioStart14; + audioCallbacks_.stop = &DeckStreamSession::audioStop14; + audioCallbacks_.cleanup = &DeckStreamSession::audioCleanup14; + audioCallbacks_.decodeAndPlaySample = &DeckStreamSession::audioDecodeAndPlaySample14; + listenerCallbacks_.stageStarting = &DeckStreamSession::listenerStageStarting14; + listenerCallbacks_.stageComplete = &DeckStreamSession::listenerStageComplete14; + listenerCallbacks_.stageFailed = &DeckStreamSession::listenerStageFailed14; + listenerCallbacks_.connectionStarted = &DeckStreamSession::listenerConnectionStarted14; + listenerCallbacks_.connectionTerminated = &DeckStreamSession::listenerConnectionTerminated14; + listenerCallbacks_.rumble = &DeckStreamSession::listenerRumble14; + listenerCallbacks_.setMotionEventState = &DeckStreamSession::listenerSetMotionEventState14; + listenerCallbacks_.setControllerLED = &DeckStreamSession::listenerSetControllerLed14; + break; + case 15: + videoCallbacks_.setup = &DeckStreamSession::videoSetup15; + videoCallbacks_.start = &DeckStreamSession::videoStart15; + videoCallbacks_.stop = &DeckStreamSession::videoStop15; + videoCallbacks_.cleanup = &DeckStreamSession::videoCleanup15; + videoCallbacks_.submitDecodeUnit = &DeckStreamSession::videoSubmitDecodeUnit15; + audioCallbacks_.init = &DeckStreamSession::audioInit15; + audioCallbacks_.start = &DeckStreamSession::audioStart15; + audioCallbacks_.stop = &DeckStreamSession::audioStop15; + audioCallbacks_.cleanup = &DeckStreamSession::audioCleanup15; + audioCallbacks_.decodeAndPlaySample = &DeckStreamSession::audioDecodeAndPlaySample15; + listenerCallbacks_.stageStarting = &DeckStreamSession::listenerStageStarting15; + listenerCallbacks_.stageComplete = &DeckStreamSession::listenerStageComplete15; + listenerCallbacks_.stageFailed = &DeckStreamSession::listenerStageFailed15; + listenerCallbacks_.connectionStarted = &DeckStreamSession::listenerConnectionStarted15; + listenerCallbacks_.connectionTerminated = &DeckStreamSession::listenerConnectionTerminated15; + listenerCallbacks_.rumble = &DeckStreamSession::listenerRumble15; + listenerCallbacks_.setMotionEventState = &DeckStreamSession::listenerSetMotionEventState15; + listenerCallbacks_.setControllerLED = &DeckStreamSession::listenerSetControllerLed15; + break; + default: + break; + } +} + +DeckStreamSession::DeckStreamSession( + DeckStreamRenderer& renderer, + DeckStreamAudio& audio, + DeckStreamInput& input, + DeckStreamSessionEvents& events) + : renderer_(renderer) + , audio_(audio) + , input_(input) + , events_(events) { + LiInitializeStreamConfiguration(&streamConfig_); + LiInitializeConnectionCallbacks(&listenerCallbacks_); + LiInitializeVideoCallbacks(&videoCallbacks_); + LiInitializeAudioCallbacks(&audioCallbacks_); + + callbackContext_ = nextOpaqueCallbackContext(); + + audioCallbacks_.capabilities = CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION; + installCallbackThunks(); + streamConfig_.streamingRemotely = STREAM_CFG_AUTO; + streamConfig_.audioConfiguration = AUDIO_CONFIGURATION_STEREO; + streamConfig_.supportedVideoFormats = VIDEO_FORMAT_H264; + + moonlightBoundary_ = DeckMoonlightBoundary{ + .listenerCallbacks = &listenerCallbacks_, + .videoCallbacks = &videoCallbacks_, + .audioCallbacks = &audioCallbacks_, + .streamConfig = &streamConfig_, + .callbackContext = callbackContext_, + .networkStartAllowed = false, + }; + +} + +DeckStreamSession::~DeckStreamSession() { + clearCallbackOwner(*this); + releaseCallbackSlot(callbackSlot_); + callbackSlot_ = kInvalidCallbackSlot; +} + +DeckStreamSessionState DeckStreamSession::state() const { + return state_; +} + +const DeckMoonlightBoundary& DeckStreamSession::moonlightBoundary() const { + return moonlightBoundary_; +} + +DeckStreamTransition DeckStreamSession::prepare(const DeckStreamRequest& request) { + if (state_ != DeckStreamSessionState::Idle && state_ != DeckStreamSessionState::Stopped) { + return fail("prepare requested while stream session is not idle"); + } + if (!isValidStreamRequest(request)) { + return fail("invalid stream request dimensions or bitrate"); + } + if (!hasCallbackSlot()) { + return fail("no moonlight callback slot available"); + } + if (hasActiveOwnerOtherThan(*this)) { + return fail("another stream callback owner is active"); + } + + request_ = request; + setCallbackOwner(*this); + streamConfig_.width = request.width; + streamConfig_.height = request.height; + streamConfig_.fps = request.fps; + streamConfig_.bitrate = request.bitrateKbps; + streamConfig_.packetSize = 1024; + return transitionTo(DeckStreamSessionState::Preparing, "prepared no-network moonlight-common-c boundary"); +} + +DeckStreamTransition DeckStreamSession::startNoNetwork() { + if (state_ != DeckStreamSessionState::Preparing) { + return fail("start requested before prepare"); + } + + transitionTo(DeckStreamSessionState::Starting, "start requested; no-network skeleton keeps LiStartConnection disabled"); + return transitionTo(DeckStreamSessionState::Active, "active skeleton session; no sockets or host connection opened"); +} + +DeckStreamTransition DeckStreamSession::stop() { + if (state_ != DeckStreamSessionState::Starting && state_ != DeckStreamSessionState::Active) { + return fail("stop requested before active stream"); + } + + transitionTo(DeckStreamSessionState::Stopping, "stopping skeleton session; LiStopConnection not called because network never started"); + auto stopped = transitionTo(DeckStreamSessionState::Stopped, "stopped no-network skeleton session"); + clearCallbackOwner(*this); + return stopped; +} + +DeckStreamTransition DeckStreamSession::cancel(const std::string_view reason) { + auto cancelled = transitionTo(DeckStreamSessionState::Cancelled, reason.empty() ? "cancelled before network start" : reason); + clearCallbackOwner(*this); + return cancelled; +} + +DeckStreamTransition DeckStreamSession::fail(const std::string_view reason) { + auto failed = transitionTo(DeckStreamSessionState::Failed, reason.empty() ? "stream skeleton failed before network start" : reason); + clearCallbackOwner(*this); + return failed; +} + +void DeckStreamSession::noteSessionEvent(const std::string_view reason) { + events_.onSessionEvent(state_, reason); +} + +DeckStreamTransition DeckStreamSession::transitionTo( + const DeckStreamSessionState state, + const std::string_view reason, + const bool networkStarted) { + state_ = state; + events_.onSessionEvent(state_, reason); + return DeckStreamTransition{ + .state = state_, + .reason = std::string(reason), + .networkStarted = networkStarted, + }; +} + +} // namespace nova::deck::stream diff --git a/clients/deck/src/stream/deck_stream_core.h b/clients/deck/src/stream/deck_stream_core.h new file mode 100644 index 00000000..3069aafa --- /dev/null +++ b/clients/deck/src/stream/deck_stream_core.h @@ -0,0 +1,442 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace nova::deck::stream { + +enum class DeckStreamSessionState { + Idle, + Preparing, + Starting, + Active, + Failed, + Stopping, + Stopped, + Cancelled, +}; + +struct DeckStreamRequest { + std::string hostId; + std::string gameId; + int width = 1280; + int height = 800; + int fps = 60; + int bitrateKbps = 20000; +}; + +struct DeckStreamTransition { + DeckStreamSessionState state = DeckStreamSessionState::Idle; + std::string reason; + bool networkStarted = false; +}; + +struct DeckMoonlightBoundary { + const CONNECTION_LISTENER_CALLBACKS* listenerCallbacks = nullptr; + const DECODER_RENDERER_CALLBACKS* videoCallbacks = nullptr; + const AUDIO_RENDERER_CALLBACKS* audioCallbacks = nullptr; + const STREAM_CONFIGURATION* streamConfig = nullptr; + void* callbackContext = nullptr; + bool networkStartAllowed = false; +}; + +class DeckStreamRenderer { +public: + virtual ~DeckStreamRenderer() = default; + virtual std::string_view adapterName() const = 0; + virtual int setup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) = 0; + virtual void start() = 0; + virtual void stop() = 0; + virtual void cleanup() = 0; + virtual int submitDecodeUnit(PDECODE_UNIT decodeUnit) = 0; +}; + +class DeckStreamAudio { +public: + virtual ~DeckStreamAudio() = default; + virtual std::string_view adapterName() const = 0; + virtual int init(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags) = 0; + virtual void start() = 0; + virtual void stop() = 0; + virtual void cleanup() = 0; + virtual void decodeAndPlaySample(char* sampleData, int sampleLength) = 0; +}; + +class DeckStreamInput { +public: + virtual ~DeckStreamInput() = default; + virtual std::string_view adapterName() const = 0; + virtual void rumble(uint16_t controllerNumber, uint16_t lowFreqMotor, uint16_t highFreqMotor) = 0; + virtual void setMotionEventState(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) = 0; + virtual void setControllerLed(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) = 0; +}; + +class DeckStreamSessionEvents { +public: + virtual ~DeckStreamSessionEvents() = default; + virtual void onSessionEvent(DeckStreamSessionState state, std::string_view reason) = 0; +}; + +class DeckStreamSession { +public: + DeckStreamSession( + DeckStreamRenderer& renderer, + DeckStreamAudio& audio, + DeckStreamInput& input, + DeckStreamSessionEvents& events); + ~DeckStreamSession(); + DeckStreamSession(const DeckStreamSession&) = delete; + DeckStreamSession& operator=(const DeckStreamSession&) = delete; + DeckStreamSession(DeckStreamSession&&) = delete; + DeckStreamSession& operator=(DeckStreamSession&&) = delete; + + DeckStreamSessionState state() const; + const DeckMoonlightBoundary& moonlightBoundary() const; + + DeckStreamTransition prepare(const DeckStreamRequest& request); + DeckStreamTransition startNoNetwork(); + DeckStreamTransition stop(); + DeckStreamTransition cancel(std::string_view reason); + DeckStreamTransition fail(std::string_view reason); + +private: + static constexpr std::size_t kInvalidCallbackSlot = static_cast(-1); + + static DeckStreamSession* ownerFromContext(void* context); + static void* contextForSlot(std::size_t slot); + static DeckStreamSession* ownerForSlot(std::size_t slot); + static bool hasActiveOwnerOtherThan(const DeckStreamSession& session); + static void setCallbackOwner(DeckStreamSession& session); + static void clearCallbackOwner(DeckStreamSession& session); + static int videoSetupForSlot(std::size_t slot, int videoFormat, int width, int height, int redrawRate, void* context, int drFlags); + static void videoStartForSlot(std::size_t slot); + static void videoStopForSlot(std::size_t slot); + static void videoCleanupForSlot(std::size_t slot); + static int videoSubmitDecodeUnitForSlot(std::size_t slot, PDECODE_UNIT decodeUnit); + static int audioInitForSlot(std::size_t slot, int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags); + static void audioStartForSlot(std::size_t slot); + static void audioStopForSlot(std::size_t slot); + static void audioCleanupForSlot(std::size_t slot); + static void audioDecodeAndPlaySampleForSlot(std::size_t slot, char* sampleData, int sampleLength); + static void listenerStageStartingForSlot(std::size_t slot, int stage); + static void listenerStageCompleteForSlot(std::size_t slot, int stage); + static void listenerStageFailedForSlot(std::size_t slot, int stage, int errorCode); + static void listenerConnectionStartedForSlot(std::size_t slot); + static void listenerConnectionTerminatedForSlot(std::size_t slot, int errorCode); + static void listenerRumbleForSlot(std::size_t slot, unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); + static void listenerSetMotionEventStateForSlot(std::size_t slot, uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz); + static void listenerSetControllerLedForSlot(std::size_t slot, uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); + void installCallbackThunks(); + bool hasCallbackSlot() const; + void noteSessionEvent(std::string_view reason); + + static int videoSetup0(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags); + static void videoStart0(); + static void videoStop0(); + static void videoCleanup0(); + static int videoSubmitDecodeUnit0(PDECODE_UNIT decodeUnit); + static int audioInit0(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags); + static void audioStart0(); + static void audioStop0(); + static void audioCleanup0(); + static void audioDecodeAndPlaySample0(char* sampleData, int sampleLength); + static void listenerStageStarting0(int stage); + static void listenerStageComplete0(int stage); + static void listenerStageFailed0(int stage, int errorCode); + static void listenerConnectionStarted0(); + static void listenerConnectionTerminated0(int errorCode); + static void listenerRumble0(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); + static void listenerSetMotionEventState0(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz); + static void listenerSetControllerLed0(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); + static int videoSetup1(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags); + static void videoStart1(); + static void videoStop1(); + static void videoCleanup1(); + static int videoSubmitDecodeUnit1(PDECODE_UNIT decodeUnit); + static int audioInit1(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags); + static void audioStart1(); + static void audioStop1(); + static void audioCleanup1(); + static void audioDecodeAndPlaySample1(char* sampleData, int sampleLength); + static void listenerStageStarting1(int stage); + static void listenerStageComplete1(int stage); + static void listenerStageFailed1(int stage, int errorCode); + static void listenerConnectionStarted1(); + static void listenerConnectionTerminated1(int errorCode); + static void listenerRumble1(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); + static void listenerSetMotionEventState1(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz); + static void listenerSetControllerLed1(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); + static int videoSetup2(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags); + static void videoStart2(); + static void videoStop2(); + static void videoCleanup2(); + static int videoSubmitDecodeUnit2(PDECODE_UNIT decodeUnit); + static int audioInit2(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags); + static void audioStart2(); + static void audioStop2(); + static void audioCleanup2(); + static void audioDecodeAndPlaySample2(char* sampleData, int sampleLength); + static void listenerStageStarting2(int stage); + static void listenerStageComplete2(int stage); + static void listenerStageFailed2(int stage, int errorCode); + static void listenerConnectionStarted2(); + static void listenerConnectionTerminated2(int errorCode); + static void listenerRumble2(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); + static void listenerSetMotionEventState2(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz); + static void listenerSetControllerLed2(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); + static int videoSetup3(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags); + static void videoStart3(); + static void videoStop3(); + static void videoCleanup3(); + static int videoSubmitDecodeUnit3(PDECODE_UNIT decodeUnit); + static int audioInit3(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags); + static void audioStart3(); + static void audioStop3(); + static void audioCleanup3(); + static void audioDecodeAndPlaySample3(char* sampleData, int sampleLength); + static void listenerStageStarting3(int stage); + static void listenerStageComplete3(int stage); + static void listenerStageFailed3(int stage, int errorCode); + static void listenerConnectionStarted3(); + static void listenerConnectionTerminated3(int errorCode); + static void listenerRumble3(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); + static void listenerSetMotionEventState3(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz); + static void listenerSetControllerLed3(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); + static int videoSetup4(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags); + static void videoStart4(); + static void videoStop4(); + static void videoCleanup4(); + static int videoSubmitDecodeUnit4(PDECODE_UNIT decodeUnit); + static int audioInit4(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags); + static void audioStart4(); + static void audioStop4(); + static void audioCleanup4(); + static void audioDecodeAndPlaySample4(char* sampleData, int sampleLength); + static void listenerStageStarting4(int stage); + static void listenerStageComplete4(int stage); + static void listenerStageFailed4(int stage, int errorCode); + static void listenerConnectionStarted4(); + static void listenerConnectionTerminated4(int errorCode); + static void listenerRumble4(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); + static void listenerSetMotionEventState4(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz); + static void listenerSetControllerLed4(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); + static int videoSetup5(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags); + static void videoStart5(); + static void videoStop5(); + static void videoCleanup5(); + static int videoSubmitDecodeUnit5(PDECODE_UNIT decodeUnit); + static int audioInit5(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags); + static void audioStart5(); + static void audioStop5(); + static void audioCleanup5(); + static void audioDecodeAndPlaySample5(char* sampleData, int sampleLength); + static void listenerStageStarting5(int stage); + static void listenerStageComplete5(int stage); + static void listenerStageFailed5(int stage, int errorCode); + static void listenerConnectionStarted5(); + static void listenerConnectionTerminated5(int errorCode); + static void listenerRumble5(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); + static void listenerSetMotionEventState5(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz); + static void listenerSetControllerLed5(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); + static int videoSetup6(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags); + static void videoStart6(); + static void videoStop6(); + static void videoCleanup6(); + static int videoSubmitDecodeUnit6(PDECODE_UNIT decodeUnit); + static int audioInit6(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags); + static void audioStart6(); + static void audioStop6(); + static void audioCleanup6(); + static void audioDecodeAndPlaySample6(char* sampleData, int sampleLength); + static void listenerStageStarting6(int stage); + static void listenerStageComplete6(int stage); + static void listenerStageFailed6(int stage, int errorCode); + static void listenerConnectionStarted6(); + static void listenerConnectionTerminated6(int errorCode); + static void listenerRumble6(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); + static void listenerSetMotionEventState6(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz); + static void listenerSetControllerLed6(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); + static int videoSetup7(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags); + static void videoStart7(); + static void videoStop7(); + static void videoCleanup7(); + static int videoSubmitDecodeUnit7(PDECODE_UNIT decodeUnit); + static int audioInit7(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags); + static void audioStart7(); + static void audioStop7(); + static void audioCleanup7(); + static void audioDecodeAndPlaySample7(char* sampleData, int sampleLength); + static void listenerStageStarting7(int stage); + static void listenerStageComplete7(int stage); + static void listenerStageFailed7(int stage, int errorCode); + static void listenerConnectionStarted7(); + static void listenerConnectionTerminated7(int errorCode); + static void listenerRumble7(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); + static void listenerSetMotionEventState7(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz); + static void listenerSetControllerLed7(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); + static int videoSetup8(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags); + static void videoStart8(); + static void videoStop8(); + static void videoCleanup8(); + static int videoSubmitDecodeUnit8(PDECODE_UNIT decodeUnit); + static int audioInit8(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags); + static void audioStart8(); + static void audioStop8(); + static void audioCleanup8(); + static void audioDecodeAndPlaySample8(char* sampleData, int sampleLength); + static void listenerStageStarting8(int stage); + static void listenerStageComplete8(int stage); + static void listenerStageFailed8(int stage, int errorCode); + static void listenerConnectionStarted8(); + static void listenerConnectionTerminated8(int errorCode); + static void listenerRumble8(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); + static void listenerSetMotionEventState8(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz); + static void listenerSetControllerLed8(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); + static int videoSetup9(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags); + static void videoStart9(); + static void videoStop9(); + static void videoCleanup9(); + static int videoSubmitDecodeUnit9(PDECODE_UNIT decodeUnit); + static int audioInit9(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags); + static void audioStart9(); + static void audioStop9(); + static void audioCleanup9(); + static void audioDecodeAndPlaySample9(char* sampleData, int sampleLength); + static void listenerStageStarting9(int stage); + static void listenerStageComplete9(int stage); + static void listenerStageFailed9(int stage, int errorCode); + static void listenerConnectionStarted9(); + static void listenerConnectionTerminated9(int errorCode); + static void listenerRumble9(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); + static void listenerSetMotionEventState9(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz); + static void listenerSetControllerLed9(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); + static int videoSetup10(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags); + static void videoStart10(); + static void videoStop10(); + static void videoCleanup10(); + static int videoSubmitDecodeUnit10(PDECODE_UNIT decodeUnit); + static int audioInit10(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags); + static void audioStart10(); + static void audioStop10(); + static void audioCleanup10(); + static void audioDecodeAndPlaySample10(char* sampleData, int sampleLength); + static void listenerStageStarting10(int stage); + static void listenerStageComplete10(int stage); + static void listenerStageFailed10(int stage, int errorCode); + static void listenerConnectionStarted10(); + static void listenerConnectionTerminated10(int errorCode); + static void listenerRumble10(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); + static void listenerSetMotionEventState10(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz); + static void listenerSetControllerLed10(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); + static int videoSetup11(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags); + static void videoStart11(); + static void videoStop11(); + static void videoCleanup11(); + static int videoSubmitDecodeUnit11(PDECODE_UNIT decodeUnit); + static int audioInit11(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags); + static void audioStart11(); + static void audioStop11(); + static void audioCleanup11(); + static void audioDecodeAndPlaySample11(char* sampleData, int sampleLength); + static void listenerStageStarting11(int stage); + static void listenerStageComplete11(int stage); + static void listenerStageFailed11(int stage, int errorCode); + static void listenerConnectionStarted11(); + static void listenerConnectionTerminated11(int errorCode); + static void listenerRumble11(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); + static void listenerSetMotionEventState11(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz); + static void listenerSetControllerLed11(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); + static int videoSetup12(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags); + static void videoStart12(); + static void videoStop12(); + static void videoCleanup12(); + static int videoSubmitDecodeUnit12(PDECODE_UNIT decodeUnit); + static int audioInit12(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags); + static void audioStart12(); + static void audioStop12(); + static void audioCleanup12(); + static void audioDecodeAndPlaySample12(char* sampleData, int sampleLength); + static void listenerStageStarting12(int stage); + static void listenerStageComplete12(int stage); + static void listenerStageFailed12(int stage, int errorCode); + static void listenerConnectionStarted12(); + static void listenerConnectionTerminated12(int errorCode); + static void listenerRumble12(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); + static void listenerSetMotionEventState12(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz); + static void listenerSetControllerLed12(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); + static int videoSetup13(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags); + static void videoStart13(); + static void videoStop13(); + static void videoCleanup13(); + static int videoSubmitDecodeUnit13(PDECODE_UNIT decodeUnit); + static int audioInit13(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags); + static void audioStart13(); + static void audioStop13(); + static void audioCleanup13(); + static void audioDecodeAndPlaySample13(char* sampleData, int sampleLength); + static void listenerStageStarting13(int stage); + static void listenerStageComplete13(int stage); + static void listenerStageFailed13(int stage, int errorCode); + static void listenerConnectionStarted13(); + static void listenerConnectionTerminated13(int errorCode); + static void listenerRumble13(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); + static void listenerSetMotionEventState13(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz); + static void listenerSetControllerLed13(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); + static int videoSetup14(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags); + static void videoStart14(); + static void videoStop14(); + static void videoCleanup14(); + static int videoSubmitDecodeUnit14(PDECODE_UNIT decodeUnit); + static int audioInit14(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags); + static void audioStart14(); + static void audioStop14(); + static void audioCleanup14(); + static void audioDecodeAndPlaySample14(char* sampleData, int sampleLength); + static void listenerStageStarting14(int stage); + static void listenerStageComplete14(int stage); + static void listenerStageFailed14(int stage, int errorCode); + static void listenerConnectionStarted14(); + static void listenerConnectionTerminated14(int errorCode); + static void listenerRumble14(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); + static void listenerSetMotionEventState14(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz); + static void listenerSetControllerLed14(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); + static int videoSetup15(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags); + static void videoStart15(); + static void videoStop15(); + static void videoCleanup15(); + static int videoSubmitDecodeUnit15(PDECODE_UNIT decodeUnit); + static int audioInit15(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags); + static void audioStart15(); + static void audioStop15(); + static void audioCleanup15(); + static void audioDecodeAndPlaySample15(char* sampleData, int sampleLength); + static void listenerStageStarting15(int stage); + static void listenerStageComplete15(int stage); + static void listenerStageFailed15(int stage, int errorCode); + static void listenerConnectionStarted15(); + static void listenerConnectionTerminated15(int errorCode); + static void listenerRumble15(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor); + static void listenerSetMotionEventState15(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz); + static void listenerSetControllerLed15(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b); + DeckStreamTransition transitionTo(DeckStreamSessionState state, std::string_view reason, bool networkStarted = false); + + DeckStreamRenderer& renderer_; + DeckStreamAudio& audio_; + DeckStreamInput& input_; + DeckStreamSessionEvents& events_; + DeckStreamSessionState state_ = DeckStreamSessionState::Idle; + DeckStreamRequest request_; + std::size_t callbackSlot_ = kInvalidCallbackSlot; + void* callbackContext_ = nullptr; + STREAM_CONFIGURATION streamConfig_{}; + CONNECTION_LISTENER_CALLBACKS listenerCallbacks_{}; + DECODER_RENDERER_CALLBACKS videoCallbacks_{}; + AUDIO_RENDERER_CALLBACKS audioCallbacks_{}; + DeckMoonlightBoundary moonlightBoundary_{}; +}; + +} // namespace nova::deck::stream diff --git a/clients/deck/src/stream/deck_stream_media_adapters.cpp b/clients/deck/src/stream/deck_stream_media_adapters.cpp new file mode 100644 index 00000000..b678e29c --- /dev/null +++ b/clients/deck/src/stream/deck_stream_media_adapters.cpp @@ -0,0 +1,317 @@ +#include "stream/deck_stream_media_adapters.h" + +extern "C" { +#include +#include +#include +#include +#include +#include +#include +#include +#include +} + +#include + +#include +#include +#include +#include +#include + +namespace nova::deck::stream { + +namespace { + +std::string ffmpegErrorString(const int errorCode) { + char buffer[AV_ERROR_MAX_STRING_SIZE] = {}; + av_strerror(errorCode, buffer, sizeof(buffer)); + return buffer; +} + +AVPixelFormat vaapiHardwareFormatCallback(AVCodecContext*, const AVPixelFormat* pixelFormats) { + for (const AVPixelFormat* format = pixelFormats; format != nullptr && *format != AV_PIX_FMT_NONE; ++format) { + if (*format == AV_PIX_FMT_VAAPI) { + return *format; + } + } + return AV_PIX_FMT_NONE; +} + +std::vector copyDecodeUnitBytes(PDECODE_UNIT decodeUnit) { + std::vector bytes; + if (decodeUnit == nullptr || decodeUnit->fullLength <= 0 || decodeUnit->bufferList == nullptr) { + return bytes; + } + bytes.reserve(static_cast(decodeUnit->fullLength)); + for (PLENTRY entry = decodeUnit->bufferList; entry != nullptr; entry = entry->next) { + if (entry->data == nullptr || entry->length <= 0) { + return {}; + } + const auto* first = reinterpret_cast(entry->data); + bytes.insert(bytes.end(), first, first + entry->length); + } + if (bytes.size() != static_cast(decodeUnit->fullLength)) { + return {}; + } + return bytes; +} + +} // namespace + +DeckLinuxMediaProbe DeckLinuxMediaProbe::detect() { + const AVHWDeviceType vaapiType = av_hwdevice_find_type_by_name("vaapi"); + AVBufferRef* hardwareDevice = nullptr; + const int priorLogLevel = av_log_get_level(); + av_log_set_level(AV_LOG_QUIET); + const int hardwareDeviceResult = av_hwdevice_ctx_create(&hardwareDevice, AV_HWDEVICE_TYPE_VAAPI, nullptr, nullptr, 0); + av_log_set_level(priorLogLevel); + if (hardwareDevice != nullptr) { + av_buffer_unref(&hardwareDevice); + } + return DeckLinuxMediaProbe{ + .ffmpegLibavcodecHeadersLinked = avcodec_version() > 0, + .ffmpegLibavutilHeadersLinked = avutil_version() > 0, + .vaapiHeadersLinked = VA_MAJOR_VERSION >= 1, + .qtQuickRhiPresentationBoundary = QQuickItem::staticMetaObject.className() != nullptr, + .h264DecoderAvailable = avcodec_find_decoder(AV_CODEC_ID_H264) != nullptr, + .runtimeVaapiDeviceAvailable = hardwareDeviceResult == 0, + .hardwareDeviceTypeName = vaapiType == AV_HWDEVICE_TYPE_VAAPI ? "vaapi" : "missing-vaapi", + .runtimeStatus = hardwareDeviceResult == 0 ? "vaapi runtime device opened" : "av_hwdevice_ctx_create(VAAPI) failed: " + ffmpegErrorString(hardwareDeviceResult), + }; +} + +std::string_view DeckVaapiFfmpegRenderer::adapterName() const { + return "ffmpeg-vaapi-h264-qt-rhi-prototype"; +} + +DeckVaapiFfmpegRenderer::~DeckVaapiFfmpegRenderer() { + resetDecoder(); +} + +int DeckVaapiFfmpegRenderer::setup( + const int videoFormat, + const int width, + const int height, + const int redrawRate, + void* context, + const int drFlags) { + (void)context; + (void)drFlags; + ++lifecycle_.setupCalls; + lifecycle_.videoFormat = videoFormat; + lifecycle_.width = width; + lifecycle_.height = height; + lifecycle_.redrawRate = redrawRate; + lifecycle_.networkStartAllowed = false; + lifecycle_.decodedHardwareFrames = 0; + lifecycle_.lastFrameWasHardwareBacked = false; + lifecycle_.lastRuntimeError.clear(); + resetDecoder(); + + const DeckLinuxMediaProbe probe = DeckLinuxMediaProbe::detect(); + lifecycle_.runtimeVaapiDeviceAvailable = probe.runtimeVaapiDeviceAvailable; + lifecycle_.runtimeStatus = probe.runtimeStatus; + if (videoFormat != VIDEO_FORMAT_H264) { + lifecycle_.lastRuntimeError = "unsupported video format for Deck FFmpeg VA-API renderer"; + return DR_NEED_IDR; + } + + int result = av_hwdevice_ctx_create(&hardwareDevice_, AV_HWDEVICE_TYPE_VAAPI, nullptr, nullptr, 0); + if (result < 0) { + lifecycle_.lastRuntimeError = "av_hwdevice_ctx_create(VAAPI) failed: " + ffmpegErrorString(result); + lifecycle_.runtimeStatus = lifecycle_.lastRuntimeError; + resetDecoder(); + return DR_NEED_IDR; + } + lifecycle_.ownsHardwareDevice = true; + lifecycle_.runtimeVaapiDeviceAvailable = true; + lifecycle_.runtimeStatus = "vaapi runtime device opened and owned"; + + const AVCodec* codec = avcodec_find_decoder(AV_CODEC_ID_H264); + if (codec == nullptr) { + lifecycle_.lastRuntimeError = "FFmpeg H.264 decoder unavailable"; + resetDecoder(); + return DR_NEED_IDR; + } + + codecContext_ = avcodec_alloc_context3(codec); + if (codecContext_ == nullptr) { + lifecycle_.lastRuntimeError = "avcodec_alloc_context3(H.264) failed"; + resetDecoder(); + return DR_NEED_IDR; + } + codecContext_->width = width; + codecContext_->height = height; + codecContext_->get_format = vaapiHardwareFormatCallback; + codecContext_->hw_device_ctx = av_buffer_ref(hardwareDevice_); + if (codecContext_->hw_device_ctx == nullptr) { + lifecycle_.lastRuntimeError = "av_buffer_ref(VAAPI device) failed"; + resetDecoder(); + return DR_NEED_IDR; + } + + result = avcodec_open2(codecContext_, codec, nullptr); + if (result < 0) { + lifecycle_.lastRuntimeError = "avcodec_open2(H.264 VA-API) failed: " + ffmpegErrorString(result); + resetDecoder(); + return DR_NEED_IDR; + } + + decodedFrame_ = av_frame_alloc(); + if (decodedFrame_ == nullptr) { + lifecycle_.lastRuntimeError = "av_frame_alloc() failed"; + resetDecoder(); + return DR_NEED_IDR; + } + + lifecycle_.ownsCodecContext = true; + ready_ = true; + return DR_OK; +} + +void DeckVaapiFfmpegRenderer::start() { + ++lifecycle_.startCalls; +} + +void DeckVaapiFfmpegRenderer::stop() { + ++lifecycle_.stopCalls; +} + +void DeckVaapiFfmpegRenderer::cleanup() { + ++lifecycle_.cleanupCalls; + resetDecoder(); +} + +int DeckVaapiFfmpegRenderer::submitDecodeUnit(PDECODE_UNIT decodeUnit) { + ++lifecycle_.submitCalls; + lifecycle_.lastFrameWasHardwareBacked = false; + if (!ready_ || decodeUnit == nullptr) { + lifecycle_.acceptedNullDecodeUnit = false; + return DR_NEED_IDR; + } + + const std::vector bytes = copyDecodeUnitBytes(decodeUnit); + if (bytes.empty()) { + lifecycle_.lastRuntimeError = "decode unit did not contain Annex-B H.264 bytes"; + return DR_NEED_IDR; + } + + AVPacket* packet = av_packet_alloc(); + if (packet == nullptr) { + lifecycle_.lastRuntimeError = "av_packet_alloc() failed"; + return DR_NEED_IDR; + } + int result = av_new_packet(packet, static_cast(bytes.size())); + if (result < 0) { + lifecycle_.lastRuntimeError = "av_new_packet() failed: " + ffmpegErrorString(result); + av_packet_free(&packet); + return DR_NEED_IDR; + } + std::copy(bytes.begin(), bytes.end(), packet->data); + + result = avcodec_send_packet(codecContext_, packet); + av_packet_free(&packet); + if (result < 0) { + lifecycle_.lastRuntimeError = "avcodec_send_packet() failed: " + ffmpegErrorString(result); + return DR_NEED_IDR; + } + + while ((result = avcodec_receive_frame(codecContext_, decodedFrame_)) == 0) { + const bool hardwareBacked = decodedFrame_->format == AV_PIX_FMT_VAAPI; + lifecycle_.lastFrameWasHardwareBacked = hardwareBacked; + av_frame_unref(decodedFrame_); + if (hardwareBacked) { + ++lifecycle_.decodedHardwareFrames; + return DR_OK; + } + } + + if (result == AVERROR(EAGAIN)) { + lifecycle_.lastRuntimeError = "H.264 packet accepted but no VA-API hardware frame was ready"; + } else if (result == AVERROR_EOF) { + lifecycle_.lastRuntimeError = "H.264 decoder reached EOF before a VA-API hardware frame"; + } else { + lifecycle_.lastRuntimeError = "avcodec_receive_frame() failed: " + ffmpegErrorString(result); + } + return DR_NEED_IDR; +} + +const DeckRendererLifecycle& DeckVaapiFfmpegRenderer::lifecycle() const { + return lifecycle_; +} + +void DeckVaapiFfmpegRenderer::resetDecoder() { + ready_ = false; + if (decodedFrame_ != nullptr) { + av_frame_free(&decodedFrame_); + } + if (codecContext_ != nullptr) { + avcodec_free_context(&codecContext_); + } + if (hardwareDevice_ != nullptr) { + av_buffer_unref(&hardwareDevice_); + } + lifecycle_.ownsHardwareDevice = hardwareDevice_ != nullptr; + lifecycle_.ownsCodecContext = codecContext_ != nullptr; +} + +DeckLinuxAudioProbe DeckLinuxAudioProbe::detect() { + const char* pipeWireVersion = pw_get_headers_version(); + const char* pulseVersion = pa_get_headers_version(); + return DeckLinuxAudioProbe{ + .pipeWireHeadersLinked = pipeWireVersion != nullptr, + .pulseFallbackHeadersLinked = pulseVersion != nullptr, + .pipeWireHeaderVersion = pipeWireVersion == nullptr ? "" : pipeWireVersion, + .pulseHeaderVersion = pulseVersion == nullptr ? "" : pulseVersion, + }; +} + +std::string_view DeckPipeWireAudio::adapterName() const { + return "pipewire-pcm-pulse-fallback-prototype"; +} + +int DeckPipeWireAudio::init( + const int audioConfiguration, + POPUS_MULTISTREAM_CONFIGURATION opusConfig, + void* context, + const int arFlags) { + (void)context; + (void)arFlags; + ++lifecycle_.initCalls; + lifecycle_.audioConfiguration = audioConfiguration; + lifecycle_.samplesPerFrame = opusConfig == nullptr ? 0 : opusConfig->samplesPerFrame; + lifecycle_.networkStartAllowed = false; + + const DeckLinuxAudioProbe probe = DeckLinuxAudioProbe::detect(); + ready_ = probe.pipeWireHeadersLinked && audioConfiguration != 0 && opusConfig != nullptr && opusConfig->samplesPerFrame > 0; + return ready_ ? 0 : -1; +} + +void DeckPipeWireAudio::start() { + ++lifecycle_.startCalls; +} + +void DeckPipeWireAudio::stop() { + ++lifecycle_.stopCalls; +} + +void DeckPipeWireAudio::cleanup() { + ++lifecycle_.cleanupCalls; + ready_ = false; +} + +void DeckPipeWireAudio::decodeAndPlaySample(char* sampleData, const int sampleLength) { + if (!ready_ || sampleData == nullptr || sampleLength <= 0) { + return; + } + ++lifecycle_.sampleCalls; + lifecycle_.lastSampleLength = sampleLength; +} + +const DeckAudioLifecycle& DeckPipeWireAudio::lifecycle() const { + return lifecycle_; +} + +} // namespace nova::deck::stream diff --git a/clients/deck/src/stream/deck_stream_media_adapters.h b/clients/deck/src/stream/deck_stream_media_adapters.h new file mode 100644 index 00000000..175aa85a --- /dev/null +++ b/clients/deck/src/stream/deck_stream_media_adapters.h @@ -0,0 +1,113 @@ +#pragma once + +#include "stream/deck_stream_core.h" + +#include +#include + +struct AVBufferRef; +struct AVCodecContext; +struct AVFrame; + +namespace nova::deck::stream { + +struct DeckLinuxMediaProbe { + bool ffmpegLibavcodecHeadersLinked = false; + bool ffmpegLibavutilHeadersLinked = false; + bool vaapiHeadersLinked = false; + bool qtQuickRhiPresentationBoundary = false; + bool h264DecoderAvailable = false; + bool runtimeVaapiDeviceAvailable = false; + std::string hardwareDeviceTypeName; + std::string runtimeStatus; + + static DeckLinuxMediaProbe detect(); +}; + +struct DeckRendererLifecycle { + int setupCalls = 0; + int startCalls = 0; + int submitCalls = 0; + int stopCalls = 0; + int cleanupCalls = 0; + bool acceptedNullDecodeUnit = false; + bool networkStartAllowed = false; + bool runtimeVaapiDeviceAvailable = false; + bool ownsHardwareDevice = false; + bool ownsCodecContext = false; + int decodedHardwareFrames = 0; + bool lastFrameWasHardwareBacked = false; + std::string runtimeStatus; + std::string lastRuntimeError; + int width = 0; + int height = 0; + int redrawRate = 0; + int videoFormat = 0; +}; + +class DeckVaapiFfmpegRenderer final : public DeckStreamRenderer { +public: + ~DeckVaapiFfmpegRenderer() override; + DeckVaapiFfmpegRenderer() = default; + DeckVaapiFfmpegRenderer(const DeckVaapiFfmpegRenderer&) = delete; + DeckVaapiFfmpegRenderer& operator=(const DeckVaapiFfmpegRenderer&) = delete; + DeckVaapiFfmpegRenderer(DeckVaapiFfmpegRenderer&&) = delete; + DeckVaapiFfmpegRenderer& operator=(DeckVaapiFfmpegRenderer&&) = delete; + + std::string_view adapterName() const override; + int setup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) override; + void start() override; + void stop() override; + void cleanup() override; + int submitDecodeUnit(PDECODE_UNIT decodeUnit) override; + + const DeckRendererLifecycle& lifecycle() const; + +private: + void resetDecoder(); + + DeckRendererLifecycle lifecycle_{}; + bool ready_ = false; + AVBufferRef* hardwareDevice_ = nullptr; + AVCodecContext* codecContext_ = nullptr; + AVFrame* decodedFrame_ = nullptr; +}; + +struct DeckLinuxAudioProbe { + bool pipeWireHeadersLinked = false; + bool pulseFallbackHeadersLinked = false; + std::string pipeWireHeaderVersion; + std::string pulseHeaderVersion; + + static DeckLinuxAudioProbe detect(); +}; + +struct DeckAudioLifecycle { + int initCalls = 0; + int startCalls = 0; + int sampleCalls = 0; + int stopCalls = 0; + int cleanupCalls = 0; + int audioConfiguration = 0; + int samplesPerFrame = 0; + int lastSampleLength = 0; + bool networkStartAllowed = false; +}; + +class DeckPipeWireAudio final : public DeckStreamAudio { +public: + std::string_view adapterName() const override; + int init(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int arFlags) override; + void start() override; + void stop() override; + void cleanup() override; + void decodeAndPlaySample(char* sampleData, int sampleLength) override; + + const DeckAudioLifecycle& lifecycle() const; + +private: + DeckAudioLifecycle lifecycle_{}; + bool ready_ = false; +}; + +} // namespace nova::deck::stream diff --git a/clients/deck/tests/deck_stream_core_test.cpp b/clients/deck/tests/deck_stream_core_test.cpp new file mode 100644 index 00000000..1dee0c39 --- /dev/null +++ b/clients/deck/tests/deck_stream_core_test.cpp @@ -0,0 +1,392 @@ +#include "stream/deck_stream_core.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace { + +using nova::deck::stream::DeckStreamRequest; +using nova::deck::stream::DeckStreamSession; +using nova::deck::stream::DeckStreamSessionState; + +struct RendererCall { + int videoFormat = 0; + int width = 0; + int height = 0; + int redrawRate = 0; + int flags = 0; + + bool operator==(const RendererCall&) const = default; +}; + +struct AudioCall { + int audioConfiguration = 0; + int flags = 0; + + bool operator==(const AudioCall&) const = default; +}; + +struct InputCall { + uint16_t controllerNumber = 0; + uint16_t first = 0; + uint16_t second = 0; + uint16_t third = 0; + + bool operator==(const InputCall&) const = default; +}; + +class RecordingEvents final : public nova::deck::stream::DeckStreamSessionEvents { +public: + void onSessionEvent(DeckStreamSessionState state, std::string_view reason) override { + states.push_back(state); + reasons.emplace_back(reason); + } + + std::vector states; + std::vector reasons; +}; + +class StubRenderer final : public nova::deck::stream::DeckStreamRenderer { +public: + std::string_view adapterName() const override { return "stub-renderer"; } + + int setup(int videoFormat, int width, int height, int redrawRate, void* context, int flags) override { + setupCalls.push_back(RendererCall{videoFormat, width, height, redrawRate, flags}); + contexts.push_back(context); + return setupResult; + } + + void start() override { ++startCalls; } + void stop() override { ++stopCalls; } + void cleanup() override { ++cleanupCalls; } + int submitDecodeUnit(PDECODE_UNIT decodeUnit) override { + decodeUnits.push_back(decodeUnit); + return DR_OK; + } + + int setupResult = 0; + int startCalls = 0; + int stopCalls = 0; + int cleanupCalls = 0; + std::vector setupCalls; + std::vector contexts; + std::vector decodeUnits; +}; + +class StubAudio final : public nova::deck::stream::DeckStreamAudio { +public: + std::string_view adapterName() const override { return "stub-audio"; } + + int init(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags) override { + initCalls.push_back(AudioCall{audioConfiguration, flags}); + opusConfigs.push_back(opusConfig); + contexts.push_back(context); + return initResult; + } + + void start() override { ++startCalls; } + void stop() override { ++stopCalls; } + void cleanup() override { ++cleanupCalls; } + void decodeAndPlaySample(char* sampleData, int sampleLength) override { + samples.push_back(sampleData); + sampleLengths.push_back(sampleLength); + } + + int initResult = 0; + int startCalls = 0; + int stopCalls = 0; + int cleanupCalls = 0; + std::vector initCalls; + std::vector opusConfigs; + std::vector contexts; + std::vector samples; + std::vector sampleLengths; +}; + +class StubInput final : public nova::deck::stream::DeckStreamInput { +public: + std::string_view adapterName() const override { return "stub-input"; } + + void rumble(uint16_t controllerNumber, uint16_t lowFreqMotor, uint16_t highFreqMotor) override { + rumbles.push_back(InputCall{controllerNumber, lowFreqMotor, highFreqMotor, 0}); + } + + void setMotionEventState(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) override { + motionStates.push_back(InputCall{controllerNumber, motionType, reportRateHz, 0}); + } + + void setControllerLed(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) override { + leds.push_back(InputCall{controllerNumber, r, g, b}); + } + + std::vector rumbles; + std::vector motionStates; + std::vector leds; +}; + +DeckStreamRequest validRequest(std::string gameId = "game-123") { + return DeckStreamRequest{ + .hostId = "host-gaming-pc", + .gameId = std::move(gameId), + .width = 1280, + .height = 800, + .fps = 60, + .bitrateKbps = 20000, + }; +} + +} // namespace + +static_assert(!std::is_copy_constructible_v); +static_assert(!std::is_copy_assignable_v); +static_assert(!std::is_move_constructible_v); +static_assert(!std::is_move_assignable_v); + +int main() { + STREAM_CONFIGURATION streamConfig; + LiInitializeStreamConfiguration(&streamConfig); + assert(streamConfig.width == 0); + assert((VIDEO_FORMAT_H264 & VIDEO_FORMAT_MASK_H264) != 0); + + DECODER_RENDERER_CALLBACKS videoCallbacks; + LiInitializeVideoCallbacks(&videoCallbacks); + AUDIO_RENDERER_CALLBACKS audioCallbacks; + LiInitializeAudioCallbacks(&audioCallbacks); + CONNECTION_LISTENER_CALLBACKS listenerCallbacks; + LiInitializeConnectionCallbacks(&listenerCallbacks); + + assert(videoCallbacks.setup == nullptr); + assert(audioCallbacks.init == nullptr); + assert(listenerCallbacks.stageStarting == nullptr); + + StubRenderer renderer; + StubAudio audio; + StubInput input; + RecordingEvents events; + + DeckStreamSession session(renderer, audio, input, events); + assert(session.state() == DeckStreamSessionState::Idle); + assert(session.moonlightBoundary().videoCallbacks != nullptr); + assert(session.moonlightBoundary().audioCallbacks != nullptr); + assert(session.moonlightBoundary().listenerCallbacks != nullptr); + assert(session.moonlightBoundary().callbackContext != nullptr); + assert(!session.moonlightBoundary().networkStartAllowed); + + const auto* sessionVideoCallbacks = session.moonlightBoundary().videoCallbacks; + const auto* sessionAudioCallbacks = session.moonlightBoundary().audioCallbacks; + const auto* sessionListenerCallbacks = session.moonlightBoundary().listenerCallbacks; + void* callbackContext = session.moonlightBoundary().callbackContext; + assert(sessionVideoCallbacks->setup != nullptr); + assert(sessionVideoCallbacks->start != nullptr); + assert(sessionVideoCallbacks->stop != nullptr); + assert(sessionVideoCallbacks->cleanup != nullptr); + assert(sessionVideoCallbacks->submitDecodeUnit != nullptr); + assert(sessionAudioCallbacks->init != nullptr); + assert(sessionAudioCallbacks->start != nullptr); + assert(sessionAudioCallbacks->stop != nullptr); + assert(sessionAudioCallbacks->cleanup != nullptr); + assert(sessionAudioCallbacks->decodeAndPlaySample != nullptr); + assert(sessionListenerCallbacks->stageStarting != nullptr); + assert(sessionListenerCallbacks->stageComplete != nullptr); + assert(sessionListenerCallbacks->stageFailed != nullptr); + assert(sessionListenerCallbacks->connectionStarted != nullptr); + assert(sessionListenerCallbacks->connectionTerminated != nullptr); + assert(sessionListenerCallbacks->rumble != nullptr); + assert(sessionListenerCallbacks->setMotionEventState != nullptr); + assert(sessionListenerCallbacks->setControllerLED != nullptr); + + const auto prepared = session.prepare(validRequest()); + assert(prepared.state == DeckStreamSessionState::Preparing); + assert(!prepared.networkStarted); + assert(session.state() == DeckStreamSessionState::Preparing); + + assert(sessionVideoCallbacks->setup(VIDEO_FORMAT_H264, 1280, 800, 60, callbackContext, 0) == 0); + sessionVideoCallbacks->start(); + assert(sessionVideoCallbacks->submitDecodeUnit(nullptr) == DR_OK); + sessionVideoCallbacks->stop(); + sessionVideoCallbacks->cleanup(); + assert(renderer.setupCalls.size() == 1); + assert(renderer.setupCalls[0].width == 1280); + assert((renderer.contexts == std::vector{callbackContext})); + assert(renderer.startCalls == 1); + assert(renderer.stopCalls == 1); + assert(renderer.cleanupCalls == 1); + assert(renderer.decodeUnits == std::vector{nullptr}); + + OPUS_MULTISTREAM_CONFIGURATION opusConfig{}; + assert(sessionAudioCallbacks->init(AUDIO_CONFIGURATION_STEREO, &opusConfig, callbackContext, 0) == 0); + sessionAudioCallbacks->start(); + char sample[] = {'n', 'o', 'v', 'a'}; + sessionAudioCallbacks->decodeAndPlaySample(sample, 4); + sessionAudioCallbacks->stop(); + sessionAudioCallbacks->cleanup(); + assert(audio.initCalls.size() == 1); + assert(audio.initCalls[0].audioConfiguration == AUDIO_CONFIGURATION_STEREO); + assert((audio.opusConfigs == std::vector{&opusConfig})); + assert((audio.contexts == std::vector{callbackContext})); + assert(audio.startCalls == 1); + assert(audio.stopCalls == 1); + assert(audio.cleanupCalls == 1); + assert(audio.samples == std::vector{sample}); + assert(audio.sampleLengths == std::vector{4}); + + sessionListenerCallbacks->stageStarting(STAGE_VIDEO_STREAM_INIT); + sessionListenerCallbacks->stageComplete(STAGE_VIDEO_STREAM_INIT); + sessionListenerCallbacks->connectionStarted(); + sessionListenerCallbacks->rumble(0, 100, 200); + sessionListenerCallbacks->setMotionEventState(0, 1, 120); + sessionListenerCallbacks->setControllerLED(0, 8, 16, 32); + sessionListenerCallbacks->connectionTerminated(ML_ERROR_GRACEFUL_TERMINATION); + assert((input.rumbles == std::vector{InputCall{0, 100, 200, 0}})); + assert((input.motionStates == std::vector{InputCall{0, 1, 120, 0}})); + assert((input.leds == std::vector{InputCall{0, 8, 16, 32}})); + + const auto started = session.startNoNetwork(); + assert(started.state == DeckStreamSessionState::Active); + assert(!started.networkStarted); + assert(session.state() == DeckStreamSessionState::Active); + + const auto stopped = session.stop(); + assert(stopped.state == DeckStreamSessionState::Stopped); + assert(session.state() == DeckStreamSessionState::Stopped); + + const std::vector expectedMainSequence{ + DeckStreamSessionState::Preparing, + DeckStreamSessionState::Preparing, + DeckStreamSessionState::Preparing, + DeckStreamSessionState::Preparing, + DeckStreamSessionState::Preparing, + DeckStreamSessionState::Starting, + DeckStreamSessionState::Active, + DeckStreamSessionState::Stopping, + DeckStreamSessionState::Stopped, + }; + assert(events.states == expectedMainSequence); + + RecordingEvents invalidStartEvents; + DeckStreamSession invalidStart(renderer, audio, input, invalidStartEvents); + const auto startBeforePrepare = invalidStart.startNoNetwork(); + assert(startBeforePrepare.state == DeckStreamSessionState::Failed); + assert(startBeforePrepare.reason == "start requested before prepare"); + assert(invalidStart.state() == DeckStreamSessionState::Failed); + assert(invalidStartEvents.states == std::vector{DeckStreamSessionState::Failed}); + + RecordingEvents invalidStopEvents; + DeckStreamSession invalidStop(renderer, audio, input, invalidStopEvents); + const auto stopBeforeStart = invalidStop.stop(); + assert(stopBeforeStart.state == DeckStreamSessionState::Failed); + assert(stopBeforeStart.reason == "stop requested before active stream"); + assert(invalidStop.state() == DeckStreamSessionState::Failed); + assert(invalidStopEvents.states == std::vector{DeckStreamSessionState::Failed}); + + RecordingEvents invalidPrepareEvents; + DeckStreamSession invalidPrepare(renderer, audio, input, invalidPrepareEvents); + auto invalidRequest = validRequest("game-invalid"); + invalidRequest.width = 0; + const auto rejectedPrepare = invalidPrepare.prepare(invalidRequest); + assert(rejectedPrepare.state == DeckStreamSessionState::Failed); + assert(rejectedPrepare.reason == "invalid stream request dimensions or bitrate"); + assert(invalidPrepare.state() == DeckStreamSessionState::Failed); + + RecordingEvents cancellableEvents; + DeckStreamSession cancellable(renderer, audio, input, cancellableEvents); + cancellable.prepare(validRequest("game-456")); + const auto cancelled = cancellable.cancel("user backed out before network start"); + assert(cancelled.state == DeckStreamSessionState::Cancelled); + assert(cancelled.reason == "user backed out before network start"); + assert(cancellable.state() == DeckStreamSessionState::Cancelled); + + RecordingEvents failingEvents; + DeckStreamSession failing(renderer, audio, input, failingEvents); + const auto failed = failing.fail("renderer adapter unavailable"); + assert(failed.state == DeckStreamSessionState::Failed); + assert(failed.reason == "renderer adapter unavailable"); + assert(failing.state() == DeckStreamSessionState::Failed); + + StubRenderer ownerARenderer; + StubAudio ownerAAudio; + StubInput ownerAInput; + RecordingEvents ownerAEvents; + DeckStreamSession ownerA(ownerARenderer, ownerAAudio, ownerAInput, ownerAEvents); + ownerA.prepare(validRequest("game-owner-a")); + auto* ownerAVideoStart = ownerA.moonlightBoundary().videoCallbacks->start; + auto* ownerAAudioStart = ownerA.moonlightBoundary().audioCallbacks->start; + auto* ownerARumble = ownerA.moonlightBoundary().listenerCallbacks->rumble; + StubRenderer ownerBRenderer; + StubAudio ownerBAudio; + StubInput ownerBInput; + RecordingEvents ownerBEvents; + DeckStreamSession ownerB(ownerBRenderer, ownerBAudio, ownerBInput, ownerBEvents); + const auto ownerBPrepare = ownerB.prepare(validRequest("game-owner-b")); + assert(ownerBPrepare.state == DeckStreamSessionState::Failed); + assert(ownerBPrepare.reason == "another stream callback owner is active"); + ownerAVideoStart(); + ownerAAudioStart(); + ownerARumble(2, 300, 400); + assert(ownerARenderer.startCalls == 1); + assert(ownerAAudio.startCalls == 1); + assert((ownerAInput.rumbles == std::vector{InputCall{2, 300, 400, 0}})); + assert(ownerBRenderer.startCalls == 0); + assert(ownerBAudio.startCalls == 0); + assert(ownerBInput.rumbles.empty()); + ownerA.cancel("owner lifetime probe complete"); + + StubRenderer staleRenderer; + StubAudio staleAudio; + StubInput staleInput; + RecordingEvents staleEvents; + DecoderRendererSetup staleVideoSetup = nullptr; + DecoderRendererStart staleVideoStart = nullptr; + AudioRendererInit staleAudioInit = nullptr; + AudioRendererStart staleAudioStart = nullptr; + ConnListenerRumble staleRumble = nullptr; + void* staleContext = nullptr; + { + DeckStreamSession scoped(staleRenderer, staleAudio, staleInput, staleEvents); + scoped.prepare(validRequest("game-stale")); + staleVideoSetup = scoped.moonlightBoundary().videoCallbacks->setup; + staleVideoStart = scoped.moonlightBoundary().videoCallbacks->start; + staleAudioInit = scoped.moonlightBoundary().audioCallbacks->init; + staleAudioStart = scoped.moonlightBoundary().audioCallbacks->start; + staleRumble = scoped.moonlightBoundary().listenerCallbacks->rumble; + staleContext = scoped.moonlightBoundary().callbackContext; + } + assert(staleVideoSetup(VIDEO_FORMAT_H264, 1280, 800, 60, staleContext, 0) == DR_NEED_IDR); + OPUS_MULTISTREAM_CONFIGURATION staleOpusConfig{}; + assert(staleAudioInit(AUDIO_CONFIGURATION_STEREO, &staleOpusConfig, staleContext, 0) == -1); + staleVideoStart(); + staleAudioStart(); + staleRumble(1, 2, 3); + assert(staleRenderer.startCalls == 0); + assert(staleAudio.startCalls == 0); + assert(staleInput.rumbles.empty()); + + StubRenderer laterRenderer; + StubAudio laterAudio; + StubInput laterInput; + RecordingEvents laterEvents; + DeckStreamSession later(laterRenderer, laterAudio, laterInput, laterEvents); + later.prepare(validRequest("game-later")); + assert(staleVideoSetup(VIDEO_FORMAT_H264, 1280, 800, 60, staleContext, 0) == DR_NEED_IDR); + assert(staleAudioInit(AUDIO_CONFIGURATION_STEREO, &staleOpusConfig, staleContext, 0) == -1); + later.cancel("later owner release"); + + for (int i = 0; i < 24; ++i) { + StubRenderer sequentialRenderer; + StubAudio sequentialAudio; + StubInput sequentialInput; + RecordingEvents sequentialEvents; + DeckStreamSession sequential(sequentialRenderer, sequentialAudio, sequentialInput, sequentialEvents); + const auto preparedSequential = sequential.prepare(validRequest("game-sequential-" + std::to_string(i))); + assert(preparedSequential.state == DeckStreamSessionState::Preparing); + sequential.cancel("sequential owner release"); + } + + return 0; +} diff --git a/clients/deck/tests/deck_stream_media_adapters_test.cpp b/clients/deck/tests/deck_stream_media_adapters_test.cpp new file mode 100644 index 00000000..959f1012 --- /dev/null +++ b/clients/deck/tests/deck_stream_media_adapters_test.cpp @@ -0,0 +1,230 @@ +#include "stream/deck_stream_media_adapters.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { + +class NoopInput final : public nova::deck::stream::DeckStreamInput { +public: + std::string_view adapterName() const override { return "noop-input"; } + void rumble(uint16_t controllerNumber, uint16_t lowFreqMotor, uint16_t highFreqMotor) override { + (void)controllerNumber; + (void)lowFreqMotor; + (void)highFreqMotor; + } + void setMotionEventState(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz) override { + (void)controllerNumber; + (void)motionType; + (void)reportRateHz; + } + void setControllerLed(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) override { + (void)controllerNumber; + (void)r; + (void)g; + (void)b; + } +}; + +class RecordingEvents final : public nova::deck::stream::DeckStreamSessionEvents { +public: + void onSessionEvent(nova::deck::stream::DeckStreamSessionState state, std::string_view reason) override { + states.push_back(state); + reasons.emplace_back(reason); + } + + std::vector states; + std::vector reasons; +}; + +std::vector readBinaryFile(const std::filesystem::path& path) { + std::ifstream input(path, std::ios::binary); + assert(input.good()); + return {std::istreambuf_iterator(input), std::istreambuf_iterator()}; +} + +std::vector makeLocalAnnexBH264IdrSample() { + const auto output = std::filesystem::temp_directory_path() / ("nova-deck-local-idr-" + std::to_string(getpid()) + ".h264"); + const pid_t pid = fork(); + assert(pid >= 0); + if (pid == 0) { + execlp( + "ffmpeg", + "ffmpeg", + "-hide_banner", + "-loglevel", + "error", + "-y", + "-f", + "lavfi", + "-i", + "color=c=black:s=128x72:r=1:d=1", + "-frames:v", + "1", + "-c:v", + "libx264", + "-preset", + "ultrafast", + "-tune", + "zerolatency", + "-x264-params", + "keyint=1:min-keyint=1:scenecut=0", + "-f", + "h264", + output.c_str(), + static_cast(nullptr)); + _exit(127); + } + int status = 0; + assert(waitpid(pid, &status, 0) == pid); + assert(WIFEXITED(status)); + assert(WEXITSTATUS(status) == 0); + auto bytes = readBinaryFile(output); + assert(!bytes.empty()); + return bytes; +} + +DECODE_UNIT makeDecodeUnit(std::vector& annexBBytes, LENTRY& entry) { + entry.next = nullptr; + entry.data = reinterpret_cast(annexBBytes.data()); + entry.length = static_cast(annexBBytes.size()); + entry.bufferType = BUFFER_TYPE_PICDATA; + + DECODE_UNIT unit{}; + unit.frameNumber = 1; + unit.frameType = FRAME_TYPE_IDR; + unit.fullLength = entry.length; + unit.bufferList = &entry; + return unit; +} + +} // namespace + +int main() { + using nova::deck::stream::DeckLinuxAudioProbe; + using nova::deck::stream::DeckLinuxMediaProbe; + using nova::deck::stream::DeckPipeWireAudio; + using nova::deck::stream::DeckStreamRequest; + using nova::deck::stream::DeckStreamSession; + using nova::deck::stream::DeckStreamSessionState; + using nova::deck::stream::DeckVaapiFfmpegRenderer; + + static_assert(!std::is_copy_constructible_v); + static_assert(!std::is_move_constructible_v); + + const DeckLinuxMediaProbe mediaProbe = DeckLinuxMediaProbe::detect(); + assert(mediaProbe.ffmpegLibavcodecHeadersLinked); + assert(mediaProbe.ffmpegLibavutilHeadersLinked); + assert(mediaProbe.vaapiHeadersLinked); + assert(mediaProbe.qtQuickRhiPresentationBoundary); + assert(mediaProbe.hardwareDeviceTypeName == std::string("vaapi")); + assert(!mediaProbe.runtimeStatus.empty()); + + DeckVaapiFfmpegRenderer renderer; + assert(renderer.adapterName() == "ffmpeg-vaapi-h264-qt-rhi-prototype"); + const int rendererSetup = renderer.setup(VIDEO_FORMAT_H264, 1280, 800, 60, nullptr, 0); + assert(rendererSetup == (renderer.lifecycle().runtimeVaapiDeviceAvailable ? DR_OK : DR_NEED_IDR)); + renderer.start(); + assert(renderer.submitDecodeUnit(nullptr) == DR_NEED_IDR); + renderer.stop(); + renderer.cleanup(); + assert(renderer.lifecycle().setupCalls == 1); + assert(renderer.lifecycle().startCalls == 1); + assert(renderer.lifecycle().submitCalls == 1); + assert(renderer.lifecycle().stopCalls == 1); + assert(renderer.lifecycle().cleanupCalls == 1); + assert(!renderer.lifecycle().acceptedNullDecodeUnit); + assert(renderer.lifecycle().networkStartAllowed == false); + assert(renderer.lifecycle().runtimeVaapiDeviceAvailable == mediaProbe.runtimeVaapiDeviceAvailable); + assert(!renderer.lifecycle().runtimeStatus.empty()); + + DeckVaapiFfmpegRenderer decodeRenderer; + const int decodeSetup = decodeRenderer.setup(VIDEO_FORMAT_H264, 128, 72, 1, nullptr, 0); + auto idrBytes = makeLocalAnnexBH264IdrSample(); + LENTRY idrEntry{}; + DECODE_UNIT idrUnit = makeDecodeUnit(idrBytes, idrEntry); + if (decodeRenderer.lifecycle().runtimeVaapiDeviceAvailable) { + assert(decodeSetup == DR_OK); + assert(decodeRenderer.lifecycle().ownsHardwareDevice); + assert(decodeRenderer.lifecycle().ownsCodecContext); + assert(decodeRenderer.submitDecodeUnit(&idrUnit) == DR_OK); + assert(decodeRenderer.lifecycle().decodedHardwareFrames == 1); + assert(decodeRenderer.lifecycle().lastFrameWasHardwareBacked); + } else { + assert(decodeSetup == DR_NEED_IDR); + assert(!decodeRenderer.lifecycle().ownsHardwareDevice); + assert(!decodeRenderer.lifecycle().ownsCodecContext); + assert(decodeRenderer.submitDecodeUnit(&idrUnit) == DR_NEED_IDR); + assert(decodeRenderer.lifecycle().decodedHardwareFrames == 0); + assert(!decodeRenderer.lifecycle().lastRuntimeError.empty()); + assert(decodeRenderer.lifecycle().lastRuntimeError.find("av_hwdevice_ctx_create(VAAPI) failed") != std::string::npos); + } + decodeRenderer.cleanup(); + + const DeckLinuxAudioProbe audioProbe = DeckLinuxAudioProbe::detect(); + assert(audioProbe.pipeWireHeadersLinked); + assert(audioProbe.pulseFallbackHeadersLinked); + + OPUS_MULTISTREAM_CONFIGURATION opusConfig{}; + opusConfig.samplesPerFrame = 240; + DeckPipeWireAudio audio; + assert(audio.adapterName() == "pipewire-pcm-pulse-fallback-prototype"); + assert(audio.init(AUDIO_CONFIGURATION_STEREO, &opusConfig, nullptr, 0) == 0); + audio.start(); + char pcm[] = {'p', 'c', 'm', '!'}; + audio.decodeAndPlaySample(pcm, 4); + audio.stop(); + audio.cleanup(); + assert(audio.lifecycle().initCalls == 1); + assert(audio.lifecycle().startCalls == 1); + assert(audio.lifecycle().sampleCalls == 1); + assert(audio.lifecycle().lastSampleLength == 4); + assert(audio.lifecycle().samplesPerFrame == 240); + assert(audio.lifecycle().stopCalls == 1); + assert(audio.lifecycle().cleanupCalls == 1); + assert(audio.lifecycle().networkStartAllowed == false); + + DeckVaapiFfmpegRenderer callbackRenderer; + DeckPipeWireAudio callbackAudio; + NoopInput input; + RecordingEvents events; + DeckStreamSession session(callbackRenderer, callbackAudio, input, events); + assert(!session.moonlightBoundary().networkStartAllowed); + const auto prepared = session.prepare(DeckStreamRequest{ + .hostId = "offline-harness-host", + .gameId = "offline-harness-game", + .width = 1280, + .height = 800, + .fps = 60, + .bitrateKbps = 20000, + }); + assert(prepared.state == DeckStreamSessionState::Preparing); + assert(!prepared.networkStarted); + const int callbackRendererSetup = session.moonlightBoundary().videoCallbacks->setup(VIDEO_FORMAT_H264, 1280, 800, 60, session.moonlightBoundary().callbackContext, 0); + assert(callbackRendererSetup == (callbackRenderer.lifecycle().runtimeVaapiDeviceAvailable ? DR_OK : DR_NEED_IDR)); + assert(session.moonlightBoundary().videoCallbacks->submitDecodeUnit(nullptr) == DR_NEED_IDR); + OPUS_MULTISTREAM_CONFIGURATION callbackOpusConfig{}; + callbackOpusConfig.samplesPerFrame = 240; + assert(session.moonlightBoundary().audioCallbacks->init(AUDIO_CONFIGURATION_STEREO, &callbackOpusConfig, session.moonlightBoundary().callbackContext, 0) == 0); + assert(callbackRenderer.lifecycle().setupCalls == 1); + assert(callbackRenderer.lifecycle().submitCalls == 1); + assert(callbackAudio.lifecycle().initCalls == 1); + const auto started = session.startNoNetwork(); + assert(started.state == DeckStreamSessionState::Active); + assert(!started.networkStarted); + session.stop(); + + return 0; +} diff --git a/docs/steam_deck_native_port_study.md b/docs/steam_deck_native_port_study.md index 8506d515..536ddb08 100644 --- a/docs/steam_deck_native_port_study.md +++ b/docs/steam_deck_native_port_study.md @@ -276,12 +276,215 @@ Non-goals for the MVP: - perfect gyro and haptics parity - background-service behavior identical to Android -## First implementation spike - -Before full implementation, run one short spike that answers these questions decisively: - -1. Is a new Qt/QML client with `moonlight-common-c` easier to shape around Nova than adapting an existing Linux Moonlight client? -2. Which Linux decode and presentation backend gives the cleanest Steam Deck path with acceptable latency? -3. What is the minimum Polaris surface needed to make the Deck client feel meaningfully like Nova instead of a generic Moonlight shell? - -If that spike does not invalidate the assumptions above, proceed with Option A. +## Deck-T4 streaming backend decision + +Decision: proceed with a Nova-owned native Linux streaming backend that links +`moonlight-common-c` directly and supplies Deck-specific adapters for decode, +presentation, audio, input, discovery, pairing, persistence, and Polaris session +orchestration. + +Reject adapting an existing Linux Moonlight client as the product base for the +next Deck slice. Existing Linux Moonlight clients remain useful references for +backend behavior, SteamOS quirks, and dependency choices, but Nova should not +inherit another client's shell, state model, settings, or launch/session UX. + +Detailed spike notes live in +`clients/deck/spikes/streaming-backend-notes.md`. + +### Why the direct `moonlight-common-c` path wins + +The current Android implementation already shows the seam Nova needs to keep: +Android owns product orchestration while `moonlight-common-c` owns the stream +transport. Android's `NvConnection` negotiates host/app/session state, installs +video/audio/listener callbacks through `MoonBridge`, and then starts the native +connection. The native bridge eventually calls `LiStartConnection(...)` with +`CONNECTION_LISTENER_CALLBACKS`, `DECODER_RENDERER_CALLBACKS`, and +`AUDIO_RENDERER_CALLBACKS`. + +For Deck, the reusable boundary is that C callback contract, not the Android JNI +bridge. The JNI layer is tied to the JVM, Android logcat, Android CPU feature +helpers, `MediaCodecDecoderRenderer`, `AndroidAudioRenderer`, Android discovery, +Android input capture, activities, services, notifications, and other lifecycle +assumptions. Those pieces should remain behavior references only. + +Direct integration keeps Nova's product shape intact: + +- Standard Moonlight-compatible hosts still work through `moonlight-common-c`. +- Polaris can stay first-class instead of being grafted onto a generic client UI. +- Continue, launch modes, watch/owned-session state, tuning sync, NovaHUD copy, + reconnect messaging, and safe diagnostics can be modeled as Deck-native Nova + surfaces from the start. +- `shared/stream-core` can become the cross-client session boundary Android and + Deck both converge toward, rather than wrapping a forked Linux app. + +### Rejected alternative: adapt an existing Linux Moonlight client + +This path remains a fallback, not the default. + +It is attractive because a mature Linux client may already have working decode, +audio, input, fullscreen, and packaging behavior. It could get a generic first +picture on screen faster. + +It is rejected for Deck-T4 because Nova's value is not just launching an app list +and displaying a stream. Nova needs Polaris-aware library metadata, launch-mode +intent, watch/resume/replace semantics, session ownership truth, tuning sync, +controller-first overlays, and handheld recovery copy. Retrofitting those into +another client's screens and state machine risks producing Moonlight cosplay with +Nova labels bolted on. It would also duplicate or discard the existing +`clients/deck` shell and delay the shared-stream boundary Nova needs for Android, +Deck, and future clients. + +Revisit this only if the direct backend cannot produce stable low-latency +SteamOS video/audio after focused testing, or if a small separable backend +component can be reused with clean provenance and attribution. Nova is already +GPLv3-lineage, but any copied client code still needs explicit license and +maintenance review before entering the repo. + +### Linux backend candidates for the direct path + +**Video and presentation** should be the first technical proof. Start with H.264 +hardware decode on a Linux media stack such as FFmpeg/libavcodec with VA-API on +Steam Deck-class AMD hardware, then prove fullscreen presentation and overlay +composition hooks. Consider GStreamer only if it shortens VA-API and +presentation integration without hiding frame pacing control. Treat software +decode as a fallback or diagnostic path, not the target. + +**Audio** should prefer PipeWire for SteamOS-era integration, with PulseAudio as +the compatibility fallback. SDL audio is acceptable as a temporary spike adapter +only if it gets decoded PCM playing while the session lifecycle boundary is being +validated. + +**Controller input** should use the Deck's built-in controls as a normal gamepad +path first, likely through SDL/GameController-style mapping or a minimal evdev +adapter if SDL is not introduced yet. Keep shell shortcuts separate from +in-stream Moonlight packets so Nova can own Command Center, NovaHUD, stream stop, +and diagnostic copy without stealing gameplay input accidentally. + +**Discovery and pairing** should start with manual host add plus pairing/cert +storage. LAN mDNS/zeroconf can follow after the no-Android credential and host +record contracts are stable. + +**Fullscreen and suspend/resume** must be tested in a Game Mode-like fullscreen +path, not only a Desktop Mode window. The stream boundary must handle connecting, +active streaming, disconnecting, suspend, interrupt, and reconnect states without +corrupting `moonlight-common-c` lifecycle. + +### Minimum Polaris surface for the next Deck vertical slice + +The next real slice should include only the Polaris surface needed to make Deck +feel like Nova instead of a generic Moonlight client: + +- host capability probe that distinguishes Polaris from standard + Moonlight-compatible hosts +- library/app card model with Polaris metadata when available and standard app + list fallback +- launch intent containing host id, game id/UUID, launch mode, stream display + mode/headless/virtual-display hint, and safe debug copy +- session truth for active/inactive, owned-by-this-client, owner name/device, + watch eligibility, quit/resume/replace permissions, and session token plumbing + where exposed +- client presentation/tuning summary for target fps, bitrate, codec, display + mode, sync state, source of truth, and relaunch-required messaging +- HUD/reconnect event stream for connection stages, transient warnings, poor + connection, no-video/no-frame/protected-content/early-termination, suspend, + and reconnect status + +Defer rich optimizer controls, profile editing, capture diagnostics UI, full +NovaHUD parity, gyro/haptics polish, touch overlays, and every Android quick-menu +action until after the first real Deck stream works. + +### First technical risks to test next + +1. Video presentation: can a Deck-native adapter consume `moonlight-common-c` + decode units and present low-latency fullscreen frames with overlay hooks? +2. Audio: can decoded PCM play through PipeWire, PulseAudio, or temporary SDL + without drift or bad teardown? +3. Controller input: can Deck controls be split cleanly between shell shortcuts + and in-stream Moonlight controller packets in Game Mode? +4. Host discovery/pairing: can manual add, pairing, and pinned certificate + storage work without Android `Context`, NSD, or keystore assumptions? +5. Suspend/resume: can the session boundary interrupt, stop, reconnect, and + report recovery states safely? +6. Game Mode fullscreen: can the Qt shell enter stream fullscreen, keep focus, + and exit/recover with readable Nova copy? + +## Deck-T7 hardware-backed Linux video/audio prototype decision + +Decision: the first hardware-backed Deck prototype should use +FFmpeg/libavcodec H.264 decode with VA-API on Steam Deck-class AMD Linux +hardware, then present through the existing Qt Deck shell via a Qt Quick/QRhi +scene-graph item instead of taking over raw DRM/KMS. Audio should start with a +native PipeWire stream fed from the Moonlight Opus/audio callback path, with +PulseAudio compatibility as the first fallback and SDL audio only as a temporary +throwaway spike if PipeWire blocks the lifecycle proof. + +This keeps the direct `moonlight-common-c` decision intact while choosing the +first concrete Linux media boundary. The existing Deck CMake target already links +the checked-out `moonlight-common-c` tree, and the no-network stream-core seam in +`clients/deck/src/stream/deck_stream_core.h` owns real +`CONNECTION_LISTENER_CALLBACKS`, `DECODER_RENDERER_CALLBACKS`, +`AUDIO_RENDERER_CALLBACKS`, and `STREAM_CONFIGURATION` storage. Deck-T6 proved +those callbacks can be initialized and routed through Nova-owned renderer, +audio, input, and session-event interfaces without `LiStartConnection` or +sockets. Deck-T7 therefore chooses the first hardware-backed adapter +implementation target rather than changing the product shell or claiming a real +stream. + +### Why this path wins + +- FFmpeg fits `moonlight-common-c`'s Annex-B decode-unit callback shape and lets + the first proof focus on H.264 before HEVC, AV1, 10-bit, HDR, and advanced + reference-frame invalidation capability gates. +- VA-API is the lowest-friction hardware-decode target for Steam Deck-class AMD + Linux systems. Vulkan decode may become useful later, but it is too much API + surface for the first offline harness. +- Presenting inside Qt Quick/QRhi preserves Nova's controller-first shell, + overlays, copy affordances, stream-stop confirmation, focus recovery, and + suspend/resume messaging. Raw DRM/KMS is rejected for the first product slice + because Steam Deck Game Mode already runs apps under gamescope; bypassing the + shell would fight the compositor and NovaHUD composition before the stream + lifecycle is proven. +- PipeWire is the right SteamOS-era audio target and still gives a PulseAudio + compatibility lane. ALSA-only output is too low-level for the first handheld + lifecycle proof, and SDL should not become a product dependency just because it + can make PCM noise quickly. +- Local Fedora dependency probes found the expected development packages for the + next CMake probe (`libavcodec`, `libavutil`, `libva`, `libva-drm`, `libdrm`, + `egl`, `wayland-client`, `Qt6Quick`, `libpipewire-0.3`, and `sdl2`). That does + not guarantee SteamOS packaging, but it means the next local harness can test + real headers/libraries rather than a paper backend. + +### Rejected alternatives + +- **Raw DRM/KMS/EGL first:** useful later for benchmarking or a minimal renderer + harness, but rejected as the first product path because it bypasses the Qt + shell and risks gamescope/focus/suspend fights. +- **SDL2 as the main stream runtime:** useful for isolated probes, but rejected + as the primary path because it would introduce a second window/input/audio + model beside the existing Qt shell before Nova has proven the stream lifecycle. +- **GStreamer first:** defer unless FFmpeg/VA-API integration stalls. It can hide + frame pacing, callback error handling, and overlay timing decisions Nova needs + to own directly. +- **Software decode first:** diagnostic fallback only; it does not answer the + hardware-backed Deck question. +- **PulseAudio-only or ALSA-only first:** PulseAudio remains a compatibility + fallback through PipeWire; ALSA is too raw for the first suspend/resume and + device-routing proof. + +Detailed Deck-T7 notes and the Deck-T8 card live in +`clients/deck/spikes/streaming-backend-notes.md`. + +### Recommended next implementation card + +Deck-T8 first hardware-backed Linux renderer/audio harness: add a local/offline +prototype under `clients/deck` that builds only when the required development +packages are present. Connect the existing no-network stream-core callbacks to an +FFmpeg+VA-API H.264 renderer adapter and a PipeWire audio adapter, feed them from +deterministic test data created at test time or checked-in source code only when +licensing/provenance is explicit, and prove setup/start/submit or +decode/play/stop/cleanup boundaries without `LiStartConnection`, sockets, host +discovery, pairing, credentials, native asset blobs, Android changes, or fake +streaming UI. Required verification: core Deck CMake/CTest, Qt smoke when +available, adapter CTest or probe skip with a clear dependency message, +fullscreen/offscreen shell boundary notes, `git diff --check`, and independent +review before commit.