/* * Copyright 2017 Mauricio Colli * VideoPlayer.java is part of NewPipe * * License: GPL-3.0+ * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ 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.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.PorterDuff; import android.os.Build; import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.SurfaceView; import android.view.View; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.PopupMenu; import android.widget.ProgressBar; import android.widget.SeekBar; import android.widget.TextView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.FixedTrackSelection; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ListHelper; import java.util.ArrayList; import java.util.List; import static org.schabi.newpipe.util.AnimationUtils.animateView; /** * Base for video players * * @author mauriciocolli */ @SuppressWarnings({"WeakerAccess", "unused"}) public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.VideoListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener, Player.EventListener, PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener { public static final boolean DEBUG = BasePlayer.DEBUG; public final String TAG; /*////////////////////////////////////////////////////////////////////////// // Intent //////////////////////////////////////////////////////////////////////////*/ public static final String STARTED_FROM_NEWPIPE = "started_from_newpipe"; public static final String MAX_RESOLUTION = "max_resolution"; private ArrayList availableStreams; private int selectedStreamIndex; /*////////////////////////////////////////////////////////////////////////// // Player //////////////////////////////////////////////////////////////////////////*/ public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds private static final TrackSelection.Factory FIXED_FACTORY = new FixedTrackSelection.Factory(); private List trackGroupInfos; private TrackGroupArray videoTrackGroups; private TrackGroup selectedVideoTrackGroup; private boolean startedFromNewPipe = true; protected boolean wasPlaying = false; /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ private View rootView; private AspectRatioFrameLayout aspectRatioFrameLayout; private SurfaceView surfaceView; private View surfaceForeground; private View loadingPanel; private ImageView endScreen; private ImageView controlAnimationView; private View controlsRoot; private TextView currentDisplaySeek; private View bottomControlsRoot; private SeekBar playbackSeekBar; private TextView playbackCurrentTime; private TextView playbackEndTime; private TextView playbackSpeed; private View topControlsRoot; private TextView qualityTextView; private ImageButton fullScreenButton; private ValueAnimator controlViewAnimator; private Handler controlsVisibilityHandler = new Handler(); private boolean isSomePopupMenuVisible = false; private boolean qualityChanged = false; private int qualityPopupMenuGroupId = 69; private PopupMenu qualityPopupMenu; private int playbackSpeedPopupMenuGroupId = 79; private PopupMenu playbackSpeedPopupMenu; /////////////////////////////////////////////////////////////////////////// public VideoPlayer(String debugTag, Context context) { super(context); this.TAG = debugTag; this.context = context; } public void setup(View rootView) { initViews(rootView); setup(); } public void initViews(View rootView) { this.rootView = rootView; this.aspectRatioFrameLayout = rootView.findViewById(R.id.aspectRatioLayout); this.surfaceView = rootView.findViewById(R.id.surfaceView); this.surfaceForeground = rootView.findViewById(R.id.surfaceForeground); this.loadingPanel = rootView.findViewById(R.id.loading_panel); this.endScreen = rootView.findViewById(R.id.endScreen); this.controlAnimationView = rootView.findViewById(R.id.controlAnimationView); this.controlsRoot = rootView.findViewById(R.id.playbackControlRoot); this.currentDisplaySeek = rootView.findViewById(R.id.currentDisplaySeek); this.playbackSeekBar = rootView.findViewById(R.id.playbackSeekBar); this.playbackCurrentTime = rootView.findViewById(R.id.playbackCurrentTime); this.playbackEndTime = rootView.findViewById(R.id.playbackEndTime); this.playbackSpeed = rootView.findViewById(R.id.playbackSpeed); this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls); this.topControlsRoot = rootView.findViewById(R.id.topControls); this.qualityTextView = rootView.findViewById(R.id.qualityTextView); this.fullScreenButton = rootView.findViewById(R.id.fullScreenButton); //this.aspectRatioFrameLayout.setAspectRatio(16.0f / 9.0f); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); this.playbackSeekBar.getProgressDrawable().setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY); this.qualityPopupMenu = new PopupMenu(context, qualityTextView); this.playbackSpeedPopupMenu = new PopupMenu(context, playbackSpeed); ((ProgressBar) this.loadingPanel.findViewById(R.id.progressBarLoadingPanel)).getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); } @Override public void initListeners() { super.initListeners(); playbackSeekBar.setOnSeekBarChangeListener(this); playbackSpeed.setOnClickListener(this); fullScreenButton.setOnClickListener(this); qualityTextView.setOnClickListener(this); } @Override public void initPlayer() { super.initPlayer(); simpleExoPlayer.setVideoSurfaceView(surfaceView); simpleExoPlayer.addVideoListener(this); if (Build.VERSION.SDK_INT >= 21) { trackSelector.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context)); } } @Override protected void postProcess(@NonNull final Intent intent) { final int resolutionTarget = intent.getIntExtra(MAX_RESOLUTION, getPreferredResolution()); trackSelector.setParameters( // Assume video is horizontal new DefaultTrackSelector.Parameters().withMaxVideoSize(Integer.MAX_VALUE, resolutionTarget) ); } public abstract int getPreferredResolution(); /*////////////////////////////////////////////////////////////////////////// // UI Builders //////////////////////////////////////////////////////////////////////////*/ private final class TrackGroupInfo { final int track; final int group; final Format format; TrackGroupInfo(final int track, final int group, final Format format) { this.track = track; this.group = group; this.format = format; } } private void buildQualityMenu() { if (qualityPopupMenu == null || videoTrackGroups == null || selectedVideoTrackGroup == null || availableStreams == null || videoTrackGroups.length != availableStreams.size()) return; qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId); trackGroupInfos = new ArrayList<>(); int acc = 0; // Each group represent a source in sorted order of how the media source was built for (int groupIndex = 0; groupIndex < videoTrackGroups.length; groupIndex++) { final TrackGroup group = videoTrackGroups.get(groupIndex); final VideoStream stream = availableStreams.get(groupIndex); // For each source, there may be one or multiple tracks depending on the source type for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { final Format format = group.getFormat(trackIndex); final boolean isSetCurrent = selectedVideoTrackGroup.indexOf(format) != -1; if (group.length == 1 && videoTrackGroups.length == availableStreams.size()) { // If the source is non-adaptive (extractor source), then we use the resolution contained in the stream if (isSetCurrent) qualityTextView.setText(stream.resolution); final String menuItem = MediaFormat.getNameById(stream.format) + " " + stream.resolution + " (" + format.width + "x" + format.height + ")"; qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, acc, Menu.NONE, menuItem); } else { // Otherwise, we have an adaptive source, which contains multiple formats and // thus have no inherent quality format if (isSetCurrent) qualityTextView.setText(resolutionStringOf(format)); final MediaFormat mediaFormat = MediaFormat.getFromMimeType(format.sampleMimeType); final String mediaName = mediaFormat == null ? format.sampleMimeType : mediaFormat.name; final String menuItem = mediaName + " " + format.width + "x" + format.height; qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, acc, Menu.NONE, menuItem); } trackGroupInfos.add(new TrackGroupInfo(trackIndex, groupIndex, format)); acc++; } } qualityPopupMenu.setOnMenuItemClickListener(this); qualityPopupMenu.setOnDismissListener(this); qualityTextView.setVisibility(View.VISIBLE); } private void buildPlaybackSpeedMenu() { if (playbackSpeedPopupMenu == null) return; playbackSpeedPopupMenu.getMenu().removeGroup(playbackSpeedPopupMenuGroupId); for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) { playbackSpeedPopupMenu.getMenu().add(playbackSpeedPopupMenuGroupId, i, Menu.NONE, formatSpeed(PLAYBACK_SPEEDS[i])); } playbackSpeed.setText(formatSpeed(getPlaybackSpeed())); playbackSpeedPopupMenu.setOnMenuItemClickListener(this); playbackSpeedPopupMenu.setOnDismissListener(this); } /*////////////////////////////////////////////////////////////////////////// // Playback Listener //////////////////////////////////////////////////////////////////////////*/ @Override public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) { super.sync(item, info); if (info != null) { final List videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false); availableStreams = new ArrayList<>(videos); selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, videos); } buildPlaybackSpeedMenu(); buildQualityMenu(); } public MediaSource sourceOf(final StreamInfo info) { final List videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false); List sources = new ArrayList<>(); for (final VideoStream video : videos) { final MediaSource mediaSource = buildMediaSource(video.url, MediaFormat.getSuffixById(video.format)); sources.add(mediaSource); } return new MergingMediaSource(sources.toArray(new MediaSource[sources.size()])); } /*////////////////////////////////////////////////////////////////////////// // States Implementation //////////////////////////////////////////////////////////////////////////*/ @Override public void onBlocked() { super.onBlocked(); controlsVisibilityHandler.removeCallbacksAndMessages(null); animateView(controlsRoot, false, 300); playbackSeekBar.setEnabled(false); // Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); animateView(endScreen, false, 0); loadingPanel.setBackgroundColor(Color.BLACK); animateView(loadingPanel, true, 0); animateView(surfaceForeground, true, 100); } @Override public void onPlaying() { super.onPlaying(); showAndAnimateControl(-1, true); playbackSeekBar.setEnabled(true); // Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); loadingPanel.setVisibility(View.GONE); showControlsThenHide(); animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); animateView(endScreen, false, 0); } @Override public void onBuffering() { if (DEBUG) Log.d(TAG, "onBuffering() called"); loadingPanel.setBackgroundColor(Color.TRANSPARENT); animateView(loadingPanel, true, 500); } @Override public void onPaused() { if (DEBUG) Log.d(TAG, "onPaused() called"); showControls(400); loadingPanel.setVisibility(View.GONE); } @Override public void onPausedSeek() { if (DEBUG) Log.d(TAG, "onPausedSeek() called"); showAndAnimateControl(-1, true); } @Override public void onCompleted() { super.onCompleted(); showControls(500); animateView(endScreen, true, 800); animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); loadingPanel.setVisibility(View.GONE); animateView(surfaceForeground, true, 100); } /*////////////////////////////////////////////////////////////////////////// // ExoPlayer Video Listener //////////////////////////////////////////////////////////////////////////*/ @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { super.onTracksChanged(trackGroups, trackSelections); if (trackSelector.getCurrentMappedTrackInfo() == null) return; qualityTextView.setVisibility(View.GONE); final int videoRendererIndex = getVideoRendererIndex(); if (videoRendererIndex == -1) return; videoTrackGroups = trackSelector.getCurrentMappedTrackInfo().getTrackGroups(videoRendererIndex); final TrackSelection trackSelection = trackSelections.get(videoRendererIndex); if (trackSelection != null) { selectedVideoTrackGroup = trackSelection.getTrackGroup(); buildQualityMenu(); } } @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { if (DEBUG) { Log.d(TAG, "onVideoSizeChanged() called with: width / height = [" + width + " / " + height + " = " + (((float) width) / height) + "], unappliedRotationDegrees = [" + unappliedRotationDegrees + "], pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]"); } aspectRatioFrameLayout.setAspectRatio(((float) width) / height); } @Override public void onRenderedFirstFrame() { animateView(surfaceForeground, false, 100); } /*////////////////////////////////////////////////////////////////////////// // General Player //////////////////////////////////////////////////////////////////////////*/ @Override public void onPrepared(boolean playWhenReady) { if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); playbackSeekBar.setMax((int) simpleExoPlayer.getDuration()); playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration())); playbackSpeed.setText(formatSpeed(getPlaybackSpeed())); super.onPrepared(playWhenReady); } @Override public void destroy() { super.destroy(); if (endScreen != null) endScreen.setImageBitmap(null); } @Override public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) { if (!isPrepared) return; if (duration != playbackSeekBar.getMax()) { playbackEndTime.setText(getTimeString(duration)); playbackSeekBar.setMax(duration); } if (currentState != STATE_PAUSED) { if (currentState != STATE_PAUSED_SEEK) playbackSeekBar.setProgress(currentProgress); playbackCurrentTime.setText(getTimeString(currentProgress)); } if (simpleExoPlayer.isLoading() || bufferPercent > 90) { playbackSeekBar.setSecondaryProgress((int) (playbackSeekBar.getMax() * ((float) bufferPercent / 100))); } if (DEBUG && bufferPercent % 20 == 0) { //Limit log Log.d(TAG, "updateProgress() called with: isVisible = " + isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]"); } } @Override public void onThumbnailReceived(Bitmap thumbnail) { super.onThumbnailReceived(thumbnail); if (thumbnail != null) endScreen.setImageBitmap(thumbnail); } protected void onFullScreenButtonClicked() { if (!isPlayerReady()) return; changeState(STATE_BLOCKED); } @Override public void onFastRewind() { super.onFastRewind(); showAndAnimateControl(R.drawable.ic_action_av_fast_rewind, true); } @Override public void onFastForward() { super.onFastForward(); showAndAnimateControl(R.drawable.ic_action_av_fast_forward, true); } /*////////////////////////////////////////////////////////////////////////// // OnClick related //////////////////////////////////////////////////////////////////////////*/ @Override public void onClick(View v) { if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); if (v.getId() == fullScreenButton.getId()) { onFullScreenButtonClicked(); } else if (v.getId() == qualityTextView.getId()) { onQualitySelectorClicked(); } else if (v.getId() == playbackSpeed.getId()) { onPlaybackSpeedClicked(); } } /** * Called when an item of the quality selector or the playback speed selector is selected */ @Override public boolean onMenuItemClick(MenuItem menuItem) { if (DEBUG) Log.d(TAG, "onMenuItemClick() called with: menuItem = [" + menuItem + "], menuItem.getItemId = [" + menuItem.getItemId() + "]"); if (qualityPopupMenuGroupId == menuItem.getGroupId()) { final int itemId = menuItem.getItemId(); final TrackGroupInfo info = trackGroupInfos.get(itemId); // Set selected quality as player lifecycle persistent parameters DefaultTrackSelector.Parameters parameters; if (info.format.width > info.format.height) { // Check if video horizontal parameters = new DefaultTrackSelector.Parameters().withMaxVideoSize(Integer.MAX_VALUE, info.format.height); } else { // Or if vertical parameters = new DefaultTrackSelector.Parameters().withMaxVideoSize(info.format.width, Integer.MAX_VALUE); } trackSelector.setParameters(parameters); final int videoRendererIndex = getVideoRendererIndex(); if (videoRendererIndex != -1) { // Override the selection with the selected quality in case of different frame rate final MappingTrackSelector.SelectionOverride override = new MappingTrackSelector.SelectionOverride(FIXED_FACTORY, info.group, info.track); trackSelector.setSelectionOverride(videoRendererIndex, videoTrackGroups, override); } return true; } else if (playbackSpeedPopupMenuGroupId == menuItem.getGroupId()) { int speedIndex = menuItem.getItemId(); float speed = PLAYBACK_SPEEDS[speedIndex]; setPlaybackSpeed(speed); playbackSpeed.setText(formatSpeed(speed)); } return false; } /** * Called when some popup menu is dismissed */ @Override public void onDismiss(PopupMenu menu) { if (DEBUG) Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]"); isSomePopupMenuVisible = false; } public void onQualitySelectorClicked() { if (DEBUG) Log.d(TAG, "onQualitySelectorClicked() called"); qualityPopupMenu.show(); isSomePopupMenuVisible = true; showControls(300); wasPlaying = simpleExoPlayer.getPlayWhenReady(); } private void onPlaybackSpeedClicked() { if (DEBUG) Log.d(TAG, "onPlaybackSpeedClicked() called"); playbackSpeedPopupMenu.show(); isSomePopupMenuVisible = true; showControls(300); } /*////////////////////////////////////////////////////////////////////////// // SeekBar Listener //////////////////////////////////////////////////////////////////////////*/ @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (DEBUG && fromUser) Log.d(TAG, "onProgressChanged() called with: seekBar = [" + seekBar + "], progress = [" + progress + "]"); //if (fromUser) playbackCurrentTime.setText(getTimeString(progress)); if (fromUser) currentDisplaySeek.setText(getTimeString(progress)); } @Override public void onStartTrackingTouch(SeekBar seekBar) { if (DEBUG) Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]"); if (getCurrentState() != STATE_PAUSED_SEEK) changeState(STATE_PAUSED_SEEK); wasPlaying = simpleExoPlayer.getPlayWhenReady(); if (isPlaying()) simpleExoPlayer.setPlayWhenReady(false); showControls(0); animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, true, 300); } @Override public void onStopTrackingTouch(SeekBar seekBar) { if (DEBUG) Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]"); simpleExoPlayer.seekTo(seekBar.getProgress()); if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) simpleExoPlayer.setPlayWhenReady(true); playbackCurrentTime.setText(getTimeString(seekBar.getProgress())); animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); if (getCurrentState() == STATE_PAUSED_SEEK) changeState(STATE_BUFFERING); if (!isProgressLoopRunning()) startProgressLoop(); } /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ public int getVideoRendererIndex() { if (simpleExoPlayer == null) return -1; for (int t = 0; t < simpleExoPlayer.getRendererCount(); t++) { if (simpleExoPlayer.getRendererType(t) == C.TRACK_TYPE_VIDEO) { return t; } } return -1; } public String resolutionStringOf(final Format format) { final String frameRate = format.frameRate > 0 ? String.valueOf((int) format.frameRate) : ""; return Math.min(format.width, format.height) + "p" + frameRate; } public boolean isControlsVisible() { return controlsRoot != null && controlsRoot.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 (controlAnimationView.getVisibility() == View.VISIBLE) { controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(controlAnimationView, PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f), PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1f), PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1f) ).setDuration(300); controlViewAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { controlAnimationView.setVisibility(View.GONE); } }); controlViewAnimator.start(); } return; } float scaleFrom = goneOnEnd ? 1f : 1f, scaleTo = goneOnEnd ? 1.8f : 1.4f; float alphaFrom = goneOnEnd ? 1f : 0f, alphaTo = goneOnEnd ? 0f : 1f; controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(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(Animator animation) { if (goneOnEnd) controlAnimationView.setVisibility(View.GONE); else controlAnimationView.setVisibility(View.VISIBLE); } }); controlAnimationView.setVisibility(View.VISIBLE); controlAnimationView.setImageDrawable(ContextCompat.getDrawable(context, drawableId)); controlViewAnimator.start(); } public boolean isSomePopupMenuVisible() { return isSomePopupMenuVisible; } public void showControlsThenHide() { if (DEBUG) Log.d(TAG, "showControlsThenHide() called"); animateView(controlsRoot, true, 300, 0, new Runnable() { @Override public void run() { hideControls(300, DEFAULT_CONTROLS_HIDE_TIME); } }); } public void showControls(long duration) { if (DEBUG) Log.d(TAG, "showControls() called"); controlsVisibilityHandler.removeCallbacksAndMessages(null); animateView(controlsRoot, true, duration); } public void hideControls(final long duration, long delay) { if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); controlsVisibilityHandler.removeCallbacksAndMessages(null); controlsVisibilityHandler.postDelayed(new Runnable() { @Override public void run() { animateView(controlsRoot, false, duration); } }, delay); } /*////////////////////////////////////////////////////////////////////////// // Getters and Setters //////////////////////////////////////////////////////////////////////////*/ public AspectRatioFrameLayout getAspectRatioFrameLayout() { return aspectRatioFrameLayout; } public SurfaceView getSurfaceView() { return surfaceView; } public boolean wasPlaying() { return wasPlaying; } public int getQualityPopupMenuGroupId() { return qualityPopupMenuGroupId; } public VideoStream getSelectedVideoStream() { return availableStreams.get(selectedStreamIndex); } public boolean isStartedFromNewPipe() { return startedFromNewPipe; } public void setStartedFromNewPipe(boolean startedFromNewPipe) { this.startedFromNewPipe = startedFromNewPipe; } public Handler getControlsVisibilityHandler() { return controlsVisibilityHandler; } public View getRootView() { return rootView; } public void setRootView(View rootView) { this.rootView = rootView; } public View getLoadingPanel() { return loadingPanel; } public ImageView getEndScreen() { return endScreen; } public ImageView getControlAnimationView() { return controlAnimationView; } public View getControlsRoot() { return controlsRoot; } public View getBottomControlsRoot() { return bottomControlsRoot; } public SeekBar getPlaybackSeekBar() { return playbackSeekBar; } public TextView getPlaybackCurrentTime() { return playbackCurrentTime; } public TextView getPlaybackEndTime() { return playbackEndTime; } public View getTopControlsRoot() { return topControlsRoot; } public TextView getQualityTextView() { return qualityTextView; } public ImageButton getFullScreenButton() { return fullScreenButton; } public PopupMenu getQualityPopupMenu() { return qualityPopupMenu; } public View getSurfaceForeground() { return surfaceForeground; } public TextView getCurrentDisplaySeek() { return currentDisplaySeek; } }