/*
* Copyright 2018 The Android Open Source Project
*
* 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.
*/
package androidx.mediarouter.media;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.ArrayMap;
import androidx.collection.SimpleArrayMap;
import androidx.media2.BaseMediaPlayer;
import androidx.media2.BaseMediaPlayer.PlayerEventCallback;
import androidx.media2.BaseRemoteMediaPlayer;
import androidx.media2.DataSourceDesc2;
import androidx.media2.MediaSession2;
import androidx.mediarouter.media.MediaRouter.RouteInfo;
import java.util.concurrent.Executor;
/**
* RouteMediaPlayer is the abstract class representing playback happens on the outside of the
* device through {@link MediaRouter}
* <p>
* If you use this to the {@link MediaSession2} followings would happen.
* <ul>
* <li>Session wouldn't handle audio focus</li>
* <li>Session would dispatch volume change event to the player instead of changing device
* volume</li>
* </ul>
*/
public abstract class RouteMediaPlayer extends BaseRemoteMediaPlayer {
@SuppressWarnings("WeakerAccess") /* synthetic access */
final Object mLock = new Object();
@GuardedBy("mLock")
private final ArrayMap<PlayerEventCallback, Executor> mCallbacks = new ArrayMap<>();
@GuardedBy("mLock")
private final Handler mHandler = new Handler(Looper.getMainLooper());
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
RouteInfo mRoute;
/**
* Updates routes info. If the same route has already set, it only notifies volume changes.
*
* @param route route
*/
public final void updateRouteInfo(@Nullable RouteInfo route) {
synchronized (mLock) {
if (mRoute != route) {
mHandler.removeCallbacksAndMessages(null);
mRoute = route;
} else {
notifyPlayerVolumeChanged();
}
}
}
@Override
public final float getMaxPlayerVolume() {
synchronized (mLock) {
if (mRoute != null) {
return mRoute.getVolumeMax();
}
}
return 1.0f;
}
@Override
public final float getPlayerVolume() {
synchronized (mLock) {
if (mRoute != null) {
return mRoute.getVolume();
}
}
return 1.0f;
}
@Override
public final void adjustPlayerVolume(final int direction) {
synchronized (mLock) {
if (mRoute != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
synchronized (mLock) {
if (mRoute != null) {
mRoute.requestUpdateVolume(direction);
}
}
}
});
}
}
}
@Override
public final void setPlayerVolume(final float volume) {
synchronized (mLock) {
if (mRoute != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
synchronized (mLock) {
if (mRoute != null) {
mRoute.requestSetVolume((int) (volume + 0.5f));
}
}
}
});
}
}
}
@Override
public final @VolumeControlType int getVolumeControlType() {
synchronized (mLock) {
if (mRoute != null) {
switch (mRoute.getVolumeHandling()) {
case RouteInfo.PLAYBACK_VOLUME_FIXED:
return VOLUME_CONTROL_FIXED;
case RouteInfo.PLAYBACK_VOLUME_VARIABLE:
return VOLUME_CONTROL_ABSOLUTE;
}
}
}
return VOLUME_CONTROL_FIXED;
}
/**
* Adds a callback to be notified of events for this player.
*
* @param executor the {@link Executor} to be used for the events.
* @param callback the callback to receive the events.
*/
@Override
public final void registerPlayerEventCallback(@NonNull Executor executor,
@NonNull PlayerEventCallback callback) {
if (executor == null) {
throw new IllegalArgumentException("executor shouldn't be null");
}
if (callback == null) {
throw new IllegalArgumentException("callback shouldn't be null");
}
synchronized (mLock) {
mCallbacks.put(callback, executor);
}
}
/**
* Removes a previously registered callback for player events.
*
* @param callback the callback to remove
*/
@Override
public final void unregisterPlayerEventCallback(@NonNull PlayerEventCallback callback) {
if (callback == null) {
throw new IllegalArgumentException("callback shouldn't be null");
}
synchronized (mLock) {
mCallbacks.remove(callback);
}
}
/**
* Notifies the current data source. Call this API when the current data soruce is changed.
* <p>
* Registered {@link PlayerEventCallback} would receive this event through the
* {@link PlayerEventCallback#onCurrentDataSourceChanged(BaseMediaPlayer, DataSourceDesc2)}.
*/
public final void notifyCurrentDataSourceChanged() {
notifyCurrentDataSourceChanged(getCurrentDataSource());
}
/**
* Notifies that the playback is completed. Call this API when no other source is about to be
* played next (i.e. playback reached the end of the list of sources to play).
* <p>
* Registered {@link PlayerEventCallback} would receive this event through the
* {@link PlayerEventCallback#onCurrentDataSourceChanged(BaseMediaPlayer, DataSourceDesc2)}
* with {@code null} {@link DataSourceDesc2}.
*/
public final void notifyPlaybackCompleted() {
notifyCurrentDataSourceChanged(null);
}
private void notifyCurrentDataSourceChanged(final DataSourceDesc2 dsd) {
SimpleArrayMap<PlayerEventCallback, Executor> callbacks = getCallbacks();
for (int i = 0; i < callbacks.size(); i++) {
final PlayerEventCallback callback = callbacks.keyAt(i);
final Executor executor = callbacks.valueAt(i);
executor.execute(new Runnable() {
@Override
public void run() {
callback.onCurrentDataSourceChanged(RouteMediaPlayer.this, dsd);
}
});
}
}
/**
* Notifies that a data source is prepared.
* <p>
* Registered {@link PlayerEventCallback} would receive this event through the
* {@link PlayerEventCallback#onCurrentDataSourceChanged(BaseMediaPlayer, DataSourceDesc2)}
* with {@code null} {@link DataSourceDesc2}.
*
* @param dsd prepared dsd
*/
public final void notifyMediaPrepared(final DataSourceDesc2 dsd) {
SimpleArrayMap<PlayerEventCallback, Executor> callbacks = getCallbacks();
for (int i = 0; i < callbacks.size(); i++) {
final PlayerEventCallback callback = callbacks.keyAt(i);
final Executor executor = callbacks.valueAt(i);
executor.execute(new Runnable() {
@Override
public void run() {
callback.onMediaPrepared(RouteMediaPlayer.this, dsd);
}
});
}
}
/**
* Notifies the current player state. Call this API when the current player state is changed.
* <p>
* Registered {@link PlayerEventCallback} would receive this event through the
* {@link PlayerEventCallback#onPlayerStateChanged(BaseMediaPlayer, int)}.
*/
public final void notifyPlayerStateChanged() {
SimpleArrayMap<PlayerEventCallback, Executor> callbacks = getCallbacks();
final @PlayerState int state = getPlayerState();
for (int i = 0; i < callbacks.size(); i++) {
final PlayerEventCallback callback = callbacks.keyAt(i);
final Executor executor = callbacks.valueAt(i);
executor.execute(new Runnable() {
@Override
public void run() {
callback.onPlayerStateChanged(RouteMediaPlayer.this, state);
}
});
}
}
/**
* Notifies that buffering state of a data source is changed.
* <p>
* Registered {@link PlayerEventCallback} would receive this event through the
* {@link PlayerEventCallback#onBufferingStateChanged(BaseMediaPlayer, DataSourceDesc2, int)}.
*
* @param dsd dsd to notify
* @param state new buffering state
*/
public final void notifyBufferingStateChanged(final DataSourceDesc2 dsd,
final @BuffState int state) {
SimpleArrayMap<PlayerEventCallback, Executor> callbacks = getCallbacks();
for (int i = 0; i < callbacks.size(); i++) {
final PlayerEventCallback callback = callbacks.keyAt(i);
final Executor executor = callbacks.valueAt(i);
executor.execute(new Runnable() {
@Override
public void run() {
callback.onBufferingStateChanged(RouteMediaPlayer.this, dsd, state);
}
});
}
}
/**
* Notifies the current playback speed. Call this API when the current playback speed is
* changed.
* <p>
* Registered {@link PlayerEventCallback} would receive this event through the
* {@link PlayerEventCallback#onPlaybackSpeedChanged(BaseMediaPlayer, float)}.
*/
public final void notifyPlaybackSpeedChanged() {
SimpleArrayMap<PlayerEventCallback, Executor> callbacks = getCallbacks();
final float speed = getPlaybackSpeed();
for (int i = 0; i < callbacks.size(); i++) {
final PlayerEventCallback callback = callbacks.keyAt(i);
final Executor executor = callbacks.valueAt(i);
executor.execute(new Runnable() {
@Override
public void run() {
callback.onPlaybackSpeedChanged(RouteMediaPlayer.this, speed);
}
});
}
}
/**
* Notifies that buffering state of a data source is changed.
* <p>
* Registered {@link PlayerEventCallback} would receive this event through the
* {@link PlayerEventCallback#onSeekCompleted(BaseMediaPlayer, long)}
*
* @param position seek position. May be differ with the position specified by
* {@link #seekTo(long)}.
*/
public final void notifySeekCompleted(final long position) {
SimpleArrayMap<PlayerEventCallback, Executor> callbacks = getCallbacks();
for (int i = 0; i < callbacks.size(); i++) {
final PlayerEventCallback callback = callbacks.keyAt(i);
final Executor executor = callbacks.valueAt(i);
executor.execute(new Runnable() {
@Override
public void run() {
callback.onSeekCompleted(RouteMediaPlayer.this, position);
}
});
}
}
/**
* Notifies the current player volume. Call this API when the current playback volume is
* changed.
*/
public final void notifyPlayerVolumeChanged() {
SimpleArrayMap<PlayerEventCallback, Executor> callbacks = getCallbacks();
final float volume = getPlayerVolume();
for (int i = 0; i < callbacks.size(); i++) {
if (!(callbacks.keyAt(i) instanceof RemotePlayerEventCallback)) {
continue;
}
final RemotePlayerEventCallback callback =
(RemotePlayerEventCallback) callbacks.keyAt(i);
final Executor executor = callbacks.valueAt(i);
if (callback instanceof RemotePlayerEventCallback) {
executor.execute(new Runnable() {
@Override
public void run() {
callback.onPlayerVolumeChanged(RouteMediaPlayer.this, volume);
}
});
}
}
}
private SimpleArrayMap<PlayerEventCallback, Executor> getCallbacks() {
SimpleArrayMap<PlayerEventCallback, Executor> callbacks = new SimpleArrayMap<>();
synchronized (mLock) {
callbacks.putAll(mCallbacks);
}
return callbacks;
}
}