mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2024-11-22 02:53:09 +01:00
Merge pull request #8678 from Stypox/media-session-ui
Create media session UI and fix player notification
This commit is contained in:
commit
75917c7f61
@ -54,7 +54,6 @@ import android.content.Intent;
|
|||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@ -99,14 +98,13 @@ import org.schabi.newpipe.player.event.PlayerEventListener;
|
|||||||
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
||||||
import org.schabi.newpipe.player.helper.AudioReactor;
|
import org.schabi.newpipe.player.helper.AudioReactor;
|
||||||
import org.schabi.newpipe.player.helper.LoadController;
|
import org.schabi.newpipe.player.helper.LoadController;
|
||||||
import org.schabi.newpipe.player.helper.MediaSessionManager;
|
|
||||||
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
||||||
|
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
||||||
import org.schabi.newpipe.player.notification.NotificationPlayerUi;
|
import org.schabi.newpipe.player.notification.NotificationPlayerUi;
|
||||||
import org.schabi.newpipe.player.playback.MediaSourceManager;
|
import org.schabi.newpipe.player.playback.MediaSourceManager;
|
||||||
import org.schabi.newpipe.player.playback.PlaybackListener;
|
import org.schabi.newpipe.player.playback.PlaybackListener;
|
||||||
import org.schabi.newpipe.player.playback.PlayerMediaSession;
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
|
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
|
||||||
@ -176,6 +174,7 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public static final int RENDERER_UNAVAILABLE = -1;
|
public static final int RENDERER_UNAVAILABLE = -1;
|
||||||
|
private static final String PICASSO_PLAYER_THUMBNAIL_TAG = "PICASSO_PLAYER_THUMBNAIL_TAG";
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Playback
|
// Playback
|
||||||
@ -196,7 +195,6 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
|
|
||||||
private ExoPlayer simpleExoPlayer;
|
private ExoPlayer simpleExoPlayer;
|
||||||
private AudioReactor audioReactor;
|
private AudioReactor audioReactor;
|
||||||
private MediaSessionManager mediaSessionManager;
|
|
||||||
|
|
||||||
@NonNull private final DefaultTrackSelector trackSelector;
|
@NonNull private final DefaultTrackSelector trackSelector;
|
||||||
@NonNull private final LoadController loadController;
|
@NonNull private final LoadController loadController;
|
||||||
@ -224,8 +222,8 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
// UIs, listeners and disposables
|
// UIs, listeners and disposables
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@SuppressWarnings("MemberName") // keep the unusual member name
|
@SuppressWarnings({"MemberName", "java:S116"}) // keep the unusual member name
|
||||||
private final PlayerUiList UIs = new PlayerUiList();
|
private final PlayerUiList UIs;
|
||||||
|
|
||||||
private BroadcastReceiver broadcastReceiver;
|
private BroadcastReceiver broadcastReceiver;
|
||||||
private IntentFilter intentFilter;
|
private IntentFilter intentFilter;
|
||||||
@ -235,6 +233,11 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
@NonNull private final SerialDisposable progressUpdateDisposable = new SerialDisposable();
|
@NonNull private final SerialDisposable progressUpdateDisposable = new SerialDisposable();
|
||||||
@NonNull private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable();
|
@NonNull private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable();
|
||||||
|
|
||||||
|
// This is the only listener we need for thumbnail loading, since there is always at most only
|
||||||
|
// one thumbnail being loaded at a time. This field is also here to maintain a strong reference,
|
||||||
|
// which would otherwise be garbage collected since Picasso holds weak references to targets.
|
||||||
|
@NonNull private final Target currentThumbnailTarget;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Utils
|
// Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@ -265,6 +268,17 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
|
|
||||||
videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver());
|
videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver());
|
||||||
audioResolver = new AudioPlaybackResolver(context, dataSource);
|
audioResolver = new AudioPlaybackResolver(context, dataSource);
|
||||||
|
|
||||||
|
currentThumbnailTarget = getCurrentThumbnailTarget();
|
||||||
|
|
||||||
|
// The UIs added here should always be present. They will be initialized when the player
|
||||||
|
// reaches the initialization step. Make sure the media session ui is before the
|
||||||
|
// notification ui in the UIs list, since the notification depends on the media session in
|
||||||
|
// PlayerUi#initPlayer(), and UIs.call() guarantees UI order is preserved.
|
||||||
|
UIs = new PlayerUiList(
|
||||||
|
new MediaSessionPlayerUi(this),
|
||||||
|
new NotificationPlayerUi(this)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private VideoPlaybackResolver.QualityResolver getQualityResolver() {
|
private VideoPlaybackResolver.QualityResolver getQualityResolver() {
|
||||||
@ -431,11 +445,6 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initUIsForCurrentPlayerType() {
|
private void initUIsForCurrentPlayerType() {
|
||||||
//noinspection SimplifyOptionalCallChains
|
|
||||||
if (!UIs.get(NotificationPlayerUi.class).isPresent()) {
|
|
||||||
UIs.addAndPrepare(new NotificationPlayerUi(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((UIs.get(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN)
|
if ((UIs.get(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN)
|
||||||
|| (UIs.get(PopupPlayerUi.class).isPresent() && playerType == PlayerType.POPUP)) {
|
|| (UIs.get(PopupPlayerUi.class).isPresent() && playerType == PlayerType.POPUP)) {
|
||||||
// correct UI already in place
|
// correct UI already in place
|
||||||
@ -506,8 +515,6 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
simpleExoPlayer.setHandleAudioBecomingNoisy(true);
|
simpleExoPlayer.setHandleAudioBecomingNoisy(true);
|
||||||
|
|
||||||
audioReactor = new AudioReactor(context, simpleExoPlayer);
|
audioReactor = new AudioReactor(context, simpleExoPlayer);
|
||||||
mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer,
|
|
||||||
new PlayerMediaSession(this));
|
|
||||||
|
|
||||||
registerBroadcastReceiver();
|
registerBroadcastReceiver();
|
||||||
|
|
||||||
@ -558,9 +565,6 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
if (playQueueManager != null) {
|
if (playQueueManager != null) {
|
||||||
playQueueManager.dispose();
|
playQueueManager.dispose();
|
||||||
}
|
}
|
||||||
if (mediaSessionManager != null) {
|
|
||||||
mediaSessionManager.dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void destroy() {
|
public void destroy() {
|
||||||
@ -577,7 +581,7 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
|
|
||||||
databaseUpdateDisposable.clear();
|
databaseUpdateDisposable.clear();
|
||||||
progressUpdateDisposable.set(null);
|
progressUpdateDisposable.set(null);
|
||||||
PicassoHelper.cancelTag(PicassoHelper.PLAYER_THUMBNAIL_TAG); // cancel thumbnail loading
|
cancelLoadingCurrentThumbnail();
|
||||||
|
|
||||||
UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object
|
UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object
|
||||||
}
|
}
|
||||||
@ -723,11 +727,6 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
Log.d(TAG, "ACTION_CONFIGURATION_CHANGED received");
|
Log.d(TAG, "ACTION_CONFIGURATION_CHANGED received");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Intent.ACTION_HEADSET_PLUG: //FIXME
|
|
||||||
/*notificationManager.cancel(NOTIFICATION_ID);
|
|
||||||
mediaSessionManager.dispose();
|
|
||||||
mediaSessionManager.enable(getBaseContext(), basePlayerImpl.simpleExoPlayer);*/
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UIs.call(playerUi -> playerUi.onBroadcastReceived(intent));
|
UIs.call(playerUi -> playerUi.onBroadcastReceived(intent));
|
||||||
@ -756,44 +755,63 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region Thumbnail loading
|
//region Thumbnail loading
|
||||||
|
|
||||||
private void initThumbnail(final String url) {
|
private Target getCurrentThumbnailTarget() {
|
||||||
if (DEBUG) {
|
// a Picasso target is just a listener for thumbnail loading events
|
||||||
Log.d(TAG, "Thumbnail - initThumbnail() called with url = ["
|
return new Target() {
|
||||||
+ (url == null ? "null" : url) + "]");
|
|
||||||
}
|
|
||||||
if (isNullOrEmpty(url)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// scale down the notification thumbnail for performance
|
|
||||||
PicassoHelper.loadScaledDownThumbnail(context, url).into(new Target() {
|
|
||||||
@Override
|
@Override
|
||||||
public void onBitmapLoaded(final Bitmap bitmap, final Picasso.LoadedFrom from) {
|
public void onBitmapLoaded(final Bitmap bitmap, final Picasso.LoadedFrom from) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Thumbnail - onLoadingComplete() called with: url = [" + url
|
Log.d(TAG, "Thumbnail - onBitmapLoaded() called with: bitmap = [" + bitmap
|
||||||
+ "], " + "loadedImage = [" + bitmap + " -> " + bitmap.getWidth() + "x"
|
+ " -> " + bitmap.getWidth() + "x" + bitmap.getHeight() + "], from = ["
|
||||||
+ bitmap.getHeight() + "], from = [" + from + "]");
|
+ from + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
currentThumbnail = bitmap;
|
currentThumbnail = bitmap;
|
||||||
// there is a new thumbnail, so changed the end screen thumbnail, too.
|
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
|
||||||
UIs.call(playerUi -> playerUi.onThumbnailLoaded(bitmap));
|
UIs.call(playerUi -> playerUi.onThumbnailLoaded(bitmap));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBitmapFailed(final Exception e, final Drawable errorDrawable) {
|
public void onBitmapFailed(final Exception e, final Drawable errorDrawable) {
|
||||||
Log.e(TAG, "Thumbnail - onBitmapFailed() called: url = [" + url + "]", e);
|
Log.e(TAG, "Thumbnail - onBitmapFailed() called", e);
|
||||||
currentThumbnail = null;
|
currentThumbnail = null;
|
||||||
|
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
|
||||||
UIs.call(playerUi -> playerUi.onThumbnailLoaded(null));
|
UIs.call(playerUi -> playerUi.onThumbnailLoaded(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPrepareLoad(final Drawable placeHolderDrawable) {
|
public void onPrepareLoad(final Drawable placeHolderDrawable) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Thumbnail - onPrepareLoad() called: url = [" + url + "]");
|
Log.d(TAG, "Thumbnail - onPrepareLoad() called");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadCurrentThumbnail(final String url) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Thumbnail - loadCurrentThumbnail() called with url = ["
|
||||||
|
+ (url == null ? "null" : url) + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
// first cancel any previous loading
|
||||||
|
cancelLoadingCurrentThumbnail();
|
||||||
|
|
||||||
|
// Unset currentThumbnail, since it is now outdated. This ensures it is not used in media
|
||||||
|
// session metadata while the new thumbnail is being loaded by Picasso.
|
||||||
|
currentThumbnail = null;
|
||||||
|
if (isNullOrEmpty(url)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// scale down the notification thumbnail for performance
|
||||||
|
PicassoHelper.loadScaledDownThumbnail(context, url)
|
||||||
|
.tag(PICASSO_PLAYER_THUMBNAIL_TAG)
|
||||||
|
.into(currentThumbnailTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelLoadingCurrentThumbnail() {
|
||||||
|
// cancel the Picasso job associated with the player thumbnail, if any
|
||||||
|
PicassoHelper.cancelTag(PICASSO_PLAYER_THUMBNAIL_TAG);
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
@ -1735,18 +1753,9 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
|
|
||||||
maybeAutoQueueNextStream(info);
|
maybeAutoQueueNextStream(info);
|
||||||
|
|
||||||
initThumbnail(info.getThumbnailUrl());
|
loadCurrentThumbnail(info.getThumbnailUrl());
|
||||||
registerStreamViewed();
|
registerStreamViewed();
|
||||||
|
|
||||||
final boolean showThumbnail = prefs.getBoolean(
|
|
||||||
context.getString(R.string.show_thumbnail_key), true);
|
|
||||||
mediaSessionManager.setMetadata(
|
|
||||||
getVideoTitle(),
|
|
||||||
getUploaderName(),
|
|
||||||
showThumbnail ? Optional.ofNullable(getThumbnail()) : Optional.empty(),
|
|
||||||
StreamTypeUtil.isLiveStream(info.getStreamType()) ? -1 : info.getDuration()
|
|
||||||
);
|
|
||||||
|
|
||||||
notifyMetadataUpdateToListeners();
|
notifyMetadataUpdateToListeners();
|
||||||
UIs.call(playerUi -> playerUi.onMetadataChanged(info));
|
UIs.call(playerUi -> playerUi.onMetadataChanged(info));
|
||||||
}
|
}
|
||||||
@ -1786,10 +1795,6 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public Bitmap getThumbnail() {
|
public Bitmap getThumbnail() {
|
||||||
if (currentThumbnail == null) {
|
|
||||||
currentThumbnail = BitmapFactory.decodeResource(
|
|
||||||
context.getResources(), R.drawable.placeholder_thumbnail_video);
|
|
||||||
}
|
|
||||||
return currentThumbnail;
|
return currentThumbnail;
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
@ -2194,10 +2199,6 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
return prefs;
|
return prefs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MediaSessionManager getMediaSessionManager() {
|
|
||||||
return mediaSessionManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public PlayerType getPlayerType() {
|
public PlayerType getPlayerType() {
|
||||||
return playerType;
|
return playerType;
|
||||||
|
@ -28,6 +28,7 @@ import android.os.Binder;
|
|||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
|
|
||||||
@ -73,9 +74,8 @@ public final class PlayerService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
player.handleIntent(intent);
|
player.handleIntent(intent);
|
||||||
if (player.getMediaSessionManager() != null) {
|
player.UIs().get(MediaSessionPlayerUi.class)
|
||||||
player.getMediaSessionManager().handleMediaButtonIntent(intent);
|
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
|
||||||
}
|
|
||||||
|
|
||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
@ -1,226 +0,0 @@
|
|||||||
package org.schabi.newpipe.player.helper;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.support.v4.media.MediaMetadataCompat;
|
|
||||||
import android.support.v4.media.session.MediaSessionCompat;
|
|
||||||
import android.support.v4.media.session.PlaybackStateCompat;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.media.session.MediaButtonReceiver;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.ForwardingPlayer;
|
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
|
||||||
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
|
|
||||||
import org.schabi.newpipe.player.mediasession.PlayQueueNavigator;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class MediaSessionManager {
|
|
||||||
private static final String TAG = MediaSessionManager.class.getSimpleName();
|
|
||||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final MediaSessionCompat mediaSession;
|
|
||||||
@NonNull
|
|
||||||
private final MediaSessionConnector sessionConnector;
|
|
||||||
|
|
||||||
private int lastTitleHashCode;
|
|
||||||
private int lastArtistHashCode;
|
|
||||||
private long lastDuration;
|
|
||||||
private int lastAlbumArtHashCode;
|
|
||||||
|
|
||||||
public MediaSessionManager(@NonNull final Context context,
|
|
||||||
@NonNull final Player player,
|
|
||||||
@NonNull final MediaSessionCallback callback) {
|
|
||||||
mediaSession = new MediaSessionCompat(context, TAG);
|
|
||||||
mediaSession.setActive(true);
|
|
||||||
|
|
||||||
mediaSession.setPlaybackState(new PlaybackStateCompat.Builder()
|
|
||||||
.setState(PlaybackStateCompat.STATE_NONE, -1, 1)
|
|
||||||
.setActions(PlaybackStateCompat.ACTION_SEEK_TO
|
|
||||||
| PlaybackStateCompat.ACTION_PLAY
|
|
||||||
| PlaybackStateCompat.ACTION_PAUSE // was play and pause now play/pause
|
|
||||||
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT
|
|
||||||
| PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
|
|
||||||
| PlaybackStateCompat.ACTION_SET_REPEAT_MODE
|
|
||||||
| PlaybackStateCompat.ACTION_STOP)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
sessionConnector = new MediaSessionConnector(mediaSession);
|
|
||||||
sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback));
|
|
||||||
sessionConnector.setPlayer(new ForwardingPlayer(player) {
|
|
||||||
@Override
|
|
||||||
public void play() {
|
|
||||||
callback.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void pause() {
|
|
||||||
callback.pause();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@SuppressWarnings("UnusedReturnValue")
|
|
||||||
public KeyEvent handleMediaButtonIntent(final Intent intent) {
|
|
||||||
return MediaButtonReceiver.handleIntent(mediaSession, intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MediaSessionCompat.Token getSessionToken() {
|
|
||||||
return mediaSession.getSessionToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* sets the Metadata - if required.
|
|
||||||
*
|
|
||||||
* @param title {@link MediaMetadataCompat#METADATA_KEY_TITLE}
|
|
||||||
* @param artist {@link MediaMetadataCompat#METADATA_KEY_ARTIST}
|
|
||||||
* @param optAlbumArt {@link MediaMetadataCompat#METADATA_KEY_ALBUM_ART}
|
|
||||||
* @param duration {@link MediaMetadataCompat#METADATA_KEY_DURATION}
|
|
||||||
* - should be a negative value for unknown durations, e.g. for livestreams
|
|
||||||
*/
|
|
||||||
public void setMetadata(@NonNull final String title,
|
|
||||||
@NonNull final String artist,
|
|
||||||
@NonNull final Optional<Bitmap> optAlbumArt,
|
|
||||||
final long duration
|
|
||||||
) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "setMetadata called:"
|
|
||||||
+ " t: " + title
|
|
||||||
+ " a: " + artist
|
|
||||||
+ " thumb: " + (
|
|
||||||
optAlbumArt.isPresent()
|
|
||||||
? optAlbumArt.get().hashCode()
|
|
||||||
: "<none>")
|
|
||||||
+ " d: " + duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mediaSession.isActive()) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "setMetadata: mediaSession not active - exiting");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!checkIfMetadataShouldBeSet(title, artist, optAlbumArt, duration)) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "setMetadata: No update required - exiting");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "setMetadata: N_Metadata update:"
|
|
||||||
+ " t: " + title
|
|
||||||
+ " a: " + artist
|
|
||||||
+ " thumb: " + (
|
|
||||||
optAlbumArt.isPresent()
|
|
||||||
? optAlbumArt.get().hashCode()
|
|
||||||
: "<none>")
|
|
||||||
+ " d: " + duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
final MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder()
|
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
|
|
||||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration);
|
|
||||||
|
|
||||||
if (optAlbumArt.isPresent()) {
|
|
||||||
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, optAlbumArt.get());
|
|
||||||
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, optAlbumArt.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaSession.setMetadata(builder.build());
|
|
||||||
|
|
||||||
lastTitleHashCode = title.hashCode();
|
|
||||||
lastArtistHashCode = artist.hashCode();
|
|
||||||
lastDuration = duration;
|
|
||||||
optAlbumArt.ifPresent(bitmap -> lastAlbumArtHashCode = bitmap.hashCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean checkIfMetadataShouldBeSet(
|
|
||||||
@NonNull final String title,
|
|
||||||
@NonNull final String artist,
|
|
||||||
@NonNull final Optional<Bitmap> optAlbumArt,
|
|
||||||
final long duration
|
|
||||||
) {
|
|
||||||
// Check if the values have changed since the last time
|
|
||||||
if (title.hashCode() != lastTitleHashCode
|
|
||||||
|| artist.hashCode() != lastArtistHashCode
|
|
||||||
|| duration != lastDuration
|
|
||||||
|| (optAlbumArt.isPresent() && optAlbumArt.get().hashCode() != lastAlbumArtHashCode)
|
|
||||||
) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG,
|
|
||||||
"checkIfMetadataShouldBeSet: true - reason: changed values since last");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the currently set metadata is valid
|
|
||||||
if (getMetadataTitle() == null
|
|
||||||
|| getMetadataArtist() == null
|
|
||||||
// Note that the duration can be <= 0 for live streams
|
|
||||||
) {
|
|
||||||
if (DEBUG) {
|
|
||||||
if (getMetadataTitle() == null) {
|
|
||||||
Log.d(TAG,
|
|
||||||
"N_getMetadataTitle: title == null");
|
|
||||||
} else if (getMetadataArtist() == null) {
|
|
||||||
Log.d(TAG,
|
|
||||||
"N_getMetadataArtist: artist == null");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we got an album art check if the current set AlbumArt is null
|
|
||||||
if (optAlbumArt.isPresent() && getMetadataAlbumArt() == null) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "N_getMetadataAlbumArt: thumb == null");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default - no update required
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private Bitmap getMetadataAlbumArt() {
|
|
||||||
return mediaSession.getController().getMetadata()
|
|
||||||
.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private String getMetadataTitle() {
|
|
||||||
return mediaSession.getController().getMetadata()
|
|
||||||
.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private String getMetadataArtist() {
|
|
||||||
return mediaSession.getController().getMetadata()
|
|
||||||
.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should be called on player destruction to prevent leakage.
|
|
||||||
*/
|
|
||||||
public void dispose() {
|
|
||||||
sessionConnector.setPlayer(null);
|
|
||||||
sessionConnector.setQueueNavigator(null);
|
|
||||||
mediaSession.setActive(false);
|
|
||||||
mediaSession.release();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
package org.schabi.newpipe.player.mediasession;
|
|
||||||
|
|
||||||
import android.support.v4.media.MediaDescriptionCompat;
|
|
||||||
|
|
||||||
public interface MediaSessionCallback {
|
|
||||||
void playPrevious();
|
|
||||||
|
|
||||||
void playNext();
|
|
||||||
|
|
||||||
void playItemAtIndex(int index);
|
|
||||||
|
|
||||||
int getCurrentPlayingIndex();
|
|
||||||
|
|
||||||
int getQueueSize();
|
|
||||||
|
|
||||||
MediaDescriptionCompat getQueueMetadata(int index);
|
|
||||||
|
|
||||||
void play();
|
|
||||||
|
|
||||||
void pause();
|
|
||||||
}
|
|
@ -0,0 +1,134 @@
|
|||||||
|
package org.schabi.newpipe.player.mediasession;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.support.v4.media.MediaMetadataCompat;
|
||||||
|
import android.support.v4.media.session.MediaSessionCompat;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media.session.MediaButtonReceiver;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.ForwardingPlayer;
|
||||||
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.player.Player;
|
||||||
|
import org.schabi.newpipe.player.ui.PlayerUi;
|
||||||
|
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||||
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class MediaSessionPlayerUi extends PlayerUi {
|
||||||
|
private static final String TAG = "MediaSessUi";
|
||||||
|
|
||||||
|
private MediaSessionCompat mediaSession;
|
||||||
|
private MediaSessionConnector sessionConnector;
|
||||||
|
|
||||||
|
public MediaSessionPlayerUi(@NonNull final Player player) {
|
||||||
|
super(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initPlayer() {
|
||||||
|
super.initPlayer();
|
||||||
|
destroyPlayer(); // release previously used resources
|
||||||
|
|
||||||
|
mediaSession = new MediaSessionCompat(context, TAG);
|
||||||
|
mediaSession.setActive(true);
|
||||||
|
|
||||||
|
sessionConnector = new MediaSessionConnector(mediaSession);
|
||||||
|
sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, player));
|
||||||
|
sessionConnector.setPlayer(getForwardingPlayer());
|
||||||
|
|
||||||
|
sessionConnector.setMetadataDeduplicationEnabled(true);
|
||||||
|
sessionConnector.setMediaMetadataProvider(exoPlayer -> buildMediaMetadata());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroyPlayer() {
|
||||||
|
super.destroyPlayer();
|
||||||
|
if (sessionConnector != null) {
|
||||||
|
sessionConnector.setPlayer(null);
|
||||||
|
sessionConnector.setQueueNavigator(null);
|
||||||
|
sessionConnector = null;
|
||||||
|
}
|
||||||
|
if (mediaSession != null) {
|
||||||
|
mediaSession.setActive(false);
|
||||||
|
mediaSession.release();
|
||||||
|
mediaSession = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
|
||||||
|
super.onThumbnailLoaded(bitmap);
|
||||||
|
if (sessionConnector != null) {
|
||||||
|
// the thumbnail is now loaded: invalidate the metadata to trigger a metadata update
|
||||||
|
sessionConnector.invalidateMediaSessionMetadata();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void handleMediaButtonIntent(final Intent intent) {
|
||||||
|
MediaButtonReceiver.handleIntent(mediaSession, intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<MediaSessionCompat.Token> getSessionToken() {
|
||||||
|
return Optional.ofNullable(mediaSession).map(MediaSessionCompat::getSessionToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private ForwardingPlayer getForwardingPlayer() {
|
||||||
|
// ForwardingPlayer means that all media session actions called on this player are
|
||||||
|
// forwarded directly to the connected exoplayer, except for the overridden methods. So
|
||||||
|
// override play and pause since our player adds more functionality to them over exoplayer.
|
||||||
|
return new ForwardingPlayer(player.getExoPlayer()) {
|
||||||
|
@Override
|
||||||
|
public void play() {
|
||||||
|
player.play();
|
||||||
|
// hide the player controls even if the play command came from the media session
|
||||||
|
player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void pause() {
|
||||||
|
player.pause();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaMetadataCompat buildMediaMetadata() {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "buildMediaMetadata called");
|
||||||
|
}
|
||||||
|
|
||||||
|
// set title and artist
|
||||||
|
final MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder()
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, player.getVideoTitle())
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, player.getUploaderName());
|
||||||
|
|
||||||
|
// set duration (-1 for livestreams or if unknown, see the METADATA_KEY_DURATION docs)
|
||||||
|
final long duration = player.getCurrentStreamInfo()
|
||||||
|
.filter(info -> !StreamTypeUtil.isLiveStream(info.getStreamType()))
|
||||||
|
.map(info -> info.getDuration() * 1000L)
|
||||||
|
.orElse(-1L);
|
||||||
|
builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration);
|
||||||
|
|
||||||
|
// set album art, unless the user asked not to, or there is no thumbnail available
|
||||||
|
final boolean showThumbnail = player.getPrefs().getBoolean(
|
||||||
|
context.getString(R.string.show_thumbnail_key), true);
|
||||||
|
Optional.ofNullable(player.getThumbnail())
|
||||||
|
.filter(bitmap -> showThumbnail)
|
||||||
|
.ifPresent(bitmap -> {
|
||||||
|
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap);
|
||||||
|
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, bitmap);
|
||||||
|
});
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
}
|
@ -1,106 +1,152 @@
|
|||||||
package org.schabi.newpipe.player.mediasession;
|
package org.schabi.newpipe.player.mediasession;
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.ResultReceiver;
|
|
||||||
import android.support.v4.media.session.MediaSessionCompat;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
|
||||||
import com.google.android.exoplayer2.util.Util;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
|
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
|
||||||
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
|
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
|
||||||
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
|
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.ResultReceiver;
|
||||||
|
import android.support.v4.media.MediaDescriptionCompat;
|
||||||
|
import android.support.v4.media.MediaMetadataCompat;
|
||||||
|
import android.support.v4.media.session.MediaSessionCompat;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.player.Player;
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator {
|
public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator {
|
||||||
public static final int DEFAULT_MAX_QUEUE_SIZE = 10;
|
private static final int MAX_QUEUE_SIZE = 10;
|
||||||
|
|
||||||
private final MediaSessionCompat mediaSession;
|
private final MediaSessionCompat mediaSession;
|
||||||
private final MediaSessionCallback callback;
|
private final Player player;
|
||||||
private final int maxQueueSize;
|
|
||||||
|
|
||||||
private long activeQueueItemId;
|
private long activeQueueItemId;
|
||||||
|
|
||||||
public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession,
|
public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession,
|
||||||
@NonNull final MediaSessionCallback callback) {
|
@NonNull final Player player) {
|
||||||
this.mediaSession = mediaSession;
|
this.mediaSession = mediaSession;
|
||||||
this.callback = callback;
|
this.player = player;
|
||||||
this.maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
|
|
||||||
|
|
||||||
this.activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
this.activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getSupportedQueueNavigatorActions(@Nullable final Player player) {
|
public long getSupportedQueueNavigatorActions(
|
||||||
|
@Nullable final com.google.android.exoplayer2.Player exoPlayer) {
|
||||||
return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM;
|
return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTimelineChanged(@NonNull final Player player) {
|
public void onTimelineChanged(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
|
||||||
publishFloatingQueueWindow();
|
publishFloatingQueueWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCurrentMediaItemIndexChanged(@NonNull final Player player) {
|
public void onCurrentMediaItemIndexChanged(
|
||||||
|
@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
|
||||||
if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID
|
if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID
|
||||||
|| player.getCurrentTimeline().getWindowCount() > maxQueueSize) {
|
|| exoPlayer.getCurrentTimeline().getWindowCount() > MAX_QUEUE_SIZE) {
|
||||||
publishFloatingQueueWindow();
|
publishFloatingQueueWindow();
|
||||||
} else if (!player.getCurrentTimeline().isEmpty()) {
|
} else if (!exoPlayer.getCurrentTimeline().isEmpty()) {
|
||||||
activeQueueItemId = player.getCurrentMediaItemIndex();
|
activeQueueItemId = exoPlayer.getCurrentMediaItemIndex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getActiveQueueItemId(@Nullable final Player player) {
|
public long getActiveQueueItemId(
|
||||||
return callback.getCurrentPlayingIndex();
|
@Nullable final com.google.android.exoplayer2.Player exoPlayer) {
|
||||||
|
return Optional.ofNullable(player.getPlayQueue()).map(PlayQueue::getIndex).orElse(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSkipToPrevious(@NonNull final Player player) {
|
public void onSkipToPrevious(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
|
||||||
callback.playPrevious();
|
player.playPrevious();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSkipToQueueItem(@NonNull final Player player, final long id) {
|
public void onSkipToQueueItem(@NonNull final com.google.android.exoplayer2.Player exoPlayer,
|
||||||
callback.playItemAtIndex((int) id);
|
final long id) {
|
||||||
|
if (player.getPlayQueue() != null) {
|
||||||
|
player.selectQueueItem(player.getPlayQueue().getItem((int) id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSkipToNext(@NonNull final Player player) {
|
public void onSkipToNext(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
|
||||||
callback.playNext();
|
player.playNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void publishFloatingQueueWindow() {
|
private void publishFloatingQueueWindow() {
|
||||||
if (callback.getQueueSize() == 0) {
|
final int windowCount = Optional.ofNullable(player.getPlayQueue())
|
||||||
|
.map(PlayQueue::size)
|
||||||
|
.orElse(0);
|
||||||
|
if (windowCount == 0) {
|
||||||
mediaSession.setQueue(Collections.emptyList());
|
mediaSession.setQueue(Collections.emptyList());
|
||||||
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Yes this is almost a copypasta, got a problem with that? =\
|
// Yes this is almost a copypasta, got a problem with that? =\
|
||||||
final int windowCount = callback.getQueueSize();
|
final int currentWindowIndex = player.getPlayQueue().getIndex();
|
||||||
final int currentWindowIndex = callback.getCurrentPlayingIndex();
|
final int queueSize = Math.min(MAX_QUEUE_SIZE, windowCount);
|
||||||
final int queueSize = Math.min(maxQueueSize, windowCount);
|
|
||||||
final int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0,
|
final int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0,
|
||||||
windowCount - queueSize);
|
windowCount - queueSize);
|
||||||
|
|
||||||
final List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
|
final List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
|
||||||
for (int i = startIndex; i < startIndex + queueSize; i++) {
|
for (int i = startIndex; i < startIndex + queueSize; i++) {
|
||||||
queue.add(new MediaSessionCompat.QueueItem(callback.getQueueMetadata(i), i));
|
queue.add(new MediaSessionCompat.QueueItem(getQueueMetadata(i), i));
|
||||||
}
|
}
|
||||||
mediaSession.setQueue(queue);
|
mediaSession.setQueue(queue);
|
||||||
activeQueueItemId = currentWindowIndex;
|
activeQueueItemId = currentWindowIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MediaDescriptionCompat getQueueMetadata(final int index) {
|
||||||
|
if (player.getPlayQueue() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final PlayQueueItem item = player.getPlayQueue().getItem(index);
|
||||||
|
if (item == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final MediaDescriptionCompat.Builder descBuilder = new MediaDescriptionCompat.Builder()
|
||||||
|
.setMediaId(String.valueOf(index))
|
||||||
|
.setTitle(item.getTitle())
|
||||||
|
.setSubtitle(item.getUploader());
|
||||||
|
|
||||||
|
// set additional metadata for A2DP/AVRCP (Audio/Video Bluetooth profiles)
|
||||||
|
final Bundle additionalMetadata = new Bundle();
|
||||||
|
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, item.getTitle());
|
||||||
|
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, item.getUploader());
|
||||||
|
additionalMetadata
|
||||||
|
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration() * 1000);
|
||||||
|
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, index + 1L);
|
||||||
|
additionalMetadata
|
||||||
|
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
|
||||||
|
descBuilder.setExtras(additionalMetadata);
|
||||||
|
|
||||||
|
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
|
||||||
|
if (thumbnailUri != null) {
|
||||||
|
descBuilder.setIconUri(thumbnailUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
return descBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCommand(@NonNull final Player player,
|
public boolean onCommand(@NonNull final com.google.android.exoplayer2.Player exoPlayer,
|
||||||
@NonNull final String command,
|
@NonNull final String command,
|
||||||
@Nullable final Bundle extras,
|
@Nullable final Bundle extras,
|
||||||
@Nullable final ResultReceiver cb) {
|
@Nullable final ResultReceiver cb) {
|
||||||
|
@ -9,6 +9,7 @@ import android.os.Build;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes;
|
import androidx.annotation.DrawableRes;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
@ -19,11 +20,13 @@ import androidx.core.content.ContextCompat;
|
|||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
|
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
import static androidx.media.app.NotificationCompat.MediaStyle;
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
|
||||||
@ -101,9 +104,13 @@ public final class NotificationUtil {
|
|||||||
player.getContext(), player.getPrefs(), nonNothingSlotCount);
|
player.getContext(), player.getPrefs(), nonNothingSlotCount);
|
||||||
final int[] compactSlots = compactSlotList.stream().mapToInt(Integer::intValue).toArray();
|
final int[] compactSlots = compactSlotList.stream().mapToInt(Integer::intValue).toArray();
|
||||||
|
|
||||||
builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
|
final MediaStyle mediaStyle = new MediaStyle().setShowActionsInCompactView(compactSlots);
|
||||||
.setMediaSession(player.getMediaSessionManager().getSessionToken())
|
player.UIs()
|
||||||
.setShowActionsInCompactView(compactSlots))
|
.get(MediaSessionPlayerUi.class)
|
||||||
|
.flatMap(MediaSessionPlayerUi::getSessionToken)
|
||||||
|
.ifPresent(mediaStyle::setMediaSession);
|
||||||
|
|
||||||
|
builder.setStyle(mediaStyle)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
|
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
|
||||||
@ -133,13 +140,10 @@ public final class NotificationUtil {
|
|||||||
notificationBuilder.setContentTitle(player.getVideoTitle());
|
notificationBuilder.setContentTitle(player.getVideoTitle());
|
||||||
notificationBuilder.setContentText(player.getUploaderName());
|
notificationBuilder.setContentText(player.getUploaderName());
|
||||||
notificationBuilder.setTicker(player.getVideoTitle());
|
notificationBuilder.setTicker(player.getVideoTitle());
|
||||||
|
|
||||||
updateActions(notificationBuilder);
|
updateActions(notificationBuilder);
|
||||||
final boolean showThumbnail = player.getPrefs().getBoolean(
|
|
||||||
player.getContext().getString(R.string.show_thumbnail_key), true);
|
|
||||||
if (showThumbnail) {
|
|
||||||
setLargeIcon(notificationBuilder);
|
setLargeIcon(notificationBuilder);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
@ -340,17 +344,26 @@ public final class NotificationUtil {
|
|||||||
/////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void setLargeIcon(final NotificationCompat.Builder builder) {
|
private void setLargeIcon(final NotificationCompat.Builder builder) {
|
||||||
|
final boolean showThumbnail = player.getPrefs().getBoolean(
|
||||||
|
player.getContext().getString(R.string.show_thumbnail_key), true);
|
||||||
|
final Bitmap thumbnail = player.getThumbnail();
|
||||||
|
if (thumbnail == null || !showThumbnail) {
|
||||||
|
// since the builder is reused, make sure the thumbnail is unset if there is not one
|
||||||
|
builder.setLargeIcon(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final boolean scaleImageToSquareAspectRatio = player.getPrefs().getBoolean(
|
final boolean scaleImageToSquareAspectRatio = player.getPrefs().getBoolean(
|
||||||
player.getContext().getString(R.string.scale_to_square_image_in_notifications_key),
|
player.getContext().getString(R.string.scale_to_square_image_in_notifications_key),
|
||||||
false);
|
false);
|
||||||
if (scaleImageToSquareAspectRatio) {
|
if (scaleImageToSquareAspectRatio) {
|
||||||
builder.setLargeIcon(getBitmapWithSquareAspectRatio(player.getThumbnail()));
|
builder.setLargeIcon(getBitmapWithSquareAspectRatio(thumbnail));
|
||||||
} else {
|
} else {
|
||||||
builder.setLargeIcon(player.getThumbnail());
|
builder.setLargeIcon(thumbnail);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Bitmap getBitmapWithSquareAspectRatio(final Bitmap bitmap) {
|
private Bitmap getBitmapWithSquareAspectRatio(@NonNull final Bitmap bitmap) {
|
||||||
// Find the smaller dimension and then take a center portion of the image that
|
// Find the smaller dimension and then take a center portion of the image that
|
||||||
// has that size.
|
// has that size.
|
||||||
final int w = bitmap.getWidth();
|
final int w = bitmap.getWidth();
|
||||||
|
@ -1,99 +0,0 @@
|
|||||||
package org.schabi.newpipe.player.playback;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.v4.media.MediaDescriptionCompat;
|
|
||||||
import android.support.v4.media.MediaMetadataCompat;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.player.Player;
|
|
||||||
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
|
||||||
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
|
||||||
|
|
||||||
public class PlayerMediaSession implements MediaSessionCallback {
|
|
||||||
private final Player player;
|
|
||||||
|
|
||||||
public PlayerMediaSession(final Player player) {
|
|
||||||
this.player = player;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void playPrevious() {
|
|
||||||
player.playPrevious();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void playNext() {
|
|
||||||
player.playNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void playItemAtIndex(final int index) {
|
|
||||||
if (player.getPlayQueue() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
player.selectQueueItem(player.getPlayQueue().getItem(index));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCurrentPlayingIndex() {
|
|
||||||
if (player.getPlayQueue() == null) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return player.getPlayQueue().getIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getQueueSize() {
|
|
||||||
if (player.getPlayQueue() == null) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return player.getPlayQueue().size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MediaDescriptionCompat getQueueMetadata(final int index) {
|
|
||||||
if (player.getPlayQueue() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final PlayQueueItem item = player.getPlayQueue().getItem(index);
|
|
||||||
if (item == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final MediaDescriptionCompat.Builder descBuilder = new MediaDescriptionCompat.Builder()
|
|
||||||
.setMediaId(String.valueOf(index))
|
|
||||||
.setTitle(item.getTitle())
|
|
||||||
.setSubtitle(item.getUploader());
|
|
||||||
|
|
||||||
// set additional metadata for A2DP/AVRCP
|
|
||||||
final Bundle additionalMetadata = new Bundle();
|
|
||||||
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, item.getTitle());
|
|
||||||
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, item.getUploader());
|
|
||||||
additionalMetadata
|
|
||||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration() * 1000);
|
|
||||||
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, index + 1);
|
|
||||||
additionalMetadata
|
|
||||||
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
|
|
||||||
descBuilder.setExtras(additionalMetadata);
|
|
||||||
|
|
||||||
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
|
|
||||||
if (thumbnailUri != null) {
|
|
||||||
descBuilder.setIconUri(thumbnailUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
return descBuilder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void play() {
|
|
||||||
player.play();
|
|
||||||
// hide the player controls even if the play command came from the media session
|
|
||||||
player.UIs().get(VideoPlayerUi.class).ifPresent(playerUi -> playerUi.hideControls(0, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void pause() {
|
|
||||||
player.pause();
|
|
||||||
}
|
|
||||||
}
|
|
@ -29,7 +29,8 @@ public abstract class PlayerUi {
|
|||||||
@NonNull protected final Player player;
|
@NonNull protected final Player player;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param player the player instance that will be usable throughout the lifetime of this UI
|
* @param player the player instance that will be usable throughout the lifetime of this UI; its
|
||||||
|
* context should already have been initialized
|
||||||
*/
|
*/
|
||||||
protected PlayerUi(@NonNull final Player player) {
|
protected PlayerUi(@NonNull final Player player) {
|
||||||
this.context = player.getContext();
|
this.context = player.getContext();
|
||||||
|
@ -8,6 +8,19 @@ import java.util.function.Consumer;
|
|||||||
public final class PlayerUiList {
|
public final class PlayerUiList {
|
||||||
final List<PlayerUi> playerUis = new ArrayList<>();
|
final List<PlayerUi> playerUis = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link PlayerUiList} starting with the provided player uis. The provided player uis
|
||||||
|
* will not be prepared like those passed to {@link #addAndPrepare(PlayerUi)}, because when
|
||||||
|
* the {@link PlayerUiList} constructor is called, the player is still not running and it
|
||||||
|
* wouldn't make sense to initialize uis then. Instead the player will initialize them by doing
|
||||||
|
* proper calls to {@link #call(Consumer)}.
|
||||||
|
*
|
||||||
|
* @param initialPlayerUis the player uis this list should start with; the order will be kept
|
||||||
|
*/
|
||||||
|
public PlayerUiList(final PlayerUi... initialPlayerUis) {
|
||||||
|
playerUis.addAll(List.of(initialPlayerUis));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the provided player ui to the list and calls on it the initialization functions that
|
* Adds the provided player ui to the list and calls on it the initialization functions that
|
||||||
* apply based on the current player state. The preparation step needs to be done since when UIs
|
* apply based on the current player state. The preparation step needs to be done since when UIs
|
||||||
@ -67,11 +80,11 @@ public final class PlayerUiList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls the provided consumer on all player UIs in the list.
|
* Calls the provided consumer on all player UIs in the list, in order of addition.
|
||||||
* @param consumer the consumer to call with player UIs
|
* @param consumer the consumer to call with player UIs
|
||||||
*/
|
*/
|
||||||
public void call(final Consumer<PlayerUi> consumer) {
|
public void call(final Consumer<PlayerUi> consumer) {
|
||||||
//noinspection SimplifyStreamApiCallChains
|
//noinspection SimplifyStreamApiCallChains
|
||||||
playerUis.stream().forEach(consumer);
|
playerUis.stream().forEachOrdered(consumer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package org.schabi.newpipe.util;
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
@ -24,7 +26,7 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
|
|
||||||
public final class PicassoHelper {
|
public final class PicassoHelper {
|
||||||
public static final String PLAYER_THUMBNAIL_TAG = "PICASSO_PLAYER_THUMBNAIL_TAG";
|
private static final String TAG = PicassoHelper.class.getSimpleName();
|
||||||
private static final String PLAYER_THUMBNAIL_TRANSFORMATION_KEY =
|
private static final String PLAYER_THUMBNAIL_TRANSFORMATION_KEY =
|
||||||
"PICASSO_PLAYER_THUMBNAIL_TRANSFORMATION_KEY";
|
"PICASSO_PLAYER_THUMBNAIL_TRANSFORMATION_KEY";
|
||||||
|
|
||||||
@ -125,10 +127,13 @@ public final class PicassoHelper {
|
|||||||
public static RequestCreator loadScaledDownThumbnail(final Context context, final String url) {
|
public static RequestCreator loadScaledDownThumbnail(final Context context, final String url) {
|
||||||
// scale down the notification thumbnail for performance
|
// scale down the notification thumbnail for performance
|
||||||
return PicassoHelper.loadThumbnail(url)
|
return PicassoHelper.loadThumbnail(url)
|
||||||
.tag(PLAYER_THUMBNAIL_TAG)
|
|
||||||
.transform(new Transformation() {
|
.transform(new Transformation() {
|
||||||
@Override
|
@Override
|
||||||
public Bitmap transform(final Bitmap source) {
|
public Bitmap transform(final Bitmap source) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Thumbnail - transform() called");
|
||||||
|
}
|
||||||
|
|
||||||
final float notificationThumbnailWidth = Math.min(
|
final float notificationThumbnailWidth = Math.min(
|
||||||
context.getResources()
|
context.getResources()
|
||||||
.getDimension(R.dimen.player_notification_thumbnail_width),
|
.getDimension(R.dimen.player_notification_thumbnail_width),
|
||||||
|
Loading…
Reference in New Issue
Block a user