Files
fegger e82e7853d1 feat(app): adaptive quality, frame rate capping, and tighter VBV
Three changes to make the stream survive constrained links:

Adaptive quality: the sender pipeline now tracks the PLI rate from
the receiver. Every 5 seconds it evaluates: >0.5 PLI/s means the
link is saturated (the receiver is dropping frames), so the CRF
increases by 2 (lower quality, fewer bits) and the encoder restarts
with a keyframe. <0.1 PLI/s means the link is stable, so the CRF
decreases by 1 (better quality) and the encoder probes upward.
Clamped to [user CRF, user CRF + 10] so quality never degrades
below what the link can handle, and never exceeds what the user
asked for. The adaptation is logged to stderr for visibility.

Frame rate capping (--fps N): throttles the capture loop to N
frames per second (0 = no cap; monitor rate). At 15fps instead of
60fps, the bandwidth requirement drops 4x at the same quality
level. Desktop content is still smooth at 15-20fps.

Tighter VBV: one frame period of buffer instead of two. A two-frame
buffer lets a keyframe spike to twice the target rate in one burst,
which overflows any constrained hop (Wi-Fi hotspot, slow switch)
and cascades into PLI storms. One frame period keeps bursts
within what the link can absorb in real time.
2026-09-09 13:00:03 +02:00

78 lines
2.2 KiB
C++

#pragma once
#include "screencast/capture/capture.h"
#include "screencast/codec/decoder.h"
#include "screencast/codec/encoder.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;
// Where encoded RTP packets are sent. Defaults to the local loopback so
// a sender and receiver on one machine work without any configuration.
Endpoint peer_rtp_endpoint{"127.0.0.1", 5004};
// Session identity from signaling; written to the sender state file for
// status widgets (waybar) and one-click restarts.
std::string session_id;
// The receiver's display resolution (0 = unknown). The sender downscales
// to this before encoding so bitrate is not spent on pixels the display
// cannot show.
int max_encode_width = 0;
int max_encode_height = 0;
// Cap the encoding frame rate (0 = no cap; use the capture rate).
// Lowering the frame rate halves the bandwidth at the same quality
// level — useful on constrained links.
int max_frame_rate = 0;
};
struct ReceiverPipelineConfig {
DecoderConfig decoder;
Endpoint local_rtp_endpoint;
std::optional<Endpoint> peer_signaling;
RendererConfig renderer;
// TCP port the signaling server listens on (offers arrive here).
std::uint16_t signaling_port = 5005;
};
// Sender pipeline: capture → encode → packetize → RTP/UDP.
class SenderPipeline {
public:
explicit SenderPipeline(SenderPipelineConfig config);
~SenderPipeline();
bool start();
void stop();
// Ask the sender to encode its next frame as a keyframe. Thread-safe;
// used by the PLI feedback path.
void request_keyframe();
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