aboutsummaryrefslogtreecommitdiff
path: root/cast
diff options
context:
space:
mode:
authorYuri Wiitala <miu@chromium.org>2020-11-30 09:49:41 -0800
committerYuri Wiitala <miu@chromium.org>2020-11-30 21:12:19 +0000
commit4d25bf856b135ed547abd259e7d7d9243d68bf74 (patch)
treee887e1b67ac4e4cfc66a49e8853ae75a8bc699fb /cast
parentc6465ca683e686cd7f7dfa347e86451425e7af25 (diff)
downloadopenscreen-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.gn2
-rw-r--r--cast/standalone_sender/DEPS1
-rw-r--r--cast/standalone_sender/main.cc115
-rw-r--r--cast/standalone_sender/receiver_chooser.cc134
-rw-r--r--cast/standalone_sender/receiver_chooser.h67
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_