diff --git a/src/app/main.cpp b/src/app/main.cpp index eb34a67..035789f 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -12,8 +12,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -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 discover_peers(int timeout_seconds, std::string& error) { std::vector peers; std::mutex mutex; @@ -73,11 +109,12 @@ std::vector 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 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 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 answer_promise; auto answer_future = answer_promise.get_future(); std::atomic answered{false}; - channel->on_message([&](const sc::SignalingMessage& message) { + channel.on_message([&](const sc::SignalingMessage& message) { if (const sc::SessionAnswer* answer = std::get_if(&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 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::vector> 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 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 peers = discover_peers(command.timeout_seconds, error); + std::vector 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::vector> 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; }