feat(app): add PLI feedback, jitter reordering, and hardware decode
Loss recovery for the streaming path: - PLI over signaling: the depacketizer now reports damaged frames (DepacketizeResult) and the receiver asks the sender for a keyframe (SessionPli, rate-limited to one per 500 ms). The sender keeps the signaling channel open during the session and honors PLIs through the new thread-safe SenderPipeline::request_keyframe(). Recovery takes one frame time instead of waiting out the GOP. - RtpJitterBuffer: reorders RTP packets by sequence number (16 packets / 60 ms) before the in-order depacketizer, so Wi-Fi reordering is not misread as loss; in-order streams release immediately, and a straggler older than the delivered sequence is discarded. - Hardware H.264 decode probe: DecoderFactory tries h264_v4l2m2m (the VideoCore path on the Pi) with an automatic software fallback and a clear journal line for the chosen path; --swdecode opts out. Validated: PLI end-to-end with a probe that drops a mid-keyframe packet over real UDP (receiver logged the damaged frame and the PLI arrived with the session id); hardware probe fails cleanly and falls back on this desktop; jitter reordering covered by unit tests. meson test 5/5 in both build configurations, valgrind clean.
This commit is contained in:
+3
-1
@@ -9,7 +9,7 @@ namespace {
|
||||
|
||||
void print_usage() {
|
||||
std::fputs("usage: screencast --send [--target monitor|window] [--peer HOST[:PORT]] [--bitrate KBPS]\n"
|
||||
" screencast --receive [--port PORT] [--signaling-port PORT] [--fullscreen]\n"
|
||||
" screencast --receive [--port PORT] [--signaling-port PORT] [--fullscreen] [--swdecode]\n"
|
||||
" screencast --discover [--timeout SECONDS]\n"
|
||||
"\n"
|
||||
"--send without --peer discovers a receiver on the LAN and requires\n"
|
||||
@@ -112,6 +112,8 @@ std::optional<Command> parse_cli(int argc, const char* const argv[]) {
|
||||
receive.signaling_port = port;
|
||||
} else if (argument == "--fullscreen") {
|
||||
receive.fullscreen = true;
|
||||
} else if (argument == "--swdecode") {
|
||||
receive.software_decode = true;
|
||||
} else if (argument == "--timeout") {
|
||||
std::string_view value;
|
||||
if (!next_argument(argc, argv, index, value) || !parse_int(value, discover.timeout_seconds) ||
|
||||
|
||||
+12
-1
@@ -167,7 +167,8 @@ int negotiate_and_stream(sc::SignalingChannel& channel, const sc::Endpoint& sign
|
||||
return 1;
|
||||
}
|
||||
const sc::SessionAnswer answer = answer_future.get();
|
||||
channel.disconnect();
|
||||
// The channel stays open: the receiver sends PLI keyframe requests over
|
||||
// it during the session.
|
||||
|
||||
if (answer.session_id != offer.session_id) {
|
||||
std::cerr << "screencast: session mismatch in the receiver's answer\n";
|
||||
@@ -196,12 +197,21 @@ int negotiate_and_stream(sc::SignalingChannel& channel, const sc::Endpoint& sign
|
||||
|
||||
sc::SenderPipeline pipeline{std::move(config)};
|
||||
if (!pipeline.start()) {
|
||||
channel.disconnect();
|
||||
return 1;
|
||||
}
|
||||
channel.on_message([&](const sc::SignalingMessage& message) {
|
||||
const sc::SessionPli* pli = std::get_if<sc::SessionPli>(&message);
|
||||
if (pli != nullptr && pli->session_id == offer.session_id) {
|
||||
pipeline.request_keyframe();
|
||||
std::cerr << "screencast: receiver requested a keyframe\n";
|
||||
}
|
||||
});
|
||||
while (!g_interrupted.load()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
pipeline.stop();
|
||||
channel.disconnect();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -289,6 +299,7 @@ int run_receiver(const sc::ReceiveCommand& command) {
|
||||
sc::ReceiverPipelineConfig config;
|
||||
config.local_rtp_endpoint = sc::Endpoint{"0.0.0.0", static_cast<std::uint16_t>(command.local_rtp_port)};
|
||||
config.signaling_port = static_cast<std::uint16_t>(command.signaling_port);
|
||||
config.decoder.hardware_accel = !command.software_decode;
|
||||
// Fullscreen is automatic under KMSDRM (headless); the flag forces it
|
||||
// on desktop sessions.
|
||||
config.renderer.fullscreen = command.fullscreen;
|
||||
|
||||
+50
-3
@@ -64,9 +64,17 @@ class SenderPipeline::Impl {
|
||||
transport_->stop();
|
||||
}
|
||||
|
||||
void request_keyframe() {
|
||||
keyframe_requested_.store(true);
|
||||
}
|
||||
|
||||
private:
|
||||
void run(std::stop_token stop_token) {
|
||||
while (!stop_token.stop_requested()) {
|
||||
if (keyframe_requested_.exchange(false) && encoder_ != nullptr) {
|
||||
encoder_->request_keyframe();
|
||||
}
|
||||
|
||||
const std::optional<CapturedFrame> frame = capture_->next_frame();
|
||||
if (!frame.has_value()) {
|
||||
break;
|
||||
@@ -109,8 +117,13 @@ class SenderPipeline::Impl {
|
||||
H264Packetizer packetizer_;
|
||||
std::unique_ptr<RtpTransport> transport_ = RtpTransportFactory::create();
|
||||
std::jthread run_thread_;
|
||||
std::atomic<bool> keyframe_requested_{false};
|
||||
};
|
||||
|
||||
void SenderPipeline::request_keyframe() {
|
||||
impl_->request_keyframe();
|
||||
}
|
||||
|
||||
SenderPipeline::SenderPipeline(SenderPipelineConfig config) : impl_(std::make_unique<Impl>(std::move(config))) {}
|
||||
|
||||
SenderPipeline::~SenderPipeline() = default;
|
||||
@@ -238,6 +251,7 @@ class ReceiverPipeline::Impl {
|
||||
signaling_ = nullptr;
|
||||
discovery_ = nullptr;
|
||||
transport_->stop();
|
||||
jitter_.clear();
|
||||
renderer_ = nullptr;
|
||||
decoder_ = nullptr;
|
||||
{
|
||||
@@ -248,13 +262,24 @@ class ReceiverPipeline::Impl {
|
||||
|
||||
private:
|
||||
void on_packet(RtpPacket packet) {
|
||||
std::optional<std::vector<std::byte>> access_unit = depacketizer_.depacketize(packet);
|
||||
if (!access_unit.has_value()) {
|
||||
// Absorb reordering (Wi-Fi) before the in-order depacketizer, so a
|
||||
// late packet is not misread as loss.
|
||||
for (RtpPacket& ordered : jitter_.push(std::move(packet))) {
|
||||
deliver_packet(ordered);
|
||||
}
|
||||
}
|
||||
|
||||
void deliver_packet(const RtpPacket& packet) {
|
||||
const DepacketizeResult result = depacketizer_.depacketize(packet);
|
||||
if (result.frame_dropped) {
|
||||
maybe_send_pli();
|
||||
}
|
||||
if (!result.access_unit.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
EncodedFrame encoded;
|
||||
encoded.data = std::move(*access_unit);
|
||||
encoded.data = std::move(*result.access_unit);
|
||||
encoded.rtp_timestamp = packet.header.timestamp;
|
||||
encoded.is_keyframe = false;
|
||||
|
||||
@@ -282,6 +307,7 @@ class ReceiverPipeline::Impl {
|
||||
if (offer == nullptr) {
|
||||
return;
|
||||
}
|
||||
session_id_ = offer->session_id;
|
||||
SessionAnswer answer;
|
||||
answer.session_id = offer->session_id;
|
||||
// Empty address: the sender targets the address of its signaling
|
||||
@@ -290,6 +316,23 @@ class ReceiverPipeline::Impl {
|
||||
signaling_->send(answer);
|
||||
}
|
||||
|
||||
// Ask the sender for a keyframe after a damaged frame, rate-limited so
|
||||
// sustained loss cannot flood the signaling channel.
|
||||
void maybe_send_pli() {
|
||||
if (signaling_ == nullptr || session_id_.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - last_pli_time_ < kPliMinInterval) {
|
||||
return;
|
||||
}
|
||||
last_pli_time_ = now;
|
||||
SessionPli pli;
|
||||
pli.session_id = session_id_;
|
||||
signaling_->send(pli);
|
||||
std::cerr << std::format("screencast: frame damaged; requesting a keyframe\n");
|
||||
}
|
||||
|
||||
void render_loop(std::stop_token stop_token) {
|
||||
while (!stop_token.stop_requested()) {
|
||||
if (!renderer_->poll_events()) {
|
||||
@@ -315,17 +358,21 @@ class ReceiverPipeline::Impl {
|
||||
}
|
||||
|
||||
static constexpr std::size_t kMaxQueuedFrames = 3;
|
||||
static constexpr std::chrono::milliseconds kPliMinInterval{500};
|
||||
|
||||
ReceiverPipelineConfig config_;
|
||||
std::unique_ptr<Renderer> renderer_;
|
||||
std::unique_ptr<Decoder> decoder_;
|
||||
H264Depacketizer depacketizer_;
|
||||
RtpJitterBuffer jitter_;
|
||||
std::unique_ptr<RtpTransport> transport_ = RtpTransportFactory::create();
|
||||
std::unique_ptr<SignalingChannel> signaling_;
|
||||
std::unique_ptr<DiscoveryService> discovery_;
|
||||
std::jthread render_thread_;
|
||||
std::mutex queue_mutex_;
|
||||
std::deque<DecodedFrame> queue_;
|
||||
std::string session_id_;
|
||||
std::chrono::steady_clock::time_point last_pli_time_{};
|
||||
bool stream_started_ = false;
|
||||
bool present_error_logged_ = false;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user