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.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.AudioManager;
|
||||
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.helper.AudioReactor;
|
||||
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.PlayerHelper;
|
||||
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.playback.MediaSourceManager;
|
||||
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.PlayQueueItem;
|
||||
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;
|
||||
private static final String PICASSO_PLAYER_THUMBNAIL_TAG = "PICASSO_PLAYER_THUMBNAIL_TAG";
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback
|
||||
|
@ -196,7 +195,6 @@ public final class Player implements PlaybackListener, Listener {
|
|||
|
||||
private ExoPlayer simpleExoPlayer;
|
||||
private AudioReactor audioReactor;
|
||||
private MediaSessionManager mediaSessionManager;
|
||||
|
||||
@NonNull private final DefaultTrackSelector trackSelector;
|
||||
@NonNull private final LoadController loadController;
|
||||
|
@ -224,8 +222,8 @@ public final class Player implements PlaybackListener, Listener {
|
|||
// UIs, listeners and disposables
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@SuppressWarnings("MemberName") // keep the unusual member name
|
||||
private final PlayerUiList UIs = new PlayerUiList();
|
||||
@SuppressWarnings({"MemberName", "java:S116"}) // keep the unusual member name
|
||||
private final PlayerUiList UIs;
|
||||
|
||||
private BroadcastReceiver broadcastReceiver;
|
||||
private IntentFilter intentFilter;
|
||||
|
@ -235,6 +233,11 @@ public final class Player implements PlaybackListener, Listener {
|
|||
@NonNull private final SerialDisposable progressUpdateDisposable = new SerialDisposable();
|
||||
@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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
@ -265,6 +268,17 @@ public final class Player implements PlaybackListener, Listener {
|
|||
|
||||
videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver());
|
||||
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() {
|
||||
|
@ -431,11 +445,6 @@ public final class Player implements PlaybackListener, Listener {
|
|||
}
|
||||
|
||||
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)
|
||||
|| (UIs.get(PopupPlayerUi.class).isPresent() && playerType == PlayerType.POPUP)) {
|
||||
// correct UI already in place
|
||||
|
@ -506,8 +515,6 @@ public final class Player implements PlaybackListener, Listener {
|
|||
simpleExoPlayer.setHandleAudioBecomingNoisy(true);
|
||||
|
||||
audioReactor = new AudioReactor(context, simpleExoPlayer);
|
||||
mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer,
|
||||
new PlayerMediaSession(this));
|
||||
|
||||
registerBroadcastReceiver();
|
||||
|
||||
|
@ -558,9 +565,6 @@ public final class Player implements PlaybackListener, Listener {
|
|||
if (playQueueManager != null) {
|
||||
playQueueManager.dispose();
|
||||
}
|
||||
if (mediaSessionManager != null) {
|
||||
mediaSessionManager.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
|
@ -577,7 +581,7 @@ public final class Player implements PlaybackListener, Listener {
|
|||
|
||||
databaseUpdateDisposable.clear();
|
||||
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
|
||||
}
|
||||
|
@ -723,11 +727,6 @@ public final class Player implements PlaybackListener, Listener {
|
|||
Log.d(TAG, "ACTION_CONFIGURATION_CHANGED received");
|
||||
}
|
||||
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));
|
||||
|
@ -756,44 +755,63 @@ public final class Player implements PlaybackListener, Listener {
|
|||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region Thumbnail loading
|
||||
|
||||
private void initThumbnail(final String url) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Thumbnail - initThumbnail() called with url = ["
|
||||
+ (url == null ? "null" : url) + "]");
|
||||
}
|
||||
if (isNullOrEmpty(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// scale down the notification thumbnail for performance
|
||||
PicassoHelper.loadScaledDownThumbnail(context, url).into(new Target() {
|
||||
private Target getCurrentThumbnailTarget() {
|
||||
// a Picasso target is just a listener for thumbnail loading events
|
||||
return new Target() {
|
||||
@Override
|
||||
public void onBitmapLoaded(final Bitmap bitmap, final Picasso.LoadedFrom from) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Thumbnail - onLoadingComplete() called with: url = [" + url
|
||||
+ "], " + "loadedImage = [" + bitmap + " -> " + bitmap.getWidth() + "x"
|
||||
+ bitmap.getHeight() + "], from = [" + from + "]");
|
||||
Log.d(TAG, "Thumbnail - onBitmapLoaded() called with: bitmap = [" + bitmap
|
||||
+ " -> " + bitmap.getWidth() + "x" + bitmap.getHeight() + "], from = ["
|
||||
+ from + "]");
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
|
||||
UIs.call(playerUi -> playerUi.onThumbnailLoaded(null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareLoad(final Drawable placeHolderDrawable) {
|
||||
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
|
||||
|
||||
|
@ -1735,18 +1753,9 @@ public final class Player implements PlaybackListener, Listener {
|
|||
|
||||
maybeAutoQueueNextStream(info);
|
||||
|
||||
initThumbnail(info.getThumbnailUrl());
|
||||
loadCurrentThumbnail(info.getThumbnailUrl());
|
||||
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();
|
||||
UIs.call(playerUi -> playerUi.onMetadataChanged(info));
|
||||
}
|
||||
|
@ -1786,10 +1795,6 @@ public final class Player implements PlaybackListener, Listener {
|
|||
|
||||
@Nullable
|
||||
public Bitmap getThumbnail() {
|
||||
if (currentThumbnail == null) {
|
||||
currentThumbnail = BitmapFactory.decodeResource(
|
||||
context.getResources(), R.drawable.placeholder_thumbnail_video);
|
||||
}
|
||||
return currentThumbnail;
|
||||
}
|
||||
//endregion
|
||||
|
@ -2194,10 +2199,6 @@ public final class Player implements PlaybackListener, Listener {
|
|||
return prefs;
|
||||
}
|
||||
|
||||
public MediaSessionManager getMediaSessionManager() {
|
||||
return mediaSessionManager;
|
||||
}
|
||||
|
||||
|
||||
public PlayerType getPlayerType() {
|
||||
return playerType;
|
||||
|
|
|
@ -28,6 +28,7 @@ import android.os.Binder;
|
|||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
|
||||
|
@ -73,9 +74,8 @@ public final class PlayerService extends Service {
|
|||
}
|
||||
|
||||
player.handleIntent(intent);
|
||||
if (player.getMediaSessionManager() != null) {
|
||||
player.getMediaSessionManager().handleMediaButtonIntent(intent);
|
||||
}
|
||||
player.UIs().get(MediaSessionPlayerUi.class)
|
||||
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
|
||||
|
||||
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;
|
||||
|
||||
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_PREVIOUS;
|
||||
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 static final int DEFAULT_MAX_QUEUE_SIZE = 10;
|
||||
private static final int MAX_QUEUE_SIZE = 10;
|
||||
|
||||
private final MediaSessionCompat mediaSession;
|
||||
private final MediaSessionCallback callback;
|
||||
private final int maxQueueSize;
|
||||
private final Player player;
|
||||
|
||||
private long activeQueueItemId;
|
||||
|
||||
public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession,
|
||||
@NonNull final MediaSessionCallback callback) {
|
||||
@NonNull final Player player) {
|
||||
this.mediaSession = mediaSession;
|
||||
this.callback = callback;
|
||||
this.maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
|
||||
this.player = player;
|
||||
|
||||
this.activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(@NonNull final Player player) {
|
||||
public void onTimelineChanged(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
|
||||
publishFloatingQueueWindow();
|
||||
}
|
||||
|
||||
@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
|
||||
|| player.getCurrentTimeline().getWindowCount() > maxQueueSize) {
|
||||
|| exoPlayer.getCurrentTimeline().getWindowCount() > MAX_QUEUE_SIZE) {
|
||||
publishFloatingQueueWindow();
|
||||
} else if (!player.getCurrentTimeline().isEmpty()) {
|
||||
activeQueueItemId = player.getCurrentMediaItemIndex();
|
||||
} else if (!exoPlayer.getCurrentTimeline().isEmpty()) {
|
||||
activeQueueItemId = exoPlayer.getCurrentMediaItemIndex();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getActiveQueueItemId(@Nullable final Player player) {
|
||||
return callback.getCurrentPlayingIndex();
|
||||
public long getActiveQueueItemId(
|
||||
@Nullable final com.google.android.exoplayer2.Player exoPlayer) {
|
||||
return Optional.ofNullable(player.getPlayQueue()).map(PlayQueue::getIndex).orElse(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSkipToPrevious(@NonNull final Player player) {
|
||||
callback.playPrevious();
|
||||
public void onSkipToPrevious(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
|
||||
player.playPrevious();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSkipToQueueItem(@NonNull final Player player, final long id) {
|
||||
callback.playItemAtIndex((int) id);
|
||||
public void onSkipToQueueItem(@NonNull final com.google.android.exoplayer2.Player exoPlayer,
|
||||
final long id) {
|
||||
if (player.getPlayQueue() != null) {
|
||||
player.selectQueueItem(player.getPlayQueue().getItem((int) id));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSkipToNext(@NonNull final Player player) {
|
||||
callback.playNext();
|
||||
public void onSkipToNext(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
|
||||
player.playNext();
|
||||
}
|
||||
|
||||
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());
|
||||
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
||||
return;
|
||||
}
|
||||
|
||||
// Yes this is almost a copypasta, got a problem with that? =\
|
||||
final int windowCount = callback.getQueueSize();
|
||||
final int currentWindowIndex = callback.getCurrentPlayingIndex();
|
||||
final int queueSize = Math.min(maxQueueSize, windowCount);
|
||||
final int currentWindowIndex = player.getPlayQueue().getIndex();
|
||||
final int queueSize = Math.min(MAX_QUEUE_SIZE, windowCount);
|
||||
final int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0,
|
||||
windowCount - queueSize);
|
||||
|
||||
final List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
|
||||
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);
|
||||
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
|
||||
public boolean onCommand(@NonNull final Player player,
|
||||
public boolean onCommand(@NonNull final com.google.android.exoplayer2.Player exoPlayer,
|
||||
@NonNull final String command,
|
||||
@Nullable final Bundle extras,
|
||||
@Nullable final ResultReceiver cb) {
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.os.Build;
|
|||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
@ -19,11 +20,13 @@ import androidx.core.content.ContextCompat;
|
|||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
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_ONE;
|
||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
|
||||
|
@ -101,9 +104,13 @@ public final class NotificationUtil {
|
|||
player.getContext(), player.getPrefs(), nonNothingSlotCount);
|
||||
final int[] compactSlots = compactSlotList.stream().mapToInt(Integer::intValue).toArray();
|
||||
|
||||
builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
|
||||
.setMediaSession(player.getMediaSessionManager().getSessionToken())
|
||||
.setShowActionsInCompactView(compactSlots))
|
||||
final MediaStyle mediaStyle = new MediaStyle().setShowActionsInCompactView(compactSlots);
|
||||
player.UIs()
|
||||
.get(MediaSessionPlayerUi.class)
|
||||
.flatMap(MediaSessionPlayerUi::getSessionToken)
|
||||
.ifPresent(mediaStyle::setMediaSession);
|
||||
|
||||
builder.setStyle(mediaStyle)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
|
||||
|
@ -133,12 +140,9 @@ public final class NotificationUtil {
|
|||
notificationBuilder.setContentTitle(player.getVideoTitle());
|
||||
notificationBuilder.setContentText(player.getUploaderName());
|
||||
notificationBuilder.setTicker(player.getVideoTitle());
|
||||
|
||||
updateActions(notificationBuilder);
|
||||
final boolean showThumbnail = player.getPrefs().getBoolean(
|
||||
player.getContext().getString(R.string.show_thumbnail_key), true);
|
||||
if (showThumbnail) {
|
||||
setLargeIcon(notificationBuilder);
|
||||
}
|
||||
setLargeIcon(notificationBuilder);
|
||||
}
|
||||
|
||||
|
||||
|
@ -340,17 +344,26 @@ public final class NotificationUtil {
|
|||
/////////////////////////////////////////////////////
|
||||
|
||||
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(
|
||||
player.getContext().getString(R.string.scale_to_square_image_in_notifications_key),
|
||||
false);
|
||||
if (scaleImageToSquareAspectRatio) {
|
||||
builder.setLargeIcon(getBitmapWithSquareAspectRatio(player.getThumbnail()));
|
||||
builder.setLargeIcon(getBitmapWithSquareAspectRatio(thumbnail));
|
||||
} 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
|
||||
// has that size.
|
||||
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;
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
this.context = player.getContext();
|
||||
|
|
|
@ -8,6 +8,19 @@ import java.util.function.Consumer;
|
|||
public final class PlayerUiList {
|
||||
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
|
||||
* 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
|
||||
*/
|
||||
public void call(final Consumer<PlayerUi> consumer) {
|
||||
//noinspection SimplifyStreamApiCallChains
|
||||
playerUis.stream().forEach(consumer);
|
||||
playerUis.stream().forEachOrdered(consumer);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package org.schabi.newpipe.util;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
|
@ -24,7 +26,7 @@ import java.util.concurrent.TimeUnit;
|
|||
import okhttp3.OkHttpClient;
|
||||
|
||||
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 =
|
||||
"PICASSO_PLAYER_THUMBNAIL_TRANSFORMATION_KEY";
|
||||
|
||||
|
@ -125,10 +127,13 @@ public final class PicassoHelper {
|
|||
public static RequestCreator loadScaledDownThumbnail(final Context context, final String url) {
|
||||
// scale down the notification thumbnail for performance
|
||||
return PicassoHelper.loadThumbnail(url)
|
||||
.tag(PLAYER_THUMBNAIL_TAG)
|
||||
.transform(new Transformation() {
|
||||
@Override
|
||||
public Bitmap transform(final Bitmap source) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Thumbnail - transform() called");
|
||||
}
|
||||
|
||||
final float notificationThumbnailWidth = Math.min(
|
||||
context.getResources()
|
||||
.getDimension(R.dimen.player_notification_thumbnail_width),
|
||||
|
|
Loading…
Reference in New Issue