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.
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user