blob: 1861af9e2ba539fc140942c4f0a25895d27a9de0 [file] [log] [blame]
// 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 "starboard/android/shared/cobalt/android_media_session_client.h"
#include "base/time/time.h"
#include "cobalt/media_session/media_session_action_details.h"
#include "cobalt/media_session/media_session_client.h"
#include "cobalt/script/sequence.h"
#include "starboard/android/shared/jni_env_ext.h"
#include "starboard/android/shared/jni_utils.h"
#include "starboard/common/log.h"
#include "starboard/common/mutex.h"
#include "starboard/once.h"
namespace starboard {
namespace android {
namespace shared {
namespace cobalt {
using ::cobalt::media_session::MediaImage;
using ::cobalt::media_session::MediaMetadataInit;
using ::cobalt::media_session::MediaSession;
using ::cobalt::media_session::MediaSessionAction;
using ::cobalt::media_session::MediaSessionActionDetails;
using ::cobalt::media_session::MediaSessionClient;
using ::cobalt::media_session::MediaSessionPlaybackState;
using ::cobalt::media_session::MediaSessionState;
using ::cobalt::media_session::kMediaSessionActionPause;
using ::cobalt::media_session::kMediaSessionActionPlay;
using ::cobalt::media_session::kMediaSessionActionSeekto;
using ::cobalt::media_session::kMediaSessionActionSeekbackward;
using ::cobalt::media_session::kMediaSessionActionSeekforward;
using ::cobalt::media_session::kMediaSessionActionStop;
using ::cobalt::media_session::kMediaSessionActionPrevioustrack;
using ::cobalt::media_session::kMediaSessionActionNexttrack;
using ::cobalt::media_session::kMediaSessionPlaybackStateNone;
using ::cobalt::media_session::kMediaSessionPlaybackStatePaused;
using ::cobalt::media_session::kMediaSessionPlaybackStatePlaying;
using MediaImageSequence = ::cobalt::script::Sequence<MediaImage>;
using ::starboard::android::shared::JniEnvExt;
using ::starboard::android::shared::ScopedLocalJavaRef;
namespace {
// These constants are from android.media.session.PlaybackState
const jlong kPlaybackStateActionStop = 1 << 0;
const jlong kPlaybackStateActionPause = 1 << 1;
const jlong kPlaybackStateActionPlay = 1 << 2;
const jlong kPlaybackStateActionRewind = 1 << 3;
const jlong kPlaybackStateActionSkipToPrevious = 1 << 4;
const jlong kPlaybackStateActionSkipToNext = 1 << 5;
const jlong kPlaybackStateActionFastForward = 1 << 6;
const jlong kPlaybackStateActionSetRating = 1 << 7; // not supported
const jlong kPlaybackStateActionSeekTo = 1 << 8;
// Converts a MediaSessionClient::AvailableActions bitset into
// a android.media.session.PlaybackState jlong bitset.
jlong MediaSessionActionsToPlaybackStateActions(
const MediaSessionState::AvailableActionsSet& actions) {
jlong result = 0;
if (actions[kMediaSessionActionPause]) {
result |= kPlaybackStateActionPause;
}
if (actions[kMediaSessionActionPlay]) {
result |= kPlaybackStateActionPlay;
}
if (actions[kMediaSessionActionSeekbackward]) {
result |= kPlaybackStateActionRewind;
}
if (actions[kMediaSessionActionPrevioustrack]) {
result |= kPlaybackStateActionSkipToPrevious;
}
if (actions[kMediaSessionActionNexttrack]) {
result |= kPlaybackStateActionSkipToNext;
}
if (actions[kMediaSessionActionSeekforward]) {
result |= kPlaybackStateActionFastForward;
}
if (actions[kMediaSessionActionSeekto]) {
result |= kPlaybackStateActionSeekTo;
}
if (actions[kMediaSessionActionStop]) {
result |= kPlaybackStateActionStop;
}
return result;
}
PlaybackState MediaSessionPlaybackStateToPlaybackState(
MediaSessionPlaybackState in_state) {
switch (in_state) {
case kMediaSessionPlaybackStatePlaying:
return kPlaying;
case kMediaSessionPlaybackStatePaused:
return kPaused;
case kMediaSessionPlaybackStateNone:
return kNone;
}
}
MediaSessionAction PlaybackStateActionToMediaSessionAction(jlong action) {
MediaSessionAction result;
switch (action) {
case kPlaybackStateActionPause:
result = kMediaSessionActionPause;
break;
case kPlaybackStateActionPlay:
result = kMediaSessionActionPlay;
break;
case kPlaybackStateActionRewind:
result = kMediaSessionActionSeekbackward;
break;
case kPlaybackStateActionSkipToPrevious:
result = kMediaSessionActionPrevioustrack;
break;
case kPlaybackStateActionSkipToNext:
result = kMediaSessionActionNexttrack;
break;
case kPlaybackStateActionFastForward:
result = kMediaSessionActionSeekforward;
break;
case kPlaybackStateActionSeekTo:
result = kMediaSessionActionSeekto;
break;
case kPlaybackStateActionStop:
result = kMediaSessionActionStop;
break;
default:
SB_NOTREACHED() << "Unsupported MediaSessionAction 0x"
<< std::hex << action;
result = static_cast<MediaSessionAction>(-1);
}
return result;
}
MediaSessionPlaybackState PlaybackStateToMediaSessionPlaybackState(
PlaybackState state) {
MediaSessionPlaybackState result;
switch (state) {
case kPlaying:
result = kMediaSessionPlaybackStatePlaying;
break;
case kPaused:
result = kMediaSessionPlaybackStatePaused;
break;
case kNone:
result = kMediaSessionPlaybackStateNone;
break;
default:
SB_NOTREACHED() << "Unsupported PlaybackState " << state;
result = static_cast<MediaSessionPlaybackState>(-1);
}
return result;
}
} // namespace
class AndroidMediaSessionClient : public MediaSessionClient {
static SbOnceControl once_flag;
static SbMutex mutex;
// The last MediaSessionClient to become active, or null.
// Used to route Java callbacks.
// In practice, only one MediaSessionClient will become active at a time.
// Protected by "mutex"
static AndroidMediaSessionClient* active_client;
static void OnceInit() { SbMutexCreate(&mutex); }
public:
static void NativeInvokeAction(jlong action, jlong seek_ms) {
SbOnce(&once_flag, OnceInit);
SbMutexAcquire(&mutex);
if (active_client != NULL) {
std::unique_ptr<MediaSessionActionDetails> details(
new MediaSessionActionDetails());
details->set_action(PlaybackStateActionToMediaSessionAction(action));
// CobaltMediaSession.java only sets seek_ms for SeekTo (not ff/rew).
if (details->action() == kMediaSessionActionSeekto) {
details->set_seek_time(seek_ms / 1000.0);
}
active_client->InvokeAction(std::move(details));
}
SbMutexRelease(&mutex);
}
static void UpdateActiveSessionPlatformPlaybackState(
MediaSessionPlaybackState state) {
SbOnce(&once_flag, OnceInit);
SbMutexAcquire(&mutex);
if (active_client != NULL) {
active_client->UpdatePlatformPlaybackState(state);
}
SbMutexRelease(&mutex);
}
AndroidMediaSessionClient() {}
virtual ~AndroidMediaSessionClient() {
SbOnce(&once_flag, OnceInit);
SbMutexAcquire(&mutex);
if (active_client == this) {
active_client = NULL;
}
SbMutexRelease(&mutex);
}
void OnMediaSessionStateChanged(
const MediaSessionState& session_state) override {
JniEnvExt* env = JniEnvExt::Get();
jint playback_state = MediaSessionPlaybackStateToPlaybackState(
session_state.actual_playback_state());
SbOnce(&once_flag, OnceInit);
SbMutexAcquire(&mutex);
if (playback_state != kNone) {
active_client = this;
} else if (active_client == this) {
active_client = NULL;
}
SbMutexRelease(&mutex);
jlong playback_state_actions = MediaSessionActionsToPlaybackStateActions(
session_state.available_actions());
ScopedLocalJavaRef<jstring> j_title;
ScopedLocalJavaRef<jstring> j_artist;
ScopedLocalJavaRef<jstring> j_album;
ScopedLocalJavaRef<jobjectArray> j_artwork;
if (session_state.has_metadata()) {
const MediaMetadataInit& media_metadata(session_state.metadata().value());
j_title.Reset(
env->NewStringStandardUTFOrAbort(media_metadata.title().c_str()));
j_artist.Reset(
env->NewStringStandardUTFOrAbort(media_metadata.artist().c_str()));
j_album.Reset(
env->NewStringStandardUTFOrAbort(media_metadata.album().c_str()));
if (media_metadata.has_artwork()) {
const MediaImageSequence& artwork(media_metadata.artwork());
ScopedLocalJavaRef<jclass> media_image_class(
env->FindClassExtOrAbort("dev/cobalt/media/MediaImage"));
jmethodID media_image_constructor = env->GetMethodID(
media_image_class.Get(), "<init>",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
env->AbortOnException();
j_artwork.Reset(static_cast<jobjectArray>(env->NewObjectArray(
artwork.size(), media_image_class.Get(), NULL)));
env->AbortOnException();
ScopedLocalJavaRef<jstring> j_src;
ScopedLocalJavaRef<jstring> j_sizes;
ScopedLocalJavaRef<jstring> j_type;
for (MediaImageSequence::size_type i = 0; i < artwork.size(); i++) {
const MediaImage& media_image(artwork.at(i));
j_src.Reset(
env->NewStringStandardUTFOrAbort(media_image.src().c_str()));
j_sizes.Reset(
env->NewStringStandardUTFOrAbort(media_image.sizes().c_str()));
j_type.Reset(
env->NewStringStandardUTFOrAbort(media_image.type().c_str()));
ScopedLocalJavaRef<jobject> j_media_image(
env->NewObject(media_image_class.Get(), media_image_constructor,
j_src.Get(), j_sizes.Get(), j_type.Get()));
env->SetObjectArrayElement(j_artwork.Get(), i, j_media_image.Get());
}
}
}
jlong duration = session_state.duration();
// Set duration to negative if duration is unknown or infinite, as with live
// playback.
// https://developer.android.com/reference/android/support/v4/media/MediaMetadataCompat#METADATA_KEY_DURATION
if (duration == kSbTimeMax) {
duration = -1;
}
env->CallStarboardVoidMethodOrAbort(
"updateMediaSession",
"(IJJFLjava/lang/String;Ljava/lang/String;Ljava/lang/String;"
"[Ldev/cobalt/media/MediaImage;J)V",
playback_state, playback_state_actions,
session_state.current_playback_position() / kSbTimeMillisecond,
static_cast<jfloat>(session_state.actual_playback_rate()),
j_title.Get(), j_artist.Get(), j_album.Get(), j_artwork.Get(),
duration);
}
};
SbOnceControl AndroidMediaSessionClient::once_flag = SB_ONCE_INITIALIZER;
SbMutex AndroidMediaSessionClient::mutex;
AndroidMediaSessionClient* AndroidMediaSessionClient::active_client = NULL;
void UpdateActiveSessionPlatformPlaybackState(PlaybackState state) {
MediaSessionPlaybackState media_session_state =
PlaybackStateToMediaSessionPlaybackState(state);
AndroidMediaSessionClient::UpdateActiveSessionPlatformPlaybackState(
media_session_state);
}
} // namespace cobalt
} // namespace shared
} // namespace android
} // namespace starboard
using starboard::android::shared::cobalt::AndroidMediaSessionClient;
extern "C" SB_EXPORT_PLATFORM
void Java_dev_cobalt_media_CobaltMediaSession_nativeInvokeAction(
JNIEnv* env,
jclass unused_clazz,
jlong action,
jlong seek_ms) {
AndroidMediaSessionClient::NativeInvokeAction(action, seek_ms);
}
namespace cobalt {
namespace media_session {
// static
std::unique_ptr<MediaSessionClient> MediaSessionClient::Create() {
return std::unique_ptr<MediaSessionClient>(new AndroidMediaSessionClient());
}
} // namespace media_session
} // namespace cobalt