Merged the latest changes

This commit is contained in:
Avently 2020-07-13 04:17:21 +03:00
commit d2aaa6f691
1254 changed files with 39193 additions and 18652 deletions

View file

@ -11,20 +11,19 @@ import android.content.ContextWrapper;
* https://gist.github.com/jankovd/891d96f476f7a9ce24e2
*/
public class AudioServiceLeakFix extends ContextWrapper {
AudioServiceLeakFix(final Context base) {
super(base);
}
AudioServiceLeakFix(Context base) {
super(base);
}
public static ContextWrapper preventLeakOf(final Context base) {
return new AudioServiceLeakFix(base);
}
public static ContextWrapper preventLeakOf(Context base) {
return new AudioServiceLeakFix(base);
}
@Override
public Object getSystemService(String name) {
if (Context.AUDIO_SERVICE.equals(name)) {
return getApplicationContext().getSystemService(name);
}
return super.getSystemService(name);
}
}
@Override
public Object getSystemService(final String name) {
if (Context.AUDIO_SERVICE.equals(name)) {
return getApplicationContext().getSystemService(name);
}
return super.getSystemService(name);
}
}

View file

@ -25,16 +25,21 @@ import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.IBinder;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.MediaSource;
@ -44,56 +49,60 @@ import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.LockManager;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
import org.schabi.newpipe.player.resolver.MediaSourceTag;
import org.schabi.newpipe.util.BitmapUtils;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
/**
* Base players joining the common properties
* Service Background Player implementing {@link VideoPlayer}.
*
* @author mauriciocolli
*/
public final class BackgroundPlayer extends Service {
private static final String TAG = "BackgroundPlayer";
private static final boolean DEBUG = BasePlayer.DEBUG;
public static final String ACTION_CLOSE = "org.schabi.newpipe.player.BackgroundPlayer.CLOSE";
public static final String ACTION_PLAY_PAUSE = "org.schabi.newpipe.player.BackgroundPlayer.PLAY_PAUSE";
public static final String ACTION_REPEAT = "org.schabi.newpipe.player.BackgroundPlayer.REPEAT";
public static final String ACTION_PLAY_NEXT = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_NEXT";
public static final String ACTION_PLAY_PREVIOUS = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_PREVIOUS";
public static final String ACTION_FAST_REWIND = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_REWIND";
public static final String ACTION_FAST_FORWARD = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_FORWARD";
public static final String ACTION_CLOSE
= "org.schabi.newpipe.player.BackgroundPlayer.CLOSE";
public static final String ACTION_PLAY_PAUSE
= "org.schabi.newpipe.player.BackgroundPlayer.PLAY_PAUSE";
public static final String ACTION_REPEAT
= "org.schabi.newpipe.player.BackgroundPlayer.REPEAT";
public static final String ACTION_PLAY_NEXT
= "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_NEXT";
public static final String ACTION_PLAY_PREVIOUS
= "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_PREVIOUS";
public static final String ACTION_FAST_REWIND
= "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_REWIND";
public static final String ACTION_FAST_FORWARD
= "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_FORWARD";
public static final String SET_IMAGE_RESOURCE_METHOD = "setImageResource";
private static final String TAG = "BackgroundPlayer";
private static final boolean DEBUG = BasePlayer.DEBUG;
private static final int NOTIFICATION_ID = 123789;
private static final int NOTIFICATION_UPDATES_BEFORE_RESET = 60;
private BasePlayerImpl basePlayerImpl;
private LockManager lockManager;
/*//////////////////////////////////////////////////////////////////////////
// Service-Activity Binder
//////////////////////////////////////////////////////////////////////////*/
private PlayerEventListener activityListener;
private IBinder mBinder;
private SharedPreferences sharedPreferences;
/*//////////////////////////////////////////////////////////////////////////
// Notification
//////////////////////////////////////////////////////////////////////////*/
private static final int NOTIFICATION_ID = 123789;
private PlayerEventListener activityListener;
private IBinder mBinder;
private NotificationManager notificationManager;
private NotificationCompat.Builder notBuilder;
private RemoteViews notRemoteView;
private RemoteViews bigNotRemoteView;
private boolean shouldUpdateOnProgress;
private int timesNotificationUpdated;
/*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle
@ -101,10 +110,12 @@ public final class BackgroundPlayer extends Service {
@Override
public void onCreate() {
if (DEBUG) Log.d(TAG, "onCreate() called");
if (DEBUG) {
Log.d(TAG, "onCreate() called");
}
notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE));
lockManager = new LockManager(this);
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
assureCorrectAppLanguage(this);
ThemeHelper.setTheme(this);
basePlayerImpl = new BasePlayerImpl(this);
basePlayerImpl.setup();
@ -114,9 +125,11 @@ public final class BackgroundPlayer extends Service {
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent +
"], flags = [" + flags + "], startId = [" + startId + "]");
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (DEBUG) {
Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], "
+ "flags = [" + flags + "], startId = [" + startId + "]");
}
basePlayerImpl.handleIntent(intent);
if (basePlayerImpl.mediaSessionManager != null) {
basePlayerImpl.mediaSessionManager.handleMediaButtonIntent(intent);
@ -126,17 +139,19 @@ public final class BackgroundPlayer extends Service {
@Override
public void onDestroy() {
if (DEBUG) Log.d(TAG, "destroy() called");
if (DEBUG) {
Log.d(TAG, "destroy() called");
}
onClose();
}
@Override
protected void attachBaseContext(Context base) {
protected void attachBaseContext(final Context base) {
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
}
@Override
public IBinder onBind(Intent intent) {
public IBinder onBind(final Intent intent) {
return mBinder;
}
@ -144,27 +159,29 @@ public final class BackgroundPlayer extends Service {
// Actions
//////////////////////////////////////////////////////////////////////////*/
private void onClose() {
if (DEBUG) Log.d(TAG, "onClose() called");
if (lockManager != null) {
lockManager.releaseWifiAndCpu();
if (DEBUG) {
Log.d(TAG, "onClose() called");
}
if (basePlayerImpl != null) {
basePlayerImpl.savePlaybackState();
basePlayerImpl.stopActivityBinding();
basePlayerImpl.destroy();
}
if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID);
if (notificationManager != null) {
notificationManager.cancel(NOTIFICATION_ID);
}
mBinder = null;
basePlayerImpl = null;
lockManager = null;
stopForeground(true);
stopSelf();
}
private void onScreenOnOff(boolean on) {
if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]");
private void onScreenOnOff(final boolean on) {
if (DEBUG) {
Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]");
}
shouldUpdateOnProgress = on;
basePlayerImpl.triggerProgressUpdate();
if (on) {
@ -180,59 +197,105 @@ public final class BackgroundPlayer extends Service {
private void resetNotification() {
notBuilder = createNotification();
timesNotificationUpdated = 0;
}
private NotificationCompat.Builder createNotification() {
notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification);
bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification_expanded);
notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID,
R.layout.player_background_notification);
bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID,
R.layout.player_background_notification_expanded);
setupNotification(notRemoteView);
setupNotification(bigNotRemoteView);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
NotificationCompat.Builder builder = new NotificationCompat
.Builder(this, getString(R.string.notification_channel_id))
.setOngoing(true)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCustomContentView(notRemoteView)
.setCustomBigContentView(bigNotRemoteView);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setLockScreenThumbnail(builder);
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
builder.setPriority(NotificationCompat.PRIORITY_MAX);
}
return builder;
}
private void setupNotification(RemoteViews remoteViews) {
if (basePlayerImpl == null) return;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void setLockScreenThumbnail(final NotificationCompat.Builder builder) {
boolean isLockScreenThumbnailEnabled = sharedPreferences.getBoolean(
getString(R.string.enable_lock_screen_video_thumbnail_key), true);
if (isLockScreenThumbnailEnabled) {
basePlayerImpl.mediaSessionManager.setLockScreenArt(
builder,
getCenteredThumbnailBitmap()
);
} else {
basePlayerImpl.mediaSessionManager.clearLockScreenArt(builder);
}
}
@Nullable
private Bitmap getCenteredThumbnailBitmap() {
final int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
final int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;
return BitmapUtils.centerCrop(basePlayerImpl.getThumbnail(), screenWidth, screenHeight);
}
private void setupNotification(final RemoteViews remoteViews) {
if (basePlayerImpl == null) {
return;
}
remoteViews.setTextViewText(R.id.notificationSongName, basePlayerImpl.getVideoTitle());
remoteViews.setTextViewText(R.id.notificationArtist, basePlayerImpl.getUploaderName());
remoteViews.setOnClickPendingIntent(R.id.notificationPlayPause,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT));
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationStop,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT));
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationRepeat,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT));
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT));
// Starts background player activity -- attempts to unlock lockscreen
final Intent intent = NavigationHelper.getBackgroundPlayerActivityIntent(this);
remoteViews.setOnClickPendingIntent(R.id.notificationContent,
PendingIntent.getActivity(this, NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT));
PendingIntent.getActivity(this, NOTIFICATION_ID, intent,
PendingIntent.FLAG_UPDATE_CURRENT));
if (basePlayerImpl.playQueue != null && basePlayerImpl.playQueue.size() > 1) {
remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_previous);
remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_next);
remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_previous);
remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_next);
remoteViews.setOnClickPendingIntent(R.id.notificationFRewind,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT));
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationFForward,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT));
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT));
} else {
remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_rewind);
remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_fastforward);
remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_rewind);
remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_fastforward);
remoteViews.setOnClickPendingIntent(R.id.notificationFRewind,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT));
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationFForward,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT));
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT));
}
setRepeatModeIcon(remoteViews, basePlayerImpl.getRepeatMode());
@ -244,14 +307,23 @@ public final class BackgroundPlayer extends Service {
*
* @param drawableId if != -1, sets the drawable with that id on the play/pause button
*/
private synchronized void updateNotification(int drawableId) {
//if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]");
if (notBuilder == null) return;
private synchronized void updateNotification(final int drawableId) {
// if (DEBUG) {
// Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]");
// }
if (notBuilder == null) {
return;
}
if (drawableId != -1) {
if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
if (notRemoteView != null) {
notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
}
if (bigNotRemoteView != null) {
bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
}
}
notificationManager.notify(NOTIFICATION_ID, notBuilder.build());
timesNotificationUpdated++;
}
/*//////////////////////////////////////////////////////////////////////////
@ -261,31 +333,34 @@ public final class BackgroundPlayer extends Service {
private void setRepeatModeIcon(final RemoteViews remoteViews, final int repeatMode) {
switch (repeatMode) {
case Player.REPEAT_MODE_OFF:
remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_off);
remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_repeat_off);
break;
case Player.REPEAT_MODE_ONE:
remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_one);
remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_repeat_one);
break;
case Player.REPEAT_MODE_ALL:
remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_all);
remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_repeat_all);
break;
}
}
//////////////////////////////////////////////////////////////////////////
protected class BasePlayerImpl extends BasePlayer {
@NonNull final private AudioPlaybackResolver resolver;
@NonNull
private final AudioPlaybackResolver resolver;
private int cachedDuration;
private String cachedDurationString;
BasePlayerImpl(Context context) {
BasePlayerImpl(final Context context) {
super(context);
this.resolver = new AudioPlaybackResolver(context, dataSource);
}
@Override
public void initPlayer(boolean playOnReady) {
public void initPlayer(final boolean playOnReady) {
super.initPlayer(playOnReady);
}
@ -294,8 +369,12 @@ public final class BackgroundPlayer extends Service {
super.handleIntent(intent);
resetNotification();
if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false);
if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false);
if (bigNotRemoteView != null) {
bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false);
}
if (notRemoteView != null) {
notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false);
}
startForeground(NOTIFICATION_ID, notBuilder.build());
}
@ -304,7 +383,9 @@ public final class BackgroundPlayer extends Service {
//////////////////////////////////////////////////////////////////////////*/
private void updateNotificationThumbnail() {
if (basePlayerImpl == null) return;
if (basePlayerImpl == null) {
return;
}
if (notRemoteView != null) {
notRemoteView.setImageViewBitmap(R.id.notificationCover,
basePlayerImpl.getThumbnail());
@ -316,7 +397,8 @@ public final class BackgroundPlayer extends Service {
}
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
public void onLoadingComplete(final String imageUri, final View view,
final Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
resetNotification();
updateNotificationThumbnail();
@ -324,20 +406,21 @@ public final class BackgroundPlayer extends Service {
}
@Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
public void onLoadingFailed(final String imageUri, final View view,
final FailReason failReason) {
super.onLoadingFailed(imageUri, view, failReason);
resetNotification();
updateNotificationThumbnail();
updateNotification(-1);
}
/*//////////////////////////////////////////////////////////////////////////
// States Implementation
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onPrepared(boolean playWhenReady) {
public void onPrepared(final boolean playWhenReady) {
super.onPrepared(playWhenReady);
simpleExoPlayer.setVolume(1.0f);
}
@Override
@ -347,22 +430,39 @@ public final class BackgroundPlayer extends Service {
}
@Override
public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) {
public void onMuteUnmuteButtonClicked() {
super.onMuteUnmuteButtonClicked();
updatePlayback();
}
@Override
public void onUpdateProgress(final int currentProgress, final int duration,
final int bufferPercent) {
updateProgress(currentProgress, duration, bufferPercent);
if (!shouldUpdateOnProgress) return;
resetNotification();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O /*Oreo*/) updateNotificationThumbnail();
if (!shouldUpdateOnProgress) {
return;
}
if (timesNotificationUpdated > NOTIFICATION_UPDATES_BEFORE_RESET) {
resetNotification();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O /*Oreo*/) {
updateNotificationThumbnail();
}
}
if (bigNotRemoteView != null) {
if(cachedDuration != duration) {
if (cachedDuration != duration) {
cachedDuration = duration;
cachedDurationString = getTimeString(duration);
}
bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false);
bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + cachedDurationString);
bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration,
currentProgress, false);
bigNotRemoteView.setTextViewText(R.id.notificationTime,
getTimeString(currentProgress) + " / " + cachedDurationString);
}
if (notRemoteView != null) {
notRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false);
notRemoteView.setProgressBar(R.id.notificationProgressBar, duration,
currentProgress, false);
}
updateNotification(-1);
}
@ -382,8 +482,12 @@ public final class BackgroundPlayer extends Service {
@Override
public void destroy() {
super.destroy();
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, null);
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, null);
if (notRemoteView != null) {
notRemoteView.setImageViewBitmap(R.id.notificationCover, null);
}
if (bigNotRemoteView != null) {
bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, null);
}
}
/*//////////////////////////////////////////////////////////////////////////
@ -391,18 +495,18 @@ public final class BackgroundPlayer extends Service {
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
public void onPlaybackParametersChanged(final PlaybackParameters playbackParameters) {
super.onPlaybackParametersChanged(playbackParameters);
updatePlayback();
}
@Override
public void onLoadingChanged(boolean isLoading) {
public void onLoadingChanged(final boolean isLoading) {
// Disable default behavior
}
@Override
public void onRepeatModeChanged(int i) {
public void onRepeatModeChanged(final int i) {
resetNotification();
updateNotification(-1);
updatePlayback();
@ -436,14 +540,14 @@ public final class BackgroundPlayer extends Service {
// Activity Event Listener
//////////////////////////////////////////////////////////////////////////*/
/*package-private*/ void setActivityListener(PlayerEventListener listener) {
/*package-private*/ void setActivityListener(final PlayerEventListener listener) {
activityListener = listener;
updateMetadata();
updatePlayback();
triggerProgressUpdate();
}
/*package-private*/ void removeActivityListener(PlayerEventListener listener) {
/*package-private*/ void removeActivityListener(final PlayerEventListener listener) {
if (activityListener == listener) {
activityListener = null;
}
@ -462,7 +566,8 @@ public final class BackgroundPlayer extends Service {
}
}
private void updateProgress(int currentProgress, int duration, int bufferPercent) {
private void updateProgress(final int currentProgress, final int duration,
final int bufferPercent) {
if (activityListener != null) {
activityListener.onProgressUpdate(currentProgress, duration, bufferPercent);
}
@ -480,27 +585,31 @@ public final class BackgroundPlayer extends Service {
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void setupBroadcastReceiver(IntentFilter intentFilter) {
super.setupBroadcastReceiver(intentFilter);
intentFilter.addAction(ACTION_CLOSE);
intentFilter.addAction(ACTION_PLAY_PAUSE);
intentFilter.addAction(ACTION_REPEAT);
intentFilter.addAction(ACTION_PLAY_PREVIOUS);
intentFilter.addAction(ACTION_PLAY_NEXT);
intentFilter.addAction(ACTION_FAST_REWIND);
intentFilter.addAction(ACTION_FAST_FORWARD);
protected void setupBroadcastReceiver(final IntentFilter intentFltr) {
super.setupBroadcastReceiver(intentFltr);
intentFltr.addAction(ACTION_CLOSE);
intentFltr.addAction(ACTION_PLAY_PAUSE);
intentFltr.addAction(ACTION_REPEAT);
intentFltr.addAction(ACTION_PLAY_PREVIOUS);
intentFltr.addAction(ACTION_PLAY_NEXT);
intentFltr.addAction(ACTION_FAST_REWIND);
intentFltr.addAction(ACTION_FAST_FORWARD);
intentFilter.addAction(Intent.ACTION_SCREEN_ON);
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
intentFltr.addAction(Intent.ACTION_SCREEN_ON);
intentFltr.addAction(Intent.ACTION_SCREEN_OFF);
intentFilter.addAction(Intent.ACTION_HEADSET_PLUG);
intentFltr.addAction(Intent.ACTION_HEADSET_PLUG);
}
@Override
public void onBroadcastReceived(Intent intent) {
public void onBroadcastReceived(final Intent intent) {
super.onBroadcastReceived(intent);
if (intent == null || intent.getAction() == null) return;
if (DEBUG) Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]");
if (intent == null || intent.getAction() == null) {
return;
}
if (DEBUG) {
Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]");
}
switch (intent.getAction()) {
case ACTION_CLOSE:
onClose();
@ -537,7 +646,7 @@ public final class BackgroundPlayer extends Service {
//////////////////////////////////////////////////////////////////////////*/
@Override
public void changeState(int state) {
public void changeState(final int state) {
super.changeState(state);
updatePlayback();
}
@ -547,8 +656,7 @@ public final class BackgroundPlayer extends Service {
super.onPlaying();
resetNotification();
updateNotificationThumbnail();
updateNotification(R.drawable.ic_pause_white);
lockManager.acquireWifiAndCpu();
updateNotification(R.drawable.exo_controls_pause);
}
@Override
@ -556,8 +664,7 @@ public final class BackgroundPlayer extends Service {
super.onPaused();
resetNotification();
updateNotificationThumbnail();
updateNotification(R.drawable.ic_play_arrow_white);
lockManager.releaseWifiAndCpu();
updateNotification(R.drawable.exo_controls_play);
}
@Override
@ -571,8 +678,7 @@ public final class BackgroundPlayer extends Service {
notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false);
}
updateNotificationThumbnail();
updateNotification(R.drawable.ic_replay_white);
lockManager.releaseWifiAndCpu();
updateNotification(R.drawable.ic_replay_white_24dp);
}
}
}

View file

@ -49,7 +49,7 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity {
}
@Override
public boolean onPlayerOptionSelected(MenuItem item) {
public boolean onPlayerOptionSelected(final MenuItem item) {
if (item.getItemId() == R.id.action_switch_popup) {
if (!PermissionHelper.isPopupEnabled(this)) {
@ -58,13 +58,13 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity {
}
this.player.setRecovery();
NavigationHelper.playOnPopupPlayer(getApplicationContext(), player.playQueue, true);
NavigationHelper.playOnPopupPlayer(getApplicationContext(), player.playQueue, this.player.isPlaying());
return true;
}
if (item.getItemId() == R.id.action_switch_background) {
this.player.setRecovery();
NavigationHelper.playOnBackgroundPlayer(getApplicationContext(), player.playQueue, true);
NavigationHelper.playOnBackgroundPlayer(getApplicationContext(), player.playQueue, this.player.isPlaying());
return true;
}
@ -78,9 +78,4 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity {
menu.findItem(R.id.action_switch_popup).setVisible(!((VideoPlayerImpl)player).popupPlayerSelected());
menu.findItem(R.id.action_switch_background).setVisible(!((VideoPlayerImpl)player).audioPlayerSelected());
}
//@Override
public Intent getPlayerShutdownIntent() {
return new Intent(ACTION_CLOSE);
}
}

File diff suppressed because it is too large Load diff

View file

@ -24,11 +24,18 @@ import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.DisplayMetrics;
import android.view.ViewGroup;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import android.util.Log;
import android.view.View;
@ -39,10 +46,12 @@ import com.google.android.exoplayer2.Player;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.helper.LockManager;
import org.schabi.newpipe.util.BitmapUtils;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
/**
* One service for all players
@ -55,7 +64,7 @@ public final class MainPlayer extends Service {
private VideoPlayerImpl playerImpl;
private WindowManager windowManager;
private LockManager lockManager;
private SharedPreferences sharedPreferences;
private final IBinder mBinder = new MainPlayer.LocalBinder();
@ -93,9 +102,10 @@ public final class MainPlayer extends Service {
@Override
public void onCreate() {
if (DEBUG) Log.d(TAG, "onCreate() called");
assureCorrectAppLanguage(this);
notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE));
lockManager = new LockManager(this);
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
ThemeHelper.setTheme(this);
createView();
@ -171,9 +181,6 @@ public final class MainPlayer extends Service {
private void onClose() {
if (DEBUG) Log.d(TAG, "onClose() called");
if (lockManager != null) {
lockManager.releaseWifiAndCpu();
}
if (playerImpl != null) {
removeViewFromParent();
@ -235,25 +242,56 @@ public final class MainPlayer extends Service {
void resetNotification() {
notBuilder = createNotification();
playerImpl.timesNotificationUpdated = 0;
}
private NotificationCompat.Builder createNotification() {
notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification);
bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification_expanded);
notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_background_notification);
bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_background_notification_expanded);
setupNotification(notRemoteView);
setupNotification(bigNotRemoteView);
final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
final NotificationCompat.Builder builder = new NotificationCompat
.Builder(this, getString(R.string.notification_channel_id))
.setOngoing(true)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCustomContentView(notRemoteView)
.setCustomBigContentView(bigNotRemoteView);
builder.setPriority(NotificationCompat.PRIORITY_MAX);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setLockScreenThumbnail(builder);
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
builder.setPriority(NotificationCompat.PRIORITY_MAX);
}
return builder;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void setLockScreenThumbnail(final NotificationCompat.Builder builder) {
final boolean isLockScreenThumbnailEnabled = sharedPreferences.getBoolean(
getString(R.string.enable_lock_screen_video_thumbnail_key), true);
if (isLockScreenThumbnailEnabled) {
playerImpl.mediaSessionManager.setLockScreenArt(
builder,
getCenteredThumbnailBitmap()
);
} else {
playerImpl.mediaSessionManager.clearLockScreenArt(builder);
}
}
@Nullable
private Bitmap getCenteredThumbnailBitmap() {
final int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
final int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;
return BitmapUtils.centerCrop(playerImpl.getThumbnail(), screenWidth, screenHeight);
}
private void setupNotification(final RemoteViews remoteViews) {
// Don't show anything until player is playing
if (playerImpl == null) return;
@ -299,13 +337,16 @@ public final class MainPlayer extends Service {
* @param drawableId if != -1, sets the drawable with that id on the play/pause button
*/
synchronized void updateNotification(final int drawableId) {
//if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]");
/*if (DEBUG) {
Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]");
}*/
if (notBuilder == null) return;
if (drawableId != -1) {
if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
}
notificationManager.notify(NOTIFICATION_ID, notBuilder.build());
playerImpl.timesNotificationUpdated++;
}
/*//////////////////////////////////////////////////////////////////////////
@ -347,10 +388,6 @@ public final class MainPlayer extends Service {
// Getters
//////////////////////////////////////////////////////////////////////////*/
LockManager getLockManager() {
return lockManager;
}
NotificationCompat.Builder getNotBuilder() {
return notBuilder;
}

View file

@ -1,6 +1,7 @@
package org.schabi.newpipe.player;
import android.os.Binder;
import androidx.annotation.NonNull;
class PlayerServiceBinder extends Binder {

View file

@ -9,11 +9,13 @@ import java.io.Serializable;
public class PlayerState implements Serializable {
@NonNull private final PlayQueue playQueue;
@NonNull
private final PlayQueue playQueue;
private final int repeatMode;
private final float playbackSpeed;
private final float playbackPitch;
@Nullable private final String playbackQuality;
@Nullable
private final String playbackQuality;
private final boolean playbackSkipSilence;
private final boolean wasPlaying;

View file

@ -47,11 +47,14 @@ public final class PopupVideoPlayerActivity extends ServicePlayerActivity {
}
@Override
public boolean onPlayerOptionSelected(MenuItem item) {
public boolean onPlayerOptionSelected(final MenuItem item) {
if (item.getItemId() == R.id.action_switch_background) {
this.player.setRecovery();
getApplicationContext().sendBroadcast(getPlayerShutdownIntent());
getApplicationContext().startService(getSwitchIntent(MainPlayer.class, MainPlayer.PlayerType.AUDIO));
getApplicationContext().startService(
getSwitchIntent(MainPlayer.class, MainPlayer.PlayerType.AUDIO)
.putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying())
);
return true;
}
return false;

View file

@ -6,11 +6,6 @@ import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.provider.Settings;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.ItemTouchHelper;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
@ -23,6 +18,12 @@ import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
@ -31,7 +32,6 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
@ -44,28 +44,27 @@ import org.schabi.newpipe.util.ThemeHelper;
import java.util.Collections;
import java.util.List;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatPitch;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public abstract class ServicePlayerActivity extends AppCompatActivity
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
View.OnClickListener, PlaybackParameterDialog.Callback {
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
protected BasePlayer player;
private boolean serviceBound;
private ServiceConnection serviceConnection;
protected BasePlayer player;
private boolean seeking;
private boolean redraw;
////////////////////////////////////////////////////////////////////////////
// Views
////////////////////////////////////////////////////////////////////////////
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
private View rootView;
private RecyclerView itemsList;
@ -83,13 +82,14 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private ImageButton repeatButton;
private ImageButton backwardButton;
private ImageButton fastRewindButton;
private ImageButton playPauseButton;
private ImageButton fastForwardButton;
private ImageButton forwardButton;
private ImageButton shuffleButton;
private ProgressBar progressBar;
private TextView playbackSpeedButton;
private TextView playbackPitchButton;
private Menu menu;
////////////////////////////////////////////////////////////////////////////
// Abstracts
@ -115,7 +115,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
////////////////////////////////////////////////////////////////////////////
@Override
protected void onCreate(Bundle savedInstanceState) {
protected void onCreate(final Bundle savedInstanceState) {
assureCorrectAppLanguage(this);
super.onCreate(savedInstanceState);
ThemeHelper.setTheme(this);
setContentView(R.layout.activity_player_queue_control);
@ -142,9 +143,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_play_queue, menu);
getMenuInflater().inflate(getPlayerOptionMenuResource(), menu);
public boolean onCreateOptionsMenu(final Menu m) {
this.menu = m;
getMenuInflater().inflate(R.menu.menu_play_queue, m);
getMenuInflater().inflate(getPlayerOptionMenuResource(), m);
onMaybeMuteChanged();
return true;
}
@ -156,24 +159,31 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
case R.id.action_settings:
NavigationHelper.openSettings(this);
return true;
case R.id.action_append_playlist:
appendAllToPlaylist();
return true;
case R.id.action_settings:
NavigationHelper.openSettings(this);
redraw = true;
case R.id.action_playback_speed:
openPlaybackParameterDialog();
return true;
case R.id.action_mute:
player.onMuteUnmuteButtonClicked();
return true;
case R.id.action_system_audio:
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
return true;
case R.id.action_switch_main:
this.player.setRecovery();
getApplicationContext().startActivity(getSwitchIntent(MainActivity.class, MainPlayer.PlayerType.VIDEO));
getApplicationContext().startActivity(
getSwitchIntent(MainActivity.class, MainPlayer.PlayerType.VIDEO)
.putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying()));
return true;
}
return onPlayerOptionSelected(item) || super.onOptionsItemSelected(item);
@ -185,25 +195,21 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
unbind();
}
Intent getSwitchIntent(final Class clazz, final MainPlayer.PlayerType playerType) {
final Intent intent = NavigationHelper.getPlayerIntent(
getApplicationContext(),
clazz,
this.player.getPlayQueue(),
this.player.getRepeatMode(),
this.player.getPlaybackSpeed(),
this.player.getPlaybackPitch(),
protected Intent getSwitchIntent(final Class clazz, final MainPlayer.PlayerType playerType) {
return NavigationHelper.getPlayerIntent(getApplicationContext(), clazz,
this.player.getPlayQueue(), this.player.getRepeatMode(),
this.player.getPlaybackSpeed(), this.player.getPlaybackPitch(),
this.player.getPlaybackSkipSilence(),
null,
true);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM);
intent.putExtra(Constants.KEY_URL, this.player.getVideoUrl());
intent.putExtra(Constants.KEY_TITLE, this.player.getVideoTitle());
intent.putExtra(VideoDetailFragment.AUTO_PLAY, true);
intent.putExtra(Constants.KEY_SERVICE_ID, this.player.getCurrentMetadata().getMetadata().getServiceId());
intent.putExtra(VideoPlayer.PLAYER_TYPE, playerType);
return intent;
true,
!this.player.isPlaying(),
this.player.isMuted())
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM)
.putExtra(Constants.KEY_URL, this.player.getVideoUrl())
.putExtra(Constants.KEY_TITLE, this.player.getVideoTitle())
.putExtra(Constants.KEY_SERVICE_ID, this.player.getCurrentMetadata().getMetadata().getServiceId())
.putExtra(VideoPlayer.PLAYER_TYPE, playerType);
}
////////////////////////////////////////////////////////////////////////////
@ -219,7 +225,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
}
private void unbind() {
if(serviceBound) {
if (serviceBound) {
unbindService(serviceConnection);
serviceBound = false;
stopPlayerListener();
@ -227,8 +233,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
if (player != null && player.getPlayQueueAdapter() != null) {
player.getPlayQueueAdapter().unsetSelectedListener();
}
if (itemsList != null) itemsList.setAdapter(null);
if (itemTouchHelper != null) itemTouchHelper.attachToRecyclerView(null);
if (itemsList != null) {
itemsList.setAdapter(null);
}
if (itemTouchHelper != null) {
itemTouchHelper.attachToRecyclerView(null);
}
itemsList = null;
itemTouchHelper = null;
@ -239,12 +249,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private ServiceConnection getServiceConnection() {
return new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
public void onServiceDisconnected(final ComponentName name) {
Log.d(getTag(), "Player service is disconnected");
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
public void onServiceConnected(final ComponentName name, final IBinder service) {
Log.d(getTag(), "Player service is connected");
if (service instanceof PlayerServiceBinder) {
@ -253,8 +263,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
player = ((MainPlayer.LocalBinder) service).getPlayer();
}
if (player == null || player.getPlayQueue() == null ||
player.getPlayQueueAdapter() == null || player.getPlayer() == null) {
if (player == null || player.getPlayQueue() == null
|| player.getPlayQueueAdapter() == null || player.getPlayer() == null) {
unbind();
finish();
} else {
@ -315,56 +325,60 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private void buildControls() {
repeatButton = rootView.findViewById(R.id.control_repeat);
backwardButton = rootView.findViewById(R.id.control_backward);
fastRewindButton = rootView.findViewById(R.id.control_fast_rewind);
playPauseButton = rootView.findViewById(R.id.control_play_pause);
fastForwardButton = rootView.findViewById(R.id.control_fast_forward);
forwardButton = rootView.findViewById(R.id.control_forward);
shuffleButton = rootView.findViewById(R.id.control_shuffle);
playbackSpeedButton = rootView.findViewById(R.id.control_playback_speed);
playbackPitchButton = rootView.findViewById(R.id.control_playback_pitch);
progressBar = rootView.findViewById(R.id.control_progress_bar);
repeatButton.setOnClickListener(this);
backwardButton.setOnClickListener(this);
fastRewindButton.setOnClickListener(this);
playPauseButton.setOnClickListener(this);
fastForwardButton.setOnClickListener(this);
forwardButton.setOnClickListener(this);
shuffleButton.setOnClickListener(this);
playbackSpeedButton.setOnClickListener(this);
playbackPitchButton.setOnClickListener(this);
}
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
final PopupMenu menu = new PopupMenu(this, view);
final MenuItem remove = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/0,
final PopupMenu popupMenu = new PopupMenu(this, view);
final MenuItem remove = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 0,
Menu.NONE, R.string.play_queue_remove);
remove.setOnMenuItemClickListener(menuItem -> {
if (player == null) return false;
if (player == null) {
return false;
}
final int index = player.getPlayQueue().indexOf(item);
if (index != -1) player.getPlayQueue().remove(index);
if (index != -1) {
player.getPlayQueue().remove(index);
}
return true;
});
final MenuItem detail = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/1,
final MenuItem detail = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 1,
Menu.NONE, R.string.play_queue_stream_detail);
detail.setOnMenuItemClickListener(menuItem -> {
onOpenDetail(item.getServiceId(), item.getUrl(), item.getTitle());
return true;
});
final MenuItem append = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/2,
final MenuItem append = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 2,
Menu.NONE, R.string.append_playlist);
append.setOnMenuItemClickListener(menuItem -> {
openPlaylistAppendDialog(Collections.singletonList(item));
return true;
});
final MenuItem share = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/3,
final MenuItem share = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 3,
Menu.NONE, R.string.share);
share.setOnMenuItemClickListener(menuItem -> {
shareUrl(item.getTitle(), item.getUrl());
return true;
});
menu.show();
popupMenu.show();
}
////////////////////////////////////////////////////////////////////////////
@ -374,8 +388,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private OnScrollBelowItemsListener getQueueScrollListener() {
return new OnScrollBelowItemsListener() {
@Override
public void onScrolledDown(RecyclerView recyclerView) {
if (player != null && player.getPlayQueue() != null && !player.getPlayQueue().isComplete()) {
public void onScrolledDown(final RecyclerView recyclerView) {
if (player != null && player.getPlayQueue() != null
&& !player.getPlayQueue().isComplete()) {
player.getPlayQueue().fetch();
} else if (itemsList != null) {
itemsList.clearOnScrollListeners();
@ -387,13 +402,17 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
return new PlayQueueItemTouchCallback() {
@Override
public void onMove(int sourceIndex, int targetIndex) {
if (player != null) player.getPlayQueue().move(sourceIndex, targetIndex);
public void onMove(final int sourceIndex, final int targetIndex) {
if (player != null) {
player.getPlayQueue().move(sourceIndex, targetIndex);
}
}
@Override
public void onSwiped(int index) {
if (index != -1) player.getPlayQueue().remove(index);
public void onSwiped(final int index) {
if (index != -1) {
player.getPlayQueue().remove(index);
}
}
};
}
@ -401,31 +420,42 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() {
return new PlayQueueItemBuilder.OnSelectedListener() {
@Override
public void selected(PlayQueueItem item, View view) {
if (player != null) player.onSelected(item);
public void selected(final PlayQueueItem item, final View view) {
if (player != null) {
player.onSelected(item);
}
}
@Override
public void held(PlayQueueItem item, View view) {
if (player == null) return;
public void held(final PlayQueueItem item, final View view) {
if (player == null) {
return;
}
final int index = player.getPlayQueue().indexOf(item);
if (index != -1) buildItemPopupMenu(item, view);
if (index != -1) {
buildItemPopupMenu(item, view);
}
}
@Override
public void onStartDrag(PlayQueueItemHolder viewHolder) {
if (itemTouchHelper != null) itemTouchHelper.startDrag(viewHolder);
public void onStartDrag(final PlayQueueItemHolder viewHolder) {
if (itemTouchHelper != null) {
itemTouchHelper.startDrag(viewHolder);
}
}
};
}
private void onOpenDetail(int serviceId, String videoUrl, String videoTitle) {
private void onOpenDetail(final int serviceId, final String videoUrl,
final String videoTitle) {
NavigationHelper.openVideoDetail(this, serviceId, videoUrl, videoTitle);
}
private void scrollToSelected() {
if (player == null) return;
if (player == null) {
return;
}
final int currentPlayingIndex = player.getPlayQueue().getIndex();
final int currentVisibleIndex;
@ -449,36 +479,29 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
////////////////////////////////////////////////////////////////////////////
@Override
public void onClick(View view) {
if (player == null) return;
public void onClick(final View view) {
if (player == null) {
return;
}
if (view.getId() == repeatButton.getId()) {
player.onRepeatClicked();
} else if (view.getId() == backwardButton.getId()) {
player.onPlayPrevious();
} else if (view.getId() == fastRewindButton.getId()) {
player.onFastRewind();
} else if (view.getId() == playPauseButton.getId()) {
player.onPlayPause();
} else if (view.getId() == fastForwardButton.getId()) {
player.onFastForward();
} else if (view.getId() == forwardButton.getId()) {
player.onPlayNext();
} else if (view.getId() == shuffleButton.getId()) {
player.onShuffleClicked();
} else if (view.getId() == playbackSpeedButton.getId()) {
openPlaybackParameterDialog();
} else if (view.getId() == playbackPitchButton.getId()) {
openPlaybackParameterDialog();
} else if (view.getId() == metadata.getId()) {
scrollToSelected();
} else if (view.getId() == progressLiveSync.getId()) {
player.seekToDefault();
}
}
@ -487,14 +510,16 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
////////////////////////////////////////////////////////////////////////////
private void openPlaybackParameterDialog() {
if (player == null) return;
if (player == null) {
return;
}
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(),
player.getPlaybackSkipSilence(), this).show(getSupportFragmentManager(), getTag());
}
@Override
public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch,
boolean playbackSkipSilence) {
public void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch,
final boolean playbackSkipSilence) {
if (player != null) {
player.setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence);
}
@ -505,7 +530,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
////////////////////////////////////////////////////////////////////////////
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
public void onProgressChanged(final SeekBar seekBar, final int progress,
final boolean fromUser) {
if (fromUser) {
final String seekTime = Localization.getDurationString(progress / 1000);
progressCurrentTime.setText(seekTime);
@ -514,14 +540,16 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
public void onStartTrackingTouch(final SeekBar seekBar) {
seeking = true;
seekDisplay.setVisibility(View.VISIBLE);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
if (player != null) player.seekTo(seekBar.getProgress());
public void onStopTrackingTouch(final SeekBar seekBar) {
if (player != null) {
player.seekTo(seekBar.getProgress());
}
seekDisplay.setVisibility(View.GONE);
seeking = false;
}
@ -545,7 +573,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
// Share
////////////////////////////////////////////////////////////////////////////
private void shareUrl(String subject, String url) {
private void shareUrl(final String subject, final String url) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
@ -562,17 +590,21 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
}
@Override
public void onPlaybackUpdate(int state, int repeatMode, boolean shuffled, PlaybackParameters parameters) {
public void onPlaybackUpdate(final int state, final int repeatMode, final boolean shuffled,
final PlaybackParameters parameters) {
onStateChanged(state);
onPlayModeChanged(repeatMode, shuffled);
onPlaybackParameterChanged(parameters);
onMaybePlaybackAdapterChanged();
onMaybeMuteChanged();
}
@Override
public void onProgressUpdate(int currentProgress, int duration, int bufferPercent) {
public void onProgressUpdate(final int currentProgress, final int duration,
final int bufferPercent) {
// Set buffer progress
progressSeekBar.setSecondaryProgress((int) (progressSeekBar.getMax() * ((float) bufferPercent / 100)));
progressSeekBar.setSecondaryProgress((int) (progressSeekBar.getMax()
* ((float) bufferPercent / 100)));
// Set Duration
progressSeekBar.setMax(duration);
@ -596,7 +628,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
}
@Override
public void onMetadataUpdate(StreamInfo info, PlayQueue queue) {
public void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) {
if (info != null) {
metadataTitle.setText(info.getName());
metadataArtist.setText(info.getUploaderName());
@ -630,13 +662,13 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private void onStateChanged(final int state) {
switch (state) {
case BasePlayer.STATE_PAUSED:
playPauseButton.setImageResource(R.drawable.ic_play_arrow_white);
playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp);
break;
case BasePlayer.STATE_PLAYING:
playPauseButton.setImageResource(R.drawable.ic_pause_white);
playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp);
break;
case BasePlayer.STATE_COMPLETED:
playPauseButton.setImageResource(R.drawable.ic_replay_white);
playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp);
break;
default:
break;
@ -677,16 +709,38 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private void onPlaybackParameterChanged(final PlaybackParameters parameters) {
if (parameters != null) {
playbackSpeedButton.setText(formatSpeed(parameters.speed));
playbackPitchButton.setText(formatPitch(parameters.pitch));
if (menu != null && player != null) {
final MenuItem item = menu.findItem(R.id.action_playback_speed);
item.setTitle(formatSpeed(parameters.speed));
}
}
}
private void onMaybePlaybackAdapterChanged() {
if (itemsList == null || player == null) return;
if (itemsList == null || player == null) {
return;
}
final PlayQueueAdapter maybeNewAdapter = player.getPlayQueueAdapter();
if (maybeNewAdapter != null && itemsList.getAdapter() != maybeNewAdapter) {
itemsList.setAdapter(maybeNewAdapter);
}
}
private void onMaybeMuteChanged() {
if (menu != null && player != null) {
MenuItem item = menu.findItem(R.id.action_mute);
//Change the mute-button item in ActionBar
//1) Text change:
item.setTitle(player.isMuted() ? R.string.unmute : R.string.mute);
//2) Icon change accordingly to current App Theme
// using rootView.getContext() because getApplicationContext() didn't work
item.setIcon(player.isMuted()
? ThemeHelper.resolveResourceIdFromAttr(rootView.getContext(),
R.attr.ic_volume_off)
: ThemeHelper.resolveResourceIdFromAttr(rootView.getContext(),
R.attr.ic_volume_up));
}
}
}

View file

@ -39,7 +39,7 @@ import android.widget.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.appcompat.content.res.AppCompatResources;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.PlaybackParameters;
@ -63,6 +63,7 @@ import org.schabi.newpipe.player.resolver.MediaSourceTag;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.views.ExpandableSurfaceView;
import org.schabi.newpipe.views.ExpandableSurfaceView;
import java.util.ArrayList;
import java.util.List;
@ -72,7 +73,7 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
/**
* Base for <b>video</b> players
* Base for <b>video</b> players.
*
* @author mauriciocolli
*/
@ -84,23 +85,27 @@ public abstract class VideoPlayer extends BasePlayer
Player.EventListener,
PopupMenu.OnMenuItemClickListener,
PopupMenu.OnDismissListener {
public static final boolean DEBUG = BasePlayer.DEBUG;
public final String TAG;
public static final boolean DEBUG = BasePlayer.DEBUG;
/*//////////////////////////////////////////////////////////////////////////
// Player
//////////////////////////////////////////////////////////////////////////*/
protected static final int RENDERER_UNAVAILABLE = -1;
public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis
public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
public static final int DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds
protected static final int RENDERER_UNAVAILABLE = -1;
@NonNull
private final VideoPlaybackResolver resolver;
private List<VideoStream> availableStreams;
private int selectedStreamIndex;
protected boolean wasPlaying = false;
@NonNull final private VideoPlaybackResolver resolver;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
@ -136,6 +141,7 @@ public abstract class VideoPlayer extends BasePlayer
private final Handler controlsVisibilityHandler = new Handler();
boolean isSomePopupMenuVisible = false;
private final int qualityPopupMenuGroupId = 69;
private PopupMenu qualityPopupMenu;
@ -147,49 +153,62 @@ public abstract class VideoPlayer extends BasePlayer
///////////////////////////////////////////////////////////////////////////
public VideoPlayer(String debugTag, Context context) {
public VideoPlayer(final String debugTag, final Context context) {
super(context);
this.TAG = debugTag;
this.resolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver());
}
public void setup(View rootView) {
initViews(rootView);
// workaround to match normalized captions like english to English or deutsch to Deutsch
private static boolean containsCaseInsensitive(final List<String> list, final String toFind) {
for (String i : list) {
if (i.equalsIgnoreCase(toFind)) {
return true;
}
}
return false;
}
public void setup(final View view) {
initViews(view);
setup();
}
public void initViews(View rootView) {
this.rootView = rootView;
this.surfaceView = rootView.findViewById(R.id.surfaceView);
this.surfaceForeground = rootView.findViewById(R.id.surfaceForeground);
this.loadingPanel = rootView.findViewById(R.id.loading_panel);
this.endScreen = rootView.findViewById(R.id.endScreen);
this.controlAnimationView = rootView.findViewById(R.id.controlAnimationView);
this.controlsRoot = rootView.findViewById(R.id.playbackControlRoot);
this.currentDisplaySeek = rootView.findViewById(R.id.currentDisplaySeek);
this.playbackSeekBar = rootView.findViewById(R.id.playbackSeekBar);
this.playbackCurrentTime = rootView.findViewById(R.id.playbackCurrentTime);
this.playbackEndTime = rootView.findViewById(R.id.playbackEndTime);
this.playbackLiveSync = rootView.findViewById(R.id.playbackLiveSync);
this.playbackSpeedTextView = rootView.findViewById(R.id.playbackSpeed);
this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls);
this.topControlsRoot = rootView.findViewById(R.id.topControls);
this.qualityTextView = rootView.findViewById(R.id.qualityTextView);
public void initViews(final View view) {
this.rootView = view;
this.surfaceView = view.findViewById(R.id.surfaceView);
this.surfaceForeground = view.findViewById(R.id.surfaceForeground);
this.loadingPanel = view.findViewById(R.id.loading_panel);
this.endScreen = view.findViewById(R.id.endScreen);
this.controlAnimationView = view.findViewById(R.id.controlAnimationView);
this.controlsRoot = view.findViewById(R.id.playbackControlRoot);
this.currentDisplaySeek = view.findViewById(R.id.currentDisplaySeek);
this.playbackSeekBar = view.findViewById(R.id.playbackSeekBar);
this.playbackCurrentTime = view.findViewById(R.id.playbackCurrentTime);
this.playbackEndTime = view.findViewById(R.id.playbackEndTime);
this.playbackLiveSync = view.findViewById(R.id.playbackLiveSync);
this.playbackSpeedTextView = view.findViewById(R.id.playbackSpeed);
this.bottomControlsRoot = view.findViewById(R.id.bottomControls);
this.topControlsRoot = view.findViewById(R.id.topControls);
this.qualityTextView = view.findViewById(R.id.qualityTextView);
this.subtitleView = rootView.findViewById(R.id.subtitleView);
this.subtitleView = view.findViewById(R.id.subtitleView);
final float captionScale = PlayerHelper.getCaptionScale(context);
final CaptionStyleCompat captionStyle = PlayerHelper.getCaptionStyle(context);
setupSubtitleView(subtitleView, captionScale, captionStyle);
this.resizeView = rootView.findViewById(R.id.resizeTextView);
resizeView.setText(PlayerHelper.resizeTypeOf(context, getSurfaceView().getResizeMode()));
this.resizeView = view.findViewById(R.id.resizeTextView);
resizeView.setText(PlayerHelper
.resizeTypeOf(context, getSurfaceView().getResizeMode()));
this.captionTextView = rootView.findViewById(R.id.captionTextView);
this.captionTextView = view.findViewById(R.id.captionTextView);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
this.playbackSeekBar.getProgressDrawable().setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY);
}
this.playbackSeekBar.getProgressDrawable().
setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY);
this.qualityPopupMenu = new PopupMenu(context, qualityTextView);
this.playbackSpeedPopupMenu = new PopupMenu(context, playbackSpeedTextView);
@ -199,9 +218,8 @@ public abstract class VideoPlayer extends BasePlayer
.getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY);
}
protected abstract void setupSubtitleView(@NonNull SubtitleView view,
final float captionScale,
@NonNull final CaptionStyleCompat captionStyle);
protected abstract void setupSubtitleView(@NonNull SubtitleView view, float captionScale,
@NonNull CaptionStyleCompat captionStyle);
@Override
public void initListeners() {
@ -233,7 +251,9 @@ public abstract class VideoPlayer extends BasePlayer
@Override
public void handleIntent(final Intent intent) {
if (intent == null) return;
if (intent == null) {
return;
}
if (intent.hasExtra(PLAYBACK_QUALITY)) {
setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY));
@ -247,13 +267,15 @@ public abstract class VideoPlayer extends BasePlayer
//////////////////////////////////////////////////////////////////////////*/
public void buildQualityMenu() {
if (qualityPopupMenu == null) return;
if (qualityPopupMenu == null) {
return;
}
qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId);
for (int i = 0; i < availableStreams.size(); i++) {
VideoStream videoStream = availableStreams.get(i);
qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE,
MediaFormat.getNameById(videoStream.getFormatId()) + " " + videoStream.resolution);
qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat
.getNameById(videoStream.getFormatId()) + " " + videoStream.resolution);
}
if (getSelectedVideoStream() != null) {
qualityTextView.setText(getSelectedVideoStream().resolution);
@ -263,11 +285,14 @@ public abstract class VideoPlayer extends BasePlayer
}
private void buildPlaybackSpeedMenu() {
if (playbackSpeedPopupMenu == null) return;
if (playbackSpeedPopupMenu == null) {
return;
}
playbackSpeedPopupMenu.getMenu().removeGroup(playbackSpeedPopupMenuGroupId);
for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) {
playbackSpeedPopupMenu.getMenu().add(playbackSpeedPopupMenuGroupId, i, Menu.NONE, formatSpeed(PLAYBACK_SPEEDS[i]));
playbackSpeedPopupMenu.getMenu().add(playbackSpeedPopupMenuGroupId, i, Menu.NONE,
formatSpeed(PLAYBACK_SPEEDS[i]));
}
playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed()));
playbackSpeedPopupMenu.setOnMenuItemClickListener(this);
@ -275,7 +300,9 @@ public abstract class VideoPlayer extends BasePlayer
}
private void buildCaptionMenu(final List<String> availableLanguages) {
if (captionPopupMenu == null) return;
if (captionPopupMenu == null) {
return;
}
captionPopupMenu.getMenu().removeGroup(captionPopupMenuGroupId);
String userPreferredLanguage = PreferenceManager.getDefaultSharedPreferences(context)
@ -286,8 +313,8 @@ public abstract class VideoPlayer extends BasePlayer
* we are only looking for "(" instead of "(auto-generated)" to hopefully get all
* internationalized variants such as "(automatisch-erzeugt)" and so on
*/
boolean searchForAutogenerated = userPreferredLanguage != null &&
!userPreferredLanguage.contains("(");
boolean searchForAutogenerated = userPreferredLanguage != null
&& !userPreferredLanguage.contains("(");
// Add option for turning off caption
MenuItem captionOffItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId,
@ -314,18 +341,19 @@ public abstract class VideoPlayer extends BasePlayer
trackSelector.setPreferredTextLanguage(captionLanguage);
trackSelector.setParameters(trackSelector.buildUponParameters()
.setRendererDisabled(textRendererIndex, false));
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(context);
prefs.edit().putString(context.getString(R.string.caption_user_set_key),
captionLanguage).commit();
}
return true;
});
// apply caption language from previous user preference
if (userPreferredLanguage != null && (captionLanguage.equals(userPreferredLanguage) ||
searchForAutogenerated && captionLanguage.startsWith(userPreferredLanguage) ||
userPreferredLanguage.contains("(") &&
captionLanguage.startsWith(userPreferredLanguage.substring(0,
userPreferredLanguage.indexOf('('))))) {
if (userPreferredLanguage != null && (captionLanguage.equals(userPreferredLanguage)
|| searchForAutogenerated && captionLanguage.startsWith(userPreferredLanguage)
|| userPreferredLanguage.contains("(") && captionLanguage.startsWith(
userPreferredLanguage
.substring(0, userPreferredLanguage.indexOf('('))))) {
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
if (textRendererIndex != RENDERER_UNAVAILABLE) {
trackSelector.setPreferredTextLanguage(captionLanguage);
@ -339,7 +367,9 @@ public abstract class VideoPlayer extends BasePlayer
}
private void updateStreamRelatedViews() {
if (getCurrentMetadata() == null) return;
if (getCurrentMetadata() == null) {
return;
}
final MediaSourceTag tag = getCurrentMetadata();
final StreamInfo metadata = tag.getMetadata();
@ -370,8 +400,10 @@ public abstract class VideoPlayer extends BasePlayer
break;
case VIDEO_STREAM:
if (metadata.getVideoStreams().size() + metadata.getVideoOnlyStreams().size() == 0)
if (metadata.getVideoStreams().size() + metadata.getVideoOnlyStreams().size()
== 0) {
break;
}
availableStreams = tag.getSortedAvailableVideoStreams();
selectedStreamIndex = tag.getSelectedVideoStreamIndex();
@ -388,6 +420,7 @@ public abstract class VideoPlayer extends BasePlayer
buildPlaybackSpeedMenu();
playbackSpeedTextView.setVisibility(View.VISIBLE);
}
/*//////////////////////////////////////////////////////////////////////////
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
@ -417,9 +450,11 @@ public abstract class VideoPlayer extends BasePlayer
animateView(controlsRoot, false, DEFAULT_CONTROLS_DURATION);
playbackSeekBar.setEnabled(false);
// Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
// Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-,
// so sets the color again
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
}
loadingPanel.setBackgroundColor(Color.BLACK);
animateView(loadingPanel, true, 0);
@ -435,9 +470,11 @@ public abstract class VideoPlayer extends BasePlayer
showAndAnimateControl(-1, true);
playbackSeekBar.setEnabled(true);
// Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
// Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-,
// so sets the color again
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
}
loadingPanel.setVisibility(View.GONE);
@ -446,20 +483,26 @@ public abstract class VideoPlayer extends BasePlayer
@Override
public void onBuffering() {
if (DEBUG) Log.d(TAG, "onBuffering() called");
if (DEBUG) {
Log.d(TAG, "onBuffering() called");
}
loadingPanel.setBackgroundColor(Color.TRANSPARENT);
}
@Override
public void onPaused() {
if (DEBUG) Log.d(TAG, "onPaused() called");
if (DEBUG) {
Log.d(TAG, "onPaused() called");
}
showControls(400);
loadingPanel.setVisibility(View.GONE);
}
@Override
public void onPausedSeek() {
if (DEBUG) Log.d(TAG, "onPausedSeek() called");
if (DEBUG) {
Log.d(TAG, "onPausedSeek() called");
}
showAndAnimateControl(-1, true);
}
@ -479,21 +522,28 @@ public abstract class VideoPlayer extends BasePlayer
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
public void onTracksChanged(final TrackGroupArray trackGroups,
final TrackSelectionArray trackSelections) {
super.onTracksChanged(trackGroups, trackSelections);
onTextTrackUpdate();
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
public void onPlaybackParametersChanged(final PlaybackParameters playbackParameters) {
super.onPlaybackParametersChanged(playbackParameters);
playbackSpeedTextView.setText(formatSpeed(playbackParameters.speed));
}
@Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
public void onVideoSizeChanged(final int width, final int height,
final int unappliedRotationDegrees,
final float pixelWidthHeightRatio) {
if (DEBUG) {
Log.d(TAG, "onVideoSizeChanged() called with: width / height = [" + width + " / " + height + " = " + (((float) width) / height) + "], unappliedRotationDegrees = [" + unappliedRotationDegrees + "], pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]");
Log.d(TAG, "onVideoSizeChanged() called with: "
+ "width / height = [" + width + " / " + height
+ " = " + (((float) width) / height) + "], "
+ "unappliedRotationDegrees = [" + unappliedRotationDegrees + "], "
+ "pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]");
}
getSurfaceView().setAspectRatio(((float) width) / height);
}
@ -510,8 +560,11 @@ public abstract class VideoPlayer extends BasePlayer
private void onTextTrackUpdate() {
final int textRenderer = getRendererIndex(C.TRACK_TYPE_TEXT);
if (captionTextView == null) return;
if (trackSelector.getCurrentMappedTrackInfo() == null || textRenderer == RENDERER_UNAVAILABLE) {
if (captionTextView == null) {
return;
}
if (trackSelector.getCurrentMappedTrackInfo() == null
|| textRenderer == RENDERER_UNAVAILABLE) {
captionTextView.setVisibility(View.GONE);
return;
}
@ -532,8 +585,8 @@ public abstract class VideoPlayer extends BasePlayer
final String preferredLanguage = trackSelector.getPreferredTextLanguage();
// Build UI
buildCaptionMenu(availableLanguages);
if (trackSelector.getParameters().getRendererDisabled(textRenderer) ||
preferredLanguage == null || (!availableLanguages.contains(preferredLanguage)
if (trackSelector.getParameters().getRendererDisabled(textRenderer)
|| preferredLanguage == null || (!availableLanguages.contains(preferredLanguage)
&& !containsCaseInsensitive(availableLanguages, preferredLanguage))) {
captionTextView.setText(R.string.caption_none);
} else {
@ -542,22 +595,15 @@ public abstract class VideoPlayer extends BasePlayer
captionTextView.setVisibility(availableLanguages.isEmpty() ? View.GONE : View.VISIBLE);
}
// workaround to match normalized captions like english to English or deutsch to Deutsch
private static boolean containsCaseInsensitive(List<String> list, String toFind) {
for(String i : list){
if(i.equalsIgnoreCase(toFind))
return true;
}
return false;
}
/*//////////////////////////////////////////////////////////////////////////
// General Player
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onPrepared(boolean playWhenReady) {
if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
public void onPrepared(final boolean playWhenReady) {
if (DEBUG) {
Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
}
playbackSeekBar.setMax((int) simpleExoPlayer.getDuration());
playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration()));
@ -569,34 +615,48 @@ public abstract class VideoPlayer extends BasePlayer
@Override
public void destroy() {
super.destroy();
if (endScreen != null) endScreen.setImageBitmap(null);
if (endScreen != null) {
endScreen.setImageBitmap(null);
}
}
@Override
public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) {
if (!isPrepared()) return;
public void onUpdateProgress(final int currentProgress, final int duration,
final int bufferPercent) {
if (!isPrepared()) {
return;
}
if (duration != playbackSeekBar.getMax()) {
playbackEndTime.setText(getTimeString(duration));
playbackSeekBar.setMax(duration);
}
if (currentState != STATE_PAUSED) {
if (currentState != STATE_PAUSED_SEEK) playbackSeekBar.setProgress(currentProgress);
if (currentState != STATE_PAUSED_SEEK) {
playbackSeekBar.setProgress(currentProgress);
}
playbackCurrentTime.setText(getTimeString(currentProgress));
}
if (simpleExoPlayer.isLoading() || bufferPercent > 90) {
playbackSeekBar.setSecondaryProgress((int) (playbackSeekBar.getMax() * ((float) bufferPercent / 100)));
playbackSeekBar.setSecondaryProgress(
(int) (playbackSeekBar.getMax() * ((float) bufferPercent / 100)));
}
if (DEBUG && bufferPercent % 20 == 0) { //Limit log
Log.d(TAG, "updateProgress() called with: isVisible = " + isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]");
Log.d(TAG, "updateProgress() called with: "
+ "isVisible = " + isControlsVisible() + ", "
+ "currentProgress = [" + currentProgress + "], "
+ "duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]");
}
playbackLiveSync.setClickable(!isLiveEdge());
}
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
public void onLoadingComplete(final String imageUri, final View view,
final Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
if (loadedImage != null) endScreen.setImageBitmap(loadedImage);
if (loadedImage != null) {
endScreen.setImageBitmap(loadedImage);
}
}
protected void toggleFullscreen() {
@ -606,13 +666,13 @@ public abstract class VideoPlayer extends BasePlayer
@Override
public void onFastRewind() {
super.onFastRewind();
showAndAnimateControl(R.drawable.ic_action_av_fast_rewind, true);
showAndAnimateControl(R.drawable.ic_fast_rewind_white_24dp, true);
}
@Override
public void onFastForward() {
super.onFastForward();
showAndAnimateControl(R.drawable.ic_action_av_fast_forward, true);
showAndAnimateControl(R.drawable.ic_fast_forward_white_24dp, true);
}
/*//////////////////////////////////////////////////////////////////////////
@ -620,8 +680,10 @@ public abstract class VideoPlayer extends BasePlayer
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onClick(View v) {
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
public void onClick(final View v) {
if (DEBUG) {
Log.d(TAG, "onClick() called with: v = [" + v + "]");
}
if (v.getId() == qualityTextView.getId()) {
onQualitySelectorClicked();
} else if (v.getId() == playbackSpeedTextView.getId()) {
@ -636,17 +698,22 @@ public abstract class VideoPlayer extends BasePlayer
}
/**
* Called when an item of the quality selector or the playback speed selector is selected
* Called when an item of the quality selector or the playback speed selector is selected.
*/
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
if (DEBUG)
Log.d(TAG, "onMenuItemClick() called with: menuItem = [" + menuItem + "], menuItem.getItemId = [" + menuItem.getItemId() + "]");
public boolean onMenuItemClick(final MenuItem menuItem) {
if (DEBUG) {
Log.d(TAG, "onMenuItemClick() called with: "
+ "menuItem = [" + menuItem + "], "
+ "menuItem.getItemId = [" + menuItem.getItemId() + "]");
}
if (qualityPopupMenuGroupId == menuItem.getGroupId()) {
final int menuItemIndex = menuItem.getItemId();
if (selectedStreamIndex == menuItemIndex ||
availableStreams == null || availableStreams.size() <= menuItemIndex) return true;
if (selectedStreamIndex == menuItemIndex || availableStreams == null
|| availableStreams.size() <= menuItemIndex) {
return true;
}
final String newResolution = availableStreams.get(menuItemIndex).resolution;
setRecovery();
@ -667,11 +734,13 @@ public abstract class VideoPlayer extends BasePlayer
}
/**
* Called when some popup menu is dismissed
* Called when some popup menu is dismissed.
*/
@Override
public void onDismiss(PopupMenu menu) {
if (DEBUG) Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]");
public void onDismiss(final PopupMenu menu) {
if (DEBUG) {
Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]");
}
isSomePopupMenuVisible = false;
if (getSelectedVideoStream() != null) {
qualityTextView.setText(getSelectedVideoStream().resolution);
@ -679,7 +748,9 @@ public abstract class VideoPlayer extends BasePlayer
}
public void onQualitySelectorClicked() {
if (DEBUG) Log.d(TAG, "onQualitySelectorClicked() called");
if (DEBUG) {
Log.d(TAG, "onQualitySelectorClicked() called");
}
qualityPopupMenu.show();
isSomePopupMenuVisible = true;
showControls(DEFAULT_CONTROLS_DURATION);
@ -695,14 +766,18 @@ public abstract class VideoPlayer extends BasePlayer
}
public void onPlaybackSpeedClicked() {
if (DEBUG) Log.d(TAG, "onPlaybackSpeedClicked() called");
if (DEBUG) {
Log.d(TAG, "onPlaybackSpeedClicked() called");
}
playbackSpeedPopupMenu.show();
isSomePopupMenuVisible = true;
showControls(DEFAULT_CONTROLS_DURATION);
}
private void onCaptionClicked() {
if (DEBUG) Log.d(TAG, "onCaptionClicked() called");
if (DEBUG) {
Log.d(TAG, "onCaptionClicked() called");
}
captionPopupMenu.show();
isSomePopupMenuVisible = true;
showControls(DEFAULT_CONTROLS_DURATION);
@ -721,26 +796,38 @@ public abstract class VideoPlayer extends BasePlayer
getResizeView().setText(PlayerHelper.resizeTypeOf(context, resizeMode));
}
protected abstract int nextResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode);
protected abstract int nextResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode);
/*//////////////////////////////////////////////////////////////////////////
// SeekBar Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (DEBUG && fromUser) Log.d(TAG, "onProgressChanged() called with: seekBar = [" + seekBar + "], progress = [" + progress + "]");
public void onProgressChanged(final SeekBar seekBar, final int progress,
final boolean fromUser) {
if (DEBUG && fromUser) {
Log.d(TAG, "onProgressChanged() called with: "
+ "seekBar = [" + seekBar + "], progress = [" + progress + "]");
}
//if (fromUser) playbackCurrentTime.setText(getTimeString(progress));
if (fromUser) currentDisplaySeek.setText(getTimeString(progress));
if (fromUser) {
currentDisplaySeek.setText(getTimeString(progress));
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
if (DEBUG) Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]");
if (getCurrentState() != STATE_PAUSED_SEEK) changeState(STATE_PAUSED_SEEK);
public void onStartTrackingTouch(final SeekBar seekBar) {
if (DEBUG) {
Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]");
}
if (getCurrentState() != STATE_PAUSED_SEEK) {
changeState(STATE_PAUSED_SEEK);
}
wasPlaying = simpleExoPlayer.getPlayWhenReady();
if (isPlaying()) simpleExoPlayer.setPlayWhenReady(false);
if (isPlaying()) {
simpleExoPlayer.setPlayWhenReady(false);
}
showControls(0);
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, true,
@ -748,17 +835,25 @@ public abstract class VideoPlayer extends BasePlayer
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
if (DEBUG) Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]");
public void onStopTrackingTouch(final SeekBar seekBar) {
if (DEBUG) {
Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]");
}
seekTo(seekBar.getProgress());
if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) simpleExoPlayer.setPlayWhenReady(true);
if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) {
simpleExoPlayer.setPlayWhenReady(true);
}
playbackCurrentTime.setText(getTimeString(seekBar.getProgress()));
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200);
if (getCurrentState() == STATE_PAUSED_SEEK) changeState(STATE_BUFFERING);
if (!isProgressLoopRunning()) startProgressLoop();
if (getCurrentState() == STATE_PAUSED_SEEK) {
changeState(STATE_BUFFERING);
}
if (!isProgressLoopRunning()) {
startProgressLoop();
}
}
/*//////////////////////////////////////////////////////////////////////////
@ -766,7 +861,9 @@ public abstract class VideoPlayer extends BasePlayer
//////////////////////////////////////////////////////////////////////////*/
public int getRendererIndex(final int trackIndex) {
if (simpleExoPlayer == null) return RENDERER_UNAVAILABLE;
if (simpleExoPlayer == null) {
return RENDERER_UNAVAILABLE;
}
for (int t = 0; t < simpleExoPlayer.getRendererCount(); t++) {
if (simpleExoPlayer.getRendererType(t) == trackIndex) {
@ -782,15 +879,21 @@ public abstract class VideoPlayer extends BasePlayer
}
/**
* Show a animation, and depending on goneOnEnd, will stay on the screen or be gone
* Show a animation, and depending on goneOnEnd, will stay on the screen or be gone.
*
* @param drawableId the drawable that will be used to animate, pass -1 to clear any animation that is visible
* @param drawableId the drawable that will be used to animate,
* pass -1 to clear any animation that is visible
* @param goneOnEnd will set the animation view to GONE on the end of the animation
*/
public void showAndAnimateControl(final int drawableId, final boolean goneOnEnd) {
if (DEBUG) Log.d(TAG, "showAndAnimateControl() called with: drawableId = [" + drawableId + "], goneOnEnd = [" + goneOnEnd + "]");
if (DEBUG) {
Log.d(TAG, "showAndAnimateControl() called with: "
+ "drawableId = [" + drawableId + "], goneOnEnd = [" + goneOnEnd + "]");
}
if (controlViewAnimator != null && controlViewAnimator.isRunning()) {
if (DEBUG) Log.d(TAG, "showAndAnimateControl: controlViewAnimator.isRunning");
if (DEBUG) {
Log.d(TAG, "showAndAnimateControl: controlViewAnimator.isRunning");
}
controlViewAnimator.end();
}
@ -803,7 +906,7 @@ public abstract class VideoPlayer extends BasePlayer
).setDuration(DEFAULT_CONTROLS_DURATION);
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
public void onAnimationEnd(final Animator animation) {
controlAnimationView.setVisibility(View.GONE);
}
});
@ -812,8 +915,10 @@ public abstract class VideoPlayer extends BasePlayer
return;
}
float scaleFrom = goneOnEnd ? 1.0f : 1.0f, scaleTo = goneOnEnd ? 1.8f : 1.4f;
float alphaFrom = goneOnEnd ? 1.0f : 0.0f, alphaTo = goneOnEnd ? 0.0f : 1.0f;
float scaleFrom = goneOnEnd ? 1f : 1f;
float scaleTo = goneOnEnd ? 1.8f : 1.4f;
float alphaFrom = goneOnEnd ? 1f : 0f;
float alphaTo = goneOnEnd ? 0f : 1f;
controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(controlAnimationView,
@ -824,15 +929,18 @@ public abstract class VideoPlayer extends BasePlayer
controlViewAnimator.setDuration(goneOnEnd ? 1000 : 500);
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (goneOnEnd) controlAnimationView.setVisibility(View.GONE);
else controlAnimationView.setVisibility(View.VISIBLE);
public void onAnimationEnd(final Animator animation) {
if (goneOnEnd) {
controlAnimationView.setVisibility(View.GONE);
} else {
controlAnimationView.setVisibility(View.VISIBLE);
}
}
});
controlAnimationView.setVisibility(View.VISIBLE);
controlAnimationView.setImageDrawable(ContextCompat.getDrawable(context, drawableId));
controlAnimationView.setImageDrawable(AppCompatResources.getDrawable(context, drawableId));
controlViewAnimator.start();
}
@ -841,35 +949,59 @@ public abstract class VideoPlayer extends BasePlayer
}
public void showControlsThenHide() {
if (DEBUG) Log.d(TAG, "showControlsThenHide() called");
if (DEBUG) {
Log.d(TAG, "showControlsThenHide() called");
}
final int hideTime = controlsRoot.isInTouchMode()
? DEFAULT_CONTROLS_HIDE_TIME
: DPAD_CONTROLS_HIDE_TIME;
animateView(controlsRoot, true, DEFAULT_CONTROLS_DURATION, 0,
() -> hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME));
() -> hideControls(DEFAULT_CONTROLS_DURATION, hideTime));
}
public void showControls(long duration) {
if (DEBUG) Log.d(TAG, "showControls() called");
public void showControls(final long duration) {
if (DEBUG) {
Log.d(TAG, "showControls() called");
}
controlsVisibilityHandler.removeCallbacksAndMessages(null);
animateView(controlsRoot, true, duration);
}
public void hideControls(final long duration, long delay) {
if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]");
controlsVisibilityHandler.removeCallbacksAndMessages(null);
controlsVisibilityHandler.postDelayed(
() -> animateView(controlsRoot, false, duration), delay);
public void safeHideControls(final long duration, final long delay) {
if (DEBUG) {
Log.d(TAG, "safeHideControls() called with: delay = [" + delay + "]");
}
if (rootView.isInTouchMode()) {
controlsVisibilityHandler.removeCallbacksAndMessages(null);
controlsVisibilityHandler.postDelayed(
() -> animateView(controlsRoot, false, duration), delay);
}
}
public void hideControlsAndButton(final long duration, long delay, View button) {
if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]");
public void hideControls(final long duration, final long delay) {
if (DEBUG) {
Log.d(TAG, "hideControls() called with: delay = [" + delay + "]");
}
controlsVisibilityHandler.removeCallbacksAndMessages(null);
controlsVisibilityHandler.postDelayed(hideControlsAndButtonHandler(duration, button), delay);
controlsVisibilityHandler.postDelayed(() ->
animateView(controlsRoot, false, duration), delay);
}
private Runnable hideControlsAndButtonHandler(long duration, View videoPlayPause)
{
public void hideControlsAndButton(final long duration, final long delay, final View button) {
if (DEBUG) {
Log.d(TAG, "hideControls() called with: delay = [" + delay + "]");
}
controlsVisibilityHandler.removeCallbacksAndMessages(null);
controlsVisibilityHandler
.postDelayed(hideControlsAndButtonHandler(duration, button), delay);
}
private Runnable hideControlsAndButtonHandler(final long duration, final View videoPlayPause) {
return () -> {
videoPlayPause.setVisibility(View.INVISIBLE);
animateView(controlsRoot, false,duration);
animateView(controlsRoot, false, duration);
};
}
@ -879,15 +1011,15 @@ public abstract class VideoPlayer extends BasePlayer
// Getters and Setters
//////////////////////////////////////////////////////////////////////////*/
public void setPlaybackQuality(final String quality) {
this.resolver.setPlaybackQuality(quality);
}
@Nullable
public String getPlaybackQuality() {
return resolver.getPlaybackQuality();
}
public void setPlaybackQuality(final String quality) {
this.resolver.setPlaybackQuality(quality);
}
public ExpandableSurfaceView getSurfaceView() {
return surfaceView;
}
@ -898,9 +1030,9 @@ public abstract class VideoPlayer extends BasePlayer
@Nullable
public VideoStream getSelectedVideoStream() {
return (selectedStreamIndex >= 0 && availableStreams != null &&
availableStreams.size() > selectedStreamIndex) ?
availableStreams.get(selectedStreamIndex) : null;
return (selectedStreamIndex >= 0 && availableStreams != null
&& availableStreams.size() > selectedStreamIndex)
? availableStreams.get(selectedStreamIndex) : null;
}
public Handler getControlsVisibilityHandler() {
@ -911,7 +1043,7 @@ public abstract class VideoPlayer extends BasePlayer
return rootView;
}
public void setRootView(View rootView) {
public void setRootView(final View rootView) {
this.rootView = rootView;
}

View file

@ -42,6 +42,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.exoplayer2.ExoPlaybackException;
@ -84,6 +85,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateRotation;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex;
import static org.schabi.newpipe.util.ListHelper.getResolutionIndex;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
/**
* Unified UI for all players
@ -97,7 +99,17 @@ public class VideoPlayerImpl extends VideoPlayer
View.OnLongClickListener {
private static final String TAG = ".VideoPlayerImpl";
static final String POPUP_SAVED_WIDTH = "popup_saved_width";
static final String POPUP_SAVED_X = "popup_saved_x";
static final String POPUP_SAVED_Y = "popup_saved_y";
private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300;
private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS |
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
private static final float MAX_GESTURE_LENGTH = 0.75f;
private static final int NOTIFICATION_UPDATES_BEFORE_RESET = 60;
private TextView titleTextView;
private TextView channelTextView;
@ -116,6 +128,7 @@ public class VideoPlayerImpl extends VideoPlayer
private ImageButton fullscreenButton;
private ImageButton playerCloseButton;
private ImageButton screenRotationButton;
private ImageButton muteButton;
private ImageButton playPauseButton;
private ImageButton playPreviousButton;
@ -141,6 +154,7 @@ public class VideoPlayerImpl extends VideoPlayer
private boolean isFullscreen = false;
private boolean isVerticalVideo = false;
boolean shouldUpdateOnProgress;
int timesNotificationUpdated;
private final MainPlayer service;
private PlayerServiceEventListener fragmentListener;
@ -164,15 +178,6 @@ public class VideoPlayerImpl extends VideoPlayer
public boolean isPopupClosing = false;
static final String POPUP_SAVED_WIDTH = "popup_saved_width";
static final String POPUP_SAVED_X = "popup_saved_x";
static final String POPUP_SAVED_Y = "popup_saved_y";
private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300;
private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS |
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
private float screenWidth, screenHeight;
private float popupWidth, popupHeight;
private float minimumWidth, minimumHeight;
@ -226,40 +231,41 @@ public class VideoPlayerImpl extends VideoPlayer
@SuppressLint("ClickableViewAccessibility")
@Override
public void initViews(View rootView) {
super.initViews(rootView);
this.titleTextView = rootView.findViewById(R.id.titleTextView);
this.channelTextView = rootView.findViewById(R.id.channelTextView);
this.volumeRelativeLayout = rootView.findViewById(R.id.volumeRelativeLayout);
this.volumeProgressBar = rootView.findViewById(R.id.volumeProgressBar);
this.volumeImageView = rootView.findViewById(R.id.volumeImageView);
this.brightnessRelativeLayout = rootView.findViewById(R.id.brightnessRelativeLayout);
this.brightnessProgressBar = rootView.findViewById(R.id.brightnessProgressBar);
this.brightnessImageView = rootView.findViewById(R.id.brightnessImageView);
this.resizingIndicator = rootView.findViewById(R.id.resizing_indicator);
this.queueButton = rootView.findViewById(R.id.queueButton);
this.repeatButton = rootView.findViewById(R.id.repeatButton);
this.shuffleButton = rootView.findViewById(R.id.shuffleButton);
this.playWithKodi = rootView.findViewById(R.id.playWithKodi);
this.openInBrowser = rootView.findViewById(R.id.openInBrowser);
this.fullscreenButton = rootView.findViewById(R.id.fullScreenButton);
this.screenRotationButton = rootView.findViewById(R.id.screenRotationButton);
this.playerCloseButton = rootView.findViewById(R.id.playerCloseButton);
public void initViews(View view) {
super.initViews(view);
this.titleTextView = view.findViewById(R.id.titleTextView);
this.channelTextView = view.findViewById(R.id.channelTextView);
this.volumeRelativeLayout = view.findViewById(R.id.volumeRelativeLayout);
this.volumeProgressBar = view.findViewById(R.id.volumeProgressBar);
this.volumeImageView = view.findViewById(R.id.volumeImageView);
this.brightnessRelativeLayout = view.findViewById(R.id.brightnessRelativeLayout);
this.brightnessProgressBar = view.findViewById(R.id.brightnessProgressBar);
this.brightnessImageView = view.findViewById(R.id.brightnessImageView);
this.resizingIndicator = view.findViewById(R.id.resizing_indicator);
this.queueButton = view.findViewById(R.id.queueButton);
this.repeatButton = view.findViewById(R.id.repeatButton);
this.shuffleButton = view.findViewById(R.id.shuffleButton);
this.playWithKodi = view.findViewById(R.id.playWithKodi);
this.openInBrowser = view.findViewById(R.id.openInBrowser);
this.fullscreenButton = view.findViewById(R.id.fullScreenButton);
this.screenRotationButton = view.findViewById(R.id.screenRotationButton);
this.playerCloseButton = view.findViewById(R.id.playerCloseButton);
this.muteButton = view.findViewById(R.id.switchMute);
this.playPauseButton = rootView.findViewById(R.id.playPauseButton);
this.playPreviousButton = rootView.findViewById(R.id.playPreviousButton);
this.playNextButton = rootView.findViewById(R.id.playNextButton);
this.playPauseButton = view.findViewById(R.id.playPauseButton);
this.playPreviousButton = view.findViewById(R.id.playPreviousButton);
this.playNextButton = view.findViewById(R.id.playNextButton);
this.moreOptionsButton = rootView.findViewById(R.id.moreOptionsButton);
this.primaryControls = rootView.findViewById(R.id.primaryControls);
this.secondaryControls = rootView.findViewById(R.id.secondaryControls);
this.shareButton = rootView.findViewById(R.id.share);
this.moreOptionsButton = view.findViewById(R.id.moreOptionsButton);
this.primaryControls = view.findViewById(R.id.primaryControls);
this.secondaryControls = view.findViewById(R.id.secondaryControls);
this.shareButton = view.findViewById(R.id.share);
this.queueLayout = rootView.findViewById(R.id.playQueuePanel);
this.itemsListCloseButton = rootView.findViewById(R.id.playQueueClose);
this.itemsList = rootView.findViewById(R.id.playQueue);
this.queueLayout = view.findViewById(R.id.playQueuePanel);
this.itemsListCloseButton = view.findViewById(R.id.playQueueClose);
this.itemsList = view.findViewById(R.id.playQueue);
closingOverlayView = rootView.findViewById(R.id.closingOverlay);
closingOverlayView = view.findViewById(R.id.closingOverlay);
titleTextView.setSelected(true);
channelTextView.setSelected(true);
@ -306,6 +312,7 @@ public class VideoPlayerImpl extends VideoPlayer
shareButton.setVisibility(View.GONE);
playWithKodi.setVisibility(View.GONE);
openInBrowser.setVisibility(View.GONE);
muteButton.setVisibility(View.GONE);
playerCloseButton.setVisibility(View.GONE);
getTopControlsRoot().bringToFront();
getTopControlsRoot().setClickable(false);
@ -324,9 +331,13 @@ public class VideoPlayerImpl extends VideoPlayer
moreOptionsButton.setImageDrawable(service.getResources().getDrawable(
R.drawable.ic_expand_more_white_24dp));
shareButton.setVisibility(View.VISIBLE);
playWithKodi.setVisibility(
defaultPreferences.getBoolean(service.getString(R.string.show_play_with_kodi_key), false) ? View.VISIBLE : View.GONE);
final boolean supportedByKore = playQueue != null
&& playQueue.getItem() != null
&& KoreUtil.isServiceSupportedByKore(playQueue.getItem().getServiceId());
final boolean kodiEnabled = defaultPreferences.getBoolean(service.getString(R.string.show_play_with_kodi_key), false);
playWithKodi.setVisibility(kodiEnabled && supportedByKore ? View.VISIBLE : View.GONE);
openInBrowser.setVisibility(View.VISIBLE);
muteButton.setVisibility(View.VISIBLE);
playerCloseButton.setVisibility(isFullscreen ? View.GONE : View.VISIBLE);
// Top controls have a large minHeight which is allows to drag the player down in fullscreen mode (just larger area
// to make easy to locate by finger)
@ -399,6 +410,7 @@ public class VideoPlayerImpl extends VideoPlayer
playWithKodi.setOnClickListener(this);
openInBrowser.setOnClickListener(this);
playerCloseButton.setOnClickListener(this);
muteButton.setOnClickListener(this);
settingsContentObserver = new ContentObserver(new Handler()) {
@Override
@ -410,6 +422,45 @@ public class VideoPlayerImpl extends VideoPlayer
getRootView().addOnLayoutChangeListener(this);
}
public boolean onKeyDown(final int keyCode) {
switch (keyCode) {
default:
break;
case KeyEvent.KEYCODE_BACK:
if (AndroidTvUtils.isTv(service) && isControlsVisible()) {
hideControls(0, 0);
hideSystemUIIfNeeded();
return true;
}
break;
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_LEFT:
case KeyEvent.KEYCODE_DPAD_DOWN:
case KeyEvent.KEYCODE_DPAD_RIGHT:
case KeyEvent.KEYCODE_DPAD_CENTER:
if (getRootView().hasFocus() && !getControlsRoot().hasFocus()) {
// do not interfere with focus in playlist etc.
return false;
}
if (getCurrentState() == BasePlayer.STATE_BLOCKED) {
return true;
}
if (!isControlsVisible()) {
playPauseButton.requestFocus();
showControlsThenHide();
showSystemUIPartially();
return true;
} else {
hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME);
}
break;
}
return false;
}
public AppCompatActivity getParentActivity() {
// ! instanceof ViewGroup means that view was added via windowManager for Popup
if (getRootView() == null || getRootView().getParent() == null || !(getRootView().getParent() instanceof ViewGroup))
@ -494,6 +545,13 @@ public class VideoPlayerImpl extends VideoPlayer
protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
super.onMetadataChanged(tag);
// show kodi button if it supports the current service and it is enabled in settings
final boolean showKodiButton =
KoreUtil.isServiceSupportedByKore(tag.getMetadata().getServiceId())
&& PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.show_play_with_kodi_key), false);
playWithKodi.setVisibility(showKodiButton ? View.VISIBLE : View.GONE);
titleTextView.setText(tag.getMetadata().getName());
channelTextView.setText(tag.getMetadata().getUploaderName());
@ -508,6 +566,13 @@ public class VideoPlayerImpl extends VideoPlayer
// Override it because we don't want playerImpl destroyed
}
@Override
public void onMuteUnmuteButtonClicked() {
super.onMuteUnmuteButtonClicked();
updatePlayback();
setMuteButton(muteButton, isMuted());
}
@Override
public void onUpdateProgress(final int currentProgress, final int duration, final int bufferPercent) {
super.onUpdateProgress(currentProgress, duration, bufferPercent);
@ -518,6 +583,10 @@ public class VideoPlayerImpl extends VideoPlayer
|| getCurrentState() == BasePlayer.STATE_PAUSED || getPlayQueue() == null)
return;
if (timesNotificationUpdated > NOTIFICATION_UPDATES_BEFORE_RESET) {
service.resetNotification();
}
if (service.getBigNotRemoteView() != null) {
if (cachedDuration != duration) {
cachedDuration = duration;
@ -560,9 +629,10 @@ public class VideoPlayerImpl extends VideoPlayer
}
@Override
protected void initPlayback(@NonNull PlayQueue queue, int repeatMode, float playbackSpeed,
float playbackPitch, boolean playbackSkipSilence, boolean playOnReady) {
super.initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, playOnReady);
protected void initPlayback(@NonNull final PlayQueue queue, final int repeatMode, final float playbackSpeed,
final float playbackPitch, final boolean playbackSkipSilence,
final boolean playOnReady, final boolean isMuted) {
super.initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, playOnReady, isMuted);
updateQueue();
}
@ -587,7 +657,9 @@ public class VideoPlayerImpl extends VideoPlayer
this.getPlaybackPitch(),
this.getPlaybackSkipSilence(),
null,
true
true,
!isPlaying(),
isMuted()
);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Constants.KEY_SERVICE_ID, getCurrentMetadata().getMetadata().getServiceId());
@ -623,13 +695,10 @@ public class VideoPlayerImpl extends VideoPlayer
super.onClick(v);
if (v.getId() == playPauseButton.getId()) {
onPlayPause();
} else if (v.getId() == playPreviousButton.getId()) {
onPlayPrevious();
} else if (v.getId() == playNextButton.getId()) {
onPlayNext();
} else if (v.getId() == queueButton.getId()) {
onQueueClicked();
return;
@ -641,23 +710,19 @@ public class VideoPlayerImpl extends VideoPlayer
return;
} else if (v.getId() == moreOptionsButton.getId()) {
onMoreOptionsClicked();
} else if (v.getId() == shareButton.getId()) {
onShareClicked();
} else if (v.getId() == playWithKodi.getId()) {
onPlayWithKodiClicked();
} else if (v.getId() == openInBrowser.getId()) {
onOpenInBrowserClicked();
} else if (v.getId() == fullscreenButton.getId()) {
toggleFullscreen();
} else if (v.getId() == screenRotationButton.getId()) {
if (!isVerticalVideo) fragmentListener.onScreenRotationButtonClicked();
else toggleFullscreen();
} else if (v.getId() == muteButton.getId()) {
onMuteUnmuteButtonClicked();
} else if (v.getId() == playerCloseButton.getId()) {
service.sendBroadcast(new Intent(VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER));
}
@ -667,7 +732,7 @@ public class VideoPlayerImpl extends VideoPlayer
animateView(getControlsRoot(), true, DEFAULT_CONTROLS_DURATION, 0, () -> {
if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) {
if (v.getId() == playPauseButton.getId()) hideControls(0, 0);
else hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
else safeHideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
}
});
}
@ -734,10 +799,9 @@ public class VideoPlayerImpl extends VideoPlayer
private void onPlayWithKodiClicked() {
if (getCurrentMetadata() == null) return;
onPause();
try {
NavigationHelper.playWithKore(getParentActivity(), Uri.parse(
getCurrentMetadata().getMetadata().getUrl().replace("https", "http")));
NavigationHelper.playWithKore(getParentActivity(), Uri.parse(getVideoUrl()));
} catch (Exception e) {
if (DEBUG) Log.i(TAG, "Failed to start kore", e);
showInstallKoreDialog(getParentActivity());
@ -766,7 +830,7 @@ public class VideoPlayerImpl extends VideoPlayer
final boolean showButton = videoPlayerSelected() && (orientationLocked || isVerticalVideo || tabletInLandscape);
screenRotationButton.setVisibility(showButton ? View.VISIBLE : View.GONE);
screenRotationButton.setImageDrawable(service.getResources().getDrawable(
isFullscreen() ? R.drawable.ic_fullscreen_exit_white : R.drawable.ic_fullscreen_white));
isFullscreen() ? R.drawable.ic_fullscreen_exit_white_24dp : R.drawable.ic_fullscreen_white_24dp));
}
private void prepareOrientation() {
@ -795,7 +859,9 @@ public class VideoPlayerImpl extends VideoPlayer
@Override
public void onDismiss(PopupMenu menu) {
super.onDismiss(menu);
if (isPlaying()) hideControls(DEFAULT_CONTROLS_DURATION, 0);
if (isPlaying()) {
hideControls(DEFAULT_CONTROLS_DURATION, 0);
}
}
@Override
@ -894,12 +960,12 @@ public class VideoPlayerImpl extends VideoPlayer
@Override
public void onBlocked() {
super.onBlocked();
playPauseButton.setImageResource(R.drawable.ic_play_arrow_white);
playPauseButton.setImageResource(R.drawable.exo_controls_play);
animatePlayButtons(false, 100);
getRootView().setKeepScreenOn(false);
service.resetNotification();
service.updateNotification(R.drawable.ic_play_arrow_white);
service.updateNotification(R.drawable.exo_controls_play);
}
@Override
@ -908,24 +974,24 @@ public class VideoPlayerImpl extends VideoPlayer
getRootView().setKeepScreenOn(true);
service.resetNotification();
service.updateNotification(R.drawable.ic_play_arrow_white);
service.updateNotification(R.drawable.exo_controls_play);
}
@Override
public void onPlaying() {
super.onPlaying();
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> {
playPauseButton.setImageResource(R.drawable.ic_pause_white);
playPauseButton.setImageResource(R.drawable.exo_controls_pause);
animatePlayButtons(true, 200);
playPauseButton.requestFocus();
});
updateWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS);
checkLandscape();
getRootView().setKeepScreenOn(true);
service.getLockManager().acquireWifiAndCpu();
service.resetNotification();
service.updateNotification(R.drawable.ic_pause_white);
service.updateNotification(R.drawable.exo_controls_pause);
service.startForeground(NOTIFICATION_ID, service.getNotBuilder().build());
}
@ -934,22 +1000,21 @@ public class VideoPlayerImpl extends VideoPlayer
public void onPaused() {
super.onPaused();
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> {
playPauseButton.setImageResource(R.drawable.ic_play_arrow_white);
playPauseButton.setImageResource(R.drawable.exo_controls_play);
animatePlayButtons(true, 200);
playPauseButton.requestFocus();
});
updateWindowFlags(IDLE_WINDOW_FLAGS);
service.resetNotification();
service.updateNotification(R.drawable.ic_play_arrow_white);
service.updateNotification(R.drawable.exo_controls_play);
// Remove running notification when user don't want music (or video in popup) to be played in background
if (!minimizeOnPopupEnabled() && !backgroundPlaybackEnabled() && videoPlayerSelected())
service.stopForeground(true);
getRootView().setKeepScreenOn(false);
service.getLockManager().releaseWifiAndCpu();
}
@Override
@ -959,14 +1024,14 @@ public class VideoPlayerImpl extends VideoPlayer
getRootView().setKeepScreenOn(true);
service.resetNotification();
service.updateNotification(R.drawable.ic_play_arrow_white);
service.updateNotification(R.drawable.exo_controls_play);
}
@Override
public void onCompleted() {
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> {
playPauseButton.setImageResource(R.drawable.ic_replay_white);
playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp);
animatePlayButtons(true, DEFAULT_CONTROLS_DURATION);
});
getRootView().setKeepScreenOn(false);
@ -974,9 +1039,7 @@ public class VideoPlayerImpl extends VideoPlayer
updateWindowFlags(IDLE_WINDOW_FLAGS);
service.resetNotification();
service.updateNotification(R.drawable.ic_replay_white);
service.getLockManager().releaseWifiAndCpu();
service.updateNotification(R.drawable.ic_replay_white_24dp);
super.onCompleted();
}
@ -1065,6 +1128,16 @@ public class VideoPlayerImpl extends VideoPlayer
}
break;
case Intent.ACTION_CONFIGURATION_CHANGED:
assureCorrectAppLanguage(service);
if (DEBUG) {
Log.d(TAG, "onConfigurationChanged() called");
}
if (popupPlayerSelected()) {
updateScreenSize();
updatePopupSize(getPopupLayoutParams().width, -1);
checkPopupPositionBounds();
}
// The only situation I need to re-calculate elements sizes is when a user rotates a device from landscape to landscape
// because in that case the controls should be aligned to another side of a screen. The problem is when user leaves
// the app and returns back (while the app in landscape) Android reports via DisplayMetrics that orientation is
@ -1098,7 +1171,8 @@ public class VideoPlayerImpl extends VideoPlayer
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
public void onLoadingComplete(String imageUri, View view,
Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
// rebuild notification here since remote view does not release bitmaps,
// causing memory leaks
@ -1167,13 +1241,16 @@ public class VideoPlayerImpl extends VideoPlayer
}
private int distanceFromCloseButton(final MotionEvent popupMotionEvent) {
final int closeOverlayButtonX = closeOverlayButton.getLeft() + closeOverlayButton.getWidth() / 2;
final int closeOverlayButtonY = closeOverlayButton.getTop() + closeOverlayButton.getHeight() / 2;
final int closeOverlayButtonX = closeOverlayButton.getLeft()
+ closeOverlayButton.getWidth() / 2;
final int closeOverlayButtonY = closeOverlayButton.getTop()
+ closeOverlayButton.getHeight() / 2;
final float fingerX = popupLayoutParams.x + popupMotionEvent.getX();
final float fingerY = popupLayoutParams.y + popupMotionEvent.getY();
return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2) + Math.pow(closeOverlayButtonY - fingerY, 2));
return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2)
+ Math.pow(closeOverlayButtonY - fingerY, 2));
}
private float getClosingRadius() {
@ -1221,6 +1298,13 @@ public class VideoPlayerImpl extends VideoPlayer
);
}
@Override
public void safeHideControls(long duration, long delay) {
if (getControlsRoot().isInTouchMode()) {
hideControls(duration, delay);
}
}
private void showOrHideButtons() {
if (playQueue == null)
return;
@ -1301,6 +1385,11 @@ public class VideoPlayerImpl extends VideoPlayer
return statusBarHeight;
}
protected void setMuteButton(final ImageButton muteButton, final boolean isMuted) {
muteButton.setImageDrawable(AppCompatResources.getDrawable(service, isMuted
? R.drawable.ic_volume_off_white_24dp : R.drawable.ic_volume_up_white_24dp));
}
/**
* @return true if main player is attached to activity and activity inside multiWindow mode
*/
@ -1483,6 +1572,7 @@ public class VideoPlayerImpl extends VideoPlayer
/**
* @see #checkPopupPositionBounds(float, float)
* @return if the popup was out of bounds and have been moved back to it
*/
@SuppressWarnings("UnusedReturnValue")
public boolean checkPopupPositionBounds() {
@ -1490,18 +1580,22 @@ public class VideoPlayerImpl extends VideoPlayer
}
/**
* Check if {@link #popupLayoutParams}' position is within a arbitrary boundary that goes from (0,0) to (boundaryWidth,
* boundaryHeight).
* Check if {@link #popupLayoutParams}' position is within a arbitrary boundary
* that goes from (0, 0) to (boundaryWidth, boundaryHeight).
* <p>
* If it's out of these boundaries, {@link #popupLayoutParams}' position is changed and {@code true} is returned
* to represent this change.
* If it's out of these boundaries, {@link #popupLayoutParams}' position is changed
* and {@code true} is returned to represent this change.
* </p>
*
* @param boundaryWidth width of the boundary
* @param boundaryHeight height of the boundary
* @return if the popup was out of bounds and have been moved back to it
*/
public boolean checkPopupPositionBounds(final float boundaryWidth, final float boundaryHeight) {
if (DEBUG) {
Log.d(TAG, "checkPopupPositionBounds() called with: boundaryWidth = ["
+ boundaryWidth + "], boundaryHeight = [" + boundaryHeight + "]");
Log.d(TAG, "checkPopupPositionBounds() called with: "
+ "boundaryWidth = [" + boundaryWidth + "], "
+ "boundaryHeight = [" + boundaryHeight + "]");
}
if (popupLayoutParams.x < 0) {
@ -1524,15 +1618,19 @@ public class VideoPlayerImpl extends VideoPlayer
}
public void savePositionAndSize() {
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(service);
final SharedPreferences sharedPreferences = PreferenceManager
.getDefaultSharedPreferences(service);
sharedPreferences.edit().putInt(POPUP_SAVED_X, popupLayoutParams.x).apply();
sharedPreferences.edit().putInt(POPUP_SAVED_Y, popupLayoutParams.y).apply();
sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, popupLayoutParams.width).apply();
}
private float getMinimumVideoHeight(final float width) {
//if (DEBUG) Log.d(TAG, "getMinimumVideoHeight() called with: width = [" + width + "], returned: " + height);
return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have
final float height = width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have
/*if (DEBUG) {
Log.d(TAG, "getMinimumVideoHeight() called with: width = [" + width + "], returned: " + height);
}*/
return height;
}
public void updateScreenSize() {
@ -1554,24 +1652,25 @@ public class VideoPlayerImpl extends VideoPlayer
maximumHeight = screenHeight;
}
public void updatePopupSize(int width, int height) {
public void updatePopupSize(final int width, final int height) {
if (DEBUG) Log.d(TAG, "updatePopupSize() called with: width = [" + width + "], height = [" + height + "]");
if (popupLayoutParams == null || windowManager == null || getParentActivity() != null || getRootView().getParent() == null)
return;
width = (int) (width > maximumWidth ? maximumWidth : width < minimumWidth ? minimumWidth : width);
final int actualWidth = (int) (width > maximumWidth ? maximumWidth : width < minimumWidth ? minimumWidth : width);
final int actualHeight;
if (height == -1) actualHeight = (int) getMinimumVideoHeight(width);
else actualHeight = (int) (height > maximumHeight ? maximumHeight : height < minimumHeight ? minimumHeight : height);
if (height == -1) height = (int) getMinimumVideoHeight(width);
else height = (int) (height > maximumHeight ? maximumHeight : height < minimumHeight ? minimumHeight : height);
popupLayoutParams.width = width;
popupLayoutParams.height = height;
popupWidth = width;
popupHeight = height;
popupLayoutParams.width = actualWidth;
popupLayoutParams.height = actualHeight;
popupWidth = actualWidth;
popupHeight = actualHeight;
getSurfaceView().setHeights((int) popupHeight, (int) popupHeight);
if (DEBUG) Log.d(TAG, "updatePopupSize() updated values: width = [" + width + "], height = [" + height + "]");
if (DEBUG) Log.d(TAG, "updatePopupSize() updated values:" +
" width = [" + actualWidth + "], height = [" + actualHeight + "]");
windowManager.updateViewLayout(getRootView(), popupLayoutParams);
}
@ -1613,7 +1712,8 @@ public class VideoPlayerImpl extends VideoPlayer
}
private void animateOverlayAndFinishService() {
final int targetTranslationY = (int) (closeOverlayButton.getRootView().getHeight() - closeOverlayButton.getY());
final int targetTranslationY = (int) (closeOverlayButton.getRootView().getHeight()
- closeOverlayButton.getY());
closeOverlayButton.animate().setListener(null).cancel();
closeOverlayButton.animate()
@ -1622,12 +1722,12 @@ public class VideoPlayerImpl extends VideoPlayer
.setDuration(400)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationCancel(Animator animation) {
public void onAnimationCancel(final Animator animation) {
end();
}
@Override
public void onAnimationEnd(Animator animation) {
public void onAnimationEnd(final Animator animation) {
end();
}
@ -1704,7 +1804,8 @@ public class VideoPlayerImpl extends VideoPlayer
}
}
private void updateProgress(final int currentProgress, final int duration, final int bufferPercent) {
private void updateProgress(final int currentProgress, final int duration,
final int bufferPercent) {
if (fragmentListener != null) {
fragmentListener.onProgressUpdate(currentProgress, duration, bufferPercent);
}

View file

@ -0,0 +1,5 @@
package org.schabi.newpipe.player.event;
public interface OnKeyDownListener {
boolean onKeyDown(final int keyCode);
}

View file

@ -8,7 +8,8 @@ import org.schabi.newpipe.player.playqueue.PlayQueue;
public interface PlayerEventListener {
void onQueueUpdate(PlayQueue queue);
void onPlaybackUpdate(int state, int repeatMode, boolean shuffled, PlaybackParameters parameters);
void onPlaybackUpdate(int state, int repeatMode, boolean shuffled,
PlaybackParameters parameters);
void onProgressUpdate(int currentProgress, int duration, int bufferPercent);
void onMetadataUpdate(StreamInfo info, PlayQueue queue);
void onServiceStopped();

View file

@ -1,6 +1,7 @@
package org.schabi.newpipe.player.event;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.*;
import androidx.appcompat.content.res.AppCompatResources;
@ -36,6 +37,13 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen
private final int maxVolume;
private static final int MOVEMENT_THRESHOLD = 40;
// [popup] initial coordinates and distance between fingers
private double initPointerDistance = -1;
private float initFirstPointerX = -1;
private float initFirstPointerY = -1;
private float initSecPointerX = -1;
private float initSecPointerY = -1;
public PlayerGestureListener(final VideoPlayerImpl playerImpl, final MainPlayer service) {
this.playerImpl = playerImpl;
@ -147,11 +155,17 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen
final float distanceX, final float distanceY) {
if (!isVolumeGestureEnabled && !isBrightnessGestureEnabled) return false;
//noinspection PointlessBooleanExpression
if (DEBUG && false) Log.d(TAG, "MainVideoPlayer.onScroll = " +
final boolean isTouchingStatusBar = initialEvent.getY() < getStatusBarHeight(service);
final boolean isTouchingNavigationBar = initialEvent.getY()
> playerImpl.getRootView().getHeight() - getNavigationBarHeight(service);
if (isTouchingStatusBar || isTouchingNavigationBar) {
return false;
}
/*if (DEBUG && false) Log.d(TAG, "MainVideoPlayer.onScroll = " +
", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" +
", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" +
", distanceXy = [" + distanceX + ", " + distanceY + "]");
", distanceXy = [" + distanceX + ", " + distanceY + "]");*/
final boolean insideThreshold = Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD;
if (!isMovingInMain && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY))
@ -171,10 +185,10 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen
if (DEBUG) Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume);
playerImpl.getVolumeImageView().setImageDrawable(
AppCompatResources.getDrawable(service, currentProgressPercent <= 0 ? R.drawable.ic_volume_off_white_72dp
: currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_72dp
: currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_72dp
: R.drawable.ic_volume_up_white_72dp)
AppCompatResources.getDrawable(service, currentProgressPercent <= 0 ? R.drawable.ic_volume_off_white_24dp
: currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_24dp
: currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_24dp
: R.drawable.ic_volume_up_white_24dp)
);
if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
@ -200,9 +214,9 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen
playerImpl.getBrightnessImageView().setImageDrawable(
AppCompatResources.getDrawable(service,
currentProgressPercent < 0.25 ? R.drawable.ic_brightness_low_white_72dp
: currentProgressPercent < 0.75 ? R.drawable.ic_brightness_medium_white_72dp
: R.drawable.ic_brightness_high_white_72dp)
currentProgressPercent < 0.25 ? R.drawable.ic_brightness_low_white_24dp
: currentProgressPercent < 0.75 ? R.drawable.ic_brightness_medium_white_24dp
: R.drawable.ic_brightness_high_white_24dp)
);
if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) {
@ -273,8 +287,8 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen
if (playerImpl.isControlsVisible()) {
playerImpl.hideControls(100, 100);
} else {
playerImpl.getPlayPauseButton().requestFocus();
playerImpl.showControlsThenHide();
}
return true;
}
@ -312,11 +326,17 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen
final float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY());
float posY = (int) (initialPopupY + diffY);
if (posX > (playerImpl.getScreenWidth() - playerImpl.getPopupWidth())) posX = (int) (playerImpl.getScreenWidth() - playerImpl.getPopupWidth());
else if (posX < 0) posX = 0;
if (posX > (playerImpl.getScreenWidth() - playerImpl.getPopupWidth())) {
posX = (int) (playerImpl.getScreenWidth() - playerImpl.getPopupWidth());
} else if (posX < 0) {
posX = 0;
}
if (posY > (playerImpl.getScreenHeight() - playerImpl.getPopupHeight())) posY = (int) (playerImpl.getScreenHeight() - playerImpl.getPopupHeight());
else if (posY < 0) posY = 0;
if (posY > (playerImpl.getScreenHeight() - playerImpl.getPopupHeight())) {
posY = (int) (playerImpl.getScreenHeight() - playerImpl.getPopupHeight());
} else if (posY < 0) {
posY = 0;
}
playerImpl.getPopupLayoutParams().x = (int) posX;
playerImpl.getPopupLayoutParams().y = (int) posY;
@ -332,15 +352,19 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen
}
}
//noinspection PointlessBooleanExpression
if (DEBUG && false) {
Log.d(TAG, "PopupVideoPlayer.onScroll = " +
", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" + ", e1.getX,Y = [" + initialEvent.getX() + ", " + initialEvent.getY() + "]" +
", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" + ", e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "]" +
", distanceX,Y = [" + distanceX + ", " + distanceY + "]" +
", posX,Y = [" + posX + ", " + posY + "]" +
", popupW,H = [" + playerImpl.getPopupWidth() + " x " + playerImpl.getPopupHeight() + "]");
}
// if (DEBUG) {
// Log.d(TAG, "PopupVideoPlayer.onScroll = "
// + "e1.getRaw = [" + initialEvent.getRawX() + ", "
// + initialEvent.getRawY() + "], "
// + "e1.getX,Y = [" + initialEvent.getX() + ", "
// + initialEvent.getY() + "], "
// + "e2.getRaw = [" + movingEvent.getRawX() + ", "
// + movingEvent.getRawY() + "], "
// + "e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "], "
// + "distanceX,Y = [" + distanceX + ", " + distanceY + "], "
// + "posX,Y = [" + posX + ", " + posY + "], "
// + "popupW,H = [" + popupWidth + " x " + popupHeight + "]");
// }
playerImpl.windowManager.updateViewLayout(playerImpl.getRootView(), playerImpl.getPopupLayoutParams());
return true;
}
@ -378,8 +402,11 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen
}
private boolean onTouchInPopup(View v, MotionEvent event) {
if (playerImpl == null) {
return false;
}
playerImpl.getGestureDetector().onTouchEvent(event);
if (playerImpl == null) return false;
if (event.getPointerCount() == 2 && !isMovingInPopup && !isResizing) {
if (DEBUG) Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.");
playerImpl.showAndAnimateControl(-1, true);
@ -388,6 +415,15 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen
playerImpl.hideControls(0, 0);
animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0);
animateView(playerImpl.getResizingIndicator(), true, 200, 0);
//record coordinates of fingers
initFirstPointerX = event.getX(0);
initFirstPointerY = event.getY(0);
initSecPointerX = event.getX(1);
initSecPointerY = event.getY(1);
//record distance between fingers
initPointerDistance = Math.hypot(initFirstPointerX - initSecPointerX,
initFirstPointerY - initSecPointerY);
isResizing = true;
}
@ -406,6 +442,13 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen
if (isResizing) {
isResizing = false;
initPointerDistance = -1;
initFirstPointerX = -1;
initFirstPointerY = -1;
initSecPointerX = -1;
initSecPointerY = -1;
animateView(playerImpl.getResizingIndicator(), false, 100, 0);
playerImpl.changeState(playerImpl.getCurrentState());
}
@ -420,29 +463,58 @@ public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListen
}
private boolean handleMultiDrag(final MotionEvent event) {
if (event.getPointerCount() != 2) return false;
if (initPointerDistance != -1 && event.getPointerCount() == 2) {
// get the movements of the fingers
double firstPointerMove = Math.hypot(event.getX(0) - initFirstPointerX,
event.getY(0) - initFirstPointerY);
double secPointerMove = Math.hypot(event.getX(1) - initSecPointerX,
event.getY(1) - initSecPointerY);
final float firstPointerX = event.getX(0);
final float secondPointerX = event.getX(1);
// minimum threshold beyond which pinch gesture will work
int minimumMove = ViewConfiguration.get(service).getScaledTouchSlop();
final float diff = Math.abs(firstPointerX - secondPointerX);
if (firstPointerX > secondPointerX) {
// second pointer is the anchor (the leftmost pointer)
playerImpl.getPopupLayoutParams().x = (int) (event.getRawX() - diff);
} else {
// first pointer is the anchor
playerImpl.getPopupLayoutParams().x = (int) event.getRawX();
if (Math.max(firstPointerMove, secPointerMove) > minimumMove) {
// calculate current distance between the pointers
final double currentPointerDistance =
Math.hypot(event.getX(0) - event.getX(1),
event.getY(0) - event.getY(1));
double popupWidth = playerImpl.getPopupWidth();
// change co-ordinates of popup so the center stays at the same position
double newWidth = (popupWidth * currentPointerDistance / initPointerDistance);
initPointerDistance = currentPointerDistance;
playerImpl.getPopupLayoutParams().x += (popupWidth - newWidth) / 2;
playerImpl.checkPopupPositionBounds();
playerImpl.updateScreenSize();
playerImpl.updatePopupSize((int) Math.min(playerImpl.getScreenWidth(), newWidth), -1);
return true;
}
}
playerImpl.checkPopupPositionBounds();
playerImpl.updateScreenSize();
final int width = (int) Math.min(playerImpl.getScreenWidth(), diff);
playerImpl.updatePopupSize(width, -1);
return true;
return false;
}
/*
* Utils
* */
private int getNavigationBarHeight(Context context) {
int resId = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android");
if (resId > 0) {
return context.getResources().getDimensionPixelSize(resId);
}
return 0;
}
private int getStatusBarHeight(Context context) {
int resId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resId > 0) {
return context.getResources().getDimensionPixelSize(resId);
}
return 0;
}
}

View file

@ -9,14 +9,14 @@ import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.media.audiofx.AudioEffect;
import android.os.Build;
import androidx.annotation.NonNull;
import android.util.Log;
import androidx.annotation.NonNull;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.analytics.AnalyticsListener;
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
AnalyticsListener {
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener {
private static final String TAG = "AudioFocusReactor";
@ -82,20 +82,20 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
return audioManager.getStreamVolume(STREAM_TYPE);
}
public int getMaxVolume() {
return audioManager.getStreamMaxVolume(STREAM_TYPE);
}
public void setVolume(final int volume) {
audioManager.setStreamVolume(STREAM_TYPE, volume, 0);
}
public int getMaxVolume() {
return audioManager.getStreamMaxVolume(STREAM_TYPE);
}
/*//////////////////////////////////////////////////////////////////////////
// AudioFocus
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAudioFocusChange(int focusChange) {
public void onAudioFocusChange(final int focusChange) {
Log.d(TAG, "onAudioFocusChange() called with: focusChange = [" + focusChange + "]");
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
@ -138,17 +138,17 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
valueAnimator.setDuration(AudioReactor.DUCK_DURATION);
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
public void onAnimationStart(final Animator animation) {
player.setVolume(from);
}
@Override
public void onAnimationCancel(Animator animation) {
public void onAnimationCancel(final Animator animation) {
player.setVolume(to);
}
@Override
public void onAnimationEnd(Animator animation) {
public void onAnimationEnd(final Animator animation) {
player.setVolume(to);
}
});
@ -162,8 +162,10 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAudioSessionId(EventTime eventTime, int audioSessionId) {
if (!PlayerHelper.isUsingDSP(context)) return;
public void onAudioSessionId(final EventTime eventTime, final int audioSessionId) {
if (!PlayerHelper.isUsingDSP(context)) {
return;
}
final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId);

View file

@ -20,8 +20,10 @@ import java.io.File;
/* package-private */ class CacheFactory implements DataSource.Factory {
private static final String TAG = "CacheFactory";
private static final String CACHE_FOLDER_NAME = "exoplayer";
private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;
private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE
| CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;
private final DefaultDataSourceFactory dataSourceFactory;
private final File cacheDir;
@ -33,11 +35,11 @@ import java.io.File;
// todo: make this a singleton?
private static SimpleCache cache;
public CacheFactory(@NonNull final Context context,
@NonNull final String userAgent,
@NonNull final TransferListener transferListener) {
this(context, userAgent, transferListener, PlayerHelper.getPreferredCacheSize(context),
PlayerHelper.getPreferredFileSize(context));
CacheFactory(@NonNull final Context context,
@NonNull final String userAgent,
@NonNull final TransferListener transferListener) {
this(context, userAgent, transferListener, PlayerHelper.getPreferredCacheSize(),
PlayerHelper.getPreferredFileSize());
}
private CacheFactory(@NonNull final Context context,
@ -55,7 +57,8 @@ import java.io.File;
}
if (cache == null) {
final LeastRecentlyUsedCacheEvictor evictor = new LeastRecentlyUsedCacheEvictor(maxCacheSize);
final LeastRecentlyUsedCacheEvictor evictor
= new LeastRecentlyUsedCacheEvictor(maxCacheSize);
cache = new SimpleCache(cacheDir, evictor, new ExoDatabaseProvider(context));
}
}
@ -72,7 +75,9 @@ import java.io.File;
}
public void tryDeleteCacheFiles() {
if (!cacheDir.exists() || !cacheDir.isDirectory()) return;
if (!cacheDir.exists() || !cacheDir.isDirectory()) {
return;
}
try {
for (File file : cacheDir.listFiles()) {
@ -85,4 +90,4 @@ import java.io.File;
Log.e(TAG, "Failed to delete file.", ignored);
}
}
}
}

View file

@ -1,7 +1,5 @@
package org.schabi.newpipe.player.helper;
import android.content.Context;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.Renderer;
@ -20,10 +18,10 @@ public class LoadController implements LoadControl {
// Default Load Control
//////////////////////////////////////////////////////////////////////////*/
public LoadController(final Context context) {
this(PlayerHelper.getPlaybackStartBufferMs(context),
PlayerHelper.getPlaybackMinimumBufferMs(context),
PlayerHelper.getPlaybackOptimalBufferMs(context));
public LoadController() {
this(PlayerHelper.getPlaybackStartBufferMs(),
PlayerHelper.getPlaybackMinimumBufferMs(),
PlayerHelper.getPlaybackOptimalBufferMs());
}
private LoadController(final int initialPlaybackBufferMs,
@ -47,8 +45,8 @@ public class LoadController implements LoadControl {
}
@Override
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroupArray,
TrackSelectionArray trackSelectionArray) {
public void onTracksSelected(final Renderer[] renderers, final TrackGroupArray trackGroupArray,
final TrackSelectionArray trackSelectionArray) {
internalLoadControl.onTracksSelected(renderers, trackGroupArray, trackSelectionArray);
}
@ -78,17 +76,18 @@ public class LoadController implements LoadControl {
}
@Override
public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) {
public boolean shouldContinueLoading(final long bufferedDurationUs,
final float playbackSpeed) {
return internalLoadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed);
}
@Override
public boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed,
boolean rebuffering) {
final boolean isInitialPlaybackBufferFilled = bufferedDurationUs >=
this.initialPlaybackBufferUs * playbackSpeed;
final boolean isInternalStartingPlayback = internalLoadControl.shouldStartPlayback(
bufferedDurationUs, playbackSpeed, rebuffering);
public boolean shouldStartPlayback(final long bufferedDurationUs, final float playbackSpeed,
final boolean rebuffering) {
final boolean isInitialPlaybackBufferFilled
= bufferedDurationUs >= this.initialPlaybackBufferUs * playbackSpeed;
final boolean isInternalStartingPlayback = internalLoadControl
.shouldStartPlayback(bufferedDurationUs, playbackSpeed, rebuffering);
return isInitialPlaybackBufferFilled || isInternalStartingPlayback;
}
}

View file

@ -18,25 +18,37 @@ public class LockManager {
private WifiManager.WifiLock wifiLock;
public LockManager(final Context context) {
powerManager = ((PowerManager) context.getApplicationContext().getSystemService(POWER_SERVICE));
wifiManager = ((WifiManager) context.getApplicationContext().getSystemService(WIFI_SERVICE));
powerManager = ((PowerManager) context.getApplicationContext()
.getSystemService(POWER_SERVICE));
wifiManager = ((WifiManager) context.getApplicationContext()
.getSystemService(WIFI_SERVICE));
}
public void acquireWifiAndCpu() {
Log.d(TAG, "acquireWifiAndCpu() called");
if (wakeLock != null && wakeLock.isHeld() && wifiLock != null && wifiLock.isHeld()) return;
if (wakeLock != null && wakeLock.isHeld() && wifiLock != null && wifiLock.isHeld()) {
return;
}
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, TAG);
if (wakeLock != null) wakeLock.acquire();
if (wifiLock != null) wifiLock.acquire();
if (wakeLock != null) {
wakeLock.acquire();
}
if (wifiLock != null) {
wifiLock.acquire();
}
}
public void releaseWifiAndCpu() {
Log.d(TAG, "releaseWifiAndCpu() called");
if (wakeLock != null && wakeLock.isHeld()) wakeLock.release();
if (wifiLock != null && wifiLock.isHeld()) wifiLock.release();
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
if (wifiLock != null && wifiLock.isHeld()) {
wifiLock.release();
}
wakeLock = null;
wifiLock = null;

View file

@ -2,11 +2,18 @@ package org.schabi.newpipe.player.helper;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.media.MediaMetadata;
import android.os.Build;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.view.KeyEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import androidx.media.app.NotificationCompat.MediaStyle;
import androidx.media.session.MediaButtonReceiver;
import com.google.android.exoplayer2.Player;
@ -19,8 +26,10 @@ import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController;
public class MediaSessionManager {
private static final String TAG = "MediaSessionManager";
@NonNull private final MediaSessionCompat mediaSession;
@NonNull private final MediaSessionConnector sessionConnector;
@NonNull
private final MediaSessionCompat mediaSession;
@NonNull
private final MediaSessionConnector sessionConnector;
public MediaSessionManager(@NonNull final Context context,
@NonNull final Player player,
@ -40,13 +49,46 @@ public class MediaSessionManager {
return MediaButtonReceiver.handleIntent(mediaSession, intent);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void setLockScreenArt(final NotificationCompat.Builder builder,
@Nullable final Bitmap thumbnailBitmap) {
if (thumbnailBitmap == null || !mediaSession.isActive()) {
return;
}
mediaSession.setMetadata(
new MediaMetadataCompat.Builder()
.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, thumbnailBitmap)
.build()
);
MediaStyle mediaStyle = new MediaStyle()
.setMediaSession(mediaSession.getSessionToken());
builder.setStyle(mediaStyle);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void clearLockScreenArt(final NotificationCompat.Builder builder) {
mediaSession.setMetadata(
new MediaMetadataCompat.Builder()
.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, null)
.build()
);
MediaStyle mediaStyle = new MediaStyle()
.setMediaSession(mediaSession.getSessionToken());
builder.setStyle(mediaStyle);
}
/**
* Should be called on player destruction to prevent leakage.
* */
*/
public void dispose() {
this.sessionConnector.setPlayer(null);
this.sessionConnector.setQueueNavigator(null);
this.mediaSession.setActive(false);
this.mediaSession.release();
}
}
}

View file

@ -3,80 +3,92 @@ package org.schabi.newpipe.player.helper;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.appcompat.app.AlertDialog;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.View;
import android.widget.CheckBox;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.SliderStrategy;
import static org.schabi.newpipe.player.BasePlayer.DEBUG;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class PlaybackParameterDialog extends DialogFragment {
@NonNull private static final String TAG = "PlaybackParameterDialog";
// Minimum allowable range in ExoPlayer
public static final double MINIMUM_PLAYBACK_VALUE = 0.10f;
public static final double MAXIMUM_PLAYBACK_VALUE = 3.00f;
private static final double MINIMUM_PLAYBACK_VALUE = 0.10f;
private static final double MAXIMUM_PLAYBACK_VALUE = 3.00f;
public static final char STEP_UP_SIGN = '+';
public static final char STEP_DOWN_SIGN = '-';
private static final char STEP_UP_SIGN = '+';
private static final char STEP_DOWN_SIGN = '-';
public static final double STEP_ONE_PERCENT_VALUE = 0.01f;
public static final double STEP_FIVE_PERCENT_VALUE = 0.05f;
public static final double STEP_TEN_PERCENT_VALUE = 0.10f;
public static final double STEP_TWENTY_FIVE_PERCENT_VALUE = 0.25f;
public static final double STEP_ONE_HUNDRED_PERCENT_VALUE = 1.00f;
private static final double STEP_ONE_PERCENT_VALUE = 0.01f;
private static final double STEP_FIVE_PERCENT_VALUE = 0.05f;
private static final double STEP_TEN_PERCENT_VALUE = 0.10f;
private static final double STEP_TWENTY_FIVE_PERCENT_VALUE = 0.25f;
private static final double STEP_ONE_HUNDRED_PERCENT_VALUE = 1.00f;
public static final double DEFAULT_TEMPO = 1.00f;
public static final double DEFAULT_PITCH = 1.00f;
public static final double DEFAULT_STEP = STEP_TWENTY_FIVE_PERCENT_VALUE;
public static final boolean DEFAULT_SKIP_SILENCE = false;
private static final double DEFAULT_TEMPO = 1.00f;
private static final double DEFAULT_PITCH = 1.00f;
private static final double DEFAULT_STEP = STEP_TWENTY_FIVE_PERCENT_VALUE;
private static final boolean DEFAULT_SKIP_SILENCE = false;
@NonNull private static final String INITIAL_TEMPO_KEY = "initial_tempo_key";
@NonNull private static final String INITIAL_PITCH_KEY = "initial_pitch_key";
@NonNull
private static final String TAG = "PlaybackParameterDialog";
@NonNull
private static final String INITIAL_TEMPO_KEY = "initial_tempo_key";
@NonNull
private static final String INITIAL_PITCH_KEY = "initial_pitch_key";
@NonNull private static final String TEMPO_KEY = "tempo_key";
@NonNull private static final String PITCH_KEY = "pitch_key";
@NonNull private static final String STEP_SIZE_KEY = "step_size_key";
@NonNull
private static final String TEMPO_KEY = "tempo_key";
@NonNull
private static final String PITCH_KEY = "pitch_key";
@NonNull
private static final String STEP_SIZE_KEY = "step_size_key";
public interface Callback {
void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch,
final boolean playbackSkipSilence);
}
@Nullable private Callback callback;
@NonNull private final SliderStrategy strategy = new SliderStrategy.Quadratic(
@NonNull
private final SliderStrategy strategy = new SliderStrategy.Quadratic(
MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE,
/*centerAt=*/1.00f, /*sliderGranularity=*/10000);
@Nullable
private Callback callback;
private double initialTempo = DEFAULT_TEMPO;
private double initialPitch = DEFAULT_PITCH;
private boolean initialSkipSilence = DEFAULT_SKIP_SILENCE;
private double tempo = DEFAULT_TEMPO;
private double pitch = DEFAULT_PITCH;
private double stepSize = DEFAULT_STEP;
@Nullable private SeekBar tempoSlider;
@Nullable private TextView tempoCurrentText;
@Nullable private TextView tempoStepDownText;
@Nullable private TextView tempoStepUpText;
@Nullable private SeekBar pitchSlider;
@Nullable private TextView pitchCurrentText;
@Nullable private TextView pitchStepDownText;
@Nullable private TextView pitchStepUpText;
@Nullable private CheckBox unhookingCheckbox;
@Nullable private CheckBox skipSilenceCheckbox;
@Nullable
private SeekBar tempoSlider;
@Nullable
private TextView tempoCurrentText;
@Nullable
private TextView tempoStepDownText;
@Nullable
private TextView tempoStepUpText;
@Nullable
private SeekBar pitchSlider;
@Nullable
private TextView pitchCurrentText;
@Nullable
private TextView pitchStepDownText;
@Nullable
private TextView pitchStepUpText;
@Nullable
private CheckBox unhookingCheckbox;
@Nullable
private CheckBox skipSilenceCheckbox;
public static PlaybackParameterDialog newInstance(final double playbackTempo,
final double playbackPitch,
@ -99,7 +111,7 @@ public class PlaybackParameterDialog extends DialogFragment {
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(Context context) {
public void onAttach(final Context context) {
super.onAttach(context);
if (context instanceof Callback) {
callback = (Callback) context;
@ -109,7 +121,8 @@ public class PlaybackParameterDialog extends DialogFragment {
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
public void onCreate(@Nullable final Bundle savedInstanceState) {
assureCorrectAppLanguage(getContext());
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO);
@ -122,7 +135,7 @@ public class PlaybackParameterDialog extends DialogFragment {
}
@Override
public void onSaveInstanceState(Bundle outState) {
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putDouble(INITIAL_TEMPO_KEY, initialTempo);
outState.putDouble(INITIAL_PITCH_KEY, initialPitch);
@ -138,7 +151,8 @@ public class PlaybackParameterDialog extends DialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
assureCorrectAppLanguage(getContext());
final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null);
setupControlViews(view);
@ -160,18 +174,18 @@ public class PlaybackParameterDialog extends DialogFragment {
// Control Views
//////////////////////////////////////////////////////////////////////////*/
private void setupControlViews(@NonNull View rootView) {
private void setupControlViews(@NonNull final View rootView) {
setupHookingControl(rootView);
setupSkipSilenceControl(rootView);
setupTempoControl(rootView);
setupPitchControl(rootView);
changeStepSize(stepSize);
setStepSize(stepSize);
setupStepSizeSelector(rootView);
}
private void setupTempoControl(@NonNull View rootView) {
private void setupTempoControl(@NonNull final View rootView) {
tempoSlider = rootView.findViewById(R.id.tempoSeekbar);
TextView tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText);
TextView tempoMaximumText = rootView.findViewById(R.id.tempoMaximumText);
@ -179,12 +193,15 @@ public class PlaybackParameterDialog extends DialogFragment {
tempoStepUpText = rootView.findViewById(R.id.tempoStepUp);
tempoStepDownText = rootView.findViewById(R.id.tempoStepDown);
if (tempoCurrentText != null)
if (tempoCurrentText != null) {
tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo));
if (tempoMaximumText != null)
}
if (tempoMaximumText != null) {
tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE));
if (tempoMinimumText != null)
}
if (tempoMinimumText != null) {
tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE));
}
if (tempoSlider != null) {
tempoSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE));
@ -193,7 +210,7 @@ public class PlaybackParameterDialog extends DialogFragment {
}
}
private void setupPitchControl(@NonNull View rootView) {
private void setupPitchControl(@NonNull final View rootView) {
pitchSlider = rootView.findViewById(R.id.pitchSeekbar);
TextView pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText);
TextView pitchMaximumText = rootView.findViewById(R.id.pitchMaximumText);
@ -201,12 +218,15 @@ public class PlaybackParameterDialog extends DialogFragment {
pitchStepDownText = rootView.findViewById(R.id.pitchStepDown);
pitchStepUpText = rootView.findViewById(R.id.pitchStepUp);
if (pitchCurrentText != null)
if (pitchCurrentText != null) {
pitchCurrentText.setText(PlayerHelper.formatPitch(pitch));
if (pitchMaximumText != null)
}
if (pitchMaximumText != null) {
pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE));
if (pitchMinimumText != null)
}
if (pitchMinimumText != null) {
pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE));
}
if (pitchSlider != null) {
pitchSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE));
@ -215,21 +235,31 @@ public class PlaybackParameterDialog extends DialogFragment {
}
}
private void setupHookingControl(@NonNull View rootView) {
private void setupHookingControl(@NonNull final View rootView) {
unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox);
if (unhookingCheckbox != null) {
unhookingCheckbox.setChecked(pitch != tempo);
// restore whether pitch and tempo are unhooked or not
unhookingCheckbox.setChecked(PreferenceManager.getDefaultSharedPreferences(getContext())
.getBoolean(getString(R.string.playback_unhook_key), true));
unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
if (isChecked) return;
// When unchecked, slide back to the minimum of current tempo or pitch
final double minimum = Math.min(getCurrentPitch(), getCurrentTempo());
setSliders(minimum);
setCurrentPlaybackParameters();
// save whether pitch and tempo are unhooked or not
PreferenceManager.getDefaultSharedPreferences(getContext())
.edit()
.putBoolean(getString(R.string.playback_unhook_key), isChecked)
.apply();
if (!isChecked) {
// when unchecked, slide back to the minimum of current tempo or pitch
final double minimum = Math.min(getCurrentPitch(), getCurrentTempo());
setSliders(minimum);
setCurrentPlaybackParameters();
}
});
}
}
private void setupSkipSilenceControl(@NonNull View rootView) {
private void setupSkipSilenceControl(@NonNull final View rootView) {
skipSilenceCheckbox = rootView.findViewById(R.id.skipSilenceCheckbox);
if (skipSilenceCheckbox != null) {
skipSilenceCheckbox.setChecked(initialSkipSilence);
@ -242,41 +272,45 @@ public class PlaybackParameterDialog extends DialogFragment {
TextView stepSizeOnePercentText = rootView.findViewById(R.id.stepSizeOnePercent);
TextView stepSizeFivePercentText = rootView.findViewById(R.id.stepSizeFivePercent);
TextView stepSizeTenPercentText = rootView.findViewById(R.id.stepSizeTenPercent);
TextView stepSizeTwentyFivePercentText = rootView.findViewById(R.id.stepSizeTwentyFivePercent);
TextView stepSizeOneHundredPercentText = rootView.findViewById(R.id.stepSizeOneHundredPercent);
TextView stepSizeTwentyFivePercentText = rootView
.findViewById(R.id.stepSizeTwentyFivePercent);
TextView stepSizeOneHundredPercentText = rootView
.findViewById(R.id.stepSizeOneHundredPercent);
if (stepSizeOnePercentText != null) {
stepSizeOnePercentText.setText(getPercentString(STEP_ONE_PERCENT_VALUE));
stepSizeOnePercentText.setOnClickListener(view ->
changeStepSize(STEP_ONE_PERCENT_VALUE));
stepSizeOnePercentText
.setOnClickListener(view -> setStepSize(STEP_ONE_PERCENT_VALUE));
}
if (stepSizeFivePercentText != null) {
stepSizeFivePercentText.setText(getPercentString(STEP_FIVE_PERCENT_VALUE));
stepSizeFivePercentText.setOnClickListener(view ->
changeStepSize(STEP_FIVE_PERCENT_VALUE));
stepSizeFivePercentText
.setOnClickListener(view -> setStepSize(STEP_FIVE_PERCENT_VALUE));
}
if (stepSizeTenPercentText != null) {
stepSizeTenPercentText.setText(getPercentString(STEP_TEN_PERCENT_VALUE));
stepSizeTenPercentText.setOnClickListener(view ->
changeStepSize(STEP_TEN_PERCENT_VALUE));
stepSizeTenPercentText
.setOnClickListener(view -> setStepSize(STEP_TEN_PERCENT_VALUE));
}
if (stepSizeTwentyFivePercentText != null) {
stepSizeTwentyFivePercentText.setText(getPercentString(STEP_TWENTY_FIVE_PERCENT_VALUE));
stepSizeTwentyFivePercentText.setOnClickListener(view ->
changeStepSize(STEP_TWENTY_FIVE_PERCENT_VALUE));
stepSizeTwentyFivePercentText
.setText(getPercentString(STEP_TWENTY_FIVE_PERCENT_VALUE));
stepSizeTwentyFivePercentText
.setOnClickListener(view -> setStepSize(STEP_TWENTY_FIVE_PERCENT_VALUE));
}
if (stepSizeOneHundredPercentText != null) {
stepSizeOneHundredPercentText.setText(getPercentString(STEP_ONE_HUNDRED_PERCENT_VALUE));
stepSizeOneHundredPercentText.setOnClickListener(view ->
changeStepSize(STEP_ONE_HUNDRED_PERCENT_VALUE));
stepSizeOneHundredPercentText
.setText(getPercentString(STEP_ONE_HUNDRED_PERCENT_VALUE));
stepSizeOneHundredPercentText
.setOnClickListener(view -> setStepSize(STEP_ONE_HUNDRED_PERCENT_VALUE));
}
}
private void changeStepSize(final double stepSize) {
private void setStepSize(final double stepSize) {
this.stepSize = stepSize;
if (tempoStepUpText != null) {
@ -319,7 +353,8 @@ public class PlaybackParameterDialog extends DialogFragment {
private SeekBar.OnSeekBarChangeListener getOnTempoChangedListener() {
return new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
public void onProgressChanged(final SeekBar seekBar, final int progress,
final boolean fromUser) {
final double currentTempo = strategy.valueOf(progress);
if (fromUser) {
onTempoSliderUpdated(currentTempo);
@ -328,12 +363,12 @@ public class PlaybackParameterDialog extends DialogFragment {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
public void onStartTrackingTouch(final SeekBar seekBar) {
// Do Nothing.
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
public void onStopTrackingTouch(final SeekBar seekBar) {
// Do Nothing.
}
};
@ -342,7 +377,8 @@ public class PlaybackParameterDialog extends DialogFragment {
private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() {
return new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
public void onProgressChanged(final SeekBar seekBar, final int progress,
final boolean fromUser) {
final double currentPitch = strategy.valueOf(progress);
if (fromUser) { // this change is first in chain
onPitchSliderUpdated(currentPitch);
@ -351,19 +387,21 @@ public class PlaybackParameterDialog extends DialogFragment {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
public void onStartTrackingTouch(final SeekBar seekBar) {
// Do Nothing.
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
public void onStopTrackingTouch(final SeekBar seekBar) {
// Do Nothing.
}
};
}
private void onTempoSliderUpdated(final double newTempo) {
if (unhookingCheckbox == null) return;
if (unhookingCheckbox == null) {
return;
}
if (!unhookingCheckbox.isChecked()) {
setSliders(newTempo);
} else {
@ -372,7 +410,9 @@ public class PlaybackParameterDialog extends DialogFragment {
}
private void onPitchSliderUpdated(final double newPitch) {
if (unhookingCheckbox == null) return;
if (unhookingCheckbox == null) {
return;
}
if (!unhookingCheckbox.isChecked()) {
setSliders(newPitch);
} else {
@ -386,12 +426,16 @@ public class PlaybackParameterDialog extends DialogFragment {
}
private void setTempoSlider(final double newTempo) {
if (tempoSlider == null) return;
if (tempoSlider == null) {
return;
}
tempoSlider.setProgress(strategy.progressOf(newTempo));
}
private void setPitchSlider(final double newPitch) {
if (pitchSlider == null) return;
if (pitchSlider == null) {
return;
}
pitchSlider.setProgress(strategy.progressOf(newPitch));
}
@ -403,27 +447,27 @@ public class PlaybackParameterDialog extends DialogFragment {
setPlaybackParameters(getCurrentTempo(), getCurrentPitch(), getCurrentSkipSilence());
}
private void setPlaybackParameters(final double tempo, final double pitch,
private void setPlaybackParameters(final double newTempo, final double newPitch,
final boolean skipSilence) {
if (callback != null && tempoCurrentText != null && pitchCurrentText != null) {
if (DEBUG) Log.d(TAG, "Setting playback parameters to " +
"tempo=[" + tempo + "], " +
"pitch=[" + pitch + "]");
if (DEBUG) {
Log.d(TAG, "Setting playback parameters to "
+ "tempo=[" + newTempo + "], "
+ "pitch=[" + newPitch + "]");
}
tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo));
pitchCurrentText.setText(PlayerHelper.formatPitch(pitch));
callback.onPlaybackParameterChanged((float) tempo, (float) pitch, skipSilence);
tempoCurrentText.setText(PlayerHelper.formatSpeed(newTempo));
pitchCurrentText.setText(PlayerHelper.formatPitch(newPitch));
callback.onPlaybackParameterChanged((float) newTempo, (float) newPitch, skipSilence);
}
}
private double getCurrentTempo() {
return tempoSlider == null ? tempo : strategy.valueOf(
tempoSlider.getProgress());
return tempoSlider == null ? tempo : strategy.valueOf(tempoSlider.getProgress());
}
private double getCurrentPitch() {
return pitchSlider == null ? pitch : strategy.valueOf(
pitchSlider.getProgress());
return pitchSlider == null ? pitch : strategy.valueOf(pitchSlider.getProgress());
}
private double getCurrentStepSize() {
@ -448,4 +492,9 @@ public class PlaybackParameterDialog extends DialogFragment {
private static String getPercentString(final double percent) {
return PlayerHelper.formatPitch(percent);
}
public interface Callback {
void onPlaybackParameterChanged(float playbackTempo, float playbackPitch,
boolean playbackSkipSilence);
}
}

View file

@ -24,30 +24,33 @@ public class PlayerDataSource {
private final DataSource.Factory cacheDataSourceFactory;
private final DataSource.Factory cachelessDataSourceFactory;
public PlayerDataSource(@NonNull final Context context,
@NonNull final String userAgent,
public PlayerDataSource(@NonNull final Context context, @NonNull final String userAgent,
@NonNull final TransferListener transferListener) {
cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener);
cachelessDataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener);
cachelessDataSourceFactory
= new DefaultDataSourceFactory(context, userAgent, transferListener);
}
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory(
cachelessDataSourceFactory), cachelessDataSourceFactory)
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
}
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
return new HlsMediaSource.Factory(cachelessDataSourceFactory)
.setAllowChunklessPreparation(true)
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
}
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(
cachelessDataSourceFactory), cachelessDataSourceFactory)
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS, true);
}
@ -67,10 +70,12 @@ public class PlayerDataSource {
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() {
return new ProgressiveMediaSource.Factory(cacheDataSourceFactory)
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
}
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory(@NonNull final String key) {
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory(
@NonNull final String key) {
return getExtractorMediaSourceFactory().setCustomCacheKey(key);
}

View file

@ -6,10 +6,11 @@ import android.content.res.Configuration;
import android.os.Build;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.view.accessibility.CaptioningManager;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.accessibility.CaptioningManager;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.text.CaptionStyleCompat;
@ -53,22 +54,12 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZ
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
public class PlayerHelper {
private PlayerHelper() {}
private static final StringBuilder stringBuilder = new StringBuilder();
private static final Formatter stringFormatter = new Formatter(stringBuilder, Locale.getDefault());
private static final NumberFormat speedFormatter = new DecimalFormat("0.##x");
private static final NumberFormat pitchFormatter = new DecimalFormat("##%");
@Retention(SOURCE)
@IntDef({MINIMIZE_ON_EXIT_MODE_NONE, MINIMIZE_ON_EXIT_MODE_BACKGROUND,
MINIMIZE_ON_EXIT_MODE_POPUP})
public @interface MinimizeMode {
int MINIMIZE_ON_EXIT_MODE_NONE = 0;
int MINIMIZE_ON_EXIT_MODE_BACKGROUND = 1;
int MINIMIZE_ON_EXIT_MODE_POPUP = 2;
}
public final class PlayerHelper {
private static final StringBuilder STRING_BUILDER = new StringBuilder();
private static final Formatter STRING_FORMATTER
= new Formatter(STRING_BUILDER, Locale.getDefault());
private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x");
private static final NumberFormat PITCH_FORMATTER = new DecimalFormat("##%");
@Retention(SOURCE)
@IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI,
@ -78,35 +69,44 @@ public class PlayerHelper {
int AUTOPLAY_TYPE_WIFI = 1;
int AUTOPLAY_TYPE_NEVER = 2;
}
private PlayerHelper() { }
////////////////////////////////////////////////////////////////////////////
// Exposed helpers
////////////////////////////////////////////////////////////////////////////
public static String getTimeString(int milliSeconds) {
public static String getTimeString(final int milliSeconds) {
int seconds = (milliSeconds % 60000) / 1000;
int minutes = (milliSeconds % 3600000) / 60000;
int hours = (milliSeconds % 86400000) / 3600000;
int days = (milliSeconds % (86400000 * 7)) / 86400000;
stringBuilder.setLength(0);
return days > 0 ? stringFormatter.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds).toString()
: hours > 0 ? stringFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString()
: stringFormatter.format("%02d:%02d", minutes, seconds).toString();
STRING_BUILDER.setLength(0);
return days > 0
? STRING_FORMATTER.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds)
.toString()
: hours > 0
? STRING_FORMATTER.format("%d:%02d:%02d", hours, minutes, seconds).toString()
: STRING_FORMATTER.format("%02d:%02d", minutes, seconds).toString();
}
public static String formatSpeed(double speed) {
return speedFormatter.format(speed);
public static String formatSpeed(final double speed) {
return SPEED_FORMATTER.format(speed);
}
public static String formatPitch(double pitch) {
return pitchFormatter.format(pitch);
public static String formatPitch(final double pitch) {
return PITCH_FORMATTER.format(pitch);
}
public static String subtitleMimeTypesOf(final MediaFormat format) {
switch (format) {
case VTT: return MimeTypes.TEXT_VTT;
case TTML: return MimeTypes.APPLICATION_TTML;
default: throw new IllegalArgumentException("Unrecognized mime type: " + format.name());
case VTT:
return MimeTypes.TEXT_VTT;
case TTML:
return MimeTypes.APPLICATION_TTML;
default:
throw new IllegalArgumentException("Unrecognized mime type: " + format.name());
}
}
@ -114,42 +114,55 @@ public class PlayerHelper {
public static String captionLanguageOf(@NonNull final Context context,
@NonNull final SubtitlesStream subtitles) {
final String displayName = subtitles.getDisplayLanguageName();
return displayName + (subtitles.isAutoGenerated() ? " (" + context.getString(R.string.caption_auto_generated)+ ")" : "");
return displayName + (subtitles.isAutoGenerated()
? " (" + context.getString(R.string.caption_auto_generated) + ")" : "");
}
@NonNull
public static String resizeTypeOf(@NonNull final Context context,
@AspectRatioFrameLayout.ResizeMode final int resizeMode) {
switch (resizeMode) {
case RESIZE_MODE_FIT: return context.getResources().getString(R.string.resize_fit);
case RESIZE_MODE_FILL: return context.getResources().getString(R.string.resize_fill);
case RESIZE_MODE_ZOOM: return context.getResources().getString(R.string.resize_zoom);
default: throw new IllegalArgumentException("Unrecognized resize mode: " + resizeMode);
case RESIZE_MODE_FIT:
return context.getResources().getString(R.string.resize_fit);
case RESIZE_MODE_FILL:
return context.getResources().getString(R.string.resize_fill);
case RESIZE_MODE_ZOOM:
return context.getResources().getString(R.string.resize_zoom);
default:
throw new IllegalArgumentException("Unrecognized resize mode: " + resizeMode);
}
}
@NonNull
public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull VideoStream video) {
public static String cacheKeyOf(@NonNull final StreamInfo info,
@NonNull final VideoStream video) {
return info.getUrl() + video.getResolution() + video.getFormat().getName();
}
@NonNull
public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull AudioStream audio) {
public static String cacheKeyOf(@NonNull final StreamInfo info,
@NonNull final AudioStream audio) {
return info.getUrl() + audio.getAverageBitrate() + audio.getFormat().getName();
}
/**
* Given a {@link StreamInfo} and the existing queue items, provide the
* {@link SinglePlayQueue} consisting of the next video for auto queuing.
* <br><br>
* <p>
* This method detects and prevents cycle by naively checking if a
* candidate next video's url already exists in the existing items.
* <br><br>
* </p>
* <p>
* To select the next video, {@link StreamInfo#getNextVideo()} is first
* checked. If it is nonnull and is not part of the existing items, then
* it will be used as the next video. Otherwise, an random item with
* non-repeating url will be selected from the {@link StreamInfo#getRelatedStreams()}.
* */
* </p>
*
* @param info currently playing stream
* @param existingItems existing items in the queue
* @return {@link SinglePlayQueue} with the next stream to queue
*/
@Nullable
public static PlayQueue autoQueueOf(@NonNull final StreamInfo info,
@NonNull final List<PlayQueueItem> existingItems) {
@ -164,7 +177,9 @@ public class PlayerHelper {
}
final List<InfoItem> relatedItems = info.getRelatedStreams();
if (relatedItems == null) return null;
if (relatedItems == null) {
return null;
}
List<StreamInfoItem> autoQueueItems = new ArrayList<>();
for (final InfoItem item : info.getRelatedStreams()) {
@ -173,7 +188,8 @@ public class PlayerHelper {
}
}
Collections.shuffle(autoQueueItems);
return autoQueueItems.isEmpty() ? null : getAutoQueuedSinglePlayQueue(autoQueueItems.get(0));
return autoQueueItems.isEmpty()
? null : getAutoQueuedSinglePlayQueue(autoQueueItems.get(0));
}
////////////////////////////////////////////////////////////////////////////
@ -234,44 +250,43 @@ public class PlayerHelper {
@NonNull
public static SeekParameters getSeekParameters(@NonNull final Context context) {
return isUsingInexactSeek(context) ?
SeekParameters.CLOSEST_SYNC : SeekParameters.EXACT;
return isUsingInexactSeek(context) ? SeekParameters.CLOSEST_SYNC : SeekParameters.EXACT;
}
public static long getPreferredCacheSize(@NonNull final Context context) {
public static long getPreferredCacheSize() {
return 64 * 1024 * 1024L;
}
public static long getPreferredFileSize(@NonNull final Context context) {
public static long getPreferredFileSize() {
return 512 * 1024L;
}
/**
* Returns the number of milliseconds the player buffers for before starting playback.
* */
public static int getPlaybackStartBufferMs(@NonNull final Context context) {
* @return the number of milliseconds the player buffers for before starting playback
*/
public static int getPlaybackStartBufferMs() {
return 500;
}
/**
* Returns the minimum number of milliseconds the player always buffers to after starting
* playback.
* */
public static int getPlaybackMinimumBufferMs(@NonNull final Context context) {
* @return the minimum number of milliseconds the player always buffers to
* after starting playback.
*/
public static int getPlaybackMinimumBufferMs() {
return 25000;
}
/**
* Returns the maximum/optimal number of milliseconds the player will buffer to once the buffer
* hits the point of {@link #getPlaybackMinimumBufferMs(Context)}.
* */
public static int getPlaybackOptimalBufferMs(@NonNull final Context context) {
* @return the maximum/optimal number of milliseconds the player will buffer to once the buffer
* hits the point of {@link #getPlaybackMinimumBufferMs()}.
*/
public static int getPlaybackOptimalBufferMs() {
return 60000;
}
public static TrackSelection.Factory getQualitySelector(@NonNull final Context context) {
return new AdaptiveTrackSelection.Factory(
/*bufferDurationRequiredForQualityIncrease=*/1000,
1000,
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION);
@ -287,7 +302,9 @@ public class PlayerHelper {
@NonNull
public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return CaptionStyleCompat.DEFAULT;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return CaptionStyleCompat.DEFAULT;
}
final CaptioningManager captioningManager = (CaptioningManager)
context.getSystemService(Context.CAPTIONING_SERVICE);
@ -299,14 +316,26 @@ public class PlayerHelper {
}
/**
* System font scaling:
* Very small - 0.25f, Small - 0.5f, Normal - 1.0f, Large - 1.5f, Very Large - 2.0f
* */
* Get scaling for captions based on system font scaling.
* <p>Options:</p>
* <ul>
* <li>Very small: 0.25f</li>
* <li>Small: 0.5f</li>
* <li>Normal: 1.0f</li>
* <li>Large: 1.5f</li>
* <li>Very large: 2.0f</li>
* </ul>
*
* @param context Android app context
* @return caption scaling
*/
public static float getCaptionScale(@NonNull final Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return 1.0f;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return 1f;
}
final CaptioningManager captioningManager = (CaptioningManager)
context.getSystemService(Context.CAPTIONING_SERVICE);
final CaptioningManager captioningManager
= (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
if (captioningManager == null || !captioningManager.isEnabled()) {
return 1.0f;
}
@ -319,7 +348,8 @@ public class PlayerHelper {
return getScreenBrightness(context, -1);
}
public static void setScreenBrightness(@NonNull final Context context, final float setScreenBrightness) {
public static void setScreenBrightness(@NonNull final Context context,
final float setScreenBrightness) {
setScreenBrightness(context, setScreenBrightness, System.currentTimeMillis());
}
@ -344,53 +374,67 @@ public class PlayerHelper {
return PreferenceManager.getDefaultSharedPreferences(context);
}
private static boolean isResumeAfterAudioFocusGain(@NonNull final Context context, final boolean b) {
return getPreferences(context).getBoolean(context.getString(R.string.resume_on_audio_focus_gain_key), b);
private static boolean isResumeAfterAudioFocusGain(@NonNull final Context context,
final boolean b) {
return getPreferences(context)
.getBoolean(context.getString(R.string.resume_on_audio_focus_gain_key), b);
}
private static boolean isVolumeGestureEnabled(@NonNull final Context context, final boolean b) {
return getPreferences(context).getBoolean(context.getString(R.string.volume_gesture_control_key), b);
private static boolean isVolumeGestureEnabled(@NonNull final Context context,
final boolean b) {
return getPreferences(context)
.getBoolean(context.getString(R.string.volume_gesture_control_key), b);
}
private static boolean isBrightnessGestureEnabled(@NonNull final Context context, final boolean b) {
return getPreferences(context).getBoolean(context.getString(R.string.brightness_gesture_control_key), b);
private static boolean isBrightnessGestureEnabled(@NonNull final Context context,
final boolean b) {
return getPreferences(context)
.getBoolean(context.getString(R.string.brightness_gesture_control_key), b);
}
private static boolean isRememberingPopupDimensions(@NonNull final Context context, final boolean b) {
return getPreferences(context).getBoolean(context.getString(R.string.popup_remember_size_pos_key), b);
private static boolean isRememberingPopupDimensions(@NonNull final Context context,
final boolean b) {
return getPreferences(context)
.getBoolean(context.getString(R.string.popup_remember_size_pos_key), b);
}
private static boolean isUsingInexactSeek(@NonNull final Context context) {
return getPreferences(context).getBoolean(context.getString(R.string.use_inexact_seek_key), false);
return getPreferences(context)
.getBoolean(context.getString(R.string.use_inexact_seek_key), false);
}
private static boolean isAutoQueueEnabled(@NonNull final Context context, final boolean b) {
return getPreferences(context).getBoolean(context.getString(R.string.auto_queue_key), b);
}
private static void setScreenBrightness(@NonNull final Context context, final float screenBrightness, final long timestamp) {
private static void setScreenBrightness(@NonNull final Context context,
final float screenBrightness, final long timestamp) {
SharedPreferences.Editor editor = getPreferences(context).edit();
editor.putFloat(context.getString(R.string.screen_brightness_key), screenBrightness);
editor.putLong(context.getString(R.string.screen_brightness_timestamp_key), timestamp);
editor.apply();
}
private static float getScreenBrightness(@NonNull final Context context, final float screenBrightness) {
private static float getScreenBrightness(@NonNull final Context context,
final float screenBrightness) {
SharedPreferences sp = getPreferences(context);
long timestamp = sp.getLong(context.getString(R.string.screen_brightness_timestamp_key), 0);
// hypothesis: 4h covers a viewing block, eg evening. External lightning conditions will change in the next
long timestamp = sp
.getLong(context.getString(R.string.screen_brightness_timestamp_key), 0);
// Hypothesis: 4h covers a viewing block, e.g. evening.
// External lightning conditions will change in the next
// viewing block so we fall back to the default brightness
if ((System.currentTimeMillis() - timestamp) > TimeUnit.HOURS.toMillis(4)) {
return screenBrightness;
} else {
return sp.getFloat(context.getString(R.string.screen_brightness_key), screenBrightness);
return sp
.getFloat(context.getString(R.string.screen_brightness_key), screenBrightness);
}
}
private static String getMinimizeOnExitAction(@NonNull final Context context,
final String key) {
return getPreferences(context).getString(context.getString(R.string.minimize_on_exit_key),
key);
return getPreferences(context)
.getString(context.getString(R.string.minimize_on_exit_key), key);
}
private static String getAutoplayType(@NonNull final Context context,
@ -399,9 +443,19 @@ public class PlayerHelper {
key);
}
private static SinglePlayQueue getAutoQueuedSinglePlayQueue(StreamInfoItem streamInfoItem) {
private static SinglePlayQueue getAutoQueuedSinglePlayQueue(
final StreamInfoItem streamInfoItem) {
SinglePlayQueue singlePlayQueue = new SinglePlayQueue(streamInfoItem);
singlePlayQueue.getItem().setAutoQueued(true);
return singlePlayQueue;
}
@Retention(SOURCE)
@IntDef({MINIMIZE_ON_EXIT_MODE_NONE, MINIMIZE_ON_EXIT_MODE_BACKGROUND,
MINIMIZE_ON_EXIT_MODE_POPUP})
public @interface MinimizeMode {
int MINIMIZE_ON_EXIT_MODE_NONE = 0;
int MINIMIZE_ON_EXIT_MODE_BACKGROUND = 1;
int MINIMIZE_ON_EXIT_MODE_POPUP = 2;
}
}

View file

@ -4,13 +4,18 @@ import android.support.v4.media.MediaDescriptionCompat;
public interface MediaSessionCallback {
void onSkipToPrevious();
void onSkipToNext();
void onSkipToIndex(final int index);
void onSkipToIndex(int index);
int getCurrentPlayingIndex();
int getQueueSize();
MediaDescriptionCompat getQueueMetadata(final int index);
MediaDescriptionCompat getQueueMetadata(int index);
void onPlay();
void onPause();
}

View file

@ -20,7 +20,6 @@ import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_T
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;
public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator {
public static final int DEFAULT_MAX_QUEUE_SIZE = 10;
@ -40,17 +39,17 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator
}
@Override
public long getSupportedQueueNavigatorActions(@Nullable Player player) {
public long getSupportedQueueNavigatorActions(@Nullable final Player player) {
return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM;
}
@Override
public void onTimelineChanged(Player player) {
public void onTimelineChanged(final Player player) {
publishFloatingQueueWindow();
}
@Override
public void onCurrentWindowIndexChanged(Player player) {
public void onCurrentWindowIndexChanged(final Player player) {
if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID
|| player.getCurrentTimeline().getWindowCount() > maxQueueSize) {
publishFloatingQueueWindow();
@ -60,22 +59,23 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator
}
@Override
public long getActiveQueueItemId(@Nullable Player player) {
public long getActiveQueueItemId(@Nullable final Player player) {
return callback.getCurrentPlayingIndex();
}
@Override
public void onSkipToPrevious(Player player, ControlDispatcher controlDispatcher) {
public void onSkipToPrevious(final Player player, final ControlDispatcher controlDispatcher) {
callback.onSkipToPrevious();
}
@Override
public void onSkipToQueueItem(Player player, ControlDispatcher controlDispatcher, long id) {
public void onSkipToQueueItem(final Player player, final ControlDispatcher controlDispatcher,
final long id) {
callback.onSkipToIndex((int) id);
}
@Override
public void onSkipToNext(Player player, ControlDispatcher controlDispatcher) {
public void onSkipToNext(final Player player, final ControlDispatcher controlDispatcher) {
callback.onSkipToNext();
}
@ -102,7 +102,8 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator
}
@Override
public boolean onCommand(Player player, ControlDispatcher controlDispatcher, String command, Bundle extras, ResultReceiver cb) {
public boolean onCommand(final Player player, final ControlDispatcher controlDispatcher,
final String command, final Bundle extras, final ResultReceiver cb) {
return false;
}
}

View file

@ -12,7 +12,7 @@ public class PlayQueuePlaybackController extends DefaultControlDispatcher {
}
@Override
public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) {
public boolean dispatchSetPlayWhenReady(final Player player, final boolean playWhenReady) {
if (playWhenReady) {
callback.onPlay();
} else {

View file

@ -1,8 +1,9 @@
package org.schabi.newpipe.player.mediasource;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
@ -15,32 +16,8 @@ import java.io.IOException;
public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSource {
private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode());
public static class FailedMediaSourceException extends Exception {
FailedMediaSourceException(String message) {
super(message);
}
FailedMediaSourceException(Throwable cause) {
super(cause);
}
}
public static final class MediaSourceResolutionException extends FailedMediaSourceException {
public MediaSourceResolutionException(String message) {
super(message);
}
}
public static final class StreamInfoLoadException extends FailedMediaSourceException {
public StreamInfoLoadException(Throwable cause) {
super(cause);
}
}
private final PlayQueueItem playQueueItem;
private final FailedMediaSourceException error;
private final long retryTimestamp;
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
@ -54,7 +31,10 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo
/**
* Permanently fail the play queue item associated with this source, with no hope of retrying.
* The error will always be propagated to ExoPlayer.
* */
*
* @param playQueueItem play queue item
* @param error exception that was the reason to fail
*/
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
@NonNull final FailedMediaSourceException error) {
this.playQueueItem = playQueueItem;
@ -80,21 +60,21 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator,
final long startPositionUs) {
return null;
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {}
public void releasePeriod(final MediaPeriod mediaPeriod) { }
@Override
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) {
Log.e(TAG, "Loading failed source: ", error);
}
@Override
protected void releaseSourceInternal() {}
protected void releaseSourceInternal() { }
@Override
public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,
@ -103,7 +83,29 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo
}
@Override
public boolean isStreamEqual(@NonNull PlayQueueItem stream) {
public boolean isStreamEqual(@NonNull final PlayQueueItem stream) {
return playQueueItem == stream;
}
public static class FailedMediaSourceException extends Exception {
FailedMediaSourceException(final String message) {
super(message);
}
FailedMediaSourceException(final Throwable cause) {
super(cause);
}
}
public static final class MediaSourceResolutionException extends FailedMediaSourceException {
public MediaSourceResolutionException(final String message) {
super(message);
}
}
public static final class StreamInfoLoadException extends FailedMediaSourceException {
public StreamInfoLoadException(final Throwable cause) {
super(cause);
}
}
}

View file

@ -1,6 +1,7 @@
package org.schabi.newpipe.player.mediasource;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -15,13 +16,11 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import java.io.IOException;
public class LoadedMediaSource implements ManagedMediaSource {
private final MediaSource source;
private final PlayQueueItem stream;
private final long expireTimestamp;
public LoadedMediaSource(@NonNull final MediaSource source,
@NonNull final PlayQueueItem stream,
public LoadedMediaSource(@NonNull final MediaSource source, @NonNull final PlayQueueItem stream,
final long expireTimestamp) {
this.source = source;
this.stream = stream;
@ -37,8 +36,9 @@ public class LoadedMediaSource implements ManagedMediaSource {
}
@Override
public void prepareSource(SourceInfoRefreshListener listener, @Nullable TransferListener mediaTransferListener) {
source.prepareSource(listener, mediaTransferListener);
public void prepareSource(final MediaSourceCaller mediaSourceCaller,
@Nullable final TransferListener mediaTransferListener) {
source.prepareSource(mediaSourceCaller, mediaTransferListener);
}
@Override
@ -47,38 +47,50 @@ public class LoadedMediaSource implements ManagedMediaSource {
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
public void enable(final MediaSourceCaller caller) {
source.enable(caller);
}
@Override
public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator,
final long startPositionUs) {
return source.createPeriod(id, allocator, startPositionUs);
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
public void releasePeriod(final MediaPeriod mediaPeriod) {
source.releasePeriod(mediaPeriod);
}
@Override
public void releaseSource(SourceInfoRefreshListener listener) {
source.releaseSource(listener);
public void disable(final MediaSourceCaller caller) {
source.disable(caller);
}
@Override
public void addEventListener(Handler handler, MediaSourceEventListener eventListener) {
public void releaseSource(final MediaSourceCaller mediaSourceCaller) {
source.releaseSource(mediaSourceCaller);
}
@Override
public void addEventListener(final Handler handler,
final MediaSourceEventListener eventListener) {
source.addEventListener(handler, eventListener);
}
@Override
public void removeEventListener(MediaSourceEventListener eventListener) {
public void removeEventListener(final MediaSourceEventListener eventListener) {
source.removeEventListener(eventListener);
}
@Override
public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity,
public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,
final boolean isInterruptable) {
return newIdentity != stream || (isInterruptable && isExpired());
}
@Override
public boolean isStreamEqual(@NonNull PlayQueueItem stream) {
return this.stream == stream;
public boolean isStreamEqual(@NonNull final PlayQueueItem otherStream) {
return this.stream == otherStream;
}
}

View file

@ -1,6 +1,7 @@
package org.schabi.newpipe.player.mediasource;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.MediaSource;
@ -10,18 +11,27 @@ public interface ManagedMediaSource extends MediaSource {
/**
* Determines whether or not this {@link ManagedMediaSource} can be replaced.
*
* @param newIdentity a stream the {@link ManagedMediaSource} should encapsulate over, if
* it is different from the existing stream in the
* {@link ManagedMediaSource}, then it should be replaced.
* @param newIdentity a stream the {@link ManagedMediaSource} should encapsulate over, if
* it is different from the existing stream in the
* {@link ManagedMediaSource}, then it should be replaced.
* @param isInterruptable specifies if this {@link ManagedMediaSource} potentially
* being played.
* */
boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,
final boolean isInterruptable);
* @return whether this could be replaces
*/
boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity, boolean isInterruptable);
/**
* Determines if the {@link PlayQueueItem} is the one the
* {@link ManagedMediaSource} encapsulates over.
* */
boolean isStreamEqual(@NonNull final PlayQueueItem stream);
*
* @param stream play queue item to check
* @return whether this source is for the specified stream
*/
boolean isStreamEqual(@NonNull PlayQueueItem stream);
@Nullable
@Override
default Object getTag() {
return this;
}
}

View file

@ -1,5 +1,7 @@
package org.schabi.newpipe.player.mediasource;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -7,7 +9,8 @@ import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ShuffleOrder;
public class ManagedMediaSourcePlaylist {
@NonNull private final ConcatenatingMediaSource internalSource;
@NonNull
private final ConcatenatingMediaSource internalSource;
public ManagedMediaSourcePlaylist() {
internalSource = new ConcatenatingMediaSource(/*isPlaylistAtomic=*/false,
@ -25,11 +28,14 @@ public class ManagedMediaSourcePlaylist {
/**
* Returns the {@link ManagedMediaSource} at the given index of the playlist.
* If the index is invalid, then null is returned.
* */
*
* @param index index of {@link ManagedMediaSource} to get from the playlist
* @return the {@link ManagedMediaSource} at the given index of the playlist
*/
@Nullable
public ManagedMediaSource get(final int index) {
return (index < 0 || index >= size()) ?
null : (ManagedMediaSource) internalSource.getMediaSource(index);
return (index < 0 || index >= size())
? null : (ManagedMediaSource) internalSource.getMediaSource(index).getTag();
}
@NonNull
@ -46,15 +52,17 @@ public class ManagedMediaSourcePlaylist {
* {@link PlaceholderMediaSource}.
*
* @see #append(ManagedMediaSource)
* */
*/
public synchronized void expand() {
append(new PlaceholderMediaSource());
}
/**
* Appends a {@link ManagedMediaSource} to the end of {@link ConcatenatingMediaSource}.
*
* @see ConcatenatingMediaSource#addMediaSource
* */
* @param source {@link ManagedMediaSource} to append
*/
public synchronized void append(@NonNull final ManagedMediaSource source) {
internalSource.addMediaSource(source);
}
@ -62,10 +70,14 @@ public class ManagedMediaSourcePlaylist {
/**
* Removes a {@link ManagedMediaSource} from {@link ConcatenatingMediaSource}
* at the given index. If this index is out of bound, then the removal is ignored.
*
* @see ConcatenatingMediaSource#removeMediaSource(int)
* */
* @param index of {@link ManagedMediaSource} to be removed
*/
public synchronized void remove(final int index) {
if (index < 0 || index > internalSource.getSize()) return;
if (index < 0 || index > internalSource.getSize()) {
return;
}
internalSource.removeMediaSource(index);
}
@ -74,11 +86,18 @@ public class ManagedMediaSourcePlaylist {
* Moves a {@link ManagedMediaSource} in {@link ConcatenatingMediaSource}
* from the given source index to the target index. If either index is out of bound,
* then the call is ignored.
*
* @see ConcatenatingMediaSource#moveMediaSource(int, int)
* */
* @param source original index of {@link ManagedMediaSource}
* @param target new index of {@link ManagedMediaSource}
*/
public synchronized void move(final int source, final int target) {
if (source < 0 || target < 0) return;
if (source >= internalSource.getSize() || target >= internalSource.getSize()) return;
if (source < 0 || target < 0) {
return;
}
if (source >= internalSource.getSize() || target >= internalSource.getSize()) {
return;
}
internalSource.moveMediaSource(source, target);
}
@ -86,20 +105,30 @@ public class ManagedMediaSourcePlaylist {
/**
* Invalidates the {@link ManagedMediaSource} at the given index by replacing it
* with a {@link PlaceholderMediaSource}.
*
* @see #update(int, ManagedMediaSource, Handler, Runnable)
* */
* @param index index of {@link ManagedMediaSource} to invalidate
* @param handler the {@link Handler} to run {@code finalizingAction}
* @param finalizingAction a {@link Runnable} which is executed immediately
* after the media source has been removed from the playlist
*/
public synchronized void invalidate(final int index,
@Nullable final Handler handler,
@Nullable final Runnable finalizingAction) {
if (get(index) instanceof PlaceholderMediaSource) return;
if (get(index) instanceof PlaceholderMediaSource) {
return;
}
update(index, new PlaceholderMediaSource(), handler, finalizingAction);
}
/**
* Updates the {@link ManagedMediaSource} in {@link ConcatenatingMediaSource}
* at the given index with a given {@link ManagedMediaSource}.
*
* @see #update(int, ManagedMediaSource, Handler, Runnable)
* */
* @param index index of {@link ManagedMediaSource} to update
* @param source new {@link ManagedMediaSource} to use
*/
public synchronized void update(final int index, @NonNull final ManagedMediaSource source) {
update(index, source, null, /*doNothing=*/null);
}
@ -108,13 +137,21 @@ public class ManagedMediaSourcePlaylist {
* Updates the {@link ManagedMediaSource} in {@link ConcatenatingMediaSource}
* at the given index with a given {@link ManagedMediaSource}. If the index is out of bound,
* then the replacement is ignored.
*
* @see ConcatenatingMediaSource#addMediaSource
* @see ConcatenatingMediaSource#removeMediaSource(int, Handler, Runnable)
* */
* @param index index of {@link ManagedMediaSource} to update
* @param source new {@link ManagedMediaSource} to use
* @param handler the {@link Handler} to run {@code finalizingAction}
* @param finalizingAction a {@link Runnable} which is executed immediately
* after the media source has been removed from the playlist
*/
public synchronized void update(final int index, @NonNull final ManagedMediaSource source,
@Nullable final Handler handler,
@Nullable final Runnable finalizingAction) {
if (index < 0 || index >= internalSource.getSize()) return;
if (index < 0 || index >= internalSource.getSize()) {
return;
}
// Add and remove are sequential on the same thread, therefore here, the exoplayer
// message queue must receive and process add before remove, effectively treating them

View file

@ -12,20 +12,32 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
public class PlaceholderMediaSource extends BaseMediaSource implements ManagedMediaSource {
// Do nothing, so this will stall the playback
@Override public void maybeThrowSourceInfoRefreshError() {}
@Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { return null; }
@Override public void releasePeriod(MediaPeriod mediaPeriod) {}
@Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {}
@Override protected void releaseSourceInternal() {}
@Override
public void maybeThrowSourceInfoRefreshError() { }
@Override
public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity,
public MediaPeriod createPeriod(final MediaPeriodId id, final Allocator allocator,
final long startPositionUs) {
return null;
}
@Override
public void releasePeriod(final MediaPeriod mediaPeriod) { }
@Override
protected void prepareSourceInternal(@Nullable final TransferListener mediaTransferListener) { }
@Override
protected void releaseSourceInternal() { }
@Override
public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,
final boolean isInterruptable) {
return true;
}
@Override
public boolean isStreamEqual(@NonNull PlayQueueItem stream) {
public boolean isStreamEqual(@NonNull final PlayQueueItem stream) {
return false;
}
}

View file

@ -27,25 +27,31 @@ public class BasePlayerMediaSession implements MediaSessionCallback {
}
@Override
public void onSkipToIndex(int index) {
if (player.getPlayQueue() == null) return;
public void onSkipToIndex(final int index) {
if (player.getPlayQueue() == null) {
return;
}
player.onSelected(player.getPlayQueue().getItem(index));
}
@Override
public int getCurrentPlayingIndex() {
if (player.getPlayQueue() == null) return -1;
if (player.getPlayQueue() == null) {
return -1;
}
return player.getPlayQueue().getIndex();
}
@Override
public int getQueueSize() {
if (player.getPlayQueue() == null) return -1;
if (player.getPlayQueue() == null) {
return -1;
}
return player.getPlayQueue().size();
}
@Override
public MediaDescriptionCompat getQueueMetadata(int index) {
public MediaDescriptionCompat getQueueMetadata(final int index) {
if (player.getPlayQueue() == null || player.getPlayQueue().getItem(index) == null) {
return null;
}
@ -60,13 +66,17 @@ public class BasePlayerMediaSession implements MediaSessionCallback {
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_DURATION, item.getDuration() * 1000);
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, index + 1);
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
additionalMetadata
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
descriptionBuilder.setExtras(additionalMetadata);
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
if (thumbnailUri != null) descriptionBuilder.setIconUri(thumbnailUri);
if (thumbnailUri != null) {
descriptionBuilder.setIconUri(thumbnailUri);
}
return descriptionBuilder.build();
}

View file

@ -1,5 +1,6 @@
package org.schabi.newpipe.player.playback;
import android.content.Context;
import android.text.TextUtils;
import android.util.Pair;
@ -7,8 +8,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.RendererCapabilities.Capabilities;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
@ -18,16 +19,21 @@ import com.google.android.exoplayer2.util.Assertions;
/**
* This class allows irregular text language labels for use when selecting text captions and
* is mostly a copy-paste from {@link DefaultTrackSelector}.
*
* <p>
* This is a hack and should be removed once ExoPlayer fixes language normalization to accept
* a broader set of languages.
* */
* </p>
*/
public class CustomTrackSelector extends DefaultTrackSelector {
private String preferredTextLanguage;
public CustomTrackSelector(TrackSelection.Factory adaptiveTrackSelectionFactory) {
super(adaptiveTrackSelectionFactory);
public CustomTrackSelector(final Context context,
final TrackSelection.Factory adaptiveTrackSelectionFactory) {
super(context, adaptiveTrackSelectionFactory);
}
private static boolean formatHasLanguage(final Format format, final String language) {
return language != null && TextUtils.equals(language, format.language);
}
public String getPreferredTextLanguage() {
@ -42,40 +48,36 @@ public class CustomTrackSelector extends DefaultTrackSelector {
}
}
private static boolean formatHasLanguage(Format format, String language) {
return language != null && TextUtils.equals(language, format.language);
}
@Override
@Nullable
protected Pair<TrackSelection.Definition, TextTrackScore> selectTextTrack(
TrackGroupArray groups,
int[][] formatSupport,
Parameters params,
@Nullable String selectedAudioLanguage)
throws ExoPlaybackException {
final TrackGroupArray groups,
@NonNull final int[][] formatSupport,
@NonNull final Parameters params,
@Nullable final String selectedAudioLanguage) {
TrackGroup selectedGroup = null;
int selectedTrackIndex = C.INDEX_UNSET;
int newPipeTrackScore = 0;
TextTrackScore selectedTrackScore = null;
for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
TrackGroup trackGroup = groups.get(groupIndex);
int[] trackFormatSupport = formatSupport[groupIndex];
@Capabilities int[] trackFormatSupport = formatSupport[groupIndex];
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
if (isSupported(trackFormatSupport[trackIndex],
params.exceedRendererCapabilitiesIfNecessary)) {
Format format = trackGroup.getFormat(trackIndex);
TextTrackScore trackScore =
new TextTrackScore(
format, params, trackFormatSupport[trackIndex], selectedAudioLanguage);
TextTrackScore trackScore = new TextTrackScore(format, params,
trackFormatSupport[trackIndex], selectedAudioLanguage);
if (formatHasLanguage(format, preferredTextLanguage)) {
selectedGroup = trackGroup;
selectedTrackIndex = trackIndex;
selectedTrackScore = trackScore;
// found user selected match (perfect!)
break;
} else if (trackScore.isWithinConstraints
&& (selectedTrackScore == null || trackScore.compareTo(selectedTrackScore) > 0)) {
break; // found user selected match (perfect!)
} else if (trackScore.isWithinConstraints && (selectedTrackScore == null
|| trackScore.compareTo(selectedTrackScore) > 0)) {
selectedGroup = trackGroup;
selectedTrackIndex = trackIndex;
selectedTrackScore = trackScore;
@ -83,10 +85,8 @@ public class CustomTrackSelector extends DefaultTrackSelector {
}
}
}
return selectedGroup == null
? null
: Pair.create(
new TrackSelection.Definition(selectedGroup, selectedTrackIndex),
Assertions.checkNotNull(selectedTrackScore));
return selectedGroup == null ? null
: Pair.create(new TrackSelection.Definition(selectedGroup, selectedTrackIndex),
Assertions.checkNotNull(selectedTrackScore));
}
}

View file

@ -1,9 +1,11 @@
package org.schabi.newpipe.player.playback;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.ArraySet;
import android.util.Log;
import com.google.android.exoplayer2.source.MediaSource;
@ -42,50 +44,20 @@ import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfo
import static org.schabi.newpipe.player.playqueue.PlayQueue.DEBUG;
public class MediaSourceManager {
@NonNull private final String TAG = "MediaSourceManager@" + hashCode();
@NonNull
private final String TAG = "MediaSourceManager@" + hashCode();
/**
* Determines how many streams before and after the current stream should be loaded.
* The default value (1) ensures seamless playback under typical network settings.
* <br><br>
* <p>
* The streams after the current will be loaded into the playlist timeline while the
* streams before will only be cached for future usage.
* </p>
*
* @see #onMediaSourceReceived(PlayQueueItem, ManagedMediaSource)
* */
private final static int WINDOW_SIZE = 1;
@NonNull private final PlaybackListener playbackListener;
@NonNull private final PlayQueue playQueue;
/**
* Determines the gap time between the playback position and the playback duration which
* the {@link #getEdgeIntervalSignal()} begins to request loading.
*
* @see #progressUpdateIntervalMillis
* */
private final long playbackNearEndGapMillis;
/**
* Determines the interval which the {@link #getEdgeIntervalSignal()} waits for between
* each request for loading, once {@link #playbackNearEndGapMillis} has reached.
* */
private final long progressUpdateIntervalMillis;
@NonNull private final Observable<Long> nearEndIntervalSignal;
/**
* Process only the last load order when receiving a stream of load orders (lessens I/O).
* <br><br>
* The higher it is, the less loading occurs during rapid noncritical timeline changes.
* <br><br>
* Not recommended to go below 100ms.
*
* @see #loadDebounced()
* */
private final long loadDebounceMillis;
@NonNull private final Disposable debouncedLoader;
@NonNull private final PublishSubject<Long> debouncedSignal;
@NonNull private Subscription playQueueReactor;
*/
private static final int WINDOW_SIZE = 1;
/**
* Determines the maximum number of disposables allowed in the {@link #loaderReactor}.
@ -94,20 +66,68 @@ public class MediaSourceManager {
*
* @see #loadImmediate()
* @see #maybeLoadItem(PlayQueueItem)
* */
private final static int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1;
@NonNull private final CompositeDisposable loaderReactor;
@NonNull private final Set<PlayQueueItem> loadingItems;
*/
private static final int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1;
@NonNull private final AtomicBoolean isBlocked;
@NonNull
private final PlaybackListener playbackListener;
@NonNull
private final PlayQueue playQueue;
@NonNull private ManagedMediaSourcePlaylist playlist;
/**
* Determines the gap time between the playback position and the playback duration which
* the {@link #getEdgeIntervalSignal()} begins to request loading.
*
* @see #progressUpdateIntervalMillis
*/
private final long playbackNearEndGapMillis;
/**
* Determines the interval which the {@link #getEdgeIntervalSignal()} waits for between
* each request for loading, once {@link #playbackNearEndGapMillis} has reached.
*/
private final long progressUpdateIntervalMillis;
@NonNull
private final Observable<Long> nearEndIntervalSignal;
/**
* Process only the last load order when receiving a stream of load orders (lessens I/O).
* <p>
* The higher it is, the less loading occurs during rapid noncritical timeline changes.
* </p>
* <p>
* Not recommended to go below 100ms.
* </p>
*
* @see #loadDebounced()
*/
private final long loadDebounceMillis;
@NonNull
private final Disposable debouncedLoader;
@NonNull
private final PublishSubject<Long> debouncedSignal;
@NonNull
private Subscription playQueueReactor;
@NonNull
private final CompositeDisposable loaderReactor;
@NonNull
private final Set<PlayQueueItem> loadingItems;
@NonNull
private final AtomicBoolean isBlocked;
@NonNull
private ManagedMediaSourcePlaylist playlist;
private Handler removeMediaSourceHandler = new Handler();
public MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue) {
this(listener, playQueue, /*loadDebounceMillis=*/400L,
this(listener, playQueue, 400L,
/*playbackNearEndGapMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.SECONDS),
/*progressUpdateIntervalMillis*/TimeUnit.MILLISECONDS.convert(2, TimeUnit.SECONDS));
}
@ -121,9 +141,9 @@ public class MediaSourceManager {
throw new IllegalArgumentException("Play Queue has not been initialized.");
}
if (playbackNearEndGapMillis < progressUpdateIntervalMillis) {
throw new IllegalArgumentException("Playback end gap=[" + playbackNearEndGapMillis +
" ms] must be longer than update interval=[ " + progressUpdateIntervalMillis +
" ms] for them to be useful.");
throw new IllegalArgumentException("Playback end gap=[" + playbackNearEndGapMillis
+ " ms] must be longer than update interval=[ " + progressUpdateIntervalMillis
+ " ms] for them to be useful.");
}
this.playbackListener = listener;
@ -154,11 +174,14 @@ public class MediaSourceManager {
/*//////////////////////////////////////////////////////////////////////////
// Exposed Methods
//////////////////////////////////////////////////////////////////////////*/
/**
* Dispose the manager and releases all message buses and loaders.
* */
*/
public void dispose() {
if (DEBUG) Log.d(TAG, "close() called.");
if (DEBUG) {
Log.d(TAG, "close() called.");
}
debouncedSignal.onComplete();
debouncedLoader.dispose();
@ -174,22 +197,22 @@ public class MediaSourceManager {
private Subscriber<PlayQueueEvent> getReactor() {
return new Subscriber<PlayQueueEvent>() {
@Override
public void onSubscribe(@NonNull Subscription d) {
public void onSubscribe(@NonNull final Subscription d) {
playQueueReactor.cancel();
playQueueReactor = d;
playQueueReactor.request(1);
}
@Override
public void onNext(@NonNull PlayQueueEvent playQueueMessage) {
public void onNext(@NonNull final PlayQueueEvent playQueueMessage) {
onPlayQueueChanged(playQueueMessage);
}
@Override
public void onError(@NonNull Throwable e) {}
public void onError(@NonNull final Throwable e) { }
@Override
public void onComplete() {}
public void onComplete() { }
};
}
@ -264,19 +287,27 @@ public class MediaSourceManager {
}
private boolean isPlaybackReady() {
if (playlist.size() != playQueue.size()) return false;
if (playlist.size() != playQueue.size()) {
return false;
}
final ManagedMediaSource mediaSource = playlist.get(playQueue.getIndex());
if (mediaSource == null) return false;
if (mediaSource == null) {
return false;
}
final PlayQueueItem playQueueItem = playQueue.getItem();
return mediaSource.isStreamEqual(playQueueItem);
}
private void maybeBlock() {
if (DEBUG) Log.d(TAG, "maybeBlock() called.");
if (DEBUG) {
Log.d(TAG, "maybeBlock() called.");
}
if (isBlocked.get()) return;
if (isBlocked.get()) {
return;
}
playbackListener.onPlaybackBlock();
resetSources();
@ -285,7 +316,9 @@ public class MediaSourceManager {
}
private void maybeUnblock() {
if (DEBUG) Log.d(TAG, "maybeUnblock() called.");
if (DEBUG) {
Log.d(TAG, "maybeUnblock() called.");
}
if (isBlocked.get()) {
isBlocked.set(false);
@ -298,10 +331,14 @@ public class MediaSourceManager {
//////////////////////////////////////////////////////////////////////////*/
private void maybeSync() {
if (DEBUG) Log.d(TAG, "maybeSync() called.");
if (DEBUG) {
Log.d(TAG, "maybeSync() called.");
}
final PlayQueueItem currentItem = playQueue.getItem();
if (isBlocked.get() || currentItem == null) return;
if (isBlocked.get() || currentItem == null) {
return;
}
playbackListener.onPlaybackSynchronize(currentItem);
}
@ -318,7 +355,8 @@ public class MediaSourceManager {
//////////////////////////////////////////////////////////////////////////*/
private Observable<Long> getEdgeIntervalSignal() {
return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS)
return Observable.interval(progressUpdateIntervalMillis,
TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
.filter(ignored ->
playbackListener.isApproachingPlaybackEdge(playbackNearEndGapMillis));
}
@ -336,9 +374,13 @@ public class MediaSourceManager {
}
private void loadImmediate() {
if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called");
if (DEBUG) {
Log.d(TAG, "MediaSource - loadImmediate() called");
}
final ItemsToLoad itemsToLoad = getItemsToLoad(playQueue);
if (itemsToLoad == null) return;
if (itemsToLoad == null) {
return;
}
// Evict the previous items being loaded to free up memory, before start loading new ones
maybeClearLoaders();
@ -350,12 +392,18 @@ public class MediaSourceManager {
}
private void maybeLoadItem(@NonNull final PlayQueueItem item) {
if (DEBUG) Log.d(TAG, "maybeLoadItem() called.");
if (playQueue.indexOf(item) >= playlist.size()) return;
if (DEBUG) {
Log.d(TAG, "maybeLoadItem() called.");
}
if (playQueue.indexOf(item) >= playlist.size()) {
return;
}
if (!loadingItems.contains(item) && isCorrectionNeeded(item)) {
if (DEBUG) Log.d(TAG, "MediaSource - Loading=[" + item.getTitle() +
"] with url=[" + item.getUrl() + "]");
if (DEBUG) {
Log.d(TAG, "MediaSource - Loading=[" + item.getTitle() + "] "
+ "with url=[" + item.getUrl() + "]");
}
loadingItems.add(item);
final Disposable loader = getLoadedMediaSource(item)
@ -370,16 +418,16 @@ public class MediaSourceManager {
return stream.getStream().map(streamInfo -> {
final MediaSource source = playbackListener.sourceOf(stream, streamInfo);
if (source == null) {
final String message = "Unable to resolve source from stream info." +
" URL: " + stream.getUrl() +
", audio count: " + streamInfo.getAudioStreams().size() +
", video count: " + streamInfo.getVideoOnlyStreams().size() +
streamInfo.getVideoStreams().size();
final String message = "Unable to resolve source from stream info. "
+ "URL: " + stream.getUrl() + ", "
+ "audio count: " + streamInfo.getAudioStreams().size() + ", "
+ "video count: " + streamInfo.getVideoOnlyStreams().size() + ", "
+ streamInfo.getVideoStreams().size();
return new FailedMediaSource(stream, new MediaSourceResolutionException(message));
}
final long expiration = System.currentTimeMillis() +
ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId());
final long expiration = System.currentTimeMillis()
+ ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId());
return new LoadedMediaSource(source, stream, expiration);
}).onErrorReturn(throwable -> new FailedMediaSource(stream,
new StreamInfoLoadException(throwable)));
@ -387,17 +435,22 @@ public class MediaSourceManager {
private void onMediaSourceReceived(@NonNull final PlayQueueItem item,
@NonNull final ManagedMediaSource mediaSource) {
if (DEBUG) Log.d(TAG, "MediaSource - Loaded=[" + item.getTitle() +
"] with url=[" + item.getUrl() + "]");
if (DEBUG) {
Log.d(TAG, "MediaSource - Loaded=[" + item.getTitle()
+ "] with url=[" + item.getUrl() + "]");
}
loadingItems.remove(item);
final int itemIndex = playQueue.indexOf(item);
// Only update the playlist timeline for items at the current index or after.
if (isCorrectionNeeded(item)) {
if (DEBUG) Log.d(TAG, "MediaSource - Updating index=[" + itemIndex + "] with " +
"title=[" + item.getTitle() + "] at url=[" + item.getUrl() + "]");
playlist.update(itemIndex, mediaSource, removeMediaSourceHandler, this::maybeSynchronizePlayer);
if (DEBUG) {
Log.d(TAG, "MediaSource - Updating index=[" + itemIndex + "] with "
+ "title=[" + item.getTitle() + "] at url=[" + item.getUrl() + "]");
}
playlist.update(itemIndex, mediaSource, removeMediaSourceHandler,
this::maybeSynchronizePlayer);
}
}
@ -406,17 +459,21 @@ public class MediaSourceManager {
* {@link com.google.android.exoplayer2.source.ConcatenatingMediaSource}
* for a given {@link PlayQueueItem} needs replacement, either due to gapless playback
* readiness or playlist desynchronization.
* <br><br>
* <p>
* If the given {@link PlayQueueItem} is currently being played and is already loaded,
* then correction is not only needed if the playlist is desynchronized. Otherwise, the
* check depends on the status (e.g. expiration or placeholder) of the
* {@link ManagedMediaSource}.
* */
* </p>
*
* @param item {@link PlayQueueItem} to check
* @return whether a correction is needed
*/
private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) {
final int index = playQueue.indexOf(item);
final ManagedMediaSource mediaSource = playlist.get(index);
return mediaSource != null && mediaSource.shouldBeReplacedWith(item,
/*mightBeInProgress=*/index != playQueue.getIndex());
index != playQueue.getIndex());
}
/**
@ -429,42 +486,53 @@ public class MediaSourceManager {
* <br><br>
* Under both cases, {@link #maybeSync()} will be called to ensure the listener
* is up-to-date.
* */
*/
private void maybeRenewCurrentIndex() {
final int currentIndex = playQueue.getIndex();
final ManagedMediaSource currentSource = playlist.get(currentIndex);
if (currentSource == null) return;
if (currentSource == null) {
return;
}
final PlayQueueItem currentItem = playQueue.getItem();
if (!currentSource.shouldBeReplacedWith(currentItem, /*canInterruptOnRenew=*/true)) {
if (!currentSource.shouldBeReplacedWith(currentItem, true)) {
maybeSynchronizePlayer();
return;
}
if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " +
"index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]");
if (DEBUG) {
Log.d(TAG, "MediaSource - Reloading currently playing, "
+ "index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]");
}
playlist.invalidate(currentIndex, removeMediaSourceHandler, this::loadImmediate);
}
private void maybeClearLoaders() {
if (DEBUG) Log.d(TAG, "MediaSource - maybeClearLoaders() called.");
if (!loadingItems.contains(playQueue.getItem()) &&
loaderReactor.size() > MAXIMUM_LOADER_SIZE) {
if (DEBUG) {
Log.d(TAG, "MediaSource - maybeClearLoaders() called.");
}
if (!loadingItems.contains(playQueue.getItem())
&& loaderReactor.size() > MAXIMUM_LOADER_SIZE) {
loaderReactor.clear();
loadingItems.clear();
}
}
/*//////////////////////////////////////////////////////////////////////////
// MediaSource Playlist Helpers
//////////////////////////////////////////////////////////////////////////*/
private void resetSources() {
if (DEBUG) Log.d(TAG, "resetSources() called.");
if (DEBUG) {
Log.d(TAG, "resetSources() called.");
}
playlist = new ManagedMediaSourcePlaylist();
}
private void populateSources() {
if (DEBUG) Log.d(TAG, "populateSources() called.");
if (DEBUG) {
Log.d(TAG, "populateSources() called.");
}
while (playlist.size() < playQueue.size()) {
playlist.expand();
}
@ -473,12 +541,15 @@ public class MediaSourceManager {
/*//////////////////////////////////////////////////////////////////////////
// Manager Helpers
//////////////////////////////////////////////////////////////////////////*/
@Nullable
private static ItemsToLoad getItemsToLoad(@NonNull final PlayQueue playQueue) {
// The current item has higher priority
final int currentIndex = playQueue.getIndex();
final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
if (currentItem == null) return null;
if (currentItem == null) {
return null;
}
// The rest are just for seamless playback
// Although timeline is not updated prior to the current index, these sources are still
@ -487,12 +558,13 @@ public class MediaSourceManager {
final int rightLimit = currentIndex + MediaSourceManager.WINDOW_SIZE + 1;
final int rightBound = Math.min(playQueue.size(), rightLimit);
final Set<PlayQueueItem> neighbors = new ArraySet<>(
playQueue.getStreams().subList(leftBound,rightBound));
playQueue.getStreams().subList(leftBound, rightBound));
// Do a round robin
final int excess = rightLimit - playQueue.size();
if (excess >= 0) {
neighbors.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
neighbors.addAll(playQueue.getStreams()
.subList(0, Math.min(playQueue.size(), excess)));
}
neighbors.remove(currentItem);
@ -500,8 +572,10 @@ public class MediaSourceManager {
}
private static class ItemsToLoad {
@NonNull final private PlayQueueItem center;
@NonNull final private Collection<PlayQueueItem> neighbors;
@NonNull
private final PlayQueueItem center;
@NonNull
private final Collection<PlayQueueItem> neighbors;
ItemsToLoad(@NonNull final PlayQueueItem center,
@NonNull final Collection<PlayQueueItem> neighbors) {

View file

@ -9,57 +9,72 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
public interface PlaybackListener {
/**
* Called to check if the currently playing stream is approaching the end of its playback.
* Implementation should return true when the current playback position is progressing within
* timeToEndMillis or less to its playback during.
*
* <p>
* May be called at any time.
* */
boolean isApproachingPlaybackEdge(final long timeToEndMillis);
* </p>
*
* @param timeToEndMillis
* @return whether the stream is approaching end of playback
*/
boolean isApproachingPlaybackEdge(long timeToEndMillis);
/**
* Called when the stream at the current queue index is not ready yet.
* Signals to the listener to block the player from playing anything and notify the source
* is now invalid.
*
* <p>
* May be called at any time.
* */
* </p>
*/
void onPlaybackBlock();
/**
* Called when the stream at the current queue index is ready.
* Signals to the listener to resume the player by preparing a new source.
*
* <p>
* May be called only when the player is blocked.
* */
void onPlaybackUnblock(final MediaSource mediaSource);
* </p>
*
* @param mediaSource
*/
void onPlaybackUnblock(MediaSource mediaSource);
/**
* Called when the queue index is refreshed.
* Signals to the listener to synchronize the player's window to the manager's
* window.
*
* <p>
* May be called anytime at any amount once unblock is called.
* */
void onPlaybackSynchronize(@NonNull final PlayQueueItem item);
* </p>
*
* @param item
*/
void onPlaybackSynchronize(@NonNull PlayQueueItem item);
/**
* Requests the listener to resolve a stream info into a media source
* according to the listener's implementation (background, popup or main video player).
*
* <p>
* May be called at any time.
* */
* </p>
* @param item
* @param info
* @return the corresponding {@link MediaSource}
*/
@Nullable
MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info);
MediaSource sourceOf(PlayQueueItem item, StreamInfo info);
/**
* Called when the play queue can no longer to played or used.
* Currently, this means the play queue is empty and complete.
* Signals to the listener that it should shutdown.
*
* <p>
* May be called at any time.
* */
* </p>
*/
void onPlaybackShutdown();
}

View file

@ -5,6 +5,7 @@ import android.util.Log;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import java.util.ArrayList;
@ -17,34 +18,31 @@ import io.reactivex.disposables.Disposable;
abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> extends PlayQueue {
boolean isInitial;
boolean isComplete;
private boolean isComplete;
final int serviceId;
final String baseUrl;
String nextUrl;
Page nextPage;
transient Disposable fetchReactor;
private transient Disposable fetchReactor;
AbstractInfoPlayQueue(final U item) {
this(item.getServiceId(), item.getUrl(), null, Collections.emptyList(), 0);
}
AbstractInfoPlayQueue(final int serviceId,
final String url,
final String nextPageUrl,
final List<StreamInfoItem> streams,
final int index) {
AbstractInfoPlayQueue(final int serviceId, final String url, final Page nextPage,
final List<StreamInfoItem> streams, final int index) {
super(index, extractListItems(streams));
this.baseUrl = url;
this.nextUrl = nextPageUrl;
this.nextPage = nextPage;
this.serviceId = serviceId;
this.isInitial = streams.isEmpty();
this.isComplete = !isInitial && (nextPageUrl == null || nextPageUrl.isEmpty());
this.isComplete = !isInitial && !Page.isValid(nextPage);
}
abstract protected String getTag();
protected abstract String getTag();
@Override
public boolean isComplete() {
@ -54,8 +52,9 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext
SingleObserver<T> getHeadListObserver() {
return new SingleObserver<T>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
if (isComplete || !isInitial || (fetchReactor != null && !fetchReactor.isDisposed())) {
public void onSubscribe(@NonNull final Disposable d) {
if (isComplete || !isInitial || (fetchReactor != null
&& !fetchReactor.isDisposed())) {
d.dispose();
} else {
fetchReactor = d;
@ -63,10 +62,12 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext
}
@Override
public void onSuccess(@NonNull T result) {
public void onSuccess(@NonNull final T result) {
isInitial = false;
if (!result.hasNextPage()) isComplete = true;
nextUrl = result.getNextPageUrl();
if (!result.hasNextPage()) {
isComplete = true;
}
nextPage = result.getNextPage();
append(extractListItems(result.getRelatedItems()));
@ -75,7 +76,7 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext
}
@Override
public void onError(@NonNull Throwable e) {
public void onError(@NonNull final Throwable e) {
Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e);
isComplete = true;
append(); // Notify change
@ -86,8 +87,9 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext
SingleObserver<ListExtractor.InfoItemsPage> getNextPageObserver() {
return new SingleObserver<ListExtractor.InfoItemsPage>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
if (isComplete || isInitial || (fetchReactor != null && !fetchReactor.isDisposed())) {
public void onSubscribe(@NonNull final Disposable d) {
if (isComplete || isInitial || (fetchReactor != null
&& !fetchReactor.isDisposed())) {
d.dispose();
} else {
fetchReactor = d;
@ -95,9 +97,11 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext
}
@Override
public void onSuccess(@NonNull ListExtractor.InfoItemsPage result) {
if (!result.hasNextPage()) isComplete = true;
nextUrl = result.getNextPageUrl();
public void onSuccess(@NonNull final ListExtractor.InfoItemsPage result) {
if (!result.hasNextPage()) {
isComplete = true;
}
nextPage = result.getNextPage();
append(extractListItems(result.getItems()));
@ -106,7 +110,7 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext
}
@Override
public void onError(@NonNull Throwable e) {
public void onError(@NonNull final Throwable e) {
Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e);
isComplete = true;
append(); // Notify change
@ -117,7 +121,9 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext
@Override
public void dispose() {
super.dispose();
if (fetchReactor != null) fetchReactor.dispose();
if (fetchReactor != null) {
fetchReactor.dispose();
}
fetchReactor = null;
}

View file

@ -1,6 +1,7 @@
package org.schabi.newpipe.player.playqueue;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
@ -17,15 +18,15 @@ public final class ChannelPlayQueue extends AbstractInfoPlayQueue<ChannelInfo, C
}
public ChannelPlayQueue(final ChannelInfo info) {
this(info.getServiceId(), info.getUrl(), info.getNextPageUrl(), info.getRelatedItems(), 0);
this(info.getServiceId(), info.getUrl(), info.getNextPage(), info.getRelatedItems(), 0);
}
public ChannelPlayQueue(final int serviceId,
final String url,
final String nextPageUrl,
final Page nextPage,
final List<StreamInfoItem> streams,
final int index) {
super(serviceId, url, nextPageUrl, streams, index);
super(serviceId, url, nextPage, streams, index);
}
@Override
@ -41,7 +42,7 @@ public final class ChannelPlayQueue extends AbstractInfoPlayQueue<ChannelInfo, C
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getHeadListObserver());
} else {
ExtractorHelper.getMoreChannelItems(this.serviceId, this.baseUrl, this.nextUrl)
ExtractorHelper.getMoreChannelItems(this.serviceId, this.baseUrl, this.nextPage)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getNextPageObserver());

View file

@ -1,8 +1,9 @@
package org.schabi.newpipe.player.playqueue;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.Log;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
@ -32,22 +33,25 @@ import io.reactivex.subjects.BehaviorSubject;
/**
* PlayQueue is responsible for keeping track of a list of streams and the index of
* the stream that should be currently playing.
*
* <p>
* This class contains basic manipulation of a playlist while also functions as a
* message bus, providing all listeners with new updates to the play queue.
*
* </p>
* <p>
* This class can be serialized for passing intents, but in order to start the
* message bus, it must be initialized.
* */
* </p>
*/
public abstract class PlayQueue implements Serializable {
private final String TAG = "PlayQueue@" + Integer.toHexString(hashCode());
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
private ArrayList<PlayQueueItem> backup;
private ArrayList<PlayQueueItem> streams;
@NonNull
private final AtomicInteger queueIndex;
private final ArrayList<PlayQueueItem> history;
@NonNull private final AtomicInteger queueIndex;
private transient BehaviorSubject<PlayQueueEvent> eventBroadcast;
private transient Flowable<PlayQueueEvent> broadcastReceiver;
@ -71,9 +75,10 @@ public abstract class PlayQueue implements Serializable {
/**
* Initializes the play queue message buses.
*
* <p>
* Also starts a self reporter for logging if debug mode is enabled.
* */
* </p>
*/
public void init() {
eventBroadcast = BehaviorSubject.create();
@ -81,15 +86,21 @@ public abstract class PlayQueue implements Serializable {
.observeOn(AndroidSchedulers.mainThread())
.startWith(new InitEvent());
if (DEBUG) broadcastReceiver.subscribe(getSelfReporter());
if (DEBUG) {
broadcastReceiver.subscribe(getSelfReporter());
}
}
/**
* Dispose the play queue by stopping all message buses.
* */
*/
public void dispose() {
if (eventBroadcast != null) eventBroadcast.onComplete();
if (reportingReactor != null) reportingReactor.cancel();
if (eventBroadcast != null) {
eventBroadcast.onComplete();
}
if (reportingReactor != null) {
reportingReactor.cancel();
}
eventBroadcast = null;
broadcastReceiver = null;
@ -99,15 +110,18 @@ public abstract class PlayQueue implements Serializable {
/**
* Checks if the queue is complete.
*
* <p>
* A queue is complete if it has loaded all items in an external playlist
* single stream or local queues are always complete.
* */
* </p>
*
* @return whether the queue is complete
*/
public abstract boolean isComplete();
/**
* Load partial queue in the background, does nothing if the queue is complete.
* */
*/
public abstract void fetch();
/*//////////////////////////////////////////////////////////////////////////
@ -115,93 +129,33 @@ public abstract class PlayQueue implements Serializable {
//////////////////////////////////////////////////////////////////////////*/
/**
* Returns the current index that should be played.
* */
* @return the current index that should be played
*/
public int getIndex() {
return queueIndex.get();
}
/**
* Returns the current item that should be played.
* */
public PlayQueueItem getItem() {
return getItem(getIndex());
}
/**
* Returns the item at the given index.
* May throw {@link IndexOutOfBoundsException}.
* */
public PlayQueueItem getItem(final int index) {
if (index < 0 || index >= streams.size() || streams.get(index) == null) return null;
return streams.get(index);
}
/**
* Returns the index of the given item using referential equality.
* May be null despite play queue contains identical item.
* */
public int indexOf(@NonNull final PlayQueueItem item) {
// referential equality, can't think of a better way to do this
// todo: better than this
return streams.indexOf(item);
}
/**
* Returns the current size of play queue.
* */
public int size() {
return streams.size();
}
/**
* Checks if the play queue is empty.
* */
public boolean isEmpty() {
return streams.isEmpty();
}
/**
* Determines if the current play queue is shuffled.
* */
public boolean isShuffled() {
return backup != null;
}
/**
* Returns an immutable view of the play queue.
* */
@NonNull
public List<PlayQueueItem> getStreams() {
return Collections.unmodifiableList(streams);
}
/**
* Returns the play queue's update broadcast.
* May be null if the play queue message bus is not initialized.
* */
@Nullable
public Flowable<PlayQueueEvent> getBroadcastReceiver() {
return broadcastReceiver;
}
/*//////////////////////////////////////////////////////////////////////////
// Write ops
//////////////////////////////////////////////////////////////////////////*/
/**
* Changes the current playing index to a new index.
*
* <p>
* This method is guarded using in a circular manner for index exceeding the play queue size.
*
* </p>
* <p>
* Will emit a {@link SelectEvent} if the index is not the current playing index.
* */
* </p>
*
* @param index the index to be set
*/
public synchronized void setIndex(final int index) {
final int oldIndex = getIndex();
int newIndex = index;
if (index < 0) newIndex = 0;
if (index >= streams.size()) newIndex = isComplete() ? index % streams.size() : streams.size() - 1;
if (index < 0) {
newIndex = 0;
}
if (index >= streams.size()) {
newIndex = isComplete() ? index % streams.size() : streams.size() - 1;
}
if (oldIndex != newIndex) history.add(streams.get(newIndex));
queueIndex.set(newIndex);
@ -209,10 +163,93 @@ public abstract class PlayQueue implements Serializable {
}
/**
* Changes the current playing index by an offset amount.
* @return the current item that should be played
*/
public PlayQueueItem getItem() {
return getItem(getIndex());
}
/**
* @param index the index of the item to return
* @return the item at the given index
* @throws IndexOutOfBoundsException
*/
public PlayQueueItem getItem(final int index) {
if (index < 0 || index >= streams.size() || streams.get(index) == null) {
return null;
}
return streams.get(index);
}
/**
* Returns the index of the given item using referential equality.
* May be null despite play queue contains identical item.
*
* @param item the item to find the index of
* @return the index of the given item
*/
public int indexOf(@NonNull final PlayQueueItem item) {
// referential equality, can't think of a better way to do this
// todo: better than this
return streams.indexOf(item);
}
/**
* @return the current size of play queue.
*/
public int size() {
return streams.size();
}
/**
* Checks if the play queue is empty.
*
* @return whether the play queue is empty
*/
public boolean isEmpty() {
return streams.isEmpty();
}
/**
* Determines if the current play queue is shuffled.
*
* @return whether the play queue is shuffled
*/
public boolean isShuffled() {
return backup != null;
}
/**
* @return an immutable view of the play queue
*/
@NonNull
public List<PlayQueueItem> getStreams() {
return Collections.unmodifiableList(streams);
}
/*//////////////////////////////////////////////////////////////////////////
// Write ops
//////////////////////////////////////////////////////////////////////////*/
/**
* Returns the play queue's update broadcast.
* May be null if the play queue message bus is not initialized.
*
* @return the play queue's update broadcast
*/
@Nullable
public Flowable<PlayQueueEvent> getBroadcastReceiver() {
return broadcastReceiver;
}
/**
* Changes the current playing index by an offset amount.
* <p>
* Will emit a {@link SelectEvent} if offset is non-zero.
* */
* </p>
*
* @param offset the offset relative to the current index
*/
public synchronized void offsetIndex(final int offset) {
setIndex(getIndex() + offset);
}
@ -221,19 +258,24 @@ public abstract class PlayQueue implements Serializable {
* Appends the given {@link PlayQueueItem}s to the current play queue.
*
* @see #append(List items)
* */
* @param items {@link PlayQueueItem}s to append
*/
public synchronized void append(@NonNull final PlayQueueItem... items) {
append(Arrays.asList(items));
}
/**
* Appends the given {@link PlayQueueItem}s to the current play queue.
*
* <p>
* If the play queue is shuffled, then append the items to the backup queue as is and
* append the shuffle items to the play queue.
*
* </p>
* <p>
* Will emit a {@link AppendEvent} on any given context.
* */
* </p>
*
* @param items {@link PlayQueueItem}s to append
*/
public synchronized void append(@NonNull final List<PlayQueueItem> items) {
final List<PlayQueueItem> itemList = new ArrayList<>(items);
@ -241,7 +283,8 @@ public abstract class PlayQueue implements Serializable {
backup.addAll(itemList);
Collections.shuffle(itemList);
}
if (!streams.isEmpty() && streams.get(streams.size() - 1).isAutoQueued() && !itemList.get(0).isAutoQueued()) {
if (!streams.isEmpty() && streams.get(streams.size() - 1).isAutoQueued()
&& !itemList.get(0).isAutoQueued()) {
streams.remove(streams.size() - 1);
}
streams.addAll(itemList);
@ -251,38 +294,38 @@ public abstract class PlayQueue implements Serializable {
/**
* Removes the item at the given index from the play queue.
*
* <p>
* The current playing index will decrement if it is greater than the index being removed.
* On cases where the current playing index exceeds the playlist range, it is set to 0.
*
* </p>
* <p>
* Will emit a {@link RemoveEvent} if the index is within the play queue index range.
* */
* </p>
*
* @param index the index of the item to remove
*/
public synchronized void remove(final int index) {
if (index >= streams.size() || index < 0) return;
if (index >= streams.size() || index < 0) {
return;
}
removeInternal(index);
broadcast(new RemoveEvent(index, getIndex()));
}
/**
* Report an exception for the item at the current index in order and the course of action:
* if the error can be skipped or the current item should be removed.
*
* Report an exception for the item at the current index in order and skip to the next one
* <p>
* This is done as a separate event as the underlying manager may have
* different implementation regarding exceptions.
* */
public synchronized void error(final boolean skippable) {
final int index = getIndex();
if (skippable) {
queueIndex.incrementAndGet();
if (streams.size() > queueIndex.get()) {
history.add(streams.get(queueIndex.get()));
}
} else {
removeInternal(index);
* </p>
*/
public synchronized void error() {
final int oldIndex = getIndex();
queueIndex.incrementAndGet();
if (streams.size() > queueIndex.get()) {
history.add(streams.get(queueIndex.get()));
}
broadcast(new ErrorEvent(index, getIndex(), skippable));
broadcast(new ErrorEvent(oldIndex, getIndex()));
}
private synchronized void removeInternal(final int removeIndex) {
@ -295,7 +338,7 @@ public abstract class PlayQueue implements Serializable {
} else if (currentIndex >= size) {
queueIndex.set(currentIndex % (size - 1));
} else if (currentIndex == removeIndex && currentIndex == size - 1){
} else if (currentIndex == removeIndex && currentIndex == size - 1) {
queueIndex.set(0);
}
@ -311,16 +354,24 @@ public abstract class PlayQueue implements Serializable {
/**
* Moves a queue item at the source index to the target index.
*
* <p>
* If the item being moved is the currently playing, then the current playing index is set
* to that of the target.
* If the moved item is not the currently playing and moves to an index <b>AFTER</b> the
* current playing index, then the current playing index is decremented.
* Vice versa if the an item after the currently playing is moved <b>BEFORE</b>.
* */
* </p>
*
* @param source the original index of the item
* @param target the new index of the item
*/
public synchronized void move(final int source, final int target) {
if (source < 0 || target < 0) return;
if (source >= streams.size() || target >= streams.size()) return;
if (source < 0 || target < 0) {
return;
}
if (source >= streams.size() || target >= streams.size()) {
return;
}
final int current = getIndex();
if (source == current) {
@ -339,11 +390,17 @@ public abstract class PlayQueue implements Serializable {
/**
* Sets the recovery record of the item at the index.
*
* <p>
* Broadcasts a recovery event.
* */
* </p>
*
* @param index index of the item
* @param position the recovery position
*/
public synchronized void setRecovery(final int index, final long position) {
if (index < 0 || index >= streams.size()) return;
if (index < 0 || index >= streams.size()) {
return;
}
streams.get(index).setRecoveryPosition(position);
broadcast(new RecoveryEvent(index, position));
@ -351,22 +408,27 @@ public abstract class PlayQueue implements Serializable {
/**
* Revoke the recovery record of the item at the index.
*
* <p>
* Broadcasts a recovery event.
* */
* </p>
*
* @param index index of the item
*/
public synchronized void unsetRecovery(final int index) {
setRecovery(index, PlayQueueItem.RECOVERY_UNSET);
}
/**
* Shuffles the current play queue.
*
* <p>
* This method first backs up the existing play queue and item being played.
* Then a newly shuffled play queue will be generated along with currently
* playing item placed at the beginning of the queue.
*
* </p>
* <p>
* Will emit a {@link ReorderEvent} in any context.
* */
* </p>
*/
public synchronized void shuffle() {
if (backup == null) {
backup = new ArrayList<>(streams);
@ -389,14 +451,18 @@ public abstract class PlayQueue implements Serializable {
/**
* Unshuffles the current play queue if a backup play queue exists.
*
* <p>
* This method undoes shuffling and index will be set to the previously playing item if found,
* otherwise, the index will reset to 0.
*
* </p>
* <p>
* Will emit a {@link ReorderEvent} if a backup exists.
* */
* </p>
*/
public synchronized void unshuffle() {
if (backup == null) return;
if (backup == null) {
return;
}
final int originIndex = getIndex();
final PlayQueueItem current = getItem();
@ -471,20 +537,23 @@ public abstract class PlayQueue implements Serializable {
private Subscriber<PlayQueueEvent> getSelfReporter() {
return new Subscriber<PlayQueueEvent>() {
@Override
public void onSubscribe(Subscription s) {
if (reportingReactor != null) reportingReactor.cancel();
public void onSubscribe(final Subscription s) {
if (reportingReactor != null) {
reportingReactor.cancel();
}
reportingReactor = s;
reportingReactor.request(1);
}
@Override
public void onNext(PlayQueueEvent event) {
Log.d(TAG, "Received broadcast: " + event.type().name() + ". Current index: " + getIndex() + ", play queue length: " + size() + ".");
public void onNext(final PlayQueueEvent event) {
Log.d(TAG, "Received broadcast: " + event.type().name() + ". "
+ "Current index: " + getIndex() + ", play queue length: " + size() + ".");
reportingReactor.request(1);
}
@Override
public void onError(Throwable t) {
public void onError(final Throwable t) {
Log.e(TAG, "Received broadcast error", t);
}

View file

@ -1,12 +1,13 @@
package org.schabi.newpipe.player.playqueue;
import android.content.Context;
import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.playqueue.events.AppendEvent;
import org.schabi.newpipe.player.playqueue.events.ErrorEvent;
@ -24,22 +25,26 @@ import io.reactivex.disposables.Disposable;
/**
* Created by Christian Schabesberger on 01.08.16.
*
* <p>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* InfoListAdapter.java is part of NewPipe.
*
* </p>
* <p>
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* </p>
* <p>
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* </p>
* <p>
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
* </p>
*/
public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
@ -55,14 +60,6 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
private Disposable playQueueReactor;
public class HFHolder extends RecyclerView.ViewHolder {
public HFHolder(View v) {
super(v);
view = v;
}
public View view;
}
public PlayQueueAdapter(final Context context, final PlayQueue playQueue) {
if (playQueue.getBroadcastReceiver() == null) {
throw new IllegalStateException("Play Queue has not been initialized.");
@ -77,18 +74,22 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
private Observer<PlayQueueEvent> getReactor() {
return new Observer<PlayQueueEvent>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
if (playQueueReactor != null) playQueueReactor.dispose();
public void onSubscribe(@NonNull final Disposable d) {
if (playQueueReactor != null) {
playQueueReactor.dispose();
}
playQueueReactor = d;
}
@Override
public void onNext(@NonNull PlayQueueEvent playQueueMessage) {
if (playQueueReactor != null) onPlayQueueChanged(playQueueMessage);
public void onNext(@NonNull final PlayQueueEvent playQueueMessage) {
if (playQueueReactor != null) {
onPlayQueueChanged(playQueueMessage);
}
}
@Override
public void onError(@NonNull Throwable e) {}
public void onError(@NonNull final Throwable e) { }
@Override
public void onComplete() {
@ -114,9 +115,6 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
break;
case ERROR:
final ErrorEvent errorEvent = (ErrorEvent) message;
if (!errorEvent.isSkippable()) {
notifyItemRemoved(errorEvent.getErrorIndex());
}
notifyItemChanged(errorEvent.getErrorIndex());
notifyItemChanged(errorEvent.getQueueIndex());
break;
@ -138,7 +136,9 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
}
public void dispose() {
if (playQueueReactor != null) playQueueReactor.dispose();
if (playQueueReactor != null) {
playQueueReactor.dispose();
}
playQueueReactor = null;
}
@ -150,7 +150,7 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
playQueueItemBuilder.setOnSelectedListener(null);
}
public void setFooter(View footer) {
public void setFooter(final View footer) {
this.footer = footer;
notifyItemChanged(playQueue.size());
}
@ -167,13 +167,15 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
@Override
public int getItemCount() {
int count = playQueue.getStreams().size();
if(footer != null && showFooter) count++;
if (footer != null && showFooter) {
count++;
}
return count;
}
@Override
public int getItemViewType(int position) {
if(footer != null && position == playQueue.getStreams().size() && showFooter) {
public int getItemViewType(final int position) {
if (footer != null && position == playQueue.getStreams().size() && showFooter) {
return FOOTER_VIEW_TYPE_ID;
}
@ -181,12 +183,13 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
switch(type) {
public RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent, final int type) {
switch (type) {
case FOOTER_VIEW_TYPE_ID:
return new HFHolder(footer);
case ITEM_VIEW_TYPE_ID:
return new PlayQueueItemHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.play_queue_item, parent, false));
return new PlayQueueItemHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.play_queue_item, parent, false));
default:
Log.e(TAG, "Attempting to create view holder with undefined type: " + type);
return new FallbackViewHolder(new View(parent.getContext()));
@ -194,19 +197,30 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(holder instanceof PlayQueueItemHolder) {
public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
if (holder instanceof PlayQueueItemHolder) {
final PlayQueueItemHolder itemHolder = (PlayQueueItemHolder) holder;
// Build the list item
playQueueItemBuilder.buildStreamInfoItem(itemHolder, playQueue.getStreams().get(position));
playQueueItemBuilder
.buildStreamInfoItem(itemHolder, playQueue.getStreams().get(position));
// Check if the current item should be selected/highlighted
final boolean isSelected = playQueue.getIndex() == position;
itemHolder.itemSelected.setVisibility(isSelected ? View.VISIBLE : View.INVISIBLE);
itemHolder.itemView.setSelected(isSelected);
} else if(holder instanceof HFHolder && position == playQueue.getStreams().size() && footer != null && showFooter) {
} else if (holder instanceof HFHolder && position == playQueue.getStreams().size()
&& footer != null && showFooter) {
((HFHolder) holder).view = footer;
}
}
public class HFHolder extends RecyclerView.ViewHolder {
public View view;
public HFHolder(final View v) {
super(v);
view = v;
}
}
}

View file

@ -14,27 +14,34 @@ import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers;
public class PlayQueueItem implements Serializable {
public final static long RECOVERY_UNSET = Long.MIN_VALUE;
private final static String EMPTY_STRING = "";
public static final long RECOVERY_UNSET = Long.MIN_VALUE;
private static final String EMPTY_STRING = "";
@NonNull final private String title;
@NonNull final private String url;
final private int serviceId;
final private long duration;
@NonNull final private String thumbnailUrl;
@NonNull final private String uploader;
@NonNull final private StreamType streamType;
@NonNull
private final String title;
@NonNull
private final String url;
private final int serviceId;
private final long duration;
@NonNull
private final String thumbnailUrl;
@NonNull
private final String uploader;
@NonNull
private final StreamType streamType;
private boolean isAutoQueued;
private long recoveryPosition;
private Throwable error;
PlayQueueItem(@NonNull final StreamInfo info) {
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
info.getThumbnailUrl(), info.getUploaderName(), info.getStreamType());
if (info.getStartPosition() > 0)
if (info.getStartPosition() > 0) {
setRecoveryPosition(info.getStartPosition() * 1000);
}
}
PlayQueueItem(@NonNull final StreamInfoItem item) {
@ -94,6 +101,10 @@ public class PlayQueueItem implements Serializable {
return recoveryPosition;
}
/*package-private*/ void setRecoveryPosition(final long recoveryPosition) {
this.recoveryPosition = recoveryPosition;
}
@Nullable
public Throwable getError() {
return error;
@ -110,15 +121,11 @@ public class PlayQueueItem implements Serializable {
return isAutoQueued;
}
public void setAutoQueued(boolean autoQueued) {
isAutoQueued = autoQueued;
}
////////////////////////////////////////////////////////////////////////////
// Item States, keep external access out
////////////////////////////////////////////////////////////////////////////
/*package-private*/ void setRecoveryPosition(final long recoveryPosition) {
this.recoveryPosition = recoveryPosition;
public void setAutoQueued(final boolean autoQueued) {
isAutoQueued = autoQueued;
}
}

View file

@ -12,25 +12,20 @@ import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
public class PlayQueueItemBuilder {
private static final String TAG = PlayQueueItemBuilder.class.toString();
public interface OnSelectedListener {
void selected(PlayQueueItem item, View view);
void held(PlayQueueItem item, View view);
void onStartDrag(PlayQueueItemHolder viewHolder);
}
private OnSelectedListener onItemClickListener;
public PlayQueueItemBuilder(final Context context) {}
public PlayQueueItemBuilder(final Context context) {
}
public void setOnSelectedListener(OnSelectedListener listener) {
public void setOnSelectedListener(final OnSelectedListener listener) {
this.onItemClickListener = listener;
}
public void buildStreamInfoItem(final PlayQueueItemHolder holder, final PlayQueueItem item) {
if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle());
if (!TextUtils.isEmpty(item.getTitle())) {
holder.itemVideoTitleView.setText(item.getTitle());
}
holder.itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.getUploader(),
NewPipe.getNameOfService(item.getServiceId())));
@ -71,4 +66,12 @@ public class PlayQueueItemBuilder {
return false;
};
}
public interface OnSelectedListener {
void selected(PlayQueueItem item, View view);
void held(PlayQueueItem item, View view);
void onStartDrag(PlayQueueItemHolder viewHolder);
}
}

View file

@ -1,10 +1,11 @@
package org.schabi.newpipe.player.playqueue;
import androidx.recyclerview.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R;
/**
@ -12,29 +13,37 @@ import org.schabi.newpipe.R;
* <p>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* StreamInfoItemHolder.java is part of NewPipe.
* </p>
* <p>
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* </p>
* <p>
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* </p>
* <p>
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
* </p>
*/
public class PlayQueueItemHolder extends RecyclerView.ViewHolder {
public final TextView itemVideoTitleView;
public final TextView itemDurationView;
final TextView itemAdditionalDetailsView;
public final TextView itemVideoTitleView, itemDurationView, itemAdditionalDetailsView;
public final ImageView itemSelected, itemThumbnailView, itemHandle;
final ImageView itemSelected;
public final ImageView itemThumbnailView;
final ImageView itemHandle;
public final View itemRoot;
public PlayQueueItemHolder(View v) {
PlayQueueItemHolder(final View v) {
super(v);
itemRoot = v.findViewById(R.id.itemRoot);
itemVideoTitleView = v.findViewById(R.id.itemVideoTitleView);

View file

@ -1,7 +1,7 @@
package org.schabi.newpipe.player.playqueue;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleCallback {
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10;
@ -11,14 +11,14 @@ public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleC
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.RIGHT);
}
public abstract void onMove(final int sourceIndex, final int targetIndex);
public abstract void onMove(int sourceIndex, int targetIndex);
public abstract void onSwiped(int index);
@Override
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
int viewSizeOutOfBounds, int totalSize,
long msSinceStartScroll) {
public int interpolateOutOfBoundsScroll(final RecyclerView recyclerView, final int viewSize,
final int viewSizeOutOfBounds, final int totalSize,
final long msSinceStartScroll) {
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize,
viewSizeOutOfBounds, totalSize, msSinceStartScroll);
final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
@ -27,8 +27,8 @@ public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleC
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
RecyclerView.ViewHolder target) {
public boolean onMove(final RecyclerView recyclerView, final RecyclerView.ViewHolder source,
final RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType()) {
return false;
}
@ -50,7 +50,7 @@ public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleC
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int swipeDir) {
onSwiped(viewHolder.getAdapterPosition());
}
}

View file

@ -1,5 +1,6 @@
package org.schabi.newpipe.player.playqueue;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
@ -16,15 +17,15 @@ public final class PlaylistPlayQueue extends AbstractInfoPlayQueue<PlaylistInfo,
}
public PlaylistPlayQueue(final PlaylistInfo info) {
this(info.getServiceId(), info.getUrl(), info.getNextPageUrl(), info.getRelatedItems(), 0);
this(info.getServiceId(), info.getUrl(), info.getNextPage(), info.getRelatedItems(), 0);
}
public PlaylistPlayQueue(final int serviceId,
final String url,
final String nextPageUrl,
final Page nextPage,
final List<StreamInfoItem> streams,
final int index) {
super(serviceId, url, nextPageUrl, streams, index);
super(serviceId, url, nextPage, streams, index);
}
@Override
@ -40,7 +41,7 @@ public final class PlaylistPlayQueue extends AbstractInfoPlayQueue<PlaylistInfo,
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getHeadListObserver());
} else {
ExtractorHelper.getMorePlaylistItems(this.serviceId, this.baseUrl, this.nextUrl)
ExtractorHelper.getMorePlaylistItems(this.serviceId, this.baseUrl, this.nextPage)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getNextPageObserver());

View file

@ -25,7 +25,7 @@ public final class SinglePlayQueue extends PlayQueue {
super(index, playQueueItemsOf(items));
}
private static List<PlayQueueItem> playQueueItemsOf(List<StreamInfoItem> items) {
private static List<PlayQueueItem> playQueueItemsOf(final List<StreamInfoItem> items) {
List<PlayQueueItem> playQueueItems = new ArrayList<>(items.size());
for (final StreamInfoItem item : items) {
playQueueItems.add(new PlayQueueItem(item));
@ -39,5 +39,6 @@ public final class SinglePlayQueue extends PlayQueue {
}
@Override
public void fetch() {}
public void fetch() {
}
}

View file

@ -1,18 +1,17 @@
package org.schabi.newpipe.player.playqueue.events;
public class AppendEvent implements PlayQueueEvent {
final private int amount;
private final int amount;
public AppendEvent(final int amount) {
this.amount = amount;
}
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.APPEND;
}
public AppendEvent(final int amount) {
this.amount = amount;
}
public int getAmount() {
return amount;
}

View file

@ -1,22 +1,19 @@
package org.schabi.newpipe.player.playqueue.events;
public class ErrorEvent implements PlayQueueEvent {
final private int errorIndex;
final private int queueIndex;
final private boolean skippable;
private final int errorIndex;
private final int queueIndex;
public ErrorEvent(final int errorIndex, final int queueIndex) {
this.errorIndex = errorIndex;
this.queueIndex = queueIndex;
}
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.ERROR;
}
public ErrorEvent(final int errorIndex, final int queueIndex, final boolean skippable) {
this.errorIndex = errorIndex;
this.queueIndex = queueIndex;
this.skippable = skippable;
}
public int getErrorIndex() {
return errorIndex;
}
@ -24,8 +21,4 @@ public class ErrorEvent implements PlayQueueEvent {
public int getQueueIndex() {
return queueIndex;
}
public boolean isSkippable() {
return skippable;
}
}

View file

@ -1,19 +1,19 @@
package org.schabi.newpipe.player.playqueue.events;
public class MoveEvent implements PlayQueueEvent {
final private int fromIndex;
final private int toIndex;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.MOVE;
}
private final int fromIndex;
private final int toIndex;
public MoveEvent(final int oldIndex, final int newIndex) {
this.fromIndex = oldIndex;
this.toIndex = newIndex;
}
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.MOVE;
}
public int getFromIndex() {
return fromIndex;
}

View file

@ -1,20 +1,19 @@
package org.schabi.newpipe.player.playqueue.events;
public class RecoveryEvent implements PlayQueueEvent {
final private int index;
final private long position;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.RECOVERY;
}
private final int index;
private final long position;
public RecoveryEvent(final int index, final long position) {
this.index = index;
this.position = position;
}
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.RECOVERY;
}
public int getIndex() {
return index;
}

View file

@ -1,20 +1,19 @@
package org.schabi.newpipe.player.playqueue.events;
public class RemoveEvent implements PlayQueueEvent {
final private int removeIndex;
final private int queueIndex;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.REMOVE;
}
private final int removeIndex;
private final int queueIndex;
public RemoveEvent(final int removeIndex, final int queueIndex) {
this.removeIndex = removeIndex;
this.queueIndex = queueIndex;
}
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.REMOVE;
}
public int getQueueIndex() {
return queueIndex;
}

View file

@ -4,16 +4,16 @@ public class ReorderEvent implements PlayQueueEvent {
private final int fromSelectedIndex;
private final int toSelectedIndex;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.REORDER;
}
public ReorderEvent(final int fromSelectedIndex, final int toSelectedIndex) {
this.fromSelectedIndex = fromSelectedIndex;
this.toSelectedIndex = toSelectedIndex;
}
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.REORDER;
}
public int getFromSelectedIndex() {
return fromSelectedIndex;
}

View file

@ -1,20 +1,19 @@
package org.schabi.newpipe.player.playqueue.events;
public class SelectEvent implements PlayQueueEvent {
final private int oldIndex;
final private int newIndex;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.SELECT;
}
private final int oldIndex;
private final int newIndex;
public SelectEvent(final int oldIndex, final int newIndex) {
this.oldIndex = oldIndex;
this.newIndex = newIndex;
}
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.SELECT;
}
public int getOldIndex() {
return oldIndex;
}

View file

@ -1,6 +1,7 @@
package org.schabi.newpipe.player.resolver;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -14,9 +15,10 @@ import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.util.ListHelper;
public class AudioPlaybackResolver implements PlaybackResolver {
@NonNull private final Context context;
@NonNull private final PlayerDataSource dataSource;
@NonNull
private final Context context;
@NonNull
private final PlayerDataSource dataSource;
public AudioPlaybackResolver(@NonNull final Context context,
@NonNull final PlayerDataSource dataSource) {
@ -26,12 +28,16 @@ public class AudioPlaybackResolver implements PlaybackResolver {
@Override
@Nullable
public MediaSource resolve(@NonNull StreamInfo info) {
public MediaSource resolve(@NonNull final StreamInfo info) {
final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
if (liveSource != null) return liveSource;
if (liveSource != null) {
return liveSource;
}
final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams());
if (index < 0 || index >= info.getAudioStreams().size()) return null;
if (index < 0 || index >= info.getAudioStreams().size()) {
return null;
}
final AudioStream audio = info.getAudioStreams().get(index);
final MediaSourceTag tag = new MediaSourceTag(info);

View file

@ -11,9 +11,11 @@ import java.util.Collections;
import java.util.List;
public class MediaSourceTag implements Serializable {
@NonNull private final StreamInfo metadata;
@NonNull
private final StreamInfo metadata;
@NonNull private final List<VideoStream> sortedAvailableVideoStreams;
@NonNull
private final List<VideoStream> sortedAvailableVideoStreams;
private final int selectedVideoStreamIndex;
public MediaSourceTag(@NonNull final StreamInfo metadata,
@ -44,8 +46,8 @@ public class MediaSourceTag implements Serializable {
@Nullable
public VideoStream getSelectedVideoStream() {
return selectedVideoStreamIndex < 0 ||
selectedVideoStreamIndex >= sortedAvailableVideoStreams.size() ? null :
sortedAvailableVideoStreams.get(selectedVideoStreamIndex);
return selectedVideoStreamIndex < 0
|| selectedVideoStreamIndex >= sortedAvailableVideoStreams.size()
? null : sortedAvailableVideoStreams.get(selectedVideoStreamIndex);
}
}

View file

@ -1,9 +1,10 @@
package org.schabi.newpipe.player.resolver;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.MediaSource;
@ -61,8 +62,8 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
@NonNull final String overrideExtension,
@NonNull final MediaSourceTag metadata) {
final Uri uri = Uri.parse(sourceUrl);
@C.ContentType final int type = TextUtils.isEmpty(overrideExtension) ?
Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
@C.ContentType final int type = TextUtils.isEmpty(overrideExtension)
? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
switch (type) {
case C.TYPE_SS:

View file

@ -4,5 +4,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public interface Resolver<Source, Product> {
@Nullable Product resolve(@NonNull Source source);
@Nullable
Product resolve(@NonNull Source source);
}

View file

@ -2,6 +2,7 @@ package org.schabi.newpipe.player.resolver;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -10,9 +11,9 @@ import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.helper.PlayerHelper;
@ -25,18 +26,15 @@ import static com.google.android.exoplayer2.C.SELECTION_FLAG_AUTOSELECT;
import static com.google.android.exoplayer2.C.TIME_UNSET;
public class VideoPlaybackResolver implements PlaybackResolver {
@NonNull
private final Context context;
@NonNull
private final PlayerDataSource dataSource;
@NonNull
private final QualityResolver qualityResolver;
public interface QualityResolver {
int getDefaultResolutionIndex(final List<VideoStream> sortedVideos);
int getOverrideResolutionIndex(final List<VideoStream> sortedVideos,
final String playbackQuality);
}
@NonNull private final Context context;
@NonNull private final PlayerDataSource dataSource;
@NonNull private final QualityResolver qualityResolver;
@Nullable private String playbackQuality;
@Nullable
private String playbackQuality;
public VideoPlaybackResolver(@NonNull final Context context,
@NonNull final PlayerDataSource dataSource,
@ -48,9 +46,11 @@ public class VideoPlaybackResolver implements PlaybackResolver {
@Override
@Nullable
public MediaSource resolve(@NonNull StreamInfo info) {
public MediaSource resolve(@NonNull final StreamInfo info) {
final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
if (liveSource != null) return liveSource;
if (liveSource != null) {
return liveSource;
}
List<MediaSource> mediaSources = new ArrayList<>();
@ -81,7 +81,7 @@ public class VideoPlaybackResolver implements PlaybackResolver {
ListHelper.getDefaultAudioFormat(context, audioStreams));
// Use the audio stream if there is no video stream, or
// Merge with audio stream in case if video does not contain audio
if (audio != null && ((video != null && video.isVideoOnly) || video == null)) {
if (audio != null && (video == null || video.isVideoOnly)) {
final MediaSource audioSource = buildMediaSource(dataSource, audio.getUrl(),
PlayerHelper.cacheKeyOf(info, audio),
MediaFormat.getSuffixById(audio.getFormatId()), tag);
@ -89,17 +89,22 @@ public class VideoPlaybackResolver implements PlaybackResolver {
}
// If there is no audio or video sources, then this media source cannot be played back
if (mediaSources.isEmpty()) return null;
if (mediaSources.isEmpty()) {
return null;
}
// Below are auxiliary media sources
// Create subtitle sources
if(info.getSubtitles() != null) {
if (info.getSubtitles() != null) {
for (final SubtitlesStream subtitle : info.getSubtitles()) {
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
if (mimeType == null) continue;
if (mimeType == null) {
continue;
}
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
SELECTION_FLAG_AUTOSELECT,
PlayerHelper.captionLanguageOf(context, subtitle));
final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
.createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
mediaSources.add(textSource);
@ -119,7 +124,13 @@ public class VideoPlaybackResolver implements PlaybackResolver {
return playbackQuality;
}
public void setPlaybackQuality(@Nullable String playbackQuality) {
public void setPlaybackQuality(@Nullable final String playbackQuality) {
this.playbackQuality = playbackQuality;
}
public interface QualityResolver {
int getDefaultResolutionIndex(List<VideoStream> sortedVideos);
int getOverrideResolutionIndex(List<VideoStream> sortedVideos, String playbackQuality);
}
}