1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include <atomic>
6 #include <functional>
7 #include <map>
8 #include <string>
9
10 // NOTE: although we use gtest here, prefer OSP_CHECKs to
11 // ASSERTS due to asynchronous concerns around test failures.
12 // Although this causes the entire test binary to fail instead of
13 // just a single test, it makes debugging easier/possible.
14 #include "cast/common/public/receiver_info.h"
15 #include "discovery/common/config.h"
16 #include "discovery/common/reporting_client.h"
17 #include "discovery/public/dns_sd_service_factory.h"
18 #include "discovery/public/dns_sd_service_publisher.h"
19 #include "discovery/public/dns_sd_service_watcher.h"
20 #include "gtest/gtest.h"
21 #include "platform/api/udp_socket.h"
22 #include "platform/base/interface_info.h"
23 #include "platform/impl/network_interface.h"
24 #include "platform/impl/platform_client_posix.h"
25 #include "platform/impl/task_runner.h"
26 #include "testing/util/task_util.h"
27 #include "util/chrono_helpers.h"
28 #include "util/osp_logging.h"
29
30 namespace openscreen {
31 namespace cast {
32 namespace {
33
34 // Maximum amount of time needed for a query to be received.
35 constexpr seconds kMaxQueryDuration{3};
36
37 // Total wait time = 4 seconds.
38 constexpr milliseconds kWaitLoopSleepTime(500);
39 constexpr int kMaxWaitLoopIterations = 8;
40
41 // Total wait time = 2.5 seconds.
42 // NOTE: This must be less than the above wait time.
43 constexpr milliseconds kCheckLoopSleepTime(100);
44 constexpr int kMaxCheckLoopIterations = 25;
45
46 // Publishes new service instances.
47 class Publisher : public discovery::DnsSdServicePublisher<ReceiverInfo> {
48 public:
Publisher(discovery::DnsSdService * service)49 explicit Publisher(discovery::DnsSdService* service) // NOLINT
50 : DnsSdServicePublisher<ReceiverInfo>(service,
51 kCastV2ServiceId,
52 ReceiverInfoToDnsSdInstance) {
53 OSP_LOG_INFO << "Initializing Publisher...\n";
54 }
55
56 ~Publisher() override = default;
57
IsInstanceIdClaimed(const std::string & requested_id)58 bool IsInstanceIdClaimed(const std::string& requested_id) {
59 auto it =
60 std::find(instance_ids_.begin(), instance_ids_.end(), requested_id);
61 return it != instance_ids_.end();
62 }
63
64 private:
65 // DnsSdPublisher::Client overrides.
OnInstanceClaimed(const std::string & requested_id)66 void OnInstanceClaimed(const std::string& requested_id) override {
67 instance_ids_.push_back(requested_id);
68 }
69
70 std::vector<std::string> instance_ids_;
71 };
72
73 // Receives incoming services and outputs their results to stdout.
74 class ServiceReceiver : public discovery::DnsSdServiceWatcher<ReceiverInfo> {
75 public:
ServiceReceiver(discovery::DnsSdService * service)76 explicit ServiceReceiver(discovery::DnsSdService* service) // NOLINT
77 : discovery::DnsSdServiceWatcher<ReceiverInfo>(
78 service,
79 kCastV2ServiceId,
80 DnsSdInstanceEndpointToReceiverInfo,
81 [this](
82 std::vector<std::reference_wrapper<const ReceiverInfo>> infos) {
83 ProcessResults(std::move(infos));
84 }) {
85 OSP_LOG_INFO << "Initializing ServiceReceiver...";
86 }
87
IsServiceFound(const ReceiverInfo & check_service)88 bool IsServiceFound(const ReceiverInfo& check_service) {
89 return std::find_if(receiver_infos_.begin(), receiver_infos_.end(),
90 [&check_service](const ReceiverInfo& info) {
91 return info.friendly_name ==
92 check_service.friendly_name;
93 }) != receiver_infos_.end();
94 }
95
EraseReceivedServices()96 void EraseReceivedServices() { receiver_infos_.clear(); }
97
98 private:
ProcessResults(std::vector<std::reference_wrapper<const ReceiverInfo>> infos)99 void ProcessResults(
100 std::vector<std::reference_wrapper<const ReceiverInfo>> infos) {
101 receiver_infos_.clear();
102 for (const ReceiverInfo& info : infos) {
103 receiver_infos_.push_back(info);
104 }
105 }
106
107 std::vector<ReceiverInfo> receiver_infos_;
108 };
109
110 class FailOnErrorReporting : public discovery::ReportingClient {
OnFatalError(Error error)111 void OnFatalError(Error error) override {
112 OSP_LOG_FATAL << "Fatal error received: '" << error << "'";
113 OSP_NOTREACHED();
114 }
115
OnRecoverableError(Error error)116 void OnRecoverableError(Error error) override {
117 // Pending resolution of openscreen:105, logging recoverable errors is
118 // disabled, as this will end up polluting the output with logs related to
119 // mDNS messages received from non-loopback network interfaces over which
120 // we have no control.
121 }
122 };
123
GetConfigSettings()124 discovery::Config GetConfigSettings() {
125 // Get the loopback interface to run on.
126 InterfaceInfo loopback = GetLoopbackInterfaceForTesting().value();
127 OSP_LOG_INFO << "Selected network interface for testing: " << loopback;
128 return discovery::Config{{std::move(loopback)}};
129 }
130
131 class DiscoveryE2ETest : public testing::Test {
132 public:
DiscoveryE2ETest()133 DiscoveryE2ETest() {
134 // Sleep to let any packets clear off the network before further tests.
135 std::this_thread::sleep_for(milliseconds(500));
136
137 PlatformClientPosix::Create(milliseconds(50));
138 task_runner_ = PlatformClientPosix::GetInstance()->GetTaskRunner();
139 }
140
~DiscoveryE2ETest()141 ~DiscoveryE2ETest() {
142 OSP_LOG_INFO << "TEST COMPLETE!";
143 dnssd_service_.reset();
144 PlatformClientPosix::ShutDown();
145 }
146
147 protected:
GetInfo(int id)148 ReceiverInfo GetInfo(int id) {
149 ReceiverInfo hosted_service;
150 hosted_service.port = 1234;
151 hosted_service.unique_id = "id" + std::to_string(id);
152 hosted_service.model_name = "openscreen-Model" + std::to_string(id);
153 hosted_service.friendly_name = "Demo" + std::to_string(id);
154 return hosted_service;
155 }
156
SetUpService(const discovery::Config & config)157 void SetUpService(const discovery::Config& config) {
158 OSP_DCHECK(!dnssd_service_.get());
159 std::atomic_bool done{false};
160 task_runner_->PostTask([this, &config, &done]() {
161 dnssd_service_ = discovery::CreateDnsSdService(
162 task_runner_, &reporting_client_, config);
163 receiver_ = std::make_unique<ServiceReceiver>(dnssd_service_.get());
164 publisher_ = std::make_unique<Publisher>(dnssd_service_.get());
165 done = true;
166 });
167 WaitForCondition([&done]() { return done.load(); }, kWaitLoopSleepTime,
168 kMaxWaitLoopIterations);
169 OSP_CHECK(done);
170 }
171
StartDiscovery()172 void StartDiscovery() {
173 OSP_DCHECK(dnssd_service_.get());
174 task_runner_->PostTask([this]() { receiver_->StartDiscovery(); });
175 }
176
177 template <typename... RecordTypes>
UpdateRecords(RecordTypes...records)178 void UpdateRecords(RecordTypes... records) {
179 OSP_DCHECK(dnssd_service_.get());
180 OSP_DCHECK(publisher_.get());
181
182 std::vector<ReceiverInfo> record_set{std::move(records)...};
183 for (ReceiverInfo& record : record_set) {
184 task_runner_->PostTask([this, r = std::move(record)]() {
185 auto error = publisher_->UpdateRegistration(r);
186 OSP_CHECK(error.ok()) << "\tFailed to update service instance '"
187 << r.friendly_name << "': " << error << "!";
188 });
189 }
190 }
191
192 template <typename... RecordTypes>
PublishRecords(RecordTypes...records)193 void PublishRecords(RecordTypes... records) {
194 OSP_DCHECK(dnssd_service_.get());
195 OSP_DCHECK(publisher_.get());
196
197 std::vector<ReceiverInfo> record_set{std::move(records)...};
198 for (ReceiverInfo& record : record_set) {
199 task_runner_->PostTask([this, r = std::move(record)]() {
200 auto error = publisher_->Register(r);
201 OSP_CHECK(error.ok()) << "\tFailed to publish service instance '"
202 << r.friendly_name << "': " << error << "!";
203 });
204 }
205 }
206
207 template <typename... AtomicBoolPtrs>
WaitUntilSeen(bool should_be_seen,AtomicBoolPtrs...bools)208 void WaitUntilSeen(bool should_be_seen, AtomicBoolPtrs... bools) {
209 OSP_DCHECK(dnssd_service_.get());
210 std::vector<std::atomic_bool*> atomic_bools{bools...};
211
212 int waiting_on = atomic_bools.size();
213 for (int i = 0; i < kMaxWaitLoopIterations; i++) {
214 waiting_on = atomic_bools.size();
215 for (std::atomic_bool* atomic : atomic_bools) {
216 if (*atomic) {
217 OSP_CHECK(should_be_seen) << "Found service instance!";
218 waiting_on--;
219 }
220 }
221
222 if (waiting_on) {
223 OSP_LOG_INFO << "\tWaiting on " << waiting_on << "...";
224 std::this_thread::sleep_for(kWaitLoopSleepTime);
225 continue;
226 }
227 return;
228 }
229 OSP_CHECK(!should_be_seen)
230 << "Could not find " << waiting_on << " service instances!";
231 }
232
CheckForClaimedIds(ReceiverInfo receiver_info,std::atomic_bool * has_been_seen)233 void CheckForClaimedIds(ReceiverInfo receiver_info,
234 std::atomic_bool* has_been_seen) {
235 OSP_DCHECK(dnssd_service_.get());
236 task_runner_->PostTask(
237 [this, info = std::move(receiver_info), has_been_seen]() mutable {
238 CheckForClaimedIds(std::move(info), has_been_seen, 0);
239 });
240 }
241
CheckForPublishedService(ReceiverInfo receiver_info,std::atomic_bool * has_been_seen)242 void CheckForPublishedService(ReceiverInfo receiver_info,
243 std::atomic_bool* has_been_seen) {
244 OSP_DCHECK(dnssd_service_.get());
245 task_runner_->PostTask(
246 [this, info = std::move(receiver_info), has_been_seen]() mutable {
247 CheckForPublishedService(std::move(info), has_been_seen, 0, true);
248 });
249 }
250
251 // TODO(issuetracker.google.com/159256503): Change this to use a polling
252 // method to wait until the service disappears rather than immediately failing
253 // if it exists, so waits throughout this file can be removed.
CheckNotPublishedService(ReceiverInfo receiver_info,std::atomic_bool * has_been_seen)254 void CheckNotPublishedService(ReceiverInfo receiver_info,
255 std::atomic_bool* has_been_seen) {
256 OSP_DCHECK(dnssd_service_.get());
257 task_runner_->PostTask(
258 [this, info = std::move(receiver_info), has_been_seen]() mutable {
259 CheckForPublishedService(std::move(info), has_been_seen, 0, false);
260 });
261 }
262 TaskRunner* task_runner_;
263 FailOnErrorReporting reporting_client_;
264 SerialDeletePtr<discovery::DnsSdService> dnssd_service_;
265 std::unique_ptr<ServiceReceiver> receiver_;
266 std::unique_ptr<Publisher> publisher_;
267
268 private:
CheckForClaimedIds(ReceiverInfo receiver_info,std::atomic_bool * has_been_seen,int attempts)269 void CheckForClaimedIds(ReceiverInfo receiver_info,
270 std::atomic_bool* has_been_seen,
271 int attempts) {
272 if (publisher_->IsInstanceIdClaimed(receiver_info.GetInstanceId())) {
273 // TODO(crbug.com/openscreen/110): Log the published service instance.
274 *has_been_seen = true;
275 return;
276 }
277
278 OSP_CHECK_LE(attempts++, kMaxCheckLoopIterations)
279 << "Service " << receiver_info.friendly_name << " publication failed.";
280 task_runner_->PostTaskWithDelay(
281 [this, info = std::move(receiver_info), has_been_seen,
282 attempts]() mutable {
283 CheckForClaimedIds(std::move(info), has_been_seen, attempts);
284 },
285 kCheckLoopSleepTime);
286 }
287
CheckForPublishedService(ReceiverInfo receiver_info,std::atomic_bool * has_been_seen,int attempts,bool expect_to_be_present)288 void CheckForPublishedService(ReceiverInfo receiver_info,
289 std::atomic_bool* has_been_seen,
290 int attempts,
291 bool expect_to_be_present) {
292 if (!receiver_->IsServiceFound(receiver_info)) {
293 if (attempts++ > kMaxCheckLoopIterations) {
294 OSP_CHECK(!expect_to_be_present)
295 << "Service " << receiver_info.friendly_name
296 << " discovery failed.";
297 return;
298 }
299 task_runner_->PostTaskWithDelay(
300 [this, info = std::move(receiver_info), has_been_seen, attempts,
301 expect_to_be_present]() mutable {
302 CheckForPublishedService(std::move(info), has_been_seen, attempts,
303 expect_to_be_present);
304 },
305 kCheckLoopSleepTime);
306 } else if (expect_to_be_present) {
307 // TODO(crbug.com/openscreen/110): Log the discovered service instance.
308 *has_been_seen = true;
309 } else {
310 OSP_LOG_FATAL << "Found instance '" << receiver_info.friendly_name
311 << "'!";
312 }
313 }
314 };
315
316 // The below runs an E2E tests. These test requires no user interaction and is
317 // intended to perform a set series of actions to validate that discovery is
318 // functioning as intended.
319 //
320 // Known issues:
321 // - The ipv6 socket in discovery/mdns/service_impl.cc fails to bind to an ipv6
322 // address on the loopback interface. Investigating this issue is pending
323 // resolution of bug
324 // https://bugs.chromium.org/p/openscreen/issues/detail?id=105.
325 //
326 // In this test, the following operations are performed:
327 // 1) Start up the Cast platform for a posix system.
328 // 2) Publish 3 CastV2 service instances to the loopback interface using mDNS,
329 // with record announcement disabled.
330 // 3) Wait for the probing phase to successfully complete.
331 // 4) Query for records published over the loopback interface, and validate that
332 // all 3 previously published services are discovered.
TEST_F(DiscoveryE2ETest,ValidateQueryFlow)333 TEST_F(DiscoveryE2ETest, ValidateQueryFlow) {
334 // Set up demo infra.
335 auto discovery_config = GetConfigSettings();
336 discovery_config.new_record_announcement_count = 0;
337 SetUpService(discovery_config);
338
339 auto instance1 = GetInfo(1);
340 auto instance2 = GetInfo(2);
341 auto instance3 = GetInfo(3);
342
343 // Start discovery and publication.
344 StartDiscovery();
345 PublishRecords(instance1, instance2, instance3);
346
347 // Wait until all probe phases complete and all instance ids are claimed. At
348 // this point, all records should be published.
349 OSP_LOG_INFO << "Service publication in progress...";
350 std::atomic_bool found1{false};
351 std::atomic_bool found2{false};
352 std::atomic_bool found3{false};
353 CheckForClaimedIds(instance1, &found1);
354 CheckForClaimedIds(instance2, &found2);
355 CheckForClaimedIds(instance3, &found3);
356 WaitUntilSeen(true, &found1, &found2, &found3);
357 OSP_LOG_INFO << "\tAll services successfully published!\n";
358
359 // Make sure all services are found through discovery.
360 OSP_LOG_INFO << "Service discovery in progress...";
361 found1 = false;
362 found2 = false;
363 found3 = false;
364 CheckForPublishedService(instance1, &found1);
365 CheckForPublishedService(instance2, &found2);
366 CheckForPublishedService(instance3, &found3);
367 WaitUntilSeen(true, &found1, &found2, &found3);
368 }
369
370 // In this test, the following operations are performed:
371 // 1) Start up the Cast platform for a posix system.
372 // 2) Start service discovery and new queries, with no query messages being
373 // sent.
374 // 3) Publish 3 CastV2 service instances to the loopback interface using mDNS,
375 // with record announcement enabled.
376 // 4) Ensure the correct records were published over the loopback interface.
377 // 5) De-register all services.
378 // 6) Ensure that goodbye records are received for all service instances.
TEST_F(DiscoveryE2ETest,ValidateAnnouncementFlow)379 TEST_F(DiscoveryE2ETest, ValidateAnnouncementFlow) {
380 // Set up demo infra.
381 auto discovery_config = GetConfigSettings();
382 discovery_config.new_query_announcement_count = 0;
383 SetUpService(discovery_config);
384
385 auto instance1 = GetInfo(1);
386 auto instance2 = GetInfo(2);
387 auto instance3 = GetInfo(3);
388
389 // Start discovery and publication.
390 StartDiscovery();
391 PublishRecords(instance1, instance2, instance3);
392
393 // Wait until all probe phases complete and all instance ids are claimed. At
394 // this point, all records should be published.
395 OSP_LOG_INFO << "Service publication in progress...";
396 std::atomic_bool found1{false};
397 std::atomic_bool found2{false};
398 std::atomic_bool found3{false};
399 CheckForClaimedIds(instance1, &found1);
400 CheckForClaimedIds(instance2, &found2);
401 CheckForClaimedIds(instance3, &found3);
402 WaitUntilSeen(true, &found1, &found2, &found3);
403 OSP_LOG_INFO << "\tAll services successfully published and announced!\n";
404
405 // Make sure all services are found through discovery.
406 OSP_LOG_INFO << "Service discovery in progress...";
407 found1 = false;
408 found2 = false;
409 found3 = false;
410 CheckForPublishedService(instance1, &found1);
411 CheckForPublishedService(instance2, &found2);
412 CheckForPublishedService(instance3, &found3);
413 WaitUntilSeen(true, &found1, &found2, &found3);
414 OSP_LOG_INFO << "\tAll services successfully discovered!\n";
415
416 // Deregister all service instances.
417 OSP_LOG_INFO << "Deregister all services...";
418 task_runner_->PostTask([this]() {
419 ErrorOr<int> result = publisher_->DeregisterAll();
420 ASSERT_FALSE(result.is_error());
421 ASSERT_EQ(result.value(), 3);
422 });
423 std::this_thread::sleep_for(seconds(3));
424 found1 = false;
425 found2 = false;
426 found3 = false;
427 CheckNotPublishedService(instance1, &found1);
428 CheckNotPublishedService(instance2, &found2);
429 CheckNotPublishedService(instance3, &found3);
430 WaitUntilSeen(false, &found1, &found2, &found3);
431 }
432
433 // In this test, the following operations are performed:
434 // 1) Start up the Cast platform for a posix system.
435 // 2) Publish one service and ensure it is NOT received.
436 // 3) Start service discovery and new queries.
437 // 4) Ensure above published service IS received.
438 // 5) Stop the started query.
439 // 6) Update a service, and ensure that no callback is received.
440 // 7) Restart the query and ensure that only the expected callbacks are
441 // received.
TEST_F(DiscoveryE2ETest,ValidateRecordsOnlyReceivedWhenQueryRunning)442 TEST_F(DiscoveryE2ETest, ValidateRecordsOnlyReceivedWhenQueryRunning) {
443 // Set up demo infra.
444 auto discovery_config = GetConfigSettings();
445 discovery_config.new_record_announcement_count = 1;
446 SetUpService(discovery_config);
447
448 auto instance = GetInfo(1);
449
450 // Start discovery and publication.
451 PublishRecords(instance);
452
453 // Wait until all probe phases complete and all instance ids are claimed. At
454 // this point, all records should be published.
455 OSP_LOG_INFO << "Service publication in progress...";
456 std::atomic_bool found{false};
457 CheckForClaimedIds(instance, &found);
458 WaitUntilSeen(true, &found);
459
460 // And ensure stopped discovery does not find the records.
461 OSP_LOG_INFO
462 << "Validating no service discovery occurs when discovery stopped...";
463 found = false;
464 CheckNotPublishedService(instance, &found);
465 WaitUntilSeen(false, &found);
466
467 // Make sure all services are found through discovery.
468 StartDiscovery();
469 OSP_LOG_INFO << "Service discovery in progress...";
470 found = false;
471 CheckForPublishedService(instance, &found);
472 WaitUntilSeen(true, &found);
473
474 // Update discovery and ensure that the updated service is seen.
475 OSP_LOG_INFO << "Updating service and waiting for discovery...";
476 auto updated_instance = instance;
477 updated_instance.friendly_name = "OtherName";
478 found = false;
479 UpdateRecords(updated_instance);
480 CheckForPublishedService(updated_instance, &found);
481 WaitUntilSeen(true, &found);
482
483 // And ensure the old service has been removed.
484 found = false;
485 CheckNotPublishedService(instance, &found);
486 WaitUntilSeen(false, &found);
487
488 // Stop discovery.
489 OSP_LOG_INFO << "Stopping discovery...";
490 task_runner_->PostTask([this]() { receiver_->StopDiscovery(); });
491
492 // Update discovery and ensure that the updated service is NOT seen.
493 OSP_LOG_INFO
494 << "Updating service and validating the change isn't received...";
495 found = false;
496 instance.friendly_name = "ThirdName";
497 UpdateRecords(instance);
498 CheckNotPublishedService(instance, &found);
499 WaitUntilSeen(false, &found);
500
501 StartDiscovery();
502 std::this_thread::sleep_for(kMaxQueryDuration);
503
504 OSP_LOG_INFO << "Service discovery in progress...";
505 found = false;
506 CheckNotPublishedService(updated_instance, &found);
507 WaitUntilSeen(false, &found);
508
509 found = false;
510 CheckForPublishedService(instance, &found);
511 WaitUntilSeen(true, &found);
512 }
513
514 // In this test, the following operations are performed:
515 // 1) Start up the Cast platform for a posix system.
516 // 2) Start service discovery and new queries.
517 // 3) Publish one service and ensure it is received.
518 // 4) Hard reset discovery
519 // 5) Ensure the same service is discovered
520 // 6) Soft reset the service, and ensure that a callback is received.
TEST_F(DiscoveryE2ETest,ValidateRefreshFlow)521 TEST_F(DiscoveryE2ETest, ValidateRefreshFlow) {
522 // Set up demo infra.
523 // NOTE: This configuration assumes that packets cannot be lost over the
524 // loopback interface.
525 auto discovery_config = GetConfigSettings();
526 discovery_config.new_record_announcement_count = 0;
527 discovery_config.new_query_announcement_count = 2;
528 SetUpService(discovery_config);
529
530 auto instance = GetInfo(1);
531
532 // Start discovery and publication.
533 StartDiscovery();
534 PublishRecords(instance);
535
536 // Wait until all probe phases complete and all instance ids are claimed. At
537 // this point, all records should be published.
538 OSP_LOG_INFO << "Service publication in progress...";
539 std::atomic_bool found{false};
540 CheckForClaimedIds(instance, &found);
541 WaitUntilSeen(true, &found);
542
543 // Make sure all services are found through discovery.
544 OSP_LOG_INFO << "Service discovery in progress...";
545 found = false;
546 CheckForPublishedService(instance, &found);
547 WaitUntilSeen(true, &found);
548
549 // Force refresh discovery, then ensure that the published service is
550 // re-discovered.
551 OSP_LOG_INFO << "Force refresh discovery...";
552 task_runner_->PostTask([this]() { receiver_->EraseReceivedServices(); });
553 std::this_thread::sleep_for(kMaxQueryDuration);
554 found = false;
555 CheckNotPublishedService(instance, &found);
556 WaitUntilSeen(false, &found);
557 task_runner_->PostTask([this]() { receiver_->ForceRefresh(); });
558
559 OSP_LOG_INFO << "Ensure that the published service is re-discovered...";
560 found = false;
561 CheckForPublishedService(instance, &found);
562 WaitUntilSeen(true, &found);
563
564 // Soft refresh discovery, then ensure that the published service is NOT
565 // re-discovered.
566 OSP_LOG_INFO << "Call DiscoverNow on discovery...";
567 task_runner_->PostTask([this]() { receiver_->EraseReceivedServices(); });
568 std::this_thread::sleep_for(kMaxQueryDuration);
569 found = false;
570 CheckNotPublishedService(instance, &found);
571 WaitUntilSeen(false, &found);
572 task_runner_->PostTask([this]() { receiver_->DiscoverNow(); });
573
574 OSP_LOG_INFO << "Ensure that the published service is re-discovered...";
575 found = false;
576 CheckForPublishedService(instance, &found);
577 WaitUntilSeen(true, &found);
578 }
579
580 } // namespace
581 } // namespace cast
582 } // namespace openscreen
583