Commit ddc545cb authored by zqzhang's avatar zqzhang Committed by Commit bot

Allow MediaSession in iframes to be routed

This CL allows MediaSession in iframes to be routed, as per spec change:
https://github.com/WICG/mediasession/pull/149

To achieve this goal, class MediaSessionServiceRouter is added for selecting
which MediaSession object to route.

The current MediaSessionService routing strategy is:

* If the top-level frame uses MediaSession API, always select the
  top-level session.
* If the top-level frame has no MediaSession, select one of an
  audio-producing frame, and route its session (or null if the frame
  does not use MediaSession API).

CL explanation:
https://docs.google.com/a/google.com/document/d/1Ht6DxjOcfBctfRT3_wOkwGoNUaJY-Q8K18MV3xdAh_8/edit?usp=sharing

BUG=670319

Review-Url: https://codereview.chromium.org/2526533002
Cr-Commit-Position: refs/heads/master@{#436266}
parent 8e40d35a
...@@ -30,7 +30,6 @@ import org.chromium.ui.base.WindowAndroid; ...@@ -30,7 +30,6 @@ import org.chromium.ui.base.WindowAndroid;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
import javax.annotation.Nullable; import javax.annotation.Nullable;
...@@ -61,7 +60,7 @@ public class MediaSessionTabHelper implements MediaImageCallback { ...@@ -61,7 +60,7 @@ public class MediaSessionTabHelper implements MediaImageCallback {
// The currently showing metadata. // The currently showing metadata.
private MediaMetadata mCurrentMetadata = null; private MediaMetadata mCurrentMetadata = null;
private MediaImageManager mMediaImageManager = null; private MediaImageManager mMediaImageManager = null;
private Set<Integer> mMediaSessionActions = new HashSet<Integer>(); private Set<Integer> mMediaSessionActions = null;
private Handler mHandler; private Handler mHandler;
// The delayed task to hide notification. Hiding notification can be immediate or delayed. // The delayed task to hide notification. Hiding notification can be immediate or delayed.
// Delayed hiding will schedule this delayed task to |mHandler|. The task will be canceled when // Delayed hiding will schedule this delayed task to |mHandler|. The task will be canceled when
...@@ -228,16 +227,8 @@ public class MediaSessionTabHelper implements MediaImageCallback { ...@@ -228,16 +227,8 @@ public class MediaSessionTabHelper implements MediaImageCallback {
} }
@Override @Override
public void mediaSessionEnabledAction(int action) { public void mediaSessionActionsChanged(Set<Integer> actions) {
if (!MediaSessionAction.isKnownValue(action)) return; mMediaSessionActions = actions;
mMediaSessionActions.add(action);
updateNotificationActions();
}
@Override
public void mediaSessionDisabledAction(int action) {
if (!MediaSessionAction.isKnownValue(action)) return;
mMediaSessionActions.remove(action);
updateNotificationActions(); updateNotificationActions();
} }
}; };
...@@ -261,7 +252,7 @@ public class MediaSessionTabHelper implements MediaImageCallback { ...@@ -261,7 +252,7 @@ public class MediaSessionTabHelper implements MediaImageCallback {
if (mMediaSessionObserver == null) return; if (mMediaSessionObserver == null) return;
mMediaSessionObserver.stopObserving(); mMediaSessionObserver.stopObserving();
mMediaSessionObserver = null; mMediaSessionObserver = null;
mMediaSessionActions.clear(); mMediaSessionActions = null;
} }
private final TabObserver mTabObserver = new EmptyTabObserver() { private final TabObserver mTabObserver = new EmptyTabObserver() {
......
...@@ -24,6 +24,7 @@ class MockMediaSessionPlayerObserver : public MediaSessionPlayerObserver { ...@@ -24,6 +24,7 @@ class MockMediaSessionPlayerObserver : public MediaSessionPlayerObserver {
void OnResume(int player_id) override {} void OnResume(int player_id) override {}
void OnSetVolumeMultiplier( void OnSetVolumeMultiplier(
int player_id, double volume_multiplier) override {} int player_id, double volume_multiplier) override {}
RenderFrameHost* GetRenderFrameHost() const override { return nullptr; }
}; };
} // anonymous namespace } // anonymous namespace
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
#include "content/browser/media/session/media_session_android.h" #include "content/browser/media/session/media_session_android.h"
#include <algorithm> #include "base/android/jni_array.h"
#include "content/browser/media/session/media_session_impl.h" #include "content/browser/media/session/media_session_impl.h"
#include "content/browser/web_contents/web_contents_android.h" #include "content/browser/web_contents/web_contents_android.h"
#include "content/browser/web_contents/web_contents_impl.h" #include "content/browser/web_contents/web_contents_impl.h"
...@@ -103,26 +103,19 @@ void MediaSessionAndroid::MediaSessionMetadataChanged( ...@@ -103,26 +103,19 @@ void MediaSessionAndroid::MediaSessionMetadataChanged(
j_metadata); j_metadata);
} }
void MediaSessionAndroid::MediaSessionEnabledAction( void MediaSessionAndroid::MediaSessionActionsChanged(
blink::mojom::MediaSessionAction action) { const std::set<blink::mojom::MediaSessionAction>& actions) {
ScopedJavaLocalRef<jobject> j_local_session = GetJavaObject(); ScopedJavaLocalRef<jobject> j_local_session = GetJavaObject();
if (j_local_session.is_null()) if (j_local_session.is_null())
return; return;
JNIEnv* env = base::android::AttachCurrentThread(); std::vector<int> actions_vec;
Java_MediaSessionImpl_mediaSessionEnabledAction(env, j_local_session, for (auto action : actions)
static_cast<int>(action)); actions_vec.push_back(static_cast<int>(action));
}
void MediaSessionAndroid::MediaSessionDisabledAction(
blink::mojom::MediaSessionAction action) {
ScopedJavaLocalRef<jobject> j_local_session = GetJavaObject();
if (j_local_session.is_null())
return;
JNIEnv* env = base::android::AttachCurrentThread(); JNIEnv* env = base::android::AttachCurrentThread();
Java_MediaSessionImpl_mediaSessionDisabledAction(env, j_local_session, Java_MediaSessionImpl_mediaSessionActionsChanged(
static_cast<int>(action)); env, j_local_session, base::android::ToJavaIntArray(env, actions_vec));
} }
void MediaSessionAndroid::Resume( void MediaSessionAndroid::Resume(
......
...@@ -39,10 +39,8 @@ class MediaSessionAndroid final : public MediaSessionObserver { ...@@ -39,10 +39,8 @@ class MediaSessionAndroid final : public MediaSessionObserver {
bool is_suspended) override; bool is_suspended) override;
void MediaSessionMetadataChanged( void MediaSessionMetadataChanged(
const base::Optional<MediaMetadata>& metadata) override; const base::Optional<MediaMetadata>& metadata) override;
void MediaSessionEnabledAction( void MediaSessionActionsChanged(
blink::mojom::MediaSessionAction action) override; const std::set<blink::mojom::MediaSessionAction>& actions) override;
void MediaSessionDisabledAction(
blink::mojom::MediaSessionAction action) override;
// MediaSession method wrappers. // MediaSession method wrappers.
void Resume(JNIEnv* env, const base::android::JavaParamRef<jobject>& j_obj); void Resume(JNIEnv* env, const base::android::JavaParamRef<jobject>& j_obj);
......
...@@ -95,6 +95,10 @@ void MediaSessionController::OnSetVolumeMultiplier(int player_id, ...@@ -95,6 +95,10 @@ void MediaSessionController::OnSetVolumeMultiplier(int player_id,
id_.first->GetRoutingID(), id_.second, volume_multiplier)); id_.first->GetRoutingID(), id_.second, volume_multiplier));
} }
RenderFrameHost* MediaSessionController::GetRenderFrameHost() const {
return id_.first;
}
void MediaSessionController::OnPlaybackPaused() { void MediaSessionController::OnPlaybackPaused() {
// We check for suspension here since the renderer may issue its own pause // We check for suspension here since the renderer may issue its own pause
// in response to or while a pause from the browser is in flight. // in response to or while a pause from the browser is in flight.
......
...@@ -50,6 +50,7 @@ class CONTENT_EXPORT MediaSessionController ...@@ -50,6 +50,7 @@ class CONTENT_EXPORT MediaSessionController
void OnSuspend(int player_id) override; void OnSuspend(int player_id) override;
void OnResume(int player_id) override; void OnResume(int player_id) override;
void OnSetVolumeMultiplier(int player_id, double volume_multiplier) override; void OnSetVolumeMultiplier(int player_id, double volume_multiplier) override;
RenderFrameHost* GetRenderFrameHost() const override;
// Test helpers. // Test helpers.
int get_player_id_for_testing() const { return player_id_; } int get_player_id_for_testing() const { return player_id_; }
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
#include <algorithm> #include <algorithm>
#include "content/browser/media/session/audio_focus_delegate.h" #include "content/browser/media/session/audio_focus_delegate.h"
#include "content/browser/media/session/media_session_controller.h"
#include "content/browser/media/session/media_session_player_observer.h" #include "content/browser/media/session/media_session_player_observer.h"
#include "content/browser/media/session/media_session_service_impl.h" #include "content/browser/media/session/media_session_service_impl.h"
#include "content/browser/web_contents/web_contents_impl.h" #include "content/browser/web_contents/web_contents_impl.h"
...@@ -25,6 +26,26 @@ namespace { ...@@ -25,6 +26,26 @@ namespace {
const double kDefaultVolumeMultiplier = 1.0; const double kDefaultVolumeMultiplier = 1.0;
const double kDuckingVolumeMultiplier = 0.2; const double kDuckingVolumeMultiplier = 0.2;
using MapRenderFrameHostToDepth = std::map<RenderFrameHost*, size_t>;
size_t ComputeFrameDepth(RenderFrameHost* rfh,
MapRenderFrameHostToDepth* map_rfh_to_depth) {
DCHECK(rfh);
size_t depth = 0;
RenderFrameHost* current_frame = rfh;
while (current_frame) {
auto it = map_rfh_to_depth->find(current_frame);
if (it != map_rfh_to_depth->end()) {
depth += it->second;
break;
}
++depth;
current_frame = current_frame->GetParent();
}
(*map_rfh_to_depth)[rfh] = depth;
return depth;
}
} // anonymous namespace } // anonymous namespace
using MediaSessionSuspendedSource = using MediaSessionSuspendedSource =
...@@ -89,11 +110,6 @@ void MediaSessionImpl::WebContentsDestroyed() { ...@@ -89,11 +110,6 @@ void MediaSessionImpl::WebContentsDestroyed() {
AbandonSystemAudioFocusIfNeeded(); AbandonSystemAudioFocusIfNeeded();
} }
void MediaSessionImpl::SetMediaSessionService(
MediaSessionServiceImpl* service) {
service_ = service;
}
void MediaSessionImpl::AddObserver(MediaSessionObserver* observer) { void MediaSessionImpl::AddObserver(MediaSessionObserver* observer) {
observers_.AddObserver(observer); observers_.AddObserver(observer);
} }
...@@ -102,13 +118,18 @@ void MediaSessionImpl::RemoveObserver(MediaSessionObserver* observer) { ...@@ -102,13 +118,18 @@ void MediaSessionImpl::RemoveObserver(MediaSessionObserver* observer) {
observers_.RemoveObserver(observer); observers_.RemoveObserver(observer);
} }
void MediaSessionImpl::SetMetadata( void MediaSessionImpl::NotifyMediaSessionMetadataChange(
const base::Optional<MediaMetadata>& metadata) { const base::Optional<MediaMetadata>& metadata) {
metadata_ = metadata;
for (auto& observer : observers_) for (auto& observer : observers_)
observer.MediaSessionMetadataChanged(metadata); observer.MediaSessionMetadataChanged(metadata);
} }
void MediaSessionImpl::NotifyMediaSessionActionsChange(
const std::set<blink::mojom::MediaSessionAction>& actions) {
for (auto& observer : observers_)
observer.MediaSessionActionsChanged(actions);
}
bool MediaSessionImpl::AddPlayer(MediaSessionPlayerObserver* observer, bool MediaSessionImpl::AddPlayer(MediaSessionPlayerObserver* observer,
int player_id, int player_id,
media::MediaContentType media_content_type) { media::MediaContentType media_content_type) {
...@@ -150,8 +171,9 @@ bool MediaSessionImpl::AddPlayer(MediaSessionPlayerObserver* observer, ...@@ -150,8 +171,9 @@ bool MediaSessionImpl::AddPlayer(MediaSessionPlayerObserver* observer,
normal_players_.clear(); normal_players_.clear();
normal_players_.insert(PlayerIdentifier(observer, player_id)); normal_players_.insert(PlayerIdentifier(observer, player_id));
NotifyAboutStateChange();
UpdateRoutedService();
NotifyAboutStateChange();
return true; return true;
} }
...@@ -174,6 +196,7 @@ void MediaSessionImpl::RemovePlayer(MediaSessionPlayerObserver* observer, ...@@ -174,6 +196,7 @@ void MediaSessionImpl::RemovePlayer(MediaSessionPlayerObserver* observer,
one_shot_players_.erase(it); one_shot_players_.erase(it);
AbandonSystemAudioFocusIfNeeded(); AbandonSystemAudioFocusIfNeeded();
UpdateRoutedService();
// The session may become controllable after removing a one-shot player. // The session may become controllable after removing a one-shot player.
// However AbandonSystemAudioFocusIfNeeded will short-return and won't notify // However AbandonSystemAudioFocusIfNeeded will short-return and won't notify
...@@ -207,6 +230,7 @@ void MediaSessionImpl::RemovePlayers(MediaSessionPlayerObserver* observer) { ...@@ -207,6 +230,7 @@ void MediaSessionImpl::RemovePlayers(MediaSessionPlayerObserver* observer) {
} }
AbandonSystemAudioFocusIfNeeded(); AbandonSystemAudioFocusIfNeeded();
UpdateRoutedService();
// The session may become controllable after removing a one-shot player. // The session may become controllable after removing a one-shot player.
// However AbandonSystemAudioFocusIfNeeded will short-return and won't notify // However AbandonSystemAudioFocusIfNeeded will short-return and won't notify
...@@ -299,24 +323,6 @@ void MediaSessionImpl::Stop(SuspendType suspend_type) { ...@@ -299,24 +323,6 @@ void MediaSessionImpl::Stop(SuspendType suspend_type) {
AbandonSystemAudioFocusIfNeeded(); AbandonSystemAudioFocusIfNeeded();
} }
void MediaSessionImpl::DidReceiveAction(
blink::mojom::MediaSessionAction action) {
if (service_)
service_->GetClient()->DidReceiveAction(action);
}
void MediaSessionImpl::OnMediaSessionEnabledAction(
blink::mojom::MediaSessionAction action) {
for (auto& observer : observers_)
observer.MediaSessionEnabledAction(action);
}
void MediaSessionImpl::OnMediaSessionDisabledAction(
blink::mojom::MediaSessionAction action) {
for (auto& observer : observers_)
observer.MediaSessionDisabledAction(action);
}
void MediaSessionImpl::StartDucking() { void MediaSessionImpl::StartDucking() {
if (is_ducking_) if (is_ducking_)
return; return;
...@@ -470,7 +476,7 @@ MediaSessionImpl::MediaSessionImpl(WebContents* web_contents) ...@@ -470,7 +476,7 @@ MediaSessionImpl::MediaSessionImpl(WebContents* web_contents)
audio_focus_type_( audio_focus_type_(
AudioFocusManager::AudioFocusType::GainTransientMayDuck), AudioFocusManager::AudioFocusType::GainTransientMayDuck),
is_ducking_(false), is_ducking_(false),
service_(nullptr) { routed_service_(nullptr) {
#if defined(OS_ANDROID) #if defined(OS_ANDROID)
session_android_.reset(new MediaSessionAndroid(this)); session_android_.reset(new MediaSessionAndroid(this));
#endif // defined(OS_ANDROID) #endif // defined(OS_ANDROID)
...@@ -552,4 +558,103 @@ bool MediaSessionImpl::AddOneShotPlayer(MediaSessionPlayerObserver* observer, ...@@ -552,4 +558,103 @@ bool MediaSessionImpl::AddOneShotPlayer(MediaSessionPlayerObserver* observer,
return true; return true;
} }
// MediaSessionService-related methods
void MediaSessionImpl::OnServiceCreated(MediaSessionServiceImpl* service) {
services_[service->GetRenderFrameHost()] = service;
}
void MediaSessionImpl::OnServiceDestroyed(MediaSessionServiceImpl* service) {
services_.erase(service->GetRenderFrameHost());
}
void MediaSessionImpl::OnMediaSessionMetadataChanged(
MediaSessionServiceImpl* service) {
if (service != routed_service_)
return;
NotifyMediaSessionMetadataChange(routed_service_->metadata());
}
void MediaSessionImpl::OnMediaSessionActionsChanged(
MediaSessionServiceImpl* service) {
if (service != routed_service_)
return;
NotifyMediaSessionActionsChange(routed_service_->actions());
}
void MediaSessionImpl::DidReceiveAction(
blink::mojom::MediaSessionAction action) {
if (!routed_service_)
return;
routed_service_->GetClient()->DidReceiveAction(action);
}
bool MediaSessionImpl::IsServiceActiveForRenderFrameHost(RenderFrameHost* rfh) {
if (!services_.count(rfh))
return false;
return services_[rfh]->metadata().has_value() ||
!services_[rfh]->actions().empty();
}
void MediaSessionImpl::UpdateRoutedService() {
MediaSessionServiceImpl* new_service = ComputeServiceForRouting();
if (new_service == routed_service_)
return;
routed_service_ = new_service;
if (routed_service_) {
NotifyMediaSessionMetadataChange(routed_service_->metadata());
NotifyMediaSessionActionsChange(routed_service_->actions());
} else {
NotifyMediaSessionMetadataChange(base::nullopt);
NotifyMediaSessionActionsChange(
std::set<blink::mojom::MediaSessionAction>());
}
}
MediaSessionServiceImpl* MediaSessionImpl::ComputeServiceForRouting() {
// The service selection strategy is: select a frame that has a playing/paused
// player and has a corresponding MediaSessionService and return the
// corresponding MediaSessionService. If multiple frames satisfy the criteria,
// prefer the top-most frame.
std::set<RenderFrameHost*> frames;
for (const auto& player : normal_players_) {
RenderFrameHost* frame = player.observer->GetRenderFrameHost();
if (frame)
frames.insert(frame);
}
for (const auto& player : one_shot_players_) {
RenderFrameHost* frame = player.observer->GetRenderFrameHost();
if (frame)
frames.insert(frame);
}
for (const auto& player : pepper_players_) {
RenderFrameHost* frame = player.observer->GetRenderFrameHost();
if (frame)
frames.insert(frame);
}
RenderFrameHost* best_frame = nullptr;
size_t min_depth = std::numeric_limits<size_t>::max();
std::map<RenderFrameHost*, size_t> map_rfh_to_depth;
for (RenderFrameHost* frame : frames) {
size_t depth = ComputeFrameDepth(frame, &map_rfh_to_depth);
if (depth >= min_depth)
continue;
if (!IsServiceActiveForRenderFrameHost(frame))
continue;
best_frame = frame;
min_depth = depth;
}
return best_frame ? services_[best_frame] : nullptr;
}
} // namespace content } // namespace content
...@@ -7,6 +7,9 @@ ...@@ -7,6 +7,9 @@
#include <stddef.h> #include <stddef.h>
#include <map>
#include <set>
#include "base/callback_list.h" #include "base/callback_list.h"
#include "base/id_map.h" #include "base/id_map.h"
#include "base/macros.h" #include "base/macros.h"
...@@ -35,6 +38,7 @@ namespace content { ...@@ -35,6 +38,7 @@ namespace content {
class AudioFocusDelegate; class AudioFocusDelegate;
class AudioFocusManagerTest; class AudioFocusManagerTest;
class MediaSessionImplServiceRoutingTest;
class MediaSessionImplStateObserver; class MediaSessionImplStateObserver;
class MediaSessionImplVisibilityBrowserTest; class MediaSessionImplVisibilityBrowserTest;
class MediaSessionObserver; class MediaSessionObserver;
...@@ -84,8 +88,10 @@ class MediaSessionImpl : public MediaSession, ...@@ -84,8 +88,10 @@ class MediaSessionImpl : public MediaSession,
void AddObserver(MediaSessionObserver* observer); void AddObserver(MediaSessionObserver* observer);
void RemoveObserver(MediaSessionObserver* observer); void RemoveObserver(MediaSessionObserver* observer);
void SetMetadata(const base::Optional<MediaMetadata>& metadata); void NotifyMediaSessionMetadataChange(
const base::Optional<MediaMetadata>& metadata() const { return metadata_; } const base::Optional<MediaMetadata>& metadata);
void NotifyMediaSessionActionsChange(
const std::set<blink::mojom::MediaSessionAction>& actions);
// Adds the given player to the current media session. Returns whether the // Adds the given player to the current media session. Returns whether the
// player was successfully added. If it returns false, AddPlayer() should be // player was successfully added. If it returns false, AddPlayer() should be
...@@ -124,17 +130,6 @@ class MediaSessionImpl : public MediaSession, ...@@ -124,17 +130,6 @@ class MediaSessionImpl : public MediaSession,
// |type| represents the origin of the request. // |type| represents the origin of the request.
CONTENT_EXPORT void Stop(MediaSession::SuspendType suspend_type) override; CONTENT_EXPORT void Stop(MediaSession::SuspendType suspend_type) override;
// Received a media session action and forward to blink::MediaSession.
void DidReceiveAction(blink::mojom::MediaSessionAction action) override;
// Called when an action is enabled in blink::MediaSession. This method will
// notify the observers that the action is enabled.
void OnMediaSessionEnabledAction(blink::mojom::MediaSessionAction action);
// Called when an action is disabled in blink::MediaSession. This method will
// notify the observers that the action is disabled.
void OnMediaSessionDisabledAction(blink::mojom::MediaSessionAction action);
// Let the media session start ducking such that the volume multiplier is // Let the media session start ducking such that the volume multiplier is
// reduced. // reduced.
CONTENT_EXPORT void StartDucking(); CONTENT_EXPORT void StartDucking();
...@@ -172,16 +167,32 @@ class MediaSessionImpl : public MediaSession, ...@@ -172,16 +167,32 @@ class MediaSessionImpl : public MediaSession,
// WebContentsObserver implementation // WebContentsObserver implementation
void WebContentsDestroyed() override; void WebContentsDestroyed() override;
// Sets the associated MediaSessionService for communicating with // MediaSessionService-related methods
// blink::MediaSession.
MediaSessionServiceImpl* GetMediaSessionService() { return service_; } // Called when a MediaSessionService is created, which registers itself to
void SetMediaSessionService(MediaSessionServiceImpl* service); // this session.
void OnServiceCreated(MediaSessionServiceImpl* service);
// Called when a MediaSessionService is destroyed, which unregisters itself
// from this session.
void OnServiceDestroyed(MediaSessionServiceImpl* service);
// Called when the metadata of a MediaSessionService has changed. Will notify
// observers if the service is currently routed.
void OnMediaSessionMetadataChanged(MediaSessionServiceImpl* service);
// Called when the actions of a MediaSessionService has changed. Will notify
// observers if the service is currently routed.
void OnMediaSessionActionsChanged(MediaSessionServiceImpl* service);
// Called when a MediaSessionAction is received. The action will be forwarded
// to blink::MediaSession corresponding to the current routed service.
void DidReceiveAction(blink::mojom::MediaSessionAction action) override;
private: private:
friend class content::WebContentsUserData<MediaSessionImpl>; friend class content::WebContentsUserData<MediaSessionImpl>;
friend class ::MediaSessionImplBrowserTest; friend class ::MediaSessionImplBrowserTest;
friend class content::MediaSessionImplVisibilityBrowserTest; friend class content::MediaSessionImplVisibilityBrowserTest;
friend class content::AudioFocusManagerTest; friend class content::AudioFocusManagerTest;
friend class content::MediaSessionImplServiceRoutingTest;
friend class content::MediaSessionImplStateObserver; friend class content::MediaSessionImplStateObserver;
CONTENT_EXPORT void SetDelegateForTests( CONTENT_EXPORT void SetDelegateForTests(
...@@ -251,6 +262,18 @@ class MediaSessionImpl : public MediaSession, ...@@ -251,6 +262,18 @@ class MediaSessionImpl : public MediaSession,
CONTENT_EXPORT bool AddOneShotPlayer(MediaSessionPlayerObserver* observer, CONTENT_EXPORT bool AddOneShotPlayer(MediaSessionPlayerObserver* observer,
int player_id); int player_id);
// MediaSessionService-related methods
// Called when the routed service may have changed.
void UpdateRoutedService();
// Returns whether the frame |rfh| uses MediaSession API.
bool IsServiceActiveForRenderFrameHost(RenderFrameHost* rfh);
// Compute the MediaSessionService that should be routed, which will be used
// to update |routed_service_|.
CONTENT_EXPORT MediaSessionServiceImpl* ComputeServiceForRouting();
std::unique_ptr<AudioFocusDelegate> delegate_; std::unique_ptr<AudioFocusDelegate> delegate_;
PlayersMap normal_players_; PlayersMap normal_players_;
PlayersMap pepper_players_; PlayersMap pepper_players_;
...@@ -267,7 +290,6 @@ class MediaSessionImpl : public MediaSession, ...@@ -267,7 +290,6 @@ class MediaSessionImpl : public MediaSession,
// StopDucking(). // StopDucking().
bool is_ducking_; bool is_ducking_;
base::Optional<MediaMetadata> metadata_;
base::CallbackList<void(State)> media_session_state_listeners_; base::CallbackList<void(State)> media_session_state_listeners_;
base::ObserverList<MediaSessionObserver> observers_; base::ObserverList<MediaSessionObserver> observers_;
...@@ -276,9 +298,15 @@ class MediaSessionImpl : public MediaSession, ...@@ -276,9 +298,15 @@ class MediaSessionImpl : public MediaSession,
std::unique_ptr<MediaSessionAndroid> session_android_; std::unique_ptr<MediaSessionAndroid> session_android_;
#endif // defined(OS_ANDROID) #endif // defined(OS_ANDROID)
// The MediaSessionService this session is associated with (the service of the // MediaSessionService-related fields
// top-level frame). using ServicesMap = std::map<RenderFrameHost*, MediaSessionServiceImpl*>;
MediaSessionServiceImpl* service_;
// The collection of all managed services (non-owned pointers). The services
// are owned by RenderFrameHost and should be registered on creation and
// unregistered on destroy.
ServicesMap services_;
// The currently routed service (non-owned pointer).
MediaSessionServiceImpl* routed_service_;
DISALLOW_COPY_AND_ASSIGN(MediaSessionImpl); DISALLOW_COPY_AND_ASSIGN(MediaSessionImpl);
}; };
......
// Copyright 2016 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 "content/browser/media/session/media_session_impl.h"
#include <map>
#include <memory>
#include "content/browser/media/session/media_session_player_observer.h"
#include "content/browser/media/session/media_session_service_impl.h"
#include "content/test/test_render_view_host.h"
#include "content/test/test_web_contents.h"
#include "media/base/media_content_type.h"
namespace content {
namespace {
static const int kPlayerId = 0;
class MockMediaSessionServiceImpl : public MediaSessionServiceImpl {
public:
explicit MockMediaSessionServiceImpl(RenderFrameHost* rfh)
: MediaSessionServiceImpl(rfh) {}
~MockMediaSessionServiceImpl() override = default;
};
class MockMediaSessionPlayerObserver : public MediaSessionPlayerObserver {
public:
explicit MockMediaSessionPlayerObserver(RenderFrameHost* rfh)
: render_frame_host_(rfh) {}
~MockMediaSessionPlayerObserver() override = default;
void OnSuspend(int player_id) override {}
void OnResume(int player_id) override {}
void OnSetVolumeMultiplier(int player_id, double volume_multiplier) override {
}
RenderFrameHost* GetRenderFrameHost() const override {
return render_frame_host_;
}
private:
RenderFrameHost* render_frame_host_;
};
} // anonymous namespace
class MediaSessionImplServiceRoutingTest
: public RenderViewHostImplTestHarness {
public:
MediaSessionImplServiceRoutingTest() = default;
~MediaSessionImplServiceRoutingTest() override = default;
void SetUp() override {
RenderViewHostImplTestHarness::SetUp();
contents()->GetMainFrame()->InitializeRenderFrameIfNeeded();
main_frame_ = contents()->GetMainFrame();
sub_frame_ = main_frame_->AppendChild("sub_frame");
player_in_main_frame_.reset(
new MockMediaSessionPlayerObserver(main_frame_));
player_in_sub_frame_.reset(new MockMediaSessionPlayerObserver(sub_frame_));
}
void TearDown() override {
services_.clear();
RenderViewHostImplTestHarness::TearDown();
}
protected:
void CreateServiceForFrame(TestRenderFrameHost* frame) {
services_[frame] = base::MakeUnique<MockMediaSessionServiceImpl>(frame);
services_[frame]->SetMetadata(MediaMetadata());
}
void StartPlayerForFrame(TestRenderFrameHost* frame) {
players_[frame] = base::MakeUnique<MockMediaSessionPlayerObserver>(frame);
MediaSessionImpl::Get(contents())
->AddPlayer(players_[frame].get(), kPlayerId,
media::MediaContentType::Persistent);
}
void ClearPlayersForFrame(TestRenderFrameHost* frame) {
if (!players_.count(frame))
return;
MediaSessionImpl::Get(contents())
->RemovePlayer(players_[frame].get(), kPlayerId);
}
MediaSessionServiceImpl* ComputeServiceForRouting() {
return MediaSessionImpl::Get(contents())->ComputeServiceForRouting();
}
TestRenderFrameHost* main_frame_;
TestRenderFrameHost* sub_frame_;
std::unique_ptr<MockMediaSessionPlayerObserver> player_in_main_frame_;
std::unique_ptr<MockMediaSessionPlayerObserver> player_in_sub_frame_;
using ServiceMap = std::map<TestRenderFrameHost*,
std::unique_ptr<MockMediaSessionServiceImpl>>;
ServiceMap services_;
using PlayerMap = std::map<TestRenderFrameHost*,
std::unique_ptr<MockMediaSessionPlayerObserver>>;
PlayerMap players_;
};
TEST_F(MediaSessionImplServiceRoutingTest, NoFrameProducesAudio) {
CreateServiceForFrame(main_frame_);
CreateServiceForFrame(sub_frame_);
ASSERT_EQ(nullptr, ComputeServiceForRouting());
}
TEST_F(MediaSessionImplServiceRoutingTest,
OnlyMainFrameProducesAudioButHasNoService) {
StartPlayerForFrame(main_frame_);
ASSERT_EQ(nullptr, ComputeServiceForRouting());
}
TEST_F(MediaSessionImplServiceRoutingTest,
OnlySubFrameProducesAudioButHasNoService) {
StartPlayerForFrame(sub_frame_);
ASSERT_EQ(nullptr, ComputeServiceForRouting());
}
TEST_F(MediaSessionImplServiceRoutingTest,
OnlyMainFrameProducesAudioButHasInactiveService) {
StartPlayerForFrame(main_frame_);
CreateServiceForFrame(main_frame_);
services_[main_frame_]->SetMetadata(base::nullopt);
ASSERT_EQ(nullptr, ComputeServiceForRouting());
}
TEST_F(MediaSessionImplServiceRoutingTest,
OnlySubFrameProducesAudioButHasInactiveService) {
StartPlayerForFrame(sub_frame_);
CreateServiceForFrame(sub_frame_);
services_[sub_frame_]->SetMetadata(base::nullopt);
ASSERT_EQ(nullptr, ComputeServiceForRouting());
}
TEST_F(MediaSessionImplServiceRoutingTest,
BothFrameProducesAudioButOnlySubFrameHasService) {
StartPlayerForFrame(main_frame_);
StartPlayerForFrame(sub_frame_);
CreateServiceForFrame(sub_frame_);
ASSERT_EQ(services_[sub_frame_].get(), ComputeServiceForRouting());
}
TEST_F(MediaSessionImplServiceRoutingTest, PreferTopMostFrame) {
StartPlayerForFrame(main_frame_);
StartPlayerForFrame(sub_frame_);
CreateServiceForFrame(main_frame_);
CreateServiceForFrame(sub_frame_);
ASSERT_EQ(services_[main_frame_].get(), ComputeServiceForRouting());
}
TEST_F(MediaSessionImplServiceRoutingTest,
RoutedServiceUpdatedAfterRemovingPlayer) {
StartPlayerForFrame(main_frame_);
StartPlayerForFrame(sub_frame_);
CreateServiceForFrame(main_frame_);
CreateServiceForFrame(sub_frame_);
ClearPlayersForFrame(main_frame_);
ASSERT_EQ(services_[sub_frame_].get(), ComputeServiceForRouting());
}
} // namespace content
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
namespace content { namespace content {
class RenderFrameHost;
class MediaSessionPlayerObserver { class MediaSessionPlayerObserver {
public: public:
MediaSessionPlayerObserver() = default; MediaSessionPlayerObserver() = default;
...@@ -22,6 +24,10 @@ class MediaSessionPlayerObserver { ...@@ -22,6 +24,10 @@ class MediaSessionPlayerObserver {
// the MediaSession. // the MediaSession.
virtual void OnSetVolumeMultiplier(int player_id, virtual void OnSetVolumeMultiplier(int player_id,
double volume_multiplier) = 0; double volume_multiplier) = 0;
// Returns the RenderFrameHost this player observer belongs to. Returns
// nullptr if unavailable.
virtual RenderFrameHost* GetRenderFrameHost() const = 0;
}; };
} // namespace content } // namespace content
......
...@@ -17,13 +17,13 @@ MediaSessionServiceImpl::MediaSessionServiceImpl( ...@@ -17,13 +17,13 @@ MediaSessionServiceImpl::MediaSessionServiceImpl(
: render_frame_host_(render_frame_host) { : render_frame_host_(render_frame_host) {
MediaSessionImpl* session = GetMediaSession(); MediaSessionImpl* session = GetMediaSession();
if (session) if (session)
session->SetMediaSessionService(this); session->OnServiceCreated(this);
} }
MediaSessionServiceImpl::~MediaSessionServiceImpl() { MediaSessionServiceImpl::~MediaSessionServiceImpl() {
MediaSessionImpl* session = GetMediaSession(); MediaSessionImpl* session = GetMediaSession();
if (session && session->GetMediaSessionService() == this) if (session)
session->SetMediaSessionService(nullptr); session->OnServiceDestroyed(this);
} }
// static // static
...@@ -50,24 +50,27 @@ void MediaSessionServiceImpl::SetMetadata( ...@@ -50,24 +50,27 @@ void MediaSessionServiceImpl::SetMetadata(
RenderProcessHost::CrashReportMode::GENERATE_CRASH_DUMP); RenderProcessHost::CrashReportMode::GENERATE_CRASH_DUMP);
return; return;
} }
metadata_ = metadata;
MediaSessionImpl* session = GetMediaSession(); MediaSessionImpl* session = GetMediaSession();
if (session) if (session)
session->SetMetadata(metadata); session->OnMediaSessionMetadataChanged(this);
} }
void MediaSessionServiceImpl::EnableAction( void MediaSessionServiceImpl::EnableAction(
blink::mojom::MediaSessionAction action) { blink::mojom::MediaSessionAction action) {
actions_.insert(action);
MediaSessionImpl* session = GetMediaSession(); MediaSessionImpl* session = GetMediaSession();
if (session) if (session)
session->OnMediaSessionEnabledAction(action); session->OnMediaSessionActionsChanged(this);
} }
void MediaSessionServiceImpl::DisableAction( void MediaSessionServiceImpl::DisableAction(
blink::mojom::MediaSessionAction action) { blink::mojom::MediaSessionAction action) {
actions_.erase(action);
MediaSessionImpl* session = GetMediaSession(); MediaSessionImpl* session = GetMediaSession();
if (session) if (session)
session->OnMediaSessionDisabledAction(action); session->OnMediaSessionActionsChanged(this);
} }
MediaSessionImpl* MediaSessionServiceImpl::GetMediaSession() { MediaSessionImpl* MediaSessionServiceImpl::GetMediaSession() {
...@@ -75,8 +78,6 @@ MediaSessionImpl* MediaSessionServiceImpl::GetMediaSession() { ...@@ -75,8 +78,6 @@ MediaSessionImpl* MediaSessionServiceImpl::GetMediaSession() {
WebContentsImpl::FromRenderFrameHost(render_frame_host_)); WebContentsImpl::FromRenderFrameHost(render_frame_host_));
if (!contents) if (!contents)
return nullptr; return nullptr;
if (render_frame_host_ != contents->GetMainFrame())
return nullptr;
return MediaSessionImpl::Get(contents); return MediaSessionImpl::Get(contents);
} }
......
...@@ -12,32 +12,39 @@ ...@@ -12,32 +12,39 @@
namespace content { namespace content {
class MediaSessionImpl;
class RenderFrameHost; class RenderFrameHost;
class MediaSessionImpl;
// There is one MediaSessionService per frame. // There is one MediaSessionService per frame. The class is owned by
class MediaSessionServiceImpl : public blink::mojom::MediaSessionService { // RenderFrameHost and should register/unregister itself to/from
// MediaSessionImpl when RenderFrameHost is created/destroyed.
class CONTENT_EXPORT MediaSessionServiceImpl
: public blink::mojom::MediaSessionService {
public: public:
~MediaSessionServiceImpl() override; ~MediaSessionServiceImpl() override;
static void Create(RenderFrameHost* render_frame_host, static void Create(RenderFrameHost* render_frame_host,
blink::mojom::MediaSessionServiceRequest request); blink::mojom::MediaSessionServiceRequest request);
const blink::mojom::MediaSessionClientPtr& GetClient() { return client_; } const blink::mojom::MediaSessionClientPtr& GetClient() { return client_; }
RenderFrameHost* GetRenderFrameHost() { return render_frame_host_; }
private: const base::Optional<MediaMetadata>& metadata() const { return metadata_; }
explicit MediaSessionServiceImpl(RenderFrameHost* render_frame_host); const std::set<blink::mojom::MediaSessionAction>& actions() const {
return actions_;
}
// blink::mojom::MediaSessionService implementation. // blink::mojom::MediaSessionService implementation.
void SetClient(blink::mojom::MediaSessionClientPtr client) override; void SetClient(blink::mojom::MediaSessionClientPtr client) override;
void SetMetadata( void SetMetadata(const base::Optional<MediaMetadata>& metadata) override;
const base::Optional<content::MediaMetadata>& metadata) override;
void EnableAction(blink::mojom::MediaSessionAction action) override; void EnableAction(blink::mojom::MediaSessionAction action) override;
void DisableAction(blink::mojom::MediaSessionAction action) override; void DisableAction(blink::mojom::MediaSessionAction action) override;
// Returns the content::MediaSession this service is associated with. Only protected:
// returns non-null when this service is in the top-level frame. explicit MediaSessionServiceImpl(RenderFrameHost* render_frame_host);
private:
MediaSessionImpl* GetMediaSession(); MediaSessionImpl* GetMediaSession();
void Bind(blink::mojom::MediaSessionServiceRequest request); void Bind(blink::mojom::MediaSessionServiceRequest request);
...@@ -48,6 +55,8 @@ class MediaSessionServiceImpl : public blink::mojom::MediaSessionService { ...@@ -48,6 +55,8 @@ class MediaSessionServiceImpl : public blink::mojom::MediaSessionService {
// The binding is removed when binding_ is cleared or goes out of scope. // The binding is removed when binding_ is cleared or goes out of scope.
std::unique_ptr<mojo::Binding<blink::mojom::MediaSessionService>> binding_; std::unique_ptr<mojo::Binding<blink::mojom::MediaSessionService>> binding_;
blink::mojom::MediaSessionClientPtr client_; blink::mojom::MediaSessionClientPtr client_;
base::Optional<MediaMetadata> metadata_;
std::set<blink::mojom::MediaSessionAction> actions_;
DISALLOW_COPY_AND_ASSIGN(MediaSessionServiceImpl); DISALLOW_COPY_AND_ASSIGN(MediaSessionServiceImpl);
}; };
......
...@@ -40,6 +40,10 @@ void MockMediaSessionPlayerObserver::OnSetVolumeMultiplier( ...@@ -40,6 +40,10 @@ void MockMediaSessionPlayerObserver::OnSetVolumeMultiplier(
players_[player_id].volume_multiplier_ = volume_multiplier; players_[player_id].volume_multiplier_ = volume_multiplier;
} }
RenderFrameHost* MockMediaSessionPlayerObserver::GetRenderFrameHost() const {
return nullptr;
}
int MockMediaSessionPlayerObserver::StartNewPlayer() { int MockMediaSessionPlayerObserver::StartNewPlayer() {
players_.push_back(MockPlayer(true, 1.0f)); players_.push_back(MockPlayer(true, 1.0f));
return players_.size() - 1; return players_.size() - 1;
......
...@@ -20,6 +20,7 @@ class MockMediaSessionPlayerObserver : public MediaSessionPlayerObserver { ...@@ -20,6 +20,7 @@ class MockMediaSessionPlayerObserver : public MediaSessionPlayerObserver {
void OnSuspend(int player_id) override; void OnSuspend(int player_id) override;
void OnResume(int player_id) override; void OnResume(int player_id) override;
void OnSetVolumeMultiplier(int player_id, double volume_multiplier) override; void OnSetVolumeMultiplier(int player_id, double volume_multiplier) override;
RenderFrameHost* GetRenderFrameHost() const override;
// Simulate that a new player started. // Simulate that a new player started.
// Returns the player_id. // Returns the player_id.
......
...@@ -59,6 +59,11 @@ void PepperPlayerDelegate::OnSetVolumeMultiplier(int player_id, ...@@ -59,6 +59,11 @@ void PepperPlayerDelegate::OnSetVolumeMultiplier(int player_id,
SetVolume(player_id, volume_multiplier); SetVolume(player_id, volume_multiplier);
} }
RenderFrameHost* PepperPlayerDelegate::GetRenderFrameHost() const {
// TODO(zqzhang): Pepper player should be associated to a RenderFrameHost.
return nullptr;
}
void PepperPlayerDelegate::SetVolume(int player_id, double volume) { void PepperPlayerDelegate::SetVolume(int player_id, double volume) {
contents_->Send(new FrameMsg_SetPepperVolume( contents_->Send(new FrameMsg_SetPepperVolume(
contents_->GetMainFrame()->routing_id(), pp_instance_, volume)); contents_->GetMainFrame()->routing_id(), pp_instance_, volume));
......
...@@ -26,6 +26,7 @@ class PepperPlayerDelegate : public MediaSessionPlayerObserver { ...@@ -26,6 +26,7 @@ class PepperPlayerDelegate : public MediaSessionPlayerObserver {
void OnResume(int player_id) override; void OnResume(int player_id) override;
void OnSetVolumeMultiplier(int player_id, void OnSetVolumeMultiplier(int player_id,
double volume_multiplier) override; double volume_multiplier) override;
RenderFrameHost* GetRenderFrameHost() const override;
private: private:
void SetVolume(int player_id, double volume); void SetVolume(int player_id, double volume);
......
...@@ -12,6 +12,8 @@ import org.chromium.content_public.browser.MediaSessionObserver; ...@@ -12,6 +12,8 @@ import org.chromium.content_public.browser.MediaSessionObserver;
import org.chromium.content_public.browser.WebContents; import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.common.MediaMetadata; import org.chromium.content_public.common.MediaMetadata;
import java.util.HashSet;
/** /**
* The MediaSessionImpl Java wrapper to allow communicating with the native MediaSessionImpl object. * The MediaSessionImpl Java wrapper to allow communicating with the native MediaSessionImpl object.
* The object is owned by Java WebContentsImpl instead of native to avoid introducing a new garbage * The object is owned by Java WebContentsImpl instead of native to avoid introducing a new garbage
...@@ -93,16 +95,12 @@ public class MediaSessionImpl extends MediaSession { ...@@ -93,16 +95,12 @@ public class MediaSessionImpl extends MediaSession {
} }
@CalledByNative @CalledByNative
private void mediaSessionEnabledAction(int action) { private void mediaSessionActionsChanged(int[] actions) {
for (mObserversIterator.rewind(); mObserversIterator.hasNext();) { HashSet<Integer> actionSet = new HashSet<Integer>();
mObserversIterator.next().mediaSessionEnabledAction(action); for (int action : actions) actionSet.add(action);
}
}
@CalledByNative
private void mediaSessionDisabledAction(int action) {
for (mObserversIterator.rewind(); mObserversIterator.hasNext();) { for (mObserversIterator.rewind(); mObserversIterator.hasNext();) {
mObserversIterator.next().mediaSessionDisabledAction(action); mObserversIterator.next().mediaSessionActionsChanged(actionSet);
} }
} }
......
...@@ -9,6 +9,8 @@ import android.support.annotation.Nullable; ...@@ -9,6 +9,8 @@ import android.support.annotation.Nullable;
import org.chromium.content.browser.MediaSessionImpl; import org.chromium.content.browser.MediaSessionImpl;
import org.chromium.content_public.common.MediaMetadata; import org.chromium.content_public.common.MediaMetadata;
import java.util.Set;
/** /**
* This class is Java implementation of native MediaSessionObserver. The class receives media * This class is Java implementation of native MediaSessionObserver. The class receives media
* session messages from Java {@link MediaSession}, which acts acts as a proxy forwarding messages * session messages from Java {@link MediaSession}, which acts acts as a proxy forwarding messages
...@@ -56,16 +58,10 @@ public abstract class MediaSessionObserver { ...@@ -56,16 +58,10 @@ public abstract class MediaSessionObserver {
public void mediaSessionMetadataChanged(MediaMetadata metadata) {} public void mediaSessionMetadataChanged(MediaMetadata metadata) {}
/** /**
* Called when the observed {@link MediaSession} has enabled an action. * Called when the observed {@link MediaSession} has changed its action list.
* @param action The enabled action. * @param actions The new action list after the change.
*/
public void mediaSessionEnabledAction(int action) {}
/**
* Called when the observed {@link MediaSession} has disabled an action.
* @param action The disabled action.
*/ */
public void mediaSessionDisabledAction(int action) {} public void mediaSessionActionsChanged(Set<Integer> actions) {}
/** /**
* Stop observing the media session. Users must explicitly call this before dereferencing the * Stop observing the media session. Users must explicitly call this before dereferencing the
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
#ifndef CONTENT_PUBLIC_BROWSER_MEDIA_SESSION_OBSERVER_H_ #ifndef CONTENT_PUBLIC_BROWSER_MEDIA_SESSION_OBSERVER_H_
#define CONTENT_PUBLIC_BROWSER_MEDIA_SESSION_OBSERVER_H_ #define CONTENT_PUBLIC_BROWSER_MEDIA_SESSION_OBSERVER_H_
#include <set>
#include "base/macros.h" #include "base/macros.h"
#include "base/optional.h" #include "base/optional.h"
#include "content/common/content_export.h" #include "content/common/content_export.h"
...@@ -41,13 +43,9 @@ class CONTENT_EXPORT MediaSessionObserver { ...@@ -41,13 +43,9 @@ class CONTENT_EXPORT MediaSessionObserver {
virtual void MediaSessionMetadataChanged( virtual void MediaSessionMetadataChanged(
const base::Optional<MediaMetadata>& metadata) {} const base::Optional<MediaMetadata>& metadata) {}
// Called when media session action is enabled. // Called when the media session action list has changed.
virtual void MediaSessionEnabledAction( virtual void MediaSessionActionsChanged(
blink::mojom::MediaSessionAction action) {} const std::set<blink::mojom::MediaSessionAction>& action) {}
// Called when media session action is disabled.
virtual void MediaSessionDisabledAction(
blink::mojom::MediaSessionAction action) {}
protected: protected:
// Create a MediaSessionObserver and start observing a session. // Create a MediaSessionObserver and start observing a session.
......
...@@ -1130,6 +1130,7 @@ test("content_unittests") { ...@@ -1130,6 +1130,7 @@ test("content_unittests") {
"../browser/media/midi_host_unittest.cc", "../browser/media/midi_host_unittest.cc",
"../browser/media/session/audio_focus_manager_unittest.cc", "../browser/media/session/audio_focus_manager_unittest.cc",
"../browser/media/session/media_session_controller_unittest.cc", "../browser/media/session/media_session_controller_unittest.cc",
"../browser/media/session/media_session_impl_service_routing_unittest.cc",
"../browser/media/session/media_session_uma_helper_unittest.cc", "../browser/media/session/media_session_uma_helper_unittest.cc",
"../browser/memory/memory_coordinator_impl_unittest.cc", "../browser/memory/memory_coordinator_impl_unittest.cc",
"../browser/memory/memory_coordinator_unittest.cc", "../browser/memory/memory_coordinator_unittest.cc",
......
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