diff --git a/README.md b/README.md index dee4ed2bd..8acf8ec43 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ NewPipe does not use any Google framework libraries, or the YouTube API. It only * Search channels * Watch videos from a channel * Orbot/Tor support (not yet directly) +* 1080p/2k/4k support ### Coming Features @@ -56,7 +57,6 @@ NewPipe does not use any Google framework libraries, or the YouTube API. It only * Search/Watch Playlists * Queeing videos * Subtitles support -* 1080p support * livestream support * ... and many more diff --git a/app/src/main/java/org/schabi/newpipe/extractor b/app/src/main/java/org/schabi/newpipe/extractor index 6ab3dc876..08457de76 160000 --- a/app/src/main/java/org/schabi/newpipe/extractor +++ b/app/src/main/java/org/schabi/newpipe/extractor @@ -1 +1 @@ -Subproject commit 6ab3dc876ebab4ed32f4ae60d3d04d000a7ea0e8 +Subproject commit 08457de7631253a23ba57cd547fe056c00eebc21 diff --git a/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java index fd28a3d23..52f42cf84 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java @@ -413,4 +413,9 @@ public class ChannelFragment extends Fragment implements ChannelExtractorWorker. public void onError(int messageId) { Toast.makeText(activity, messageId, Toast.LENGTH_LONG).show(); } + + @Override + public void onUnrecoverableError(Exception exception) { + activity.finish(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java index d60fac737..007cb0082 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java @@ -88,7 +88,7 @@ class ActionBarHandler { VideoStream item = videoStreams.get(i); itemArray[i] = MediaFormat.getNameById(item.format) + " " + item.resolution; } - int defaultResolution = Utils.getPreferredResolution(activity, videoStreams); + int defaultResolution = Utils.getDefaultResolution(activity, videoStreams); ArrayAdapter itemAdapter = new ArrayAdapter<>(activity.getBaseContext(), android.R.layout.simple_spinner_dropdown_item, itemArray); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 8e803ff87..05b9908c0 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -88,6 +88,7 @@ public class VideoDetailFragment extends Fragment implements StreamExtractorWork private AppCompatActivity activity; private OnItemSelectedListener onItemSelectedListener; + private ArrayList sortedStreamVideosList; private ActionBarHandler actionBarHandler; private InfoItemBuilder infoItemBuilder = null; @@ -98,8 +99,11 @@ public class VideoDetailFragment extends Fragment implements StreamExtractorWork private String videoUrl; private int serviceId = -1; + private AtomicBoolean wasLoading = new AtomicBoolean(false); private AtomicBoolean isLoading = new AtomicBoolean(false); - private boolean needUpdate = false; + private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1; + private static final int RESOLUTIONS_MENU_UPDATE_FLAG = 0x2; + private int updateFlags = 0; private boolean autoPlayEnabled; private boolean showRelatedStreams; @@ -203,7 +207,8 @@ public class VideoDetailFragment extends Fragment implements StreamExtractorWork public void onViewCreated(View rootView, Bundle savedInstanceState) { initViews(rootView); initListeners(); - isLoading.set(true); + selectAndLoadVideo(serviceId, videoUrl, videoTitle); + wasLoading.set(false); } @Override @@ -251,19 +256,23 @@ public class VideoDetailFragment extends Fragment implements StreamExtractorWork // Currently only used for enable/disable related videos // but can be extended for other live settings changes - if (needUpdate) { - if (relatedStreamsView != null) initRelatedVideos(currentStreamInfo); - needUpdate = false; + if (updateFlags != 0) { + if (!isLoading.get()) { + if ((updateFlags & RELATED_STREAMS_UPDATE_FLAG) != 0) initRelatedVideos(currentStreamInfo); + if ((updateFlags & RESOLUTIONS_MENU_UPDATE_FLAG) != 0) setupActionBarHandler(currentStreamInfo); + } + updateFlags = 0; } // Check if it was loading when the activity was stopped/paused, // because when this happen, the curExtractorWorker is cancelled - if (isLoading.get()) selectAndLoadVideo(serviceId, videoUrl, videoTitle); + if (wasLoading.getAndSet(false)) selectAndLoadVideo(serviceId, videoUrl, videoTitle); } @Override public void onStop() { super.onStop(); + wasLoading.set(curExtractorWorker.isRunning()); if (curExtractorWorker != null && curExtractorWorker.isRunning()) curExtractorWorker.cancel(); } @@ -301,7 +310,11 @@ public class VideoDetailFragment extends Fragment implements StreamExtractorWork public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (key.equals(getString(R.string.show_next_video_key))) { showRelatedStreams = sharedPreferences.getBoolean(key, true); - needUpdate = true; + updateFlags |= RELATED_STREAMS_UPDATE_FLAG; + } else if (key.equals(getString(R.string.preferred_video_format_key)) + || key.equals(getString(R.string.default_resolution_key)) + || key.equals(getString(R.string.show_higher_resolutions_key))) { + updateFlags |= RESOLUTIONS_MENU_UPDATE_FLAG; } } @@ -480,7 +493,8 @@ public class VideoDetailFragment extends Fragment implements StreamExtractorWork activity.getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST); } - actionBarHandler.setupStreamList(info.video_streams); + sortedStreamVideosList = Utils.getSortedStreamVideosList(activity, info.video_streams, info.video_only_streams, false); + actionBarHandler.setupStreamList(sortedStreamVideosList); actionBarHandler.setOnShareListener(new ActionBarHandler.OnActionListener() { @Override public void onActionSelected(int selectedStreamId) { @@ -517,15 +531,10 @@ public class VideoDetailFragment extends Fragment implements StreamExtractorWork } if (streamThumbnail != null) ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail; - Intent i = new Intent(activity, PopupVideoPlayer.class); Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); - i.putExtra(AbstractPlayer.VIDEO_TITLE, info.title) - .putExtra(AbstractPlayer.CHANNEL_NAME, info.uploader) - .putExtra(AbstractPlayer.VIDEO_URL, info.webpage_url) - .putExtra(AbstractPlayer.INDEX_SEL_VIDEO_STREAM, selectedStreamId) - .putExtra(AbstractPlayer.VIDEO_STREAMS_LIST, new ArrayList<>(info.video_streams)); - if (info.start_position > 0) i.putExtra(AbstractPlayer.START_POSITION, info.start_position * 1000); - activity.startService(i); + Intent mIntent = NavigationHelper.getOpenPlayerIntent(activity, PopupVideoPlayer.class, info, selectedStreamId); + if (info.start_position > 0) mIntent.putExtra(AbstractPlayer.START_POSITION, info.start_position * 1000); + activity.startService(mIntent); } }); @@ -586,8 +595,8 @@ public class VideoDetailFragment extends Fragment implements StreamExtractorWork args.putString(DownloadDialog.FILE_SUFFIX_AUDIO, audioSuffix); } - if (info.video_streams != null) { - VideoStream selectedStreamItem = info.video_streams.get(selectedStreamId); + if (sortedStreamVideosList != null) { + VideoStream selectedStreamItem = sortedStreamVideosList.get(selectedStreamId); String videoSuffix = "." + MediaFormat.getSuffixById(selectedStreamItem.format); args.putString(DownloadDialog.FILE_SUFFIX_VIDEO, videoSuffix); args.putString(DownloadDialog.VIDEO_URL, selectedStreamItem.url); @@ -737,6 +746,7 @@ public class VideoDetailFragment extends Fragment implements StreamExtractorWork } public void loadSelectedVideo() { + isLoading.set(true); pushToStack(videoUrl, videoTitle); if (curExtractorWorker != null && curExtractorWorker.isRunning()) curExtractorWorker.cancel(); @@ -753,6 +763,7 @@ public class VideoDetailFragment extends Fragment implements StreamExtractorWork if (scrollY < 30) animateView(videoTitleTextView, false, 200, new Runnable() { @Override public void run() { + if (videoTitleTextView == null) return; videoTitleTextView.setText(videoTitle != null ? videoTitle : ""); animateView(videoTitleTextView, true, 400, null); } @@ -775,12 +786,11 @@ public class VideoDetailFragment extends Fragment implements StreamExtractorWork curExtractorWorker = new StreamExtractorWorker(activity, serviceId, videoUrl, this); curExtractorWorker.start(); - isLoading.set(true); } public void playVideo(StreamInfo info) { // ----------- THE MAGIC MOMENT --------------- - VideoStream selectedVideoStream = info.video_streams.get(actionBarHandler.getSelectedVideoStream()); + VideoStream selectedVideoStream = sortedStreamVideosList.get(actionBarHandler.getSelectedVideoStream()); if (PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(this.getString(R.string.use_external_video_player_key), false)) { @@ -813,30 +823,24 @@ public class VideoDetailFragment extends Fragment implements StreamExtractorWork builder.create().show(); } } else { - Intent intent; + Intent mIntent; boolean useOldPlayer = PreferenceManager.getDefaultSharedPreferences(activity) .getBoolean(getString(R.string.use_old_player_key), false) || (Build.VERSION.SDK_INT < 16); if (!useOldPlayer) { // ExoPlayer if (streamThumbnail != null) ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail; - intent = new Intent(activity, ExoPlayerActivity.class) - .putExtra(AbstractPlayer.VIDEO_TITLE, info.title) - .putExtra(AbstractPlayer.VIDEO_URL, info.webpage_url) - .putExtra(AbstractPlayer.CHANNEL_NAME, info.uploader) - .putExtra(AbstractPlayer.INDEX_SEL_VIDEO_STREAM, actionBarHandler.getSelectedVideoStream()) - .putExtra(AbstractPlayer.VIDEO_STREAMS_LIST, new ArrayList<>(info.video_streams)); - if (info.start_position > 0) intent.putExtra(AbstractPlayer.START_POSITION, info.start_position * 1000); + mIntent = NavigationHelper.getOpenPlayerIntent(activity, ExoPlayerActivity.class, info, actionBarHandler.getSelectedVideoStream()); + if (info.start_position > 0) mIntent.putExtra(AbstractPlayer.START_POSITION, info.start_position * 1000); } else { // Internal Player - intent = new Intent(activity, PlayVideoActivity.class) + mIntent = new Intent(activity, PlayVideoActivity.class) .putExtra(PlayVideoActivity.VIDEO_TITLE, info.title) .putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.url) .putExtra(PlayVideoActivity.VIDEO_URL, info.webpage_url) .putExtra(PlayVideoActivity.START_POSITION, info.start_position); } - //intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); + startActivity(mIntent); } } @@ -928,6 +932,7 @@ public class VideoDetailFragment extends Fragment implements StreamExtractorWork // Since newpipe is designed to work even if certain information is not available, // the UI has to react on missing information. + videoTitle = info.title; videoTitleTextView.setText(info.title); if (!info.uploader.isEmpty()) uploaderTextView.setText(info.uploader); uploaderTextView.setVisibility(!info.uploader.isEmpty() ? View.VISIBLE : View.GONE); @@ -1031,4 +1036,9 @@ public class VideoDetailFragment extends Fragment implements StreamExtractorWork thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.not_available_monkey)); Toast.makeText(activity, R.string.content_not_available, Toast.LENGTH_LONG).show(); } + + @Override + public void onUnrecoverableError(Exception exception) { + activity.finish(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java b/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java index e05477b98..2faf09cc3 100644 --- a/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; @@ -60,6 +61,7 @@ import com.google.android.exoplayer2.util.Util; import org.schabi.newpipe.ActivityCommunicator; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.stream_info.AudioStream; import org.schabi.newpipe.extractor.stream_info.VideoStream; import java.io.File; @@ -93,6 +95,7 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa public static final String VIDEO_URL = "video_url"; public static final String VIDEO_STREAMS_LIST = "video_streams_list"; + public static final String VIDEO_ONLY_AUDIO_STREAM = "video_only_audio_stream"; public static final String VIDEO_TITLE = "video_title"; public static final String INDEX_SEL_VIDEO_STREAM = "index_selected_video_stream"; public static final String START_POSITION = "start_position"; @@ -105,7 +108,8 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa private Bitmap videoThumbnail; private String channelName = ""; private int selectedIndexStream; - private ArrayList videoStreamsList; + private ArrayList videoStreamsList = new ArrayList<>(); + private AudioStream videoOnlyAudioStream; /*////////////////////////////////////////////////////////////////////////// // Player @@ -277,6 +281,9 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa if (serializable instanceof ArrayList) videoStreamsList = (ArrayList) serializable; if (serializable instanceof Vector) videoStreamsList = new ArrayList<>((List) serializable); + Serializable audioStream = intent.getSerializableExtra(VIDEO_ONLY_AUDIO_STREAM); + if (audioStream != null) videoOnlyAudioStream = (AudioStream) audioStream; + videoUrl = intent.getStringExtra(VIDEO_URL); videoTitle = intent.getStringExtra(VIDEO_TITLE); videoStartPos = intent.getIntExtra(START_POSITION, -1); @@ -288,13 +295,15 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa e.printStackTrace(); } - playVideo(getSelectedStreamUri(), true); + playVideo(getSelectedVideoStream(), true); } - public void playVideo(Uri videoURI, boolean autoPlay) { - if (DEBUG) Log.d(TAG, "playVideo() called with: videoURI = [" + videoURI + "], autoPlay = [" + autoPlay + "]"); + public void playVideo(VideoStream videoStream, boolean autoPlay) { + if (DEBUG) { + Log.d(TAG, "playVideo() called with: videoStream = [" + videoStream + ", " + videoStream.url + ", isVideoOnly = " + videoStream.isVideoOnly + "], autoPlay = [" + autoPlay + "]"); + } - if (videoURI == null || simpleExoPlayer == null) { + if (videoStream == null || videoStream.url == null || simpleExoPlayer == null) { onError(); return; } @@ -305,7 +314,7 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId); buildQualityMenu(qualityPopupMenu); - videoSource = buildMediaSource(videoURI, MediaFormat.getSuffixById(videoStreamsList.get(selectedIndexStream).format)); + videoSource = buildMediaSource(videoStream, MediaFormat.getSuffixById(getSelectedVideoStream().format)); if (simpleExoPlayer.getPlaybackState() != ExoPlayer.STATE_IDLE) simpleExoPlayer.stop(); if (videoStartPos > 0) simpleExoPlayer.seekTo(videoStartPos); @@ -323,22 +332,34 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa if (progressLoop != null) stopProgressLoop(); } - private MediaSource buildMediaSource(Uri uri, String overrideExtension) { - if (DEBUG) Log.d(TAG, "buildMediaSource() called with: uri = [" + uri + "], overrideExtension = [" + overrideExtension + "]"); + private MediaSource buildMediaSource(VideoStream videoStream, String overrideExtension) { + if (DEBUG) { + Log.d(TAG, "buildMediaSource() called with: videoStream = [" + videoStream + ", " + videoStream.url + "isVideoOnly = " + videoStream.isVideoOnly + "], overrideExtension = [" + overrideExtension + "]"); + } + Uri uri = Uri.parse(videoStream.url); int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); + MediaSource mediaSource; switch (type) { case C.TYPE_SS: - return new SsMediaSource(uri, cacheDataSourceFactory, new DefaultSsChunkSource.Factory(cacheDataSourceFactory), null, null); + mediaSource = new SsMediaSource(uri, cacheDataSourceFactory, new DefaultSsChunkSource.Factory(cacheDataSourceFactory), null, null); + break; case C.TYPE_DASH: - return new DashMediaSource(uri, cacheDataSourceFactory, new DefaultDashChunkSource.Factory(cacheDataSourceFactory), null, null); + mediaSource = new DashMediaSource(uri, cacheDataSourceFactory, new DefaultDashChunkSource.Factory(cacheDataSourceFactory), null, null); + break; case C.TYPE_HLS: - return new HlsMediaSource(uri, cacheDataSourceFactory, null, null); + mediaSource = new HlsMediaSource(uri, cacheDataSourceFactory, null, null); + break; case C.TYPE_OTHER: - return new ExtractorMediaSource(uri, cacheDataSourceFactory, extractorsFactory, null, null); + mediaSource = new ExtractorMediaSource(uri, cacheDataSourceFactory, extractorsFactory, null, null); + break; default: { throw new IllegalStateException("Unsupported type: " + type); } } + if (!videoStream.isVideoOnly) return mediaSource; + + Uri audioUri = Uri.parse(videoOnlyAudioStream.url); + return new MergingMediaSource(mediaSource, new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null)); } public void buildQualityMenu(PopupMenu popupMenu) { @@ -346,7 +367,7 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa VideoStream videoStream = videoStreamsList.get(i); popupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution); } - qualityTextView.setText(videoStreamsList.get(selectedIndexStream).resolution); + qualityTextView.setText(getSelectedVideoStream().resolution); popupMenu.setOnMenuItemClickListener(this); popupMenu.setOnDismissListener(this); @@ -590,7 +611,7 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa if (DEBUG) Log.d(TAG, "onVideoPlayPause() called"); if (currentState == STATE_COMPLETED) { changeState(STATE_LOADING); - if (qualityChanged) playVideo(getSelectedStreamUri(), true); + if (qualityChanged) playVideo(getSelectedVideoStream(), true); simpleExoPlayer.seekTo(0); return; } @@ -632,10 +653,10 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa if (selectedIndexStream == menuItem.getItemId()) return true; setVideoStartPos((int) getPlayer().getCurrentPosition()); - if (!(getCurrentState() == STATE_COMPLETED)) playVideo(Uri.parse(getVideoStreamsList().get(menuItem.getItemId()).url), wasPlaying); + selectedIndexStream = menuItem.getItemId(); + if (!(getCurrentState() == STATE_COMPLETED)) playVideo(getSelectedVideoStream(), wasPlaying); else qualityChanged = true; - selectedIndexStream = menuItem.getItemId(); qualityTextView.setText(menuItem.getTitle()); return true; } @@ -647,7 +668,7 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa public void onDismiss(PopupMenu menu) { if (DEBUG) Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]"); isQualityPopupMenuVisible = false; - qualityTextView.setText(videoStreamsList.get(selectedIndexStream).resolution); + qualityTextView.setText(getSelectedVideoStream().resolution); } public abstract void onFullScreenButtonClicked(); @@ -658,7 +679,7 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa isQualityPopupMenuVisible = true; animateView(getControlsRoot(), true, 300, 0); - VideoStream videoStream = videoStreamsList.get(selectedIndexStream); + VideoStream videoStream = getSelectedVideoStream(); qualityTextView.setText(MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution); wasPlaying = isPlaying(); } @@ -967,8 +988,12 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa return currentState; } + public VideoStream getSelectedVideoStream() { + return videoStreamsList.get(selectedIndexStream); + } + public Uri getSelectedStreamUri() { - return Uri.parse(videoStreamsList.get(selectedIndexStream).url); + return Uri.parse(getSelectedVideoStream().url); } public int getQualityPopupMenuGroupId() { @@ -1015,7 +1040,7 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa this.channelName = channelName; } - public int getSelectedIndexStream() { + public int getSelectedStreamIndex() { return selectedIndexStream; } @@ -1023,6 +1048,14 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa this.selectedIndexStream = selectedIndexStream; } + public void setAudioStream(AudioStream audioStream) { + this.videoOnlyAudioStream = audioStream; + } + + public AudioStream getAudioStream() { + return videoOnlyAudioStream; + } + public ArrayList getVideoStreamsList() { return videoStreamsList; } diff --git a/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java index 435fd189b..4aabad3ac 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java @@ -8,7 +8,6 @@ import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.graphics.Color; import android.media.AudioManager; -import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.annotation.Nullable; @@ -24,6 +23,8 @@ import android.widget.TextView; import android.widget.Toast; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.stream_info.VideoStream; +import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ThemeHelper; @@ -105,10 +106,9 @@ public class ExoPlayerActivity extends Activity { super.onResume(); if (DEBUG) Log.d(TAG, "onResume() called"); if (activityPaused) { - //playerImpl.getPlayer().setPlayWhenReady(true); playerImpl.getPlayPauseButton().setImageResource(R.drawable.ic_play_arrow_white); playerImpl.initPlayer(); - playerImpl.playVideo(playerImpl.getSelectedStreamUri(), false); + playerImpl.playVideo(playerImpl.getSelectedVideoStream(), false); activityPaused = false; } } @@ -238,8 +238,8 @@ public class ExoPlayerActivity extends Activity { } @Override - public void playVideo(Uri videoURI, boolean autoPlay) { - super.playVideo(videoURI, autoPlay); + public void playVideo(VideoStream videoStream, boolean autoPlay) { + super.playVideo(videoStream, autoPlay); playPauseButton.setImageResource(autoPlay ? R.drawable.ic_pause_white : R.drawable.ic_play_arrow_white); } @@ -254,16 +254,10 @@ public class ExoPlayerActivity extends Activity { return; } - Intent i = new Intent(ExoPlayerActivity.this, PopupVideoPlayer.class); - i.putExtra(AbstractPlayer.VIDEO_TITLE, getVideoTitle()) - .putExtra(AbstractPlayer.CHANNEL_NAME, getChannelName()) - .putExtra(AbstractPlayer.VIDEO_URL, getVideoUrl()) - .putExtra(AbstractPlayer.INDEX_SEL_VIDEO_STREAM, getSelectedIndexStream()) - .putExtra(AbstractPlayer.VIDEO_STREAMS_LIST, getVideoStreamsList()) - .putExtra(AbstractPlayer.START_POSITION, ((int) getPlayer().getCurrentPosition())); - context.startService(i); + if (playerImpl != null) playerImpl.destroy(); + context.startService(NavigationHelper.getOpenPlayerIntent(context, PopupVideoPlayer.class, playerImpl)); + ((View) getControlAnimationView().getParent()).setVisibility(View.GONE); - if (playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(false); ExoPlayerActivity.this.finish(); } @@ -346,7 +340,7 @@ public class ExoPlayerActivity extends Activity { @Override public void onDismiss(PopupMenu menu) { super.onDismiss(menu); - if (isPlaying()) animateView(getControlsRoot(), false, 500, 0, true); + if (isPlaying()) animateView(getControlsRoot(), false, 500, 0); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 6e4fa9776..216c4eb74 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -12,7 +12,6 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.PixelFormat; -import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.IBinder; @@ -36,18 +35,16 @@ import org.schabi.newpipe.ActivityCommunicator; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; +import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.stream_info.StreamExtractor; import org.schabi.newpipe.extractor.stream_info.StreamInfo; import org.schabi.newpipe.extractor.stream_info.VideoStream; import org.schabi.newpipe.util.Constants; +import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.Utils; - -import java.io.IOException; -import java.util.ArrayList; +import org.schabi.newpipe.workers.StreamExtractorWorker; /** * Service Popup Player implementing AbstractPlayer @@ -85,6 +82,7 @@ public class PopupVideoPlayer extends Service { private DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().cacheInMemory(true).build(); private AbstractPlayerImpl playerImpl; + private StreamExtractorWorker currentExtractorWorker; /*////////////////////////////////////////////////////////////////////////// // Service LifeCycle @@ -110,8 +108,8 @@ public class PopupVideoPlayer extends Service { if (imageLoader != null) imageLoader.clearMemoryCache(); if (intent.getStringExtra(Constants.KEY_URL) != null) { playerImpl.setStartedFromNewPipe(false); - Thread fetcher = new Thread(new FetcherRunnable(intent)); - fetcher.start(); + currentExtractorWorker = new StreamExtractorWorker(this, 0, intent.getStringExtra(Constants.KEY_URL), new FetcherRunnable(this)); + currentExtractorWorker.start(); } else { playerImpl.setStartedFromNewPipe(true); playerImpl.handleIntent(intent); @@ -135,6 +133,10 @@ public class PopupVideoPlayer extends Service { if (imageLoader != null) imageLoader.clearMemoryCache(); if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID); if (broadcastReceiver != null) unregisterReceiver(broadcastReceiver); + if (currentExtractorWorker != null) { + currentExtractorWorker.cancel(); + currentExtractorWorker = null; + } } @Override @@ -306,8 +308,8 @@ public class PopupVideoPlayer extends Service { } @Override - public void playVideo(Uri videoURI, boolean autoPlay) { - super.playVideo(videoURI, autoPlay); + public void playVideo(VideoStream videoStream, boolean autoPlay) { + super.playVideo(videoStream, autoPlay); windowLayoutParams.width = (int) getMinimumVideoWidth(currentPopupHeight); windowManager.updateViewLayout(getRootView(), windowLayoutParams); @@ -321,18 +323,8 @@ public class PopupVideoPlayer extends Service { public void onFullScreenButtonClicked() { if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called"); Intent intent; - //if (getSharedPreferences().getBoolean(getResources().getString(R.string.use_exoplayer_key), false)) { - // TODO: Remove this check when ExoPlayer is the default - // For now just disable the non-exoplayer player - //noinspection ConstantConditions,ConstantIfStatement - if (true) { - intent = new Intent(PopupVideoPlayer.this, ExoPlayerActivity.class) - .putExtra(AbstractPlayer.VIDEO_TITLE, getVideoTitle()) - .putExtra(AbstractPlayer.VIDEO_URL, getVideoUrl()) - .putExtra(AbstractPlayer.CHANNEL_NAME, getChannelName()) - .putExtra(AbstractPlayer.INDEX_SEL_VIDEO_STREAM, getSelectedIndexStream()) - .putExtra(AbstractPlayer.VIDEO_STREAMS_LIST, getVideoStreamsList()) - .putExtra(AbstractPlayer.START_POSITION, ((int) getPlayer().getCurrentPosition())); + if (!getSharedPreferences().getBoolean(getResources().getString(R.string.use_old_player_key), false)) { + intent = NavigationHelper.getOpenPlayerIntent(context, ExoPlayerActivity.class, playerImpl); if (!playerImpl.isStartedFromNewPipe()) intent.putExtra(AbstractPlayer.STARTED_FROM_NEWPIPE, false); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } else { @@ -343,8 +335,9 @@ public class PopupVideoPlayer extends Service { .putExtra(PlayVideoActivity.START_POSITION, Math.round(getPlayer().getCurrentPosition() / 1000f)); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } - context.startActivity(intent); stopSelf(); + if (playerImpl != null) playerImpl.destroy(); + context.startActivity(intent); } @Override @@ -510,84 +503,123 @@ public class PopupVideoPlayer extends Service { /** * Fetcher used if open by a link out of NewPipe */ - private class FetcherRunnable implements Runnable { - private final Intent intent; + private class FetcherRunnable implements StreamExtractorWorker.OnStreamInfoReceivedListener { + private final Context context; private final Handler mainHandler; - FetcherRunnable(Intent intent) { - this.intent = intent; + FetcherRunnable(Context context) { this.mainHandler = new Handler(PopupVideoPlayer.this.getMainLooper()); + this.context = context; } @Override - public void run() { - StreamExtractor streamExtractor; - try { - StreamingService service = NewPipe.getService(0); - if (service == null) return; - streamExtractor = service.getExtractorInstance(intent.getStringExtra(Constants.KEY_URL)); - StreamInfo info = StreamInfo.getVideoInfo(streamExtractor); - playerImpl.setVideoStreamsList(info.video_streams instanceof ArrayList - ? (ArrayList) info.video_streams - : new ArrayList<>(info.video_streams)); + public void onReceive(StreamInfo info) { + playerImpl.setVideoTitle(info.title); + playerImpl.setVideoUrl(info.webpage_url); + playerImpl.setChannelName(info.uploader); - int defaultResolution = Utils.getPreferredResolution(PopupVideoPlayer.this, info.video_streams); - playerImpl.setSelectedIndexStream(defaultResolution); + playerImpl.setVideoStreamsList(Utils.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false)); + playerImpl.setAudioStream(Utils.getHighestQualityAudio(info.audio_streams)); - if (DEBUG) { - Log.d(TAG, "FetcherRunnable.StreamExtractor: chosen = " - + MediaFormat.getNameById(info.video_streams.get(defaultResolution).format) + " " - + info.video_streams.get(defaultResolution).resolution + " > " - + info.video_streams.get(defaultResolution).url); - } + int defaultResolution = Utils.getPopupDefaultResolution(context, playerImpl.getVideoStreamsList()); + playerImpl.setSelectedIndexStream(defaultResolution); - playerImpl.setVideoUrl(info.webpage_url); - playerImpl.setVideoTitle(info.title); - playerImpl.setChannelName(info.uploader); - if (info.start_position > 0) playerImpl.setVideoStartPos(info.start_position * 1000); - else playerImpl.setVideoStartPos(-1); - - mainHandler.post(new Runnable() { - @Override - public void run() { - playerImpl.playVideo(playerImpl.getSelectedStreamUri(), true); - } - }); - - imageLoader.resume(); - imageLoader.loadImage(info.thumbnail_url, displayImageOptions, new SimpleImageLoadingListener() { - @Override - public void onLoadingComplete(String imageUri, View view, final Bitmap loadedImage) { - mainHandler.post(new Runnable() { - @Override - public void run() { - playerImpl.setVideoThumbnail(loadedImage); - if (loadedImage != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); - updateNotification(-1); - ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = loadedImage; - } - }); - } - }); - } catch (IOException ie) { - if (DEBUG) ie.printStackTrace(); - mainHandler.post(new Runnable() { - @Override - public void run() { - Toast.makeText(PopupVideoPlayer.this, R.string.network_error, Toast.LENGTH_SHORT).show(); - } - }); - stopSelf(); - } catch (Exception e) { - if (DEBUG) e.printStackTrace(); - mainHandler.post(new Runnable() { - @Override - public void run() { - Toast.makeText(PopupVideoPlayer.this, R.string.content_not_available, Toast.LENGTH_SHORT).show(); - } - }); - stopSelf(); + if (DEBUG) { + Log.d(TAG, "FetcherRunnable.StreamExtractor: chosen = " + + MediaFormat.getNameById(info.video_streams.get(defaultResolution).format) + " " + + info.video_streams.get(defaultResolution).resolution + " > " + + info.video_streams.get(defaultResolution).url); } + + if (info.start_position > 0) playerImpl.setVideoStartPos(info.start_position * 1000); + else playerImpl.setVideoStartPos(-1); + + mainHandler.post(new Runnable() { + @Override + public void run() { + playerImpl.playVideo(playerImpl.getSelectedVideoStream(), true); + } + }); + + imageLoader.resume(); + imageLoader.loadImage(info.thumbnail_url, displayImageOptions, new SimpleImageLoadingListener() { + @Override + public void onLoadingComplete(String imageUri, View view, final Bitmap loadedImage) { + mainHandler.post(new Runnable() { + @Override + public void run() { + playerImpl.setVideoThumbnail(loadedImage); + if (loadedImage != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); + updateNotification(-1); + ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = loadedImage; + } + }); + } + }); + } + + @Override + public void onError(final int messageId) { + mainHandler.post(new Runnable() { + @Override + public void run() { + Toast.makeText(context, messageId, Toast.LENGTH_LONG).show(); + } + }); + stopSelf(); + } + + @Override + public void onReCaptchaException() { + mainHandler.post(new Runnable() { + @Override + public void run() { + Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show(); + } + }); + // Starting ReCaptcha Challenge Activity + Intent intent = new Intent(context, ReCaptchaActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + stopSelf(); + } + + @Override + public void onBlockedByGemaError() { + mainHandler.post(new Runnable() { + @Override + public void run() { + Toast.makeText(context, R.string.blocked_by_gema, Toast.LENGTH_LONG).show(); + } + }); + stopSelf(); + } + + @Override + public void onContentErrorWithMessage(final int messageId) { + mainHandler.post(new Runnable() { + @Override + public void run() { + Toast.makeText(context, messageId, Toast.LENGTH_LONG).show(); + } + }); + stopSelf(); + } + + @Override + public void onContentError() { + mainHandler.post(new Runnable() { + @Override + public void run() { + Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show(); + } + }); + stopSelf(); + } + + @Override + public void onUnrecoverableError(Exception exception) { + stopSelf(); } } diff --git a/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java b/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java index c15042de8..14c72c561 100644 --- a/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java +++ b/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java @@ -121,6 +121,7 @@ public class ErrorActivity extends AppCompatActivity { Intent intent = new Intent(context, ErrorActivity.class); intent.putExtra(ERROR_INFO, errorInfo); intent.putExtra(ERROR_LIST, elToSl(el)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } }).show(); @@ -130,6 +131,7 @@ public class ErrorActivity extends AppCompatActivity { Intent intent = new Intent(context, ErrorActivity.class); intent.putExtra(ERROR_INFO, errorInfo); intent.putExtra(ERROR_LIST, elToSl(el)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } } @@ -180,7 +182,7 @@ public class ErrorActivity extends AppCompatActivity { Intent intent = new Intent(context, ErrorActivity.class); intent.putExtra(ERROR_INFO, errorInfo); intent.putExtra(ERROR_LIST, el); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsFragment.java index b30b2af48..5ca2fb180 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsFragment.java @@ -22,7 +22,6 @@ import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import java.util.ArrayList; -import java.util.List; import info.guardianproject.netcipher.proxy.OrbotHelper; @@ -53,6 +52,7 @@ public class SettingsFragment extends PreferenceFragment SharedPreferences.OnSharedPreferenceChangeListener prefListener; // get keys String DEFAULT_RESOLUTION_PREFERENCE; + String DEFAULT_POPUP_RESOLUTION_PREFERENCE; String PREFERRED_VIDEO_FORMAT_PREFERENCE; String DEFAULT_AUDIO_FORMAT_PREFERENCE; String SEARCH_LANGUAGE_PREFERENCE; @@ -61,6 +61,7 @@ public class SettingsFragment extends PreferenceFragment String USE_TOR_KEY; String THEME; private ListPreference defaultResolutionPreference; + private ListPreference defaultPopupResolutionPreference; private ListPreference preferredVideoFormatPreference; private ListPreference defaultAudioFormatPreference; private ListPreference searchLanguagePreference; @@ -80,6 +81,7 @@ public class SettingsFragment extends PreferenceFragment // get keys DEFAULT_RESOLUTION_PREFERENCE = getString(R.string.default_resolution_key); + DEFAULT_POPUP_RESOLUTION_PREFERENCE = getString(R.string.default_popup_resolution_key); PREFERRED_VIDEO_FORMAT_PREFERENCE = getString(R.string.preferred_video_format_key); DEFAULT_AUDIO_FORMAT_PREFERENCE = getString(R.string.default_audio_format_key); SEARCH_LANGUAGE_PREFERENCE = getString(R.string.search_language_key); @@ -91,6 +93,8 @@ public class SettingsFragment extends PreferenceFragment // get pref objects defaultResolutionPreference = (ListPreference) findPreference(DEFAULT_RESOLUTION_PREFERENCE); + defaultPopupResolutionPreference = + (ListPreference) findPreference(DEFAULT_POPUP_RESOLUTION_PREFERENCE); preferredVideoFormatPreference = (ListPreference) findPreference(PREFERRED_VIDEO_FORMAT_PREFERENCE); defaultAudioFormatPreference = @@ -103,6 +107,9 @@ public class SettingsFragment extends PreferenceFragment final String currentTheme = defaultPreferences.getString(THEME, "Light"); + // TODO: Clean this, as the class is already implementing the class + // and those double equals... + prefListener = new SharedPreferences.OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, @@ -260,6 +267,9 @@ public class SettingsFragment extends PreferenceFragment defaultResolutionPreference.setSummary( defaultPreferences.getString(DEFAULT_RESOLUTION_PREFERENCE, getString(R.string.default_resolution_value))); + defaultPopupResolutionPreference.setSummary( + defaultPreferences.getString(DEFAULT_POPUP_RESOLUTION_PREFERENCE, + getString(R.string.default_popup_resolution_value))); preferredVideoFormatPreference.setSummary( defaultPreferences.getString(PREFERRED_VIDEO_FORMAT_PREFERENCE, getString(R.string.preferred_video_format_default))); diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 17708dbef..72a104eee 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -8,12 +8,36 @@ import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.stream_info.StreamInfo; import org.schabi.newpipe.fragments.OnItemSelectedListener; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; +import org.schabi.newpipe.player.AbstractPlayer; @SuppressWarnings({"unused", "WeakerAccess"}) public class NavigationHelper { + public static Intent getOpenPlayerIntent(Context context, Class targetClazz, StreamInfo info, int selectedStreamIndex) { + return new Intent(context, targetClazz) + .putExtra(AbstractPlayer.VIDEO_TITLE, info.title) + .putExtra(AbstractPlayer.VIDEO_URL, info.webpage_url) + .putExtra(AbstractPlayer.CHANNEL_NAME, info.uploader) + .putExtra(AbstractPlayer.INDEX_SEL_VIDEO_STREAM, selectedStreamIndex) + .putExtra(AbstractPlayer.VIDEO_STREAMS_LIST, Utils.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false)) + .putExtra(AbstractPlayer.VIDEO_ONLY_AUDIO_STREAM, Utils.getHighestQualityAudio(info.audio_streams)); + } + + public static Intent getOpenPlayerIntent(Context context, Class targetClazz, AbstractPlayer instance) { + return new Intent(context, targetClazz) + .putExtra(AbstractPlayer.VIDEO_TITLE, instance.getVideoTitle()) + .putExtra(AbstractPlayer.VIDEO_URL, instance.getVideoUrl()) + .putExtra(AbstractPlayer.CHANNEL_NAME, instance.getChannelName()) + .putExtra(AbstractPlayer.INDEX_SEL_VIDEO_STREAM, instance.getSelectedStreamIndex()) + .putExtra(AbstractPlayer.VIDEO_STREAMS_LIST, instance.getVideoStreamsList()) + .putExtra(AbstractPlayer.VIDEO_ONLY_AUDIO_STREAM, instance.getAudioStream()) + .putExtra(AbstractPlayer.START_POSITION, ((int) instance.getPlayer().getCurrentPosition())); + } + + /*////////////////////////////////////////////////////////////////////////// // Through Interface (faster) //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/util/Utils.java b/app/src/main/java/org/schabi/newpipe/util/Utils.java index 98d8bf25b..a1cb5fefa 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Utils.java +++ b/app/src/main/java/org/schabi/newpipe/util/Utils.java @@ -9,28 +9,31 @@ import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream_info.AudioStream; import org.schabi.newpipe.extractor.stream_info.VideoStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; +@SuppressWarnings("WeakerAccess") public class Utils { + private static final List HIGH_RESOLUTION_LIST = Arrays.asList("1440p", "2160p", "1440p60", "2160p60"); + /** - * Return the index of the default stream in the list, based on the - * preferred resolution and format chosen in the settings + * Return the index of the default stream in the list, based on the parameters + * defaultResolution and preferredFormat + * + * @param videoStreams the list that will be extracted the index * - * @param videoStreams the list that will be extracted the index * @return index of the preferred resolution&format */ - public static int getPreferredResolution(Context context, List videoStreams) { - SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context); - if (defaultPreferences == null) return 0; + public static int getDefaultResolution(String defaultResolution, String preferredFormat, List videoStreams) { - String defaultResolution = defaultPreferences - .getString(context.getString(R.string.default_resolution_key), - context.getString(R.string.default_resolution_value)); - - String preferredFormat = defaultPreferences - .getString(context.getString(R.string.preferred_video_format_key), - context.getString(R.string.preferred_video_format_default)); + if (defaultResolution.equals("Best resolution")) { + return 0; + } // first try to find the one with the right resolution int selectedFormat = 0; @@ -50,16 +53,69 @@ public class Utils { } } + if (selectedFormat == 0 && !videoStreams.get(selectedFormat).resolution.contains(defaultResolution.replace("p60", "p"))) { + // Maybe there's no 60 fps variant available, so fallback to the normal version + String replace = defaultResolution.replace("p60", "p"); + for (int i = 0; i < videoStreams.size(); i++) { + VideoStream item = videoStreams.get(i); + if (replace.equals(item.resolution)) selectedFormat = i; + } + + // than try to find the one with the right resolution and format + for (int i = 0; i < videoStreams.size(); i++) { + VideoStream item = videoStreams.get(i); + if (replace.equals(item.resolution) + && preferredFormat.equals(MediaFormat.getNameById(item.format))) { + selectedFormat = i; + } + } + + } + // this is actually an error, // but maybe there is really no stream fitting to the default value. return selectedFormat; } + /** + * @see #getDefaultResolution(String, String, List) + */ + public static int getDefaultResolution(Context context, List videoStreams) { + SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context); + if (defaultPreferences == null) return 0; + + String defaultResolution = defaultPreferences + .getString(context.getString(R.string.default_resolution_key), context.getString(R.string.default_resolution_value)); + + String preferredFormat = defaultPreferences + .getString(context.getString(R.string.preferred_video_format_key), context.getString(R.string.preferred_video_format_default)); + + return getDefaultResolution(defaultResolution, preferredFormat, videoStreams); + } + + /** + * @see #getDefaultResolution(String, String, List) + */ + public static int getPopupDefaultResolution(Context context, List videoStreams) { + SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context); + if (defaultPreferences == null) return 0; + + String defaultResolution = defaultPreferences + .getString(context.getString(R.string.default_popup_resolution_key), context.getString(R.string.default_popup_resolution_value)); + + String preferredFormat = defaultPreferences + .getString(context.getString(R.string.preferred_video_format_key), context.getString(R.string.preferred_video_format_default)); + + return getDefaultResolution(defaultResolution, preferredFormat, videoStreams); + } + /** * Return the index of the default stream in the list, based on the * preferred audio format chosen in the settings * + * @param context context to get the preferred audio format * @param audioStreams the list that will be extracted the index + * * @return index of the preferred format */ public static int getPreferredAudioFormat(Context context, List audioStreams) { @@ -88,4 +144,125 @@ public class Utils { return 0; } + + /** + * Get the audio from the list with the highest bitrate + * + * @param audioStreams list the audio streams + * @return audio with highest average bitrate + */ + public static AudioStream getHighestQualityAudio(List audioStreams) { + int highestQualityIndex = 0; + + for (int i = 1; i < audioStreams.size(); i++) { + AudioStream audioStream = audioStreams.get(i); + if (audioStream.avgBitrate > audioStreams.get(highestQualityIndex).avgBitrate) highestQualityIndex = i; + } + + return audioStreams.get(highestQualityIndex); + } + + /** + * Join the two lists of video streams (video_only and normal videos), and sort them according with preferred format + * chosen by the user + * + * @param context context to search for the format to give preference + * @param videoStreams normal videos list + * @param videoOnlyStreams video only stream list + * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest + * @return the sorted list + */ + public static ArrayList getSortedStreamVideosList(Context context, List videoStreams, List videoOnlyStreams, boolean ascendingOrder) { + boolean showHigherResolutions = PreferenceManager.getDefaultSharedPreferences(context).getBoolean(context.getString(R.string.show_higher_resolutions_key), false); + String preferredFormatString = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.preferred_video_format_key), context.getString(R.string.preferred_video_format_default)); + MediaFormat preferredFormat = MediaFormat.WEBM; + switch (preferredFormatString) { + case "WebM": + preferredFormat = MediaFormat.WEBM; + break; + case "MPEG-4": + preferredFormat = MediaFormat.MPEG_4; + break; + case "3GPP": + preferredFormat = MediaFormat.v3GPP; + break; + default: + break; + } + return getSortedStreamVideosList(preferredFormat, showHigherResolutions, videoStreams, videoOnlyStreams, ascendingOrder); + } + //show_higher_resolutions_key + + /** + * Join the two lists of video streams (video_only and normal videos), and sort them according with preferred format + * chosen by the user + * + * @param preferredFormat format to give preference + * @param showHigherResolutions + * @param videoStreams normal videos list + * @param videoOnlyStreams video only stream list + * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest @return the sorted list + * @return the sorted list + */ + public static ArrayList getSortedStreamVideosList(MediaFormat preferredFormat, boolean showHigherResolutions, List videoStreams, List videoOnlyStreams, boolean ascendingOrder) { + ArrayList retList = new ArrayList<>(); + HashMap hashMap = new HashMap<>(); + + if (videoOnlyStreams != null) { + for (VideoStream stream : videoOnlyStreams) { + if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(stream.resolution)) continue; + retList.add(stream); + } + } + if (videoStreams != null) { + for (VideoStream stream : videoStreams) { + if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(stream.resolution)) continue; + retList.add(stream); + } + } + + // Add all to the hashmap + for (VideoStream videoStream : retList) hashMap.put(videoStream.resolution, videoStream); + + // Override the values when the key == resolution, with the preferredFormat + for (VideoStream videoStream : retList) { + if (videoStream.format == preferredFormat.id) hashMap.put(videoStream.resolution, videoStream); + } + + retList.clear(); + retList.addAll(hashMap.values()); + sortStreamList(retList, ascendingOrder); + return retList; + } + + /** + * Sort the streams list depending on the parameter ascendingOrder; + *

+ * It works like that:
+ * - Take a string resolution, remove the letters, replace "0p60" (for 60fps videos) with "1" + * and sort by the greatest:
+ *

+     *      720p     ->  720
+     *      720p60   ->  721
+     *      360p     ->  360
+     *      1080p    ->  1080
+     *      1080p60  ->  1081
+     * 

+ * ascendingOrder ? 360 < 720 < 721 < 1080 < 1081 + * !ascendingOrder ? 1081 < 1080 < 721 < 720 < 360/pre>

+ *

+ * @param videoStreams list that the sorting will be applied + * @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest + */ + public static void sortStreamList(List videoStreams, final boolean ascendingOrder) { + Collections.sort(videoStreams, new Comparator() { + @Override + public int compare(VideoStream o1, VideoStream o2) { + int res1 = Integer.parseInt(o1.resolution.replace("0p60", "1").replaceAll("[^\\d.]", "")); + int res2 = Integer.parseInt(o2.resolution.replace("0p60", "1").replaceAll("[^\\d.]", "")); + + return ascendingOrder ? res1 - res2 : res2 - res1; + } + }); + } } diff --git a/app/src/main/java/org/schabi/newpipe/workers/ChannelExtractorWorker.java b/app/src/main/java/org/schabi/newpipe/workers/ChannelExtractorWorker.java index 6b815b493..5275688d4 100644 --- a/app/src/main/java/org/schabi/newpipe/workers/ChannelExtractorWorker.java +++ b/app/src/main/java/org/schabi/newpipe/workers/ChannelExtractorWorker.java @@ -33,6 +33,11 @@ public class ChannelExtractorWorker extends ExtractorWorker { public interface OnChannelInfoReceive { void onReceive(ChannelInfo info); void onError(int messageId); + /** + * Called when an unrecoverable error has occurred. + *

This is a good place to finish the caller.

+ */ + void onUnrecoverableError(Exception exception); } /** @@ -74,9 +79,11 @@ public class ChannelExtractorWorker extends ExtractorWorker { @Override - protected void handleException(Exception exception, int serviceId, String url) { + protected void handleException(final Exception exception, int serviceId, String url) { + if (callback == null || getHandler() == null || isInterrupted()) return; + if (exception instanceof IOException) { - if (callback != null) getHandler().post(new Runnable() { + getHandler().post(new Runnable() { @Override public void run() { callback.onError(R.string.network_error); @@ -84,10 +91,20 @@ public class ChannelExtractorWorker extends ExtractorWorker { }); } else if (exception instanceof ParsingException || exception instanceof ExtractionException) { ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_CHANNEL, getServiceName(), url, R.string.parsing_error)); - finishIfActivity(); + getHandler().post(new Runnable() { + @Override + public void run() { + callback.onUnrecoverableError(exception); + } + }); } else { ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_CHANNEL, getServiceName(), url, R.string.general_error)); - finishIfActivity(); + getHandler().post(new Runnable() { + @Override + public void run() { + callback.onUnrecoverableError(exception); + } + }); } } diff --git a/app/src/main/java/org/schabi/newpipe/workers/ExtractorWorker.java b/app/src/main/java/org/schabi/newpipe/workers/ExtractorWorker.java index b26d62ae8..442a9d8ab 100644 --- a/app/src/main/java/org/schabi/newpipe/workers/ExtractorWorker.java +++ b/app/src/main/java/org/schabi/newpipe/workers/ExtractorWorker.java @@ -135,13 +135,6 @@ public abstract class ExtractorWorker extends Thread { this.service = null; } - /** - * If the context passed in the constructor is an {@link Activity}, finish it. - */ - protected void finishIfActivity() { - if (getContext() instanceof Activity) ((Activity) getContext()).finish(); - } - public Handler getHandler() { return handler; } diff --git a/app/src/main/java/org/schabi/newpipe/workers/StreamExtractorWorker.java b/app/src/main/java/org/schabi/newpipe/workers/StreamExtractorWorker.java index bc65f8dc8..7ff04adf2 100644 --- a/app/src/main/java/org/schabi/newpipe/workers/StreamExtractorWorker.java +++ b/app/src/main/java/org/schabi/newpipe/workers/StreamExtractorWorker.java @@ -35,6 +35,12 @@ public class StreamExtractorWorker extends ExtractorWorker { void onBlockedByGemaError(); void onContentErrorWithMessage(int messageId); void onContentError(); + + /** + * Called when an unrecoverable error has occurred. + *

This is a good place to finish the caller.

+ */ + void onUnrecoverableError(Exception exception); } /** @@ -62,7 +68,7 @@ public class StreamExtractorWorker extends ExtractorWorker { if (streamInfo != null && !streamInfo.errors.isEmpty()) handleErrorsDuringExtraction(streamInfo.errors, ErrorActivity.REQUESTED_STREAM); - if (callback != null && streamInfo != null && !isInterrupted()) getHandler().post(new Runnable() { + if (callback != null && getHandler() != null && streamInfo != null && !isInterrupted()) getHandler().post(new Runnable() { @Override public void run() { if (isInterrupted() || callback == null) return; @@ -75,37 +81,39 @@ public class StreamExtractorWorker extends ExtractorWorker { } @Override - protected void handleException(final Exception exception, int serviceId, String url) { + protected void handleException(final Exception exception, int serviceId, final String url) { + if (callback == null || getHandler() == null || isInterrupted()) return; + if (exception instanceof ReCaptchaException) { - if (callback != null) getHandler().post(new Runnable() { + getHandler().post(new Runnable() { @Override public void run() { callback.onReCaptchaException(); } }); } else if (exception instanceof IOException) { - if (callback != null) getHandler().post(new Runnable() { + getHandler().post(new Runnable() { @Override public void run() { callback.onError(R.string.network_error); } }); } else if (exception instanceof YoutubeStreamExtractor.GemaException) { - if (callback != null) getHandler().post(new Runnable() { + getHandler().post(new Runnable() { @Override public void run() { callback.onBlockedByGemaError(); } }); } else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) { - if (callback != null) getHandler().post(new Runnable() { + getHandler().post(new Runnable() { @Override public void run() { callback.onContentErrorWithMessage(R.string.live_streams_not_supported); } }); } else if (exception instanceof StreamExtractor.ContentNotAvailableException) { - if (callback != null) getHandler().post(new Runnable() { + getHandler().post(new Runnable() { @Override public void run() { callback.onContentError(); @@ -114,7 +122,12 @@ public class StreamExtractorWorker extends ExtractorWorker { } else if (exception instanceof YoutubeStreamExtractor.DecryptException) { // custom service related exceptions ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, getServiceName(), url, R.string.youtube_signature_decryption_error)); - finishIfActivity(); + getHandler().post(new Runnable() { + @Override + public void run() { + callback.onUnrecoverableError(exception); + } + }); } else if (exception instanceof StreamInfo.StreamExctractException) { if (!streamInfo.errors.isEmpty()) { // !!! if this case ever kicks in someone gets kicked out !!! @@ -122,13 +135,29 @@ public class StreamExtractorWorker extends ExtractorWorker { } else { ErrorActivity.reportError(getHandler(), getContext(), streamInfo.errors, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, getServiceName(), url, R.string.could_not_get_stream)); } - finishIfActivity(); + + getHandler().post(new Runnable() { + @Override + public void run() { + callback.onUnrecoverableError(exception); + } + }); } else if (exception instanceof ParsingException) { ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, getServiceName(), url, R.string.parsing_error)); - finishIfActivity(); + getHandler().post(new Runnable() { + @Override + public void run() { + callback.onUnrecoverableError(exception); + } + }); } else { ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, getServiceName(), url, R.string.general_error)); - finishIfActivity(); + getHandler().post(new Runnable() { + @Override + public void run() { + callback.onUnrecoverableError(exception); + } + }); } } diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 27cd19554..0d2cbbf50 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -16,14 +16,24 @@ default_resolution_preference 360p + Best resolution + 1080p60 + 1080p + 720p60 720p + 480p 360p 240p 144p - preferrfed_video_format - WebM + default_popup_resolution_key + 480p + + show_higher_resolutions_key + + preferred_video_format + MPEG-4 WebM MPEG-4 @@ -33,12 +43,7 @@ show_play_with_kodi theme - @string/light_theme_title - - @string/light_theme_title - @string/dark_theme_title - @string/black_theme_title - + @string/dark_theme_title @string/light_theme_title @string/dark_theme_title diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c4340a093..05f45e93c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -38,6 +38,9 @@ Autoplay when called from another app Automatically play a video when NewPipe is called from another app. Default resolution + Default popup resolution + Show higher resolutions + Only some devices support playing 2k/4k videos Play with Kodi Kore app not found. Install Kore? https://f-droid.org/repository/browse/?fdfilter=Kore&fdid=org.xbmc.kore @@ -67,6 +70,12 @@ Audio + + @string/light_theme_title + @string/dark_theme_title + @string/black_theme_title + + Next video Show next and similar videos URL not supported diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index ee462ff35..ba4a40b6d 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -1,7 +1,8 @@ + xmlns:tools="http://schemas.android.com/tools" + android:title="@string/settings_activity_title" + android:key="general_preferences"> + + + + + android:defaultValue="@string/preferred_video_format_default" + tools:summary="MPEG-4"/> audioStreamsTestList = Arrays.asList( + new AudioStream("", MediaFormat.M4A.id, /**/ 120, 0, 0), + new AudioStream("", MediaFormat.WEBMA.id, /**/ 190, 0, 0), + new AudioStream("", MediaFormat.M4A.id, /**/ 130, 0, 0), + new AudioStream("", MediaFormat.WEBMA.id, /**/ 60, 0, 0), + new AudioStream("", MediaFormat.M4A.id, /**/ 320, 0, 0), + new AudioStream("", MediaFormat.WEBMA.id, /**/ 320, 0, 0)); + + private List videoStreamsTestList = Arrays.asList( + new VideoStream("", /**/ MediaFormat.MPEG_4.id, /**/ "720p"), + new VideoStream("", /**/ MediaFormat.v3GPP.id, /**/ "240p"), + new VideoStream("", /**/ MediaFormat.WEBM.id, /**/ "480p"), + new VideoStream("", /**/ MediaFormat.v3GPP.id, /**/ "144p"), + new VideoStream("", /**/ MediaFormat.MPEG_4.id, /**/ "360p"), + new VideoStream("", /**/ MediaFormat.WEBM.id, /**/ "360p")); + + private List videoOnlyStreamsTestList = Arrays.asList( + new VideoStream(true, "", /**/ MediaFormat.MPEG_4.id, /**/ "720p"), + new VideoStream(true, "", /**/ MediaFormat.MPEG_4.id, /**/ "720p"), + new VideoStream(true, "", /**/ MediaFormat.MPEG_4.id, /**/ "2160p"), + new VideoStream(true, "", /**/ MediaFormat.MPEG_4.id, /**/ "1440p60"), + new VideoStream(true, "", /**/ MediaFormat.WEBM.id, /**/ "720p60"), + new VideoStream(true, "", /**/ MediaFormat.MPEG_4.id, /**/ "2160p60"), + new VideoStream(true, "", /**/ MediaFormat.MPEG_4.id, /**/ "720p60"), + new VideoStream(true, "", /**/ MediaFormat.MPEG_4.id, /**/ "1080p"), + new VideoStream(true, "", /**/ MediaFormat.MPEG_4.id, /**/ "1080p60")); + + @Test + public void getHighestQualityAudioTest() throws Exception { + assertEquals(320, Utils.getHighestQualityAudio(audioStreamsTestList).avgBitrate); + } + + @Test + public void getSortedStreamVideosListTest() throws Exception { + List result = Utils.getSortedStreamVideosList(MediaFormat.MPEG_4, true, videoStreamsTestList, videoOnlyStreamsTestList, true); + + List expected = Arrays.asList("144p", "240p", "360p", "480p", "720p", "720p60", "1080p", "1080p60", "1440p60", "2160p", "2160p60"); + //for (VideoStream videoStream : result) System.out.println(videoStream.resolution + " > " + MediaFormat.getSuffixById(videoStream.format) + " > " + videoStream.isVideoOnly); + + assertEquals(result.size(), expected.size()); + for (int i = 0; i < result.size(); i++) { + assertEquals(result.get(i).resolution, expected.get(i)); + } + + //////////////////// + // Reverse Order // + ////////////////// + + result = Utils.getSortedStreamVideosList(MediaFormat.MPEG_4, true, videoStreamsTestList, videoOnlyStreamsTestList, false); + + expected = Arrays.asList("2160p60", "2160p", "1440p60", "1080p60", "1080p", "720p60", "720p", "480p", "360p", "240p", "144p"); + assertEquals(result.size(), expected.size()); + for (int i = 0; i < result.size(); i++) assertEquals(result.get(i).resolution, expected.get(i)); + + //////////////////////////////////// + // Don't show Higher resolutions // + ////////////////////////////////// + + result = Utils.getSortedStreamVideosList(MediaFormat.MPEG_4, false, videoStreamsTestList, videoOnlyStreamsTestList, false); + expected = Arrays.asList("1080p60", "1080p", "720p60", "720p", "480p", "360p", "240p", "144p"); + assertEquals(result.size(), expected.size()); + for (int i = 0; i < result.size(); i++) assertEquals(result.get(i).resolution, expected.get(i)); + } + +} \ No newline at end of file