Files
screen_cast/src/render/sdl_renderer.cpp
T
fegger 10870bc6c9 feat(app): implement Phase 5 local UDP sender->receiver loopback
Wire the first end-to-end pipeline: capture -> encode -> packetize ->
UDP -> depacketize -> decode -> render.

- UdpRtpTransport: raw POSIX UDP sockets (IPv4 via getaddrinfo), a
  receive jthread woken by socket close on stop; port 0 skips binding
  so the sender uses an OS-assigned source port. ASIO stays deferred
  to the signaling phase per ARCHITECTURE.md.
- SdlRenderer: SDL3 window/renderer with RGBA texture upload; the
  texture is recreated on resolution change. RendererFactory now
  returns RendererResult so SDL init failures carry a message,
  mirroring the codec/capture error patterns.
- screencast binary: parse_cli plus SenderPipeline/ReceiverPipeline
  per the app scaffolds; the sender creates its encoder once capture
  reports real dimensions, the receiver keeps a bounded 3-frame queue
  to hold latency down and renders on its own thread until the window
  closes. cli argv signature fixed to 'const char* const*' so main's
  argv converts implicitly.
- Encoder: drop AV_CODEC_FLAG_GLOBAL_HEADER so libx264 repeats SPS/PPS
  in-band at each keyframe -- the receiver decodes from the bitstream
  alone, which also makes mid-stream joins and later PLI recovery
  work without out-of-band parameter negotiation. The round-trip test
  now exercises exactly that path.
- tests: new udp-loopback integration test pushes synthetic frames
  through a real localhost socket and decodes 10/10 frames with the
  right dimensions; valgrind clean (loopback + codec). meson test 4/4.

Manual validation on the desktop (receiver window shows the captured
desktop) is documented in docs/RUNBOOK.md.
2026-09-07 11:02:40 +02:00

148 lines
4.2 KiB
C++

#include "screencast/render/renderer.h"
#include <SDL3/SDL.h>
#include <string>
namespace sc {
namespace {
// DecodedFrame pixels are AV_PIX_FMT_RGBA: memory order R, G, B, A, which
// matches SDL_PIXELFORMAT_RGBA8888.
constexpr SDL_PixelFormat kSdlPixelFormat = SDL_PIXELFORMAT_RGBA8888;
class SdlRenderer final : public Renderer {
public:
explicit SdlRenderer(const RendererConfig& config) : config_(config) {}
~SdlRenderer() override {
shutdown();
}
SdlRenderer(const SdlRenderer&) = delete;
SdlRenderer& operator=(const SdlRenderer&) = delete;
const std::string& last_error() const {
return last_error_;
}
bool initialize() {
if (!SDL_Init(SDL_INIT_VIDEO)) {
last_error_ = std::string{"SDL_Init failed: "} + SDL_GetError();
return false;
}
sdl_inited_ = true;
window_ = SDL_CreateWindow(config_.window_title.c_str(), config_.initial_width, config_.initial_height, 0);
if (window_ == nullptr) {
last_error_ = std::string{"SDL_CreateWindow failed: "} + SDL_GetError();
shutdown();
return false;
}
renderer_ = SDL_CreateRenderer(window_, nullptr);
if (renderer_ == nullptr) {
last_error_ = std::string{"SDL_CreateRenderer failed: "} + SDL_GetError();
shutdown();
return false;
}
return true;
}
bool present(const DecodedFrame& frame) override {
if (frame.width <= 0 || frame.height <= 0) {
return false;
}
if (frame.width != texture_width_ || frame.height != texture_height_) {
if (!recreate_texture(frame.width, frame.height)) {
return false;
}
}
const int pitch = frame.width * 4;
if (!SDL_UpdateTexture(texture_, nullptr, frame.rgba_pixels.data(), pitch)) {
return false;
}
if (!SDL_RenderTexture(renderer_, texture_, nullptr, nullptr)) {
return false;
}
SDL_RenderPresent(renderer_);
return true;
}
bool poll_events() override {
if (closed_) {
return false;
}
SDL_Event event{};
while (SDL_PollEvent(&event)) {
if (event.type == SDL_EVENT_QUIT ||
(event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED && event.window.windowID == SDL_GetWindowID(window_))) {
closed_ = true;
return false;
}
}
return true;
}
void shutdown() override {
if (texture_ != nullptr) {
SDL_DestroyTexture(texture_);
texture_ = nullptr;
}
if (renderer_ != nullptr) {
SDL_DestroyRenderer(renderer_);
renderer_ = nullptr;
}
if (window_ != nullptr) {
SDL_DestroyWindow(window_);
window_ = nullptr;
}
if (sdl_inited_) {
SDL_Quit();
sdl_inited_ = false;
}
}
private:
bool recreate_texture(int width, int height) {
if (texture_ != nullptr) {
SDL_DestroyTexture(texture_);
texture_ = nullptr;
}
texture_ = SDL_CreateTexture(renderer_, kSdlPixelFormat, SDL_TEXTUREACCESS_STREAMING, width, height);
if (texture_ == nullptr) {
last_error_ = std::string{"SDL_CreateTexture failed: "} + SDL_GetError();
texture_width_ = 0;
texture_height_ = 0;
return false;
}
texture_width_ = width;
texture_height_ = height;
return true;
}
RendererConfig config_;
std::string last_error_;
bool sdl_inited_ = false;
bool closed_ = false;
SDL_Window* window_ = nullptr;
SDL_Renderer* renderer_ = nullptr;
SDL_Texture* texture_ = nullptr;
int texture_width_ = 0;
int texture_height_ = 0;
};
} // namespace
RendererResult<std::unique_ptr<Renderer>> RendererFactory::create(const RendererConfig& config) {
auto renderer = std::make_unique<SdlRenderer>(config);
if (!renderer->initialize()) {
return RendererError{renderer->last_error()};
}
return std::unique_ptr<Renderer>(std::move(renderer));
}
} // namespace sc