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>
diff --git a/cast/standalone_sender/BUILD.gn b/cast/standalone_sender/BUILD.gn
index 753e9e7..0143163 100644
--- a/cast/standalone_sender/BUILD.gn
+++ b/cast/standalone_sender/BUILD.gn
@@ -43,6 +43,8 @@
         "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 3074fec..09c99d0 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 423f264..f99fa3e 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
 
-      -r, --remote=addr[:port]
-           Specify the destination (e.g., 192.168.1.22:9999 or [::1]:12345).
+or
 
-           Default if not set: %s
+usage: %s <options> addr[:port] media_file
+
+   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 @@
       -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 @@
   // 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 @@
   };
 
   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 @@
 
   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 @@
   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 0000000..7d6732e
--- /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 0000000..a2fd398
--- /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_