aboutsummaryrefslogtreecommitdiff
path: root/cast/sender
diff options
context:
space:
mode:
authorbtolsch <btolsch@chromium.org>2020-02-07 15:02:08 -0800
committerCommit Bot <commit-bot@chromium.org>2020-02-12 21:11:14 +0000
commit267593067f71fceb09ed55850ad149a4f84f0705 (patch)
treea4d0d6b90c59400f28c60c62d0023e5862ec9985 /cast/sender
parent4ab0c36c448da6f816ff10d7c5943b7ab67f80e7 (diff)
downloadopenscreen-267593067f71fceb09ed55850ad149a4f84f0705.tar.gz
Add Cast app availability tracker
This change ports the CastAppAvailabilityTracker from Chromium in preparation for the app discovery service. Bug: openscreen:60 Change-Id: Id450f6f616a35ae1c1959fa3326593370daf072c Reviewed-on: https://chromium-review.googlesource.com/c/openscreen/+/2029249 Commit-Queue: Brandon Tolsch <btolsch@chromium.org> Reviewed-by: Takumi Fujimoto <takumif@chromium.org>
Diffstat (limited to 'cast/sender')
-rw-r--r--cast/sender/BUILD.gn19
-rw-r--r--cast/sender/DEPS6
-rw-r--r--cast/sender/cast_app_availability_tracker.cc165
-rw-r--r--cast/sender/cast_app_availability_tracker.h124
-rw-r--r--cast/sender/cast_app_availability_tracker_unittest.cc159
-rw-r--r--cast/sender/public/DEPS7
-rw-r--r--cast/sender/public/README.md2
-rw-r--r--cast/sender/public/cast_media_source.cc45
-rw-r--r--cast/sender/public/cast_media_source.h42
9 files changed, 559 insertions, 10 deletions
diff --git a/cast/sender/BUILD.gn b/cast/sender/BUILD.gn
index ae36bf27..fd295b92 100644
--- a/cast/sender/BUILD.gn
+++ b/cast/sender/BUILD.gn
@@ -27,16 +27,35 @@ source_set("channel") {
]
}
+source_set("sender") {
+ sources = [
+ "cast_app_availability_tracker.cc",
+ "cast_app_availability_tracker.h",
+ "public/cast_media_source.cc",
+ "public/cast_media_source.h",
+ ]
+
+ public_deps = [
+ ":channel",
+ "../../platform",
+ "../../util",
+ ]
+}
+
source_set("unittests") {
testonly = true
sources = [
+ "cast_app_availability_tracker_unittest.cc",
"channel/cast_auth_util_unittest.cc",
]
deps = [
":channel",
+ ":sender",
"../../platform",
+ "../../platform:test",
"../../testing/util",
+ "../../third_party/googletest:gmock",
"../../third_party/googletest:gtest",
"../common/certificate/proto:certificate_proto",
"../common/certificate/proto:certificate_unittest_proto",
diff --git a/cast/sender/DEPS b/cast/sender/DEPS
index 7ab7a51a..48f4daee 100644
--- a/cast/sender/DEPS
+++ b/cast/sender/DEPS
@@ -1,7 +1,7 @@
# -*- Mode: Python; -*-
include_rules = [
- # libcast sender code must not depend on the receiver.
- '+cast/common',
- '+cast/sender'
+ # libcast sender code must not depend on the receiver.
+ '+cast/common',
+ '+cast/sender',
]
diff --git a/cast/sender/cast_app_availability_tracker.cc b/cast/sender/cast_app_availability_tracker.cc
new file mode 100644
index 00000000..6c21c799
--- /dev/null
+++ b/cast/sender/cast_app_availability_tracker.cc
@@ -0,0 +1,165 @@
+// 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/sender/cast_app_availability_tracker.h"
+
+#include "util/logging.h"
+
+namespace openscreen {
+namespace cast {
+
+CastAppAvailabilityTracker::CastAppAvailabilityTracker() = default;
+CastAppAvailabilityTracker::~CastAppAvailabilityTracker() = default;
+
+std::vector<std::string> CastAppAvailabilityTracker::RegisterSource(
+ const CastMediaSource& source) {
+ if (registered_sources_.find(source.source_id()) !=
+ registered_sources_.end()) {
+ return {};
+ }
+
+ registered_sources_.emplace(source.source_id(), source);
+
+ std::vector<std::string> new_app_ids;
+ for (const std::string& app_id : source.app_ids()) {
+ if (++registration_count_by_app_id_[app_id] == 1) {
+ new_app_ids.push_back(app_id);
+ }
+ }
+ return new_app_ids;
+}
+
+void CastAppAvailabilityTracker::UnregisterSource(
+ const CastMediaSource& source) {
+ UnregisterSource(source.source_id());
+}
+
+void CastAppAvailabilityTracker::UnregisterSource(
+ const std::string& source_id) {
+ auto it = registered_sources_.find(source_id);
+ if (it == registered_sources_.end()) {
+ return;
+ }
+
+ for (const std::string& app_id : it->second.app_ids()) {
+ auto count_it = registration_count_by_app_id_.find(app_id);
+ OSP_DCHECK(count_it != registration_count_by_app_id_.end());
+ if (--(count_it->second) == 0) {
+ registration_count_by_app_id_.erase(count_it);
+ }
+ }
+
+ registered_sources_.erase(it);
+}
+
+std::vector<CastMediaSource> CastAppAvailabilityTracker::UpdateAppAvailability(
+ const std::string& device_id,
+ const std::string& app_id,
+ AppAvailability availability) {
+ auto& availabilities = app_availabilities_[device_id];
+ auto it = availabilities.find(app_id);
+
+ AppAvailabilityResult old_availability = it == availabilities.end()
+ ? AppAvailabilityResult::kUnknown
+ : it->second.availability;
+ AppAvailabilityResult new_availability = availability.availability;
+
+ // Updated if status changes from/to kAvailable.
+ bool updated = (old_availability == AppAvailabilityResult::kAvailable ||
+ new_availability == AppAvailabilityResult::kAvailable) &&
+ old_availability != new_availability;
+ availabilities[app_id] = availability;
+
+ if (!updated) {
+ return {};
+ }
+
+ std::vector<CastMediaSource> affected_sources;
+ for (const auto& source : registered_sources_) {
+ if (source.second.ContainsAppId(app_id)) {
+ affected_sources.push_back(source.second);
+ }
+ }
+ return affected_sources;
+}
+
+std::vector<CastMediaSource> CastAppAvailabilityTracker::RemoveResultsForDevice(
+ const std::string& device_id) {
+ auto affected_sources = GetSupportedSources(device_id);
+ app_availabilities_.erase(device_id);
+ return affected_sources;
+}
+
+std::vector<CastMediaSource> CastAppAvailabilityTracker::GetSupportedSources(
+ const std::string& device_id) const {
+ auto it = app_availabilities_.find(device_id);
+ if (it == app_availabilities_.end()) {
+ return std::vector<CastMediaSource>();
+ }
+
+ // Find all app IDs that are available on the device.
+ std::vector<std::string> supported_app_ids;
+ for (const auto& availability : it->second) {
+ if (availability.second.availability == AppAvailabilityResult::kAvailable) {
+ supported_app_ids.push_back(availability.first);
+ }
+ }
+
+ // Find all registered sources whose query results contain the device ID.
+ std::vector<CastMediaSource> sources;
+ for (const auto& source : registered_sources_) {
+ if (source.second.ContainsAnyAppIdFrom(supported_app_ids)) {
+ sources.push_back(source.second);
+ }
+ }
+ return sources;
+}
+
+CastAppAvailabilityTracker::AppAvailability
+CastAppAvailabilityTracker::GetAvailability(const std::string& device_id,
+ const std::string& app_id) const {
+ auto availabilities_it = app_availabilities_.find(device_id);
+ if (availabilities_it == app_availabilities_.end()) {
+ return {AppAvailabilityResult::kUnknown, Clock::time_point{}};
+ }
+
+ const auto& availability_map = availabilities_it->second;
+ auto availability_it = availability_map.find(app_id);
+ if (availability_it == availability_map.end()) {
+ return {AppAvailabilityResult::kUnknown, Clock::time_point{}};
+ }
+
+ return availability_it->second;
+}
+
+std::vector<std::string> CastAppAvailabilityTracker::GetRegisteredApps() const {
+ std::vector<std::string> registered_apps;
+ for (const auto& app_ids_and_count : registration_count_by_app_id_) {
+ registered_apps.push_back(app_ids_and_count.first);
+ }
+
+ return registered_apps;
+}
+
+std::vector<std::string> CastAppAvailabilityTracker::GetAvailableDevices(
+ const CastMediaSource& source) const {
+ std::vector<std::string> device_ids;
+ // For each device, check if there is at least one available app in |source|.
+ for (const auto& availabilities : app_availabilities_) {
+ for (const std::string& app_id : source.app_ids()) {
+ const auto& availabilities_map = availabilities.second;
+ auto availability_it = availabilities_map.find(app_id);
+ if (availability_it != availabilities_map.end() &&
+ availability_it->second.availability ==
+ AppAvailabilityResult::kAvailable) {
+ device_ids.push_back(availabilities.first);
+ break;
+ }
+ }
+ }
+ return device_ids;
+}
+
+} // namespace cast
+} // namespace openscreen
diff --git a/cast/sender/cast_app_availability_tracker.h b/cast/sender/cast_app_availability_tracker.h
new file mode 100644
index 00000000..bced8a50
--- /dev/null
+++ b/cast/sender/cast_app_availability_tracker.h
@@ -0,0 +1,124 @@
+// 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_SENDER_CAST_APP_AVAILABILITY_TRACKER_H_
+#define CAST_SENDER_CAST_APP_AVAILABILITY_TRACKER_H_
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include "cast/sender/channel/message_util.h"
+#include "cast/sender/public/cast_media_source.h"
+#include "platform/api/time.h"
+
+namespace openscreen {
+namespace cast {
+
+// Tracks device queries and their extracted Cast app IDs and their
+// availabilities on discovered devices.
+// Example usage:
+///
+// (1) A page is interested in a Cast URL (e.g. by creating a
+// PresentationRequest with the URL) like "cast:foo". To register the source to
+// be tracked:
+// CastAppAvailabilityTracker tracker;
+// auto source = CastMediaSource::From("cast:foo");
+// auto new_app_ids = tracker.RegisterSource(source.value());
+//
+// (2) The set of app IDs returned by the tracker can then be used by the caller
+// to send an app availability request to each of the discovered devices.
+//
+// (3) Once the caller knows the availability value for a (device, app) pair, it
+// may inform the tracker to update its results:
+// auto affected_sources =
+// tracker.UpdateAppAvailability(device_id, app_id, {availability, now});
+//
+// (4) The tracker returns a subset of discovered sources that were affected by
+// the update. The caller can then call |GetAvailableDevices()| to get the
+// updated results for each affected source.
+//
+// (5a): At any time, the caller may call |RemoveResultsForDevice()| to remove
+// cached results pertaining to the device, when it detects that a device is
+// removed or no longer valid.
+//
+// (5b): At any time, the caller may call |GetAvailableDevices()| (even before
+// the source is registered) to determine if there are cached results available.
+class CastAppAvailabilityTracker {
+ public:
+ // The result of an app availability request and the time when it is obtained.
+ struct AppAvailability {
+ AppAvailabilityResult availability;
+ Clock::time_point time;
+ };
+
+ CastAppAvailabilityTracker();
+ ~CastAppAvailabilityTracker();
+
+ CastAppAvailabilityTracker(const CastAppAvailabilityTracker&) = delete;
+ CastAppAvailabilityTracker& operator=(const CastAppAvailabilityTracker&) =
+ delete;
+
+ // Registers |source| with the tracker. Returns a list of new app IDs that
+ // were previously not known to the tracker.
+ std::vector<std::string> RegisterSource(const CastMediaSource& source);
+
+ // Unregisters the source given by |source| or |source_id| with the tracker.
+ void UnregisterSource(const std::string& source_id);
+ void UnregisterSource(const CastMediaSource& source);
+
+ // Updates the availability of |app_id| on |device_id| to |availability|.
+ // Returns a list of registered CastMediaSources for which the set of
+ // available devices might have been updated by this call. The caller should
+ // call |GetAvailableDevices| with the returned CastMediaSources to get the
+ // updated lists.
+ std::vector<CastMediaSource> UpdateAppAvailability(
+ const std::string& device_id,
+ const std::string& app_id,
+ AppAvailability availability);
+
+ // Removes all results associated with |device_id|, i.e. when the device
+ // becomes invalid. Returns a list of registered CastMediaSources for which
+ // the set of available devices might have been updated by this call. The
+ // caller should call |GetAvailableDevices| with the returned CastMediaSources
+ // to get the updated lists.
+ std::vector<CastMediaSource> RemoveResultsForDevice(
+ const std::string& device_id);
+
+ // Returns a list of registered CastMediaSources supported by |device_id|.
+ std::vector<CastMediaSource> GetSupportedSources(
+ const std::string& device_id) const;
+
+ // Returns the availability for |app_id| on |device_id| and the time at which
+ // the availability was determined. If availability is kUnknown, then the time
+ // may be null (e.g. if an availability request was never sent).
+ AppAvailability GetAvailability(const std::string& device_id,
+ const std::string& app_id) const;
+
+ // Returns a list of registered app IDs.
+ std::vector<std::string> GetRegisteredApps() const;
+
+ // Returns a list of device IDs compatible with |source|, using the current
+ // availability info.
+ std::vector<std::string> GetAvailableDevices(
+ const CastMediaSource& source) const;
+
+ private:
+ // App ID to availability.
+ using AppAvailabilityMap = std::map<std::string, AppAvailability>;
+
+ // Registered sources and corresponding CastMediaSources.
+ std::map<std::string, CastMediaSource> registered_sources_;
+
+ // App IDs tracked and the number of registered sources containing them.
+ std::map<std::string, int> registration_count_by_app_id_;
+
+ // IDs and app availabilities of known devices.
+ std::map<std::string, AppAvailabilityMap> app_availabilities_;
+};
+
+} // namespace cast
+} // namespace openscreen
+
+#endif // CAST_SENDER_CAST_APP_AVAILABILITY_TRACKER_H_
diff --git a/cast/sender/cast_app_availability_tracker_unittest.cc b/cast/sender/cast_app_availability_tracker_unittest.cc
new file mode 100644
index 00000000..b45d3563
--- /dev/null
+++ b/cast/sender/cast_app_availability_tracker_unittest.cc
@@ -0,0 +1,159 @@
+// 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/sender/cast_app_availability_tracker.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "platform/test/fake_clock.h"
+
+namespace openscreen {
+namespace cast {
+namespace {
+
+using ::testing::UnorderedElementsAreArray;
+
+MATCHER_P(CastMediaSourcesEqual, expected, "") {
+ if (expected.size() != arg.size())
+ return false;
+ return std::equal(
+ expected.begin(), expected.end(), arg.begin(),
+ [](const CastMediaSource& source1, const CastMediaSource& source2) {
+ return source1.source_id() == source2.source_id();
+ });
+}
+
+} // namespace
+
+class CastAppAvailabilityTrackerTest : public ::testing::Test {
+ public:
+ CastAppAvailabilityTrackerTest() : clock_(Clock::now()) {}
+ ~CastAppAvailabilityTrackerTest() override = default;
+
+ Clock::time_point Now() const { return clock_.now(); }
+
+ protected:
+ FakeClock clock_;
+ CastAppAvailabilityTracker tracker_;
+};
+
+TEST_F(CastAppAvailabilityTrackerTest, RegisterSource) {
+ CastMediaSource source1("cast:AAA?clientId=1", {"AAA"});
+ CastMediaSource source2("cast:AAA?clientId=2", {"AAA"});
+
+ std::vector<std::string> expected_app_ids = {"AAA"};
+ EXPECT_EQ(expected_app_ids, tracker_.RegisterSource(source1));
+
+ EXPECT_EQ(std::vector<std::string>{}, tracker_.RegisterSource(source1));
+ EXPECT_EQ(std::vector<std::string>{}, tracker_.RegisterSource(source2));
+
+ tracker_.UnregisterSource(source1);
+ tracker_.UnregisterSource(source2);
+
+ EXPECT_EQ(expected_app_ids, tracker_.RegisterSource(source1));
+ EXPECT_EQ(expected_app_ids, tracker_.GetRegisteredApps());
+}
+
+TEST_F(CastAppAvailabilityTrackerTest, RegisterSourceReturnsMultipleAppIds) {
+ CastMediaSource source1("urn:x-org.chromium.media:source:tab:1",
+ {"0F5096E8", "85CDB22F"});
+
+ // Mirorring app ids.
+ std::vector<std::string> expected_app_ids = {"0F5096E8", "85CDB22F"};
+ EXPECT_THAT(tracker_.RegisterSource(source1),
+ UnorderedElementsAreArray(expected_app_ids));
+ EXPECT_THAT(tracker_.GetRegisteredApps(),
+ UnorderedElementsAreArray(expected_app_ids));
+}
+
+TEST_F(CastAppAvailabilityTrackerTest, MultipleAppIdsAlreadyTrackingOne) {
+ // One of the mirroring app IDs.
+ CastMediaSource source1("cast:0F5096E8?clientId=123", {"0F5096E8"});
+
+ std::vector<std::string> new_app_ids = {"0F5096E8"};
+ std::vector<std::string> registered_app_ids = {"0F5096E8"};
+ EXPECT_EQ(new_app_ids, tracker_.RegisterSource(source1));
+ EXPECT_EQ(registered_app_ids, tracker_.GetRegisteredApps());
+
+ CastMediaSource source2("urn:x-org.chromium.media:source:tab:1",
+ {"0F5096E8", "85CDB22F"});
+
+ new_app_ids = {"85CDB22F"};
+ registered_app_ids = {"0F5096E8", "85CDB22F"};
+
+ EXPECT_EQ(new_app_ids, tracker_.RegisterSource(source2));
+ EXPECT_THAT(tracker_.GetRegisteredApps(),
+ UnorderedElementsAreArray(registered_app_ids));
+}
+
+TEST_F(CastAppAvailabilityTrackerTest, UpdateAppAvailability) {
+ CastMediaSource source1("cast:AAA?clientId=1", {"AAA"});
+ CastMediaSource source2("cast:AAA?clientId=2", {"AAA"});
+ CastMediaSource source3("cast:BBB?clientId=3", {"BBB"});
+
+ tracker_.RegisterSource(source3);
+
+ // |source3| not affected.
+ EXPECT_THAT(
+ tracker_.UpdateAppAvailability(
+ "deviceId1", "AAA", {AppAvailabilityResult::kAvailable, Now()}),
+ CastMediaSourcesEqual(std::vector<CastMediaSource>()));
+
+ std::vector<std::string> devices_1 = {"deviceId1"};
+ std::vector<std::string> devices_1_2 = {"deviceId1", "deviceId2"};
+ std::vector<CastMediaSource> sources_1 = {source1};
+ std::vector<CastMediaSource> sources_1_2 = {source1, source2};
+
+ // Tracker returns available devices even though sources aren't registered.
+ EXPECT_EQ(devices_1, tracker_.GetAvailableDevices(source1));
+ EXPECT_EQ(devices_1, tracker_.GetAvailableDevices(source2));
+ EXPECT_TRUE(tracker_.GetAvailableDevices(source3).empty());
+
+ tracker_.RegisterSource(source1);
+ // Only |source1| is registered for this app.
+ EXPECT_THAT(
+ tracker_.UpdateAppAvailability(
+ "deviceId2", "AAA", {AppAvailabilityResult::kAvailable, Now()}),
+ CastMediaSourcesEqual(sources_1));
+ EXPECT_THAT(tracker_.GetAvailableDevices(source1),
+ UnorderedElementsAreArray(devices_1_2));
+ EXPECT_THAT(tracker_.GetAvailableDevices(source2),
+ UnorderedElementsAreArray(devices_1_2));
+ EXPECT_TRUE(tracker_.GetAvailableDevices(source3).empty());
+
+ tracker_.RegisterSource(source2);
+ EXPECT_THAT(
+ tracker_.UpdateAppAvailability(
+ "deviceId2", "AAA", {AppAvailabilityResult::kUnavailable, Now()}),
+ CastMediaSourcesEqual(sources_1_2));
+ EXPECT_EQ(devices_1, tracker_.GetAvailableDevices(source1));
+ EXPECT_EQ(devices_1, tracker_.GetAvailableDevices(source2));
+ EXPECT_TRUE(tracker_.GetAvailableDevices(source3).empty());
+}
+
+TEST_F(CastAppAvailabilityTrackerTest, RemoveResultsForDevice) {
+ CastMediaSource source1("cast:AAA?clientId=1", {"AAA"});
+
+ tracker_.UpdateAppAvailability("deviceId1", "AAA",
+ {AppAvailabilityResult::kAvailable, Now()});
+ EXPECT_EQ(AppAvailabilityResult::kAvailable,
+ tracker_.GetAvailability("deviceId1", "AAA").availability);
+
+ std::vector<std::string> expected_device_ids = {"deviceId1"};
+ EXPECT_EQ(expected_device_ids, tracker_.GetAvailableDevices(source1));
+
+ // Unrelated device ID.
+ tracker_.RemoveResultsForDevice("deviceId2");
+ EXPECT_EQ(AppAvailabilityResult::kAvailable,
+ tracker_.GetAvailability("deviceId1", "AAA").availability);
+ EXPECT_EQ(expected_device_ids, tracker_.GetAvailableDevices(source1));
+
+ tracker_.RemoveResultsForDevice("deviceId1");
+ EXPECT_EQ(AppAvailabilityResult::kUnknown,
+ tracker_.GetAvailability("deviceId1", "AAA").availability);
+ EXPECT_EQ(std::vector<std::string>{}, tracker_.GetAvailableDevices(source1));
+}
+
+} // namespace cast
+} // namespace openscreen
diff --git a/cast/sender/public/DEPS b/cast/sender/public/DEPS
index 44de6584..3cfae918 100644
--- a/cast/sender/public/DEPS
+++ b/cast/sender/public/DEPS
@@ -1,12 +1,7 @@
# -*- Mode: Python; -*-
include_rules = [
- # By default, openscreen implementation libraries should not be exposed
- # through public APIs.
- '-base',
- '-platform',
-
# Dependencies on the implementation are not allowed in public/.
'-cast/sender',
- '+cast/sender/public'
+ '+cast/sender/public',
]
diff --git a/cast/sender/public/README.md b/cast/sender/public/README.md
index ace86163..b670d110 100644
--- a/cast/sender/public/README.md
+++ b/cast/sender/public/README.md
@@ -1,4 +1,4 @@
-# cast/receiver/public
+# cast/sender/public
This module contains an implementation of the Cast "sender", i.e. the client
that discovers Cast devices on the LAN and launches apps on them.
diff --git a/cast/sender/public/cast_media_source.cc b/cast/sender/public/cast_media_source.cc
new file mode 100644
index 00000000..ebc9a3fe
--- /dev/null
+++ b/cast/sender/public/cast_media_source.cc
@@ -0,0 +1,45 @@
+// 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/sender/public/cast_media_source.h"
+
+#include <algorithm>
+
+#include "util/logging.h"
+
+namespace openscreen {
+namespace cast {
+
+// static
+ErrorOr<CastMediaSource> CastMediaSource::From(const std::string& source) {
+ // TODO(btolsch): Implement when we have URL parsing.
+ OSP_UNIMPLEMENTED();
+ return Error::Code::kUnknownError;
+}
+
+CastMediaSource::CastMediaSource(std::string source,
+ std::vector<std::string> app_ids)
+ : source_id_(std::move(source)), app_ids_(std::move(app_ids)) {}
+
+CastMediaSource::CastMediaSource(const CastMediaSource& other) = default;
+CastMediaSource::CastMediaSource(CastMediaSource&& other) = default;
+
+CastMediaSource::~CastMediaSource() = default;
+
+CastMediaSource& CastMediaSource::operator=(const CastMediaSource& other) =
+ default;
+CastMediaSource& CastMediaSource::operator=(CastMediaSource&& other) = default;
+
+bool CastMediaSource::ContainsAppId(const std::string& app_id) const {
+ return std::find(app_ids_.begin(), app_ids_.end(), app_id) != app_ids_.end();
+}
+
+bool CastMediaSource::ContainsAnyAppIdFrom(
+ const std::vector<std::string>& app_ids) const {
+ return std::find_first_of(app_ids_.begin(), app_ids_.end(), app_ids.begin(),
+ app_ids.end()) != app_ids_.end();
+}
+
+} // namespace cast
+} // namespace openscreen
diff --git a/cast/sender/public/cast_media_source.h b/cast/sender/public/cast_media_source.h
new file mode 100644
index 00000000..18af80f8
--- /dev/null
+++ b/cast/sender/public/cast_media_source.h
@@ -0,0 +1,42 @@
+// 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_SENDER_PUBLIC_CAST_MEDIA_SOURCE_H_
+#define CAST_SENDER_PUBLIC_CAST_MEDIA_SOURCE_H_
+
+#include <string>
+#include <vector>
+
+#include "platform/base/error.h"
+
+namespace openscreen {
+namespace cast {
+
+class CastMediaSource {
+ public:
+ static ErrorOr<CastMediaSource> From(const std::string& source);
+
+ CastMediaSource(std::string source, std::vector<std::string> app_ids);
+ CastMediaSource(const CastMediaSource& other);
+ CastMediaSource(CastMediaSource&& other);
+ ~CastMediaSource();
+
+ CastMediaSource& operator=(const CastMediaSource& other);
+ CastMediaSource& operator=(CastMediaSource&& other);
+
+ bool ContainsAppId(const std::string& app_id) const;
+ bool ContainsAnyAppIdFrom(const std::vector<std::string>& app_ids) const;
+
+ const std::string& source_id() const { return source_id_; }
+ const std::vector<std::string>& app_ids() const { return app_ids_; }
+
+ private:
+ std::string source_id_;
+ std::vector<std::string> app_ids_;
+};
+
+} // namespace cast
+} // namespace openscreen
+
+#endif // CAST_SENDER_PUBLIC_CAST_MEDIA_SOURCE_H_