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:
+142
-54
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user