mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-06-18 11:20:00 +00:00
79e98db3bd
Apply the requested changes, use ShareUtils.shareText to share an stream in the play queue and optimize imports for Java files, using Android Studio functionality. Apply the requested changes and do little improvements Apply the requested changes, use ShareUtils.shareText to share an stream in the play queue and optimize imports for Java files, using Android Studio functionality.
4075 lines
154 KiB
Java
4075 lines
154 KiB
Java
package org.schabi.newpipe.player;
|
|
|
|
import android.animation.Animator;
|
|
import android.animation.AnimatorListenerAdapter;
|
|
import android.animation.ObjectAnimator;
|
|
import android.animation.PropertyValuesHolder;
|
|
import android.animation.ValueAnimator;
|
|
import android.annotation.SuppressLint;
|
|
import android.content.BroadcastReceiver;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.IntentFilter;
|
|
import android.content.SharedPreferences;
|
|
import android.content.res.Resources;
|
|
import android.database.ContentObserver;
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.BitmapFactory;
|
|
import android.graphics.Color;
|
|
import android.graphics.PorterDuff;
|
|
import android.graphics.PorterDuffColorFilter;
|
|
import android.media.AudioManager;
|
|
import android.net.Uri;
|
|
import android.os.Build;
|
|
import android.os.Handler;
|
|
import android.provider.Settings;
|
|
import android.util.DisplayMetrics;
|
|
import android.util.Log;
|
|
import android.util.TypedValue;
|
|
import android.view.GestureDetector;
|
|
import android.view.KeyEvent;
|
|
import android.view.LayoutInflater;
|
|
import android.view.Menu;
|
|
import android.view.MenuItem;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.WindowManager;
|
|
import android.view.animation.AnticipateInterpolator;
|
|
import android.widget.FrameLayout;
|
|
import android.widget.ImageButton;
|
|
import android.widget.ImageView;
|
|
import android.widget.LinearLayout;
|
|
import android.widget.PopupMenu;
|
|
import android.widget.ProgressBar;
|
|
import android.widget.RelativeLayout;
|
|
import android.widget.SeekBar;
|
|
import android.widget.TextView;
|
|
import android.widget.Toast;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.appcompat.app.AppCompatActivity;
|
|
import androidx.appcompat.content.res.AppCompatResources;
|
|
import androidx.core.content.ContextCompat;
|
|
import androidx.core.view.DisplayCutoutCompat;
|
|
import androidx.core.view.ViewCompat;
|
|
import androidx.preference.PreferenceManager;
|
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
|
import androidx.recyclerview.widget.RecyclerView;
|
|
|
|
import com.google.android.exoplayer2.C;
|
|
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
|
import com.google.android.exoplayer2.PlaybackParameters;
|
|
import com.google.android.exoplayer2.RenderersFactory;
|
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
|
import com.google.android.exoplayer2.Timeline;
|
|
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
|
import com.google.android.exoplayer2.source.MediaSource;
|
|
import com.google.android.exoplayer2.source.TrackGroup;
|
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
|
import com.google.android.exoplayer2.text.CaptionStyleCompat;
|
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
|
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
|
import com.google.android.exoplayer2.ui.SubtitleView;
|
|
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
|
import com.google.android.exoplayer2.video.VideoListener;
|
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
|
import com.nostra13.universalimageloader.core.assist.FailReason;
|
|
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
|
|
|
import org.schabi.newpipe.DownloaderImpl;
|
|
import org.schabi.newpipe.MainActivity;
|
|
import org.schabi.newpipe.R;
|
|
import org.schabi.newpipe.databinding.PlayerBinding;
|
|
import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding;
|
|
import org.schabi.newpipe.extractor.MediaFormat;
|
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|
import org.schabi.newpipe.extractor.stream.StreamSegment;
|
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
|
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
|
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
|
import org.schabi.newpipe.info_list.StreamSegmentAdapter;
|
|
import org.schabi.newpipe.ktx.AnimationType;
|
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
|
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
|
import org.schabi.newpipe.player.event.PlayerEventListener;
|
|
import org.schabi.newpipe.player.event.PlayerGestureListener;
|
|
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
|
import org.schabi.newpipe.player.helper.AudioReactor;
|
|
import org.schabi.newpipe.player.helper.LoadController;
|
|
import org.schabi.newpipe.player.helper.MediaSessionManager;
|
|
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
|
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
|
import org.schabi.newpipe.player.playback.CustomTrackSelector;
|
|
import org.schabi.newpipe.player.playback.MediaSourceManager;
|
|
import org.schabi.newpipe.player.playback.PlaybackListener;
|
|
import org.schabi.newpipe.player.playback.PlayerMediaSession;
|
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
|
import org.schabi.newpipe.player.playqueue.PlayQueueAdapter;
|
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
|
import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder;
|
|
import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder;
|
|
import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
|
|
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
|
|
import org.schabi.newpipe.player.resolver.MediaSourceTag;
|
|
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
|
import org.schabi.newpipe.util.DeviceUtils;
|
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
|
import org.schabi.newpipe.util.KoreUtil;
|
|
import org.schabi.newpipe.util.ListHelper;
|
|
import org.schabi.newpipe.util.NavigationHelper;
|
|
import org.schabi.newpipe.util.SerializedCache;
|
|
import org.schabi.newpipe.util.ShareUtils;
|
|
import org.schabi.newpipe.views.ExpandableSurfaceView;
|
|
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
|
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|
import io.reactivex.rxjava3.core.Observable;
|
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
import io.reactivex.rxjava3.disposables.Disposable;
|
|
import io.reactivex.rxjava3.disposables.SerialDisposable;
|
|
|
|
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AD_INSERTION;
|
|
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL;
|
|
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION;
|
|
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK;
|
|
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT;
|
|
import static com.google.android.exoplayer2.Player.DiscontinuityReason;
|
|
import static com.google.android.exoplayer2.Player.EventListener;
|
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
|
import static com.google.android.exoplayer2.Player.RepeatMode;
|
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
|
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
|
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
|
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
|
|
import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE;
|
|
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD;
|
|
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND;
|
|
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT;
|
|
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE;
|
|
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS;
|
|
import static org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION;
|
|
import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT;
|
|
import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE;
|
|
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
|
|
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;
|
|
import static org.schabi.newpipe.player.helper.PlayerHelper.buildCloseOverlayLayoutParams;
|
|
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
|
import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimizeOnExitAction;
|
|
import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimumVideoHeight;
|
|
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
|
|
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
|
|
import static org.schabi.newpipe.player.helper.PlayerHelper.isPlaybackResumeEnabled;
|
|
import static org.schabi.newpipe.player.helper.PlayerHelper.nextRepeatMode;
|
|
import static org.schabi.newpipe.player.helper.PlayerHelper.nextResizeModeAndSaveToPrefs;
|
|
import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlaybackParametersFromPrefs;
|
|
import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlayerTypeFromIntent;
|
|
import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePopupLayoutParamsFromPrefs;
|
|
import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences;
|
|
import static org.schabi.newpipe.player.helper.PlayerHelper.savePlaybackParametersToPrefs;
|
|
import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex;
|
|
import static org.schabi.newpipe.util.ListHelper.getResolutionIndex;
|
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
|
import static org.schabi.newpipe.util.Localization.containsCaseInsensitive;
|
|
|
|
public final class Player implements
|
|
EventListener,
|
|
PlaybackListener,
|
|
ImageLoadingListener,
|
|
VideoListener,
|
|
SeekBar.OnSeekBarChangeListener,
|
|
View.OnClickListener,
|
|
PopupMenu.OnMenuItemClickListener,
|
|
PopupMenu.OnDismissListener,
|
|
View.OnLongClickListener {
|
|
public static final boolean DEBUG = MainActivity.DEBUG;
|
|
public static final String TAG = Player.class.getSimpleName();
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// States
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
public static final int STATE_PREFLIGHT = -1;
|
|
public static final int STATE_BLOCKED = 123;
|
|
public static final int STATE_PLAYING = 124;
|
|
public static final int STATE_BUFFERING = 125;
|
|
public static final int STATE_PAUSED = 126;
|
|
public static final int STATE_PAUSED_SEEK = 127;
|
|
public static final int STATE_COMPLETED = 128;
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Intent
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
public static final String REPEAT_MODE = "repeat_mode";
|
|
public static final String PLAYBACK_QUALITY = "playback_quality";
|
|
public static final String PLAY_QUEUE_KEY = "play_queue_key";
|
|
public static final String APPEND_ONLY = "append_only";
|
|
public static final String RESUME_PLAYBACK = "resume_playback";
|
|
public static final String PLAY_WHEN_READY = "play_when_ready";
|
|
public static final String SELECT_ON_APPEND = "select_on_append";
|
|
public static final String PLAYER_TYPE = "player_type";
|
|
public static final String IS_MUTED = "is_muted";
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Time constants
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
public static final int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds
|
|
public static final int PROGRESS_LOOP_INTERVAL_MILLIS = 500; // 500 millis
|
|
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
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Other constants
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
private static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f};
|
|
|
|
private static final int RENDERER_UNAVAILABLE = -1;
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Playback
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
private PlayQueue playQueue;
|
|
private PlayQueueAdapter playQueueAdapter;
|
|
private StreamSegmentAdapter segmentAdapter;
|
|
|
|
@Nullable private MediaSourceManager playQueueManager;
|
|
|
|
@Nullable private PlayQueueItem currentItem;
|
|
@Nullable private MediaSourceTag currentMetadata;
|
|
@Nullable private Bitmap currentThumbnail;
|
|
|
|
@Nullable private Toast errorToast;
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Player
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
private SimpleExoPlayer simpleExoPlayer;
|
|
private AudioReactor audioReactor;
|
|
private MediaSessionManager mediaSessionManager;
|
|
|
|
@NonNull private final CustomTrackSelector trackSelector;
|
|
@NonNull private final LoadController loadController;
|
|
@NonNull private final RenderersFactory renderFactory;
|
|
|
|
@NonNull private final VideoPlaybackResolver videoResolver;
|
|
@NonNull private final AudioPlaybackResolver audioResolver;
|
|
|
|
private final MainPlayer service; //TODO try to remove and replace everything with context
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Player states
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
private PlayerType playerType = PlayerType.VIDEO;
|
|
private int currentState = STATE_PREFLIGHT;
|
|
|
|
// audio only mode does not mean that player type is background, but that the player was
|
|
// minimized to background but will resume automatically to the original player type
|
|
private boolean isAudioOnly = false;
|
|
private boolean isPrepared = false;
|
|
private boolean wasPlaying = false;
|
|
private boolean isFullscreen = false;
|
|
private boolean isVerticalVideo = false;
|
|
private boolean fragmentIsVisible = false;
|
|
|
|
private List<VideoStream> availableStreams;
|
|
private int selectedStreamIndex;
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Views
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
private PlayerBinding binding;
|
|
|
|
private ValueAnimator controlViewAnimator;
|
|
private final Handler controlsVisibilityHandler = new Handler();
|
|
|
|
// fullscreen player
|
|
private boolean isQueueVisible = false;
|
|
private boolean areSegmentsVisible = false;
|
|
private ItemTouchHelper itemTouchHelper;
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Popup menus ("popup" means that they pop up, not that they belong to the popup player)
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
private static final int POPUP_MENU_ID_QUALITY = 69;
|
|
private static final int POPUP_MENU_ID_PLAYBACK_SPEED = 79;
|
|
private static final int POPUP_MENU_ID_CAPTION = 89;
|
|
|
|
private boolean isSomePopupMenuVisible = false;
|
|
private PopupMenu qualityPopupMenu;
|
|
private PopupMenu playbackSpeedPopupMenu;
|
|
private PopupMenu captionPopupMenu;
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Popup player
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
private PlayerPopupCloseOverlayBinding closeOverlayBinding;
|
|
|
|
private boolean isPopupClosing = false;
|
|
|
|
private float screenWidth;
|
|
private float screenHeight;
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Popup player window manager
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
public static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
|
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
|
|
public static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS
|
|
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
|
|
|
|
@Nullable private WindowManager.LayoutParams popupLayoutParams; // null if player is not popup
|
|
@Nullable private final WindowManager windowManager;
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Gestures
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
private static final float MAX_GESTURE_LENGTH = 0.75f;
|
|
|
|
private int maxGestureLength; // scaled
|
|
private GestureDetector gestureDetector;
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Listeners and disposables
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
private BroadcastReceiver broadcastReceiver;
|
|
private IntentFilter intentFilter;
|
|
private PlayerServiceEventListener fragmentListener;
|
|
private PlayerEventListener activityListener;
|
|
private ContentObserver settingsContentObserver;
|
|
|
|
@NonNull private final SerialDisposable progressUpdateDisposable = new SerialDisposable();
|
|
@NonNull private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable();
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Utils
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
@NonNull private final Context context;
|
|
@NonNull private final SharedPreferences prefs;
|
|
@NonNull private final HistoryRecordManager recordManager;
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Constructor
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
public Player(@NonNull final MainPlayer service) {
|
|
this.service = service;
|
|
context = service;
|
|
prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
recordManager = new HistoryRecordManager(context);
|
|
|
|
setupBroadcastReceiver();
|
|
|
|
trackSelector = new CustomTrackSelector(context, PlayerHelper.getQualitySelector());
|
|
final PlayerDataSource dataSource = new PlayerDataSource(context, DownloaderImpl.USER_AGENT,
|
|
new DefaultBandwidthMeter.Builder(context).build());
|
|
loadController = new LoadController();
|
|
renderFactory = new DefaultRenderersFactory(context);
|
|
|
|
videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver());
|
|
audioResolver = new AudioPlaybackResolver(context, dataSource);
|
|
|
|
windowManager = ContextCompat.getSystemService(context, WindowManager.class);
|
|
}
|
|
|
|
private VideoPlaybackResolver.QualityResolver getQualityResolver() {
|
|
return new VideoPlaybackResolver.QualityResolver() {
|
|
@Override
|
|
public int getDefaultResolutionIndex(final List<VideoStream> sortedVideos) {
|
|
return videoPlayerSelected()
|
|
? ListHelper.getDefaultResolutionIndex(context, sortedVideos)
|
|
: ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos);
|
|
}
|
|
|
|
@Override
|
|
public int getOverrideResolutionIndex(final List<VideoStream> sortedVideos,
|
|
final String playbackQuality) {
|
|
return videoPlayerSelected()
|
|
? getResolutionIndex(context, sortedVideos, playbackQuality)
|
|
: getPopupResolutionIndex(context, sortedVideos, playbackQuality);
|
|
}
|
|
};
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Setup and initialization
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
public void setupFromView(@NonNull final PlayerBinding playerBinding) {
|
|
initViews(playerBinding);
|
|
if (exoPlayerIsNull()) {
|
|
initPlayer(true);
|
|
}
|
|
initListeners();
|
|
}
|
|
|
|
private void initViews(@NonNull final PlayerBinding playerBinding) {
|
|
binding = playerBinding;
|
|
setupSubtitleView();
|
|
|
|
binding.resizeTextView
|
|
.setText(PlayerHelper.resizeTypeOf(context, binding.surfaceView.getResizeMode()));
|
|
|
|
binding.playbackSeekBar.getThumb()
|
|
.setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN));
|
|
binding.playbackSeekBar.getProgressDrawable()
|
|
.setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY));
|
|
|
|
qualityPopupMenu = new PopupMenu(context, binding.qualityTextView);
|
|
playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed);
|
|
captionPopupMenu = new PopupMenu(context, binding.captionTextView);
|
|
|
|
binding.progressBarLoadingPanel.getIndeterminateDrawable()
|
|
.setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY));
|
|
|
|
binding.titleTextView.setSelected(true);
|
|
binding.channelTextView.setSelected(true);
|
|
|
|
// Prevent hiding of bottom sheet via swipe inside queue
|
|
binding.itemsList.setNestedScrollingEnabled(false);
|
|
}
|
|
|
|
private void initPlayer(final boolean playOnReady) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "initPlayer() called with: playOnReady = [" + playOnReady + "]");
|
|
}
|
|
|
|
simpleExoPlayer = new SimpleExoPlayer.Builder(context, renderFactory)
|
|
.setTrackSelector(trackSelector)
|
|
.setLoadControl(loadController)
|
|
.build();
|
|
simpleExoPlayer.addListener(this);
|
|
simpleExoPlayer.setPlayWhenReady(playOnReady);
|
|
simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context));
|
|
simpleExoPlayer.setWakeMode(C.WAKE_MODE_NETWORK);
|
|
simpleExoPlayer.setHandleAudioBecomingNoisy(true);
|
|
|
|
audioReactor = new AudioReactor(context, simpleExoPlayer);
|
|
mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer,
|
|
new PlayerMediaSession(this));
|
|
|
|
registerBroadcastReceiver();
|
|
|
|
// Setup video view
|
|
simpleExoPlayer.setVideoSurfaceView(binding.surfaceView);
|
|
simpleExoPlayer.addVideoListener(this);
|
|
|
|
// Setup subtitle view
|
|
simpleExoPlayer.addTextOutput(binding.subtitleView);
|
|
|
|
// Setup audio session with onboard equalizer
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
trackSelector.setParameters(trackSelector.buildUponParameters()
|
|
.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context)));
|
|
}
|
|
}
|
|
|
|
private void initListeners() {
|
|
binding.playbackSeekBar.setOnSeekBarChangeListener(this);
|
|
binding.playbackSpeed.setOnClickListener(this);
|
|
binding.qualityTextView.setOnClickListener(this);
|
|
binding.captionTextView.setOnClickListener(this);
|
|
binding.resizeTextView.setOnClickListener(this);
|
|
binding.playbackLiveSync.setOnClickListener(this);
|
|
|
|
final PlayerGestureListener listener = new PlayerGestureListener(this, service);
|
|
gestureDetector = new GestureDetector(context, listener);
|
|
binding.getRoot().setOnTouchListener(listener);
|
|
|
|
binding.queueButton.setOnClickListener(this);
|
|
binding.segmentsButton.setOnClickListener(this);
|
|
binding.repeatButton.setOnClickListener(this);
|
|
binding.shuffleButton.setOnClickListener(this);
|
|
|
|
binding.playPauseButton.setOnClickListener(this);
|
|
binding.playPreviousButton.setOnClickListener(this);
|
|
binding.playNextButton.setOnClickListener(this);
|
|
|
|
binding.moreOptionsButton.setOnClickListener(this);
|
|
binding.moreOptionsButton.setOnLongClickListener(this);
|
|
binding.share.setOnClickListener(this);
|
|
binding.fullScreenButton.setOnClickListener(this);
|
|
binding.screenRotationButton.setOnClickListener(this);
|
|
binding.playWithKodi.setOnClickListener(this);
|
|
binding.openInBrowser.setOnClickListener(this);
|
|
binding.playerCloseButton.setOnClickListener(this);
|
|
binding.switchMute.setOnClickListener(this);
|
|
|
|
settingsContentObserver = new ContentObserver(new Handler()) {
|
|
@Override
|
|
public void onChange(final boolean selfChange) {
|
|
setupScreenRotationButton();
|
|
}
|
|
};
|
|
context.getContentResolver().registerContentObserver(
|
|
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false,
|
|
settingsContentObserver);
|
|
binding.getRoot().addOnLayoutChangeListener(this::onLayoutChange);
|
|
|
|
ViewCompat.setOnApplyWindowInsetsListener(binding.itemsListPanel, (view, windowInsets) -> {
|
|
final DisplayCutoutCompat cutout = windowInsets.getDisplayCutout();
|
|
if (cutout != null) {
|
|
view.setPadding(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(),
|
|
cutout.getSafeInsetRight(), cutout.getSafeInsetBottom());
|
|
}
|
|
return windowInsets;
|
|
});
|
|
|
|
// PlaybackControlRoot already consumed window insets but we should pass them to
|
|
// player_overlays too. Without it they will be off-centered
|
|
binding.playbackControlRoot.addOnLayoutChangeListener(
|
|
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
|
|
binding.playerOverlays.setPadding(
|
|
v.getPaddingLeft(),
|
|
v.getPaddingTop(),
|
|
v.getPaddingRight(),
|
|
v.getPaddingBottom()));
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Playback initialization via intent
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
public void handleIntent(@NonNull final Intent intent) {
|
|
// fail fast if no play queue was provided
|
|
final String queueCache = intent.getStringExtra(PLAY_QUEUE_KEY);
|
|
if (queueCache == null) {
|
|
return;
|
|
}
|
|
final PlayQueue newQueue = SerializedCache.getInstance().take(queueCache, PlayQueue.class);
|
|
if (newQueue == null) {
|
|
return;
|
|
}
|
|
|
|
final PlayerType oldPlayerType = playerType;
|
|
playerType = retrievePlayerTypeFromIntent(intent);
|
|
// We need to setup audioOnly before super(), see "sourceOf"
|
|
isAudioOnly = audioPlayerSelected();
|
|
|
|
if (intent.hasExtra(PLAYBACK_QUALITY)) {
|
|
setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY));
|
|
}
|
|
|
|
// Resolve append intents
|
|
if (intent.getBooleanExtra(APPEND_ONLY, false) && playQueue != null) {
|
|
final int sizeBeforeAppend = playQueue.size();
|
|
playQueue.append(newQueue.getStreams());
|
|
|
|
if ((intent.getBooleanExtra(SELECT_ON_APPEND, false)
|
|
|| currentState == STATE_COMPLETED) && newQueue.getStreams().size() > 0) {
|
|
playQueue.setIndex(sizeBeforeAppend);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this);
|
|
final float playbackSpeed = savedParameters.speed;
|
|
final float playbackPitch = savedParameters.pitch;
|
|
final boolean playbackSkipSilence = savedParameters.skipSilence;
|
|
|
|
final boolean samePlayQueue = playQueue != null && playQueue.equals(newQueue);
|
|
final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode());
|
|
final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true);
|
|
final boolean isMuted = intent.getBooleanExtra(IS_MUTED, isMuted());
|
|
|
|
/*
|
|
* There are 3 situations when playback shouldn't be started from scratch (zero timestamp):
|
|
* 1. User pressed on a timestamp link and the same video should be rewound to the timestamp
|
|
* 2. User changed a player from, for example. main to popup, or from audio to main, etc
|
|
* 3. User chose to resume a video based on a saved timestamp from history of played videos
|
|
* In those cases time will be saved because re-init of the play queue is a not an instant
|
|
* task and requires network calls
|
|
* */
|
|
// seek to timestamp if stream is already playing
|
|
if (!exoPlayerIsNull()
|
|
&& newQueue.size() == 1 && newQueue.getItem() != null
|
|
&& playQueue != null && playQueue.size() == 1 && playQueue.getItem() != null
|
|
&& newQueue.getItem().getUrl().equals(playQueue.getItem().getUrl())
|
|
&& newQueue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) {
|
|
// Player can have state = IDLE when playback is stopped or failed
|
|
// and we should retry() in this case
|
|
if (simpleExoPlayer.getPlaybackState()
|
|
== com.google.android.exoplayer2.Player.STATE_IDLE) {
|
|
simpleExoPlayer.retry();
|
|
}
|
|
simpleExoPlayer.seekTo(playQueue.getIndex(), newQueue.getItem().getRecoveryPosition());
|
|
simpleExoPlayer.setPlayWhenReady(playWhenReady);
|
|
|
|
} else if (!exoPlayerIsNull()
|
|
&& samePlayQueue
|
|
&& playQueue != null
|
|
&& !playQueue.isDisposed()) {
|
|
// Do not re-init the same PlayQueue. Save time
|
|
// Player can have state = IDLE when playback is stopped or failed
|
|
// and we should retry() in this case
|
|
if (simpleExoPlayer.getPlaybackState()
|
|
== com.google.android.exoplayer2.Player.STATE_IDLE) {
|
|
simpleExoPlayer.retry();
|
|
}
|
|
simpleExoPlayer.setPlayWhenReady(playWhenReady);
|
|
|
|
} else if (intent.getBooleanExtra(RESUME_PLAYBACK, false)
|
|
&& isPlaybackResumeEnabled(this)
|
|
&& !samePlayQueue
|
|
&& !newQueue.isEmpty()
|
|
&& newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) {
|
|
databaseUpdateDisposable.add(recordManager.loadStreamState(newQueue.getItem())
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
// Do not place initPlayback() in doFinally() because
|
|
// it restarts playback after destroy()
|
|
//.doFinally()
|
|
.subscribe(
|
|
state -> {
|
|
newQueue.setRecovery(newQueue.getIndex(), state.getProgressTime());
|
|
initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch,
|
|
playbackSkipSilence, playWhenReady, isMuted);
|
|
},
|
|
error -> {
|
|
if (DEBUG) {
|
|
error.printStackTrace();
|
|
}
|
|
// In case any error we can start playback without history
|
|
initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch,
|
|
playbackSkipSilence, playWhenReady, isMuted);
|
|
},
|
|
() -> {
|
|
// Completed but not found in history
|
|
initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch,
|
|
playbackSkipSilence, playWhenReady, isMuted);
|
|
}
|
|
));
|
|
} else {
|
|
// Good to go...
|
|
// In a case of equal PlayQueues we can re-init old one but only when it is disposed
|
|
initPlayback(samePlayQueue ? playQueue : newQueue, repeatMode, playbackSpeed,
|
|
playbackPitch, playbackSkipSilence, playWhenReady, isMuted);
|
|
}
|
|
|
|
if (oldPlayerType != playerType && playQueue != null) {
|
|
// If playerType changes from one to another we should reload the player
|
|
// (to disable/enable video stream or to set quality)
|
|
setRecovery();
|
|
reloadPlayQueueManager();
|
|
}
|
|
|
|
setupElementsVisibility();
|
|
setupElementsSize();
|
|
|
|
if (audioPlayerSelected()) {
|
|
service.removeViewFromParent();
|
|
} else if (popupPlayerSelected()) {
|
|
binding.getRoot().setVisibility(View.VISIBLE);
|
|
initPopup();
|
|
initPopupCloseOverlay();
|
|
binding.playPauseButton.requestFocus();
|
|
} else {
|
|
binding.getRoot().setVisibility(View.VISIBLE);
|
|
initVideoPlayer();
|
|
closeItemsList();
|
|
// Android TV: without it focus will frame the whole player
|
|
binding.playPauseButton.requestFocus();
|
|
|
|
if (simpleExoPlayer.getPlayWhenReady()) {
|
|
play();
|
|
} else {
|
|
pause();
|
|
}
|
|
}
|
|
NavigationHelper.sendPlayerStartedEvent(context);
|
|
}
|
|
|
|
private void initPlayback(@NonNull final PlayQueue queue,
|
|
@RepeatMode final int repeatMode,
|
|
final float playbackSpeed,
|
|
final float playbackPitch,
|
|
final boolean playbackSkipSilence,
|
|
final boolean playOnReady,
|
|
final boolean isMuted) {
|
|
destroyPlayer();
|
|
initPlayer(playOnReady);
|
|
setRepeatMode(repeatMode);
|
|
setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence);
|
|
|
|
playQueue = queue;
|
|
playQueue.init();
|
|
reloadPlayQueueManager();
|
|
|
|
if (playQueueAdapter != null) {
|
|
playQueueAdapter.dispose();
|
|
}
|
|
playQueueAdapter = new PlayQueueAdapter(context, playQueue);
|
|
segmentAdapter = new StreamSegmentAdapter(getStreamSegmentListener());
|
|
|
|
simpleExoPlayer.setVolume(isMuted ? 0 : 1);
|
|
notifyQueueUpdateToListeners();
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Destroy and recovery
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
private void destroyPlayer() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "destroyPlayer() called");
|
|
}
|
|
if (!exoPlayerIsNull()) {
|
|
simpleExoPlayer.removeListener(this);
|
|
simpleExoPlayer.stop();
|
|
simpleExoPlayer.release();
|
|
}
|
|
if (isProgressLoopRunning()) {
|
|
stopProgressLoop();
|
|
}
|
|
if (playQueue != null) {
|
|
playQueue.dispose();
|
|
}
|
|
if (audioReactor != null) {
|
|
audioReactor.dispose();
|
|
}
|
|
if (playQueueManager != null) {
|
|
playQueueManager.dispose();
|
|
}
|
|
if (mediaSessionManager != null) {
|
|
mediaSessionManager.dispose();
|
|
}
|
|
|
|
if (playQueueAdapter != null) {
|
|
playQueueAdapter.unsetSelectedListener();
|
|
playQueueAdapter.dispose();
|
|
}
|
|
}
|
|
|
|
public void destroy() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "destroy() called");
|
|
}
|
|
destroyPlayer();
|
|
unregisterBroadcastReceiver();
|
|
|
|
databaseUpdateDisposable.clear();
|
|
progressUpdateDisposable.set(null);
|
|
ImageLoader.getInstance().stop();
|
|
|
|
if (binding != null) {
|
|
binding.endScreen.setImageBitmap(null);
|
|
}
|
|
|
|
context.getContentResolver().unregisterContentObserver(settingsContentObserver);
|
|
}
|
|
|
|
public void setRecovery() {
|
|
if (playQueue == null || exoPlayerIsNull()) {
|
|
return;
|
|
}
|
|
|
|
final int queuePos = playQueue.getIndex();
|
|
final long windowPos = simpleExoPlayer.getCurrentPosition();
|
|
|
|
if (windowPos > 0 && windowPos <= simpleExoPlayer.getDuration()) {
|
|
setRecovery(queuePos, windowPos);
|
|
}
|
|
}
|
|
|
|
private void setRecovery(final int queuePos, final long windowPos) {
|
|
if (playQueue.size() <= queuePos) {
|
|
return;
|
|
}
|
|
|
|
if (DEBUG) {
|
|
Log.d(TAG, "Setting recovery, queue: " + queuePos + ", pos: " + windowPos);
|
|
}
|
|
playQueue.setRecovery(queuePos, windowPos);
|
|
}
|
|
|
|
private void reloadPlayQueueManager() {
|
|
if (playQueueManager != null) {
|
|
playQueueManager.dispose();
|
|
}
|
|
|
|
if (playQueue != null) {
|
|
playQueueManager = new MediaSourceManager(this, playQueue);
|
|
}
|
|
}
|
|
|
|
@Override // own playback listener
|
|
public void onPlaybackShutdown() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onPlaybackShutdown() called");
|
|
}
|
|
// destroys the service, which in turn will destroy the player
|
|
service.onDestroy();
|
|
}
|
|
|
|
public void smoothStopPlayer() {
|
|
// Pausing would make transition from one stream to a new stream not smooth, so only stop
|
|
simpleExoPlayer.stop(false);
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Player type specific setup
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
private void initVideoPlayer() {
|
|
// restore last resize mode
|
|
setResizeMode(prefs.getInt(context.getString(R.string.last_resize_mode),
|
|
AspectRatioFrameLayout.RESIZE_MODE_FIT));
|
|
binding.getRoot().setLayoutParams(new FrameLayout.LayoutParams(
|
|
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
|
|
}
|
|
|
|
@SuppressLint("RtlHardcoded")
|
|
private void initPopup() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "initPopup() called");
|
|
}
|
|
|
|
// Popup is already added to windowManager
|
|
if (popupHasParent()) {
|
|
return;
|
|
}
|
|
|
|
updateScreenSize();
|
|
|
|
popupLayoutParams = retrievePopupLayoutParamsFromPrefs(this);
|
|
binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height);
|
|
|
|
checkPopupPositionBounds();
|
|
|
|
binding.loadingPanel.setMinimumWidth(popupLayoutParams.width);
|
|
binding.loadingPanel.setMinimumHeight(popupLayoutParams.height);
|
|
|
|
service.removeViewFromParent();
|
|
Objects.requireNonNull(windowManager).addView(binding.getRoot(), popupLayoutParams);
|
|
|
|
// Popup doesn't have aspectRatio selector, using FIT automatically
|
|
setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
|
|
}
|
|
|
|
@SuppressLint("RtlHardcoded")
|
|
private void initPopupCloseOverlay() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "initPopupCloseOverlay() called");
|
|
}
|
|
|
|
// closeOverlayView is already added to windowManager
|
|
if (closeOverlayBinding != null) {
|
|
return;
|
|
}
|
|
|
|
closeOverlayBinding = PlayerPopupCloseOverlayBinding.inflate(LayoutInflater.from(context));
|
|
|
|
final WindowManager.LayoutParams closeOverlayLayoutParams = buildCloseOverlayLayoutParams();
|
|
closeOverlayBinding.closeButton.setVisibility(View.GONE);
|
|
Objects.requireNonNull(windowManager).addView(
|
|
closeOverlayBinding.getRoot(), closeOverlayLayoutParams);
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Elements visibility and size: popup and main players have different look
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
/**
|
|
* This method ensures that popup and main players have different look.
|
|
* We use one layout for both players and need to decide what to show and what to hide.
|
|
* Additional measuring should be done inside {@link #setupElementsSize}.
|
|
*/
|
|
private void setupElementsVisibility() {
|
|
if (popupPlayerSelected()) {
|
|
binding.fullScreenButton.setVisibility(View.VISIBLE);
|
|
binding.screenRotationButton.setVisibility(View.GONE);
|
|
binding.resizeTextView.setVisibility(View.GONE);
|
|
binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.GONE);
|
|
binding.queueButton.setVisibility(View.GONE);
|
|
binding.segmentsButton.setVisibility(View.GONE);
|
|
binding.moreOptionsButton.setVisibility(View.GONE);
|
|
binding.topControls.setOrientation(LinearLayout.HORIZONTAL);
|
|
binding.primaryControls.getLayoutParams().width
|
|
= LinearLayout.LayoutParams.WRAP_CONTENT;
|
|
binding.secondaryControls.setAlpha(1.0f);
|
|
binding.secondaryControls.setVisibility(View.VISIBLE);
|
|
binding.secondaryControls.setTranslationY(0);
|
|
binding.share.setVisibility(View.GONE);
|
|
binding.playWithKodi.setVisibility(View.GONE);
|
|
binding.openInBrowser.setVisibility(View.GONE);
|
|
binding.switchMute.setVisibility(View.GONE);
|
|
binding.playerCloseButton.setVisibility(View.GONE);
|
|
binding.topControls.bringToFront();
|
|
binding.topControls.setClickable(false);
|
|
binding.topControls.setFocusable(false);
|
|
binding.bottomControls.bringToFront();
|
|
closeItemsList();
|
|
} else if (videoPlayerSelected()) {
|
|
binding.fullScreenButton.setVisibility(View.GONE);
|
|
setupScreenRotationButton();
|
|
binding.resizeTextView.setVisibility(View.VISIBLE);
|
|
binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.VISIBLE);
|
|
binding.moreOptionsButton.setVisibility(View.VISIBLE);
|
|
binding.topControls.setOrientation(LinearLayout.VERTICAL);
|
|
binding.primaryControls.getLayoutParams().width
|
|
= LinearLayout.LayoutParams.MATCH_PARENT;
|
|
binding.secondaryControls.setVisibility(View.INVISIBLE);
|
|
binding.moreOptionsButton.setImageDrawable(AppCompatResources.getDrawable(context,
|
|
R.drawable.ic_expand_more_white_24dp));
|
|
binding.share.setVisibility(View.VISIBLE);
|
|
binding.openInBrowser.setVisibility(View.VISIBLE);
|
|
binding.switchMute.setVisibility(View.VISIBLE);
|
|
binding.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)
|
|
binding.topControls.setClickable(true);
|
|
binding.topControls.setFocusable(true);
|
|
}
|
|
showHideKodiButton();
|
|
|
|
if (isFullscreen) {
|
|
binding.titleTextView.setVisibility(View.VISIBLE);
|
|
binding.channelTextView.setVisibility(View.VISIBLE);
|
|
} else {
|
|
binding.titleTextView.setVisibility(View.GONE);
|
|
binding.channelTextView.setVisibility(View.GONE);
|
|
}
|
|
setMuteButton(binding.switchMute, isMuted());
|
|
|
|
animateRotation(binding.moreOptionsButton, DEFAULT_CONTROLS_DURATION, 0);
|
|
}
|
|
|
|
/**
|
|
* Changes padding, size of elements based on player selected right now.
|
|
* Popup player has small padding in comparison with the main player
|
|
*/
|
|
private void setupElementsSize() {
|
|
final Resources res = context.getResources();
|
|
final int buttonsMinWidth;
|
|
final int playerTopPad;
|
|
final int controlsPad;
|
|
final int buttonsPad;
|
|
|
|
if (popupPlayerSelected()) {
|
|
buttonsMinWidth = 0;
|
|
playerTopPad = 0;
|
|
controlsPad = res.getDimensionPixelSize(R.dimen.player_popup_controls_padding);
|
|
buttonsPad = res.getDimensionPixelSize(R.dimen.player_popup_buttons_padding);
|
|
} else if (videoPlayerSelected()) {
|
|
buttonsMinWidth = res.getDimensionPixelSize(R.dimen.player_main_buttons_min_width);
|
|
playerTopPad = res.getDimensionPixelSize(R.dimen.player_main_top_padding);
|
|
controlsPad = res.getDimensionPixelSize(R.dimen.player_main_controls_padding);
|
|
buttonsPad = res.getDimensionPixelSize(R.dimen.player_main_buttons_padding);
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
binding.topControls.setPaddingRelative(controlsPad, playerTopPad, controlsPad, 0);
|
|
binding.bottomControls.setPaddingRelative(controlsPad, 0, controlsPad, 0);
|
|
binding.qualityTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
|
binding.playbackSpeed.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
|
binding.playbackSpeed.setMinimumWidth(buttonsMinWidth);
|
|
binding.captionTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
|
}
|
|
|
|
private void showHideKodiButton() {
|
|
// show kodi button if it supports the current service and it is enabled in settings
|
|
binding.playWithKodi.setVisibility(videoPlayerSelected()
|
|
&& playQueue != null && playQueue.getItem() != null
|
|
&& KoreUtil.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId())
|
|
? View.VISIBLE : View.GONE);
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Broadcast receiver
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
private void setupBroadcastReceiver() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "setupBroadcastReceiver() called");
|
|
}
|
|
|
|
broadcastReceiver = new BroadcastReceiver() {
|
|
@Override
|
|
public void onReceive(final Context ctx, final Intent intent) {
|
|
onBroadcastReceived(intent);
|
|
}
|
|
};
|
|
intentFilter = new IntentFilter();
|
|
|
|
intentFilter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
|
|
|
|
intentFilter.addAction(ACTION_CLOSE);
|
|
intentFilter.addAction(ACTION_PLAY_PAUSE);
|
|
intentFilter.addAction(ACTION_PLAY_PREVIOUS);
|
|
intentFilter.addAction(ACTION_PLAY_NEXT);
|
|
intentFilter.addAction(ACTION_FAST_REWIND);
|
|
intentFilter.addAction(ACTION_FAST_FORWARD);
|
|
intentFilter.addAction(ACTION_REPEAT);
|
|
intentFilter.addAction(ACTION_SHUFFLE);
|
|
intentFilter.addAction(ACTION_RECREATE_NOTIFICATION);
|
|
|
|
intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED);
|
|
intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED);
|
|
|
|
intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
|
|
intentFilter.addAction(Intent.ACTION_SCREEN_ON);
|
|
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
|
|
intentFilter.addAction(Intent.ACTION_HEADSET_PLUG);
|
|
}
|
|
|
|
private void onBroadcastReceived(final Intent intent) {
|
|
if (intent == null || intent.getAction() == null) {
|
|
return;
|
|
}
|
|
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]");
|
|
}
|
|
|
|
switch (intent.getAction()) {
|
|
case AudioManager.ACTION_AUDIO_BECOMING_NOISY:
|
|
pause();
|
|
break;
|
|
case ACTION_CLOSE:
|
|
service.onDestroy();
|
|
break;
|
|
case ACTION_PLAY_PAUSE:
|
|
playPause();
|
|
if (!fragmentIsVisible) {
|
|
// Ensure that we have audio-only stream playing when a user
|
|
// started to play from notification's play button from outside of the app
|
|
onFragmentStopped();
|
|
}
|
|
break;
|
|
case ACTION_PLAY_PREVIOUS:
|
|
playPrevious();
|
|
break;
|
|
case ACTION_PLAY_NEXT:
|
|
playNext();
|
|
break;
|
|
case ACTION_FAST_REWIND:
|
|
fastRewind();
|
|
break;
|
|
case ACTION_FAST_FORWARD:
|
|
fastForward();
|
|
break;
|
|
case ACTION_REPEAT:
|
|
onRepeatClicked();
|
|
break;
|
|
case ACTION_SHUFFLE:
|
|
onShuffleClicked();
|
|
break;
|
|
case ACTION_RECREATE_NOTIFICATION:
|
|
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true);
|
|
break;
|
|
case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED:
|
|
fragmentIsVisible = true;
|
|
useVideoSource(true);
|
|
break;
|
|
case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED:
|
|
fragmentIsVisible = false;
|
|
onFragmentStopped();
|
|
break;
|
|
case Intent.ACTION_CONFIGURATION_CHANGED:
|
|
assureCorrectAppLanguage(service);
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onConfigurationChanged() called");
|
|
}
|
|
if (popupPlayerSelected()) {
|
|
updateScreenSize();
|
|
changePopupSize(popupLayoutParams.width);
|
|
checkPopupPositionBounds();
|
|
}
|
|
// Close it because when changing orientation from portrait
|
|
// (in fullscreen mode) the size of queue layout can be larger than the screen size
|
|
closeItemsList();
|
|
break;
|
|
case Intent.ACTION_SCREEN_ON:
|
|
// Interrupt playback only when screen turns on
|
|
// and user is watching video in popup player.
|
|
// Same actions for video player will be handled in ACTION_VIDEO_FRAGMENT_RESUMED
|
|
if (popupPlayerSelected() && (isPlaying() || isLoading())) {
|
|
useVideoSource(true);
|
|
}
|
|
break;
|
|
case Intent.ACTION_SCREEN_OFF:
|
|
// Interrupt playback only when screen turns off with popup player working
|
|
if (popupPlayerSelected() && (isPlaying() || isLoading())) {
|
|
useVideoSource(false);
|
|
}
|
|
break;
|
|
case Intent.ACTION_HEADSET_PLUG: //FIXME
|
|
/*notificationManager.cancel(NOTIFICATION_ID);
|
|
mediaSessionManager.dispose();
|
|
mediaSessionManager.enable(getBaseContext(), basePlayerImpl.simpleExoPlayer);*/
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void registerBroadcastReceiver() {
|
|
// Try to unregister current first
|
|
unregisterBroadcastReceiver();
|
|
context.registerReceiver(broadcastReceiver, intentFilter);
|
|
}
|
|
|
|
private void unregisterBroadcastReceiver() {
|
|
try {
|
|
context.unregisterReceiver(broadcastReceiver);
|
|
} catch (final IllegalArgumentException unregisteredException) {
|
|
Log.w(TAG, "Broadcast receiver already unregistered: "
|
|
+ unregisteredException.getMessage());
|
|
}
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Thumbnail loading
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
private void initThumbnail(final String url) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "Thumbnail - initThumbnail() called");
|
|
}
|
|
if (url == null || url.isEmpty()) {
|
|
return;
|
|
}
|
|
ImageLoader.getInstance().resume();
|
|
ImageLoader.getInstance()
|
|
.loadImage(url, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, this);
|
|
}
|
|
|
|
@Override
|
|
public void onLoadingStarted(final String imageUri, final View view) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "Thumbnail - onLoadingStarted() called on: "
|
|
+ "imageUri = [" + imageUri + "], view = [" + view + "]");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onLoadingFailed(final String imageUri, final View view,
|
|
final FailReason failReason) {
|
|
Log.e(TAG, "Thumbnail - onLoadingFailed() called on imageUri = [" + imageUri + "]",
|
|
failReason.getCause());
|
|
currentThumbnail = null;
|
|
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
|
}
|
|
|
|
@Override
|
|
public void onLoadingComplete(final String imageUri, final View view,
|
|
final Bitmap loadedImage) {
|
|
final float width = Math.min(
|
|
context.getResources().getDimension(R.dimen.player_notification_thumbnail_width),
|
|
loadedImage.getWidth());
|
|
|
|
if (DEBUG) {
|
|
Log.d(TAG, "Thumbnail - onLoadingComplete() called with: "
|
|
+ "imageUri = [" + imageUri + "], view = [" + view + "], "
|
|
+ "loadedImage = [" + loadedImage + "], "
|
|
+ loadedImage.getWidth() + "x" + loadedImage.getHeight()
|
|
+ ", scaled width = " + width);
|
|
}
|
|
|
|
currentThumbnail = Bitmap.createScaledBitmap(loadedImage,
|
|
(int) width,
|
|
(int) (loadedImage.getHeight() / (loadedImage.getWidth() / width)), true);
|
|
binding.endScreen.setImageBitmap(loadedImage);
|
|
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
|
}
|
|
|
|
@Override
|
|
public void onLoadingCancelled(final String imageUri, final View view) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: "
|
|
+ "imageUri = [" + imageUri + "], view = [" + view + "]");
|
|
}
|
|
currentThumbnail = null;
|
|
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Popup player utils
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
/**
|
|
* Check if {@link #popupLayoutParams}' position is within a arbitrary boundary
|
|
* that goes from (0, 0) to (screenWidth, screenHeight).
|
|
* <p>
|
|
* If it's out of these boundaries, {@link #popupLayoutParams}' position is changed
|
|
* and {@code true} is returned to represent this change.
|
|
* </p>
|
|
*/
|
|
public void checkPopupPositionBounds() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "checkPopupPositionBounds() called with: "
|
|
+ "screenWidth = [" + screenWidth + "], "
|
|
+ "screenHeight = [" + screenHeight + "]");
|
|
}
|
|
if (popupLayoutParams == null) {
|
|
return;
|
|
}
|
|
|
|
if (popupLayoutParams.x < 0) {
|
|
popupLayoutParams.x = 0;
|
|
} else if (popupLayoutParams.x > screenWidth - popupLayoutParams.width) {
|
|
popupLayoutParams.x = (int) (screenWidth - popupLayoutParams.width);
|
|
}
|
|
|
|
if (popupLayoutParams.y < 0) {
|
|
popupLayoutParams.y = 0;
|
|
} else if (popupLayoutParams.y > screenHeight - popupLayoutParams.height) {
|
|
popupLayoutParams.y = (int) (screenHeight - popupLayoutParams.height);
|
|
}
|
|
}
|
|
|
|
public void updateScreenSize() {
|
|
if (windowManager != null) {
|
|
final DisplayMetrics metrics = new DisplayMetrics();
|
|
windowManager.getDefaultDisplay().getMetrics(metrics);
|
|
|
|
screenWidth = metrics.widthPixels;
|
|
screenHeight = metrics.heightPixels;
|
|
if (DEBUG) {
|
|
Log.d(TAG, "updateScreenSize() called: screenWidth = ["
|
|
+ screenWidth + "], screenHeight = [" + screenHeight + "]");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Changes the size of the popup based on the width.
|
|
* @param width the new width, height is calculated with
|
|
* {@link PlayerHelper#getMinimumVideoHeight(float)}
|
|
*/
|
|
public void changePopupSize(final int width) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "changePopupSize() called with: width = [" + width + "]");
|
|
}
|
|
|
|
if (anyPopupViewIsNull()) {
|
|
return;
|
|
}
|
|
|
|
final float minimumWidth = context.getResources().getDimension(R.dimen.popup_minimum_width);
|
|
final int actualWidth = (int) (width > screenWidth ? screenWidth
|
|
: (width < minimumWidth ? minimumWidth : width));
|
|
final int actualHeight = (int) getMinimumVideoHeight(width);
|
|
if (DEBUG) {
|
|
Log.d(TAG, "updatePopupSize() updated values:"
|
|
+ " width = [" + actualWidth + "], height = [" + actualHeight + "]");
|
|
}
|
|
|
|
popupLayoutParams.width = actualWidth;
|
|
popupLayoutParams.height = actualHeight;
|
|
binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height);
|
|
Objects.requireNonNull(windowManager)
|
|
.updateViewLayout(binding.getRoot(), popupLayoutParams);
|
|
}
|
|
|
|
private void changePopupWindowFlags(final int flags) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "changePopupWindowFlags() called with: flags = [" + flags + "]");
|
|
}
|
|
|
|
if (!anyPopupViewIsNull()) {
|
|
popupLayoutParams.flags = flags;
|
|
Objects.requireNonNull(windowManager)
|
|
.updateViewLayout(binding.getRoot(), popupLayoutParams);
|
|
}
|
|
}
|
|
|
|
public void closePopup() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing);
|
|
}
|
|
if (isPopupClosing) {
|
|
return;
|
|
}
|
|
isPopupClosing = true;
|
|
|
|
saveStreamProgressState();
|
|
Objects.requireNonNull(windowManager).removeView(binding.getRoot());
|
|
|
|
animatePopupOverlayAndFinishService();
|
|
}
|
|
|
|
public void removePopupFromView() {
|
|
if (windowManager != null) {
|
|
final boolean isCloseOverlayHasParent = closeOverlayBinding != null
|
|
&& closeOverlayBinding.closeButton.getParent() != null;
|
|
if (popupHasParent()) {
|
|
windowManager.removeView(binding.getRoot());
|
|
}
|
|
if (isCloseOverlayHasParent) {
|
|
windowManager.removeView(closeOverlayBinding.getRoot());
|
|
}
|
|
}
|
|
}
|
|
|
|
private void animatePopupOverlayAndFinishService() {
|
|
final int targetTranslationY =
|
|
(int) (closeOverlayBinding.closeButton.getRootView().getHeight()
|
|
- closeOverlayBinding.closeButton.getY());
|
|
|
|
closeOverlayBinding.closeButton.animate().setListener(null).cancel();
|
|
closeOverlayBinding.closeButton.animate()
|
|
.setInterpolator(new AnticipateInterpolator())
|
|
.translationY(targetTranslationY)
|
|
.setDuration(400)
|
|
.setListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationCancel(final Animator animation) {
|
|
end();
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationEnd(final Animator animation) {
|
|
end();
|
|
}
|
|
|
|
private void end() {
|
|
Objects.requireNonNull(windowManager)
|
|
.removeView(closeOverlayBinding.getRoot());
|
|
closeOverlayBinding = null;
|
|
service.onDestroy();
|
|
}
|
|
}).start();
|
|
}
|
|
|
|
private boolean popupHasParent() {
|
|
return binding != null
|
|
&& binding.getRoot().getLayoutParams() instanceof WindowManager.LayoutParams
|
|
&& binding.getRoot().getParent() != null;
|
|
}
|
|
|
|
private boolean anyPopupViewIsNull() {
|
|
// TODO understand why checking getParentActivity() != null
|
|
return popupLayoutParams == null || windowManager == null
|
|
|| getParentActivity() != null || binding.getRoot().getParent() == null;
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Playback parameters
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
public float getPlaybackSpeed() {
|
|
return getPlaybackParameters().speed;
|
|
}
|
|
|
|
private void setPlaybackSpeed(final float speed) {
|
|
setPlaybackParameters(speed, getPlaybackPitch(), getPlaybackSkipSilence());
|
|
}
|
|
|
|
public float getPlaybackPitch() {
|
|
return getPlaybackParameters().pitch;
|
|
}
|
|
|
|
public boolean getPlaybackSkipSilence() {
|
|
return getPlaybackParameters().skipSilence;
|
|
}
|
|
|
|
public PlaybackParameters getPlaybackParameters() {
|
|
if (exoPlayerIsNull()) {
|
|
return PlaybackParameters.DEFAULT;
|
|
}
|
|
return simpleExoPlayer.getPlaybackParameters();
|
|
}
|
|
|
|
/**
|
|
* Sets the playback parameters of the player, and also saves them to shared preferences.
|
|
* Speed and pitch are rounded up to 2 decimal places before being used or saved.
|
|
*
|
|
* @param speed the playback speed, will be rounded to up to 2 decimal places
|
|
* @param pitch the playback pitch, will be rounded to up to 2 decimal places
|
|
* @param skipSilence skip silence during playback
|
|
*/
|
|
public void setPlaybackParameters(final float speed, final float pitch,
|
|
final boolean skipSilence) {
|
|
final float roundedSpeed = Math.round(speed * 100.0f) / 100.0f;
|
|
final float roundedPitch = Math.round(pitch * 100.0f) / 100.0f;
|
|
|
|
savePlaybackParametersToPrefs(this, roundedSpeed, roundedPitch, skipSilence);
|
|
simpleExoPlayer.setPlaybackParameters(
|
|
new PlaybackParameters(roundedSpeed, roundedPitch, skipSilence));
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Progress loop and updates
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
private void onUpdateProgress(final int currentProgress,
|
|
final int duration,
|
|
final int bufferPercent) {
|
|
if (!isPrepared) {
|
|
return;
|
|
}
|
|
|
|
if (duration != binding.playbackSeekBar.getMax()) {
|
|
binding.playbackEndTime.setText(getTimeString(duration));
|
|
binding.playbackSeekBar.setMax(duration);
|
|
}
|
|
if (currentState != STATE_PAUSED) {
|
|
if (currentState != STATE_PAUSED_SEEK) {
|
|
binding.playbackSeekBar.setProgress(currentProgress);
|
|
}
|
|
binding.playbackCurrentTime.setText(getTimeString(currentProgress));
|
|
}
|
|
if (simpleExoPlayer.isLoading() || bufferPercent > 90) {
|
|
binding.playbackSeekBar.setSecondaryProgress(
|
|
(int) (binding.playbackSeekBar.getMax() * ((float) bufferPercent / 100)));
|
|
}
|
|
if (DEBUG && bufferPercent % 20 == 0) { //Limit log
|
|
Log.d(TAG, "notifyProgressUpdateToListeners() called with: "
|
|
+ "isVisible = " + isControlsVisible() + ", "
|
|
+ "currentProgress = [" + currentProgress + "], "
|
|
+ "duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]");
|
|
}
|
|
binding.playbackLiveSync.setClickable(!isLiveEdge());
|
|
|
|
notifyProgressUpdateToListeners(currentProgress, duration, bufferPercent);
|
|
|
|
if (areSegmentsVisible) {
|
|
segmentAdapter.selectSegmentAt(getNearestStreamSegmentPosition(currentProgress));
|
|
}
|
|
|
|
final boolean showThumbnail = prefs.getBoolean(
|
|
context.getString(R.string.show_thumbnail_key), true);
|
|
// setMetadata only updates the metadata when any of the metadata keys are null
|
|
mediaSessionManager.setMetadata(getVideoTitle(), getUploaderName(),
|
|
showThumbnail ? getThumbnail() : null, duration);
|
|
}
|
|
|
|
private void startProgressLoop() {
|
|
progressUpdateDisposable.set(getProgressUpdateDisposable());
|
|
}
|
|
|
|
private void stopProgressLoop() {
|
|
progressUpdateDisposable.set(null);
|
|
}
|
|
|
|
private boolean isProgressLoopRunning() {
|
|
return progressUpdateDisposable.get() != null;
|
|
}
|
|
|
|
private void triggerProgressUpdate() {
|
|
if (exoPlayerIsNull()) {
|
|
return;
|
|
}
|
|
onUpdateProgress(
|
|
Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
|
|
(int) simpleExoPlayer.getDuration(),
|
|
simpleExoPlayer.getBufferedPercentage()
|
|
);
|
|
}
|
|
|
|
private Disposable getProgressUpdateDisposable() {
|
|
return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, MILLISECONDS,
|
|
AndroidSchedulers.mainThread())
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
.subscribe(ignored -> triggerProgressUpdate(),
|
|
error -> Log.e(TAG, "Progress update failure: ", error));
|
|
}
|
|
|
|
@Override // seekbar listener
|
|
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) {
|
|
binding.currentDisplaySeek.setText(getTimeString(progress));
|
|
}
|
|
}
|
|
|
|
@Override // seekbar listener
|
|
public void onStartTrackingTouch(final SeekBar seekBar) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]");
|
|
}
|
|
if (currentState != STATE_PAUSED_SEEK) {
|
|
changeState(STATE_PAUSED_SEEK);
|
|
}
|
|
|
|
saveWasPlaying();
|
|
if (isPlaying()) {
|
|
simpleExoPlayer.setPlayWhenReady(false);
|
|
}
|
|
|
|
showControls(0);
|
|
animate(binding.currentDisplaySeek, true, DEFAULT_CONTROLS_DURATION,
|
|
AnimationType.SCALE_AND_ALPHA);
|
|
}
|
|
|
|
@Override // seekbar listener
|
|
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);
|
|
}
|
|
|
|
binding.playbackCurrentTime.setText(getTimeString(seekBar.getProgress()));
|
|
animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA);
|
|
|
|
if (currentState == STATE_PAUSED_SEEK) {
|
|
changeState(STATE_BUFFERING);
|
|
}
|
|
if (!isProgressLoopRunning()) {
|
|
startProgressLoop();
|
|
}
|
|
if (wasPlaying) {
|
|
showControlsThenHide();
|
|
}
|
|
}
|
|
|
|
public void saveWasPlaying() {
|
|
this.wasPlaying = simpleExoPlayer.getPlayWhenReady();
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Controls showing / hiding
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
public boolean isControlsVisible() {
|
|
return binding != null && binding.playbackControlRoot.getVisibility() == View.VISIBLE;
|
|
}
|
|
|
|
/**
|
|
* 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 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 (controlViewAnimator != null && controlViewAnimator.isRunning()) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "showAndAnimateControl: controlViewAnimator.isRunning");
|
|
}
|
|
controlViewAnimator.end();
|
|
}
|
|
|
|
if (drawableId == -1) {
|
|
if (binding.controlAnimationView.getVisibility() == View.VISIBLE) {
|
|
controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(
|
|
binding.controlAnimationView,
|
|
PropertyValuesHolder.ofFloat(View.ALPHA, 1.0f, 0.0f),
|
|
PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1.0f),
|
|
PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1.0f)
|
|
).setDuration(DEFAULT_CONTROLS_DURATION);
|
|
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(final Animator animation) {
|
|
binding.controlAnimationView.setVisibility(View.GONE);
|
|
}
|
|
});
|
|
controlViewAnimator.start();
|
|
}
|
|
return;
|
|
}
|
|
|
|
final float scaleFrom = goneOnEnd ? 1f : 1f;
|
|
final float scaleTo = goneOnEnd ? 1.8f : 1.4f;
|
|
final float alphaFrom = goneOnEnd ? 1f : 0f;
|
|
final float alphaTo = goneOnEnd ? 0f : 1f;
|
|
|
|
|
|
controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(
|
|
binding.controlAnimationView,
|
|
PropertyValuesHolder.ofFloat(View.ALPHA, alphaFrom, alphaTo),
|
|
PropertyValuesHolder.ofFloat(View.SCALE_X, scaleFrom, scaleTo),
|
|
PropertyValuesHolder.ofFloat(View.SCALE_Y, scaleFrom, scaleTo)
|
|
);
|
|
controlViewAnimator.setDuration(goneOnEnd ? 1000 : 500);
|
|
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(final Animator animation) {
|
|
binding.controlAnimationView.setVisibility(goneOnEnd ? View.GONE : View.VISIBLE);
|
|
}
|
|
});
|
|
|
|
|
|
binding.controlAnimationView.setVisibility(View.VISIBLE);
|
|
binding.controlAnimationView.setImageDrawable(
|
|
AppCompatResources.getDrawable(context, drawableId));
|
|
controlViewAnimator.start();
|
|
}
|
|
|
|
public void showControlsThenHide() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "showControlsThenHide() called");
|
|
}
|
|
showOrHideButtons();
|
|
showSystemUIPartially();
|
|
|
|
final int hideTime = binding.playbackControlRoot.isInTouchMode()
|
|
? DEFAULT_CONTROLS_HIDE_TIME
|
|
: DPAD_CONTROLS_HIDE_TIME;
|
|
|
|
showHideShadow(true, DEFAULT_CONTROLS_DURATION);
|
|
animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION,
|
|
AnimationType.ALPHA, 0, () -> hideControls(DEFAULT_CONTROLS_DURATION, hideTime));
|
|
}
|
|
|
|
public void showControls(final long duration) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "showControls() called");
|
|
}
|
|
showOrHideButtons();
|
|
showSystemUIPartially();
|
|
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
|
showHideShadow(true, duration);
|
|
animate(binding.playbackControlRoot, true, duration);
|
|
}
|
|
|
|
public void hideControls(final long duration, final long delay) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "hideControls() called with: duration = [" + duration
|
|
+ "], delay = [" + delay + "]");
|
|
}
|
|
|
|
showOrHideButtons();
|
|
|
|
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
|
controlsVisibilityHandler.postDelayed(() -> {
|
|
showHideShadow(false, duration);
|
|
animate(binding.playbackControlRoot, false, duration, AnimationType.ALPHA,
|
|
0, this::hideSystemUIIfNeeded);
|
|
}, delay);
|
|
}
|
|
|
|
private void showHideShadow(final boolean show, final long duration) {
|
|
animate(binding.playerTopShadow, show, duration, AnimationType.ALPHA, 0, null);
|
|
animate(binding.playerBottomShadow, show, duration, AnimationType.ALPHA, 0, null);
|
|
}
|
|
|
|
private void showOrHideButtons() {
|
|
if (playQueue == null) {
|
|
return;
|
|
}
|
|
|
|
final boolean showPrev = playQueue.getIndex() != 0;
|
|
final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size();
|
|
final boolean showQueue = playQueue.getStreams().size() > 1 && !popupPlayerSelected();
|
|
boolean showSegment = false;
|
|
if (currentMetadata != null) {
|
|
showSegment = !currentMetadata.getMetadata().getStreamSegments().isEmpty()
|
|
&& !popupPlayerSelected();
|
|
}
|
|
|
|
binding.playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE);
|
|
binding.playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f);
|
|
binding.playNextButton.setVisibility(showNext ? View.VISIBLE : View.INVISIBLE);
|
|
binding.playNextButton.setAlpha(showNext ? 1.0f : 0.0f);
|
|
binding.queueButton.setVisibility(showQueue ? View.VISIBLE : View.GONE);
|
|
binding.queueButton.setAlpha(showQueue ? 1.0f : 0.0f);
|
|
binding.segmentsButton.setVisibility(showSegment ? View.VISIBLE : View.GONE);
|
|
binding.segmentsButton.setAlpha(showSegment ? 1.0f : 0.0f);
|
|
}
|
|
|
|
private void showSystemUIPartially() {
|
|
final AppCompatActivity activity = getParentActivity();
|
|
if (isFullscreen && activity != null) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
|
|
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
|
}
|
|
final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
|
|
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
|
|
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
|
}
|
|
}
|
|
|
|
private void hideSystemUIIfNeeded() {
|
|
if (fragmentListener != null) {
|
|
fragmentListener.hideSystemUiIfNeeded();
|
|
}
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Playback states
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
@Override // exoplayer listener
|
|
public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "ExoPlayer - onPlayerStateChanged() called with: "
|
|
+ "playWhenReady = [" + playWhenReady + "], "
|
|
+ "playbackState = [" + playbackState + "]");
|
|
}
|
|
|
|
if (currentState == STATE_PAUSED_SEEK) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "ExoPlayer - onPlayerStateChanged() is currently blocked");
|
|
}
|
|
return;
|
|
}
|
|
|
|
switch (playbackState) {
|
|
case com.google.android.exoplayer2.Player.STATE_IDLE: // 1
|
|
isPrepared = false;
|
|
break;
|
|
case com.google.android.exoplayer2.Player.STATE_BUFFERING: // 2
|
|
if (isPrepared) {
|
|
changeState(STATE_BUFFERING);
|
|
}
|
|
break;
|
|
case com.google.android.exoplayer2.Player.STATE_READY: //3
|
|
maybeUpdateCurrentMetadata();
|
|
maybeCorrectSeekPosition();
|
|
if (!isPrepared) {
|
|
isPrepared = true;
|
|
onPrepared(playWhenReady);
|
|
}
|
|
changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED);
|
|
break;
|
|
case com.google.android.exoplayer2.Player.STATE_ENDED: // 4
|
|
changeState(STATE_COMPLETED);
|
|
if (currentMetadata != null) {
|
|
resetStreamProgressState(currentMetadata.getMetadata());
|
|
}
|
|
isPrepared = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
@Override // exoplayer listener
|
|
public void onLoadingChanged(final boolean isLoading) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "ExoPlayer - onLoadingChanged() called with: "
|
|
+ "isLoading = [" + isLoading + "]");
|
|
}
|
|
|
|
if (!isLoading && currentState == STATE_PAUSED && isProgressLoopRunning()) {
|
|
stopProgressLoop();
|
|
} else if (isLoading && !isProgressLoopRunning()) {
|
|
startProgressLoop();
|
|
}
|
|
|
|
maybeUpdateCurrentMetadata();
|
|
}
|
|
|
|
@Override // own playback listener
|
|
public void onPlaybackBlock() {
|
|
if (exoPlayerIsNull()) {
|
|
return;
|
|
}
|
|
if (DEBUG) {
|
|
Log.d(TAG, "Playback - onPlaybackBlock() called");
|
|
}
|
|
|
|
currentItem = null;
|
|
currentMetadata = null;
|
|
simpleExoPlayer.stop();
|
|
isPrepared = false;
|
|
|
|
changeState(STATE_BLOCKED);
|
|
}
|
|
|
|
@Override // own playback listener
|
|
public void onPlaybackUnblock(final MediaSource mediaSource) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "Playback - onPlaybackUnblock() called");
|
|
}
|
|
|
|
if (exoPlayerIsNull()) {
|
|
return;
|
|
}
|
|
if (currentState == STATE_BLOCKED) {
|
|
changeState(STATE_BUFFERING);
|
|
}
|
|
simpleExoPlayer.prepare(mediaSource);
|
|
}
|
|
|
|
public void changeState(final int state) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "changeState() called with: state = [" + state + "]");
|
|
}
|
|
currentState = state;
|
|
switch (state) {
|
|
case STATE_BLOCKED:
|
|
onBlocked();
|
|
break;
|
|
case STATE_PLAYING:
|
|
onPlaying();
|
|
break;
|
|
case STATE_BUFFERING:
|
|
onBuffering();
|
|
break;
|
|
case STATE_PAUSED:
|
|
onPaused();
|
|
break;
|
|
case STATE_PAUSED_SEEK:
|
|
onPausedSeek();
|
|
break;
|
|
case STATE_COMPLETED:
|
|
onCompleted();
|
|
break;
|
|
}
|
|
notifyPlaybackUpdateToListeners();
|
|
}
|
|
|
|
private void onPrepared(final boolean playWhenReady) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
|
|
}
|
|
|
|
binding.playbackSeekBar.setMax((int) simpleExoPlayer.getDuration());
|
|
binding.playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration()));
|
|
binding.playbackSpeed.setText(formatSpeed(getPlaybackSpeed()));
|
|
|
|
if (playWhenReady) {
|
|
audioReactor.requestAudioFocus();
|
|
}
|
|
}
|
|
|
|
private void onBlocked() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onBlocked() called");
|
|
}
|
|
if (!isProgressLoopRunning()) {
|
|
startProgressLoop();
|
|
}
|
|
|
|
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
|
animate(binding.playbackControlRoot, false, DEFAULT_CONTROLS_DURATION);
|
|
|
|
binding.playbackSeekBar.setEnabled(false);
|
|
binding.playbackSeekBar.getThumb()
|
|
.setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN));
|
|
|
|
binding.loadingPanel.setBackgroundColor(Color.BLACK);
|
|
animate(binding.loadingPanel, true, 0);
|
|
animate(binding.surfaceForeground, true, 100);
|
|
|
|
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp);
|
|
animatePlayButtons(false, 100);
|
|
binding.getRoot().setKeepScreenOn(false);
|
|
|
|
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
|
}
|
|
|
|
private void onPlaying() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onPlaying() called");
|
|
}
|
|
if (!isProgressLoopRunning()) {
|
|
startProgressLoop();
|
|
}
|
|
|
|
updateStreamRelatedViews();
|
|
|
|
showAndAnimateControl(-1, true);
|
|
|
|
binding.playbackSeekBar.setEnabled(true);
|
|
binding.playbackSeekBar.getThumb()
|
|
.setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN));
|
|
|
|
binding.loadingPanel.setVisibility(View.GONE);
|
|
|
|
animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA);
|
|
|
|
animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
|
|
() -> {
|
|
binding.playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp);
|
|
animatePlayButtons(true, 200);
|
|
if (!isQueueVisible) {
|
|
binding.playPauseButton.requestFocus();
|
|
}
|
|
});
|
|
|
|
changePopupWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS);
|
|
checkLandscape();
|
|
binding.getRoot().setKeepScreenOn(true);
|
|
|
|
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
|
}
|
|
|
|
private void onBuffering() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onBuffering() called");
|
|
}
|
|
binding.loadingPanel.setBackgroundColor(Color.TRANSPARENT);
|
|
|
|
binding.getRoot().setKeepScreenOn(true);
|
|
|
|
if (NotificationUtil.getInstance().shouldUpdateBufferingSlot()) {
|
|
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
|
}
|
|
}
|
|
|
|
private void onPaused() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onPaused() called");
|
|
}
|
|
|
|
if (isProgressLoopRunning()) {
|
|
stopProgressLoop();
|
|
}
|
|
|
|
showControls(400);
|
|
binding.loadingPanel.setVisibility(View.GONE);
|
|
|
|
animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
|
|
() -> {
|
|
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp);
|
|
animatePlayButtons(true, 200);
|
|
if (!isQueueVisible) {
|
|
binding.playPauseButton.requestFocus();
|
|
}
|
|
});
|
|
|
|
changePopupWindowFlags(IDLE_WINDOW_FLAGS);
|
|
|
|
// Remove running notification when user does not want minimization to background or popup
|
|
if (PlayerHelper.isMinimizeOnExitDisabled(context) && videoPlayerSelected()) {
|
|
NotificationUtil.getInstance().cancelNotificationAndStopForeground(service);
|
|
} else {
|
|
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
|
}
|
|
|
|
binding.getRoot().setKeepScreenOn(false);
|
|
}
|
|
|
|
private void onPausedSeek() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onPausedSeek() called");
|
|
}
|
|
showAndAnimateControl(-1, true);
|
|
|
|
animatePlayButtons(false, 100);
|
|
binding.getRoot().setKeepScreenOn(true);
|
|
|
|
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
|
}
|
|
|
|
private void onCompleted() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onCompleted() called");
|
|
}
|
|
|
|
animate(binding.playPauseButton, false, 0, AnimationType.SCALE_AND_ALPHA, 0,
|
|
() -> {
|
|
binding.playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp);
|
|
animatePlayButtons(true, DEFAULT_CONTROLS_DURATION);
|
|
});
|
|
|
|
binding.getRoot().setKeepScreenOn(false);
|
|
changePopupWindowFlags(IDLE_WINDOW_FLAGS);
|
|
|
|
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
|
if (isFullscreen) {
|
|
toggleFullscreen();
|
|
}
|
|
|
|
if (playQueue.getIndex() < playQueue.size() - 1) {
|
|
playQueue.offsetIndex(+1);
|
|
}
|
|
if (isProgressLoopRunning()) {
|
|
stopProgressLoop();
|
|
}
|
|
|
|
showControls(500);
|
|
animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA);
|
|
binding.loadingPanel.setVisibility(View.GONE);
|
|
animate(binding.surfaceForeground, true, 100);
|
|
}
|
|
|
|
private void animatePlayButtons(final boolean show, final int duration) {
|
|
animate(binding.playPauseButton, show, duration, AnimationType.SCALE_AND_ALPHA);
|
|
|
|
boolean showQueueButtons = show;
|
|
if (playQueue == null) {
|
|
showQueueButtons = false;
|
|
}
|
|
|
|
if (!showQueueButtons || playQueue.getIndex() > 0) {
|
|
animate(
|
|
binding.playPreviousButton,
|
|
showQueueButtons,
|
|
duration,
|
|
AnimationType.SCALE_AND_ALPHA);
|
|
}
|
|
if (!showQueueButtons || playQueue.getIndex() + 1 < playQueue.getStreams().size()) {
|
|
animate(
|
|
binding.playNextButton,
|
|
showQueueButtons,
|
|
duration,
|
|
AnimationType.SCALE_AND_ALPHA);
|
|
}
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Repeat and shuffle
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
public void onRepeatClicked() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onRepeatClicked() called");
|
|
}
|
|
setRepeatMode(nextRepeatMode(getRepeatMode()));
|
|
}
|
|
|
|
public void onShuffleClicked() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onShuffleClicked() called");
|
|
}
|
|
|
|
if (exoPlayerIsNull()) {
|
|
return;
|
|
}
|
|
simpleExoPlayer.setShuffleModeEnabled(!simpleExoPlayer.getShuffleModeEnabled());
|
|
}
|
|
|
|
@RepeatMode
|
|
public int getRepeatMode() {
|
|
return exoPlayerIsNull() ? REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode();
|
|
}
|
|
|
|
private void setRepeatMode(@RepeatMode final int repeatMode) {
|
|
if (!exoPlayerIsNull()) {
|
|
simpleExoPlayer.setRepeatMode(repeatMode);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "ExoPlayer - onRepeatModeChanged() called with: "
|
|
+ "repeatMode = [" + repeatMode + "]");
|
|
}
|
|
setRepeatModeButton(binding.repeatButton, repeatMode);
|
|
onShuffleOrRepeatModeChanged();
|
|
}
|
|
|
|
@Override
|
|
public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "ExoPlayer - onShuffleModeEnabledChanged() called with: "
|
|
+ "mode = [" + shuffleModeEnabled + "]");
|
|
}
|
|
|
|
if (playQueue != null) {
|
|
if (shuffleModeEnabled) {
|
|
playQueue.shuffle();
|
|
} else {
|
|
playQueue.unshuffle();
|
|
}
|
|
}
|
|
|
|
setShuffleButton(binding.shuffleButton, shuffleModeEnabled);
|
|
onShuffleOrRepeatModeChanged();
|
|
}
|
|
|
|
private void onShuffleOrRepeatModeChanged() {
|
|
notifyPlaybackUpdateToListeners();
|
|
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
|
}
|
|
|
|
private void setRepeatModeButton(final ImageButton imageButton, final int repeatMode) {
|
|
switch (repeatMode) {
|
|
case REPEAT_MODE_OFF:
|
|
imageButton.setImageResource(R.drawable.exo_controls_repeat_off);
|
|
break;
|
|
case REPEAT_MODE_ONE:
|
|
imageButton.setImageResource(R.drawable.exo_controls_repeat_one);
|
|
break;
|
|
case REPEAT_MODE_ALL:
|
|
imageButton.setImageResource(R.drawable.exo_controls_repeat_all);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void setShuffleButton(final ImageButton button, final boolean shuffled) {
|
|
button.setImageAlpha(shuffled ? 255 : 77);
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Mute / Unmute
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
public void onMuteUnmuteButtonClicked() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onMuteUnmuteButtonClicked() called");
|
|
}
|
|
simpleExoPlayer.setVolume(isMuted() ? 1 : 0);
|
|
notifyPlaybackUpdateToListeners();
|
|
setMuteButton(binding.switchMute, isMuted());
|
|
}
|
|
|
|
boolean isMuted() {
|
|
return !exoPlayerIsNull() && simpleExoPlayer.getVolume() == 0;
|
|
}
|
|
|
|
private void setMuteButton(final ImageButton button, final boolean isMuted) {
|
|
button.setImageDrawable(AppCompatResources.getDrawable(context, isMuted
|
|
? R.drawable.ic_volume_off_white_24dp : R.drawable.ic_volume_up_white_24dp));
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// ExoPlayer listeners (that didn't fit in other categories)
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
@Override
|
|
public void onTimelineChanged(@NonNull final Timeline timeline, final int reason) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "ExoPlayer - onTimelineChanged() called with "
|
|
+ "timeline size = [" + timeline.getWindowCount() + "], "
|
|
+ "reason = [" + reason + "]");
|
|
}
|
|
|
|
maybeUpdateCurrentMetadata();
|
|
// force recreate notification to ensure seek bar is shown when preparation finishes
|
|
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true);
|
|
}
|
|
|
|
@Override
|
|
public void onTracksChanged(@NonNull final TrackGroupArray trackGroups,
|
|
@NonNull final TrackSelectionArray trackSelections) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "ExoPlayer - onTracksChanged(), "
|
|
+ "track group size = " + trackGroups.length);
|
|
}
|
|
maybeUpdateCurrentMetadata();
|
|
onTextTracksChanged();
|
|
}
|
|
|
|
@Override
|
|
public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playbackParameters) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "ExoPlayer - playbackParameters(), speed = [" + playbackParameters.speed
|
|
+ "], pitch = [" + playbackParameters.pitch + "]");
|
|
}
|
|
binding.playbackSpeed.setText(formatSpeed(playbackParameters.speed));
|
|
}
|
|
|
|
@Override
|
|
public void onPositionDiscontinuity(@DiscontinuityReason final int discontinuityReason) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with "
|
|
+ "discontinuityReason = [" + discontinuityReason + "]");
|
|
}
|
|
if (playQueue == null) {
|
|
return;
|
|
}
|
|
|
|
// Refresh the playback if there is a transition to the next video
|
|
final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
|
|
switch (discontinuityReason) {
|
|
case DISCONTINUITY_REASON_PERIOD_TRANSITION:
|
|
// When player is in single repeat mode and a period transition occurs,
|
|
// we need to register a view count here since no metadata has changed
|
|
if (getRepeatMode() == REPEAT_MODE_ONE && newWindowIndex == playQueue.getIndex()) {
|
|
registerStreamViewed();
|
|
break;
|
|
}
|
|
case DISCONTINUITY_REASON_SEEK:
|
|
case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
|
|
case DISCONTINUITY_REASON_INTERNAL:
|
|
if (playQueue.getIndex() != newWindowIndex) {
|
|
resetStreamProgressState(playQueue.getItem());
|
|
playQueue.setIndex(newWindowIndex);
|
|
}
|
|
break;
|
|
case DISCONTINUITY_REASON_AD_INSERTION:
|
|
break; // only makes Android Studio linter happy, as there are no ads
|
|
}
|
|
|
|
maybeUpdateCurrentMetadata();
|
|
}
|
|
|
|
@Override
|
|
public void onRenderedFirstFrame() {
|
|
//TODO check if this causes black screen when switching to fullscreen
|
|
animate(binding.surfaceForeground, false, DEFAULT_CONTROLS_DURATION);
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Errors
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
/**
|
|
* Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}.
|
|
* <p>There are multiple types of errors:</p>
|
|
* <ul>
|
|
* <li>{@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}</li>
|
|
* <li>{@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}:
|
|
* If a runtime error occurred, then we can try to recover it by restarting the playback
|
|
* after setting the timestamp recovery.</li>
|
|
* <li>{@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER}:
|
|
* If the renderer failed, treat the error as unrecoverable.</li>
|
|
* </ul>
|
|
*
|
|
* @see #processSourceError(IOException)
|
|
* @see com.google.android.exoplayer2.Player.EventListener#onPlayerError(ExoPlaybackException)
|
|
*/
|
|
@Override
|
|
public void onPlayerError(@NonNull final ExoPlaybackException error) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "ExoPlayer - onPlayerError() called with: " + "error = [" + error + "]");
|
|
}
|
|
if (errorToast != null) {
|
|
errorToast.cancel();
|
|
errorToast = null;
|
|
}
|
|
|
|
saveStreamProgressState();
|
|
|
|
switch (error.type) {
|
|
case ExoPlaybackException.TYPE_SOURCE:
|
|
processSourceError(error.getSourceException());
|
|
showStreamError(error);
|
|
break;
|
|
case ExoPlaybackException.TYPE_UNEXPECTED:
|
|
showRecoverableError(error);
|
|
setRecovery();
|
|
reloadPlayQueueManager();
|
|
break;
|
|
case ExoPlaybackException.TYPE_OUT_OF_MEMORY:
|
|
case ExoPlaybackException.TYPE_REMOTE:
|
|
case ExoPlaybackException.TYPE_RENDERER:
|
|
default:
|
|
showUnrecoverableError(error);
|
|
onPlaybackShutdown();
|
|
break;
|
|
}
|
|
|
|
if (fragmentListener != null) {
|
|
fragmentListener.onPlayerError(error);
|
|
}
|
|
}
|
|
|
|
private void processSourceError(final IOException error) {
|
|
if (exoPlayerIsNull() || playQueue == null) {
|
|
return;
|
|
}
|
|
setRecovery();
|
|
|
|
if (error instanceof BehindLiveWindowException) {
|
|
reloadPlayQueueManager();
|
|
} else {
|
|
playQueue.error();
|
|
}
|
|
}
|
|
|
|
private void showStreamError(final Exception exception) {
|
|
exception.printStackTrace();
|
|
|
|
if (errorToast == null) {
|
|
errorToast = Toast
|
|
.makeText(context, R.string.player_stream_failure, Toast.LENGTH_SHORT);
|
|
errorToast.show();
|
|
}
|
|
}
|
|
|
|
private void showRecoverableError(final Exception exception) {
|
|
exception.printStackTrace();
|
|
|
|
if (errorToast == null) {
|
|
errorToast = Toast
|
|
.makeText(context, R.string.player_recoverable_failure, Toast.LENGTH_SHORT);
|
|
errorToast.show();
|
|
}
|
|
}
|
|
|
|
private void showUnrecoverableError(final Exception exception) {
|
|
exception.printStackTrace();
|
|
|
|
if (errorToast != null) {
|
|
errorToast.cancel();
|
|
}
|
|
errorToast = Toast
|
|
.makeText(context, R.string.player_unrecoverable_failure, Toast.LENGTH_SHORT);
|
|
errorToast.show();
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Playback position and seek
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
@Override // own playback listener (this is a getter)
|
|
public boolean isApproachingPlaybackEdge(final long timeToEndMillis) {
|
|
// If live, then not near playback edge
|
|
// If not playing, then not approaching playback edge
|
|
if (exoPlayerIsNull() || isLive() || !isPlaying()) {
|
|
return false;
|
|
}
|
|
|
|
final long currentPositionMillis = simpleExoPlayer.getCurrentPosition();
|
|
final long currentDurationMillis = simpleExoPlayer.getDuration();
|
|
return currentDurationMillis - currentPositionMillis < timeToEndMillis;
|
|
}
|
|
|
|
/**
|
|
* Checks if the current playback is a livestream AND is playing at or beyond the live edge.
|
|
*
|
|
* @return whether the livestream is playing at or beyond the edge
|
|
*/
|
|
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
|
public boolean isLiveEdge() {
|
|
if (exoPlayerIsNull() || !isLive()) {
|
|
return false;
|
|
}
|
|
|
|
final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline();
|
|
final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
|
|
if (currentTimeline.isEmpty() || currentWindowIndex < 0
|
|
|| currentWindowIndex >= currentTimeline.getWindowCount()) {
|
|
return false;
|
|
}
|
|
|
|
final Timeline.Window timelineWindow = new Timeline.Window();
|
|
currentTimeline.getWindow(currentWindowIndex, timelineWindow);
|
|
return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition();
|
|
}
|
|
|
|
@Override // own playback listener
|
|
public void onPlaybackSynchronize(@NonNull final PlayQueueItem item) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "Playback - onPlaybackSynchronize() called with "
|
|
+ "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]");
|
|
}
|
|
if (exoPlayerIsNull() || playQueue == null) {
|
|
return;
|
|
}
|
|
|
|
final boolean onPlaybackInitial = currentItem == null;
|
|
final boolean hasPlayQueueItemChanged = currentItem != item;
|
|
|
|
final int currentPlayQueueIndex = playQueue.indexOf(item);
|
|
final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex();
|
|
final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount();
|
|
|
|
// If nothing to synchronize
|
|
if (!hasPlayQueueItemChanged) {
|
|
return;
|
|
}
|
|
currentItem = item;
|
|
|
|
// Check if on wrong window
|
|
if (currentPlayQueueIndex != playQueue.getIndex()) {
|
|
Log.e(TAG, "Playback - Play Queue may be desynchronized: item "
|
|
+ "index=[" + currentPlayQueueIndex + "], "
|
|
+ "queue index=[" + playQueue.getIndex() + "]");
|
|
|
|
// Check if bad seek position
|
|
} else if ((currentPlaylistSize > 0 && currentPlayQueueIndex >= currentPlaylistSize)
|
|
|| currentPlayQueueIndex < 0) {
|
|
Log.e(TAG, "Playback - Trying to seek to invalid "
|
|
+ "index=[" + currentPlayQueueIndex + "] with "
|
|
+ "playlist length=[" + currentPlaylistSize + "]");
|
|
|
|
} else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial
|
|
|| !isPlaying()) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "Playback - Rewinding to correct "
|
|
+ "index=[" + currentPlayQueueIndex + "], "
|
|
+ "from=[" + currentPlaylistIndex + "], "
|
|
+ "size=[" + currentPlaylistSize + "].");
|
|
}
|
|
|
|
if (item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) {
|
|
simpleExoPlayer.seekTo(currentPlayQueueIndex, item.getRecoveryPosition());
|
|
playQueue.unsetRecovery(currentPlayQueueIndex);
|
|
} else {
|
|
simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void maybeCorrectSeekPosition() {
|
|
if (playQueue == null || exoPlayerIsNull() || currentMetadata == null) {
|
|
return;
|
|
}
|
|
|
|
final PlayQueueItem currentSourceItem = playQueue.getItem();
|
|
if (currentSourceItem == null) {
|
|
return;
|
|
}
|
|
|
|
final StreamInfo currentInfo = currentMetadata.getMetadata();
|
|
final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000;
|
|
if (presetStartPositionMillis > 0L) {
|
|
// Has another start position?
|
|
if (DEBUG) {
|
|
Log.d(TAG, "Playback - Seeking to preset start "
|
|
+ "position=[" + presetStartPositionMillis + "]");
|
|
}
|
|
seekTo(presetStartPositionMillis);
|
|
}
|
|
}
|
|
|
|
public void seekTo(final long positionMillis) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]");
|
|
}
|
|
if (!exoPlayerIsNull()) {
|
|
// prevent invalid positions when fast-forwarding/-rewinding
|
|
long normalizedPositionMillis = positionMillis;
|
|
if (normalizedPositionMillis < 0) {
|
|
normalizedPositionMillis = 0;
|
|
} else if (normalizedPositionMillis > simpleExoPlayer.getDuration()) {
|
|
normalizedPositionMillis = simpleExoPlayer.getDuration();
|
|
}
|
|
|
|
simpleExoPlayer.seekTo(normalizedPositionMillis);
|
|
}
|
|
}
|
|
|
|
private void seekBy(final long offsetMillis) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "seekBy() called with: offsetMillis = [" + offsetMillis + "]");
|
|
}
|
|
seekTo(simpleExoPlayer.getCurrentPosition() + offsetMillis);
|
|
}
|
|
|
|
public void seekToDefault() {
|
|
if (!exoPlayerIsNull()) {
|
|
simpleExoPlayer.seekToDefaultPosition();
|
|
}
|
|
}
|
|
|
|
@Override // exoplayer override
|
|
public void onSeekProcessed() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "ExoPlayer - onSeekProcessed() called");
|
|
}
|
|
if (isPrepared) {
|
|
saveStreamProgressState();
|
|
}
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Player actions (play, pause, previous, fast-forward, ...)
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
public void play() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "play() called");
|
|
}
|
|
if (audioReactor == null || playQueue == null || exoPlayerIsNull()) {
|
|
return;
|
|
}
|
|
|
|
audioReactor.requestAudioFocus();
|
|
|
|
if (currentState == STATE_COMPLETED) {
|
|
if (playQueue.getIndex() == 0) {
|
|
seekToDefault();
|
|
} else {
|
|
playQueue.setIndex(0);
|
|
}
|
|
}
|
|
|
|
simpleExoPlayer.setPlayWhenReady(true);
|
|
saveStreamProgressState();
|
|
}
|
|
|
|
public void pause() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "pause() called");
|
|
}
|
|
if (audioReactor == null || exoPlayerIsNull()) {
|
|
return;
|
|
}
|
|
|
|
audioReactor.abandonAudioFocus();
|
|
simpleExoPlayer.setPlayWhenReady(false);
|
|
saveStreamProgressState();
|
|
}
|
|
|
|
public void playPause() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onPlayPause() called");
|
|
}
|
|
|
|
if (isPlaying()) {
|
|
pause();
|
|
} else {
|
|
play();
|
|
}
|
|
}
|
|
|
|
public void playPrevious() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onPlayPrevious() called");
|
|
}
|
|
if (exoPlayerIsNull() || playQueue == null) {
|
|
return;
|
|
}
|
|
|
|
/* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT_MILLIS milliseconds,
|
|
* restart current track. Also restart the track if the current track
|
|
* is the first in a queue.*/
|
|
if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT_MILLIS
|
|
|| playQueue.getIndex() == 0) {
|
|
seekToDefault();
|
|
playQueue.offsetIndex(0);
|
|
} else {
|
|
saveStreamProgressState();
|
|
playQueue.offsetIndex(-1);
|
|
}
|
|
triggerProgressUpdate();
|
|
}
|
|
|
|
public void playNext() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onPlayNext() called");
|
|
}
|
|
if (playQueue == null) {
|
|
return;
|
|
}
|
|
|
|
saveStreamProgressState();
|
|
playQueue.offsetIndex(+1);
|
|
triggerProgressUpdate();
|
|
}
|
|
|
|
public void fastForward() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "fastRewind() called");
|
|
}
|
|
seekBy(retrieveSeekDurationFromPreferences(this));
|
|
triggerProgressUpdate();
|
|
showAndAnimateControl(R.drawable.ic_fast_forward_white_24dp, true);
|
|
}
|
|
|
|
public void fastRewind() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "fastRewind() called");
|
|
}
|
|
seekBy(-retrieveSeekDurationFromPreferences(this));
|
|
triggerProgressUpdate();
|
|
showAndAnimateControl(R.drawable.ic_fast_rewind_white_24dp, true);
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// StreamInfo history: views and progress
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
private void registerStreamViewed() {
|
|
if (currentMetadata != null) {
|
|
databaseUpdateDisposable.add(recordManager.onViewed(currentMetadata.getMetadata())
|
|
.onErrorComplete().subscribe());
|
|
}
|
|
}
|
|
|
|
private void saveStreamProgressState(final StreamInfo info, final long progress) {
|
|
if (info == null) {
|
|
return;
|
|
}
|
|
if (DEBUG) {
|
|
Log.d(TAG, "saveStreamProgressState() called");
|
|
}
|
|
if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
|
|
final Disposable stateSaver = recordManager.saveStreamState(info, progress)
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
.doOnError((e) -> {
|
|
if (DEBUG) {
|
|
e.printStackTrace();
|
|
}
|
|
})
|
|
.onErrorComplete()
|
|
.subscribe();
|
|
databaseUpdateDisposable.add(stateSaver);
|
|
}
|
|
}
|
|
|
|
private void resetStreamProgressState(final PlayQueueItem queueItem) {
|
|
if (queueItem == null) {
|
|
return;
|
|
}
|
|
if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
|
|
final Disposable stateSaver = queueItem.getStream()
|
|
.flatMapCompletable(info -> recordManager.saveStreamState(info, 0))
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
.doOnError((e) -> {
|
|
if (DEBUG) {
|
|
e.printStackTrace();
|
|
}
|
|
})
|
|
.onErrorComplete()
|
|
.subscribe();
|
|
databaseUpdateDisposable.add(stateSaver);
|
|
}
|
|
}
|
|
|
|
private void resetStreamProgressState(final StreamInfo info) {
|
|
saveStreamProgressState(info, 0);
|
|
}
|
|
|
|
public void saveStreamProgressState() {
|
|
if (exoPlayerIsNull() || currentMetadata == null) {
|
|
return;
|
|
}
|
|
final StreamInfo currentInfo = currentMetadata.getMetadata();
|
|
if (playQueue != null) {
|
|
// Save current position. It will help to restore this position once a user
|
|
// wants to play prev or next stream from the queue
|
|
playQueue.setRecovery(playQueue.getIndex(), simpleExoPlayer.getContentPosition());
|
|
}
|
|
saveStreamProgressState(currentInfo, simpleExoPlayer.getCurrentPosition());
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Metadata
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
private void onMetadataChanged(@NonNull final MediaSourceTag tag) {
|
|
final StreamInfo info = tag.getMetadata();
|
|
if (DEBUG) {
|
|
Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName());
|
|
}
|
|
|
|
initThumbnail(info.getThumbnailUrl());
|
|
registerStreamViewed();
|
|
updateStreamRelatedViews();
|
|
showHideKodiButton();
|
|
|
|
binding.titleTextView.setText(tag.getMetadata().getName());
|
|
binding.channelTextView.setText(tag.getMetadata().getUploaderName());
|
|
|
|
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
|
notifyMetadataUpdateToListeners();
|
|
|
|
if (areSegmentsVisible) {
|
|
if (segmentAdapter.setItems(info)) {
|
|
final int adapterPosition = getNearestStreamSegmentPosition(
|
|
simpleExoPlayer.getCurrentPosition());
|
|
segmentAdapter.selectSegmentAt(adapterPosition);
|
|
binding.itemsList.scrollToPosition(adapterPosition);
|
|
} else {
|
|
closeItemsList();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void maybeUpdateCurrentMetadata() {
|
|
if (exoPlayerIsNull()) {
|
|
return;
|
|
}
|
|
|
|
final MediaSourceTag metadata;
|
|
try {
|
|
metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag();
|
|
} catch (IndexOutOfBoundsException | ClassCastException error) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "Could not update metadata: " + error.getMessage());
|
|
error.printStackTrace();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (metadata == null) {
|
|
return;
|
|
}
|
|
maybeAutoQueueNextStream(metadata);
|
|
|
|
if (currentMetadata == metadata) {
|
|
return;
|
|
}
|
|
currentMetadata = metadata;
|
|
onMetadataChanged(metadata);
|
|
}
|
|
|
|
@NonNull
|
|
private String getVideoUrl() {
|
|
return currentMetadata == null
|
|
? context.getString(R.string.unknown_content)
|
|
: currentMetadata.getMetadata().getUrl();
|
|
}
|
|
|
|
@NonNull
|
|
public String getVideoTitle() {
|
|
return currentMetadata == null
|
|
? context.getString(R.string.unknown_content)
|
|
: currentMetadata.getMetadata().getName();
|
|
}
|
|
|
|
@NonNull
|
|
public String getUploaderName() {
|
|
return currentMetadata == null
|
|
? context.getString(R.string.unknown_content)
|
|
: currentMetadata.getMetadata().getUploaderName();
|
|
}
|
|
|
|
@Nullable
|
|
public Bitmap getThumbnail() {
|
|
return currentThumbnail == null
|
|
? BitmapFactory.decodeResource(context.getResources(), R.drawable.dummy_thumbnail)
|
|
: currentThumbnail;
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Play queue, segments and streams
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag metadata) {
|
|
if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1
|
|
|| getRepeatMode() != REPEAT_MODE_OFF
|
|
|| !PlayerHelper.isAutoQueueEnabled(context)) {
|
|
return;
|
|
}
|
|
// auto queue when starting playback on the last item when not repeating
|
|
final PlayQueue autoQueue = PlayerHelper.autoQueueOf(metadata.getMetadata(),
|
|
playQueue.getStreams());
|
|
if (autoQueue != null) {
|
|
playQueue.append(autoQueue.getStreams());
|
|
}
|
|
}
|
|
|
|
public void selectQueueItem(final PlayQueueItem item) {
|
|
if (playQueue == null || exoPlayerIsNull()) {
|
|
return;
|
|
}
|
|
|
|
final int index = playQueue.indexOf(item);
|
|
if (index == -1) {
|
|
return;
|
|
}
|
|
|
|
if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) {
|
|
seekToDefault();
|
|
} else {
|
|
saveStreamProgressState();
|
|
}
|
|
playQueue.setIndex(index);
|
|
}
|
|
|
|
@Override
|
|
public void onPlayQueueEdited() {
|
|
notifyPlaybackUpdateToListeners();
|
|
showOrHideButtons();
|
|
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
|
}
|
|
|
|
private void onQueueClicked() {
|
|
isQueueVisible = true;
|
|
|
|
hideSystemUIIfNeeded();
|
|
buildQueue();
|
|
|
|
binding.itemsListHeaderTitle.setVisibility(View.GONE);
|
|
binding.shuffleButton.setVisibility(View.VISIBLE);
|
|
binding.repeatButton.setVisibility(View.VISIBLE);
|
|
|
|
hideControls(0, 0);
|
|
binding.itemsListPanel.requestFocus();
|
|
animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION,
|
|
AnimationType.SLIDE_AND_ALPHA);
|
|
|
|
binding.itemsList.scrollToPosition(playQueue.getIndex());
|
|
}
|
|
|
|
private void buildQueue() {
|
|
binding.itemsList.setAdapter(playQueueAdapter);
|
|
binding.itemsList.setClickable(true);
|
|
binding.itemsList.setLongClickable(true);
|
|
|
|
binding.itemsList.clearOnScrollListeners();
|
|
binding.itemsList.addOnScrollListener(getQueueScrollListener());
|
|
|
|
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
|
itemTouchHelper.attachToRecyclerView(binding.itemsList);
|
|
|
|
playQueueAdapter.setSelectedListener(getOnSelectedListener());
|
|
|
|
binding.itemsListClose.setOnClickListener(view -> closeItemsList());
|
|
}
|
|
|
|
private void onSegmentsClicked() {
|
|
areSegmentsVisible = true;
|
|
|
|
hideSystemUIIfNeeded();
|
|
buildSegments();
|
|
|
|
binding.itemsListHeaderTitle.setVisibility(View.VISIBLE);
|
|
binding.shuffleButton.setVisibility(View.GONE);
|
|
binding.repeatButton.setVisibility(View.GONE);
|
|
|
|
hideControls(0, 0);
|
|
binding.itemsListPanel.requestFocus();
|
|
animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION,
|
|
AnimationType.SLIDE_AND_ALPHA);
|
|
|
|
final int adapterPosition = getNearestStreamSegmentPosition(simpleExoPlayer
|
|
.getCurrentPosition());
|
|
segmentAdapter.selectSegmentAt(adapterPosition);
|
|
binding.itemsList.scrollToPosition(adapterPosition);
|
|
}
|
|
|
|
private void buildSegments() {
|
|
binding.itemsList.setAdapter(segmentAdapter);
|
|
binding.itemsList.setClickable(true);
|
|
binding.itemsList.setLongClickable(false);
|
|
|
|
binding.itemsList.clearOnScrollListeners();
|
|
if (itemTouchHelper != null) {
|
|
itemTouchHelper.attachToRecyclerView(null);
|
|
}
|
|
|
|
if (currentMetadata != null) {
|
|
segmentAdapter.setItems(currentMetadata.getMetadata());
|
|
}
|
|
|
|
binding.shuffleButton.setVisibility(View.GONE);
|
|
binding.repeatButton.setVisibility(View.GONE);
|
|
binding.itemsListClose.setOnClickListener(view -> closeItemsList());
|
|
}
|
|
|
|
public void closeItemsList() {
|
|
if (isQueueVisible || areSegmentsVisible) {
|
|
isQueueVisible = false;
|
|
areSegmentsVisible = false;
|
|
|
|
if (itemTouchHelper != null) {
|
|
itemTouchHelper.attachToRecyclerView(null);
|
|
}
|
|
|
|
animate(binding.itemsListPanel, false, DEFAULT_CONTROLS_DURATION,
|
|
AnimationType.SLIDE_AND_ALPHA, 0, () -> {
|
|
// Even when queueLayout is GONE it receives touch events
|
|
// and ruins normal behavior of the app. This line fixes it
|
|
binding.itemsListPanel.setTranslationY(
|
|
-binding.itemsListPanel.getHeight() * 5);
|
|
});
|
|
binding.playPauseButton.requestFocus();
|
|
}
|
|
}
|
|
|
|
private OnScrollBelowItemsListener getQueueScrollListener() {
|
|
return new OnScrollBelowItemsListener() {
|
|
@Override
|
|
public void onScrolledDown(final RecyclerView recyclerView) {
|
|
if (playQueue != null && !playQueue.isComplete()) {
|
|
playQueue.fetch();
|
|
} else if (binding != null) {
|
|
binding.itemsList.clearOnScrollListeners();
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() {
|
|
return (item, seconds) -> {
|
|
segmentAdapter.selectSegment(item);
|
|
seekTo(seconds * 1000);
|
|
triggerProgressUpdate();
|
|
};
|
|
}
|
|
|
|
private int getNearestStreamSegmentPosition(final long playbackPosition) {
|
|
int nearestPosition = 0;
|
|
final List<StreamSegment> segments = currentMetadata.getMetadata().getStreamSegments();
|
|
|
|
for (int i = 0; i < segments.size(); i++) {
|
|
if (segments.get(i).getStartTimeSeconds() * 1000 > playbackPosition) {
|
|
break;
|
|
}
|
|
nearestPosition++;
|
|
}
|
|
return Math.max(0, nearestPosition - 1);
|
|
}
|
|
|
|
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
|
return new PlayQueueItemTouchCallback() {
|
|
@Override
|
|
public void onMove(final int sourceIndex, final int targetIndex) {
|
|
if (playQueue != null) {
|
|
playQueue.move(sourceIndex, targetIndex);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onSwiped(final int index) {
|
|
if (index != -1) {
|
|
playQueue.remove(index);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() {
|
|
return new PlayQueueItemBuilder.OnSelectedListener() {
|
|
@Override
|
|
public void selected(final PlayQueueItem item, final View view) {
|
|
selectQueueItem(item);
|
|
}
|
|
|
|
@Override
|
|
public void held(final PlayQueueItem item, final View view) {
|
|
final int index = playQueue.indexOf(item);
|
|
if (index != -1) {
|
|
playQueue.remove(index);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onStartDrag(final PlayQueueItemHolder viewHolder) {
|
|
if (itemTouchHelper != null) {
|
|
itemTouchHelper.startDrag(viewHolder);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
@Override // own playback listener
|
|
@Nullable
|
|
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
|
|
return (isAudioOnly ? audioResolver : videoResolver).resolve(info);
|
|
}
|
|
|
|
public void disablePreloadingOfCurrentTrack() {
|
|
loadController.disablePreloadingOfCurrentTrack();
|
|
}
|
|
|
|
@Nullable
|
|
public VideoStream getSelectedVideoStream() {
|
|
return (selectedStreamIndex >= 0 && availableStreams != null
|
|
&& availableStreams.size() > selectedStreamIndex)
|
|
? availableStreams.get(selectedStreamIndex) : null;
|
|
}
|
|
|
|
private void updateStreamRelatedViews() {
|
|
if (currentMetadata == null) {
|
|
return;
|
|
}
|
|
final StreamInfo info = currentMetadata.getMetadata();
|
|
|
|
binding.qualityTextView.setVisibility(View.GONE);
|
|
binding.playbackSpeed.setVisibility(View.GONE);
|
|
|
|
binding.playbackEndTime.setVisibility(View.GONE);
|
|
binding.playbackLiveSync.setVisibility(View.GONE);
|
|
|
|
switch (info.getStreamType()) {
|
|
case AUDIO_STREAM:
|
|
binding.surfaceView.setVisibility(View.GONE);
|
|
binding.endScreen.setVisibility(View.VISIBLE);
|
|
binding.playbackEndTime.setVisibility(View.VISIBLE);
|
|
break;
|
|
|
|
case AUDIO_LIVE_STREAM:
|
|
binding.surfaceView.setVisibility(View.GONE);
|
|
binding.endScreen.setVisibility(View.VISIBLE);
|
|
binding.playbackLiveSync.setVisibility(View.VISIBLE);
|
|
break;
|
|
|
|
case LIVE_STREAM:
|
|
binding.surfaceView.setVisibility(View.VISIBLE);
|
|
binding.endScreen.setVisibility(View.GONE);
|
|
binding.playbackLiveSync.setVisibility(View.VISIBLE);
|
|
break;
|
|
|
|
case VIDEO_STREAM:
|
|
if (info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) {
|
|
break;
|
|
}
|
|
|
|
availableStreams = currentMetadata.getSortedAvailableVideoStreams();
|
|
selectedStreamIndex = currentMetadata.getSelectedVideoStreamIndex();
|
|
buildQualityMenu();
|
|
|
|
binding.qualityTextView.setVisibility(View.VISIBLE);
|
|
binding.surfaceView.setVisibility(View.VISIBLE);
|
|
default:
|
|
binding.endScreen.setVisibility(View.GONE);
|
|
binding.playbackEndTime.setVisibility(View.VISIBLE);
|
|
break;
|
|
}
|
|
|
|
buildPlaybackSpeedMenu();
|
|
binding.playbackSpeed.setVisibility(View.VISIBLE);
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Popup menus ("popup" means that they pop up, not that they belong to the popup player)
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
private void buildQualityMenu() {
|
|
if (qualityPopupMenu == null) {
|
|
return;
|
|
}
|
|
qualityPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_QUALITY);
|
|
|
|
for (int i = 0; i < availableStreams.size(); i++) {
|
|
final VideoStream videoStream = availableStreams.get(i);
|
|
qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat
|
|
.getNameById(videoStream.getFormatId()) + " " + videoStream.resolution);
|
|
}
|
|
if (getSelectedVideoStream() != null) {
|
|
binding.qualityTextView.setText(getSelectedVideoStream().resolution);
|
|
}
|
|
qualityPopupMenu.setOnMenuItemClickListener(this);
|
|
qualityPopupMenu.setOnDismissListener(this);
|
|
}
|
|
|
|
private void buildPlaybackSpeedMenu() {
|
|
if (playbackSpeedPopupMenu == null) {
|
|
return;
|
|
}
|
|
playbackSpeedPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_PLAYBACK_SPEED);
|
|
|
|
for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) {
|
|
playbackSpeedPopupMenu.getMenu().add(POPUP_MENU_ID_PLAYBACK_SPEED, i, Menu.NONE,
|
|
formatSpeed(PLAYBACK_SPEEDS[i]));
|
|
}
|
|
binding.playbackSpeed.setText(formatSpeed(getPlaybackSpeed()));
|
|
playbackSpeedPopupMenu.setOnMenuItemClickListener(this);
|
|
playbackSpeedPopupMenu.setOnDismissListener(this);
|
|
}
|
|
|
|
private void buildCaptionMenu(final List<String> availableLanguages) {
|
|
if (captionPopupMenu == null) {
|
|
return;
|
|
}
|
|
captionPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_CAPTION);
|
|
|
|
final String userPreferredLanguage =
|
|
prefs.getString(context.getString(R.string.caption_user_set_key), null);
|
|
/*
|
|
* only search for autogenerated cc as fallback
|
|
* if "(auto-generated)" was not already selected
|
|
* 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("(");
|
|
|
|
// Add option for turning off caption
|
|
final MenuItem captionOffItem = captionPopupMenu.getMenu().add(POPUP_MENU_ID_CAPTION,
|
|
0, Menu.NONE, R.string.caption_none);
|
|
captionOffItem.setOnMenuItemClickListener(menuItem -> {
|
|
final int textRendererIndex = getCaptionRendererIndex();
|
|
if (textRendererIndex != RENDERER_UNAVAILABLE) {
|
|
trackSelector.setParameters(trackSelector.buildUponParameters()
|
|
.setRendererDisabled(textRendererIndex, true));
|
|
}
|
|
prefs.edit().remove(context.getString(R.string.caption_user_set_key)).apply();
|
|
return true;
|
|
});
|
|
|
|
// Add all available captions
|
|
for (int i = 0; i < availableLanguages.size(); i++) {
|
|
final String captionLanguage = availableLanguages.get(i);
|
|
final MenuItem captionItem = captionPopupMenu.getMenu().add(POPUP_MENU_ID_CAPTION,
|
|
i + 1, Menu.NONE, captionLanguage);
|
|
captionItem.setOnMenuItemClickListener(menuItem -> {
|
|
final int textRendererIndex = getCaptionRendererIndex();
|
|
if (textRendererIndex != RENDERER_UNAVAILABLE) {
|
|
trackSelector.setPreferredTextLanguage(captionLanguage);
|
|
trackSelector.setParameters(trackSelector.buildUponParameters()
|
|
.setRendererDisabled(textRendererIndex, false));
|
|
prefs.edit().putString(context.getString(R.string.caption_user_set_key),
|
|
captionLanguage).apply();
|
|
}
|
|
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('(')))))) {
|
|
final int textRendererIndex = getCaptionRendererIndex();
|
|
if (textRendererIndex != RENDERER_UNAVAILABLE) {
|
|
trackSelector.setPreferredTextLanguage(captionLanguage);
|
|
trackSelector.setParameters(trackSelector.buildUponParameters()
|
|
.setRendererDisabled(textRendererIndex, false));
|
|
}
|
|
searchForAutogenerated = false;
|
|
}
|
|
}
|
|
captionPopupMenu.setOnDismissListener(this);
|
|
}
|
|
|
|
/**
|
|
* Called when an item of the quality selector or the playback speed selector is selected.
|
|
*/
|
|
@Override
|
|
public boolean onMenuItemClick(final MenuItem menuItem) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onMenuItemClick() called with: "
|
|
+ "menuItem = [" + menuItem + "], "
|
|
+ "menuItem.getItemId = [" + menuItem.getItemId() + "]");
|
|
}
|
|
|
|
if (menuItem.getGroupId() == POPUP_MENU_ID_QUALITY) {
|
|
final int menuItemIndex = menuItem.getItemId();
|
|
if (selectedStreamIndex == menuItemIndex || availableStreams == null
|
|
|| availableStreams.size() <= menuItemIndex) {
|
|
return true;
|
|
}
|
|
|
|
saveStreamProgressState(); //TODO added, check if good
|
|
final String newResolution = availableStreams.get(menuItemIndex).resolution;
|
|
setRecovery();
|
|
setPlaybackQuality(newResolution);
|
|
reloadPlayQueueManager();
|
|
|
|
binding.qualityTextView.setText(menuItem.getTitle());
|
|
return true;
|
|
} else if (menuItem.getGroupId() == POPUP_MENU_ID_PLAYBACK_SPEED) {
|
|
final int speedIndex = menuItem.getItemId();
|
|
final float speed = PLAYBACK_SPEEDS[speedIndex];
|
|
|
|
setPlaybackSpeed(speed);
|
|
binding.playbackSpeed.setText(formatSpeed(speed));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Called when some popup menu is dismissed.
|
|
*/
|
|
@Override
|
|
public void onDismiss(final PopupMenu menu) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]");
|
|
}
|
|
isSomePopupMenuVisible = false; //TODO check if this works
|
|
if (getSelectedVideoStream() != null) {
|
|
binding.qualityTextView.setText(getSelectedVideoStream().resolution);
|
|
}
|
|
if (isPlaying()) {
|
|
hideControls(DEFAULT_CONTROLS_DURATION, 0);
|
|
hideSystemUIIfNeeded();
|
|
}
|
|
}
|
|
|
|
private void onQualitySelectorClicked() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onQualitySelectorClicked() called");
|
|
}
|
|
qualityPopupMenu.show();
|
|
isSomePopupMenuVisible = true;
|
|
|
|
final VideoStream videoStream = getSelectedVideoStream();
|
|
if (videoStream != null) {
|
|
final String qualityText = MediaFormat.getNameById(videoStream.getFormatId()) + " "
|
|
+ videoStream.resolution;
|
|
binding.qualityTextView.setText(qualityText);
|
|
}
|
|
|
|
saveWasPlaying();
|
|
}
|
|
|
|
private void onPlaybackSpeedClicked() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onPlaybackSpeedClicked() called");
|
|
}
|
|
if (videoPlayerSelected()) {
|
|
PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch(),
|
|
getPlaybackSkipSilence(), this::setPlaybackParameters)
|
|
.show(getParentActivity().getSupportFragmentManager(), null);
|
|
} else {
|
|
playbackSpeedPopupMenu.show();
|
|
isSomePopupMenuVisible = true;
|
|
}
|
|
}
|
|
|
|
private void onCaptionClicked() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onCaptionClicked() called");
|
|
}
|
|
captionPopupMenu.show();
|
|
isSomePopupMenuVisible = true;
|
|
}
|
|
|
|
private void setPlaybackQuality(final String quality) {
|
|
videoResolver.setPlaybackQuality(quality);
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Captions (text tracks)
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
private void setupSubtitleView() {
|
|
final float captionScale = PlayerHelper.getCaptionScale(context);
|
|
final CaptionStyleCompat captionStyle = PlayerHelper.getCaptionStyle(context);
|
|
if (popupPlayerSelected()) {
|
|
final float captionRatio = (captionScale - 1.0f) / 5.0f + 1.0f;
|
|
binding.subtitleView.setFractionalTextSize(
|
|
SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio);
|
|
} else {
|
|
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
|
final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels);
|
|
final float captionRatioInverse = 20f + 4f * (1.0f - captionScale);
|
|
binding.subtitleView.setFixedTextSize(
|
|
TypedValue.COMPLEX_UNIT_PX, (float) minimumLength / captionRatioInverse);
|
|
}
|
|
binding.subtitleView.setApplyEmbeddedStyles(captionStyle == CaptionStyleCompat.DEFAULT);
|
|
binding.subtitleView.setStyle(captionStyle);
|
|
}
|
|
|
|
private void onTextTracksChanged() {
|
|
final int textRenderer = getCaptionRendererIndex();
|
|
|
|
if (binding == null) {
|
|
return;
|
|
}
|
|
if (trackSelector.getCurrentMappedTrackInfo() == null
|
|
|| textRenderer == RENDERER_UNAVAILABLE) {
|
|
binding.captionTextView.setVisibility(View.GONE);
|
|
return;
|
|
}
|
|
|
|
final TrackGroupArray textTracks = trackSelector.getCurrentMappedTrackInfo()
|
|
.getTrackGroups(textRenderer);
|
|
|
|
// Extract all loaded languages
|
|
final List<String> availableLanguages = new ArrayList<>(textTracks.length);
|
|
for (int i = 0; i < textTracks.length; i++) {
|
|
final TrackGroup textTrack = textTracks.get(i);
|
|
if (textTrack.length > 0 && textTrack.getFormat(0) != null) {
|
|
availableLanguages.add(textTrack.getFormat(0).language);
|
|
}
|
|
}
|
|
|
|
// Normalize mismatching language strings
|
|
final String preferredLanguage = trackSelector.getPreferredTextLanguage();
|
|
// Build UI
|
|
buildCaptionMenu(availableLanguages);
|
|
if (trackSelector.getParameters().getRendererDisabled(textRenderer)
|
|
|| preferredLanguage == null || (!availableLanguages.contains(preferredLanguage)
|
|
&& !containsCaseInsensitive(availableLanguages, preferredLanguage))) {
|
|
binding.captionTextView.setText(R.string.caption_none);
|
|
} else {
|
|
binding.captionTextView.setText(preferredLanguage);
|
|
}
|
|
binding.captionTextView.setVisibility(
|
|
availableLanguages.isEmpty() ? View.GONE : View.VISIBLE);
|
|
}
|
|
|
|
private int getCaptionRendererIndex() {
|
|
if (exoPlayerIsNull()) {
|
|
return RENDERER_UNAVAILABLE;
|
|
}
|
|
|
|
for (int t = 0; t < simpleExoPlayer.getRendererCount(); t++) {
|
|
if (simpleExoPlayer.getRendererType(t) == C.TRACK_TYPE_TEXT) {
|
|
return t;
|
|
}
|
|
}
|
|
|
|
return RENDERER_UNAVAILABLE;
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Click listeners
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
@Override
|
|
public void onClick(final View v) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
|
}
|
|
if (v.getId() == binding.qualityTextView.getId()) {
|
|
onQualitySelectorClicked();
|
|
} else if (v.getId() == binding.playbackSpeed.getId()) {
|
|
onPlaybackSpeedClicked();
|
|
} else if (v.getId() == binding.resizeTextView.getId()) {
|
|
onResizeClicked();
|
|
} else if (v.getId() == binding.captionTextView.getId()) {
|
|
onCaptionClicked();
|
|
} else if (v.getId() == binding.playbackLiveSync.getId()) {
|
|
seekToDefault();
|
|
} else if (v.getId() == binding.playPauseButton.getId()) {
|
|
playPause();
|
|
} else if (v.getId() == binding.playPreviousButton.getId()) {
|
|
playPrevious();
|
|
} else if (v.getId() == binding.playNextButton.getId()) {
|
|
playNext();
|
|
} else if (v.getId() == binding.queueButton.getId()) {
|
|
onQueueClicked();
|
|
return;
|
|
} else if (v.getId() == binding.segmentsButton.getId()) {
|
|
onSegmentsClicked();
|
|
return;
|
|
} else if (v.getId() == binding.repeatButton.getId()) {
|
|
onRepeatClicked();
|
|
return;
|
|
} else if (v.getId() == binding.shuffleButton.getId()) {
|
|
onShuffleClicked();
|
|
return;
|
|
} else if (v.getId() == binding.moreOptionsButton.getId()) {
|
|
onMoreOptionsClicked();
|
|
} else if (v.getId() == binding.share.getId()) {
|
|
onShareClicked();
|
|
} else if (v.getId() == binding.playWithKodi.getId()) {
|
|
onPlayWithKodiClicked();
|
|
} else if (v.getId() == binding.openInBrowser.getId()) {
|
|
onOpenInBrowserClicked();
|
|
} else if (v.getId() == binding.fullScreenButton.getId()) {
|
|
setRecovery();
|
|
NavigationHelper.playOnMainPlayer(context, playQueue, true);
|
|
return;
|
|
} else if (v.getId() == binding.screenRotationButton.getId()) {
|
|
// Only if it's not a vertical video or vertical video but in landscape with locked
|
|
// orientation a screen orientation can be changed automatically
|
|
if (!isVerticalVideo
|
|
|| (service.isLandscape() && globalScreenOrientationLocked(context))) {
|
|
fragmentListener.onScreenRotationButtonClicked();
|
|
} else {
|
|
toggleFullscreen();
|
|
}
|
|
} else if (v.getId() == binding.switchMute.getId()) {
|
|
onMuteUnmuteButtonClicked();
|
|
} else if (v.getId() == binding.playerCloseButton.getId()) {
|
|
context.sendBroadcast(new Intent(VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER));
|
|
}
|
|
|
|
if (currentState != STATE_COMPLETED) {
|
|
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
|
showHideShadow(true, DEFAULT_CONTROLS_DURATION);
|
|
animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION,
|
|
AnimationType.ALPHA, 0, () -> {
|
|
if (currentState == STATE_PLAYING && !isSomePopupMenuVisible) {
|
|
if (v.getId() == binding.playPauseButton.getId()
|
|
// Hide controls in fullscreen immediately
|
|
|| (v.getId() == binding.screenRotationButton.getId()
|
|
&& isFullscreen)) {
|
|
hideControls(0, 0);
|
|
} else {
|
|
hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onLongClick(final View v) {
|
|
if (v.getId() == binding.moreOptionsButton.getId() && isFullscreen) {
|
|
fragmentListener.onMoreOptionsLongClicked();
|
|
hideControls(0, 0);
|
|
hideSystemUIIfNeeded();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public boolean onKeyDown(final int keyCode) {
|
|
switch (keyCode) {
|
|
default:
|
|
break;
|
|
case KeyEvent.KEYCODE_SPACE:
|
|
if (isFullscreen) {
|
|
playPause();
|
|
}
|
|
break;
|
|
case KeyEvent.KEYCODE_BACK:
|
|
if (DeviceUtils.isTv(context) && isControlsVisible()) {
|
|
hideControls(0, 0);
|
|
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 (binding.getRoot().hasFocus() && !binding.playbackControlRoot.hasFocus()) {
|
|
// do not interfere with focus in playlist etc.
|
|
return false;
|
|
}
|
|
|
|
if (currentState == Player.STATE_BLOCKED) {
|
|
return true;
|
|
}
|
|
|
|
if (!isControlsVisible()) {
|
|
if (!isQueueVisible) {
|
|
binding.playPauseButton.requestFocus();
|
|
}
|
|
showControlsThenHide();
|
|
showSystemUIPartially();
|
|
return true;
|
|
} else {
|
|
hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME);
|
|
}
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void onMoreOptionsClicked() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onMoreOptionsClicked() called");
|
|
}
|
|
|
|
final boolean isMoreControlsVisible =
|
|
binding.secondaryControls.getVisibility() == View.VISIBLE;
|
|
|
|
animateRotation(binding.moreOptionsButton, DEFAULT_CONTROLS_DURATION,
|
|
isMoreControlsVisible ? 0 : 180);
|
|
animate(binding.secondaryControls, !isMoreControlsVisible, DEFAULT_CONTROLS_DURATION,
|
|
AnimationType.SLIDE_AND_ALPHA, 0, () -> {
|
|
// Fix for a ripple effect on background drawable.
|
|
// When view returns from GONE state it takes more milliseconds than returning
|
|
// from INVISIBLE state. And the delay makes ripple background end to fast
|
|
if (isMoreControlsVisible) {
|
|
binding.secondaryControls.setVisibility(View.INVISIBLE);
|
|
}
|
|
});
|
|
showControls(DEFAULT_CONTROLS_DURATION);
|
|
}
|
|
|
|
private void onShareClicked() {
|
|
// share video at the current time (youtube.com/watch?v=ID&t=SECONDS)
|
|
// Timestamp doesn't make sense in a live stream so drop it
|
|
|
|
final int ts = binding.playbackSeekBar.getProgress() / 1000;
|
|
String videoUrl = getVideoUrl();
|
|
if (!isLive() && ts >= 0 && currentMetadata != null
|
|
&& currentMetadata.getMetadata().getServiceId() == YouTube.getServiceId()) {
|
|
videoUrl += ("&t=" + ts);
|
|
}
|
|
ShareUtils.shareText(context, getVideoTitle(), videoUrl);
|
|
}
|
|
|
|
private void onPlayWithKodiClicked() {
|
|
if (currentMetadata != null) {
|
|
pause();
|
|
try {
|
|
NavigationHelper.playWithKore(context, Uri.parse(getVideoUrl()));
|
|
} catch (final Exception e) {
|
|
if (DEBUG) {
|
|
Log.i(TAG, "Failed to start kore", e);
|
|
}
|
|
KoreUtil.showInstallKoreDialog(getParentActivity());
|
|
}
|
|
}
|
|
}
|
|
|
|
private void onOpenInBrowserClicked() {
|
|
if (currentMetadata != null) {
|
|
ShareUtils.openUrlInBrowser(getParentActivity(),
|
|
currentMetadata.getMetadata().getOriginalUrl());
|
|
}
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Video size, resize, orientation, fullscreen
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
private void setupScreenRotationButton() {
|
|
binding.screenRotationButton.setVisibility(videoPlayerSelected()
|
|
&& (globalScreenOrientationLocked(context) || isVerticalVideo
|
|
|| DeviceUtils.isTablet(context))
|
|
? View.VISIBLE : View.GONE);
|
|
binding.screenRotationButton.setImageDrawable(AppCompatResources.getDrawable(context,
|
|
isFullscreen ? R.drawable.ic_fullscreen_exit_white_24dp
|
|
: R.drawable.ic_fullscreen_white_24dp));
|
|
}
|
|
|
|
private void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) {
|
|
binding.surfaceView.setResizeMode(resizeMode);
|
|
binding.resizeTextView.setText(PlayerHelper.resizeTypeOf(context, resizeMode));
|
|
}
|
|
|
|
void onResizeClicked() {
|
|
if (binding != null) {
|
|
setResizeMode(nextResizeModeAndSaveToPrefs(this, binding.surfaceView.getResizeMode()));
|
|
}
|
|
}
|
|
|
|
@Override // exoplayer listener
|
|
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 + "]");
|
|
}
|
|
|
|
binding.surfaceView.setAspectRatio(((float) width) / height);
|
|
isVerticalVideo = width < height;
|
|
|
|
if (globalScreenOrientationLocked(context)
|
|
&& isFullscreen
|
|
&& service.isLandscape() == isVerticalVideo
|
|
&& !DeviceUtils.isTv(context)
|
|
&& !DeviceUtils.isTablet(context)
|
|
&& fragmentListener != null) {
|
|
// set correct orientation
|
|
fragmentListener.onScreenRotationButtonClicked();
|
|
}
|
|
|
|
setupScreenRotationButton();
|
|
}
|
|
|
|
public void toggleFullscreen() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "toggleFullscreen() called");
|
|
}
|
|
if (popupPlayerSelected() || exoPlayerIsNull() || currentMetadata == null
|
|
|| fragmentListener == null) {
|
|
return;
|
|
}
|
|
//changeState(STATE_BLOCKED); TODO check what this does
|
|
|
|
isFullscreen = !isFullscreen;
|
|
if (!isFullscreen) {
|
|
// Apply window insets because Android will not do it when orientation changes
|
|
// from landscape to portrait (open vertical video to reproduce)
|
|
binding.playbackControlRoot.setPadding(0, 0, 0, 0);
|
|
} else {
|
|
// Android needs tens milliseconds to send new insets but a user is able to see
|
|
// how controls changes it's position from `0` to `nav bar height` padding.
|
|
// So just hide the controls to hide this visual inconsistency
|
|
hideControls(0, 0);
|
|
}
|
|
fragmentListener.onFullscreenStateChanged(isFullscreen);
|
|
|
|
if (isFullscreen) {
|
|
binding.titleTextView.setVisibility(View.VISIBLE);
|
|
binding.channelTextView.setVisibility(View.VISIBLE);
|
|
binding.playerCloseButton.setVisibility(View.GONE);
|
|
} else {
|
|
binding.titleTextView.setVisibility(View.GONE);
|
|
binding.channelTextView.setVisibility(View.GONE);
|
|
binding.playerCloseButton.setVisibility(
|
|
videoPlayerSelected() ? View.VISIBLE : View.GONE);
|
|
}
|
|
setupScreenRotationButton();
|
|
}
|
|
|
|
public void checkLandscape() {
|
|
final AppCompatActivity parent = getParentActivity();
|
|
final boolean videoInLandscapeButNotInFullscreen =
|
|
service.isLandscape() && !isFullscreen && videoPlayerSelected() && !isAudioOnly;
|
|
|
|
final boolean notPaused = currentState != STATE_COMPLETED && currentState != STATE_PAUSED;
|
|
if (parent != null
|
|
&& videoInLandscapeButNotInFullscreen
|
|
&& notPaused
|
|
&& !DeviceUtils.isTablet(context)) {
|
|
toggleFullscreen();
|
|
}
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Gestures
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
@SuppressWarnings("checkstyle:ParameterNumber")
|
|
private void onLayoutChange(final View view, final int l, final int t, final int r, final int b,
|
|
final int ol, final int ot, final int or, final int ob) {
|
|
if (l != ol || t != ot || r != or || b != ob) {
|
|
// Use smaller value to be consistent between screen orientations
|
|
// (and to make usage easier)
|
|
final int width = r - l;
|
|
final int height = b - t;
|
|
final int min = Math.min(width, height);
|
|
maxGestureLength = (int) (min * MAX_GESTURE_LENGTH);
|
|
|
|
if (DEBUG) {
|
|
Log.d(TAG, "maxGestureLength = " + maxGestureLength);
|
|
}
|
|
|
|
binding.volumeProgressBar.setMax(maxGestureLength);
|
|
binding.brightnessProgressBar.setMax(maxGestureLength);
|
|
|
|
setInitialGestureValues();
|
|
binding.itemsListPanel.getLayoutParams().height
|
|
= height - binding.itemsListPanel.getTop();
|
|
}
|
|
}
|
|
|
|
private void setInitialGestureValues() {
|
|
if (audioReactor != null) {
|
|
final float currentVolumeNormalized =
|
|
(float) audioReactor.getVolume() / audioReactor.getMaxVolume();
|
|
binding.volumeProgressBar.setProgress(
|
|
(int) (binding.volumeProgressBar.getMax() * currentVolumeNormalized));
|
|
}
|
|
}
|
|
|
|
private int distanceFromCloseButton(final MotionEvent popupMotionEvent) {
|
|
final int closeOverlayButtonX = closeOverlayBinding.closeButton.getLeft()
|
|
+ closeOverlayBinding.closeButton.getWidth() / 2;
|
|
final int closeOverlayButtonY = closeOverlayBinding.closeButton.getTop()
|
|
+ closeOverlayBinding.closeButton.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));
|
|
}
|
|
|
|
private float getClosingRadius() {
|
|
final int buttonRadius = closeOverlayBinding.closeButton.getWidth() / 2;
|
|
// 20% wider than the button itself
|
|
return buttonRadius * 1.2f;
|
|
}
|
|
|
|
public boolean isInsideClosingRadius(final MotionEvent popupMotionEvent) {
|
|
return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius();
|
|
}
|
|
//endregion
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Activity / fragment binding
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
public void setFragmentListener(final PlayerServiceEventListener listener) {
|
|
fragmentListener = listener;
|
|
fragmentIsVisible = true;
|
|
// Apply window insets because Android will not do it when orientation changes
|
|
// from landscape to portrait
|
|
if (!isFullscreen) {
|
|
binding.playbackControlRoot.setPadding(0, 0, 0, 0);
|
|
}
|
|
binding.itemsListPanel.setPadding(0, 0, 0, 0);
|
|
notifyQueueUpdateToListeners();
|
|
notifyMetadataUpdateToListeners();
|
|
notifyPlaybackUpdateToListeners();
|
|
triggerProgressUpdate();
|
|
}
|
|
|
|
public void removeFragmentListener(final PlayerServiceEventListener listener) {
|
|
if (fragmentListener == listener) {
|
|
fragmentListener = null;
|
|
}
|
|
}
|
|
|
|
void setActivityListener(final PlayerEventListener listener) {
|
|
activityListener = listener;
|
|
// TODO why not queue update?
|
|
notifyMetadataUpdateToListeners();
|
|
notifyPlaybackUpdateToListeners();
|
|
triggerProgressUpdate();
|
|
}
|
|
|
|
void removeActivityListener(final PlayerEventListener listener) {
|
|
if (activityListener == listener) {
|
|
activityListener = null;
|
|
}
|
|
}
|
|
|
|
void stopActivityBinding() {
|
|
if (fragmentListener != null) {
|
|
fragmentListener.onServiceStopped();
|
|
fragmentListener = null;
|
|
}
|
|
if (activityListener != null) {
|
|
activityListener.onServiceStopped();
|
|
activityListener = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This will be called when a user goes to another app/activity, turns off a screen.
|
|
* We don't want to interrupt playback and don't want to see notification so
|
|
* next lines of code will enable audio-only playback only if needed
|
|
*/
|
|
private void onFragmentStopped() {
|
|
if (videoPlayerSelected() && (isPlaying() || isLoading())) {
|
|
switch (getMinimizeOnExitAction(context)) {
|
|
case MINIMIZE_ON_EXIT_MODE_BACKGROUND:
|
|
useVideoSource(false);
|
|
case MINIMIZE_ON_EXIT_MODE_POPUP:
|
|
setRecovery();
|
|
NavigationHelper.playOnPopupPlayer(getParentActivity(), playQueue, true);
|
|
case MINIMIZE_ON_EXIT_MODE_NONE: default:
|
|
pause();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void notifyQueueUpdateToListeners() {
|
|
if (fragmentListener != null && playQueue != null) {
|
|
fragmentListener.onQueueUpdate(playQueue);
|
|
}
|
|
if (activityListener != null && playQueue != null) {
|
|
activityListener.onQueueUpdate(playQueue);
|
|
}
|
|
}
|
|
|
|
private void notifyMetadataUpdateToListeners() {
|
|
if (fragmentListener != null && currentMetadata != null) {
|
|
fragmentListener.onMetadataUpdate(currentMetadata.getMetadata(), playQueue);
|
|
}
|
|
if (activityListener != null && currentMetadata != null) {
|
|
activityListener.onMetadataUpdate(currentMetadata.getMetadata(), playQueue);
|
|
}
|
|
}
|
|
|
|
private void notifyPlaybackUpdateToListeners() {
|
|
if (fragmentListener != null && !exoPlayerIsNull() && playQueue != null) {
|
|
fragmentListener.onPlaybackUpdate(currentState, getRepeatMode(),
|
|
playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters());
|
|
}
|
|
if (activityListener != null && !exoPlayerIsNull() && playQueue != null) {
|
|
activityListener.onPlaybackUpdate(currentState, getRepeatMode(),
|
|
playQueue.isShuffled(), getPlaybackParameters());
|
|
}
|
|
}
|
|
|
|
private void notifyProgressUpdateToListeners(final int currentProgress,
|
|
final int duration,
|
|
final int bufferPercent) {
|
|
if (fragmentListener != null) {
|
|
fragmentListener.onProgressUpdate(currentProgress, duration, bufferPercent);
|
|
}
|
|
if (activityListener != null) {
|
|
activityListener.onProgressUpdate(currentProgress, duration, bufferPercent);
|
|
}
|
|
}
|
|
|
|
public AppCompatActivity getParentActivity() {
|
|
// ! instanceof ViewGroup means that view was added via windowManager for Popup
|
|
if (binding == null || !(binding.getRoot().getParent() instanceof ViewGroup)) {
|
|
return null;
|
|
}
|
|
|
|
return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext();
|
|
}
|
|
|
|
private void useVideoSource(final boolean video) {
|
|
if (playQueue == null || isAudioOnly == !video || audioPlayerSelected()) {
|
|
return;
|
|
}
|
|
|
|
isAudioOnly = !video;
|
|
// When a user returns from background controls could be hidden
|
|
// but systemUI will be shown 100%. Hide it
|
|
if (!isAudioOnly && !isControlsVisible()) {
|
|
hideSystemUIIfNeeded();
|
|
}
|
|
setRecovery();
|
|
reloadPlayQueueManager();
|
|
}
|
|
//endregion
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Getters
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
//region
|
|
|
|
public int getCurrentState() {
|
|
return currentState;
|
|
}
|
|
|
|
public boolean exoPlayerIsNull() {
|
|
return simpleExoPlayer == null;
|
|
}
|
|
|
|
public boolean isStopped() {
|
|
return exoPlayerIsNull()
|
|
|| simpleExoPlayer.getPlaybackState() == SimpleExoPlayer.STATE_IDLE;
|
|
}
|
|
|
|
public boolean isPlaying() {
|
|
return !exoPlayerIsNull() && simpleExoPlayer.isPlaying();
|
|
}
|
|
|
|
private boolean isLoading() {
|
|
return !exoPlayerIsNull() && simpleExoPlayer.isLoading();
|
|
}
|
|
|
|
private boolean isLive() {
|
|
try {
|
|
return !exoPlayerIsNull() && simpleExoPlayer.isCurrentWindowDynamic();
|
|
} catch (@NonNull final IndexOutOfBoundsException e) {
|
|
// Why would this even happen =(... but lets log it anyway, better safe than sorry
|
|
if (DEBUG) {
|
|
Log.d(TAG, "player.isCurrentWindowDynamic() failed: " + e.getMessage());
|
|
e.printStackTrace();
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
@NonNull
|
|
public Context getContext() {
|
|
return context;
|
|
}
|
|
|
|
@NonNull
|
|
public SharedPreferences getPrefs() {
|
|
return prefs;
|
|
}
|
|
|
|
public MediaSessionManager getMediaSessionManager() {
|
|
return mediaSessionManager;
|
|
}
|
|
|
|
|
|
public PlayerType getPlayerType() {
|
|
return playerType;
|
|
}
|
|
|
|
public boolean audioPlayerSelected() {
|
|
return playerType == PlayerType.AUDIO;
|
|
}
|
|
|
|
public boolean videoPlayerSelected() {
|
|
return playerType == PlayerType.VIDEO;
|
|
}
|
|
|
|
public boolean popupPlayerSelected() {
|
|
return playerType == PlayerType.POPUP;
|
|
}
|
|
|
|
|
|
public PlayQueue getPlayQueue() {
|
|
return playQueue;
|
|
}
|
|
|
|
public AudioReactor getAudioReactor() {
|
|
return audioReactor;
|
|
}
|
|
|
|
public GestureDetector getGestureDetector() {
|
|
return gestureDetector;
|
|
}
|
|
|
|
public boolean isFullscreen() {
|
|
return isFullscreen;
|
|
}
|
|
|
|
public boolean isVerticalVideo() {
|
|
return isVerticalVideo;
|
|
}
|
|
|
|
public boolean isPopupClosing() {
|
|
return isPopupClosing;
|
|
}
|
|
|
|
|
|
public boolean isSomePopupMenuVisible() {
|
|
return isSomePopupMenuVisible;
|
|
}
|
|
|
|
public ImageButton getPlayPauseButton() {
|
|
return binding.playPauseButton;
|
|
}
|
|
|
|
public View getClosingOverlayView() {
|
|
return closeOverlayBinding.getRoot();
|
|
}
|
|
|
|
public ProgressBar getVolumeProgressBar() {
|
|
return binding.volumeProgressBar;
|
|
}
|
|
|
|
public ProgressBar getBrightnessProgressBar() {
|
|
return binding.brightnessProgressBar;
|
|
}
|
|
|
|
public int getMaxGestureLength() {
|
|
return maxGestureLength;
|
|
}
|
|
|
|
public ImageView getVolumeImageView() {
|
|
return binding.volumeImageView;
|
|
}
|
|
|
|
public RelativeLayout getVolumeRelativeLayout() {
|
|
return binding.volumeRelativeLayout;
|
|
}
|
|
|
|
public ImageView getBrightnessImageView() {
|
|
return binding.brightnessImageView;
|
|
}
|
|
|
|
public RelativeLayout getBrightnessRelativeLayout() {
|
|
return binding.brightnessRelativeLayout;
|
|
}
|
|
|
|
public FloatingActionButton getCloseOverlayButton() {
|
|
return closeOverlayBinding.closeButton;
|
|
}
|
|
|
|
public View getLoadingPanel() {
|
|
return binding.loadingPanel;
|
|
}
|
|
|
|
public TextView getCurrentDisplaySeek() {
|
|
return binding.currentDisplaySeek;
|
|
}
|
|
|
|
public TextView getResizingIndicator() {
|
|
return binding.resizingIndicator;
|
|
}
|
|
|
|
@Nullable
|
|
public WindowManager.LayoutParams getPopupLayoutParams() {
|
|
return popupLayoutParams;
|
|
}
|
|
|
|
@Nullable
|
|
public WindowManager getWindowManager() {
|
|
return windowManager;
|
|
}
|
|
|
|
public float getScreenWidth() {
|
|
return screenWidth;
|
|
}
|
|
|
|
public float getScreenHeight() {
|
|
return screenHeight;
|
|
}
|
|
|
|
public View getRootView() {
|
|
return binding.getRoot();
|
|
}
|
|
|
|
public ExpandableSurfaceView getSurfaceView() {
|
|
return binding.surfaceView;
|
|
}
|
|
|
|
public PlayQueueAdapter getPlayQueueAdapter() {
|
|
return playQueueAdapter;
|
|
}
|
|
|
|
//endregion
|
|
}
|