feat(network): implement Phase 6 LAN discovery and session signaling
Add mDNS/DNS-SD discovery and JSON session negotiation so two peers on a LAN connect without hard-coded addresses. - Discovery (Avahi threaded-poll client): the receiver announces _screencast._tcp with its signaling port; senders browse and resolve peers. Strict lock ordering (poll lock before state mutex) keeps the callbacks deadlock-free; name collisions rename via avahi_alternative_service_name. - Signaling: one JSON object per newline-terminated TCP line. The receiver hosts a server (port 5005) and answers session offers with its RTP port; senders connect, offer, and stream to the negotiated endpoint. WebSocket was deferred: no WS library is installed, the skill permits plain TCP, and the wire format is transport-agnostic. - Dual-stack transports: this machine resolves its own services over IPv6, so getaddrinfo now runs AF_UNSPEC and listeners bind IPv6 with IPV6_V6ONLY=0 (IPv4 fallback), covering UDP and TCP alike. The signaling client shutdown now uses shutdown() so a reader blocked in recv() cannot hang the join (a plain close() does not wake it). - CLI: --discover lists receivers (deduped to one entry per host); --send auto-disovers when exactly one receiver is found; --peer targets a receiver directly; --signaling-port overrides the default. Validation: meson test 5/5 (new signaling round-trip test), valgrind clean. End-to-end over loopback: --discover finds the announced receiver, a probe negotiated a session and streamed 60 frames over both IPv4 and IPv6, and the receiver reported stream started. mDNS resolution was verified against avahi-browse as an independent reference.
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
#include "screencast/app/pipeline.h"
|
||||
|
||||
#include "screencast/network/discovery.h"
|
||||
#include "screencast/network/h264_packetizer.h"
|
||||
#include "screencast/network/signaling.h"
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
@@ -11,6 +16,17 @@
|
||||
#include <thread>
|
||||
|
||||
namespace sc {
|
||||
namespace {
|
||||
|
||||
std::string receiver_service_name() {
|
||||
std::array<char, 256> hostname{};
|
||||
if (::gethostname(hostname.data(), hostname.size()) != 0 || hostname[0] == '\0') {
|
||||
return "Screencast receiver";
|
||||
}
|
||||
return std::string{"Screencast receiver on "} + hostname.data();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class SenderPipeline::Impl {
|
||||
public:
|
||||
@@ -127,6 +143,29 @@ class ReceiverPipeline::Impl {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Signaling server: answer session offers with our RTP port so a
|
||||
// sender can find the media endpoint without configuration.
|
||||
auto signaling_result = SignalingFactory::create_server(config_.signaling_port);
|
||||
if (is_network_error(signaling_result)) {
|
||||
std::cerr << std::format("screencast: {}\n", network_error(signaling_result).message);
|
||||
transport_->stop();
|
||||
return false;
|
||||
}
|
||||
signaling_ = std::move(network_value(signaling_result));
|
||||
signaling_->on_message([this](const SignalingMessage& message) { handle_signaling(message); });
|
||||
|
||||
// mDNS announcement; best effort, since senders can still use --peer.
|
||||
auto discovery_result = DiscoveryFactory::create_avahi();
|
||||
if (is_network_error(discovery_result)) {
|
||||
std::cerr << std::format("screencast: discovery unavailable: {}\n",
|
||||
network_error(discovery_result).message);
|
||||
} else {
|
||||
discovery_ = std::move(network_value(discovery_result));
|
||||
if (!discovery_->announce(receiver_service_name(), config_.signaling_port)) {
|
||||
std::cerr << std::format("screencast: announcing the receiver failed: {}\n", discovery_->last_error());
|
||||
}
|
||||
}
|
||||
|
||||
// Every SDL call — window creation, event pumping, presenting, and
|
||||
// destruction — happens on the render thread. The Wayland backend
|
||||
// does not tolerate cross-thread windows: created on another thread,
|
||||
@@ -163,11 +202,19 @@ class ReceiverPipeline::Impl {
|
||||
|
||||
std::unique_lock lock(init_mutex);
|
||||
if (!init_done_cv.wait_for(lock, std::chrono::seconds(10), [&init_done] { return init_done; })) {
|
||||
if (discovery_ != nullptr) {
|
||||
discovery_->stop();
|
||||
}
|
||||
signaling_ = nullptr;
|
||||
transport_->stop();
|
||||
return false;
|
||||
}
|
||||
if (!init_ok) {
|
||||
render_thread_ = std::jthread{};
|
||||
if (discovery_ != nullptr) {
|
||||
discovery_->stop();
|
||||
}
|
||||
signaling_ = nullptr;
|
||||
transport_->stop();
|
||||
return false;
|
||||
}
|
||||
@@ -176,6 +223,16 @@ class ReceiverPipeline::Impl {
|
||||
|
||||
void stop() {
|
||||
render_thread_ = std::jthread{};
|
||||
// Join the signaling threads before dropping the channel so no
|
||||
// callback races the destruction.
|
||||
if (signaling_ != nullptr) {
|
||||
signaling_->disconnect();
|
||||
}
|
||||
if (discovery_ != nullptr) {
|
||||
discovery_->stop();
|
||||
}
|
||||
signaling_ = nullptr;
|
||||
discovery_ = nullptr;
|
||||
transport_->stop();
|
||||
renderer_ = nullptr;
|
||||
decoder_ = nullptr;
|
||||
@@ -216,6 +273,19 @@ class ReceiverPipeline::Impl {
|
||||
}
|
||||
}
|
||||
|
||||
void handle_signaling(const SignalingMessage& message) {
|
||||
const SessionOffer* offer = std::get_if<SessionOffer>(&message);
|
||||
if (offer == nullptr) {
|
||||
return;
|
||||
}
|
||||
SessionAnswer answer;
|
||||
answer.session_id = offer->session_id;
|
||||
// Empty address: the sender targets the address of its signaling
|
||||
// connection, which reaches this RTP port.
|
||||
answer.rtp_endpoint = Endpoint{"", config_.local_rtp_endpoint.port};
|
||||
signaling_->send(answer);
|
||||
}
|
||||
|
||||
void render_loop(std::stop_token stop_token) {
|
||||
while (!stop_token.stop_requested()) {
|
||||
if (!renderer_->poll_events()) {
|
||||
@@ -247,6 +317,8 @@ class ReceiverPipeline::Impl {
|
||||
std::unique_ptr<Decoder> decoder_;
|
||||
H264Depacketizer depacketizer_;
|
||||
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_;
|
||||
|
||||
Reference in New Issue
Block a user