| // Copyright 2017 The Cobalt Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "cobalt/media_session/media_session_client.h" |
| |
| #include <algorithm> |
| #include <cmath> |
| #include <memory> |
| #include <string> |
| |
| #include "base/logging.h" |
| #include "cobalt/script/sequence.h" |
| #include "starboard/time.h" |
| |
| using MediaImageSequence = ::cobalt::script::Sequence<MediaImage>; |
| |
| namespace cobalt { |
| namespace media_session { |
| |
| namespace { |
| |
| // Delay to re-query position state after an action has been invoked. |
| const base::TimeDelta kUpdateDelay = base::TimeDelta::FromMilliseconds(250); |
| |
| // Delay to check if the media session state is not active. |
| const base::TimeDelta kMaybeFreezeDelay = |
| base::TimeDelta::FromMilliseconds(1500); |
| |
| // Guess the media position state for the media session. |
| void GuessMediaPositionState(MediaSessionState* session_state, |
| const media::WebMediaPlayer** guess_player, |
| const media::WebMediaPlayer* current_player) { |
| // Assume the player with the biggest video size is the one controlled by the |
| // media session. This isn't perfect, so it's best that the web app set the |
| // media position state explicitly. |
| if (*guess_player == nullptr || |
| (*guess_player)->GetNaturalSize().GetArea() < |
| current_player->GetNaturalSize().GetArea()) { |
| *guess_player = current_player; |
| |
| MediaPositionState position_state; |
| float duration = (*guess_player)->GetDuration(); |
| if (std::isfinite(duration)) { |
| position_state.set_duration(duration); |
| } else if (std::isinf(duration)) { |
| position_state.set_duration(kSbTimeMax); |
| } else { |
| position_state.set_duration(0.0); |
| } |
| position_state.set_playback_rate((*guess_player)->GetPlaybackRate()); |
| position_state.set_position((*guess_player)->GetCurrentTime()); |
| |
| *session_state = MediaSessionState(session_state->metadata(), |
| SbTimeGetMonotonicNow(), position_state, |
| session_state->actual_playback_state(), |
| session_state->available_actions()); |
| } |
| } |
| } // namespace |
| |
| MediaSessionClient::MediaSessionClient(MediaSession* media_session) |
| : media_session_(media_session), |
| platform_playback_state_(kMediaSessionPlaybackStateNone), |
| sequence_number_(0) { |
| extension_ = static_cast<const CobaltExtensionMediaSessionApi*>( |
| SbSystemGetExtension(kCobaltExtensionMediaSessionName)); |
| if (extension_) { |
| if (strcmp(extension_->name, kCobaltExtensionMediaSessionName) != 0 || |
| extension_->version < 1) { |
| LOG(WARNING) << "Wrong MediaSession extension supplied"; |
| extension_ = nullptr; |
| } else if (extension_->RegisterMediaSessionCallbacks != nullptr) { |
| extension_->RegisterMediaSessionCallbacks( |
| this, &InvokeActionCallback, &UpdatePlatformPlaybackStateCallback); |
| } |
| } |
| } |
| |
| MediaSessionClient::~MediaSessionClient() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| // Destroy the platform's MediaSessionClient, if it exists. |
| if (extension_ != NULL && |
| extension_->DestroyMediaSessionClientCallback != NULL) { |
| extension_->DestroyMediaSessionClientCallback(); |
| } |
| } |
| |
| void MediaSessionClient::SetMediaPlayerFactory( |
| const media::WebMediaPlayerFactory* factory) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| media_player_factory_ = factory; |
| } |
| |
| MediaSessionPlaybackState MediaSessionClient::ComputeActualPlaybackState() |
| const { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| // Per https://wicg.github.io/mediasession/#guessed-playback-state |
| // - If the "declared playback state" is "playing", then return "playing" |
| // - Otherwise, return the guessed playback state |
| MediaSessionPlaybackState declared_state; |
| declared_state = media_session_->playback_state(); |
| if (declared_state == kMediaSessionPlaybackStatePlaying) { |
| return kMediaSessionPlaybackStatePlaying; |
| } |
| |
| if (platform_playback_state_ == kMediaSessionPlaybackStatePlaying) { |
| // "...guessed playback state is playing if any of them is |
| // potentially playing and not muted..." |
| return kMediaSessionPlaybackStatePlaying; |
| } |
| |
| // It's not super clear what to do when the declared state or the |
| // active media session state is kPaused or kNone |
| |
| if (declared_state == kMediaSessionPlaybackStatePaused) { |
| return kMediaSessionPlaybackStatePaused; |
| } |
| |
| return kMediaSessionPlaybackStateNone; |
| } |
| |
| MediaSessionState::AvailableActionsSet |
| MediaSessionClient::ComputeAvailableActions() const { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| // "Available actions" are determined based on active media session |
| // and supported media session actions. |
| // Note for cobalt, there's only one window/tab so there's only one |
| // "active media session" |
| // https://wicg.github.io/mediasession/#actions-model |
| // |
| // Note that this is essentially the "media session actions update algorithm" |
| // inverted. |
| MediaSessionState::AvailableActionsSet result = |
| MediaSessionState::AvailableActionsSet(); |
| |
| for (MediaSession::ActionMap::iterator it = |
| media_session_->action_map_.begin(); |
| it != media_session_->action_map_.end(); ++it) { |
| result[it->first] = true; |
| } |
| |
| switch (ComputeActualPlaybackState()) { |
| case kMediaSessionPlaybackStatePlaying: |
| // "If the active media session’s actual playback state is playing, remove |
| // play from available actions." |
| result[kMediaSessionActionPlay] = false; |
| break; |
| case kMediaSessionPlaybackStateNone: |
| // Not defined in the spec: disable Seekbackward, Seekforward, SeekTo, & |
| // Stop when no media is playing. |
| result[kMediaSessionActionSeekbackward] = false; |
| result[kMediaSessionActionSeekforward] = false; |
| result[kMediaSessionActionSeekto] = false; |
| result[kMediaSessionActionStop] = false; |
| // Fall-through intended (None case falls through to Paused case). |
| case kMediaSessionPlaybackStatePaused: |
| // "Otherwise, remove pause from available actions." |
| result[kMediaSessionActionPause] = false; |
| break; |
| } |
| |
| return result; |
| } |
| |
| void MediaSessionClient::PostDelayedTaskForMaybeFreezeCallback() { |
| media_session_->task_runner_->PostDelayedTask( |
| FROM_HERE, |
| base::Bind(&MediaSessionClient::RunMaybeFreezeCallback, |
| base::Unretained(this), ++sequence_number_), |
| kMaybeFreezeDelay); |
| } |
| |
| void MediaSessionClient::UpdatePlatformPlaybackState( |
| CobaltExtensionMediaSessionPlaybackState state) { |
| DCHECK(media_session_->task_runner_); |
| if (!media_session_->task_runner_->BelongsToCurrentThread()) { |
| media_session_->task_runner_->PostTask( |
| FROM_HERE, base::Bind(&MediaSessionClient::UpdatePlatformPlaybackState, |
| base::Unretained(this), state)); |
| return; |
| } |
| |
| platform_playback_state_ = ConvertPlaybackState(state); |
| if (session_state_.actual_playback_state() != ComputeActualPlaybackState()) { |
| UpdateMediaSessionState(); |
| } |
| |
| PostDelayedTaskForMaybeFreezeCallback(); |
| } |
| |
| void MediaSessionClient::RunMaybeFreezeCallback(int sequence_number) { |
| if (sequence_number != sequence_number_) return; |
| |
| if (!is_active() && !maybe_freeze_callback_.is_null()) { |
| maybe_freeze_callback_.Run(); |
| } |
| } |
| |
| void MediaSessionClient::InvokeActionInternal( |
| std::unique_ptr<CobaltExtensionMediaSessionActionDetails> details) { |
| DCHECK(details->action >= 0 && |
| details->action < kCobaltExtensionMediaSessionActionNumActions); |
| |
| // Some fields should only be set for applicable actions. |
| DCHECK(details->seek_offset < 0.0 || |
| details->action == kCobaltExtensionMediaSessionActionSeekforward || |
| details->action == kCobaltExtensionMediaSessionActionSeekbackward); |
| DCHECK(details->seek_time < 0.0 || |
| details->action == kCobaltExtensionMediaSessionActionSeekto); |
| DCHECK(!details->fast_seek || |
| details->action == kCobaltExtensionMediaSessionActionSeekto); |
| |
| DCHECK(media_session_->task_runner_); |
| if (!media_session_->task_runner_->BelongsToCurrentThread()) { |
| media_session_->task_runner_->PostTask( |
| FROM_HERE, base::Bind(&MediaSessionClient::InvokeActionInternal, |
| base::Unretained(this), base::Passed(&details))); |
| return; |
| } |
| |
| MediaSession::ActionMap::iterator it = media_session_->action_map_.find( |
| ConvertMediaSessionAction(details->action)); |
| |
| if (it == media_session_->action_map_.end()) { |
| return; |
| } |
| |
| std::unique_ptr<MediaSessionActionDetails> script_details = |
| ConvertActionDetails(*details); |
| it->second->value().Run(*script_details); |
| |
| // Queue a session update to reflect the effects of the action. |
| if (!media_session_->media_position_state_) { |
| media_session_->MaybeQueueChangeTask(kUpdateDelay); |
| } |
| } |
| |
| void MediaSessionClient::UpdateMediaSessionState() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| scoped_refptr<MediaMetadata> session_metadata(media_session_->metadata()); |
| base::Optional<MediaMetadataInit> metadata; |
| if (session_metadata) { |
| metadata.emplace(); |
| metadata->set_title(session_metadata->title()); |
| metadata->set_artist(session_metadata->artist()); |
| metadata->set_album(session_metadata->album()); |
| metadata->set_artwork(session_metadata->artwork()); |
| } |
| |
| session_state_ = MediaSessionState( |
| metadata, media_session_->last_position_updated_time_, |
| media_session_->media_position_state_, ComputeActualPlaybackState(), |
| ComputeAvailableActions()); |
| |
| // Compute the media position state if it's not set in the media session. |
| if (!media_session_->media_position_state_ && media_player_factory_) { |
| const media::WebMediaPlayer* player = nullptr; |
| media_player_factory_->EnumerateWebMediaPlayers(base::BindRepeating( |
| &GuessMediaPositionState, &session_state_, &player)); |
| |
| // The media duration may be reported as 0 when seeking. Re-query the |
| // media session state after a delay. |
| if (session_state_.actual_playback_state() == |
| kMediaSessionPlaybackStatePlaying && |
| session_state_.duration() == 0) { |
| media_session_->MaybeQueueChangeTask(kUpdateDelay); |
| } |
| } |
| |
| OnMediaSessionStateChanged(session_state_); |
| } |
| |
| void MediaSessionClient::OnMediaSessionStateChanged( |
| const MediaSessionState& session_state) { |
| if (extension_ && extension_->version >= 1) { |
| CobaltExtensionMediaSessionState ext_state; |
| size_t artwork_size = 0; |
| if (session_state.has_metadata() && |
| session_state.metadata().value().has_artwork()) { |
| artwork_size = session_state.metadata().value().artwork().size(); |
| } |
| std::unique_ptr<CobaltExtensionMediaImage[]> ext_artwork = |
| std::unique_ptr<CobaltExtensionMediaImage[]>( |
| new CobaltExtensionMediaImage[artwork_size]); |
| |
| ext_state.duration = session_state.duration(); |
| ext_state.actual_playback_rate = session_state.actual_playback_rate(); |
| ext_state.current_playback_position = |
| session_state.current_playback_position(); |
| ext_state.has_position_state = session_state.has_position_state(); |
| ext_state.actual_playback_state = |
| ConvertPlaybackState(session_state.actual_playback_state()); |
| ConvertMediaSessionActions(session_state.available_actions(), |
| ext_state.available_actions); |
| std::string album = ""; |
| std::string artist = ""; |
| std::string title = ""; |
| |
| if (session_state.has_metadata()) { |
| const MediaMetadataInit& metadata = session_state.metadata().value(); |
| album = metadata.album(); |
| artist = metadata.artist(); |
| title = metadata.title(); |
| if (artwork_size > 0) { |
| const MediaImageSequence& artwork(metadata.artwork()); |
| for (MediaImageSequence::size_type i = 0; i < artwork_size; i++) { |
| const MediaImage& media_image(artwork.at(i)); |
| CobaltExtensionMediaImage ext_image; |
| ext_image.src = media_image.src().c_str(); |
| ext_image.size = media_image.sizes().c_str(); |
| ext_image.type = media_image.type().c_str(); |
| ext_artwork[i] = ext_image; |
| } |
| } |
| } |
| CobaltExtensionMediaMetadata ext_metadata = { |
| album.c_str(), artist.c_str(), title.c_str(), ext_artwork.get(), |
| artwork_size}; |
| ext_state.metadata = &ext_metadata; |
| |
| extension_->OnMediaSessionStateChanged(ext_state); |
| } |
| } |
| |
| // static |
| void MediaSessionClient::UpdatePlatformPlaybackStateCallback( |
| CobaltExtensionMediaSessionPlaybackState state, void* callback_context) { |
| MediaSessionClient* client = |
| static_cast<MediaSessionClient*>(callback_context); |
| client->UpdatePlatformPlaybackState(state); |
| } |
| |
| // static |
| void MediaSessionClient::InvokeActionCallback( |
| CobaltExtensionMediaSessionActionDetails details, void* callback_context) { |
| MediaSessionClient* client = |
| static_cast<MediaSessionClient*>(callback_context); |
| client->InvokeAction(details); |
| } |
| |
| CobaltExtensionMediaSessionPlaybackState |
| MediaSessionClient::ConvertPlaybackState(MediaSessionPlaybackState state) { |
| switch (state) { |
| case kMediaSessionPlaybackStatePlaying: |
| return kCobaltExtensionMediaSessionPlaying; |
| case kMediaSessionPlaybackStatePaused: |
| return kCobaltExtensionMediaSessionPaused; |
| case kMediaSessionPlaybackStateNone: |
| default: |
| return kCobaltExtensionMediaSessionNone; |
| } |
| } |
| |
| MediaSessionPlaybackState MediaSessionClient::ConvertPlaybackState( |
| CobaltExtensionMediaSessionPlaybackState state) { |
| switch (state) { |
| case kCobaltExtensionMediaSessionPlaying: |
| return kMediaSessionPlaybackStatePlaying; |
| case kCobaltExtensionMediaSessionPaused: |
| return kMediaSessionPlaybackStatePaused; |
| case kCobaltExtensionMediaSessionNone: |
| default: |
| return kMediaSessionPlaybackStateNone; |
| } |
| } |
| |
| void MediaSessionClient::ConvertMediaSessionActions( |
| const MediaSessionState::AvailableActionsSet& actions, |
| bool result[kCobaltExtensionMediaSessionActionNumActions]) { |
| for (int i = 0; i < kCobaltExtensionMediaSessionActionNumActions; i++) { |
| result[i] = false; |
| MediaSessionAction action = static_cast<MediaSessionAction>(i); |
| if (actions[action]) { |
| result[ConvertMediaSessionAction(action)] = true; |
| } |
| } |
| } |
| |
| std::unique_ptr<MediaSessionActionDetails> |
| MediaSessionClient::ConvertActionDetails( |
| const CobaltExtensionMediaSessionActionDetails& ext_details) { |
| std::unique_ptr<MediaSessionActionDetails> details( |
| new MediaSessionActionDetails()); |
| details->set_action(ConvertMediaSessionAction(ext_details.action)); |
| if (ext_details.seek_offset >= 0.0) { |
| details->set_seek_offset(ext_details.seek_offset); |
| } |
| if (ext_details.seek_time >= 0.0) { |
| details->set_seek_time(ext_details.seek_time); |
| } |
| details->set_fast_seek(ext_details.fast_seek); |
| return details; |
| } |
| |
| CobaltExtensionMediaSessionAction MediaSessionClient::ConvertMediaSessionAction( |
| MediaSessionAction action) { |
| switch (action) { |
| case kMediaSessionActionPause: |
| return kCobaltExtensionMediaSessionActionPause; |
| case kMediaSessionActionSeekbackward: |
| return kCobaltExtensionMediaSessionActionSeekbackward; |
| case kMediaSessionActionPrevioustrack: |
| return kCobaltExtensionMediaSessionActionPrevioustrack; |
| case kMediaSessionActionNexttrack: |
| return kCobaltExtensionMediaSessionActionNexttrack; |
| case kMediaSessionActionSeekforward: |
| return kCobaltExtensionMediaSessionActionSeekforward; |
| case kMediaSessionActionSeekto: |
| return kCobaltExtensionMediaSessionActionSeekto; |
| case kMediaSessionActionStop: |
| return kCobaltExtensionMediaSessionActionStop; |
| case kMediaSessionActionPlay: |
| default: |
| return kCobaltExtensionMediaSessionActionPlay; |
| } |
| } |
| |
| MediaSessionAction MediaSessionClient::ConvertMediaSessionAction( |
| CobaltExtensionMediaSessionAction action) { |
| switch (action) { |
| case kCobaltExtensionMediaSessionActionPause: |
| return kMediaSessionActionPause; |
| case kCobaltExtensionMediaSessionActionSeekbackward: |
| return kMediaSessionActionSeekbackward; |
| case kCobaltExtensionMediaSessionActionPrevioustrack: |
| return kMediaSessionActionPrevioustrack; |
| case kCobaltExtensionMediaSessionActionNexttrack: |
| return kMediaSessionActionNexttrack; |
| case kCobaltExtensionMediaSessionActionSeekforward: |
| return kMediaSessionActionSeekforward; |
| case kCobaltExtensionMediaSessionActionSeekto: |
| return kMediaSessionActionSeekto; |
| case kCobaltExtensionMediaSessionActionStop: |
| return kMediaSessionActionStop; |
| case kCobaltExtensionMediaSessionActionPlay: |
| default: |
| return kMediaSessionActionPlay; |
| } |
| } |
| } // namespace media_session |
| } // namespace cobalt |