Scaffold C++20 screencast project with Meson, agent workflow, and phase plan

This commit is contained in:
2026-08-28 21:54:32 +02:00
commit 742611b841
25 changed files with 1322 additions and 0 deletions
+34
View File
@@ -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.
+51
View File
@@ -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.
- `<domain>/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.
+91
View File
@@ -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<T,E>`, `std::optional<T>`, 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<T>` instead of sentinel values or
out-parameters.
- **Fallible operations**: use `std::expected<T,E>` or `std::error_code` for
recoverable errors; avoid exceptions for control flow.
- **Views**: use `std::span<T>` 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 `<format>` / `std::format` and `<chrono>`.
- **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`
@@ -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`
+69
View File
@@ -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`
+73
View File
@@ -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`
+28
View File
@@ -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
+264
View File
@@ -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::`, `<format>`, `<chrono>`, 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<T>` instead of sentinel values or
out-parameters.
- **Fallible operations**: use `std::expected<T,E>` or `std::error_code` for
recoverable errors. Avoid exceptions for control flow.
- **References and views**: use `std::span<T>` 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 `<format>` / `std::format` and `<chrono>` 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.
+40
View File
@@ -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).
+69
View File
@@ -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<std::byte>`).
- 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.
+92
View File
@@ -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.
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <optional>
#include <string_view>
#include <variant>
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<SendCommand, ReceiveCommand>;
// Parse command line arguments. Prints usage and returns std::nullopt on error.
std::optional<Command> parse_cli ( int argc, const char *argv[] );
} // namespace sc
+58
View File
@@ -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 <memory>
#include <optional>
namespace sc {
struct SenderPipelineConfig {
CaptureTarget capture_target;
EncoderConfig encoder;
Endpoint local_rtp_endpoint;
std::optional<Endpoint> signaling_server;
};
struct ReceiverPipelineConfig {
DecoderConfig decoder;
Endpoint local_rtp_endpoint;
std::optional<Endpoint> 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> 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> impl_;
};
} // namespace sc
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include <cstdint>
#include <memory>
#include <optional>
#include <span>
#include <string_view>
#include <variant>
#include <vector>
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<std::byte> 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<CapturedFrame> 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<CaptureSession> create(CaptureTarget target);
};
} // namespace sc
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include "screencast/codec/encoder.h"
#include <cstdint>
#include <memory>
#include <optional>
#include <span>
#include <vector>
namespace sc {
struct DecodedFrame {
int width = 0;
int height = 0;
uint64_t capture_timestamp_ns = 0;
std::vector<std::byte> 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<DecodedFrame> decode ( const EncodedFrame &frame ) = 0;
};
class DecoderFactory {
public:
static std::unique_ptr<Decoder> create ( const DecoderConfig &config );
};
} // namespace sc
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include "screencast/capture/capture.h"
#include <cstdint>
#include <memory>
#include <span>
#include <string_view>
#include <vector>
namespace sc {
struct EncodedFrame {
uint64_t capture_timestamp_ns = 0;
uint32_t rtp_timestamp = 0;
bool is_keyframe = false;
std::vector<std::byte> 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<EncodedFrame> 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<Encoder> create ( const EncoderConfig &config );
};
} // namespace sc
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <functional>
#include <memory>
#include <string>
namespace sc {
struct DiscoveredPeer {
std::string service_name;
std::string host;
uint16_t signaling_port = 0;
};
class DiscoveryService {
public:
using PeerCallback = std::function<void ( const DiscoveredPeer & )>;
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<DiscoveryService> create_avahi ();
};
} // namespace sc
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <cstdint>
#include <span>
#include <vector>
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<std::byte, 12> out ) const noexcept;
static std::optional<RtpHeader> parse ( std::span<const std::byte, 12> in ) noexcept;
};
struct RtpPacket {
RtpHeader header;
std::vector<std::byte> payload;
std::vector<std::byte> serialize () const;
static std::optional<RtpPacket> parse ( std::span<const std::byte> in ) noexcept;
};
} // namespace sc
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include "screencast/network/transport.h"
#include <functional>
#include <memory>
#include <string>
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<SessionOffer, SessionAnswer>;
class SignalingChannel {
public:
using MessageCallback = std::function<void ( const SignalingMessage & )>;
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<SignalingChannel> create_websocket_client ();
static std::unique_ptr<SignalingChannel> create_websocket_server ( uint16_t port );
};
} // namespace sc
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include "screencast/network/rtp_packet.h"
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
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<void ( RtpPacket )>;
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<RtpTransport> create ();
};
} // namespace sc
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include "screencast/codec/decoder.h"
#include <cstdint>
#include <memory>
#include <optional>
#include <string_view>
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<Renderer> create ( const RendererConfig &config );
};
} // namespace sc
+26
View File
@@ -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')
+5
View File
@@ -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)')
+2
View File
@@ -0,0 +1,2 @@
# Empty stub: implementation files will be added phase by phase.
# For now the project configures successfully with no targets.
+1
View File
@@ -0,0 +1 @@
# Empty stub: tests will be added as modules land.