blob: d83f5edce3877e6d481025a1a26d694023c4b673 [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/mediasession/media_session.h"
#include <memory>
#include <optional>
#include "base/notreached.h"
#include "base/time/default_tick_clock.h"
#include "base/time/time.h"
#include "third_party/blink/public/mojom/frame/user_activation_notification_type.mojom-blink.h"
#include "third_party/blink/public/platform/browser_interface_broker_proxy.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_media_position_state.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_media_session_action_details.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_media_session_action_handler.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_media_session_playback_state.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_media_session_seek_to_action_details.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/navigator.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/modules/mediasession/media_metadata.h"
#include "third_party/blink/renderer/modules/mediasession/media_metadata_sanitizer.h"
#include "third_party/blink/renderer/modules/mediasession/media_session_type_converters.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
namespace blink {
namespace {
using ::media_session::mojom::blink::MediaSessionAction;
V8MediaSessionAction::Enum MojomActionToActionEnum(MediaSessionAction action) {
switch (action) {
case MediaSessionAction::kPlay:
return V8MediaSessionAction::Enum::kPlay;
case MediaSessionAction::kPause:
return V8MediaSessionAction::Enum::kPause;
case MediaSessionAction::kPreviousTrack:
return V8MediaSessionAction::Enum::kPrevioustrack;
case MediaSessionAction::kNextTrack:
return V8MediaSessionAction::Enum::kNexttrack;
case MediaSessionAction::kSeekBackward:
return V8MediaSessionAction::Enum::kSeekbackward;
case MediaSessionAction::kSeekForward:
return V8MediaSessionAction::Enum::kSeekforward;
case MediaSessionAction::kSkipAd:
return V8MediaSessionAction::Enum::kSkipad;
case MediaSessionAction::kStop:
return V8MediaSessionAction::Enum::kStop;
case MediaSessionAction::kSeekTo:
return V8MediaSessionAction::Enum::kSeekto;
case MediaSessionAction::kToggleMicrophone:
return V8MediaSessionAction::Enum::kTogglemicrophone;
case MediaSessionAction::kToggleCamera:
return V8MediaSessionAction::Enum::kTogglecamera;
case MediaSessionAction::kHangUp:
return V8MediaSessionAction::Enum::kHangup;
case MediaSessionAction::kPreviousSlide:
return V8MediaSessionAction::Enum::kPreviousslide;
case MediaSessionAction::kNextSlide:
return V8MediaSessionAction::Enum::kNextslide;
case MediaSessionAction::kEnterPictureInPicture:
return V8MediaSessionAction::Enum::kEnterpictureinpicture;
case MediaSessionAction::kScrubTo:
case MediaSessionAction::kExitPictureInPicture:
case MediaSessionAction::kSwitchAudioDevice:
case MediaSessionAction::kEnterAutoPictureInPicture:
case MediaSessionAction::kSetMute:
case MediaSessionAction::kRaise:
NOTREACHED();
}
NOTREACHED();
}
MediaSessionAction ActionEnumToMojomAction(V8MediaSessionAction::Enum action) {
switch (action) {
case V8MediaSessionAction::Enum::kPlay:
return MediaSessionAction::kPlay;
case V8MediaSessionAction::Enum::kPause:
return MediaSessionAction::kPause;
case V8MediaSessionAction::Enum::kPrevioustrack:
return MediaSessionAction::kPreviousTrack;
case V8MediaSessionAction::Enum::kNexttrack:
return MediaSessionAction::kNextTrack;
case V8MediaSessionAction::Enum::kSeekbackward:
return MediaSessionAction::kSeekBackward;
case V8MediaSessionAction::Enum::kSeekforward:
return MediaSessionAction::kSeekForward;
case V8MediaSessionAction::Enum::kSkipad:
return MediaSessionAction::kSkipAd;
case V8MediaSessionAction::Enum::kStop:
return MediaSessionAction::kStop;
case V8MediaSessionAction::Enum::kSeekto:
return MediaSessionAction::kSeekTo;
case V8MediaSessionAction::Enum::kTogglemicrophone:
return MediaSessionAction::kToggleMicrophone;
case V8MediaSessionAction::Enum::kTogglecamera:
return MediaSessionAction::kToggleCamera;
case V8MediaSessionAction::Enum::kHangup:
return MediaSessionAction::kHangUp;
case V8MediaSessionAction::Enum::kPreviousslide:
return MediaSessionAction::kPreviousSlide;
case V8MediaSessionAction::Enum::kNextslide:
return MediaSessionAction::kNextSlide;
case V8MediaSessionAction::Enum::kEnterpictureinpicture:
return MediaSessionAction::kEnterPictureInPicture;
}
NOTREACHED();
}
V8MediaSessionPlaybackState::Enum MediaSessionPlaybackStateToEnum(
mojom::blink::MediaSessionPlaybackState state) {
switch (state) {
case mojom::blink::MediaSessionPlaybackState::NONE:
return V8MediaSessionPlaybackState::Enum::kNone;
case mojom::blink::MediaSessionPlaybackState::PAUSED:
return V8MediaSessionPlaybackState::Enum::kPaused;
case mojom::blink::MediaSessionPlaybackState::PLAYING:
return V8MediaSessionPlaybackState::Enum::kPlaying;
}
NOTREACHED();
}
mojom::blink::MediaSessionPlaybackState EnumToMediaSessionPlaybackState(
const V8MediaSessionPlaybackState::Enum& state) {
switch (state) {
case V8MediaSessionPlaybackState::Enum::kNone:
return mojom::blink::MediaSessionPlaybackState::NONE;
case V8MediaSessionPlaybackState::Enum::kPaused:
return mojom::blink::MediaSessionPlaybackState::PAUSED;
case V8MediaSessionPlaybackState::Enum::kPlaying:
return mojom::blink::MediaSessionPlaybackState::PLAYING;
}
NOTREACHED();
}
} // anonymous namespace
const char MediaSession::kSupplementName[] = "MediaSession";
MediaSession* MediaSession::mediaSession(Navigator& navigator) {
MediaSession* supplement =
Supplement<Navigator>::From<MediaSession>(navigator);
if (!supplement) {
supplement = MakeGarbageCollected<MediaSession>(navigator);
ProvideTo(navigator, supplement);
}
return supplement;
}
MediaSession::MediaSession(Navigator& navigator)
: Supplement<Navigator>(navigator),
clock_(base::DefaultTickClock::GetInstance()),
playback_state_(mojom::blink::MediaSessionPlaybackState::NONE),
service_(navigator.GetExecutionContext()),
client_receiver_(this, navigator.DomWindow()) {}
void MediaSession::setPlaybackState(
const V8MediaSessionPlaybackState& playback_state) {
playback_state_ = EnumToMediaSessionPlaybackState(playback_state.AsEnum());
RecalculatePositionState(/*was_set=*/false);
mojom::blink::MediaSessionService* service = GetService();
if (service)
service->SetPlaybackState(playback_state_);
}
V8MediaSessionPlaybackState MediaSession::playbackState() {
return V8MediaSessionPlaybackState(
MediaSessionPlaybackStateToEnum(playback_state_));
}
void MediaSession::setMetadata(MediaMetadata* metadata) {
if (metadata)
metadata->SetSession(this);
if (metadata_)
metadata_->SetSession(nullptr);
metadata_ = metadata;
OnMetadataChanged();
}
MediaMetadata* MediaSession::metadata() const {
return metadata_.Get();
}
void MediaSession::OnMetadataChanged() {
mojom::blink::MediaSessionService* service = GetService();
if (!service)
return;
// OnMetadataChanged() is called from a timer. The Window/ExecutionContext
// might detaches in the meantime. See https://crbug.com/1269522
ExecutionContext* context = GetSupplementable()->DomWindow();
if (!context)
return;
service->SetMetadata(
MediaMetadataSanitizer::SanitizeAndConvertToMojo(metadata_, context));
}
void MediaSession::setActionHandler(const V8MediaSessionAction& action,
V8MediaSessionActionHandler* handler,
ExceptionState& exception_state) {
auto action_value = action.AsEnum();
if (action_value == V8MediaSessionAction::Enum::kSkipad) {
LocalDOMWindow* window = GetSupplementable()->DomWindow();
if (!RuntimeEnabledFeatures::SkipAdEnabled(window)) {
exception_state.ThrowTypeError(
"The provided value 'skipad' is not a valid enum "
"value of type MediaSessionAction.");
return;
}
UseCounter::Count(window, WebFeature::kMediaSessionSkipAd);
}
if (action_value == V8MediaSessionAction::Enum::kEnterpictureinpicture) {
if (!RuntimeEnabledFeatures::MediaSessionEnterPictureInPictureEnabled()) {
exception_state.ThrowTypeError(
"The provided value 'enterpictureinpicture'"
" is not a valid enum "
"value of type MediaSessionAction.");
return;
}
UseCounter::Count(GetSupplementable()->DomWindow(),
WebFeature::kMediaSessionEnterPictureInPicture);
}
if (handler) {
auto add_result = action_handlers_.Set(action_value, handler);
if (!add_result.is_new_entry)
return;
NotifyActionChange(action_value, ActionChangeType::kActionEnabled);
} else {
if (action_handlers_.find(action_value) == action_handlers_.end()) {
return;
}
action_handlers_.erase(action_value);
NotifyActionChange(action_value, ActionChangeType::kActionDisabled);
}
}
void MediaSession::setPositionState(MediaPositionState* position_state,
ExceptionState& exception_state) {
// If the dictionary is empty / null then we should reset the position state.
if (!position_state->hasDuration() && !position_state->hasPlaybackRate() &&
!position_state->hasPosition()) {
position_state_ = nullptr;
declared_playback_rate_ = 0.0;
if (auto* service = GetService())
service->SetPositionState(nullptr);
return;
}
// The duration cannot be missing.
if (!position_state->hasDuration()) {
exception_state.ThrowTypeError("The duration must be provided.");
return;
}
// The duration cannot be NaN.
if (std::isnan(position_state->duration())) {
exception_state.ThrowTypeError("The provided duration cannot be NaN.");
return;
}
// The duration cannot be negative.
if (position_state->duration() < 0) {
exception_state.ThrowTypeError(
"The provided duration cannot be less than zero.");
return;
}
// The position cannot be negative.
if (position_state->hasPosition() && position_state->position() < 0) {
exception_state.ThrowTypeError(
"The provided position cannot be less than zero.");
return;
}
// The position cannot be greater than the duration.
if (position_state->hasPosition() &&
position_state->position() > position_state->duration()) {
exception_state.ThrowTypeError(
"The provided position cannot be greater than the duration.");
return;
}
// The playback rate cannot be equal to zero.
if (position_state->hasPlaybackRate() &&
position_state->playbackRate() == 0) {
exception_state.ThrowTypeError(
"The provided playbackRate cannot be equal to zero.");
return;
}
position_state_ =
mojo::ConvertTo<media_session::mojom::blink::MediaPositionPtr>(
position_state);
declared_playback_rate_ = position_state_->playback_rate;
RecalculatePositionState(/*was_set=*/true);
}
void MediaSession::setMicrophoneActive(bool active) {
auto* service = GetService();
if (!service)
return;
if (active) {
service->SetMicrophoneState(
media_session::mojom::MicrophoneState::kUnmuted);
} else {
service->SetMicrophoneState(media_session::mojom::MicrophoneState::kMuted);
}
}
void MediaSession::setCameraActive(bool active) {
auto* service = GetService();
if (!service)
return;
if (active) {
service->SetCameraState(media_session::mojom::CameraState::kTurnedOn);
} else {
service->SetCameraState(media_session::mojom::CameraState::kTurnedOff);
}
}
void MediaSession::NotifyActionChange(V8MediaSessionAction::Enum action,
ActionChangeType type) {
mojom::blink::MediaSessionService* service = GetService();
if (!service)
return;
auto mojom_action = ActionEnumToMojomAction(action);
switch (type) {
case ActionChangeType::kActionEnabled:
service->EnableAction(mojom_action);
break;
case ActionChangeType::kActionDisabled:
service->DisableAction(mojom_action);
break;
}
}
base::TimeDelta MediaSession::GetPositionNow() const {
const base::TimeTicks now = clock_->NowTicks();
const base::TimeDelta elapsed_time =
position_state_->playback_rate *
(now - position_state_->last_updated_time);
const base::TimeDelta updated_position =
position_state_->position + elapsed_time;
const base::TimeDelta start = base::Seconds(0);
if (updated_position <= start)
return start;
else if (updated_position >= position_state_->duration)
return position_state_->duration;
else
return updated_position;
}
void MediaSession::RecalculatePositionState(bool was_set) {
if (!position_state_)
return;
double new_playback_rate =
playback_state_ == mojom::blink::MediaSessionPlaybackState::PAUSED
? 0.0
: declared_playback_rate_;
if (!was_set && new_playback_rate == position_state_->playback_rate)
return;
// If we updated the position state because of the playback rate then we
// should update the time.
if (!was_set) {
position_state_->position = GetPositionNow();
}
position_state_->playback_rate = new_playback_rate;
position_state_->last_updated_time = clock_->NowTicks();
if (auto* service = GetService())
service->SetPositionState(position_state_.Clone());
}
mojom::blink::MediaSessionService* MediaSession::GetService() {
if (service_) {
return service_.get();
}
LocalDOMWindow* window = GetSupplementable()->DomWindow();
if (!window) {
return nullptr;
}
// See https://bit.ly/2S0zRAS for task types.
auto task_runner = window->GetTaskRunner(TaskType::kMiscPlatformAPI);
window->GetBrowserInterfaceBroker().GetInterface(
service_.BindNewPipeAndPassReceiver(task_runner));
if (service_.get())
service_->SetClient(client_receiver_.BindNewPipeAndPassRemote(task_runner));
return service_.get();
}
void MediaSession::DidReceiveAction(
media_session::mojom::blink::MediaSessionAction action,
mojom::blink::MediaSessionActionDetailsPtr details) {
LocalDOMWindow* window = GetSupplementable()->DomWindow();
if (!window)
return;
LocalFrame::NotifyUserActivation(
window->GetFrame(),
mojom::blink::UserActivationNotificationType::kInteraction);
auto v8_action = MojomActionToActionEnum(action);
auto iter = action_handlers_.find(v8_action);
if (iter == action_handlers_.end())
return;
const auto* blink_details =
mojo::TypeConverter<const blink::MediaSessionActionDetails*,
blink::mojom::blink::MediaSessionActionDetailsPtr>::
ConvertWithV8Action(details, v8_action);
iter->value->InvokeAndReportException(this, blink_details);
}
void MediaSession::Trace(Visitor* visitor) const {
visitor->Trace(client_receiver_);
visitor->Trace(metadata_);
visitor->Trace(action_handlers_);
visitor->Trace(service_);
ScriptWrappable::Trace(visitor);
Supplement<Navigator>::Trace(visitor);
}
} // namespace blink