NewPipe/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java

2480 lines
103 KiB
Java
Raw Normal View History

package org.schabi.newpipe.fragments.detail;
2015-09-04 02:15:03 +02:00
import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
import android.animation.ValueAnimator;
import android.app.Activity;
2020-07-14 19:21:32 +02:00
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.database.ContentObserver;
2020-09-15 13:43:43 +02:00
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
2015-09-04 02:15:03 +02:00
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
2020-08-16 10:24:58 +02:00
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
2020-07-14 19:21:32 +02:00
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
2020-10-31 21:55:45 +01:00
import android.view.ViewTreeObserver;
2020-07-14 19:21:32 +02:00
import android.view.WindowManager;
import android.view.animation.DecelerateInterpolator;
2020-07-14 19:21:32 +02:00
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import android.widget.Toast;
2020-08-16 10:24:58 +02:00
import androidx.annotation.AttrRes;
2019-10-04 14:59:08 +02:00
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
2020-08-16 10:24:58 +02:00
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
2020-08-16 10:24:58 +02:00
import androidx.core.content.ContextCompat;
2020-10-31 21:55:45 +01:00
import androidx.preference.PreferenceManager;
2020-08-16 10:24:58 +02:00
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
2019-10-04 14:59:08 +02:00
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.tabs.TabLayout;
import com.squareup.picasso.Callback;
import org.schabi.newpipe.App;
2016-08-02 21:17:54 +02:00
import org.schabi.newpipe.R;
2021-10-09 18:46:20 +02:00
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.FragmentVideoDetailBinding;
import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.error.ErrorInfo;
2021-12-01 09:43:24 +01:00
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.error.UserAction;
2017-06-17 13:43:09 +02:00
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
2018-09-23 03:32:19 +02:00
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.EmptyFragment;
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
import org.schabi.newpipe.ktx.AnimationType;
2021-10-09 18:46:20 +02:00
import org.schabi.newpipe.local.dialog.PlaylistDialog;
2018-09-03 01:22:59 +02:00
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.Player;
2022-04-08 09:35:14 +02:00
import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.PlayerType;
2020-07-13 03:17:21 +02:00
import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder;
2020-07-14 19:21:32 +02:00
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
2022-04-08 09:35:14 +02:00
import org.schabi.newpipe.player.ui.MainPlayerUi;
import org.schabi.newpipe.player.ui.VideoPlayerUi;
2017-10-08 21:04:37 +02:00
import org.schabi.newpipe.util.Constants;
2020-08-16 10:24:58 +02:00
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ListHelper;
2017-05-08 15:33:26 +02:00
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.PicassoHelper;
Add support of other delivery methods than progressive HTTP (in the player only) Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
2022-06-16 11:13:19 +02:00
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.ArrayList;
2020-07-14 19:21:32 +02:00
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
2022-04-08 09:35:14 +02:00
import java.util.Optional;
2019-04-13 09:31:32 +02:00
import java.util.concurrent.TimeUnit;
import icepick.State;
2020-10-31 21:55:45 +01:00
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public final class VideoDetailFragment
2018-02-16 11:31:25 +01:00
extends BaseStateFragment<StreamInfo>
implements BackPressable,
SharedPreferences.OnSharedPreferenceChangeListener,
View.OnClickListener,
View.OnLongClickListener,
PlayerServiceExtendedEventListener,
2020-07-13 03:17:21 +02:00
OnKeyDownListener {
public static final String KEY_SWITCHING_PLAYERS = "switching_players";
private static final float MAX_OVERLAY_ALPHA = 0.9f;
private static final float MAX_PLAYER_HEIGHT = 0.7f;
2020-07-14 19:21:32 +02:00
public static final String ACTION_SHOW_MAIN_PLAYER =
App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER";
2020-07-14 19:21:32 +02:00
public static final String ACTION_HIDE_MAIN_PLAYER =
App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER";
public static final String ACTION_PLAYER_STARTED =
App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_PLAYER_STARTED";
2020-07-14 19:21:32 +02:00
public static final String ACTION_VIDEO_FRAGMENT_RESUMED =
App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED";
2020-07-14 19:21:32 +02:00
public static final String ACTION_VIDEO_FRAGMENT_STOPPED =
App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED";
2020-06-27 05:25:50 +02:00
private static final String COMMENTS_TAB_TAG = "COMMENTS";
private static final String RELATED_TAB_TAG = "NEXT VIDEO";
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
2020-06-27 05:25:50 +02:00
private static final String PICASSO_VIDEO_DETAILS_TAG = "PICASSO_VIDEO_DETAILS_TAG";
// tabs
2018-09-03 01:22:59 +02:00
private boolean showComments;
private boolean showRelatedItems;
private boolean showDescription;
private String selectedTabTag;
@AttrRes @NonNull final List<Integer> tabIcons = new ArrayList<>();
@StringRes @NonNull final List<Integer> tabContentDescriptions = new ArrayList<>();
private boolean tabSettingsChanged = false;
private int lastAppBarVerticalOffset = Integer.MAX_VALUE; // prevents useless updates
2020-06-27 05:25:50 +02:00
2018-09-03 01:22:59 +02:00
@State
protected int serviceId = Constants.NO_SERVICE_ID;
@State
@NonNull
protected String title = "";
2018-09-03 01:22:59 +02:00
@State
@Nullable
protected String url = null;
@Nullable
protected PlayQueue playQueue = null;
@State
int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
2020-01-13 17:24:28 +01:00
@State
int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
@State
2020-01-13 17:24:28 +01:00
protected boolean autoPlayEnabled = true;
@Nullable
private StreamInfo currentInfo = null;
private Disposable currentWorker;
2018-09-03 01:22:59 +02:00
@NonNull
private final CompositeDisposable disposables = new CompositeDisposable();
2019-04-13 09:31:32 +02:00
@Nullable
private Disposable positionSubscriber = null;
2017-06-28 03:39:33 +02:00
private BottomSheetBehavior<FrameLayout> bottomSheetBehavior;
2022-08-01 04:55:24 +02:00
private BottomSheetBehavior.BottomSheetCallback bottomSheetCallback;
private BroadcastReceiver broadcastReceiver;
2018-02-16 12:18:15 +01:00
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private FragmentVideoDetailBinding binding;
private TabAdapter pageAdapter;
2018-09-03 01:22:59 +02:00
private ContentObserver settingsContentObserver;
@Nullable
2022-04-08 09:35:14 +02:00
private PlayerService playerService;
2021-01-08 18:35:33 +01:00
private Player player;
private final PlayerHolder playerHolder = PlayerHolder.getInstance();
/*//////////////////////////////////////////////////////////////////////////
// Service management
//////////////////////////////////////////////////////////////////////////*/
@Override
2021-01-08 18:35:33 +01:00
public void onServiceConnected(final Player connectedPlayer,
2022-04-08 09:35:14 +02:00
final PlayerService connectedPlayerService,
final boolean playAfterConnect) {
player = connectedPlayer;
playerService = connectedPlayerService;
// It will do nothing if the player is not in fullscreen mode
hideSystemUiIfNeeded();
2022-04-08 09:35:14 +02:00
final Optional<MainPlayerUi> playerUi = player.UIs().get(MainPlayerUi.class);
if (!player.videoPlayerSelected() && !playAfterConnect) {
return;
2020-07-14 19:21:32 +02:00
}
if (DeviceUtils.isLandscape(requireContext())) {
// If the video is playing but orientation changed
// let's make the video in fullscreen again
checkLandscape();
2022-04-08 09:35:14 +02:00
} else if (playerUi.map(ui -> ui.isFullscreen() && !ui.isVerticalVideo()).orElse(false)
// Tablet UI has orientation-independent fullscreen
&& !DeviceUtils.isTablet(activity)) {
// Device is in portrait orientation after rotation but UI is in fullscreen.
// Return back to non-fullscreen state
2022-04-08 09:35:14 +02:00
playerUi.ifPresent(MainPlayerUi::toggleFullscreen);
2020-07-14 19:21:32 +02:00
}
2022-04-08 09:35:14 +02:00
//noinspection SimplifyOptionalCallChains
if (playAfterConnect
|| (currentInfo != null
&& isAutoplayEnabled()
2022-04-08 09:35:14 +02:00
&& !playerUi.isPresent())) {
autoPlayEnabled = true; // forcefully start playing
openVideoPlayerAutoFullscreen();
2020-07-14 19:21:32 +02:00
}
}
@Override
public void onServiceDisconnected() {
playerService = null;
player = null;
restoreDefaultBrightness();
}
2018-02-16 12:18:15 +01:00
/*////////////////////////////////////////////////////////////////////////*/
public static VideoDetailFragment getInstance(final int serviceId,
@Nullable final String videoUrl,
@NonNull final String name,
@Nullable final PlayQueue queue) {
2020-08-16 10:24:58 +02:00
final VideoDetailFragment instance = new VideoDetailFragment();
instance.setInitialData(serviceId, videoUrl, name, queue);
return instance;
}
2020-09-29 23:49:34 +02:00
public static VideoDetailFragment getInstanceInCollapsedState() {
final VideoDetailFragment instance = new VideoDetailFragment();
instance.updateBottomSheetState(BottomSheetBehavior.STATE_COLLAPSED);
return instance;
}
2020-04-02 13:51:10 +02:00
/*//////////////////////////////////////////////////////////////////////////
// Fragment's Lifecycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(final Bundle savedInstanceState) {
2015-09-04 02:15:03 +02:00
super.onCreate(savedInstanceState);
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
showComments = prefs.getBoolean(getString(R.string.show_comments_key), true);
showRelatedItems = prefs.getBoolean(getString(R.string.show_next_video_key), true);
showDescription = prefs.getBoolean(getString(R.string.show_description_key), true);
selectedTabTag = prefs.getString(
getString(R.string.stream_info_selected_tab_key), COMMENTS_TAB_TAG);
prefs.registerOnSharedPreferenceChangeListener(this);
setupBroadcastReceiver();
settingsContentObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(final boolean selfChange) {
if (activity != null && !globalScreenOrientationLocked(activity)) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
2020-07-14 19:21:32 +02:00
}
}
};
activity.getContentResolver().registerContentObserver(
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false,
settingsContentObserver);
}
2015-09-04 02:15:03 +02:00
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
2021-06-23 16:53:01 +02:00
binding = FragmentVideoDetailBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onPause() {
super.onPause();
if (currentWorker != null) {
currentWorker.dispose();
}
restoreDefaultBrightness();
2020-08-27 22:55:57 +02:00
PreferenceManager.getDefaultSharedPreferences(requireContext())
.edit()
.putString(getString(R.string.stream_info_selected_tab_key),
pageAdapter.getItemTitle(binding.viewPager.getCurrentItem()))
.apply();
}
@Override
public void onResume() {
super.onResume();
if (DEBUG) {
Log.d(TAG, "onResume() called");
}
2020-02-29 00:57:54 +01:00
activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED));
setupBrightness();
if (tabSettingsChanged) {
tabSettingsChanged = false;
initTabs();
if (currentInfo != null) {
updateTabs(currentInfo);
}
}
// Check if it was loading when the fragment was stopped/paused
2020-07-14 19:21:32 +02:00
if (wasLoading.getAndSet(false) && !wasCleared()) {
startLoading(false);
2020-07-14 19:21:32 +02:00
}
}
@Override
public void onStop() {
super.onStop();
2020-07-14 19:21:32 +02:00
if (!activity.isChangingConfigurations()) {
2020-02-29 00:57:54 +01:00
activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_STOPPED));
2020-07-14 19:21:32 +02:00
}
}
@Override
public void onDestroy() {
super.onDestroy();
2020-07-14 19:21:32 +02:00
// Stop the service when user leaves the app with double back press
// if video player is selected. Otherwise unbind
if (activity.isFinishing() && isPlayerAvailable() && player.videoPlayerSelected()) {
playerHolder.stopService();
2020-07-14 19:21:32 +02:00
} else {
playerHolder.setListener(null);
2020-07-14 19:21:32 +02:00
}
2018-02-16 11:31:25 +01:00
PreferenceManager.getDefaultSharedPreferences(activity)
.unregisterOnSharedPreferenceChangeListener(this);
activity.unregisterReceiver(broadcastReceiver);
activity.getContentResolver().unregisterContentObserver(settingsContentObserver);
if (positionSubscriber != null) {
positionSubscriber.dispose();
}
if (currentWorker != null) {
currentWorker.dispose();
}
disposables.clear();
2019-04-13 09:31:32 +02:00
positionSubscriber = null;
currentWorker = null;
bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback);
if (activity.isFinishing()) {
playQueue = null;
currentInfo = null;
stack = new LinkedList<>();
}
}
2021-06-23 16:53:01 +02:00
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
@Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case ReCaptchaActivity.RECAPTCHA_REQUEST:
if (resultCode == Activity.RESULT_OK) {
NavigationHelper.openVideoDetailFragment(requireContext(), getFM(),
serviceId, url, title, null, false);
} else {
Log.e(TAG, "ReCaptcha failed");
}
break;
default:
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
break;
}
}
@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
final String key) {
if (key.equals(getString(R.string.show_comments_key))) {
showComments = sharedPreferences.getBoolean(key, true);
tabSettingsChanged = true;
} else if (key.equals(getString(R.string.show_next_video_key))) {
showRelatedItems = sharedPreferences.getBoolean(key, true);
tabSettingsChanged = true;
} else if (key.equals(getString(R.string.show_description_key))) {
showDescription = sharedPreferences.getBoolean(key, true);
tabSettingsChanged = true;
}
}
/*//////////////////////////////////////////////////////////////////////////
// OnClick
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onClick(final View v) {
switch (v.getId()) {
case R.id.detail_controls_background:
openBackgroundPlayer(false);
break;
case R.id.detail_controls_popup:
openPopupPlayer(false);
break;
case R.id.detail_controls_playlist_append:
if (getFM() != null && currentInfo != null) {
disposables.add(
2021-10-09 18:46:20 +02:00
PlaylistDialog.createCorrespondingDialog(
getContext(),
2022-07-15 04:10:29 +02:00
List.of(new StreamEntity(currentInfo)),
2021-10-09 18:46:20 +02:00
dialog -> dialog.show(getFM(), TAG)
)
);
}
break;
case R.id.detail_controls_download:
if (PermissionHelper.checkStoragePermissions(activity,
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
this.openDownloadDialog();
}
break;
case R.id.detail_controls_share:
if (currentInfo != null) {
ShareUtils.shareText(requireContext(), currentInfo.getName(),
currentInfo.getUrl(), currentInfo.getThumbnailUrl());
}
break;
case R.id.detail_controls_open_in_browser:
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(requireContext(), currentInfo.getUrl());
}
break;
case R.id.detail_controls_play_with_kodi:
if (currentInfo != null) {
try {
NavigationHelper.playWithKore(
requireContext(), Uri.parse(currentInfo.getUrl()));
} catch (final Exception e) {
if (DEBUG) {
Log.i(TAG, "Failed to start kore", e);
}
KoreUtils.showInstallKoreDialog(requireContext());
}
}
break;
case R.id.detail_uploader_root_layout:
if (isEmpty(currentInfo.getSubChannelUrl())) {
if (!isEmpty(currentInfo.getUploaderUrl())) {
openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName());
}
2020-05-08 18:03:19 +02:00
if (DEBUG) {
Log.i(TAG, "Can't open sub-channel because we got no channel URL");
}
} else {
openChannel(currentInfo.getSubChannelUrl(),
currentInfo.getSubChannelName());
}
break;
case R.id.detail_thumbnail_root_layout:
// make sure not to open any player if there is nothing currently loaded!
// FIXME removing this `if` causes the player service to start correctly, then stop,
// then restart badly without calling `startForeground()`, causing a crash when
// later closing the detail fragment
if (currentInfo != null) {
autoPlayEnabled = true; // forcefully start playing
// FIXME Workaround #7427
if (isPlayerAvailable()) {
player.setRecovery();
}
openVideoPlayerAutoFullscreen();
}
break;
case R.id.detail_title_root_layout:
toggleTitleAndSecondaryControls();
break;
case R.id.overlay_thumbnail:
case R.id.overlay_metadata_layout:
case R.id.overlay_buttons_layout:
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
break;
case R.id.overlay_play_queue_button:
NavigationHelper.openPlayQueue(getContext());
break;
case R.id.overlay_play_pause_button:
2020-02-29 00:57:54 +01:00
if (playerIsNotStopped()) {
2021-01-08 18:35:33 +01:00
player.playPause();
2022-04-08 09:35:14 +02:00
player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
showSystemUi();
2020-07-14 19:21:32 +02:00
} else {
autoPlayEnabled = true; // forcefully start playing
openVideoPlayer(false);
2020-07-14 19:21:32 +02:00
}
setOverlayPlayPauseImage(isPlayerAvailable() && player.isPlaying());
break;
case R.id.overlay_close_button:
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
break;
}
}
private void openChannel(final String subChannelUrl, final String subChannelName) {
try {
NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(),
subChannelUrl, subChannelName);
2020-08-16 10:24:58 +02:00
} catch (final Exception e) {
2021-12-01 09:43:24 +01:00
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
}
}
@Override
public boolean onLongClick(final View v) {
if (isLoading.get() || currentInfo == null) {
return false;
}
switch (v.getId()) {
case R.id.detail_controls_background:
openBackgroundPlayer(true);
break;
case R.id.detail_controls_popup:
openPopupPlayer(true);
break;
case R.id.detail_controls_download:
NavigationHelper.openDownloads(activity);
break;
case R.id.overlay_thumbnail:
case R.id.overlay_metadata_layout:
openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName());
break;
case R.id.detail_uploader_root_layout:
if (isEmpty(currentInfo.getSubChannelUrl())) {
Log.w(TAG,
"Can't open parent channel because we got no parent channel URL");
} else {
openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName());
}
break;
2020-06-10 23:11:06 +02:00
case R.id.detail_title_root_layout:
2020-08-27 22:56:58 +02:00
ShareUtils.copyToClipboard(requireContext(),
binding.detailVideoTitleView.getText().toString());
break;
}
return true;
}
private void toggleTitleAndSecondaryControls() {
if (binding.detailSecondaryControlPanel.getVisibility() == View.GONE) {
binding.detailVideoTitleView.setMaxLines(10);
2021-01-17 15:59:29 +01:00
animateRotation(binding.detailToggleSecondaryControlsView,
2022-04-08 09:35:14 +02:00
VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 180);
binding.detailSecondaryControlPanel.setVisibility(View.VISIBLE);
} else {
binding.detailVideoTitleView.setMaxLines(1);
2021-01-17 15:59:29 +01:00
animateRotation(binding.detailToggleSecondaryControlsView,
2022-04-08 09:35:14 +02:00
VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 0);
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
}
// view pager height has changed, update the tab layout
updateTabLayoutVisibility();
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
}
2021-06-23 16:53:01 +02:00
@Override // called from onViewCreated in {@link BaseFragment#onViewCreated}
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
2018-09-23 03:32:19 +02:00
pageAdapter = new TabAdapter(getChildFragmentManager());
binding.viewPager.setAdapter(pageAdapter);
binding.tabLayout.setupWithViewPager(binding.viewPager);
2018-12-08 22:51:55 +01:00
binding.detailThumbnailRootLayout.requestFocus();
binding.detailControlsPlayWithKodi.setVisibility(
KoreUtils.shouldShowPlayWithKodi(requireContext(), serviceId)
? View.VISIBLE
: View.GONE
);
binding.detailControlsCrashThePlayer.setVisibility(
DEBUG && PreferenceManager.getDefaultSharedPreferences(getContext())
.getBoolean(getString(R.string.show_crash_the_player_key), false)
? View.VISIBLE
: View.GONE
);
accommodateForTvAndDesktopMode();
2018-09-23 03:32:19 +02:00
}
@Override
protected void initListeners() {
super.initListeners();
binding.detailTitleRootLayout.setOnClickListener(this);
binding.detailTitleRootLayout.setOnLongClickListener(this);
binding.detailUploaderRootLayout.setOnClickListener(this);
binding.detailUploaderRootLayout.setOnLongClickListener(this);
binding.detailThumbnailRootLayout.setOnClickListener(this);
2021-01-14 22:34:55 +01:00
binding.detailControlsBackground.setOnClickListener(this);
binding.detailControlsBackground.setOnLongClickListener(this);
binding.detailControlsPopup.setOnClickListener(this);
binding.detailControlsPopup.setOnLongClickListener(this);
binding.detailControlsPlaylistAppend.setOnClickListener(this);
binding.detailControlsDownload.setOnClickListener(this);
binding.detailControlsDownload.setOnLongClickListener(this);
binding.detailControlsShare.setOnClickListener(this);
binding.detailControlsOpenInBrowser.setOnClickListener(this);
binding.detailControlsPlayWithKodi.setOnClickListener(this);
if (DEBUG) {
binding.detailControlsCrashThePlayer.setOnClickListener(
2021-11-28 13:42:26 +01:00
v -> VideoDetailPlayerCrasher.onCrashThePlayer(
this.getContext(),
this.player)
2021-11-23 20:11:36 +01:00
);
}
binding.overlayThumbnail.setOnClickListener(this);
binding.overlayThumbnail.setOnLongClickListener(this);
binding.overlayMetadataLayout.setOnClickListener(this);
binding.overlayMetadataLayout.setOnLongClickListener(this);
binding.overlayButtonsLayout.setOnClickListener(this);
binding.overlayPlayQueueButton.setOnClickListener(this);
binding.overlayCloseButton.setOnClickListener(this);
binding.overlayPlayPauseButton.setOnClickListener(this);
binding.detailControlsBackground.setOnTouchListener(getOnControlsTouchListener());
binding.detailControlsPopup.setOnTouchListener(getOnControlsTouchListener());
binding.appBarLayout.addOnOffsetChangedListener((layout, verticalOffset) -> {
// prevent useless updates to tab layout visibility if nothing changed
if (verticalOffset != lastAppBarVerticalOffset) {
lastAppBarVerticalOffset = verticalOffset;
// the view was scrolled
updateTabLayoutVisibility();
}
});
setupBottomPlayer();
2021-12-21 00:18:58 +01:00
if (!playerHolder.isBound()) {
setHeightThumbnail();
} else {
playerHolder.startService(false, this);
}
}
private View.OnTouchListener getOnControlsTouchListener() {
2020-11-20 00:54:27 +01:00
return (view, motionEvent) -> {
2018-02-16 11:31:25 +01:00
if (!PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(getString(R.string.show_hold_to_append_key), true)) {
return false;
}
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
animate(binding.touchAppendDetail, true, 250, AnimationType.ALPHA,
0, () ->
animate(binding.touchAppendDetail, false, 1500,
AnimationType.ALPHA, 1000));
}
return false;
};
}
private void initThumbnailViews(@NonNull final StreamInfo info) {
PicassoHelper.loadDetailsThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailThumbnailImageView, new Callback() {
@Override
public void onSuccess() {
// nothing to do, the image was loaded correctly into the thumbnail
}
2018-02-16 11:31:25 +01:00
@Override
public void onError(final Exception e) {
showSnackBarError(new ErrorInfo(e, UserAction.LOAD_IMAGE,
info.getThumbnailUrl(), info));
}
});
2018-02-16 11:31:25 +01:00
PicassoHelper.loadAvatar(info.getSubChannelAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailSubChannelThumbnailView);
PicassoHelper.loadAvatar(info.getUploaderAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailUploaderThumbnailView);
}
/*//////////////////////////////////////////////////////////////////////////
// OwnStack
//////////////////////////////////////////////////////////////////////////*/
/**
* Stack that contains the "navigation history".<br>
* The peek is the current video.
*/
private static LinkedList<StackItem> stack = new LinkedList<>();
2015-09-04 02:15:03 +02:00
@Override
2020-07-13 03:17:21 +02:00
public boolean onKeyDown(final int keyCode) {
2022-04-08 09:35:14 +02:00
return isPlayerAvailable()
&& player.UIs().get(VideoPlayerUi.class)
.map(playerUi -> playerUi.onKeyDown(keyCode)).orElse(false);
}
@Override
public boolean onBackPressed() {
if (DEBUG) {
2020-07-13 03:17:21 +02:00
Log.d(TAG, "onBackPressed() called");
}
// If we are in fullscreen mode just exit from it via first back press
2022-04-08 09:35:14 +02:00
if (isFullscreen()) {
2020-07-21 00:43:49 +02:00
if (!DeviceUtils.isTablet(activity)) {
2021-01-08 18:35:33 +01:00
player.pause();
2020-07-14 19:21:32 +02:00
}
restoreDefaultOrientation();
setAutoPlay(false);
return true;
}
// If we have something in history of played items we replay it here
if (isPlayerAvailable()
&& player.getPlayQueue() != null
&& player.videoPlayerSelected()
2020-06-27 05:25:50 +02:00
&& player.getPlayQueue().previous()) {
2021-09-19 19:09:11 +02:00
return true; // no code here, as previous() was used in the if
}
2021-09-19 19:09:11 +02:00
// That means that we are on the start of the stack,
if (stack.size() <= 1) {
restoreDefaultOrientation();
2021-09-19 19:09:11 +02:00
return false; // let MainActivity handle the onBack (e.g. to minimize the mini player)
}
2021-09-19 19:09:11 +02:00
// Remove top
stack.pop();
// Get stack item from the new top
2021-09-19 19:09:11 +02:00
setupFromHistoryItem(Objects.requireNonNull(stack.peek()));
return true;
}
private void setupFromHistoryItem(final StackItem item) {
setAutoPlay(false);
hideMainPlayerOnLoadingNewStream();
setInitialData(item.getServiceId(), item.getUrl(),
item.getTitle() == null ? "" : item.getTitle(), item.getPlayQueue());
startLoading(false);
// Maybe an item was deleted in background activity
2020-07-14 19:21:32 +02:00
if (item.getPlayQueue().getItem() == null) {
return;
}
final PlayQueueItem playQueueItem = item.getPlayQueue().getItem();
// Update title, url, uploader from the last item in the stack (it's current now)
final boolean isPlayerStopped = !isPlayerAvailable() || player.isStopped();
2020-07-14 19:21:32 +02:00
if (playQueueItem != null && isPlayerStopped) {
updateOverlayData(playQueueItem.getTitle(),
playQueueItem.getUploader(), playQueueItem.getThumbnailUrl());
}
}
/*//////////////////////////////////////////////////////////////////////////
// Info loading and handling
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void doInitialLoadLogic() {
2020-07-14 19:21:32 +02:00
if (wasCleared()) {
return;
}
if (currentInfo == null) {
prepareAndLoadInfo();
} else {
prepareAndHandleInfoIfNeededAfterDelay(currentInfo, false, 50);
}
}
public void selectAndLoadVideo(final int newServiceId,
@Nullable final String newUrl,
@NonNull final String newTitle,
@Nullable final PlayQueue newQueue) {
if (isPlayerAvailable() && newQueue != null && playQueue != null
&& playQueue.getItem() != null && !playQueue.getItem().getUrl().equals(newUrl)) {
// Preloading can be disabled since playback is surely being replaced.
player.disablePreloadingOfCurrentTrack();
}
setInitialData(newServiceId, newUrl, newTitle, newQueue);
startLoading(false, true);
}
private void prepareAndHandleInfoIfNeededAfterDelay(final StreamInfo info,
final boolean scrollToTop,
final long delay) {
new Handler(Looper.getMainLooper()).postDelayed(() -> {
if (activity == null) {
return;
}
// Data can already be drawn, don't spend time twice
if (info.getName().equals(binding.detailVideoTitleView.getText().toString())) {
return;
}
prepareAndHandleInfo(info, scrollToTop);
}, delay);
}
2020-04-02 13:51:10 +02:00
private void prepareAndHandleInfo(final StreamInfo info, final boolean scrollToTop) {
if (DEBUG) {
Log.d(TAG, "prepareAndHandleInfo() called with: "
+ "info = [" + info + "], scrollToTop = [" + scrollToTop + "]");
}
showLoading();
initTabs();
2020-07-14 19:21:32 +02:00
if (scrollToTop) {
scrollToTop();
}
2018-12-19 06:28:59 +01:00
handleResult(info);
showContent();
}
protected void prepareAndLoadInfo() {
scrollToTop();
startLoading(false);
}
@Override
public void startLoading(final boolean forceLoad) {
super.startLoading(forceLoad);
initTabs();
currentInfo = null;
if (currentWorker != null) {
currentWorker.dispose();
}
runWorker(forceLoad, stack.isEmpty());
}
private void startLoading(final boolean forceLoad, final boolean addToBackStack) {
2020-07-14 19:21:32 +02:00
super.startLoading(forceLoad);
initTabs();
currentInfo = null;
2020-07-14 19:21:32 +02:00
if (currentWorker != null) {
currentWorker.dispose();
}
runWorker(forceLoad, addToBackStack);
}
private void runWorker(final boolean forceLoad, final boolean addToBackStack) {
2020-07-13 03:17:21 +02:00
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
2020-11-20 00:54:27 +01:00
.subscribe(result -> {
isLoading.set(false);
hideMainPlayerOnLoadingNewStream();
if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean(
getString(R.string.show_age_restricted_content), false)) {
hideAgeRestrictedContent();
} else {
handleResult(result);
showContent();
2020-07-13 03:17:21 +02:00
if (addToBackStack) {
2020-07-14 19:21:32 +02:00
if (playQueue == null) {
playQueue = new SinglePlayQueue(result);
}
if (stack.isEmpty() || !stack.peek().getPlayQueue().equals(playQueue)) {
stack.push(new StackItem(serviceId, url, title, playQueue));
2020-07-14 19:21:32 +02:00
}
}
2020-07-14 19:21:32 +02:00
if (isAutoplayEnabled()) {
openVideoPlayerAutoFullscreen();
2020-07-13 03:17:21 +02:00
}
}
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
url == null ? "no url" : url, serviceId)));
}
2018-09-23 03:32:19 +02:00
/*//////////////////////////////////////////////////////////////////////////
// Tabs
//////////////////////////////////////////////////////////////////////////*/
private void initTabs() {
if (pageAdapter.getCount() != 0) {
selectedTabTag = pageAdapter.getItemTitle(binding.viewPager.getCurrentItem());
}
pageAdapter.clearAllItems();
tabIcons.clear();
tabContentDescriptions.clear();
if (shouldShowComments()) {
2020-07-14 19:21:32 +02:00
pageAdapter.addFragment(
CommentsFragment.getInstance(serviceId, url, title), COMMENTS_TAB_TAG);
2021-03-27 15:45:49 +01:00
tabIcons.add(R.drawable.ic_comment);
tabContentDescriptions.add(R.string.comments_tab_description);
}
if (showRelatedItems && binding.relatedItemsLayout == null) {
// temp empty fragment. will be updated in handleResult
pageAdapter.addFragment(EmptyFragment.newInstance(false), RELATED_TAB_TAG);
2021-03-27 15:45:49 +01:00
tabIcons.add(R.drawable.ic_art_track);
tabContentDescriptions.add(R.string.related_items_tab_description);
}
if (showDescription) {
// temp empty fragment. will be updated in handleResult
pageAdapter.addFragment(EmptyFragment.newInstance(false), DESCRIPTION_TAB_TAG);
2021-03-27 15:45:49 +01:00
tabIcons.add(R.drawable.ic_description);
tabContentDescriptions.add(R.string.description_tab_description);
}
if (pageAdapter.getCount() == 0) {
pageAdapter.addFragment(EmptyFragment.newInstance(true), EMPTY_TAB_TAG);
}
2018-10-02 18:00:11 +02:00
pageAdapter.notifyDataSetUpdate();
if (pageAdapter.getCount() >= 2) {
2020-08-16 10:24:58 +02:00
final int position = pageAdapter.getItemPositionByTitle(selectedTabTag);
if (position != -1) {
binding.viewPager.setCurrentItem(position);
}
updateTabIconsAndContentDescriptions();
}
// the page adapter now contains tabs: show the tab layout
updateTabLayoutVisibility();
2018-09-23 03:32:19 +02:00
}
/**
* To be called whenever {@link #pageAdapter} is modified, since that triggers a refresh in
* {@link FragmentVideoDetailBinding#tabLayout} resetting all tab's icons and content
* descriptions. This reads icons from {@link #tabIcons} and content descriptions from
* {@link #tabContentDescriptions}, which are all set in {@link #initTabs()}.
*/
private void updateTabIconsAndContentDescriptions() {
for (int i = 0; i < tabIcons.size(); ++i) {
final TabLayout.Tab tab = binding.tabLayout.getTabAt(i);
if (tab != null) {
tab.setIcon(tabIcons.get(i));
tab.setContentDescription(tabContentDescriptions.get(i));
}
}
}
private void updateTabs(@NonNull final StreamInfo info) {
if (showRelatedItems) {
if (binding.relatedItemsLayout == null) { // phone
pageAdapter.updateItem(RELATED_TAB_TAG, RelatedItemsFragment.getInstance(info));
2021-01-14 22:34:55 +01:00
} else { // tablet + TV
getChildFragmentManager().beginTransaction()
.replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info))
.commitAllowingStateLoss();
2022-04-08 09:35:14 +02:00
binding.relatedItemsLayout.setVisibility(isFullscreen() ? View.GONE : View.VISIBLE);
}
}
if (showDescription) {
pageAdapter.updateItem(DESCRIPTION_TAB_TAG, new DescriptionFragment(info));
}
binding.viewPager.setVisibility(View.VISIBLE);
// make sure the tab layout is visible
updateTabLayoutVisibility();
pageAdapter.notifyDataSetUpdate();
updateTabIconsAndContentDescriptions();
}
private boolean shouldShowComments() {
try {
2019-02-15 20:53:26 +01:00
return showComments && NewPipe.getService(serviceId)
.getServiceInfo()
.getMediaCapabilities()
.contains(COMMENTS);
2020-08-16 10:24:58 +02:00
} catch (final ExtractionException e) {
return false;
2018-09-23 03:32:19 +02:00
}
}
public void updateTabLayoutVisibility() {
if (binding == null) {
//If binding is null we do not need to and should not do anything with its object(s)
return;
}
if (pageAdapter.getCount() < 2 || binding.viewPager.getVisibility() != View.VISIBLE) {
// hide tab layout if there is only one tab or if the view pager is also hidden
binding.tabLayout.setVisibility(View.GONE);
} else {
// call `post()` to be sure `viewPager.getHitRect()`
// is up to date and not being currently recomputed
binding.tabLayout.post(() -> {
final var activity = getActivity();
if (activity != null) {
final Rect pagerHitRect = new Rect();
binding.viewPager.getHitRect(pagerHitRect);
final int height = DeviceUtils.getWindowHeight(activity.getWindowManager());
final int viewPagerVisibleHeight = height - pagerHitRect.top;
// see TabLayout.DEFAULT_HEIGHT, which is equal to 48dp
final float tabLayoutHeight = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 48, getResources().getDisplayMetrics());
if (viewPagerVisibleHeight > tabLayoutHeight * 2) {
// no translation at all when viewPagerVisibleHeight > tabLayout.height * 3
binding.tabLayout.setTranslationY(
Math.max(0, tabLayoutHeight * 3 - viewPagerVisibleHeight));
binding.tabLayout.setVisibility(View.VISIBLE);
} else {
// view pager is not visible enough
binding.tabLayout.setVisibility(View.GONE);
}
}
});
}
}
public void scrollToTop() {
binding.appBarLayout.setExpanded(true, true);
// notify tab layout of scrolling
updateTabLayoutVisibility();
}
/*//////////////////////////////////////////////////////////////////////////
// Play Utils
//////////////////////////////////////////////////////////////////////////*/
private void toggleFullscreenIfInFullscreenMode() {
// If a user watched video inside fullscreen mode and than chose another player
// return to non-fullscreen mode
2022-04-08 09:35:14 +02:00
if (isPlayerAvailable()) {
player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
if (playerUi.isFullscreen()) {
playerUi.toggleFullscreen();
}
});
}
}
private void openBackgroundPlayer(final boolean append) {
2020-07-14 19:21:32 +02:00
final boolean useExternalAudioPlayer = PreferenceManager
.getDefaultSharedPreferences(activity)
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
toggleFullscreenIfInFullscreenMode();
if (isPlayerAvailable()) {
// FIXME Workaround #7427
player.setRecovery();
}
2020-08-27 22:55:57 +02:00
if (!useExternalAudioPlayer) {
openNormalBackgroundPlayer(append);
} else {
final List<AudioStream> audioStreams = getUrlAndNonTorrentStreams(
currentInfo.getAudioStreams());
final int index = ListHelper.getDefaultAudioFormat(activity, audioStreams);
if (index == -1) {
Toast.makeText(activity, R.string.no_audio_streams_available_for_external_players,
Toast.LENGTH_SHORT).show();
return;
}
startOnExternalPlayer(activity, currentInfo, audioStreams.get(index));
}
}
private void openPopupPlayer(final boolean append) {
if (!PermissionHelper.isPopupEnabled(activity)) {
PermissionHelper.showPopupEnablementToast(activity);
return;
}
// See UI changes while remote playQueue changes
if (!isPlayerAvailable()) {
playerHolder.startService(false, this);
} else {
// FIXME Workaround #7427
player.setRecovery();
2020-07-14 19:21:32 +02:00
}
toggleFullscreenIfInFullscreenMode();
final PlayQueue queue = setupPlayQueueForIntent(append);
Add play next to long press menu & refactor enqueue methods (#6872) * added mvp play next button in long press menu; new intent handling, new long press dialog entry, new dialog functions, new strings * changed line length for checkstyle pass * cleaned comments, moved strings * Update app/src/main/res/values/strings.xml to make long press entry more descriptive Co-authored-by: opusforlife2 <53176348+opusforlife2@users.noreply.github.com> * Update app/src/main/res/values/strings.xml Co-authored-by: Stypox <stypox@pm.me> * replace redundant nextOnVideoPlayer methods Co-authored-by: Stypox <stypox@pm.me> * add enqueueNextOnPlayer and enqueueOnPlayer without selectOnAppend and RESUME_PLAYBACK/ deprecate enqueueNextOn*Player and enqueueOn*Player methods add getPlayerIntent, getPlayerEnqueueIntent and getPlayerEnqueueNextIntent without selectOnAppend and RESUME_PLAYBACK/ deprecate those with add section comments * removed deprecated methods removed redundant methods * removed deprecated methods removed redundant methods * replaced APPEND_ONLY, removed SELECT_ON_APPEND / replaced remaining enqueueOn*Player methods * now works with playlists * renamed dialog entry * checking for >1 items in the queue using the PlayerHolder * making enqueue*OnPlayer safe to call when no video is playing (defaulting to audio) * corrected strings * improve getQueueSize in PlayerHolder * long press to enqueue only if queue isnt empty * add Whitespace Co-authored-by: Stypox <stypox@pm.me> * clarify comments / add spaces * PlayerType as parameter of the enqueueOnPlayer method add Helper method * using the helper function everywhere (except for the background and popup long-press actions (also on playlists, history, ...)), so basically nowhere / passing checkstyle * assimilated the enqueue*OnPlayer methods * removed redundant comment, variable * simplify code line Co-authored-by: Stypox <stypox@pm.me> * move if * replace workaround for isPlayerOpen() Co-authored-by: Stypox <stypox@pm.me> * replaced workarounds (getType), corrected static access with getInstance * remove unused imports * changed method call to original, new method doesnt exist yet. * Use getter method instead of property access syntax. * improve conditional for play next entry Co-authored-by: Stypox <stypox@pm.me> * show play next btn in feed fragment Co-authored-by: Stypox <stypox@pm.me> * add play next to local playlist and statistics fragment Co-authored-by: Stypox <stypox@pm.me> * formating Co-authored-by: Stypox <stypox@pm.me> * correcting logic Co-authored-by: Stypox <stypox@pm.me> * remove 2 year old unused string, formating Co-authored-by: Stypox <stypox@pm.me> * correct enqueue (next) conditionals, default to background if no player is open. Dont generally default to background play. * remove player open checks from button long press enqueue actions * improve log msg * Rename next to enqueue_next * Refactor kotlin Co-authored-by: opusforlife2 <53176348+opusforlife2@users.noreply.github.com> Co-authored-by: Stypox <stypox@pm.me>
2021-09-18 11:22:49 +02:00
if (append) { //resumePlayback: false
NavigationHelper.enqueueOnPlayer(activity, queue, PlayerType.POPUP);
} else {
2020-07-14 19:21:32 +02:00
replaceQueueIfUserConfirms(() -> NavigationHelper
.playOnPopupPlayer(activity, queue, true));
}
}
/**
* Opens the video player, in fullscreen if needed. In order to open fullscreen, the activity
* is toggled to landscape orientation (which will then cause fullscreen mode).
*
* @param directlyFullscreenIfApplicable whether to open fullscreen if we are not already
* in landscape and screen orientation is locked
*/
public void openVideoPlayer(final boolean directlyFullscreenIfApplicable) {
if (directlyFullscreenIfApplicable
&& !DeviceUtils.isLandscape(requireContext())
&& PlayerHelper.globalScreenOrientationLocked(requireContext())) {
// Make sure the bottom sheet turns out expanded. When this code kicks in the bottom
// sheet could not have fully expanded yet, and thus be in the STATE_SETTLING state.
// When the activity is rotated, and its state is saved and then restored, the bottom
// sheet would forget what it was doing, since even if STATE_SETTLING is restored, it
// doesn't tell which state it was settling to, and thus the bottom sheet settles to
// STATE_COLLAPSED. This can be solved by manually setting the state that will be
// restored (i.e. bottomSheetState) to STATE_EXPANDED.
updateBottomSheetState(BottomSheetBehavior.STATE_EXPANDED);
// toggle landscape in order to open directly in fullscreen
onScreenRotationButtonClicked();
}
2018-02-16 11:31:25 +01:00
if (PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
showExternalPlaybackDialog();
} else {
2020-06-27 05:25:50 +02:00
replaceQueueIfUserConfirms(this::openMainPlayer);
}
}
/**
* If the option to start directly fullscreen is enabled, calls
* {@link #openVideoPlayer(boolean)} with {@code directlyFullscreenIfApplicable = true}, so that
* if the user is not already in landscape and he has screen orientation locked the activity
* rotates and fullscreen starts. Otherwise, if the option to start directly fullscreen is
* disabled, calls {@link #openVideoPlayer(boolean)} with {@code directlyFullscreenIfApplicable
* = false}, hence preventing it from going directly fullscreen.
*/
public void openVideoPlayerAutoFullscreen() {
openVideoPlayer(PlayerHelper.isStartMainPlayerFullscreenEnabled(requireContext()));
}
private void openNormalBackgroundPlayer(final boolean append) {
// See UI changes while remote playQueue changes
if (!isPlayerAvailable()) {
playerHolder.startService(false, this);
2020-07-14 19:21:32 +02:00
}
final PlayQueue queue = setupPlayQueueForIntent(append);
if (append) {
Add play next to long press menu & refactor enqueue methods (#6872) * added mvp play next button in long press menu; new intent handling, new long press dialog entry, new dialog functions, new strings * changed line length for checkstyle pass * cleaned comments, moved strings * Update app/src/main/res/values/strings.xml to make long press entry more descriptive Co-authored-by: opusforlife2 <53176348+opusforlife2@users.noreply.github.com> * Update app/src/main/res/values/strings.xml Co-authored-by: Stypox <stypox@pm.me> * replace redundant nextOnVideoPlayer methods Co-authored-by: Stypox <stypox@pm.me> * add enqueueNextOnPlayer and enqueueOnPlayer without selectOnAppend and RESUME_PLAYBACK/ deprecate enqueueNextOn*Player and enqueueOn*Player methods add getPlayerIntent, getPlayerEnqueueIntent and getPlayerEnqueueNextIntent without selectOnAppend and RESUME_PLAYBACK/ deprecate those with add section comments * removed deprecated methods removed redundant methods * removed deprecated methods removed redundant methods * replaced APPEND_ONLY, removed SELECT_ON_APPEND / replaced remaining enqueueOn*Player methods * now works with playlists * renamed dialog entry * checking for >1 items in the queue using the PlayerHolder * making enqueue*OnPlayer safe to call when no video is playing (defaulting to audio) * corrected strings * improve getQueueSize in PlayerHolder * long press to enqueue only if queue isnt empty * add Whitespace Co-authored-by: Stypox <stypox@pm.me> * clarify comments / add spaces * PlayerType as parameter of the enqueueOnPlayer method add Helper method * using the helper function everywhere (except for the background and popup long-press actions (also on playlists, history, ...)), so basically nowhere / passing checkstyle * assimilated the enqueue*OnPlayer methods * removed redundant comment, variable * simplify code line Co-authored-by: Stypox <stypox@pm.me> * move if * replace workaround for isPlayerOpen() Co-authored-by: Stypox <stypox@pm.me> * replaced workarounds (getType), corrected static access with getInstance * remove unused imports * changed method call to original, new method doesnt exist yet. * Use getter method instead of property access syntax. * improve conditional for play next entry Co-authored-by: Stypox <stypox@pm.me> * show play next btn in feed fragment Co-authored-by: Stypox <stypox@pm.me> * add play next to local playlist and statistics fragment Co-authored-by: Stypox <stypox@pm.me> * formating Co-authored-by: Stypox <stypox@pm.me> * correcting logic Co-authored-by: Stypox <stypox@pm.me> * remove 2 year old unused string, formating Co-authored-by: Stypox <stypox@pm.me> * correct enqueue (next) conditionals, default to background if no player is open. Dont generally default to background play. * remove player open checks from button long press enqueue actions * improve log msg * Rename next to enqueue_next * Refactor kotlin Co-authored-by: opusforlife2 <53176348+opusforlife2@users.noreply.github.com> Co-authored-by: Stypox <stypox@pm.me>
2021-09-18 11:22:49 +02:00
NavigationHelper.enqueueOnPlayer(activity, queue, PlayerType.AUDIO);
} else {
2020-07-14 19:21:32 +02:00
replaceQueueIfUserConfirms(() -> NavigationHelper
.playOnBackgroundPlayer(activity, queue, true));
}
}
2020-06-27 05:25:50 +02:00
private void openMainPlayer() {
if (!isPlayerServiceAvailable()) {
playerHolder.startService(autoPlayEnabled, this);
2020-07-14 19:21:32 +02:00
return;
}
if (currentInfo == null) {
return;
}
final PlayQueue queue = setupPlayQueueForIntent(false);
tryAddVideoPlayerView();
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
2022-04-08 09:35:14 +02:00
PlayerService.class, queue, true, autoPlayEnabled);
ContextCompat.startForegroundService(activity, playerIntent);
}
/**
* When the video detail fragment is already showing details for a video and the user opens a
* new one, the video detail fragment changes all of its old data to the new stream, so if there
* is a video player currently open it should be hidden. This method does exactly that. If
* autoplay is enabled, the underlying player is not stopped completely, since it is going to
* be reused in a few milliseconds and the flickering would be annoying.
*/
private void hideMainPlayerOnLoadingNewStream() {
2022-04-08 09:35:14 +02:00
//noinspection SimplifyOptionalCallChains
if (!isPlayerServiceAvailable() || !getRoot().isPresent()
2020-07-14 19:21:32 +02:00
|| !player.videoPlayerSelected()) {
return;
2020-07-14 19:21:32 +02:00
}
removeVideoPlayerView();
if (isAutoplayEnabled()) {
playerService.stopForImmediateReusing();
2022-04-08 09:35:14 +02:00
getRoot().ifPresent(view -> view.setVisibility(View.GONE));
} else {
playerHolder.stopService();
}
}
private PlayQueue setupPlayQueueForIntent(final boolean append) {
2020-07-14 19:21:32 +02:00
if (append) {
return new SinglePlayQueue(currentInfo);
}
PlayQueue queue = playQueue;
// Size can be 0 because queue removes bad stream automatically when error occurs
if (queue == null || queue.isEmpty()) {
queue = new SinglePlayQueue(currentInfo);
2020-07-14 19:21:32 +02:00
}
return queue;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
public void setAutoPlay(final boolean autoPlay) {
this.autoPlayEnabled = autoPlay;
}
private void startOnExternalPlayer(@NonNull final Context context,
@NonNull final StreamInfo info,
@NonNull final Stream selectedStream) {
NavigationHelper.playOnExternalPlayer(context, currentInfo.getName(),
currentInfo.getSubChannelName(), selectedStream);
final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext());
disposables.add(recordManager.onViewed(info).onErrorComplete()
.subscribe(
ignored -> { /* successful */ },
error -> Log.e(TAG, "Register view failure: ", error)
));
}
private boolean isExternalPlayerEnabled() {
return PreferenceManager.getDefaultSharedPreferences(requireContext())
.getBoolean(getString(R.string.use_external_video_player_key), false);
}
// This method overrides default behaviour when setAutoPlay() is called.
// Don't auto play if the user selected an external player or disabled it in settings
private boolean isAutoplayEnabled() {
2020-01-13 17:24:28 +01:00
return autoPlayEnabled
&& !isExternalPlayerEnabled()
&& (!isPlayerAvailable() || player.videoPlayerSelected())
2020-01-26 05:33:52 +01:00
&& bottomSheetState != BottomSheetBehavior.STATE_HIDDEN
&& PlayerHelper.isAutoplayAllowedByUser(requireContext());
}
private void tryAddVideoPlayerView() {
if (isPlayerAvailable() && getView() != null) {
// Setup the surface view height, so that it fits the video correctly; this is done also
// here, and not only in the Handler, to avoid a choppy fullscreen rotation animation.
setHeightThumbnail();
}
// do all the null checks in the posted lambda, too, since the player, the binding and the
// view could be set or unset before the lambda gets executed on the next main thread cycle
new Handler(Looper.getMainLooper()).post(() -> {
if (!isPlayerAvailable() || getView() == null) {
return;
}
// setup the surface view height, so that it fits the video correctly
setHeightThumbnail();
player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
// sometimes binding would be null here, even though getView() != null above u.u
if (binding != null) {
// prevent from re-adding a view multiple times
playerUi.removeViewFromParent();
binding.playerPlaceholder.addView(playerUi.getBinding().getRoot());
playerUi.setupVideoSurfaceIfNeeded();
}
});
});
}
private void removeVideoPlayerView() {
makeDefaultHeightForVideoPlaceholder();
2022-04-08 09:35:14 +02:00
if (player != null) {
player.UIs().get(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent);
}
}
private void makeDefaultHeightForVideoPlaceholder() {
2020-07-14 19:21:32 +02:00
if (getView() == null) {
return;
}
binding.playerPlaceholder.getLayoutParams().height = FrameLayout.LayoutParams.MATCH_PARENT;
binding.playerPlaceholder.requestLayout();
}
private final ViewTreeObserver.OnPreDrawListener preDrawListener =
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
final DisplayMetrics metrics = getResources().getDisplayMetrics();
if (getView() != null) {
final int height = (DeviceUtils.isInMultiWindow(activity)
? requireView()
: activity.getWindow().getDecorView()).getHeight();
setHeightThumbnail(height, metrics);
getView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
}
return false;
}
};
/**
2020-07-14 19:21:32 +02:00
* Method which controls the size of thumbnail and the size of main player inside
* a layout with thumbnail. It decides what height the player should have in both
* screen orientations. It knows about multiWindow feature
* and about videos with aspectRatio ZOOM (the height for them will be a bit higher,
* {@link #MAX_PLAYER_HEIGHT})
*/
private void setHeightThumbnail() {
final DisplayMetrics metrics = getResources().getDisplayMetrics();
final boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
2022-04-08 09:35:14 +02:00
if (isFullscreen()) {
final int height = (DeviceUtils.isInMultiWindow(activity)
? requireView()
: activity.getWindow().getDecorView()).getHeight();
// Height is zero when the view is not yet displayed like after orientation change
if (height != 0) {
setHeightThumbnail(height, metrics);
} else {
requireView().getViewTreeObserver().addOnPreDrawListener(preDrawListener);
}
2020-07-14 19:21:32 +02:00
} else {
final int height = (int) (isPortrait
? metrics.widthPixels / (16.0f / 9.0f)
: metrics.heightPixels / 2.0f);
setHeightThumbnail(height, metrics);
2020-07-14 19:21:32 +02:00
}
}
private void setHeightThumbnail(final int newHeight, final DisplayMetrics metrics) {
binding.detailThumbnailImageView.setLayoutParams(
new FrameLayout.LayoutParams(
RelativeLayout.LayoutParams.MATCH_PARENT, newHeight));
binding.detailThumbnailImageView.setMinimumHeight(newHeight);
if (isPlayerAvailable()) {
final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
2022-04-08 09:35:14 +02:00
player.UIs().get(VideoPlayerUi.class).ifPresent(ui ->
ui.getBinding().surfaceView.setHeights(newHeight,
ui.isFullscreen() ? newHeight : maxHeight));
}
}
2018-12-19 06:28:59 +01:00
private void showContent() {
binding.detailContentRootHiding.setVisibility(View.VISIBLE);
}
protected void setInitialData(final int newServiceId,
@Nullable final String newUrl,
@NonNull final String newTitle,
@Nullable final PlayQueue newPlayQueue) {
this.serviceId = newServiceId;
this.url = newUrl;
this.title = newTitle;
this.playQueue = newPlayQueue;
}
private void setErrorImage(final int imageResource) {
if (binding == null || activity == null) {
return;
}
binding.detailThumbnailImageView.setImageDrawable(
AppCompatResources.getDrawable(requireContext(), imageResource));
animate(binding.detailThumbnailImageView, false, 0, AnimationType.ALPHA,
0, () -> animate(binding.detailThumbnailImageView, true, 500));
}
@Override
public void handleError() {
super.handleError();
2021-02-14 13:40:17 +01:00
setErrorImage(R.drawable.not_available_monkey);
if (binding.relatedItemsLayout != null) { // hide related streams for tablets
binding.relatedItemsLayout.setVisibility(View.INVISIBLE);
2021-02-14 13:40:17 +01:00
}
// hide comments / related streams / description tabs
binding.viewPager.setVisibility(View.GONE);
binding.tabLayout.setVisibility(View.GONE);
}
2021-02-14 13:40:17 +01:00
private void hideAgeRestrictedContent() {
showTextError(getString(R.string.restricted_video,
getString(R.string.show_age_restricted_content_title)));
}
private void setupBroadcastReceiver() {
broadcastReceiver = new BroadcastReceiver() {
@Override
2020-07-14 19:21:32 +02:00
public void onReceive(final Context context, final Intent intent) {
2020-11-18 23:58:41 +01:00
switch (intent.getAction()) {
case ACTION_SHOW_MAIN_PLAYER:
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
break;
case ACTION_HIDE_MAIN_PLAYER:
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
break;
case ACTION_PLAYER_STARTED:
// If the state is not hidden we don't need to show the mini player
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
// Rebound to the service if it was closed via notification or mini player
2021-12-21 00:18:58 +01:00
if (!playerHolder.isBound()) {
playerHolder.startService(
false, VideoDetailFragment.this);
2020-11-18 23:58:41 +01:00
}
break;
}
}
};
2020-08-16 10:24:58 +02:00
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ACTION_SHOW_MAIN_PLAYER);
intentFilter.addAction(ACTION_HIDE_MAIN_PLAYER);
intentFilter.addAction(ACTION_PLAYER_STARTED);
activity.registerReceiver(broadcastReceiver, intentFilter);
}
/*//////////////////////////////////////////////////////////////////////////
// Orientation listener
//////////////////////////////////////////////////////////////////////////*/
private void restoreDefaultOrientation() {
2021-09-19 19:09:11 +02:00
if (isPlayerAvailable() && player.videoPlayerSelected()) {
toggleFullscreenIfInFullscreenMode();
2020-07-14 19:21:32 +02:00
}
// This will show systemUI and pause the player.
// User can tap on Play button and video will be in fullscreen mode again
2020-07-14 19:21:32 +02:00
// Note for tablet: trying to avoid orientation changes since it's not easy
// to physically rotate the tablet every time
2021-09-19 19:09:11 +02:00
if (activity != null && !DeviceUtils.isTablet(activity)) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
2020-07-14 19:21:32 +02:00
}
}
2017-06-28 03:39:33 +02:00
/*//////////////////////////////////////////////////////////////////////////
// Contract
2017-06-28 03:39:33 +02:00
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
2019-08-07 12:00:47 +02:00
super.showLoading();
2019-08-07 12:00:47 +02:00
//if data is already cached, transition from VISIBLE -> INVISIBLE -> VISIBLE is not required
if (!ExtractorHelper.isCached(serviceId, url, InfoItem.InfoType.STREAM)) {
binding.detailContentRootHiding.setVisibility(View.INVISIBLE);
2019-08-07 12:00:47 +02:00
}
animate(binding.detailThumbnailPlayButton, false, 50);
animate(binding.detailDurationView, false, 100);
animate(binding.detailPositionView, false, 100);
animate(binding.positionView, false, 50);
binding.detailVideoTitleView.setText(title);
binding.detailVideoTitleView.setMaxLines(1);
animate(binding.detailVideoTitleView, true, 0);
2021-01-17 15:59:29 +01:00
binding.detailToggleSecondaryControlsView.setVisibility(View.GONE);
binding.detailTitleRootLayout.setClickable(false);
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
if (binding.relatedItemsLayout != null) {
if (showRelatedItems) {
binding.relatedItemsLayout.setVisibility(
2022-04-08 09:35:14 +02:00
isFullscreen() ? View.GONE : View.INVISIBLE);
} else {
binding.relatedItemsLayout.setVisibility(View.GONE);
2018-12-08 22:51:55 +01:00
}
}
PicassoHelper.cancelTag(PICASSO_VIDEO_DETAILS_TAG);
binding.detailThumbnailImageView.setImageBitmap(null);
binding.detailSubChannelThumbnailView.setImageBitmap(null);
}
@Override
public void handleResult(@NonNull final StreamInfo info) {
super.handleResult(info);
currentInfo = info;
2020-01-10 15:32:05 +01:00
setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName(), playQueue);
updateTabs(info);
animate(binding.detailThumbnailPlayButton, true, 200);
binding.detailVideoTitleView.setText(title);
2021-12-28 21:54:36 +01:00
binding.detailSubChannelThumbnailView.setVisibility(View.GONE);
if (!isEmpty(info.getSubChannelName())) {
displayBothUploaderAndSubChannel(info);
} else if (!isEmpty(info.getUploaderName())) {
displayUploaderAsSubChannel(info);
2017-12-08 15:05:08 +01:00
} else {
binding.detailUploaderTextView.setVisibility(View.GONE);
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
2017-12-08 15:05:08 +01:00
}
2022-07-15 19:53:48 +02:00
final Drawable buddyDrawable =
AppCompatResources.getDrawable(activity, R.drawable.placeholder_person);
binding.detailSubChannelThumbnailView.setImageDrawable(buddyDrawable);
binding.detailUploaderThumbnailView.setImageDrawable(buddyDrawable);
2017-12-08 15:05:08 +01:00
if (info.getViewCount() >= 0) {
2022-06-18 17:49:04 +02:00
if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
binding.detailViewCountView.setText(Localization.listeningCount(activity,
info.getViewCount()));
2022-06-18 17:49:04 +02:00
} else if (info.getStreamType().equals(StreamType.LIVE_STREAM)) {
binding.detailViewCountView.setText(Localization
.localizeWatchingCount(activity, info.getViewCount()));
} else {
binding.detailViewCountView.setText(Localization
.localizeViewCount(activity, info.getViewCount()));
}
binding.detailViewCountView.setVisibility(View.VISIBLE);
2017-12-08 15:05:08 +01:00
} else {
binding.detailViewCountView.setVisibility(View.GONE);
2017-12-08 15:05:08 +01:00
}
2017-12-08 15:05:08 +01:00
if (info.getDislikeCount() == -1 && info.getLikeCount() == -1) {
binding.detailThumbsDownImgView.setVisibility(View.VISIBLE);
binding.detailThumbsUpImgView.setVisibility(View.VISIBLE);
binding.detailThumbsUpCountView.setVisibility(View.GONE);
binding.detailThumbsDownCountView.setVisibility(View.GONE);
binding.detailThumbsDisabledView.setVisibility(View.VISIBLE);
} else {
2017-12-08 15:05:08 +01:00
if (info.getDislikeCount() >= 0) {
binding.detailThumbsDownCountView.setText(Localization
.shortCount(activity, info.getDislikeCount()));
binding.detailThumbsDownCountView.setVisibility(View.VISIBLE);
binding.detailThumbsDownImgView.setVisibility(View.VISIBLE);
2017-12-08 15:05:08 +01:00
} else {
binding.detailThumbsDownCountView.setVisibility(View.GONE);
binding.detailThumbsDownImgView.setVisibility(View.GONE);
2017-12-08 15:05:08 +01:00
}
2017-06-28 03:39:33 +02:00
2017-12-08 15:05:08 +01:00
if (info.getLikeCount() >= 0) {
binding.detailThumbsUpCountView.setText(Localization.shortCount(activity,
info.getLikeCount()));
binding.detailThumbsUpCountView.setVisibility(View.VISIBLE);
binding.detailThumbsUpImgView.setVisibility(View.VISIBLE);
2017-12-08 15:05:08 +01:00
} else {
binding.detailThumbsUpCountView.setVisibility(View.GONE);
binding.detailThumbsUpImgView.setVisibility(View.GONE);
2017-12-08 15:05:08 +01:00
}
binding.detailThumbsDisabledView.setVisibility(View.GONE);
}
if (info.getDuration() > 0) {
binding.detailDurationView.setText(Localization.getDurationString(info.getDuration()));
binding.detailDurationView.setBackgroundColor(
2019-01-31 13:24:02 +01:00
ContextCompat.getColor(activity, R.color.duration_background_color));
animate(binding.detailDurationView, true, 100);
} else if (info.getStreamType() == StreamType.LIVE_STREAM) {
binding.detailDurationView.setText(R.string.duration_live);
binding.detailDurationView.setBackgroundColor(
2019-01-31 13:24:02 +01:00
ContextCompat.getColor(activity, R.color.live_duration_background_color));
animate(binding.detailDurationView, true, 100);
} else {
binding.detailDurationView.setVisibility(View.GONE);
}
binding.detailTitleRootLayout.setClickable(true);
2021-01-17 15:59:29 +01:00
binding.detailToggleSecondaryControlsView.setRotation(0);
binding.detailToggleSecondaryControlsView.setVisibility(View.VISIBLE);
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
2019-04-13 09:31:32 +02:00
updateProgressInfo(info);
initThumbnailViews(info);
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
binding.detailMetaInfoSeparator, disposables);
if (!isPlayerAvailable() || player.isStopped()) {
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());
2020-07-14 19:21:32 +02:00
}
2017-12-08 15:05:08 +01:00
if (!info.getErrors().isEmpty()) {
// Bandcamp fan pages are not yet supported and thus a ContentNotAvailableException is
// thrown. This is not an error and thus should not be shown to the user.
for (final Throwable throwable : info.getErrors()) {
if (throwable instanceof ContentNotSupportedException
&& "Fan pages are not supported".equals(throwable.getMessage())) {
info.getErrors().remove(throwable);
}
}
if (!info.getErrors().isEmpty()) {
showSnackBarError(new ErrorInfo(info.getErrors(),
UserAction.REQUESTED_STREAM, info.getUrl(), info));
}
}
Add support of other delivery methods than progressive HTTP (in the player only) Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
2022-06-16 11:13:19 +02:00
binding.detailControlsDownload.setVisibility(
2022-06-18 17:49:04 +02:00
StreamTypeUtil.isLiveStream(info.getStreamType()) ? View.GONE : View.VISIBLE);
binding.detailControlsBackground.setVisibility(info.getAudioStreams().isEmpty()
? View.GONE : View.VISIBLE);
final boolean noVideoStreams =
info.getVideoStreams().isEmpty() && info.getVideoOnlyStreams().isEmpty();
binding.detailControlsPopup.setVisibility(noVideoStreams ? View.GONE : View.VISIBLE);
binding.detailThumbnailPlayButton.setImageResource(
noVideoStreams ? R.drawable.ic_headset_shadow : R.drawable.ic_play_arrow_shadow);
}
private void displayUploaderAsSubChannel(final StreamInfo info) {
binding.detailSubChannelTextView.setText(info.getUploaderName());
binding.detailSubChannelTextView.setVisibility(View.VISIBLE);
binding.detailSubChannelTextView.setSelected(true);
binding.detailUploaderTextView.setVisibility(View.GONE);
}
private void displayBothUploaderAndSubChannel(final StreamInfo info) {
binding.detailSubChannelTextView.setText(info.getSubChannelName());
binding.detailSubChannelTextView.setVisibility(View.VISIBLE);
binding.detailSubChannelTextView.setSelected(true);
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
if (!isEmpty(info.getUploaderName())) {
binding.detailUploaderTextView.setText(
String.format(getString(R.string.video_detail_by), info.getUploaderName()));
binding.detailUploaderTextView.setVisibility(View.VISIBLE);
binding.detailUploaderTextView.setSelected(true);
} else {
binding.detailUploaderTextView.setVisibility(View.GONE);
}
}
public void openDownloadDialog() {
if (currentInfo == null) {
return;
}
try {
2022-06-18 17:40:22 +02:00
final DownloadDialog downloadDialog = new DownloadDialog(activity, currentInfo);
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
2020-08-16 10:24:58 +02:00
} catch (final Exception e) {
2021-12-01 09:43:24 +01:00
ErrorUtil.showSnackbar(activity, new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG,
"Showing download dialog", currentInfo));
}
}
/*//////////////////////////////////////////////////////////////////////////
// Stream Results
//////////////////////////////////////////////////////////////////////////*/
2019-04-13 09:31:32 +02:00
private void updateProgressInfo(@NonNull final StreamInfo info) {
if (positionSubscriber != null) {
positionSubscriber.dispose();
}
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
final boolean playbackResumeEnabled = prefs
.getBoolean(activity.getString(R.string.enable_watch_history_key), true)
&& prefs.getBoolean(activity.getString(R.string.enable_playback_resume_key), true);
final boolean showPlaybackPosition = prefs.getBoolean(
activity.getString(R.string.enable_playback_state_lists_key), true);
if (!playbackResumeEnabled) {
if (playQueue == null || playQueue.getStreams().isEmpty()
2020-07-14 19:21:32 +02:00
|| playQueue.getItem().getRecoveryPosition() == RECOVERY_UNSET
|| !showPlaybackPosition) {
binding.positionView.setVisibility(View.INVISIBLE);
binding.detailPositionView.setVisibility(View.GONE);
2020-07-13 03:17:21 +02:00
// TODO: Remove this check when separation of concerns is done.
// (live streams weren't getting updated because they are mixed)
Add support of other delivery methods than progressive HTTP (in the player only) Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
2022-06-16 11:13:19 +02:00
if (!StreamTypeUtil.isLiveStream(info.getStreamType())) {
2020-07-13 03:17:21 +02:00
return;
}
} else {
// Show saved position from backStack if user allows it
showPlaybackProgress(playQueue.getItem().getRecoveryPosition(),
playQueue.getItem().getDuration() * 1000);
animate(binding.positionView, true, 500);
animate(binding.detailPositionView, true, 500);
}
2020-07-14 19:21:32 +02:00
return;
}
2019-04-13 09:31:32 +02:00
final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext());
// TODO: Separate concerns when updating database data.
// (move the updating part to when the loading happens)
2019-04-13 09:31:32 +02:00
positionSubscriber = recordManager.loadStreamState(info)
.subscribeOn(Schedulers.io())
.onErrorComplete()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(state -> {
showPlaybackProgress(state.getProgressMillis(), info.getDuration() * 1000);
animate(binding.positionView, true, 500);
animate(binding.detailPositionView, true, 500);
2019-04-13 09:31:32 +02:00
}, e -> {
if (DEBUG) {
e.printStackTrace();
}
2019-04-13 09:31:32 +02:00
}, () -> {
binding.positionView.setVisibility(View.GONE);
binding.detailPositionView.setVisibility(View.GONE);
2019-04-13 09:31:32 +02:00
});
}
private void showPlaybackProgress(final long progress, final long duration) {
final int progressSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(progress);
final int durationSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(duration);
2020-08-16 21:44:27 +02:00
// If the old and the new progress values have a big difference then use
// animation. Otherwise don't because it affects CPU
final boolean shouldAnimate = Math.abs(binding.positionView.getProgress()
- progressSeconds) > 2;
binding.positionView.setMax(durationSeconds);
2020-08-16 21:44:27 +02:00
if (shouldAnimate) {
binding.positionView.setProgressAnimated(progressSeconds);
} else {
binding.positionView.setProgress(progressSeconds);
}
final String position = Localization.getDurationString(progressSeconds);
if (position != binding.detailPositionView.getText()) {
binding.detailPositionView.setText(position);
}
if (binding.positionView.getVisibility() != View.VISIBLE) {
animate(binding.positionView, true, 100);
animate(binding.detailPositionView, true, 100);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Player event listener
//////////////////////////////////////////////////////////////////////////*/
2022-04-08 09:35:14 +02:00
@Override
public void onViewCreated() {
tryAddVideoPlayerView();
2022-04-08 09:35:14 +02:00
}
@Override
public void onQueueUpdate(final PlayQueue queue) {
playQueue = queue;
if (DEBUG) {
Log.d(TAG, "onQueueUpdate() called with: serviceId = ["
+ serviceId + "], videoUrl = [" + url + "], name = ["
+ title + "], playQueue = [" + playQueue + "]");
}
2020-07-14 19:21:32 +02:00
// This should be the only place where we push data to stack.
// It will allow to have live instance of PlayQueue with actual information about
// deleted/added items inside Channel/Playlist queue and makes possible to have
// a history of played items
@Nullable final StackItem stackPeek = stack.peek();
if (stackPeek != null && !stackPeek.getPlayQueue().equals(queue)) {
@Nullable final PlayQueueItem playQueueItem = queue.getItem();
if (playQueueItem != null) {
stack.push(new StackItem(playQueueItem.getServiceId(), playQueueItem.getUrl(),
playQueueItem.getTitle(), queue));
return;
} // else continue below
2020-02-29 00:57:54 +01:00
}
@Nullable final StackItem stackWithQueue = findQueueInStack(queue);
if (stackWithQueue != null) {
// On every MainPlayer service's destroy() playQueue gets disposed and
// no longer able to track progress. That's why we update our cached disposed
// queue with the new one that is active and have the same history.
// Without that the cached playQueue will have an old recovery position
stackWithQueue.setPlayQueue(queue);
}
}
@Override
2020-07-14 19:21:32 +02:00
public void onPlaybackUpdate(final int state,
final int repeatMode,
final boolean shuffled,
final PlaybackParameters parameters) {
setOverlayPlayPauseImage(player != null && player.isPlaying());
switch (state) {
2021-01-08 18:35:33 +01:00
case Player.STATE_PLAYING:
if (binding.positionView.getAlpha() != 1.0f
2020-01-10 15:32:05 +01:00
&& player.getPlayQueue() != null
&& player.getPlayQueue().getItem() != null
&& player.getPlayQueue().getItem().getUrl().equals(url)) {
animate(binding.positionView, true, 100);
animate(binding.detailPositionView, true, 100);
2020-01-10 15:32:05 +01:00
}
break;
}
}
@Override
2020-07-14 19:21:32 +02:00
public void onProgressUpdate(final int currentProgress,
final int duration,
final int bufferPercent) {
// Progress updates every second even if media is paused. It's useless until playing
2021-01-08 18:35:33 +01:00
if (!player.isPlaying() || playQueue == null) {
2020-07-14 19:21:32 +02:00
return;
}
2020-07-14 19:21:32 +02:00
if (player.getPlayQueue().getItem().getUrl().equals(url)) {
2020-01-10 15:32:05 +01:00
showPlaybackProgress(currentProgress, duration);
2020-07-14 19:21:32 +02:00
}
}
@Override
public void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) {
final StackItem item = findQueueInStack(queue);
if (item != null) {
2020-07-14 19:21:32 +02:00
// When PlayQueue can have multiple streams (PlaylistPlayQueue or ChannelPlayQueue)
// every new played stream gives new title and url.
// StackItem contains information about first played stream. Let's update it here
item.setTitle(info.getName());
item.setUrl(info.getUrl());
}
2020-07-14 19:21:32 +02:00
// They are not equal when user watches something in popup while browsing in fragment and
// then changes screen orientation. In that case the fragment will set itself as
// a service listener and will receive initial call to onMetadataUpdate()
if (!queue.equals(playQueue)) {
return;
}
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());
2020-07-14 19:21:32 +02:00
if (currentInfo != null && info.getUrl().equals(currentInfo.getUrl())) {
return;
}
currentInfo = info;
setInitialData(info.getServiceId(), info.getUrl(), info.getName(), queue);
setAutoPlay(false);
// Delay execution just because it freezes the main thread, and while playing
// next/previous video you see visual glitches
// (when non-vertical video goes after vertical video)
prepareAndHandleInfoIfNeededAfterDelay(info, true, 200);
}
@Override
public void onPlayerError(final PlaybackException error, final boolean isCatchableException) {
if (!isCatchableException) {
// Properly exit from fullscreen
toggleFullscreenIfInFullscreenMode();
hideMainPlayerOnLoadingNewStream();
}
}
@Override
public void onServiceStopped() {
setOverlayPlayPauseImage(false);
2020-07-14 19:21:32 +02:00
if (currentInfo != null) {
updateOverlayData(currentInfo.getName(),
currentInfo.getUploaderName(),
currentInfo.getThumbnailUrl());
}
}
@Override
public void onFullscreenStateChanged(final boolean fullscreen) {
setupBrightness();
2022-04-08 09:35:14 +02:00
//noinspection SimplifyOptionalCallChains
if (!isPlayerAndPlayerServiceAvailable()
2022-04-08 09:35:14 +02:00
|| !player.UIs().get(MainPlayerUi.class).isPresent()
|| getRoot().map(View::getParent).orElse(null) == null) {
2020-07-14 19:21:32 +02:00
return;
}
if (fullscreen) {
2020-02-29 00:57:54 +01:00
hideSystemUiIfNeeded();
binding.overlayPlayPauseButton.requestFocus();
} else {
showSystemUi();
}
if (binding.relatedItemsLayout != null) {
binding.relatedItemsLayout.setVisibility(fullscreen ? View.GONE : View.VISIBLE);
2020-07-14 19:21:32 +02:00
}
scrollToTop();
tryAddVideoPlayerView();
}
@Override
public void onScreenRotationButtonClicked() {
2020-07-14 19:21:32 +02:00
// In tablet user experience will be better if screen will not be rotated
// from landscape to portrait every time.
// Just turn on fullscreen mode in landscape orientation
// or portrait & unlocked global orientation
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
if (DeviceUtils.isTablet(activity)
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
2022-04-08 09:35:14 +02:00
player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen);
return;
}
final int newOrientation = isLandscape
? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
: ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
activity.setRequestedOrientation(newOrientation);
}
/*
* Will scroll down to description view after long click on moreOptionsButton
* */
@Override
public void onMoreOptionsLongClicked() {
2020-07-14 19:21:32 +02:00
final CoordinatorLayout.LayoutParams params =
(CoordinatorLayout.LayoutParams) binding.appBarLayout.getLayoutParams();
final AppBarLayout.Behavior behavior = (AppBarLayout.Behavior) params.getBehavior();
2020-07-14 19:21:32 +02:00
final ValueAnimator valueAnimator = ValueAnimator
.ofInt(0, -binding.playerPlaceholder.getHeight());
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(animation -> {
behavior.setTopAndBottomOffset((int) animation.getAnimatedValue());
binding.appBarLayout.requestLayout();
});
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.setDuration(500);
valueAnimator.start();
}
/*//////////////////////////////////////////////////////////////////////////
// Player related utils
//////////////////////////////////////////////////////////////////////////*/
private void showSystemUi() {
2020-07-14 19:21:32 +02:00
if (DEBUG) {
Log.d(TAG, "showSystemUi() called");
}
2020-07-14 19:21:32 +02:00
if (activity == null) {
return;
}
// Prevent jumping of the player on devices with cutout
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
}
activity.getWindow().getDecorView().setSystemUiVisibility(0);
2020-09-15 13:43:43 +02:00
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
2022-07-06 23:46:20 +02:00
activity.getWindow().setStatusBarColor(ThemeHelper.resolveColorFromAttr(
requireContext(), android.R.attr.colorPrimary));
}
private void hideSystemUi() {
2020-07-14 19:21:32 +02:00
if (DEBUG) {
Log.d(TAG, "hideSystemUi() called");
}
2020-07-14 19:21:32 +02:00
if (activity == null) {
return;
}
// Prevent jumping of the player on devices with cutout
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}
2020-09-15 13:43:43 +02:00
int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
2020-09-15 13:43:43 +02:00
// In multiWindow mode status bar is not transparent for devices with cutout
// if I include this flag. So without it is better in this case
final boolean isInMultiWindow = DeviceUtils.isInMultiWindow(activity);
if (!isInMultiWindow) {
2020-09-15 13:43:43 +02:00
visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN;
}
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
2020-09-15 13:43:43 +02:00
2022-04-08 09:35:14 +02:00
if (isInMultiWindow || isFullscreen()) {
2020-09-15 13:43:43 +02:00
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
2020-09-15 18:50:46 +02:00
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
2020-09-15 13:43:43 +02:00
}
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
// Listener implementation
2022-07-15 19:53:48 +02:00
@Override
2020-02-29 00:57:54 +01:00
public void hideSystemUiIfNeeded() {
2022-04-08 09:35:14 +02:00
if (isFullscreen()
2020-07-14 19:21:32 +02:00
&& bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
hideSystemUi();
2020-07-14 19:21:32 +02:00
}
}
2022-04-08 09:35:14 +02:00
private boolean isFullscreen() {
return isPlayerAvailable() && player.UIs().get(VideoPlayerUi.class)
.map(VideoPlayerUi::isFullscreen).orElse(false);
}
2020-02-29 00:57:54 +01:00
private boolean playerIsNotStopped() {
return isPlayerAvailable() && !player.isStopped();
2020-02-29 00:57:54 +01:00
}
private void restoreDefaultBrightness() {
final WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
if (lp.screenBrightness == -1) {
return;
}
// Restore the old brightness when fragment.onPause() called or
// when a player is in portrait
lp.screenBrightness = -1;
activity.getWindow().setAttributes(lp);
}
private void setupBrightness() {
2020-07-14 19:21:32 +02:00
if (activity == null) {
return;
}
final WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
2022-04-08 09:35:14 +02:00
if (!isFullscreen() || bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
// Apply system brightness when the player is not in fullscreen
restoreDefaultBrightness();
} else {
2020-12-26 18:47:08 +01:00
// Do not restore if user has disabled brightness gesture
if (!PlayerHelper.isBrightnessGestureEnabled(activity)) {
return;
}
// Restore already saved brightness level
final float brightnessLevel = PlayerHelper.getScreenBrightness(activity);
if (brightnessLevel == lp.screenBrightness) {
return;
2020-07-14 19:21:32 +02:00
}
lp.screenBrightness = brightnessLevel;
activity.getWindow().setAttributes(lp);
}
}
/**
* Make changes to the UI to accommodate for better usability on bigger screens such as TVs
* or in Android's desktop mode (DeX etc.)
*/
private void accommodateForTvAndDesktopMode() {
if (DeviceUtils.isTv(getContext())) {
// remove ripple effects from detail controls
final int transparent = ContextCompat.getColor(requireContext(),
R.color.transparent_background_color);
binding.detailControlsPlaylistAppend.setBackgroundColor(transparent);
binding.detailControlsBackground.setBackgroundColor(transparent);
binding.detailControlsPopup.setBackgroundColor(transparent);
binding.detailControlsDownload.setBackgroundColor(transparent);
binding.detailControlsShare.setBackgroundColor(transparent);
binding.detailControlsOpenInBrowser.setBackgroundColor(transparent);
binding.detailControlsPlayWithKodi.setBackgroundColor(transparent);
}
if (DeviceUtils.isDesktopMode(getContext())) {
// Remove the "hover" overlay (since it is visible on all mouse events and interferes
// with the video content being played)
binding.detailThumbnailRootLayout.setForeground(null);
}
}
private void checkLandscape() {
2020-07-14 19:21:32 +02:00
if ((!player.isPlaying() && player.getPlayQueue() != playQueue)
|| player.getPlayQueue() == null) {
setAutoPlay(true);
2020-07-14 19:21:32 +02:00
}
2022-04-08 09:35:14 +02:00
player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::checkLandscape);
// Let's give a user time to look at video information page if video is not playing
if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
2021-01-08 18:35:33 +01:00
player.play();
}
}
/*
2020-07-14 19:21:32 +02:00
* Means that the player fragment was swiped away via BottomSheetLayout
* and is empty but ready for any new actions. See cleanUp()
* */
private boolean wasCleared() {
return url == null;
}
@Nullable
private StackItem findQueueInStack(final PlayQueue queue) {
StackItem item = null;
final Iterator<StackItem> iterator = stack.descendingIterator();
while (iterator.hasNext()) {
final StackItem next = iterator.next();
if (next.getPlayQueue().equals(queue)) {
item = next;
break;
}
}
return item;
}
2020-06-27 05:25:50 +02:00
private void replaceQueueIfUserConfirms(final Runnable onAllow) {
@Nullable final PlayQueue activeQueue = isPlayerAvailable() ? player.getPlayQueue() : null;
2020-06-27 05:25:50 +02:00
// Player will have STATE_IDLE when a user pressed back button
2020-06-27 05:25:50 +02:00
if (isClearingQueueConfirmationRequired(activity)
&& playerIsNotStopped()
&& activeQueue != null
&& !activeQueue.equals(playQueue)) {
2020-06-27 05:25:50 +02:00
showClearingQueueConfirmation(onAllow);
} else {
onAllow.run();
}
}
private void showClearingQueueConfirmation(final Runnable onAllow) {
new AlertDialog.Builder(activity)
.setTitle(R.string.clear_queue_confirmation_description)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok, (dialog, which) -> {
onAllow.run();
dialog.dismiss();
}).show();
}
private void showExternalPlaybackDialog() {
Add support of other delivery methods than progressive HTTP (in the player only) Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
2022-06-16 11:13:19 +02:00
if (currentInfo == null) {
return;
}
Add support of other delivery methods than progressive HTTP (in the player only) Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
2022-06-16 11:13:19 +02:00
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.select_quality_external_players);
builder.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
ShareUtils.openUrlInBrowser(requireActivity(), url));
2022-06-18 17:49:04 +02:00
final List<VideoStream> videoStreamsForExternalPlayers =
2022-06-18 17:49:04 +02:00
ListHelper.getSortedStreamVideosList(
activity,
getUrlAndNonTorrentStreams(currentInfo.getVideoStreams()),
getUrlAndNonTorrentStreams(currentInfo.getVideoOnlyStreams()),
2022-06-18 17:49:04 +02:00
false,
false
);
Add support of other delivery methods than progressive HTTP (in the player only) Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
2022-06-16 11:13:19 +02:00
if (videoStreamsForExternalPlayers.isEmpty()) {
builder.setMessage(R.string.no_video_streams_available_for_external_players);
builder.setPositiveButton(R.string.ok, null);
2022-06-18 17:49:04 +02:00
Add support of other delivery methods than progressive HTTP (in the player only) Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
2022-06-16 11:13:19 +02:00
} else {
final int selectedVideoStreamIndexForExternalPlayers =
ListHelper.getDefaultResolutionIndex(activity, videoStreamsForExternalPlayers);
2022-07-29 21:21:15 +02:00
final CharSequence[] resolutions = videoStreamsForExternalPlayers.stream()
.map(VideoStream::getResolution).toArray(CharSequence[]::new);
Add support of other delivery methods than progressive HTTP (in the player only) Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
2022-06-16 11:13:19 +02:00
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers,
null);
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(R.string.ok, (dialog, i) -> {
final int index = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
// We don't have to manage the index validity because if there is no stream
// available for external players, this code will be not executed and if there is
// no stream which matches the default resolution, 0 is returned by
// ListHelper.getDefaultResolutionIndex.
// The index cannot be outside the bounds of the list as its always between 0 and
// the list size - 1, .
startOnExternalPlayer(activity, currentInfo,
videoStreamsForExternalPlayers.get(index));
});
}
builder.show();
}
/*
* Remove unneeded information while waiting for a next task
* */
private void cleanUp() {
// New beginning
stack.clear();
2020-07-14 19:21:32 +02:00
if (currentWorker != null) {
currentWorker.dispose();
}
playerHolder.stopService();
setInitialData(0, null, "", null);
currentInfo = null;
updateOverlayData(null, null, null);
}
/*//////////////////////////////////////////////////////////////////////////
// Bottom mini player
//////////////////////////////////////////////////////////////////////////*/
/**
* That's for Android TV support. Move focus from main fragment to the player or back
* based on what is currently selected
*
* @param toMain if true than the main fragment will be focused or the player otherwise
*/
private void moveFocusToMainFragment(final boolean toMain) {
setupBrightness();
final ViewGroup mainFragment = requireActivity().findViewById(R.id.fragment_holder);
// Hamburger button steels a focus even under bottomSheet
final Toolbar toolbar = requireActivity().findViewById(R.id.toolbar);
final int afterDescendants = ViewGroup.FOCUS_AFTER_DESCENDANTS;
final int blockDescendants = ViewGroup.FOCUS_BLOCK_DESCENDANTS;
if (toMain) {
mainFragment.setDescendantFocusability(afterDescendants);
toolbar.setDescendantFocusability(afterDescendants);
((ViewGroup) requireView()).setDescendantFocusability(blockDescendants);
// Only focus the mainFragment if the mainFragment (e.g. search-results)
// or the toolbar (e.g. Textfield for search) don't have focus.
// This was done to fix problems with the keyboard input, see also #7490
if (!mainFragment.hasFocus() && !toolbar.hasFocus()) {
mainFragment.requestFocus();
}
} else {
mainFragment.setDescendantFocusability(blockDescendants);
toolbar.setDescendantFocusability(blockDescendants);
((ViewGroup) requireView()).setDescendantFocusability(afterDescendants);
// Only focus the player if it not already has focus
if (!binding.getRoot().hasFocus()) {
binding.detailThumbnailRootLayout.requestFocus();
}
}
}
/**
* When the mini player exists the view underneath it is not touchable.
* Bottom padding should be equal to the mini player's height in this case
*
* @param showMore whether main fragment should be expanded or not
*/
private void manageSpaceAtTheBottom(final boolean showMore) {
final int peekHeight = getResources().getDimensionPixelSize(R.dimen.mini_player_height);
final ViewGroup holder = requireActivity().findViewById(R.id.fragment_holder);
final int newBottomPadding;
if (showMore) {
newBottomPadding = 0;
} else {
newBottomPadding = peekHeight;
}
if (holder.getPaddingBottom() == newBottomPadding) {
return;
}
holder.setPadding(holder.getPaddingLeft(),
holder.getPaddingTop(),
holder.getPaddingRight(),
newBottomPadding);
}
private void setupBottomPlayer() {
2020-07-14 19:21:32 +02:00
final CoordinatorLayout.LayoutParams params =
(CoordinatorLayout.LayoutParams) binding.appBarLayout.getLayoutParams();
final AppBarLayout.Behavior behavior = (AppBarLayout.Behavior) params.getBehavior();
final FrameLayout bottomSheetLayout = activity.findViewById(R.id.fragment_player_holder);
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout);
bottomSheetBehavior.setState(lastStableBottomSheetState);
updateBottomSheetState(lastStableBottomSheetState);
final int peekHeight = getResources().getDimensionPixelSize(R.dimen.mini_player_height);
if (bottomSheetState != BottomSheetBehavior.STATE_HIDDEN) {
manageSpaceAtTheBottom(false);
bottomSheetBehavior.setPeekHeight(peekHeight);
if (bottomSheetState == BottomSheetBehavior.STATE_COLLAPSED) {
binding.overlayLayout.setAlpha(MAX_OVERLAY_ALPHA);
} else if (bottomSheetState == BottomSheetBehavior.STATE_EXPANDED) {
binding.overlayLayout.setAlpha(0);
setOverlayElementsClickable(false);
}
}
bottomSheetCallback = new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull final View bottomSheet, final int newState) {
updateBottomSheetState(newState);
switch (newState) {
case BottomSheetBehavior.STATE_HIDDEN:
moveFocusToMainFragment(true);
manageSpaceAtTheBottom(true);
bottomSheetBehavior.setPeekHeight(0);
cleanUp();
break;
case BottomSheetBehavior.STATE_EXPANDED:
moveFocusToMainFragment(false);
manageSpaceAtTheBottom(false);
bottomSheetBehavior.setPeekHeight(peekHeight);
2020-07-14 19:21:32 +02:00
// Disable click because overlay buttons located on top of buttons
// from the player
setOverlayElementsClickable(false);
2020-02-29 00:57:54 +01:00
hideSystemUiIfNeeded();
2020-06-27 05:25:50 +02:00
// Conditions when the player should be expanded to fullscreen
if (DeviceUtils.isLandscape(requireContext())
&& isPlayerAvailable()
&& player.isPlaying()
2022-04-08 09:35:14 +02:00
&& !isFullscreen()
&& !DeviceUtils.isTablet(activity)) {
player.UIs().get(MainPlayerUi.class)
.ifPresent(MainPlayerUi::toggleFullscreen);
2020-07-14 19:21:32 +02:00
}
setOverlayLook(binding.appBarLayout, behavior, 1);
break;
case BottomSheetBehavior.STATE_COLLAPSED:
moveFocusToMainFragment(true);
manageSpaceAtTheBottom(false);
bottomSheetBehavior.setPeekHeight(peekHeight);
// Re-enable clicks
setOverlayElementsClickable(true);
if (isPlayerAvailable()) {
2022-04-08 09:35:14 +02:00
player.UIs().get(MainPlayerUi.class)
.ifPresent(MainPlayerUi::closeItemsList);
2020-07-14 19:21:32 +02:00
}
setOverlayLook(binding.appBarLayout, behavior, 0);
break;
case BottomSheetBehavior.STATE_DRAGGING:
case BottomSheetBehavior.STATE_SETTLING:
2022-04-08 09:35:14 +02:00
if (isFullscreen()) {
2020-07-14 19:21:32 +02:00
showSystemUi();
}
2022-04-08 09:35:14 +02:00
if (isPlayerAvailable()) {
player.UIs().get(MainPlayerUi.class).ifPresent(ui -> {
if (ui.isControlsVisible()) {
ui.hideControls(0, 0);
}
2022-04-08 09:35:14 +02:00
});
2020-07-14 19:21:32 +02:00
}
break;
case BottomSheetBehavior.STATE_HALF_EXPANDED:
break;
}
}
@Override
public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
setOverlayLook(binding.appBarLayout, behavior, slideOffset);
}
};
bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback);
// User opened a new page and the player will hide itself
activity.getSupportFragmentManager().addOnBackStackChangedListener(() -> {
2020-07-14 19:21:32 +02:00
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
2020-07-14 19:21:32 +02:00
}
});
}
private void updateOverlayData(@Nullable final String overlayTitle,
2020-07-14 19:21:32 +02:00
@Nullable final String uploader,
@Nullable final String thumbnailUrl) {
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
binding.overlayThumbnail.setImageDrawable(null);
PicassoHelper.loadDetailsThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.overlayThumbnail);
}
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {
2021-03-27 15:45:49 +01:00
final int drawable = playerIsPlaying
? R.drawable.ic_pause
: R.drawable.ic_play_arrow;
binding.overlayPlayPauseButton.setImageResource(drawable);
}
2020-07-14 19:21:32 +02:00
private void setOverlayLook(final AppBarLayout appBar,
final AppBarLayout.Behavior behavior,
final float slideOffset) {
// SlideOffset < 0 when mini player is about to close via swipe.
// Stop animation in this case
if (behavior == null || slideOffset < 0) {
return;
}
binding.overlayLayout.setAlpha(Math.min(MAX_OVERLAY_ALPHA, 1 - slideOffset));
// These numbers are not special. They just do a cool transition
behavior.setTopAndBottomOffset(
(int) (-binding.detailThumbnailImageView.getHeight() * 2 * (1 - slideOffset) / 3));
appBar.requestLayout();
}
private void setOverlayElementsClickable(final boolean enable) {
binding.overlayThumbnail.setClickable(enable);
binding.overlayThumbnail.setLongClickable(enable);
binding.overlayMetadataLayout.setClickable(enable);
binding.overlayMetadataLayout.setLongClickable(enable);
binding.overlayButtonsLayout.setClickable(enable);
binding.overlayPlayQueueButton.setClickable(enable);
binding.overlayPlayPauseButton.setClickable(enable);
binding.overlayCloseButton.setClickable(enable);
}
// helpers to check the state of player and playerService
boolean isPlayerAvailable() {
return (player != null);
}
boolean isPlayerServiceAvailable() {
return (playerService != null);
}
boolean isPlayerAndPlayerServiceAvailable() {
return (player != null && playerService != null);
}
2022-04-08 09:35:14 +02:00
public Optional<View> getRoot() {
if (player == null) {
return Optional.empty();
}
return player.UIs().get(VideoPlayerUi.class)
.map(playerUi -> playerUi.getBinding().getRoot());
}
private void updateBottomSheetState(final int newState) {
bottomSheetState = newState;
if (newState != BottomSheetBehavior.STATE_DRAGGING
&& newState != BottomSheetBehavior.STATE_SETTLING) {
lastStableBottomSheetState = newState;
}
}
}