From 742611b8411b5a1a8f9ce7ecbe2be8a23e6218f1 Mon Sep 17 00:00:00 2001 From: Florian Egger Date: Fri, 28 Aug 2026 21:54:32 +0200 Subject: [PATCH] Scaffold C++20 screencast project with Meson, agent workflow, and phase plan --- .agents/MEMORY.md | 34 +++ .agents/skills/agent-memory/SKILL.md | 51 ++++ .agents/skills/cpp-meson-build/SKILL.md | 91 ++++++ .../skills/linux-multimedia-capture/SKILL.md | 57 ++++ .agents/skills/rtp-networking/SKILL.md | 69 +++++ .agents/skills/screencast-app/SKILL.md | 73 +++++ .gitignore | 28 ++ AGENTS.md | 264 ++++++++++++++++++ README.md | 40 +++ docs/ARCHITECTURE.md | 69 +++++ docs/PHASES.md | 92 ++++++ include/screencast/app/cli.h | 25 ++ include/screencast/app/pipeline.h | 58 ++++ include/screencast/capture/capture.h | 56 ++++ include/screencast/codec/decoder.h | 39 +++ include/screencast/codec/encoder.h | 47 ++++ include/screencast/network/discovery.h | 35 +++ include/screencast/network/rtp_packet.h | 33 +++ include/screencast/network/signaling.h | 47 ++++ include/screencast/network/transport.h | 43 +++ include/screencast/render/renderer.h | 37 +++ meson.build | 26 ++ meson_options.txt | 5 + src/meson.build | 2 + tests/meson.build | 1 + 25 files changed, 1322 insertions(+) create mode 100644 .agents/MEMORY.md create mode 100644 .agents/skills/agent-memory/SKILL.md create mode 100644 .agents/skills/cpp-meson-build/SKILL.md create mode 100644 .agents/skills/linux-multimedia-capture/SKILL.md create mode 100644 .agents/skills/rtp-networking/SKILL.md create mode 100644 .agents/skills/screencast-app/SKILL.md create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 README.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/PHASES.md create mode 100644 include/screencast/app/cli.h create mode 100644 include/screencast/app/pipeline.h create mode 100644 include/screencast/capture/capture.h create mode 100644 include/screencast/codec/decoder.h create mode 100644 include/screencast/codec/encoder.h create mode 100644 include/screencast/network/discovery.h create mode 100644 include/screencast/network/rtp_packet.h create mode 100644 include/screencast/network/signaling.h create mode 100644 include/screencast/network/transport.h create mode 100644 include/screencast/render/renderer.h create mode 100644 meson.build create mode 100644 meson_options.txt create mode 100644 src/meson.build create mode 100644 tests/meson.build diff --git a/.agents/MEMORY.md b/.agents/MEMORY.md new file mode 100644 index 0000000..eea7692 --- /dev/null +++ b/.agents/MEMORY.md @@ -0,0 +1,34 @@ +# Project Memory — screen_cast + +Last updated: initial scaffold. + +## Project state + +- Empty project scaffolded with Meson build, C++20, module headers, and agent + workflow files. +- No implementation yet; headers are placeholders. +- Phase plan is in `docs/PHASES.md`. Agents are now explicitly instructed in + `AGENTS.md` to read `docs/PHASES.md` before planning and not to start work + belonging to a later phase unless requested. + +## Decisions + +- Language: C++20 with explicit modern-C++ guidelines in `AGENTS.md` and + `cpp-meson-build/SKILL.md`. +- Build system: Meson. +- Capture: PipeWire + xdg-desktop-portal. +- Encode/Decode: FFmpeg (libavcodec, libavutil, libswscale). +- Transport: RTP over UDP; signaling via WebSocket/JSON. +- Discovery: mDNS/Avahi. +- Rendering: SDL2 or SDL3 + OpenGL. +- Namespace: `sc`. + +## Active blockers + +None. + +## Open questions + +- GUI framework (Qt6 vs. none / CLI only) — deferred to later phase. +- Hardware acceleration strategy (VAAPI / Vulkan Video / NVENC) — evaluate after + software encode path works. diff --git a/.agents/skills/agent-memory/SKILL.md b/.agents/skills/agent-memory/SKILL.md new file mode 100644 index 0000000..fc85deb --- /dev/null +++ b/.agents/skills/agent-memory/SKILL.md @@ -0,0 +1,51 @@ +--- +name: agent-memory +description: | + Persistent handoff memory for multi-session agent work on the screen_cast + project. Read this skill at the start of every new agent conversation so the + current thread can bootstrap context from the repository rather than from chat + history. +disable-model-invocation: false +--- + +## Always read at the start of a new conversation + +1. `AGENTS.md` — mandatory project workflow. +2. This skill (`agent-memory/SKILL.md`). +3. `.agents/MEMORY.md` — current handoff log. +4. Any domain-specific `RUNBOOK.md` relevant to the task (e.g. + `docs/RUNBOOK.md`). +5. All other skills applicable to the task (see `AGENTS.md` skill selection). + +## Purpose + +Zed agent threads do not share conversation history. This project therefore +keeps the shared state in the repository itself: + +- `.agents/MEMORY.md` — rolling handoff log: current focus, completed work, + open blockers, decisions, files that matter. +- `/RUNBOOK.md` — operational memory for recurring workflows + (build/CI, capture validation, networking smoke tests). + +Both files are ordinary markdown under git, so their history is preserved. + +## After significant work + +Update `.agents/MEMORY.md`: + +- **Current focus**: one-line summary of what the thread was working on. +- **Completed**: concrete outcomes, file paths, commits. +- **Open issues / blockers**: anything unresolved at the end of the thread. +- **Decisions & conventions**: choices that future threads must respect. +- **Files that matter right now**: paths the next thread should read first. + +Update domain `RUNBOOK.md` when the task reveals a reusable observation: + +- non-obvious build or dependency workarounds; +- validation steps that caught errors; +- commands or snippets that should be reused. + +## When starting a new thread + +Paste a brief handoff if helpful, but **do not rely on it**. Always verify the +actual current state from the memory files, git log, and the relevant code. diff --git a/.agents/skills/cpp-meson-build/SKILL.md b/.agents/skills/cpp-meson-build/SKILL.md new file mode 100644 index 0000000..cc82f49 --- /dev/null +++ b/.agents/skills/cpp-meson-build/SKILL.md @@ -0,0 +1,91 @@ +--- +name: cpp-meson-build +description: | + Guidelines for C++20 development and Meson build management in the + screen_cast project. Read before changing source files, build files, + dependencies, or toolchain configuration. +disable-model-invocation: false +--- + +# cpp-meson-build + +Guidelines for C++20 development and Meson build management in the `screen_cast` +project. + +## Scope + +Use this skill for: + +- Adding or changing C++ source files, headers, or namespaces. +- Modifying `meson.build` files, compiler flags, or targets. +- Adding or updating dependencies (system libraries, pkg-config, subprojects). +- Toolchain, sanitizer, static-analysis, or formatter configuration. + +## Meson conventions + +- Keep `meson.build` files declarative and readable. +- Prefer `dependency()` with `pkg-config` names over manual `-l` flags. +- Pin required C++ standard: `cpp_std = 'c++20'`. +- Put one target per `meson.build` file where practical. +- Use `include_directories('include')` for the public API headers. +- Declare unit tests with `test()` and keep them deterministic. + +## Dependency verification + +Before adding a new dependency: + +- verify the pkg-config file exists on the target system (e.g. + `pkg-config --exists libavcodec`); +- check that the required headers compile; +- update `meson.build` and this skill's `references/` if needed. + +## Error handling + +- Prefer `std::expected`, `std::optional`, or `std::error_code` over + exceptions for recoverable failures. +- Use RAII wrappers; avoid raw owning pointers. +- Keep headers minimal and forward-declare where possible. + +## Modern C++ guidelines + +All project code must use C++20 idioms: + +- **Ownership**: use `std::unique_ptr` and `std::shared_ptr`; no raw `new`/`delete` + or raw owning pointers. +- **Optional values**: use `std::optional` instead of sentinel values or + out-parameters. +- **Fallible operations**: use `std::expected` or `std::error_code` for + recoverable errors; avoid exceptions for control flow. +- **Views**: use `std::span` for non-owning buffers and `std::string_view` + for read-only strings. +- **Compile-time**: prefer `constexpr`/`consteval` where possible. +- **Type safety**: prefer `enum class`, `std::variant`, and `std::optional` + over bare integers or bool flags. +- **Concurrency**: prefer `std::jthread`, `std::stop_token`, and standard + synchronization primitives. +- **Containers**: use standard containers; avoid C arrays and raw buffers. +- **Formatting/time**: use `` / `std::format` and ``. +- **Algorithms**: prefer `std::ranges` over hand-rolled loops where it + improves clarity. +- **Callables**: avoid `std::function` unless type erasure is required. +- **Concepts**: use `requires` clauses to express interface contracts where + beneficial. + +Reject C-style patterns (`printf`, `sprintf`, manual memory management with +`malloc`/`free`, raw arrays) when a standard-library equivalent exists. + +## Useful commands + +```sh +meson setup build +meson compile -C build +meson test -C build --print-errorlogs +meson configure build +``` + +## References + +- `meson.build` (project root) +- `meson_options.txt` (if present) +- `docs/ARCHITECTURE.md` +- `docs/PHASES.md` diff --git a/.agents/skills/linux-multimedia-capture/SKILL.md b/.agents/skills/linux-multimedia-capture/SKILL.md new file mode 100644 index 0000000..dec0f9d --- /dev/null +++ b/.agents/skills/linux-multimedia-capture/SKILL.md @@ -0,0 +1,57 @@ +--- +name: linux-multimedia-capture +description: | + Guidelines for Linux screen/audio capture, FFmpeg encoding/decoding, and + hardware acceleration in the screen_cast project. +disable-model-invocation: false +--- + +# linux-multimedia-capture + +Guidelines for Linux screen/audio capture, FFmpeg encoding/decoding, and +hardware acceleration in the `screen_cast` project. + +## Scope + +Use this skill for: + +- Screen or window capture (PipeWire, xdg-desktop-portal D-Bus interfaces). +- Audio capture or playback (PipeWire audio nodes). +- Video encoding/decoding with FFmpeg/libavcodec. +- Pixel format conversion, scaling (libswscale, libavutil). +- Hardware acceleration (VAAPI, Vulkan Video, NVENC) via FFmpeg. + +## Capture + +- Use PipeWire with the `xdg-desktop-portal` screen-cast portal for Wayland and + X11 compatibility. +- Prefer screencast source types that request monitor, window, or virtual. +- Treat portal session tokens, node IDs, and stream FDs as owned resources; + close and destroy them on error paths. + +## Encoding / decoding + +- Use `AVCodecContext`, `AVFrame`, and `AVPacket` through RAII wrappers. +- Validate codec capabilities (e.g. supported pixel formats, thread counts). +- For H.264 baseline path use `libx264` or `h264_vaapi` when available. +- Parse NAL units carefully before packetizing; reject malformed data. + +## Hardware acceleration + +- Probe available encoders with `avcodec_find_encoder_by_name()`. +- Prefer VAAPI on Intel/AMD, NVENC on NVIDIA, Vulkan Video where supported. +- Keep a software fallback path for compatibility. + +## Security / safety + +- Bounds-check all buffer sizes before copying into packets. +- Validate resolution, stride, and pixel-format assumptions from the portal. +- Avoid exposing capture tokens or session handles in logs. + +## References + +- PipeWire API docs and `libpipewire-0.3` +- `libportal` (optional wrapper for the desktop portal) +- FFmpeg documentation: `doc/APIchanges`, `doc/examples` +- `docs/ARCHITECTURE.md` +- `docs/PHASES.md` diff --git a/.agents/skills/rtp-networking/SKILL.md b/.agents/skills/rtp-networking/SKILL.md new file mode 100644 index 0000000..2590daf --- /dev/null +++ b/.agents/skills/rtp-networking/SKILL.md @@ -0,0 +1,69 @@ +--- +name: rtp-networking +description: | + Guidelines for RTP/UDP transport, signaling, discovery, NAT traversal, and + encryption in the screen_cast project. +disable-model-invocation: false +--- + +# rtp-networking + +Guidelines for RTP/UDP transport, signaling, discovery, NAT traversal, and +encryption in the `screen_cast` project. + +## Scope + +Use this skill for: + +- RTP packetization, depacketization, and sequencing. +- UDP socket I/O and async event loops. +- Peer discovery (mDNS/DNS-SD). +- Session signaling and control channels. +- Optional SRTP/DTLS or ICE-lite/NAT traversal. + +## Transport + +- Default transport is **RTP over IPv4/IPv6 UDP**. +- Use a small, project-specific RTP header parser/serializer (12-byte base + header plus extensions if needed). +- Fragment large NAL units across RTP packets using FU-A fragmentation for + H.264. + +## Resilience + +- Start without retransmission; add **NACK / PLI** once the baseline path works. +- Keep a jitter buffer on the receiver to absorb network jitter without adding + excessive latency. +- Sequence numbers drive depacketization; detect loss and request key frames on + large gaps. + +## Discovery + +- Use **Avahi** (`avahi-client`) for mDNS/DNS-SD service announcements. +- Service name example: `_screencast._tcp` for signaling, `_screencast-rtp._udp` + for media. + +## Signaling + +- Control channel is JSON over WebSocket or plain TCP. +- Negotiate at least: codec, resolution, frame rate, UDP endpoints, session ID. +- Keep signaling independent of media transport so either can evolve. + +## Security (future) + +- SRTP key exchange via DTLS or pre-shared keys over the signaling channel. +- Do not hardcode keys; rotate per session. + +## Async I/O + +- Prefer an existing event loop library (e.g. `asio`, `libuv`) over raw threads + and `select()`. +- Keep network and capture threads separated with lock-free queues where + practical. + +## References + +- RFC 3550 (RTP), RFC 6184 (H.264 RTP payload) +- Avahi client documentation +- `docs/ARCHITECTURE.md` +- `docs/PHASES.md` diff --git a/.agents/skills/screencast-app/SKILL.md b/.agents/skills/screencast-app/SKILL.md new file mode 100644 index 0000000..0b963ff --- /dev/null +++ b/.agents/skills/screencast-app/SKILL.md @@ -0,0 +1,73 @@ +--- +name: screencast-app +description: | + Overall screencast application architecture, sender/receiver modes, + protocol negotiation, and integration decisions in the screen_cast project. +disable-model-invocation: false +--- + +# screencast-app + +Overall screencast application architecture, sender/receiver modes, protocol +negotiation, and integration decisions in the `screen_cast` project. + +## Scope + +Use this skill for: + +- Designing sender/receiver flows and state machines. +- Deciding on CLI/GUI entry points and common application lifecycle. +- Session negotiation formats and codec selection. +- Integrating capture, encode, network, decode, and render modules. +- Defining public API boundaries and module ownership. + +## High-level architecture + +``` +Sender: Capture → Encode → Packetize → RTP/UDP +Receiver: RTP/UDP → Depacketize → Decode → Render +Control: Signaling (WebSocket/JSON) + Discovery (mDNS) +``` + +- The same binary supports both `--send` and `--receive` modes. +- Each mode owns a pipeline object that wires the modules together. +- Keep modules isolated; each module owns its thread(s) and exposes a small + boundary to the pipeline. + +## Pipeline rules + +- Frame ownership is transferred with `std::unique_ptr` or shared frame pool. +- Timestamp everything in the capture clock domain and carry RTP timestamps + through the pipeline. +- On sender error, drain packets in flight before shutting down. +- On receiver error, request a key frame (PLI) or restart decoder if necessary. + +## Protocol negotiation + +Minimum negotiated fields: + +- `codec`: `h264` initially. +- `width`, `height`, `frame_rate`. +- `rtp_endpoint`: `host:port` for media. +- `signaling_endpoint`: host/port for control (optional if already connected). +- `session_id`: unique string to correlate media and control. + +## CLI / entry points + +- `screencast --send [--target monitor|window]` +- `screencast --receive [--peer-address ADDR]` +- Optional `--signaling-url`, `--codec`, `--bitrate`, `--hwaccel` flags. + +## GUI + +- GUI is optional and lives in a separate target/library. +- The core library must be usable from CLI and tests without a GUI. + +## References + +- `docs/ARCHITECTURE.md` +- `docs/PHASES.md` — must be consulted before starting implementation; do not + work on a later phase unless the current phase is complete or the user + explicitly requests it. +- Other domain skills: `linux-multimedia-capture`, `rtp-networking`, + `cpp-meson-build` diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84ff330 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Build directories +build/ +build-*/ +*.build/ + +# Meson +meson-logs/ +meson-private/ +.subproject/ + +# Generated +*.o +*.a +*.so +*.dylib +*.exe +compile_commands.json + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..27ac2bc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,264 @@ +# AGENTS.md — Screencast (Linux) Agent Workflow + +This document governs how Zed agents work in the `screen_cast` project. It is mandatory reading before any non-trivial implementation task. + +## Agent memory + +Before starting work in a new agent conversation: + +- read this `AGENTS.md`; +- read the `agent-memory/SKILL.md` skill; +- read `.agents/MEMORY.md` for the current project handoff; +- read `docs/PHASES.md` to understand the current phase and milestones; +- read any domain-specific `RUNBOOK.md` relevant to the task (e.g. + `docs/RUNBOOK.md` or `docs/multimedia/RUNBOOK.md`). + +Update `.agents/MEMORY.md` when the task produces significant state, blockers, +or decisions. Update the relevant `RUNBOOK.md` when the task reveals reusable +operational observations. Update `docs/PHASES.md` when a phase is completed or +its plan changes. + +## Skills + +Skills are task-specific instructions and must be read before planning work. + +Before producing a plan, identify all skills applicable to the task and read +their `SKILL.md` files. + +Skills are self-contained directories under `.agents/skills/`. + +When a skill is selected, read its `SKILL.md` and consult any relevant +documentation under that skill's `references/` directory. + +Skills are task-specific guidance, not a replacement for the mandatory project +workflow in this file. The agent must follow `AGENTS.md` even when no skill +applies. + +### Skill selection + +- C++ project, Meson build, dependencies, or toolchain issues: + `cpp-meson-build/SKILL.md` +- Linux screen/audio capture, PipeWire, xdg-desktop-portal, FFmpeg, VAAPI, or + codec work: + `linux-multimedia-capture/SKILL.md` +- RTP/UDP networking, transport, signaling, NAT traversal, or encryption: + `rtp-networking/SKILL.md` +- Overall screencast application architecture, sender/receiver modes, + protocol negotiation, or integration decisions: + `screencast-app/SKILL.md` + +Multiple skills may apply. Read all applicable ones. + +For example, implementing sender-side capture + encode will normally require: + +1. `cpp-meson-build/SKILL.md` +2. `linux-multimedia-capture/SKILL.md` +3. `screencast-app/SKILL.md` + +A networking change will normally require: + +1. `cpp-meson-build/SKILL.md` +2. `rtp-networking/SKILL.md` +3. `screencast-app/SKILL.md` + +Do not assume that a skill is applicable merely because its subject is mentioned +incidentally. Use the task's actual scope to determine applicability. + +## Mandatory development workflow + +All non-trivial implementation tasks follow this workflow: + +### 1. Understand + +Before changing anything: + +- read `AGENTS.md`; +- read all applicable skills; +- inspect the existing implementation and directory structure; +- search the project for related symbols, build rules, and tests; +- identify system dependencies (pkg-config names, headers, runtime packages); +- identify dependencies and integration points. + +Do not modify files during this phase. + +### Plan against the phase roadmap + +- `docs/PHASES.md` defines the project milestones and the current active phase. +- **Do not start implementation that belongs to a later phase** unless the user + explicitly asks for it or the current phase is already complete and + validated. +- When planning work, identify which phase(s) the task touches and include that + in the implementation plan. +- Update `docs/PHASES.md` when a phase is finished, blocked, or its scope + changes; update `.agents/MEMORY.md` at the same time. + +### 2. Plan + +Produce a concise implementation plan containing: + +- understanding of the requirement; +- relevant existing project functionality; +- files/components likely to change; +- proposed implementation; +- build/dependency implications; +- tests to add or modify; +- multimedia/networking/security considerations; +- potential risks or unresolved questions. + +### 3. Wait for approval + +**STOP after presenting the plan.** + +Do not create, modify, delete, or rename files until the user explicitly +approves the plan. + +Questions and clarification are allowed during this phase. + +### 4. Implement + +After approval: + +- implement the approved plan; +- prefer existing project modules and standard library functionality; +- keep the change as small and focused as practical; +- do not introduce unrelated refactoring; +- follow the C++ and project conventions described in this file and in skills. + +If implementation reveals that the approved plan is materially wrong or +incomplete, stop and explain the discrepancy rather than silently expanding +scope. + +### 5. Validate + +Run appropriate tests and checks. + +At minimum: + +- the project must still configure with Meson (`meson setup build`); +- the project must still compile (`ninja -C build`); +- unit/integration tests pass (`meson test -C build --print-errorlogs`); +- static analysis or formatting checks configured in the project; +- manual smoke test of the affected component where automated tests are + insufficient. + +For multimedia or networking changes, validate runtime behavior on an actual +Linux desktop session (Wayland or X11) when feasible. + +### 6. Review + +Before considering the task complete, review the resulting changes for: + +- correctness; +- integration with existing modules; +- reuse of existing functionality; +- maintainability (small headers, clear ownership, RAII); +- security (buffer handling, bounds checks, no secrets committed); +- thread safety and async I/O correctness; +- regression risk; +- build/dependency impact. + +## Verify before assuming + +Never invent or assume the existence of an API. + +Before using a: + +- system library or package; +- header file or symbol; +- class, function, or macro; +- build option or pkg-config name; +- external protocol or service; + +verify that it exists in the current environment, project dependencies, or a +reliable source (distribution package index, upstream API docs, local header). + +Do not infer existence from naming conventions, training knowledge, or +similarly named components. If existence cannot be verified, stop and report the +uncertainty. + +In particular, never add a dependency merely because it would be a plausible +package name. + +## Prefer framework and standard functionality + +Before implementing custom functionality, search the project and the C++ +standard library for an existing mechanism. + +Prefer, in order: + +1. existing project functionality that already satisfies the requirement; +2. C++ standard library facilities (`std::`, ``, ``, etc.); +3. widely adopted third-party libraries already used by the project; +4. a small project-specific implementation only when the above are insufficient. + +Do not duplicate functionality merely because implementing it locally appears +simpler. + +When choosing a custom implementation over an existing mechanism, explain the +reason in the plan. + +## Git + +- Keep commits focused and logically coherent. +- Prefer one component or one phase per commit where practical. +- Do not mix unrelated changes. +- Review the staged diff before committing. +- Never commit secrets, credentials, local configuration, or generated build + artifacts. +- Do not create a commit unless the user explicitly asks for one or the task + explicitly requires it. +- When committing, write concise, descriptive messages in imperative mood. + +## Coding conventions + +### Language baseline + +- C++20 is mandatory. The project must compile with `-std=c++20` and all + source must use C++20 idioms. +- Prefer standard library facilities over hand-written alternatives. +- No C-style constructs when a modern C++ equivalent exists. + +### Modern C++ guidelines + +- **Ownership**: use `std::unique_ptr` and `std::shared_ptr`; never use raw + `new`/`delete` or raw owning pointers. +- **Optional values**: use `std::optional` instead of sentinel values or + out-parameters. +- **Fallible operations**: use `std::expected` or `std::error_code` for + recoverable errors. Avoid exceptions for control flow. +- **References and views**: use `std::span` for non-owning contiguous data + and `std::string_view` for read-only strings. +- **Compile-time computation**: prefer `constexpr`, `consteval`, and + `std::integral_constant` where applicable. +- **Type safety**: use `enum class`, `std::variant`, and `std::optional` + instead of bare integers or bool flags. +- **Concurrency**: prefer `std::jthread`, `std::stop_token`, and standard + synchronization primitives. Avoid manual thread lifecycle management. +- **Containers**: use `std::vector`, `std::array`, `std::deque`, and standard + containers; avoid C arrays and raw buffers. +- **Formatting and time**: use `` / `std::format` and `` instead + of `printf`, `sprintf`, or manual time math. +- **Algorithms**: prefer `std::ranges` algorithms over hand-rolled loops + where clarity improves. +- **Lambdas and callable abstractions**: use `std::function` sparingly; prefer + function pointers, templates, or `auto` parameters for simple callbacks. +- **Concepts and constraints**: use `requires` clauses or `std::concepts` to + express interface contracts where beneficial. + +### Style + +- Use `namespace sc` for all project code. +- Prefer `snake_case` for functions and variables, `PascalCase` for types, + `SCREAMING_SNAKE_CASE` for constants. +- Keep headers minimal and free of unnecessary includes; forward-declare where + possible. +- Format code with the configured formatter (`clang-format` if available). + +### Error handling + +- Use explicit error handling. Prefer `std::expected`, `std::optional`, or + `std::error_code` over exceptions for recoverable errors. +- Use RAII for resource cleanup; do not leave resources unreleased on error + paths. +- Avoid `assert` for user-facing error conditions; use real error returns or + logging. diff --git a/README.md b/README.md new file mode 100644 index 0000000..413d9f6 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# screen_cast + +A native Linux peer-to-peer screencast application. + +- **Send** your desktop or a window to another Linux machine. +- **Receive** a stream and render it in a window. +- Built with **C++20**, **Meson**, **PipeWire**, **FFmpeg**, **RTP/UDP**, and + **SDL**. + +> This project is in early development. Only the skeleton exists so far. + +## Quick start + +Requirements: + +- C++20 compiler (GCC 12+, Clang 16+) +- Meson + Ninja +- (later phases) FFmpeg dev packages, PipeWire dev, SDL2/3 dev + +Build and run tests: + +```sh +meson setup build +meson compile -C build +meson test -C build --print-errorlogs +``` + +## Architecture + +See `docs/ARCHITECTURE.md` for module boundaries and design rules. + +## Roadmap + +Development is split into phases in `docs/PHASES.md`. + +Current phase: **Phase 1 — project skeleton**. + +## License + +MIT — see `LICENSE` (to be added). diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..f4bfab6 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,69 @@ +# Architecture + +`screen_cast` is a native Linux screencast application that sends and receives +desktop video over the LAN. It is intentionally split into small, replaceable +modules so that capture, codec, network, and rendering concerns can evolve +independently. + +## Goals + +- Works on modern Linux desktops (Wayland and X11). +- Low-latency peer-to-peer streaming. +- CLI-first; optional GUI later. +- Software and hardware-accelerated encoding paths. +- Minimal, auditable network protocol. + +## Module overview + +``` +┌─────────────┐ ┌────────────┐ ┌─────────────┐ ┌──────────┐ +│ Capture │────▶│ Encoder │────▶│ Packetize │────▶│ RTP/UDP │ Sender +│ (PipeWire) │ │ (FFmpeg) │ │ (RTP) │ │ Transport│ +└─────────────┘ └────────────┘ └─────────────┘ └──────────┘ + +┌──────────┐ ┌─────────────┐ ┌────────────┐ ┌─────────────┐ +│ RTP/UDP │────▶│ Depacketize │────▶│ Decoder │────▶│ Renderer │ Receiver +│Transport │ │ (RTP) │ │ (FFmpeg) │ │ (SDL/GL) │ +└──────────┘ └─────────────┘ └────────────┘ └─────────────┘ + +┌──────────────────────────────────────────────────────────────────┐ +│ Control / Signaling / Discovery │ +│ WebSocket/JSON control + mDNS/Avahi discovery │ +└──────────────────────────────────────────────────────────────────┘ +``` + +## Module responsibilities + +| Module | Directory | Responsibility | +|---|---|---| +| Capture | `include/screencast/capture/` | Acquire raw frames from PipeWire/portal. | +| Codec | `include/screencast/codec/` | Encode to / decode from H.264 with FFmpeg. | +| Network | `include/screencast/network/` | RTP framing, UDP transport, signaling, discovery. | +| Render | `include/screencast/render/` | Display decoded frames. | +| App | `include/screencast/app/` | CLI parsing, sender/receiver pipelines. | + +## Pipeline rules + +- Each pipeline owns its modules and threads. +- Frames are transferred as owned buffers (`std::vector`). +- Timestamps start in the capture clock domain and are converted to RTP clock + domain once at packetization. +- On sender error the transport is drained and stopped. +- On receiver loss a PLI is sent over signaling; the sender inserts a keyframe. + +## Namespace + +All project code lives in `namespace sc`. + +## Dependencies (planned) + +| Library | pkg-config | Phase | +|---|---|---| +| FFmpeg | `libavcodec`, `libavutil`, `libswscale` | 2 | +| PipeWire | `libpipewire-0.3` | 3 | +| SDL2/3 | `sdl2` / `sdl3` | 5 | +| ASIO | bundled or standalone `asio` | 6 | +| Avahi | `avahi-client` | 7 | +| WebSocket | `websocketpp` or `uWebSockets` | 7 | + +See `docs/PHASES.md` for the phased build plan. diff --git a/docs/PHASES.md b/docs/PHASES.md new file mode 100644 index 0000000..19dffae --- /dev/null +++ b/docs/PHASES.md @@ -0,0 +1,92 @@ +# Development Phases + +This file breaks the project into incremental milestones. Each phase produces a +working, testable slice of functionality. Do not start a phase until the +previous one is validated. + +## Phase 1 — Project Skeleton + +**Goal**: configure, compile, and run tests with no real dependencies. + +- [x] Meson build files (`meson.build`, `meson_options.txt`). +- [x] Public module headers with `namespace sc`. +- [ ] Unit test that exercises a trivial utility function. +- [ ] `README.md` with build instructions. + +**Validation**: `meson setup build && meson compile -C build && meson test -C build`. + +## Phase 2 — Software H.264 Encode / Decode + +**Goal**: encode raw pixel buffers to H.264 and decode them back, purely with +FFmpeg software paths. + +- Add `libavcodec`, `libavutil`, `libswscale` dependencies. +- Implement `EncoderFactory::create()` and `Encoder::encode()`. +- Implement `DecoderFactory::create()` and `Decoder::decode()`. +- Round-trip test: synthetic RGB frames → H.264 → decoded RGB. + +**Validation**: unit test produces visually/structurally correct round-trip +frames. + +## Phase 3 — PipeWire Screen Capture + +**Goal**: capture the desktop and feed frames into the encoder. + +- Add `libpipewire-0.3` dependency. +- Implement `CaptureFactory::create()` using the xdg-desktop-portal. +- Wire `CaptureSession::next_frame()` → `Encoder::encode()` in a local smoke + test that just writes a few encoded frames to disk. + +**Validation**: manual run on a real Linux desktop session produces a valid +H.264 bitstream. + +## Phase 4 — RTP Framing + +**Goal**: packetize NAL units into RTP and depacketize them. + +- Implement `RtpHeader` and `RtpPacket` serialize/parse. +- Add H.264 NAL splitting and FU-A fragmentation. +- Unit test for serialization, fragmentation, and reassembly. + +**Validation**: unit tests cover single-NAL and fragmented packet paths. + +## Phase 5 — Local UDP Sender → Receiver Loopback + +**Goal**: send RTP packets over UDP and render the result locally. + +- Implement `RtpTransport` with ASIO or raw UDP sockets. +- Wire sender pipeline: capture → encode → RTP → localhost UDP. +- Wire receiver pipeline: localhost UDP → RTP → decode → renderer. +- Add SDL2/3 dependency and a minimal `Renderer`. + +**Validation**: `screencast --send` and `screencast --receive` on the same +machine show the captured desktop in a window. + +## Phase 6 — LAN Signaling and Discovery + +**Goal**: two peers on the same LAN can find each other and negotiate a +session. + +- Implement mDNS/DNS-SD discovery with Avahi. +- Implement JSON WebSocket signaling. +- Extend CLI with `--peer-address` or `--discover`. + +**Validation**: two machines on the same LAN connect without hard-coded IP +addresses. + +## Phase 7 — Resilience and Polish + +**Goal**: loss recovery, hardware acceleration, and packaging. + +- NACK / PLI feedback loop. +- Jitter buffer on the receiver. +- VAAPI/NVENC hardware encode probes and fallback. +- Optional GUI target behind `meson -Dgui=true`. +- `.desktop` file, icon, packaging notes. + +**Validation**: sustained streaming under packet loss; hardware accel smoke +where available. + +## Current phase + +Phase 1 — skeleton and tooling. diff --git a/include/screencast/app/cli.h b/include/screencast/app/cli.h new file mode 100644 index 0000000..d83e3d7 --- /dev/null +++ b/include/screencast/app/cli.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include + +namespace sc { + +struct SendCommand { + std::string_view target = "monitor"; // monitor, window, region + std::string_view peer_address; // optional + int bitrate_kbps = 4000; +}; + +struct ReceiveCommand { + std::string_view peer_address; // optional + int local_rtp_port = 5004; +}; + +using Command = std::variant; + +// Parse command line arguments. Prints usage and returns std::nullopt on error. +std::optional parse_cli ( int argc, const char *argv[] ); + +} // namespace sc diff --git a/include/screencast/app/pipeline.h b/include/screencast/app/pipeline.h new file mode 100644 index 0000000..4a2c1c0 --- /dev/null +++ b/include/screencast/app/pipeline.h @@ -0,0 +1,58 @@ +#pragma once + +#include "screencast/capture/capture.h" +#include "screencast/codec/decoder.h" +#include "screencast/codec/encoder.h" +#include "screencast/network/discovery.h" +#include "screencast/network/signaling.h" +#include "screencast/network/transport.h" +#include "screencast/render/renderer.h" + +#include +#include + +namespace sc { + +struct SenderPipelineConfig { + CaptureTarget capture_target; + EncoderConfig encoder; + Endpoint local_rtp_endpoint; + std::optional signaling_server; +}; + +struct ReceiverPipelineConfig { + DecoderConfig decoder; + Endpoint local_rtp_endpoint; + std::optional peer_signaling; + RendererConfig renderer; +}; + +// Sender pipeline: capture → encode → packetize → RTP/UDP. +class SenderPipeline { + public: + explicit SenderPipeline ( SenderPipelineConfig config ); + ~SenderPipeline (); + + bool start (); + void stop (); + + private: + class Impl; + std::unique_ptr impl_; +}; + +// Receiver pipeline: RTP/UDP → depacketize → decode → render. +class ReceiverPipeline { + public: + explicit ReceiverPipeline ( ReceiverPipelineConfig config ); + ~ReceiverPipeline (); + + bool start (); + void stop (); + + private: + class Impl; + std::unique_ptr impl_; +}; + +} // namespace sc diff --git a/include/screencast/capture/capture.h b/include/screencast/capture/capture.h new file mode 100644 index 0000000..b81d0a2 --- /dev/null +++ b/include/screencast/capture/capture.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace sc { + +// Opaque resource owned by the capture implementation. +struct CapturedFrame { + int width = 0; + int height = 0; + uint64_t timestamp_ns = 0; // capture clock, monotonic + std::vector pixels; + // TODO: pixel format, stride, FD handle for dmabuf +}; + +// Capture target: whole monitor, specific window, or a region. +struct CaptureTargetWholeScreen {}; +struct CaptureTargetWindow { + std::string_view window_id; +}; +struct CaptureTargetRegion { + int x = 0; + int y = 0; + int width = 0; + int height = 0; +}; + +using CaptureTarget = std::variant< + CaptureTargetWholeScreen, + CaptureTargetWindow, + CaptureTargetRegion>; + +class CaptureSession { +public: + virtual ~CaptureSession() = default; + + // Blocking call to acquire one frame. Returns std::nullopt on graceful stop. + virtual std::optional next_frame() = 0; + + // Request the session to stop. May be called from another thread. + virtual void stop() = 0; +}; + +// Factory for the PipeWire / xdg-desktop-portal capture backend. +class CaptureFactory { +public: + static std::unique_ptr create(CaptureTarget target); +}; + +} // namespace sc diff --git a/include/screencast/codec/decoder.h b/include/screencast/codec/decoder.h new file mode 100644 index 0000000..821ea2e --- /dev/null +++ b/include/screencast/codec/decoder.h @@ -0,0 +1,39 @@ +#pragma once + +#include "screencast/codec/encoder.h" + +#include +#include +#include +#include +#include + +namespace sc { + +struct DecodedFrame { + int width = 0; + int height = 0; + uint64_t capture_timestamp_ns = 0; + std::vector rgba_pixels; +}; + +struct DecoderConfig { + std::string_view codec_name = "h264"; + int width = 0; + int height = 0; +}; + +class Decoder { + public: + virtual ~Decoder () = default; + + // Feed one encoded frame. Returns decoded frames when available. + virtual std::vector decode ( const EncodedFrame &frame ) = 0; +}; + +class DecoderFactory { + public: + static std::unique_ptr create ( const DecoderConfig &config ); +}; + +} // namespace sc diff --git a/include/screencast/codec/encoder.h b/include/screencast/codec/encoder.h new file mode 100644 index 0000000..becbc21 --- /dev/null +++ b/include/screencast/codec/encoder.h @@ -0,0 +1,47 @@ +#pragma once + +#include "screencast/capture/capture.h" + +#include +#include +#include +#include +#include + +namespace sc { + +struct EncodedFrame { + uint64_t capture_timestamp_ns = 0; + uint32_t rtp_timestamp = 0; + bool is_keyframe = false; + std::vector data; // Annex-B or AVCC depending on config +}; + +struct EncoderConfig { + std::string_view codec_name = "h264"; + int width = 0; + int height = 0; + int frame_rate_num = 30; + int frame_rate_den = 1; + int bitrate_kbps = 4000; + bool hardware_accel = false; +}; + +class Encoder { + public: + virtual ~Encoder () = default; + + // Encode one captured frame. Returns empty if the encoder emits no packet + // for this frame. + virtual std::vector encode ( const CapturedFrame &frame ) = 0; + + // Force the next output to be a keyframe. + virtual void request_keyframe () = 0; +}; + +class EncoderFactory { + public: + static std::unique_ptr create ( const EncoderConfig &config ); +}; + +} // namespace sc diff --git a/include/screencast/network/discovery.h b/include/screencast/network/discovery.h new file mode 100644 index 0000000..6d4c053 --- /dev/null +++ b/include/screencast/network/discovery.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +namespace sc { + +struct DiscoveredPeer { + std::string service_name; + std::string host; + uint16_t signaling_port = 0; +}; + +class DiscoveryService { + public: + using PeerCallback = std::function; + + virtual ~DiscoveryService () = default; + + // Announce this peer on the LAN. + virtual bool announce ( const std::string &service_name, uint16_t signaling_port ) = 0; + + // Browse for peers; callback is invoked for each newly found service. + virtual bool browse ( PeerCallback on_peer ) = 0; + + virtual void stop () = 0; +}; + +class DiscoveryFactory { + public: + static std::unique_ptr create_avahi (); +}; + +} // namespace sc diff --git a/include/screencast/network/rtp_packet.h b/include/screencast/network/rtp_packet.h new file mode 100644 index 0000000..a78c9f3 --- /dev/null +++ b/include/screencast/network/rtp_packet.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +namespace sc { + +// Minimal RTP header (RFC 3550) without extensions. +struct RtpHeader { + uint8_t version = 2; + bool padding = false; + bool extension = false; + uint8_t csrc_count = 0; + bool marker = false; + uint7_t payload_type = 96; // dynamic + uint16_t sequence_number = 0; + uint32_t timestamp = 0; + uint32_t ssrc = 0; + + bool serialize ( std::span out ) const noexcept; + static std::optional parse ( std::span in ) noexcept; +}; + +struct RtpPacket { + RtpHeader header; + std::vector payload; + + std::vector serialize () const; + static std::optional parse ( std::span in ) noexcept; +}; + +} // namespace sc diff --git a/include/screencast/network/signaling.h b/include/screencast/network/signaling.h new file mode 100644 index 0000000..ccaf2af --- /dev/null +++ b/include/screencast/network/signaling.h @@ -0,0 +1,47 @@ +#pragma once + +#include "screencast/network/transport.h" + +#include +#include +#include + +namespace sc { + +// JSON-based control messages exchanged before or during a session. +struct SessionOffer { + std::string session_id; + std::string codec_name; + int width = 0; + int height = 0; + int frame_rate_num = 30; + int frame_rate_den = 1; + Endpoint rtp_endpoint; +}; + +struct SessionAnswer { + std::string session_id; + Endpoint rtp_endpoint; +}; + +using SignalingMessage = std::variant; + +class SignalingChannel { + public: + using MessageCallback = std::function; + + virtual ~SignalingChannel () = default; + + virtual bool connect ( const Endpoint &server ) = 0; + virtual void send ( const SignalingMessage &message ) = 0; + virtual void on_message ( MessageCallback callback ) = 0; + virtual void disconnect () = 0; +}; + +class SignalingFactory { + public: + static std::unique_ptr create_websocket_client (); + static std::unique_ptr create_websocket_server ( uint16_t port ); +}; + +} // namespace sc diff --git a/include/screencast/network/transport.h b/include/screencast/network/transport.h new file mode 100644 index 0000000..415b70c --- /dev/null +++ b/include/screencast/network/transport.h @@ -0,0 +1,43 @@ +#pragma once + +#include "screencast/network/rtp_packet.h" + +#include +#include +#include +#include + +namespace sc { + +struct Endpoint { + std::string address; + uint16_t port = 0; +}; + +// UDP transport for RTP packets. Owned by the sender or receiver pipeline. +class RtpTransport { + public: + using ReceiveCallback = std::function; + + virtual ~RtpTransport () = default; + + // Bind locally and start the receive loop. Callback is invoked on the + // transport's thread. + virtual bool start ( const Endpoint &local_endpoint, ReceiveCallback on_receive ) = 0; + + // Send a packet to the configured peer. + virtual bool send ( const RtpPacket &packet ) = 0; + + // Set the peer endpoint dynamically (e.g. after signaling). + virtual void set_peer ( const Endpoint &peer ) = 0; + + // Stop the transport and close sockets. + virtual void stop () = 0; +}; + +class RtpTransportFactory { + public: + static std::unique_ptr create (); +}; + +} // namespace sc diff --git a/include/screencast/render/renderer.h b/include/screencast/render/renderer.h new file mode 100644 index 0000000..962e8c4 --- /dev/null +++ b/include/screencast/render/renderer.h @@ -0,0 +1,37 @@ +#pragma once + +#include "screencast/codec/decoder.h" + +#include +#include +#include +#include + +namespace sc { + +struct RendererConfig { + std::string_view window_title = "screencast receiver"; + int initial_width = 1280; + int initial_height = 720; +}; + +class Renderer { + public: + virtual ~Renderer () = default; + + // Present one decoded frame. Returns false if the window was closed. + virtual bool present ( const DecodedFrame &frame ) = 0; + + // Pump events (window close, resize). Non-blocking. + virtual bool poll_events () = 0; + + // Destroy the window and release GPU resources. + virtual void shutdown () = 0; +}; + +class RendererFactory { + public: + static std::unique_ptr create ( const RendererConfig &config ); +}; + +} // namespace sc diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..933e6f7 --- /dev/null +++ b/meson.build @@ -0,0 +1,26 @@ +project('screen_cast', 'cpp', + version : '0.1.0', + default_options : [ + 'cpp_std=c++20', + 'warning_level=3', + 'werror=false', + 'buildtype=release', + ]) + +# Public and private include directories +inc = include_directories('include') + +# Build the core static library first; dependencies will be added later as +# phases require them. +subdir('src') + +# Tests +enable_tests = get_option('tests') +if enable_tests + subdir('tests') +endif + +# Summary for the user +summary({ + 'tests': enable_tests, +}, section: 'Build options') diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..1ba0de3 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,5 @@ +option('tests', type : 'boolean', value : true, + description : 'Build unit and integration tests') + +option('gui', type : 'boolean', value : false, + description : 'Build the optional GUI target (deferred to a later phase)') diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..adad535 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,2 @@ +# Empty stub: implementation files will be added phase by phase. +# For now the project configures successfully with no targets. diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 0000000..1a29ca8 --- /dev/null +++ b/tests/meson.build @@ -0,0 +1 @@ +# Empty stub: tests will be added as modules land.