Commit d3444394 authored by Becca Hughes's avatar Becca Hughes Committed by Commit Bot

[Audio Focus] Add sessions to media internals

Add a list of media sessions and their state
to a new audio focus tab on media internals.

BUG=870418

Change-Id: I654b9444c1abc3f5ae12c29d070627e137f5e137
Reviewed-on: https://chromium-review.googlesource.com/1161500
Commit-Queue: Becca Hughes <beccahughes@chromium.org>
Reviewed-by: default avatarMounir Lamouri <mlamouri@chromium.org>
Reviewed-by: default avatarChrome Cunningham <chcunningham@chromium.org>
Cr-Commit-Position: refs/heads/master@{#584216}
parent bbc341fb
......@@ -16,6 +16,8 @@
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "build/build_config.h"
#include "content/browser/media/session/audio_focus_manager.h"
#include "content/browser/media/session/media_session_impl.h"
#include "content/browser/renderer_host/media/media_stream_manager.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
......@@ -698,6 +700,34 @@ void MediaInternals::SendVideoCaptureDeviceCapabilities() {
&video_capture_capabilities_cached_data_));
}
#if !defined(OS_ANDROID)
void MediaInternals::SendAudioFocusState() {
if (!CanUpdate())
return;
base::DictionaryValue audio_focus_data;
const std::list<MediaSessionImpl*>& stack =
AudioFocusManager::GetInstance()->audio_focus_stack_;
// We should go backwards through the stack so the top of the stack is always
// shown first in the list.
base::ListValue stack_data;
for (auto iter = stack.rbegin(); iter != stack.rend(); ++iter) {
MediaSessionImpl::DebugInfo debug_info = (*iter)->GetDebugInfo();
base::DictionaryValue media_session_data;
media_session_data.SetKey("name", base::Value(debug_info.name));
media_session_data.SetKey("owner", base::Value(debug_info.owner));
media_session_data.SetKey("state", base::Value(debug_info.state));
stack_data.GetList().push_back(std::move(media_session_data));
}
audio_focus_data.SetKey("sessions", std::move(stack_data));
SendUpdate(
SerializeUpdate("media.onReceiveAudioFocusState", &audio_focus_data));
}
#endif // !defined(OS_ANDROID)
void MediaInternals::UpdateVideoCaptureDeviceCapabilities(
const std::vector<std::tuple<media::VideoCaptureDeviceDescriptor,
media::VideoCaptureFormats>>&
......@@ -778,6 +808,25 @@ void MediaInternals::OnProcessTerminatedForTesting(int process_id) {
uma_handler_->OnProcessTerminated(process_id);
}
#if !defined(OS_ANDROID)
void MediaInternals::OnFocusGained(MediaSession* media_session,
AudioFocusType type) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
base::BindOnce(&MediaInternals::SendAudioFocusState,
base::Unretained(this)));
}
void MediaInternals::OnFocusLost(MediaSession* media_session) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
base::BindOnce(&MediaInternals::SendAudioFocusState,
base::Unretained(this)));
}
#endif // !defined(OS_ANDROID)
void MediaInternals::SendUpdate(const base::string16& update) {
// SendUpdate() may be called from any thread, but must run on the UI thread.
if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) {
......
......@@ -18,6 +18,8 @@
#include "base/strings/string16.h"
#include "base/synchronization/lock.h"
#include "base/values.h"
#include "build/build_config.h"
#include "content/browser/media/session/audio_focus_observer.h"
#include "content/common/content_export.h"
#include "content/public/browser/notification_observer.h"
#include "content/public/browser/notification_registrar.h"
......@@ -33,10 +35,15 @@ struct MediaLogEvent;
namespace content {
enum class AudioFocusType;
// This class stores information about currently active media.
// TODO(crbug.com/812557): Remove inheritance from media::AudioLogFactory once
// the creation of the AudioManager instance moves to the audio service.
class CONTENT_EXPORT MediaInternals : public media::AudioLogFactory,
#if !defined(OS_ANDROID)
public AudioFocusObserver,
#endif
public NotificationObserver {
public:
// Called with the update string.
......@@ -74,6 +81,11 @@ class CONTENT_EXPORT MediaInternals : public media::AudioLogFactory,
// UpdateCallback.
void SendVideoCaptureDeviceCapabilities();
#if !defined(OS_ANDROID)
// Sends all audio focus information to each registered UpdateCallback.
void SendAudioFocusState();
#endif
// Called to inform of the capabilities enumerated for video devices.
void UpdateVideoCaptureDeviceCapabilities(
const std::vector<std::tuple<media::VideoCaptureDeviceDescriptor,
......@@ -109,6 +121,12 @@ class CONTENT_EXPORT MediaInternals : public media::AudioLogFactory,
MediaInternals();
#if !defined(OS_ANDROID)
// AudioFocusObserver implementation.
void OnFocusGained(MediaSession* media_session, AudioFocusType type) override;
void OnFocusLost(MediaSession* media_session) override;
#endif
// Sends |update| to each registered UpdateCallback. Safe to call from any
// thread, but will forward to the IO thread.
void SendUpdate(const base::string16& update);
......
......@@ -6,6 +6,7 @@
#include "base/bind.h"
#include "base/location.h"
#include "build/build_config.h"
#include "content/browser/media/media_internals.h"
#include "content/browser/media/media_internals_handler.h"
#include "content/public/browser/browser_thread.h"
......@@ -48,6 +49,10 @@ void MediaInternalsProxy::GetEverythingOnIOThread() {
// TODO(xhwang): Investigate whether we can update on UI thread directly.
MediaInternals::GetInstance()->SendAudioStreamData();
MediaInternals::GetInstance()->SendVideoCaptureDeviceCapabilities();
#if !defined(OS_ANDROID)
MediaInternals::GetInstance()->SendAudioFocusState();
#endif
}
void MediaInternalsProxy::UpdateUIOnUIThread(const base::string16& update) {
......
......@@ -12,13 +12,20 @@
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_command_line.h"
#include "base/test/test_message_loop.h"
#include "base/threading/thread_task_runner_handle.h"
#include "build/build_config.h"
#include "content/browser/media/session/audio_focus_manager.h"
#include "content/browser/media/session/audio_focus_type.h"
#include "content/browser/media/session/media_session_impl.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "content/test/test_web_contents.h"
#include "media/base/audio_parameters.h"
#include "media/base/channel_layout.h"
#include "media/base/media_log.h"
#include "media/base/media_switches.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/geometry/size.h"
......@@ -279,4 +286,139 @@ INSTANTIATE_TEST_CASE_P(
media::AudioLogFactory::AUDIO_OUTPUT_CONTROLLER,
media::AudioLogFactory::AUDIO_OUTPUT_STREAM));
// TODO(https://crbug.com/873320): AudioFocusManager is not available on
// Android.
#if !defined(OS_ANDROID)
namespace {
// Test page titles.
const char kTestTitle1[] = "Test Title 1";
const char kTestTitle2[] = "Test Title 2";
} // namespace
class MediaInternalsAudioFocusTest : public testing::Test,
public MediaInternalsTestBase {
public:
void SetUp() override {
update_cb_ =
base::BindRepeating(&MediaInternalsAudioFocusTest::UpdateCallbackImpl,
base::Unretained(this));
scoped_command_line_.GetProcessCommandLine()->AppendSwitch(
switches::kEnableAudioFocus);
content::MediaInternals::GetInstance()->AddUpdateCallback(update_cb_);
browser_context_.reset(new TestBrowserContext());
}
void TearDown() override {
content::MediaInternals::GetInstance()->RemoveUpdateCallback(update_cb_);
browser_context_.reset();
}
protected:
void ExpectValue(base::ListValue expected_list) {
base::DictionaryValue expected_data;
expected_data.SetKey("sessions", std::move(expected_list));
EXPECT_EQ(expected_data, update_data_);
}
std::unique_ptr<TestWebContents> CreateWebContents() {
return TestWebContents::Create(
browser_context_.get(), SiteInstance::Create(browser_context_.get()));
}
base::Value GetAddressAsValue(MediaSessionImpl* media_session) {
std::stringstream stream;
stream << media_session;
return base::Value(stream.str());
}
void RemoveAllPlayersForTest(MediaSessionImpl* session) {
session->RemoveAllPlayersForTest();
}
MediaInternals::UpdateCallback update_cb_;
private:
base::test::ScopedCommandLine scoped_command_line_;
std::unique_ptr<TestBrowserContext> browser_context_;
};
TEST_F(MediaInternalsAudioFocusTest, AudioFocusStateIsUpdated) {
// Create a test media session and request audio focus.
std::unique_ptr<TestWebContents> web_contents1 = CreateWebContents();
web_contents1->SetTitle(base::UTF8ToUTF16(kTestTitle1));
MediaSessionImpl* media_session1 = MediaSessionImpl::Get(web_contents1.get());
media_session1->RequestSystemAudioFocus(AudioFocusType::Gain);
base::RunLoop().RunUntilIdle();
// Check JSON is what we expect.
{
base::DictionaryValue expected_session;
expected_session.SetKey("name", GetAddressAsValue(media_session1));
expected_session.SetKey("owner", base::Value(kTestTitle1));
expected_session.SetKey("state", base::Value("Active"));
base::ListValue expected_list;
expected_list.GetList().push_back(std::move(expected_session));
ExpectValue(std::move(expected_list));
}
// Create another media session.
std::unique_ptr<TestWebContents> web_contents2 = CreateWebContents();
web_contents2->SetTitle(base::UTF8ToUTF16(kTestTitle2));
MediaSessionImpl* media_session2 = MediaSessionImpl::Get(web_contents2.get());
media_session2->RequestSystemAudioFocus(AudioFocusType::GainTransientMayDuck);
base::RunLoop().RunUntilIdle();
// Check JSON is what we expect.
{
base::DictionaryValue expected_session1;
expected_session1.SetKey("name", GetAddressAsValue(media_session2));
expected_session1.SetKey("owner", base::Value(kTestTitle2));
expected_session1.SetKey("state", base::Value("Active"));
base::DictionaryValue expected_session2;
expected_session2.SetKey("name", GetAddressAsValue(media_session1));
expected_session2.SetKey("owner", base::Value(kTestTitle1));
expected_session2.SetKey("state", base::Value("Active Ducked"));
base::ListValue expected_list;
expected_list.GetList().push_back(std::move(expected_session1));
expected_list.GetList().push_back(std::move(expected_session2));
ExpectValue(std::move(expected_list));
}
// Abandon audio focus.
RemoveAllPlayersForTest(media_session2);
base::RunLoop().RunUntilIdle();
// Check JSON is what we expect.
{
base::DictionaryValue expected_session;
expected_session.SetKey("name", GetAddressAsValue(media_session1));
expected_session.SetKey("owner", base::Value(kTestTitle1));
expected_session.SetKey("state", base::Value("Active"));
base::ListValue expected_list;
expected_list.GetList().push_back(std::move(expected_session));
ExpectValue(std::move(expected_list));
}
// Abandon audio focus.
RemoveAllPlayersForTest(media_session1);
base::RunLoop().RunUntilIdle();
// Check JSON is what we expect.
{
base::ListValue expected_list;
ExpectValue(std::move(expected_list));
}
}
#endif // !defined(OS_ANDROID)
} // namespace content
......@@ -35,6 +35,9 @@ class CONTENT_EXPORT AudioFocusManager {
friend struct base::DefaultSingletonTraits<AudioFocusManager>;
friend class AudioFocusManagerTest;
// Media internals UI needs access to internal state.
friend class MediaInternals;
AudioFocusManager();
~AudioFocusManager();
......
......@@ -7,6 +7,7 @@
#include <algorithm>
#include "base/numerics/ranges.h"
#include "base/strings/string_util.h"
#include "content/browser/media/session/audio_focus_delegate.h"
#include "content/browser/media/session/audio_focus_type.h"
#include "content/browser/media/session/media_session_controller.h"
......@@ -33,6 +34,12 @@ namespace {
const double kUnduckedVolumeMultiplier = 1.0;
const double kDefaultDuckingVolumeMultiplier = 0.2;
const char kDebugInfoOwnerSeparator[] = " - ";
const char kDebugInfoDucked[] = "Ducked";
const char kDebugInfoActive[] = "Active";
const char kDebugInfoInactive[] = "Inactive";
const char kDebugInfoStateSeparator[] = " ";
using MapRenderFrameHostToDepth = std::map<RenderFrameHost*, size_t>;
size_t ComputeFrameDepth(RenderFrameHost* rfh,
......@@ -73,6 +80,13 @@ MediaSessionUserAction MediaSessionActionToUserAction(
return MediaSessionUserAction::Count;
}
// If the string is not empty then push it to the back of a vector.
void MaybePushBackString(std::vector<std::string>& vector,
const std::string& str) {
if (!str.empty())
vector.push_back(str);
}
} // anonymous namespace
using MediaSessionSuspendedSource =
......@@ -585,6 +599,32 @@ bool MediaSessionImpl::RequestSystemAudioFocus(
return result;
}
const MediaSessionImpl::DebugInfo MediaSessionImpl::GetDebugInfo() {
MediaSessionImpl::DebugInfo debug_info;
// Convert the address of |this| to a string and use it as the name.
std::stringstream stream;
stream << this;
debug_info.name = stream.str();
// Add the title and the url to the owner.
std::vector<std::string> owner_parts;
MaybePushBackString(owner_parts,
base::UTF16ToUTF8(web_contents()->GetTitle()));
MaybePushBackString(owner_parts,
web_contents()->GetLastCommittedURL().spec());
debug_info.owner = base::JoinString(owner_parts, kDebugInfoOwnerSeparator);
// Add the ducking state to state.
std::vector<std::string> state_parts;
MaybePushBackString(state_parts,
IsActive() ? kDebugInfoActive : kDebugInfoInactive);
MaybePushBackString(state_parts, is_ducking_ ? kDebugInfoDucked : "");
debug_info.state = base::JoinString(state_parts, kDebugInfoStateSeparator);
return debug_info;
}
void MediaSessionImpl::AbandonSystemAudioFocusIfNeeded() {
if (audio_focus_state_ == State::INACTIVE || !normal_players_.empty() ||
!pepper_players_.empty() || !one_shot_players_.empty()) {
......
......@@ -205,6 +205,19 @@ class MediaSessionImpl : public MediaSession,
// Returns whether the request was granted.
CONTENT_EXPORT bool RequestSystemAudioFocus(AudioFocusType audio_focus_type);
// Returns debugging information to be displayed on chrome://media-internals.
struct DebugInfo {
// A unique name for the MediaSession.
std::string name;
// The title and URL of the owning WebContents.
std::string owner;
// State information stored in a string e.g. Ducked.
std::string state;
};
const DebugInfo GetDebugInfo();
private:
friend class content::WebContentsUserData<MediaSessionImpl>;
friend class ::MediaSessionImplBrowserTest;
......@@ -213,6 +226,7 @@ class MediaSessionImpl : public MediaSession,
friend class content::MediaSessionImplServiceRoutingTest;
friend class content::MediaSessionImplStateObserver;
friend class content::MediaSessionServiceImplBrowserTest;
friend class MediaInternalsAudioFocusTest;
CONTENT_EXPORT void SetDelegateForTests(
std::unique_ptr<AudioFocusDelegate> delegate);
......
......@@ -16,6 +16,8 @@ var ClientRenderer = (function() {
this.logTable = logElement.querySelector('tbody');
this.graphElement = document.getElementById('graphs');
this.audioPropertyName = document.getElementById('audio-property-name');
this.audioFocusSessionListElement_ =
document.getElementById('audio-focus-session-list');
this.players = null;
this.selectedPlayer = null;
......@@ -138,6 +140,19 @@ var ClientRenderer = (function() {
}
},
/**
* Called when the list of audio focus sessions has changed.
* @param sessions A list of media sessions that contain the current state.
*/
audioFocusSessionUpdated: function(sessions) {
removeChildren(this.audioFocusSessionListElement_);
sessions.forEach(session => {
this.audioFocusSessionListElement_.appendChild(
this.createAudioFocusSessionRow_(session));
});
},
/**
* Called when an audio component is removed from the collection.
* @param componentType Integer AudioComponent enum value; must match values
......@@ -528,6 +543,15 @@ var ClientRenderer = (function() {
this.drawLog_();
}
},
createAudioFocusSessionRow_: function(session) {
const template = $('audio-focus-session-row');
const span = template.content.querySelectorAll('span');
span[0].textContent = session.name;
span[1].textContent = session.owner;
span[2].textContent = session.state;
return document.importNode(template.content, true);
},
};
return ClientRenderer;
......
......@@ -27,6 +27,13 @@ var media = (function() {
manager.updateVideoCaptureCapabilities(videoCaptureCapabilities);
};
media.onReceiveAudioFocusState = function(audioFocusState) {
if (!audioFocusState)
return;
manager.updateAudioFocusSessions(audioFocusState.sessions);
};
media.updateAudioComponent = function(component) {
var uniqueComponentId = component.owner_id + ':' + component.component_id;
switch (component.status) {
......
......@@ -40,6 +40,14 @@ var Manager = (function() {
}
Manager.prototype = {
/**
* Updates the audio focus state.
* @param sessions A list of media sessions that contain the current state.
*/
updateAudioFocusSessions: function(sessions) {
this.clientRenderer_.audioFocusSessionUpdated(sessions);
},
/**
* Updates an audio-component.
* @param componentType Integer AudioComponent enum value; must match values
......
......@@ -178,11 +178,9 @@ h3 {
color: rgba(0, 0, 0, .5);
}
label.audio-focus-session,
label.selectable-button {
-webkit-user-select: none;
display: inline-block;
background: #BDF;
cursor: pointer;
border: solid 1px #999;
border-radius: 3px;
padding: 6px;
......@@ -191,6 +189,12 @@ label.selectable-button {
word-break: break-all;
}
label.selectable-button {
background: #BDF;
cursor: pointer;
user-select: none;
}
input.selectable-button {
display: none;
}
......@@ -205,6 +209,7 @@ input.selectable-button:hover + label.selectable-button {
border-color: #666;
}
label.audio-focus-session,
label.destructed-player {
background-color: #EEE;
}
......@@ -226,3 +231,6 @@ label.destructed-player {
display: none;
}
#audio-focus-session-list {
list-style: none;
}
......@@ -25,6 +25,7 @@ found in the LICENSE file.
<tab>Players</tab>
<tab>Audio</tab>
<tab>Video Capture</tab>
<tab>Audio Focus</tab>
</tabs>
<tabpanels>
<tabpanel id="players">
......@@ -120,6 +121,21 @@ found in the LICENSE file.
</table>
</div>
</tabpanel>
<tabpanel id="players">
<div id="list-wrapper">
<h2>Active Sessions</h2>
<ul id="audio-focus-session-list" class="show-none-if-empty"></ul>
</div>
<template id="audio-focus-session-row">
<li>
<label class="audio-focus-session">
<span class="player-name"></span><br />
<span class="player-frame"></span><br />
<span class="player-desc"></span>
</label>
</li>
</template>
</tabpanel>
</tabpanels>
</tabbox>
<dialog id="clipboard-dialog">
......
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