diff options
author | Yuri Wiitala <miu@chromium.org> | 2020-11-30 09:49:41 -0800 |
---|---|---|
committer | Yuri Wiitala <miu@chromium.org> | 2020-11-30 21:12:19 +0000 |
commit | 4d25bf856b135ed547abd259e7d7d9243d68bf74 (patch) | |
tree | e887e1b67ac4e4cfc66a49e8853ae75a8bc699fb /cast | |
parent | c6465ca683e686cd7f7dfa347e86451425e7af25 (diff) | |
download | openscreen-4d25bf856b135ed547abd259e7d7d9243d68bf74.tar.gz |
Add discovery and console menu interface to standalone sender.
This patch allows the standalone sender to be run in one of two ways:
1) by specifying an IP:port for direct connection to a Cast Receiver, or
2) by specifying a network interface for LAN discovery of Cast
Receivers. In case #2, once Cast Receiver(s) have been discovered, a
console menu is printed and asks the user to choose one.
Bug: b/162542369
Change-Id: I6c46bd0c868dbea3d6e0f7ff1960af4ab86c2a1c
Reviewed-on: https://chromium-review.googlesource.com/c/openscreen/+/2556568
Reviewed-by: Jordan Bayles <jophba@chromium.org>
Diffstat (limited to 'cast')
-rw-r--r-- | cast/standalone_sender/BUILD.gn | 2 | ||||
-rw-r--r-- | cast/standalone_sender/DEPS | 1 | ||||
-rw-r--r-- | cast/standalone_sender/main.cc | 115 | ||||
-rw-r--r-- | cast/standalone_sender/receiver_chooser.cc | 134 | ||||
-rw-r--r-- | cast/standalone_sender/receiver_chooser.h | 67 |
5 files changed, 271 insertions, 48 deletions
diff --git a/cast/standalone_sender/BUILD.gn b/cast/standalone_sender/BUILD.gn index 753e9e75..0143163a 100644 --- a/cast/standalone_sender/BUILD.gn +++ b/cast/standalone_sender/BUILD.gn @@ -43,6 +43,8 @@ if (!build_with_chromium) { "looping_file_cast_agent.h", "looping_file_sender.cc", "looping_file_sender.h", + "receiver_chooser.cc", + "receiver_chooser.h", "simulated_capturer.cc", "simulated_capturer.h", "streaming_opus_encoder.cc", diff --git a/cast/standalone_sender/DEPS b/cast/standalone_sender/DEPS index 3074fec2..09c99d09 100644 --- a/cast/standalone_sender/DEPS +++ b/cast/standalone_sender/DEPS @@ -4,5 +4,6 @@ include_rules = [ '+cast', + '+discovery', '+platform/impl', ] diff --git a/cast/standalone_sender/main.cc b/cast/standalone_sender/main.cc index 423f264f..f99fa3e2 100644 --- a/cast/standalone_sender/main.cc +++ b/cast/standalone_sender/main.cc @@ -7,49 +7,47 @@ #if defined(CAST_STANDALONE_SENDER_HAVE_EXTERNAL_LIBS) #include <getopt.h> -#include <chrono> #include <cinttypes> -#include <csignal> #include <cstdio> #include <cstring> #include <iostream> #include <sstream> +#include <vector> #include "cast/common/certificate/cast_trust_store.h" #include "cast/standalone_sender/constants.h" #include "cast/standalone_sender/looping_file_cast_agent.h" +#include "cast/standalone_sender/receiver_chooser.h" #include "cast/streaming/constants.h" -#include "cast/streaming/environment.h" -#include "cast/streaming/sender.h" -#include "cast/streaming/sender_packet_router.h" -#include "cast/streaming/session_config.h" -#include "cast/streaming/ssrc.h" +#include "platform/api/network_interface.h" #include "platform/api/time.h" #include "platform/base/error.h" #include "platform/base/ip_address.h" #include "platform/impl/platform_client_posix.h" #include "platform/impl/task_runner.h" #include "platform/impl/text_trace_logging_platform.h" -#include "util/alarm.h" -#include "util/chrono_helpers.h" #include "util/stringprintf.h" namespace openscreen { namespace cast { namespace { -IPEndpoint GetDefaultEndpoint() { - return IPEndpoint{IPAddress::kV4LoopbackAddress(), kDefaultCastPort}; -} - void LogUsage(const char* argv0) { constexpr char kTemplate[] = R"( -usage: %s <options> <media_file> +usage: %s <options> network_interface media_file + +or - -r, --remote=addr[:port] - Specify the destination (e.g., 192.168.1.22:9999 or [::1]:12345). +usage: %s <options> addr[:port] media_file - Default if not set: %s + The first form runs this application in discovery+interactive mode. It will + scan for Cast Receivers on the LAN reachable from the given network + interface, and then the user will choose one interactively via a menu on the + console. + + The second form runs this application in direct mode. It will not attempt to + discover Cast Receivers, and instead connect directly to the Cast Receiver at + addr:[port] (e.g., 192.168.1.22, 192.168.1.22:%d or [::1]:%d). -m, --max-bitrate=N Specifies the maximum bits per second for the media streams. @@ -78,9 +76,27 @@ usage: %s <options> <media_file> -h, --help: Show this help message. )"; - std::cerr << StringPrintf(kTemplate, argv0, - GetDefaultEndpoint().ToString().c_str(), - kDefaultMaxBitrate); + std::cerr << StringPrintf(kTemplate, argv0, argv0, kDefaultCastPort, + kDefaultCastPort, kDefaultMaxBitrate); +} + +// Attempts to parse |string_form| into an IPEndpoint. The format is a +// standard-format IPv4 or IPv6 address followed by an optional colon and port. +// If the port is not provided, kDefaultCastPort is assumed. +// +// If the parse fails, a zero-port IPEndpoint is returned. +IPEndpoint ParseAsEndpoint(const char* string_form) { + IPEndpoint result{}; + const ErrorOr<IPEndpoint> parsed_endpoint = IPEndpoint::Parse(string_form); + if (parsed_endpoint.is_value()) { + result = parsed_endpoint.value(); + } else { + const ErrorOr<IPAddress> parsed_address = IPAddress::Parse(string_form); + if (parsed_address.is_value()) { + result = {parsed_address.value(), kDefaultCastPort}; + } + } + return result; } int StandaloneSenderMain(int argc, char* argv[]) { @@ -89,7 +105,6 @@ int StandaloneSenderMain(int argc, char* argv[]) { // being exposed, consider if it applies to the standalone receiver, // standalone sender, osp demo, and test_main argument options. const struct option kArgumentOptions[] = { - {"remote", required_argument, nullptr, 'r'}, {"max-bitrate", required_argument, nullptr, 'm'}, #if defined(CAST_ALLOW_DEVELOPER_CERTIFICATE) {"developer-certificate", required_argument, nullptr, 'd'}, @@ -102,31 +117,14 @@ int StandaloneSenderMain(int argc, char* argv[]) { }; bool is_verbose = false; - IPEndpoint remote_endpoint = GetDefaultEndpoint(); std::string developer_certificate_path; - [[maybe_unused]] bool use_android_rtp_hack = false; - [[maybe_unused]] int max_bitrate = kDefaultMaxBitrate; + bool use_android_rtp_hack = false; + int max_bitrate = kDefaultMaxBitrate; std::unique_ptr<TextTraceLoggingPlatform> trace_logger; int ch = -1; - while ((ch = getopt_long(argc, argv, "r:m:d:atvh", kArgumentOptions, + while ((ch = getopt_long(argc, argv, "m:d:atvh", kArgumentOptions, nullptr)) != -1) { switch (ch) { - case 'r': { - const ErrorOr<IPEndpoint> parsed_endpoint = IPEndpoint::Parse(optarg); - if (parsed_endpoint.is_value()) { - remote_endpoint = parsed_endpoint.value(); - } else { - const ErrorOr<IPAddress> parsed_address = IPAddress::Parse(optarg); - if (parsed_address.is_value()) { - remote_endpoint.address = parsed_address.value(); - } else { - OSP_LOG_ERROR << "Invalid --remote specified: " << optarg; - LogUsage(argv[0]); - return 1; - } - } - break; - } case 'm': max_bitrate = atoi(optarg); if (max_bitrate < kMinRequiredBitrate) { @@ -158,16 +156,15 @@ int StandaloneSenderMain(int argc, char* argv[]) { openscreen::SetLogLevel(is_verbose ? openscreen::LogLevel::kVerbose : openscreen::LogLevel::kInfo); - // The last command line argument must be the path to the file. - const char* path = nullptr; - if (optind == (argc - 1)) { - path = argv[optind]; - } - - if (!path || !remote_endpoint.port) { + // The second to last command line argument must be one of: 1) the network + // interface name or 2) a specific IP address (port is optional). The last + // argument must be the path to the file. + if (optind != (argc - 2)) { LogUsage(argv[0]); return 1; } + const char* const iface_or_endpoint = argv[optind++]; + const char* const path = argv[optind]; #if defined(CAST_ALLOW_DEVELOPER_CERTIFICATE) if (!developer_certificate_path.empty()) { @@ -179,6 +176,28 @@ int StandaloneSenderMain(int argc, char* argv[]) { PlatformClientPosix::Create(Clock::duration{50}, Clock::duration{50}, std::unique_ptr<TaskRunnerImpl>(task_runner)); + IPEndpoint remote_endpoint = ParseAsEndpoint(iface_or_endpoint); + if (!remote_endpoint.port) { + for (const InterfaceInfo& interface : GetNetworkInterfaces()) { + if (interface.name == iface_or_endpoint) { + ReceiverChooser chooser(interface, task_runner, + [&](IPEndpoint endpoint) { + remote_endpoint = endpoint; + task_runner->RequestStopSoon(); + }); + task_runner->RunUntilSignaled(); + break; + } + } + + if (!remote_endpoint.port) { + OSP_LOG_ERROR << "No Cast Receiver chosen, or bad command-line argument. " + "Cannot continue."; + LogUsage(argv[0]); + return 2; + } + } + std::unique_ptr<LoopingFileCastAgent> cast_agent; task_runner->PostTask([&] { cast_agent = std::make_unique<LoopingFileCastAgent>(task_runner); diff --git a/cast/standalone_sender/receiver_chooser.cc b/cast/standalone_sender/receiver_chooser.cc new file mode 100644 index 00000000..7d6732e2 --- /dev/null +++ b/cast/standalone_sender/receiver_chooser.cc @@ -0,0 +1,134 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/standalone_sender/receiver_chooser.h" + +#include <cstdint> +#include <iostream> +#include <string> +#include <utility> + +#include "discovery/common/config.h" +#include "platform/api/time.h" +#include "util/osp_logging.h" + +namespace openscreen { +namespace cast { + +ReceiverChooser::ReceiverChooser(const InterfaceInfo& interface, + TaskRunner* task_runner, + ResultCallback result_callback) + : result_callback_(std::move(result_callback)), + menu_alarm_(&Clock::now, task_runner) { + using discovery::Config; + Config config; + // TODO(miu): Remove AddressFamilies from the Config in a follow-up patch. No + // client uses this to do anything other than "enabled for all address + // families," and so it doesn't need to be configurable. + Config::NetworkInfo::AddressFamilies families = + Config::NetworkInfo::kNoAddressFamily; + if (interface.GetIpAddressV4()) { + families |= Config::NetworkInfo::kUseIpV4; + } + if (interface.GetIpAddressV6()) { + families |= Config::NetworkInfo::kUseIpV6; + } + config.network_info.push_back({interface, families}); + config.enable_publication = false; + config.enable_querying = true; + service_ = + discovery::CreateDnsSdService(task_runner, this, std::move(config)); + + watcher_ = std::make_unique<discovery::DnsSdServiceWatcher<ServiceInfo>>( + service_.get(), kCastV2ServiceId, DnsSdInstanceEndpointToServiceInfo, + [this](std::vector<std::reference_wrapper<const ServiceInfo>> all) { + OnDnsWatcherUpdate(std::move(all)); + }); + + OSP_LOG_INFO << "Starting discovery. Note that it can take dozens of seconds " + "to detect anything on some networks!"; + task_runner->PostTask([this] { watcher_->StartDiscovery(); }); +} + +ReceiverChooser::~ReceiverChooser() = default; + +void ReceiverChooser::OnFatalError(Error error) { + OSP_LOG_FATAL << "Fatal error: " << error; +} + +void ReceiverChooser::OnRecoverableError(Error error) { + OSP_VLOG << "Recoverable error: " << error; +} + +void ReceiverChooser::OnDnsWatcherUpdate( + std::vector<std::reference_wrapper<const ServiceInfo>> all) { + bool added_some = false; + for (const ServiceInfo& info : all) { + if (!info.IsValid() || (!info.v4_address && !info.v6_address)) { + continue; + } + const std::string& instance_id = info.GetInstanceId(); + if (std::any_of(discovered_receivers_.begin(), discovered_receivers_.end(), + [&](const ServiceInfo& known) { + return known.GetInstanceId() == instance_id; + })) { + continue; + } + + OSP_LOG_INFO << "Discovered: " << info.friendly_name + << " (id: " << instance_id << ')'; + discovered_receivers_.push_back(info); + added_some = true; + } + + if (added_some) { + menu_alarm_.ScheduleFromNow([this] { PrintMenuAndHandleChoice(); }, + kWaitForStragglersDelay); + } +} + +void ReceiverChooser::PrintMenuAndHandleChoice() { + if (!result_callback_) { + return; // A choice has already been made. + } + + std::cout << '\n'; + for (size_t i = 0; i < discovered_receivers_.size(); ++i) { + const ServiceInfo& info = discovered_receivers_[i]; + std::cout << '[' << i << "]: " << info.friendly_name << " @ "; + if (info.v6_address) { + std::cout << info.v6_address; + } else { + OSP_DCHECK(info.v4_address); + std::cout << info.v4_address; + } + std::cout << ':' << info.port << '\n'; + } + std::cout << "\nEnter choice, or 'n' to wait longer: " << std::flush; + + int menu_choice = -1; + if (std::cin >> menu_choice || std::cin.eof()) { + const auto callback_on_stack = std::move(result_callback_); + if (menu_choice >= 0 && + menu_choice < static_cast<int>(discovered_receivers_.size())) { + const ServiceInfo& choice = discovered_receivers_[menu_choice]; + if (choice.v6_address) { + callback_on_stack(IPEndpoint{choice.v6_address, choice.port}); + } else { + callback_on_stack(IPEndpoint{choice.v4_address, choice.port}); + } + } else { + callback_on_stack(IPEndpoint{}); // Signal "bad choice" or EOF. + } + return; + } + + // Clear bad input flag, and skip past what the user entered. + std::cin.clear(); + std::string garbage; + std::getline(std::cin, garbage); +} + +} // namespace cast +} // namespace openscreen diff --git a/cast/standalone_sender/receiver_chooser.h b/cast/standalone_sender/receiver_chooser.h new file mode 100644 index 00000000..a2fd398f --- /dev/null +++ b/cast/standalone_sender/receiver_chooser.h @@ -0,0 +1,67 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STANDALONE_SENDER_RECEIVER_CHOOSER_H_ +#define CAST_STANDALONE_SENDER_RECEIVER_CHOOSER_H_ + +#include <functional> +#include <memory> +#include <vector> + +#include "cast/common/public/service_info.h" +#include "discovery/common/reporting_client.h" +#include "discovery/public/dns_sd_service_factory.h" +#include "discovery/public/dns_sd_service_watcher.h" +#include "platform/api/network_interface.h" +#include "platform/api/serial_delete_ptr.h" +#include "platform/api/task_runner.h" +#include "platform/base/ip_address.h" +#include "util/alarm.h" +#include "util/chrono_helpers.h" + +namespace openscreen { +namespace cast { + +// Discovers Cast Receivers on the LAN for a given network interface, and +// provides a console menu interface for the user to choose one. +class ReceiverChooser final : public discovery::ReportingClient { + public: + using ResultCallback = std::function<void(IPEndpoint)>; + + ReceiverChooser(const InterfaceInfo& interface, + TaskRunner* task_runner, + ResultCallback result_callback); + + ~ReceiverChooser() final; + + private: + // discovery::ReportingClient implementation. + void OnFatalError(Error error) final; + void OnRecoverableError(Error error) final; + + // Called from the DnsWatcher with |all| ServiceInfos any time there is a + // change in the set of discovered devices. + void OnDnsWatcherUpdate( + std::vector<std::reference_wrapper<const ServiceInfo>> all); + + // Called from |menu_alarm_| when it is a good time for the user to choose + // from the discovered-so-far set of Cast Receivers. + void PrintMenuAndHandleChoice(); + + ResultCallback result_callback_; + SerialDeletePtr<discovery::DnsSdService> service_; + std::unique_ptr<discovery::DnsSdServiceWatcher<ServiceInfo>> watcher_; + std::vector<ServiceInfo> discovered_receivers_; + Alarm menu_alarm_; + + // After there is another Cast Receiver discovered, ready to show to the user + // via the console menu, how long should the ReceiverChooser wait for + // additional receivers to be discovered and be included in the menu too? + static constexpr auto kWaitForStragglersDelay = seconds(5); +}; + +} // namespace cast +} // namespace openscreen + +#endif // CAST_STANDALONE_SENDER_RECEIVER_CHOOSER_H_ |