fix(app): try every discovered receiver address in preference order

The sender used the first address a receiver resolved to, which on
the Pi was a 6to4 2002:: address that is unreachable between LAN
peers (the router runs a tunnel) — signaling could not connect, no
RTP ever flowed, and the receiver showed a black screen with an
empty journal while the user's earlier discovery looked healthy.

Discovery now keeps all of a receiver's addresses and the sender
tries them sorted by reachability preference (private IPv4, public
IPv4, ULA, global IPv6, 6to4, link-local) until the signaling
connection succeeds; --discover lists them in the same order.
Verification against the real Pi: it lists both addresses with
192.168.178.131 first, the probe negotiated over it, and 100
synthetic frames streamed to the negotiated RTP port.
This commit is contained in:
2026-09-07 14:07:40 +02:00
parent 236584c240
commit 67eee23137
+142 -54
View File
@@ -12,8 +12,10 @@
#include <format>
#include <future>
#include <iostream>
#include <map>
#include <memory>
#include <mutex>
#include <numeric>
#include <random>
#include <string>
#include <string_view>
@@ -60,6 +62,40 @@ std::string make_session_id() {
return id;
}
bool is_private_ipv4(std::string_view host) {
if (host.rfind("192.168.", 0) == 0 || host.rfind("10.", 0) == 0) {
return true;
}
if (host.rfind("172.", 0) == 0) {
const std::size_t second = host.find('.', 5);
if (second != std::string_view::npos) {
const int octet = std::stoi(std::string{host.substr(5, second - 5)});
return octet >= 16 && octet <= 31;
}
}
return false;
}
// Ordering for trying a receiver's addresses: private IPv4 first (LANs,
// most reliable), then public IPv4, ULA, and global IPv6. 6to4 (2002::) and
// link-local addresses last: 6to4 is frequently unreachable between LAN
// peers, and link-local needs a scope id to even route.
int address_preference(std::string_view host) {
if (host.find(':') == std::string_view::npos) {
return is_private_ipv4(host) ? 0 : 1;
}
if (host.rfind("fd", 0) == 0 || host.rfind("fc", 0) == 0) {
return 2;
}
if (host.rfind("2002:", 0) == 0) {
return 4;
}
if (host.rfind("fe80:", 0) == 0) {
return 5;
}
return 3;
}
std::vector<sc::DiscoveredPeer> discover_peers(int timeout_seconds, std::string& error) {
std::vector<sc::DiscoveredPeer> peers;
std::mutex mutex;
@@ -73,11 +109,12 @@ std::vector<sc::DiscoveredPeer> discover_peers(int timeout_seconds, std::string&
if (!discovery->browse([&](const sc::DiscoveredPeer& peer) {
std::lock_guard lock(mutex);
// One host with many interfaces resolves to several addresses;
// a single entry per (name, port) keeps discovery readable and
// lets senders auto-pick without ambiguity.
// A host resolves to several addresses (one per interface and
// family); keep them all — the sender tries them in preference
// order, because some (6to4, link-local) may be unreachable.
const bool known = std::any_of(peers.begin(), peers.end(), [&peer](const sc::DiscoveredPeer& existing) {
return existing.service_name == peer.service_name && existing.signaling_port == peer.signaling_port;
return existing.service_name == peer.service_name && existing.host == peer.host &&
existing.signaling_port == peer.signaling_port;
});
if (!known) {
peers.push_back(peer);
@@ -103,53 +140,13 @@ std::vector<sc::DiscoveredPeer> discover_peers(int timeout_seconds, std::string&
#ifdef SC_HAS_SENDER
int run_sender(const sc::SendCommand& command) {
// 1. Find the receiver's signaling endpoint: explicit --peer, or discover
// exactly one receiver on the LAN.
sc::Endpoint signaling;
if (!command.peer_address.empty()) {
signaling = parse_endpoint(command.peer_address, kDefaultSignalingPort);
} else {
std::string error;
const std::vector<sc::DiscoveredPeer> peers = discover_peers(kSenderDiscoveryTimeoutSeconds, error);
if (!error.empty()) {
std::cerr << std::format("screencast: discovery failed: {}\n", error);
return 1;
}
if (peers.empty()) {
std::cerr << "screencast: no receiver found on the LAN; run 'screencast --receive' on the "
"target machine, or pass --peer\n";
return 1;
}
if (peers.size() > 1) {
for (const sc::DiscoveredPeer& peer : peers) {
std::cerr << std::format(
"screencast: {} at {}:{}\n", peer.service_name, peer.host, peer.signaling_port);
}
std::cerr << "screencast: multiple receivers found; pass --peer to choose one\n";
return 1;
}
signaling = sc::Endpoint{peers.front().host, peers.front().signaling_port};
std::cout << std::format("screencast: found receiver '{}'\n", peers.front().service_name);
}
// 2. Negotiate a session over the signaling channel.
auto channel_result = sc::SignalingFactory::create_client();
if (sc::is_network_error(channel_result)) {
std::cerr << std::format("screencast: {}\n", sc::network_error(channel_result).message);
return 1;
}
auto channel = std::move(sc::network_value(channel_result));
if (!channel->connect(signaling)) {
std::cerr << std::format(
"screencast: failed to connect to the receiver at {}:{}\n", signaling.address, signaling.port);
return 1;
}
// Offer, wait for the answer, and stream to the negotiated endpoint. The
// channel must already be connected.
int negotiate_and_stream(sc::SignalingChannel& channel, const sc::Endpoint& signaling, const sc::SendCommand& command) {
std::promise<sc::SessionAnswer> answer_promise;
auto answer_future = answer_promise.get_future();
std::atomic<bool> answered{false};
channel->on_message([&](const sc::SignalingMessage& message) {
channel.on_message([&](const sc::SignalingMessage& message) {
if (const sc::SessionAnswer* answer = std::get_if<sc::SessionAnswer>(&message)) {
if (!answered.exchange(true)) {
answer_promise.set_value(*answer);
@@ -162,22 +159,22 @@ int run_sender(const sc::SendCommand& command) {
offer.codec_name = "h264";
offer.frame_rate_num = 25;
offer.frame_rate_den = 1;
channel->send(offer);
channel.send(offer);
if (answer_future.wait_for(std::chrono::seconds(5)) != std::future_status::ready) {
std::cerr << "screencast: the receiver did not answer the session offer\n";
channel->disconnect();
channel.disconnect();
return 1;
}
const sc::SessionAnswer answer = answer_future.get();
channel->disconnect();
channel.disconnect();
if (answer.session_id != offer.session_id) {
std::cerr << "screencast: session mismatch in the receiver's answer\n";
return 1;
}
// 3. Stream to the negotiated RTP endpoint. An empty address means
// Stream to the negotiated RTP endpoint. An empty address means
// "the address you reached me on".
const sc::Endpoint rtp_endpoint = answer.rtp_endpoint.address.empty()
? sc::Endpoint{signaling.address, answer.rtp_endpoint.port}
@@ -208,6 +205,84 @@ int run_sender(const sc::SendCommand& command) {
return 0;
}
int run_sender(const sc::SendCommand& command) {
// Find the receiver's signaling endpoint: explicit --peer, or discover
// exactly one receiver on the LAN. A receiver may resolve to several
// addresses; try them in reachability order until the signaling
// connection succeeds.
auto channel_result = sc::SignalingFactory::create_client();
if (sc::is_network_error(channel_result)) {
std::cerr << std::format("screencast: {}\n", sc::network_error(channel_result).message);
return 1;
}
auto channel = std::move(sc::network_value(channel_result));
sc::Endpoint signaling;
if (!command.peer_address.empty()) {
signaling = parse_endpoint(command.peer_address, kDefaultSignalingPort);
if (!channel->connect(signaling)) {
std::cerr << std::format(
"screencast: failed to connect to the receiver at {}:{}\n", signaling.address, signaling.port);
return 1;
}
return negotiate_and_stream(*channel, signaling, command);
}
std::string error;
std::vector<sc::DiscoveredPeer> peers = discover_peers(kSenderDiscoveryTimeoutSeconds, error);
if (!error.empty()) {
std::cerr << std::format("screencast: discovery failed: {}\n", error);
return 1;
}
if (peers.empty()) {
std::cerr << "screencast: no receiver found on the LAN; run 'screencast --receive' on the "
"target machine, or pass --peer\n";
return 1;
}
// Group the addresses by receiver (name + signaling port): one entry
// per address, but they are all the same host.
std::map<std::pair<std::string, std::uint16_t>, std::vector<std::string>> receivers;
for (const sc::DiscoveredPeer& peer : peers) {
receivers[{peer.service_name, peer.signaling_port}].push_back(peer.host);
}
if (receivers.size() > 1) {
for (const auto& [key, hosts] : receivers) {
std::cerr << std::format(
"screencast: {} at {}\n",
key.first,
std::accumulate(hosts.begin(), hosts.end(), std::string{}, [](std::string lhs, const std::string& rhs) {
return lhs.empty() ? rhs : lhs + ", " + rhs;
}));
}
std::cerr << "screencast: multiple receivers found; pass --peer to choose one\n";
return 1;
}
auto [name, port] = receivers.begin()->first;
std::vector<std::string> hosts = receivers.begin()->second;
std::sort(hosts.begin(), hosts.end(), [](const std::string& lhs, const std::string& rhs) {
return address_preference(lhs) < address_preference(rhs);
});
std::cout << std::format("screencast: found receiver '{}'\n", name);
bool connected = false;
std::string tried;
for (const std::string& host : hosts) {
if (channel->connect(sc::Endpoint{host, port})) {
signaling = sc::Endpoint{host, port};
connected = true;
break;
}
tried += (tried.empty() ? "" : ", ") + host;
}
if (!connected) {
std::cerr << std::format("screencast: could not reach the receiver (tried {})\n", tried);
return 1;
}
return negotiate_and_stream(*channel, signaling, command);
}
#endif // SC_HAS_SENDER
int run_receiver(const sc::ReceiveCommand& command) {
@@ -231,7 +306,7 @@ int run_receiver(const sc::ReceiveCommand& command) {
int run_discover(const sc::DiscoverCommand& command) {
std::string error;
const std::vector<sc::DiscoveredPeer> peers = discover_peers(command.timeout_seconds, error);
std::vector<sc::DiscoveredPeer> peers = discover_peers(command.timeout_seconds, error);
if (!error.empty()) {
std::cerr << std::format("screencast: discovery failed: {}\n", error);
return 1;
@@ -240,8 +315,21 @@ int run_discover(const sc::DiscoverCommand& command) {
std::cout << "no screencast receivers found\n";
return 0;
}
// One line per receiver, addresses sorted by reachability preference.
std::map<std::pair<std::string, std::uint16_t>, std::vector<std::string>> receivers;
for (const sc::DiscoveredPeer& peer : peers) {
std::cout << std::format("{} at {}:{}\n", peer.service_name, peer.host, peer.signaling_port);
receivers[{peer.service_name, peer.signaling_port}].push_back(peer.host);
}
for (auto& [key, hosts] : receivers) {
std::sort(hosts.begin(), hosts.end(), [](const std::string& lhs, const std::string& rhs) {
return address_preference(lhs) < address_preference(rhs);
});
std::string joined =
std::accumulate(hosts.begin(), hosts.end(), std::string{}, [](std::string lhs, const std::string& rhs) {
return lhs.empty() ? rhs : lhs + ", " + rhs;
});
std::cout << std::format("{} at {} (signaling port {})\n", key.first, joined, key.second);
}
return 0;
}