Commit a8e2568e authored by Yuri Wiitala's avatar Yuri Wiitala

Introduce audio::GroupCoordinator and audio::GroupMember concept.

The GroupCoordinator will be used in the new Audio Service loopback and
muting features. It tracks one or more audio outputs that are members of
the same group, and notifies interested entities (e.g., a muter, a
loopback mixer, etc.) of membership changes to the group.

Bug: 824019
Change-Id: I9b8b1564e8fa7977d2d8dd21c466c7cbf5cdd3f2
Reviewed-on: https://chromium-review.googlesource.com/987093Reviewed-by: default avatarMax Morin <maxmorin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#548257}
parent baf01a8f
......@@ -32,6 +32,9 @@ source_set("lib") {
sources = [
"debug_recording.cc",
"debug_recording.h",
"group_coordinator.cc",
"group_coordinator.h",
"group_member.h",
"in_process_audio_manager_accessor.cc",
"in_process_audio_manager_accessor.h",
"output_stream.cc",
......@@ -61,6 +64,7 @@ source_set("tests") {
sources = [
"debug_recording_unittest.cc",
"group_coordinator_unittest.cc",
"output_stream_unittest.cc",
"test/audio_system_to_service_adapter_test.cc",
"test/debug_recording_session_unittest.cc",
......
// Copyright 2018 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 "services/audio/group_coordinator.h"
#include <algorithm>
#include "base/stl_util.h"
#include "services/audio/group_member.h"
namespace audio {
GroupCoordinator::GroupCoordinator() {
DETACH_FROM_SEQUENCE(sequence_checker_);
}
GroupCoordinator::~GroupCoordinator() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(groups_.empty());
}
void GroupCoordinator::RegisterGroupMember(GroupMember* member) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(member);
const auto it = FindGroup(member->GetGroupId());
std::vector<GroupMember*>& members = it->second.members;
DCHECK(!base::ContainsValue(members, member));
members.push_back(member);
for (Observer* observer : it->second.observers) {
observer->OnMemberJoinedGroup(member);
}
}
void GroupCoordinator::UnregisterGroupMember(GroupMember* member) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(member);
const auto group_it = FindGroup(member->GetGroupId());
std::vector<GroupMember*>& members = group_it->second.members;
const auto member_it = std::find(members.begin(), members.end(), member);
DCHECK(member_it != members.end());
members.erase(member_it);
for (Observer* observer : group_it->second.observers) {
observer->OnMemberLeftGroup(member);
}
MaybePruneGroupMapEntry(group_it);
}
void GroupCoordinator::AddObserver(const base::UnguessableToken& group_id,
Observer* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(observer);
std::vector<Observer*>& observers = FindGroup(group_id)->second.observers;
DCHECK(!base::ContainsValue(observers, observer));
observers.push_back(observer);
}
void GroupCoordinator::RemoveObserver(const base::UnguessableToken& group_id,
Observer* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(observer);
const auto group_it = FindGroup(group_id);
std::vector<Observer*>& observers = group_it->second.observers;
const auto it = std::find(observers.begin(), observers.end(), observer);
DCHECK(it != observers.end());
observers.erase(it);
MaybePruneGroupMapEntry(group_it);
}
const std::vector<GroupMember*>& GroupCoordinator::GetCurrentMembers(
const base::UnguessableToken& group_id) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (const auto& entry : groups_) {
if (entry.first == group_id) {
return entry.second.members;
}
}
static const std::vector<GroupMember*> empty_set;
return empty_set;
}
GroupCoordinator::GroupMap::iterator GroupCoordinator::FindGroup(
const base::UnguessableToken& group_id) {
for (auto it = groups_.begin(); it != groups_.end(); ++it) {
if (it->first == group_id) {
return it;
}
}
// Group does not exist. Create a new entry.
groups_.emplace_back();
const auto new_it = groups_.end() - 1;
new_it->first = group_id;
return new_it;
}
void GroupCoordinator::MaybePruneGroupMapEntry(GroupMap::iterator it) {
if (it->second.members.empty() && it->second.observers.empty()) {
groups_.erase(it);
}
}
GroupCoordinator::Observer::~Observer() = default;
GroupCoordinator::Group::Group() = default;
GroupCoordinator::Group::~Group() = default;
GroupCoordinator::Group::Group(GroupCoordinator::Group&& other) = default;
GroupCoordinator::Group& GroupCoordinator::Group::operator=(
GroupCoordinator::Group&& other) = default;
} // namespace audio
// Copyright 2018 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 SERVICES_AUDIO_GROUP_COORDINATOR_H_
#define SERVICES_AUDIO_GROUP_COORDINATOR_H_
#include <utility>
#include <vector>
#include "base/macros.h"
#include "base/sequence_checker.h"
#include "base/unguessable_token.h"
namespace audio {
class GroupMember;
// Manages a registry of group members and notifies observers as membership in
// the group changes.
class GroupCoordinator {
public:
// Interface for entities that wish to montior and take action as members
// join/leave a particular group.
class Observer {
public:
virtual void OnMemberJoinedGroup(GroupMember* member) = 0;
virtual void OnMemberLeftGroup(GroupMember* member) = 0;
protected:
virtual ~Observer();
};
GroupCoordinator();
~GroupCoordinator();
// Registers/Unregisters a group |member|. The member must remain valid until
// after UnregisterGroupMember() is called.
void RegisterGroupMember(GroupMember* member);
void UnregisterGroupMember(GroupMember* member);
void AddObserver(const base::UnguessableToken& group_id, Observer* observer);
void RemoveObserver(const base::UnguessableToken& group_id,
Observer* observer);
// Returns the current members in the group having the given |group_id|. Note
// that the validity of the returned reference is uncertain once any of the
// other non-const methods are called.
const std::vector<GroupMember*>& GetCurrentMembers(
const base::UnguessableToken& group_id) const;
private:
struct Group {
std::vector<GroupMember*> members;
std::vector<Observer*> observers;
Group();
~Group();
Group(Group&& other);
Group& operator=(Group&& other);
private:
DISALLOW_COPY_AND_ASSIGN(Group);
};
using GroupMap = std::vector<std::pair<base::UnguessableToken, Group>>;
// Returns an iterator to the entry associated with the given |group_id|,
// creating a new one if necessary.
GroupMap::iterator FindGroup(const base::UnguessableToken& group_id);
// Deletes the entry in |groups_| if it has no members or observers remaining.
void MaybePruneGroupMapEntry(GroupMap::iterator it);
GroupMap groups_;
SEQUENCE_CHECKER(sequence_checker_);
DISALLOW_COPY_AND_ASSIGN(GroupCoordinator);
};
} // namespace audio
#endif // SERVICES_AUDIO_GROUP_COORDINATOR_H_
// Copyright 2018 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 "services/audio/group_coordinator.h"
#include "base/stl_util.h"
#include "base/unguessable_token.h"
#include "media/base/audio_parameters.h"
#include "services/audio/group_member.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using base::UnguessableToken;
using testing::AtLeast;
using testing::NiceMock;
using testing::ReturnRef;
using testing::Sequence;
using testing::StrictMock;
using testing::_;
namespace audio {
namespace {
class MockGroupMember : public GroupMember {
public:
explicit MockGroupMember(const UnguessableToken& group_id)
: group_id_(group_id) {}
~MockGroupMember() override = default;
const UnguessableToken& GetGroupId() final { return group_id_; }
MOCK_METHOD0(GetAudioParameters, const media::AudioParameters&());
MOCK_METHOD1(StartSnooping, void(Snooper* snooper));
MOCK_METHOD1(StopSnooping, void(Snooper* snooper));
MOCK_METHOD0(StartMuting, void());
MOCK_METHOD0(StopMuting, void());
MOCK_METHOD0(IsMuting, bool());
private:
const UnguessableToken group_id_;
DISALLOW_COPY_AND_ASSIGN(MockGroupMember);
};
class MockGroupObserver : public GroupCoordinator::Observer {
public:
MockGroupObserver() = default;
~MockGroupObserver() override = default;
MOCK_METHOD1(OnMemberJoinedGroup, void(GroupMember* member));
MOCK_METHOD1(OnMemberLeftGroup, void(GroupMember* member));
private:
DISALLOW_COPY_AND_ASSIGN(MockGroupObserver);
};
TEST(GroupCoordinatorTest, NeverUsed) {
GroupCoordinator coordinator;
}
TEST(GroupCoordinatorTest, RegistersMembersInSameGroup) {
const UnguessableToken group_id = UnguessableToken::Create();
StrictMock<MockGroupMember> member1(group_id);
StrictMock<MockGroupMember> member2(group_id);
// An observer should see each member join and leave the group once.
StrictMock<MockGroupObserver> observer;
Sequence join_leave_sequence;
EXPECT_CALL(observer, OnMemberJoinedGroup(&member1))
.InSequence(join_leave_sequence);
EXPECT_CALL(observer, OnMemberJoinedGroup(&member2))
.InSequence(join_leave_sequence);
EXPECT_CALL(observer, OnMemberLeftGroup(&member1))
.InSequence(join_leave_sequence);
EXPECT_CALL(observer, OnMemberLeftGroup(&member2))
.InSequence(join_leave_sequence);
GroupCoordinator coordinator;
coordinator.AddObserver(group_id, &observer);
coordinator.RegisterGroupMember(&member1);
coordinator.RegisterGroupMember(&member2);
const std::vector<GroupMember*>& members =
coordinator.GetCurrentMembers(group_id);
EXPECT_EQ(2u, members.size());
EXPECT_TRUE(base::ContainsValue(members, &member1));
EXPECT_TRUE(base::ContainsValue(members, &member2));
EXPECT_TRUE(
coordinator.GetCurrentMembers(UnguessableToken::Create()).empty());
coordinator.UnregisterGroupMember(&member1);
coordinator.UnregisterGroupMember(&member2);
EXPECT_TRUE(coordinator.GetCurrentMembers(group_id).empty());
coordinator.RemoveObserver(group_id, &observer);
EXPECT_TRUE(coordinator.GetCurrentMembers(group_id).empty());
}
TEST(GroupCoordinatorTest, RegistersMembersInDifferentGroups) {
const UnguessableToken group_id_a = UnguessableToken::Create();
StrictMock<MockGroupMember> member_a_1(group_id_a);
StrictMock<MockGroupMember> member_a_2(group_id_a);
StrictMock<MockGroupObserver> observer_a;
Sequence join_leave_sequence_a;
EXPECT_CALL(observer_a, OnMemberJoinedGroup(&member_a_1))
.InSequence(join_leave_sequence_a);
EXPECT_CALL(observer_a, OnMemberJoinedGroup(&member_a_2))
.InSequence(join_leave_sequence_a);
EXPECT_CALL(observer_a, OnMemberLeftGroup(&member_a_1))
.InSequence(join_leave_sequence_a);
EXPECT_CALL(observer_a, OnMemberLeftGroup(&member_a_2))
.InSequence(join_leave_sequence_a);
const UnguessableToken group_id_b = UnguessableToken::Create();
StrictMock<MockGroupMember> member_b_1(group_id_b);
StrictMock<MockGroupObserver> observer_b;
Sequence join_leave_sequence_b;
EXPECT_CALL(observer_b, OnMemberJoinedGroup(&member_b_1))
.InSequence(join_leave_sequence_b);
EXPECT_CALL(observer_b, OnMemberLeftGroup(&member_b_1))
.InSequence(join_leave_sequence_b);
GroupCoordinator coordinator;
coordinator.AddObserver(group_id_a, &observer_a);
coordinator.AddObserver(group_id_b, &observer_b);
coordinator.RegisterGroupMember(&member_a_1);
coordinator.RegisterGroupMember(&member_b_1);
coordinator.RegisterGroupMember(&member_a_2);
const std::vector<GroupMember*>& members_a =
coordinator.GetCurrentMembers(group_id_a);
EXPECT_EQ(2u, members_a.size());
EXPECT_TRUE(base::ContainsValue(members_a, &member_a_1));
EXPECT_TRUE(base::ContainsValue(members_a, &member_a_2));
EXPECT_EQ(std::vector<GroupMember*>({&member_b_1}),
coordinator.GetCurrentMembers(group_id_b));
EXPECT_TRUE(
coordinator.GetCurrentMembers(UnguessableToken::Create()).empty());
coordinator.UnregisterGroupMember(&member_a_1);
EXPECT_EQ(std::vector<GroupMember*>({&member_a_2}),
coordinator.GetCurrentMembers(group_id_a));
coordinator.UnregisterGroupMember(&member_b_1);
EXPECT_TRUE(coordinator.GetCurrentMembers(group_id_b).empty());
coordinator.UnregisterGroupMember(&member_a_2);
EXPECT_TRUE(coordinator.GetCurrentMembers(group_id_a).empty());
coordinator.RemoveObserver(group_id_a, &observer_a);
coordinator.RemoveObserver(group_id_b, &observer_b);
EXPECT_TRUE(coordinator.GetCurrentMembers(group_id_a).empty());
EXPECT_TRUE(coordinator.GetCurrentMembers(group_id_b).empty());
}
TEST(GroupCoordinatorTest, TracksMembersWithoutAnObserverPresent) {
const UnguessableToken group_id = UnguessableToken::Create();
StrictMock<MockGroupMember> member1(group_id);
StrictMock<MockGroupMember> member2(group_id);
GroupCoordinator coordinator;
coordinator.RegisterGroupMember(&member1);
coordinator.RegisterGroupMember(&member2);
const std::vector<GroupMember*>& members =
coordinator.GetCurrentMembers(group_id);
EXPECT_EQ(2u, members.size());
EXPECT_TRUE(base::ContainsValue(members, &member1));
EXPECT_TRUE(base::ContainsValue(members, &member2));
EXPECT_TRUE(
coordinator.GetCurrentMembers(UnguessableToken::Create()).empty());
coordinator.UnregisterGroupMember(&member1);
coordinator.UnregisterGroupMember(&member2);
EXPECT_TRUE(coordinator.GetCurrentMembers(group_id).empty());
}
TEST(GroupCoordinatorTest, NotifiesOnlyWhileObserving) {
const UnguessableToken group_id = UnguessableToken::Create();
StrictMock<MockGroupMember> member1(group_id);
StrictMock<MockGroupMember> member2(group_id);
// The observer will only be around at the time when member2 joins the group
// and when member1 leaves the group.
StrictMock<MockGroupObserver> observer;
Sequence join_leave_sequence;
EXPECT_CALL(observer, OnMemberJoinedGroup(&member1)).Times(0);
EXPECT_CALL(observer, OnMemberJoinedGroup(&member2))
.InSequence(join_leave_sequence);
EXPECT_CALL(observer, OnMemberLeftGroup(&member1))
.InSequence(join_leave_sequence);
EXPECT_CALL(observer, OnMemberLeftGroup(&member2)).Times(0);
GroupCoordinator coordinator;
coordinator.RegisterGroupMember(&member1);
EXPECT_EQ(std::vector<GroupMember*>({&member1}),
coordinator.GetCurrentMembers(group_id));
coordinator.AddObserver(group_id, &observer);
coordinator.RegisterGroupMember(&member2);
const std::vector<GroupMember*>& members =
coordinator.GetCurrentMembers(group_id);
EXPECT_EQ(2u, members.size());
EXPECT_TRUE(base::ContainsValue(members, &member1));
EXPECT_TRUE(base::ContainsValue(members, &member2));
coordinator.UnregisterGroupMember(&member1);
EXPECT_EQ(std::vector<GroupMember*>({&member2}),
coordinator.GetCurrentMembers(group_id));
coordinator.RemoveObserver(group_id, &observer);
EXPECT_EQ(std::vector<GroupMember*>({&member2}),
coordinator.GetCurrentMembers(group_id));
coordinator.UnregisterGroupMember(&member2);
EXPECT_TRUE(coordinator.GetCurrentMembers(group_id).empty());
}
} // namespace
} // namespace audio
// Copyright 2018 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 SERVICES_AUDIO_GROUP_MEMBER_H_
#define SERVICES_AUDIO_GROUP_MEMBER_H_
#include "base/time/time.h"
namespace base {
class UnguessableToken;
}
namespace media {
class AudioBus;
class AudioParameters;
} // namespace media
namespace audio {
// Interface for accessing signal data and controlling a members of an audio
// group. A group is defined by a common group identifier that all members
// share.
//
// The purpose of the grouping concept is to allow a feature to identify all
// audio flows that come from the same logical unit, such as a browser tab. The
// audio flows can then be duplicated, or other group-wide control exercised on
// all members (such as audio muting).
class GroupMember {
public:
class Snooper {
public:
// Provides read-only access to the data flowing through a GroupMember.
virtual void OnData(const media::AudioBus& audio_bus,
base::TimeTicks reference_time,
double volume) = 0;
protected:
virtual ~Snooper() = default;
};
// Returns the string identifier of the group. This must not change for the
// lifetime of this group member.
virtual const base::UnguessableToken& GetGroupId() = 0;
// Returns the audio parameters of the snoopable audio data. The parameters
// must not change for the lifetime of this group member, but can be different
// than those of other members.
virtual const media::AudioParameters& GetAudioParameters() = 0;
// Starts/Stops snooping on the audio data flowing through this group member.
virtual void StartSnooping(Snooper* snooper) = 0;
virtual void StopSnooping(Snooper* snooper) = 0;
// Starts/Stops muting of the outbound audio signal from this group member.
// However, the audio data being sent to Snoopers should be the original,
// unmuted audio. Note that an equal number of start versus stop calls here is
// not required, and the implementation should ignore redundant calls.
virtual void StartMuting() = 0;
virtual void StopMuting() = 0;
protected:
virtual ~GroupMember() = default;
};
} // namespace audio
#endif // SERVICES_AUDIO_GROUP_MEMBER_H_
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment