diff options
Diffstat (limited to 'app/src/main/java')
21 files changed, 1302 insertions, 1653 deletions
diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/activities/PostVideoActivity.java b/app/src/main/java/ml/docilealligator/infinityforreddit/activities/PostVideoActivity.java index 8477942e..d9e36ee5 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/activities/PostVideoActivity.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/activities/PostVideoActivity.java @@ -27,9 +27,9 @@ import androidx.core.content.ContextCompat; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.bumptech.glide.request.RequestOptions; -import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DataSource; @@ -189,7 +189,7 @@ public class PostVideoActivity extends BaseActivity implements FlairBottomSheetF private FlairBottomSheetFragment mFlairSelectionBottomSheetFragment; private Snackbar mPostingSnackbar; private DataSource.Factory dataSourceFactory; - private SimpleExoPlayer player; + private ExoPlayer player; @Override protected void onCreate(Bundle savedInstanceState) { @@ -216,7 +216,7 @@ public class PostVideoActivity extends BaseActivity implements FlairBottomSheetF mGlide = Glide.with(this); - player = ExoPlayerFactory.newSimpleInstance(this); + player = new ExoPlayer.Builder(this).build(); videoPlayerView.setPlayer(player); dataSourceFactory = new DefaultDataSourceFactory(this, Util.getUserAgent(this, "Infinity")); @@ -491,7 +491,7 @@ public class PostVideoActivity extends BaseActivity implements FlairBottomSheetF constraintLayout.setVisibility(View.GONE); selectAgainTextView.setVisibility(View.VISIBLE); videoPlayerView.setVisibility(View.VISIBLE); - player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(videoUri)); + player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(videoUri))); player.setPlayWhenReady(true); wasPlaying = true; } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/activities/ViewVideoActivity.java b/app/src/main/java/ml/docilealligator/infinityforreddit/activities/ViewVideoActivity.java index 2fab98e7..39398315 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/activities/ViewVideoActivity.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/activities/ViewVideoActivity.java @@ -7,6 +7,7 @@ import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES; import android.Manifest; import android.app.AlertDialog; +import android.app.Dialog; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; @@ -24,6 +25,7 @@ import android.os.Bundle; import android.os.Handler; import android.provider.Settings; import android.text.Html; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.OrientationEventListener; @@ -45,29 +47,26 @@ import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Tracks; import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.TrackSelectionDialogBuilder; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.video.VideoListener; +import com.google.android.exoplayer2.video.VideoSize; import com.google.android.material.bottomappbar.BottomAppBar; import com.google.android.material.snackbar.Snackbar; +import com.google.common.collect.ImmutableList; import com.otaliastudios.zoom.ZoomEngine; import com.otaliastudios.zoom.ZoomSurfaceView; @@ -176,7 +175,7 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe public Typeface typeface; private Uri mVideoUri; - private SimpleExoPlayer player; + private ExoPlayer player; private DefaultTrackSelector trackSelector; private DataSource.Factory dataSourceFactory; @@ -403,7 +402,7 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe String postTitle = intent.getStringExtra(EXTRA_POST_TITLE); setSmallTitle(postTitle); - playerControlView.setVisibilityListener(visibility -> { + playerControlView.addVisibilityListener(visibility -> { switch (visibility) { case View.GONE: getWindow().getDecorView().setSystemUiVisibility( @@ -422,21 +421,20 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe } }); - TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); - trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); + trackSelector = new DefaultTrackSelector(this); if (videoType == VIDEO_TYPE_NORMAL && isDataSavingMode && dataSavingModeDefaultResolution > 0) { trackSelector.setParameters( trackSelector.buildUponParameters() .setMaxVideoSize(dataSavingModeDefaultResolution, dataSavingModeDefaultResolution)); } - player = ExoPlayerFactory.newSimpleInstance(this, trackSelector); + player = new ExoPlayer.Builder(this).setTrackSelector(trackSelector).build(); playerControlView.setPlayer(player); - player.addVideoListener(new VideoListener() { + player.addListener(new Player.Listener() { @Override - public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - zoomSurfaceView.setContentSize(width, height); + public void onVideoSizeChanged(VideoSize videoSize) { + zoomSurfaceView.setContentSize(videoSize.width, videoSize.height); } }); zoomSurfaceView.addCallback(new ZoomSurfaceView.Callback() { @@ -506,9 +504,10 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe if (mVideoUri == null) { loadStreamableVideo(shortCode, savedInstanceState); } else { - dataSourceFactory = new CacheDataSourceFactory(mSimpleCache, - new DefaultDataSourceFactory(ViewVideoActivity.this, APIUtils.getRedgifsUserAgent(ViewVideoActivity.this))); - player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri)); + dataSourceFactory = new CacheDataSource.Factory().setCache(mSimpleCache) + .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory().setUserAgent(APIUtils.getRedgifsUserAgent(ViewVideoActivity.this))); + player.prepare(); + player.setMediaSource(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(mVideoUri))); preparePlayer(savedInstanceState); } } else if (videoType == VIDEO_TYPE_V_REDD_IT) { @@ -537,9 +536,10 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe loadGfycatOrRedgifsVideo(redgifsRetrofit, gfycatId, false, savedInstanceState, false); } } else { - dataSourceFactory = new CacheDataSourceFactory(mSimpleCache, - new DefaultDataSourceFactory(ViewVideoActivity.this, APIUtils.getRedgifsUserAgent(ViewVideoActivity.this))); - player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri)); + dataSourceFactory = new CacheDataSource.Factory().setCache(mSimpleCache) + .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory().setUserAgent(APIUtils.getRedgifsUserAgent(ViewVideoActivity.this))); + player.prepare(); + player.setMediaSource(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(mVideoUri))); preparePlayer(savedInstanceState); } } else if (videoType == VIDEO_TYPE_DIRECT || videoType == VIDEO_TYPE_IMGUR) { @@ -550,10 +550,11 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe videoFileName = "imgur-" + FilenameUtils.getName(videoDownloadUrl); } // Produces DataSource instances through which media data is loaded. - dataSourceFactory = new CacheDataSourceFactory(mSimpleCache, - new DefaultHttpDataSourceFactory(APIUtils.getRedgifsUserAgent(ViewVideoActivity.this))); + dataSourceFactory = new CacheDataSource.Factory().setCache(mSimpleCache) + .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory().setUserAgent(APIUtils.getRedgifsUserAgent(ViewVideoActivity.this))); // Prepare the player with the source. - player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri)); + player.prepare(); + player.setMediaSource(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(mVideoUri))); preparePlayer(savedInstanceState); } else { videoDownloadUrl = intent.getStringExtra(EXTRA_VIDEO_DOWNLOAD_URL); @@ -561,10 +562,11 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe id = intent.getStringExtra(EXTRA_ID); videoFileName = subredditName + "-" + id + ".mp4"; // Produces DataSource instances through which media data is loaded. - dataSourceFactory = new CacheDataSourceFactory(mSimpleCache, - new DefaultHttpDataSourceFactory(APIUtils.getRedgifsUserAgent(ViewVideoActivity.this))); + dataSourceFactory = new CacheDataSource.Factory().setCache(mSimpleCache) + .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory().setUserAgent(APIUtils.getRedgifsUserAgent(ViewVideoActivity.this))); // Prepare the player with the source. - player.prepare(new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri)); + player.prepare(); + player.setMediaSource(new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(mVideoUri))); preparePlayer(savedInstanceState); } } @@ -615,25 +617,29 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe muteButton.setImageResource(R.drawable.ic_unmute_24dp); } - player.addListener(new Player.EventListener() { + player.addListener(new Player.Listener() { @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + public void onTracksChanged(@NonNull Tracks tracks) { + ImmutableList<Tracks.Group> trackGroups = tracks.getGroups(); if (!trackGroups.isEmpty()) { if (videoType == VIDEO_TYPE_NORMAL) { hdButton.setVisibility(View.VISIBLE); hdButton.setOnClickListener(view -> { - TrackSelectionDialogBuilder builder = new TrackSelectionDialogBuilder(ViewVideoActivity.this, getString(R.string.select_video_quality), trackSelector, 0); + TrackSelectionDialogBuilder builder = new TrackSelectionDialogBuilder(ViewVideoActivity.this, getString(R.string.select_video_quality), player, 0); builder.setShowDisableOption(true); builder.setAllowAdaptiveSelections(false); - AlertDialog alertDialog = builder.build(); - alertDialog.show(); - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); - alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); + Dialog dialog = builder.build(); + dialog.show(); + if (dialog instanceof AlertDialog) { + Log.i("asfadsf", "asdfasdf"); + ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); + ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); + } }); } - for (int i = 0; i < trackGroups.length; i++) { - String mimeType = trackGroups.get(i).getFormat(0).sampleMimeType; + for (int i = 0; i < trackGroups.size(); i++) { + String mimeType = trackGroups.get(i).getTrackFormat(0).sampleMimeType; if (mimeType != null && mimeType.contains("audio")) { muteButton.setVisibility(View.VISIBLE); muteButton.setOnClickListener(view -> { @@ -688,10 +694,11 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe progressBar.setVisibility(View.GONE); mVideoUri = Uri.parse(webm); videoDownloadUrl = mp4; - dataSourceFactory = new CacheDataSourceFactory(mSimpleCache, - new DefaultDataSourceFactory(ViewVideoActivity.this, APIUtils.getRedgifsUserAgent(ViewVideoActivity.this))); + dataSourceFactory = new CacheDataSource.Factory().setCache(mSimpleCache) + .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory().setUserAgent(APIUtils.getRedgifsUserAgent(ViewVideoActivity.this))); preparePlayer(savedInstanceState); - player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri)); + player.prepare(); + player.setMediaSource(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(mVideoUri))); } @Override @@ -717,10 +724,11 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe progressBar.setVisibility(View.GONE); mVideoUri = Uri.parse(webm); videoDownloadUrl = mp4; - dataSourceFactory = new CacheDataSourceFactory(mSimpleCache, - new DefaultDataSourceFactory(ViewVideoActivity.this, APIUtils.getRedgifsUserAgent(ViewVideoActivity.this))); + dataSourceFactory = new CacheDataSource.Factory().setCache(mSimpleCache) + .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory().setUserAgent(APIUtils.getRedgifsUserAgent(ViewVideoActivity.this))); preparePlayer(savedInstanceState); - player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri)); + player.prepare(); + player.setMediaSource(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(mVideoUri))); } @Override @@ -782,10 +790,11 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe videoType = VIDEO_TYPE_IMGUR; videoFileName = "imgur-" + FilenameUtils.getName(videoDownloadUrl); // Produces DataSource instances through which media data is loaded. - dataSourceFactory = new CacheDataSourceFactory(mSimpleCache, - new DefaultHttpDataSourceFactory(APIUtils.getRedgifsUserAgent(ViewVideoActivity.this))); + dataSourceFactory = new CacheDataSource.Factory().setCache(mSimpleCache) + .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory().setUserAgent(APIUtils.getRedgifsUserAgent(ViewVideoActivity.this))); // Prepare the player with the source. - player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri)); + player.prepare(); + player.setMediaSource(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(mVideoUri))); preparePlayer(savedInstanceState); } else { progressBar.setVisibility(View.GONE); @@ -797,11 +806,12 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe videoFileName = subredditName + "-" + id + ".mp4"; // Produces DataSource instances through which media data is loaded. - dataSourceFactory = new CacheDataSourceFactory(mSimpleCache, - new DefaultHttpDataSourceFactory(APIUtils.getRedgifsUserAgent(ViewVideoActivity.this))); + dataSourceFactory = new CacheDataSource.Factory().setCache(mSimpleCache) + .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory().setUserAgent(APIUtils.getRedgifsUserAgent(ViewVideoActivity.this))); // Prepare the player with the source. preparePlayer(savedInstanceState); - player.prepare(new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri)); + player.prepare(); + player.setMediaSource(new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(mVideoUri))); } else { Toast.makeText(ViewVideoActivity.this, R.string.error_fetching_v_redd_it_video_cannot_get_video_url, Toast.LENGTH_LONG).show(); } @@ -839,10 +849,11 @@ public class ViewVideoActivity extends AppCompatActivity implements CustomFontRe progressBar.setVisibility(View.GONE); videoDownloadUrl = streamableVideo.mp4 == null ? streamableVideo.mp4Mobile.url : streamableVideo.mp4.url; mVideoUri = Uri.parse(videoDownloadUrl); - dataSourceFactory = new CacheDataSourceFactory(mSimpleCache, - new DefaultDataSourceFactory(ViewVideoActivity.this, APIUtils.getRedgifsUserAgent(ViewVideoActivity.this))); + dataSourceFactory = new CacheDataSource.Factory().setCache(mSimpleCache) + .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory().setUserAgent(APIUtils.getRedgifsUserAgent(ViewVideoActivity.this))); preparePlayer(savedInstanceState); - player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mVideoUri)); + player.prepare(); + player.setMediaSource(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(mVideoUri))); } @Override diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/HistoryPostRecyclerViewAdapter.java b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/HistoryPostRecyclerViewAdapter.java index 2eba5f0c..2f3f9923 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/HistoryPostRecyclerViewAdapter.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/HistoryPostRecyclerViewAdapter.java @@ -42,10 +42,12 @@ import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.Target; +import com.google.android.exoplayer2.Tracks; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.PlayerView; +import com.google.common.collect.ImmutableList; import com.libRG.CustomTextView; import org.greenrobot.eventbus.EventBus; @@ -2755,10 +2757,11 @@ public class HistoryPostRecyclerViewAdapter extends PagingDataAdapter<Post, Recy helper = new ExoPlayerViewHelper(this, mediaUri, null, mExoCreator); helper.addEventListener(new Playable.DefaultEventListener() { @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + public void onTracksChanged(@NonNull Tracks tracks) { + ImmutableList<Tracks.Group> trackGroups = tracks.getGroups(); if (!trackGroups.isEmpty()) { - for (int i = 0; i < trackGroups.length; i++) { - String mimeType = trackGroups.get(i).getFormat(0).sampleMimeType; + for (int i = 0; i < trackGroups.size(); i++) { + String mimeType = trackGroups.get(i).getTrackFormat(0).sampleMimeType; if (mimeType != null && mimeType.contains("audio")) { if (mFragment.getMasterMutingOption() != null) { volume = mFragment.getMasterMutingOption() ? 0f : 1f; @@ -4022,10 +4025,11 @@ public class HistoryPostRecyclerViewAdapter extends PagingDataAdapter<Post, Recy helper = new ExoPlayerViewHelper(this, mediaUri, null, mExoCreator); helper.addEventListener(new Playable.DefaultEventListener() { @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + public void onTracksChanged(@NonNull Tracks tracks) { + ImmutableList<Tracks.Group> trackGroups = tracks.getGroups(); if (!trackGroups.isEmpty()) { - for (int i = 0; i < trackGroups.length; i++) { - String mimeType = trackGroups.get(i).getFormat(0).sampleMimeType; + for (int i = 0; i < trackGroups.size(); i++) { + String mimeType = trackGroups.get(i).getTrackFormat(0).sampleMimeType; if (mimeType != null && mimeType.contains("audio")) { if (mFragment.getMasterMutingOption() != null) { volume = mFragment.getMasterMutingOption() ? 0f : 1f; diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostDetailRecyclerViewAdapter.java b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostDetailRecyclerViewAdapter.java index 4db8166f..b2687f93 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostDetailRecyclerViewAdapter.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostDetailRecyclerViewAdapter.java @@ -40,10 +40,10 @@ import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.Target; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.Tracks; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.PlayerView; +import com.google.common.collect.ImmutableList; import com.libRG.CustomTextView; import org.commonmark.ext.gfm.tables.TableBlock; @@ -1761,10 +1761,11 @@ public class PostDetailRecyclerViewAdapter extends RecyclerView.Adapter<Recycler helper = new ExoPlayerViewHelper(this, mediaUri, null, mExoCreator); helper.addEventListener(new Playable.DefaultEventListener() { @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + public void onTracksChanged(@NonNull Tracks tracks) { + ImmutableList<Tracks.Group> trackGroups = tracks.getGroups(); if (!trackGroups.isEmpty()) { - for (int i = 0; i < trackGroups.length; i++) { - String mimeType = trackGroups.get(i).getFormat(0).sampleMimeType; + for (int i = 0; i < trackGroups.size(); i++) { + String mimeType = trackGroups.get(i).getTrackFormat(0).sampleMimeType; if (mimeType != null && mimeType.contains("audio")) { helper.setVolume(volume); muteButton.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostRecyclerViewAdapter.java b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostRecyclerViewAdapter.java index 4bf33ddf..1a25e34e 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostRecyclerViewAdapter.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/PostRecyclerViewAdapter.java @@ -42,10 +42,12 @@ import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.Target; +import com.google.android.exoplayer2.Tracks; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.PlayerView; +import com.google.common.collect.ImmutableList; import com.libRG.CustomTextView; import org.greenrobot.eventbus.EventBus; @@ -2860,10 +2862,11 @@ public class PostRecyclerViewAdapter extends PagingDataAdapter<Post, RecyclerVie helper = new ExoPlayerViewHelper(this, mediaUri, null, mExoCreator); helper.addEventListener(new Playable.DefaultEventListener() { @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + public void onTracksChanged(@NonNull Tracks tracks) { + ImmutableList<Tracks.Group> trackGroups = tracks.getGroups(); if (!trackGroups.isEmpty()) { - for (int i = 0; i < trackGroups.length; i++) { - String mimeType = trackGroups.get(i).getFormat(0).sampleMimeType; + for (int i = 0; i < trackGroups.size(); i++) { + String mimeType = trackGroups.get(i).getTrackFormat(0).sampleMimeType; if (mimeType != null && mimeType.contains("audio")) { if (mFragment.getMasterMutingOption() != null) { volume = mFragment.getMasterMutingOption() ? 0f : 1f; @@ -4175,10 +4178,11 @@ public class PostRecyclerViewAdapter extends PagingDataAdapter<Post, RecyclerVie helper = new ExoPlayerViewHelper(this, mediaUri, null, mExoCreator); helper.addEventListener(new Playable.DefaultEventListener() { @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + public void onTracksChanged(@NonNull Tracks tracks) { + ImmutableList<Tracks.Group> trackGroups = tracks.getGroups(); if (!trackGroups.isEmpty()) { - for (int i = 0; i < trackGroups.length; i++) { - String mimeType = trackGroups.get(i).getFormat(0).sampleMimeType; + for (int i = 0; i < trackGroups.size(); i++) { + String mimeType = trackGroups.get(i).getTrackFormat(0).sampleMimeType; if (mimeType != null && mimeType.contains("audio")) { if (mFragment.getMasterMutingOption() != null) { volume = mFragment.getMasterMutingOption() ? 0f : 1f; diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/customviews/LoopAvailableExoCreator.java b/app/src/main/java/ml/docilealligator/infinityforreddit/customviews/LoopAvailableExoCreator.java index 01fc9a32..560f7b12 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/customviews/LoopAvailableExoCreator.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/customviews/LoopAvailableExoCreator.java @@ -4,8 +4,8 @@ import android.content.SharedPreferences; import androidx.annotation.NonNull; +import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.SimpleExoPlayer; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.videoautoplay.Config; @@ -22,8 +22,8 @@ public class LoopAvailableExoCreator extends DefaultExoCreator { @NonNull @Override - public SimpleExoPlayer createPlayer() { - SimpleExoPlayer player = super.createPlayer(); + public ExoPlayer createPlayer() { + ExoPlayer player = super.createPlayer(); if (sharedPreferences.getBoolean(SharedPreferencesUtils.LOOP_VIDEO, true)) { player.setRepeatMode(Player.REPEAT_MODE_ALL); } else { diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewImgurVideoFragment.java b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewImgurVideoFragment.java index a2fcfb9e..b53b3ddb 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewImgurVideoFragment.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewImgurVideoFragment.java @@ -7,7 +7,6 @@ import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.media.AudioManager; -import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; @@ -26,22 +25,21 @@ import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; -import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Tracks; import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.material.bottomappbar.BottomAppBar; +import com.google.common.collect.ImmutableList; import javax.inject.Inject; import javax.inject.Named; @@ -54,6 +52,7 @@ import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.activities.ViewImgurMediaActivity; import ml.docilealligator.infinityforreddit.bottomsheetfragments.PlaybackSpeedBottomSheetFragment; import ml.docilealligator.infinityforreddit.services.DownloadMediaService; +import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; @@ -78,7 +77,7 @@ public class ViewImgurVideoFragment extends Fragment { ImageView downloadImageView; private ViewImgurMediaActivity activity; private ImgurMedia imgurMedia; - private SimpleExoPlayer player; + private ExoPlayer player; private DataSource.Factory dataSourceFactory; private boolean wasPlaying = false; private boolean isMute = false; @@ -87,6 +86,8 @@ public class ViewImgurVideoFragment extends Fragment { @Inject @Named("default") SharedPreferences mSharedPreferences; + @Inject + SimpleCache mSimpleCache; public ViewImgurVideoFragment() { // Required empty public constructor @@ -143,13 +144,13 @@ public class ViewImgurVideoFragment extends Fragment { } }); - TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); - TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); - player = ExoPlayerFactory.newSimpleInstance(activity, trackSelector); + TrackSelector trackSelector = new DefaultTrackSelector(activity); + player = new ExoPlayer.Builder(activity).setTrackSelector(trackSelector).build(); videoPlayerView.setPlayer(player); - dataSourceFactory = new DefaultDataSourceFactory(activity, - Util.getUserAgent(activity, "Infinity")); - player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse(imgurMedia.getLink()))); + dataSourceFactory = new CacheDataSource.Factory().setCache(mSimpleCache) + .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory().setUserAgent(APIUtils.getRedgifsUserAgent(activity))); + player.prepare(); + player.setMediaSource(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(imgurMedia.getLink()))); if (savedInstanceState != null) { playbackSpeed = savedInstanceState.getInt(PLAYBACK_SPEED_STATE); @@ -278,12 +279,13 @@ public class ViewImgurVideoFragment extends Fragment { muteButton.setImageResource(R.drawable.ic_unmute_24dp); } - player.addListener(new Player.EventListener() { + player.addListener(new Player.Listener() { @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + public void onTracksChanged(@NonNull Tracks tracks) { + ImmutableList<Tracks.Group> trackGroups = tracks.getGroups(); if (!trackGroups.isEmpty()) { - for (int i = 0; i < trackGroups.length; i++) { - String mimeType = trackGroups.get(i).getFormat(0).sampleMimeType; + for (int i = 0; i < trackGroups.size(); i++) { + String mimeType = trackGroups.get(i).getTrackFormat(0).sampleMimeType; if (mimeType != null && mimeType.contains("audio")) { muteButton.setVisibility(View.VISIBLE); muteButton.setOnClickListener(view -> { diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewRPANBroadcastFragment.java b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewRPANBroadcastFragment.java index a053b68a..02884e88 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewRPANBroadcastFragment.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewRPANBroadcastFragment.java @@ -1,10 +1,10 @@ package ml.docilealligator.infinityforreddit.fragments; import android.app.AlertDialog; +import android.app.Dialog; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Configuration; -import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.view.LayoutInflater; @@ -23,21 +23,20 @@ import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Tracks; import com.google.android.exoplayer2.source.BehindLiveWindowException; -import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.ui.TrackSelectionDialogBuilder; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import com.google.android.exoplayer2.upstream.cache.SimpleCache; +import com.google.common.collect.ImmutableList; import org.json.JSONException; import org.json.JSONObject; @@ -58,6 +57,7 @@ import ml.docilealligator.infinityforreddit.activities.RPANActivity; import ml.docilealligator.infinityforreddit.adapters.RPANCommentStreamRecyclerViewAdapter; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; +import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.JSONUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import okhttp3.OkHttpClient; @@ -105,9 +105,11 @@ public class ViewRPANBroadcastFragment extends Fragment { @Inject @Named("rpan") OkHttpClient okHttpClient; + @Inject + SimpleCache mSimpleCache; private RPANActivity mActivity; private RPANBroadcast rpanBroadcast; - private SimpleExoPlayer player; + private ExoPlayer player; private DefaultTrackSelector trackSelector; private DataSource.Factory dataSourceFactory; private Handler handler; @@ -175,9 +177,8 @@ public class ViewRPANBroadcastFragment extends Fragment { } }); - TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); - trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); - player = ExoPlayerFactory.newSimpleInstance(mActivity, trackSelector); + trackSelector = new DefaultTrackSelector(mActivity); + player = new ExoPlayer.Builder(mActivity).setTrackSelector(trackSelector).build(); playerView.setPlayer(player); wasPlaying = true; @@ -202,9 +203,10 @@ public class ViewRPANBroadcastFragment extends Fragment { muteButton.setImageResource(R.drawable.ic_unmute_24dp); } - player.addListener(new Player.EventListener() { + player.addListener(new Player.Listener() { @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + public void onTracksChanged(@NonNull Tracks tracks) { + ImmutableList<Tracks.Group> trackGroups = tracks.getGroups(); if (!trackGroups.isEmpty()) { if (isDataSavingMode) { trackSelector.setParameters( @@ -214,18 +216,19 @@ public class ViewRPANBroadcastFragment extends Fragment { hdButton.setVisibility(View.VISIBLE); hdButton.setOnClickListener(view -> { - TrackSelectionDialogBuilder builder = new TrackSelectionDialogBuilder(mActivity, - getString(R.string.select_video_quality), trackSelector, 0); + TrackSelectionDialogBuilder builder = new TrackSelectionDialogBuilder(mActivity, getString(R.string.select_video_quality), player, 0); builder.setShowDisableOption(true); builder.setAllowAdaptiveSelections(false); - AlertDialog alertDialog = builder.build(); - alertDialog.show(); - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); - alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); + Dialog dialog = builder.build(); + dialog.show(); + if (dialog instanceof AlertDialog) { + ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); + ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(mCustomThemeWrapper.getPrimaryTextColor()); + } }); - for (int i = 0; i < trackGroups.length; i++) { - String mimeType = trackGroups.get(i).getFormat(0).sampleMimeType; + for (int i = 0; i < trackGroups.size(); i++) { + String mimeType = trackGroups.get(i).getTrackFormat(0).sampleMimeType; if (mimeType != null && mimeType.contains("audio")) { muteButton.setVisibility(View.VISIBLE); muteButton.setOnClickListener(view -> { @@ -350,9 +353,11 @@ public class ViewRPANBroadcastFragment extends Fragment { public void onResume() { super.onResume(); if (dataSourceFactory == null) { - dataSourceFactory = new DefaultHttpDataSourceFactory(Util.getUserAgent(mActivity, "Infinity")); + dataSourceFactory = new CacheDataSource.Factory().setCache(mSimpleCache) + .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory().setUserAgent(APIUtils.getRedgifsUserAgent(mActivity))); // Prepare the player with the source. - player.prepare(new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse(rpanBroadcast.rpanStream.hlsUrl))); + player.prepare(); + player.setMediaSource(new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(rpanBroadcast.rpanStream.hlsUrl))); if (mSharedPreferences.getBoolean(SharedPreferencesUtils.LOOP_VIDEO, true)) { player.setRepeatMode(Player.REPEAT_MODE_ALL); } else { diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewRedditGalleryVideoFragment.java b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewRedditGalleryVideoFragment.java index fd27d4db..93e028c1 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewRedditGalleryVideoFragment.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/fragments/ViewRedditGalleryVideoFragment.java @@ -7,7 +7,6 @@ import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.media.AudioManager; -import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; @@ -26,22 +25,21 @@ import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; -import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Tracks; import com.google.android.exoplayer2.source.ProgressiveMediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.material.bottomappbar.BottomAppBar; +import com.google.common.collect.ImmutableList; import javax.inject.Inject; import javax.inject.Named; @@ -54,6 +52,7 @@ import ml.docilealligator.infinityforreddit.activities.ViewRedditGalleryActivity import ml.docilealligator.infinityforreddit.bottomsheetfragments.PlaybackSpeedBottomSheetFragment; import ml.docilealligator.infinityforreddit.post.Post; import ml.docilealligator.infinityforreddit.services.DownloadMediaService; +import ml.docilealligator.infinityforreddit.utils.APIUtils; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; import ml.docilealligator.infinityforreddit.utils.Utils; @@ -82,7 +81,7 @@ public class ViewRedditGalleryVideoFragment extends Fragment { private Post.Gallery galleryVideo; private String subredditName; private boolean isNsfw; - private SimpleExoPlayer player; + private ExoPlayer player; private DataSource.Factory dataSourceFactory; private boolean wasPlaying = false; private boolean isMute = false; @@ -91,6 +90,8 @@ public class ViewRedditGalleryVideoFragment extends Fragment { @Inject @Named("default") SharedPreferences mSharedPreferences; + @Inject + SimpleCache mSimpleCache; public ViewRedditGalleryVideoFragment() { // Required empty public constructor @@ -153,13 +154,13 @@ public class ViewRedditGalleryVideoFragment extends Fragment { } }); - TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); - TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); - player = ExoPlayerFactory.newSimpleInstance(activity, trackSelector); + TrackSelector trackSelector = new DefaultTrackSelector(activity); + player = new ExoPlayer.Builder(activity).setTrackSelector(trackSelector).build(); videoPlayerView.setPlayer(player); - dataSourceFactory = new DefaultDataSourceFactory(activity, - Util.getUserAgent(activity, "Infinity")); - player.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse(galleryVideo.url))); + dataSourceFactory = new CacheDataSource.Factory().setCache(mSimpleCache) + .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory().setUserAgent(APIUtils.getRedgifsUserAgent(activity))); + player.prepare(); + player.setMediaSource(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(galleryVideo.url))); if (savedInstanceState != null) { playbackSpeed = savedInstanceState.getInt(PLAYBACK_SPEED_STATE); @@ -290,12 +291,13 @@ public class ViewRedditGalleryVideoFragment extends Fragment { muteButton.setImageResource(R.drawable.ic_unmute_24dp); } - player.addListener(new Player.EventListener() { + player.addListener(new Player.Listener() { @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + public void onTracksChanged(@NonNull Tracks tracks) { + ImmutableList<Tracks.Group> trackGroups = tracks.getGroups(); if (!trackGroups.isEmpty()) { - for (int i = 0; i < trackGroups.length; i++) { - String mimeType = trackGroups.get(i).getFormat(0).sampleMimeType; + for (int i = 0; i < trackGroups.size(); i++) { + String mimeType = trackGroups.get(i).getTrackFormat(0).sampleMimeType; if (mimeType != null && mimeType.contains("audio")) { muteButton.setVisibility(View.VISIBLE); muteButton.setOnClickListener(view -> { diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/Config.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/Config.java index 7fe095d4..8513c4bf 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/Config.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/Config.java @@ -29,15 +29,11 @@ import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.DefaultRenderersFactory.ExtensionRendererMode; import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.cache.Cache; -import ml.docilealligator.infinityforreddit.videoautoplay.annotations.Beta; - /** * Necessary configuration for {@link ExoCreator} to produces {@link SimpleExoPlayer} and * {@link MediaSource}. Instance of this class must be construct using {@link Builder}. @@ -61,7 +57,6 @@ public final class Config { @NonNull final MediaSourceBuilder mediaSourceBuilder; // Nullable options - @Nullable final DrmSessionManager<FrameworkMediaCrypto> drmSessionManager; @Nullable final Cache cache; // null by default // If null, ExoCreator must come up with a default one. // This is to help customizing the Data source, for example using OkHttp extension. @@ -69,17 +64,16 @@ public final class Config { @SuppressWarnings("WeakerAccess") // Config(@Nullable Context context, int extensionMode, @NonNull BaseMeter meter, - @NonNull LoadControl loadControl, - @Nullable DataSource.Factory dataSourceFactory, - @NonNull MediaSourceBuilder mediaSourceBuilder, - @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, @Nullable Cache cache) { + @NonNull LoadControl loadControl, + @Nullable DataSource.Factory dataSourceFactory, + @NonNull MediaSourceBuilder mediaSourceBuilder, + @Nullable Cache cache) { this.context = context != null ? context.getApplicationContext() : null; this.extensionMode = extensionMode; this.meter = meter; this.loadControl = loadControl; this.dataSourceFactory = dataSourceFactory; this.mediaSourceBuilder = mediaSourceBuilder; - this.drmSessionManager = drmSessionManager; this.cache = cache; } @@ -93,7 +87,6 @@ public final class Config { if (!meter.equals(config.meter)) return false; if (!loadControl.equals(config.loadControl)) return false; if (!mediaSourceBuilder.equals(config.mediaSourceBuilder)) return false; - if (!ObjectsCompat.equals(drmSessionManager, config.drmSessionManager)) return false; if (!ObjectsCompat.equals(cache, config.cache)) return false; return ObjectsCompat.equals(dataSourceFactory, config.dataSourceFactory); } @@ -103,7 +96,6 @@ public final class Config { result = 31 * result + meter.hashCode(); result = 31 * result + loadControl.hashCode(); result = 31 * result + mediaSourceBuilder.hashCode(); - result = 31 * result + (drmSessionManager != null ? drmSessionManager.hashCode() : 0); result = 31 * result + (cache != null ? cache.hashCode() : 0); result = 31 * result + (dataSourceFactory != null ? dataSourceFactory.hashCode() : 0); return result; @@ -111,7 +103,6 @@ public final class Config { @SuppressWarnings("unused") public Builder newBuilder() { return new Builder(context).setCache(this.cache) - .setDrmSessionManager(this.drmSessionManager) .setExtensionMode(this.extensionMode) .setLoadControl(this.loadControl) .setMediaSourceBuilder(this.mediaSourceBuilder) @@ -145,7 +136,6 @@ public final class Config { private LoadControl loadControl = new DefaultLoadControl(); private DataSource.Factory dataSourceFactory = null; private MediaSourceBuilder mediaSourceBuilder = MediaSourceBuilder.DEFAULT; - private DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null; private Cache cache = null; public Builder setExtensionMode(@ExtensionRendererMode int extensionMode) { @@ -175,13 +165,6 @@ public final class Config { return this; } - @Beta // - public Builder setDrmSessionManager( - @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) { - this.drmSessionManager = drmSessionManager; - return this; - } - public Builder setCache(@Nullable Cache cache) { this.cache = cache; return this; @@ -189,7 +172,7 @@ public final class Config { public Config build() { return new Config(context, extensionMode, meter, loadControl, dataSourceFactory, - mediaSourceBuilder, drmSessionManager, cache); + mediaSourceBuilder, cache); } } } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/DefaultExoCreator.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/DefaultExoCreator.java index af3bbd84..4c82c34b 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/DefaultExoCreator.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/DefaultExoCreator.java @@ -27,22 +27,26 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.DefaultRenderersFactory; +import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.source.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultDataSource; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import ml.docilealligator.infinityforreddit.utils.APIUtils; + /** * Usage: use this as-it or inheritance. * @@ -50,144 +54,139 @@ import java.io.IOException; * @since 3.4.0 */ -@SuppressWarnings({ "unused", "WeakerAccess" }) // +@SuppressWarnings({"unused", "WeakerAccess"}) // public class DefaultExoCreator implements ExoCreator, MediaSourceEventListener { - final ToroExo toro; // per application - final Config config; - private final TrackSelector trackSelector; // 'maybe' stateless - private final LoadControl loadControl; // stateless - private final MediaSourceBuilder mediaSourceBuilder; // stateless - private final RenderersFactory renderersFactory; // stateless - private final DataSource.Factory mediaDataSourceFactory; // stateless - private final DataSource.Factory manifestDataSourceFactory; // stateless - - public DefaultExoCreator(@NonNull ToroExo toro, @NonNull Config config) { - this.toro = checkNotNull(toro); - this.config = checkNotNull(config); - trackSelector = new DefaultTrackSelector(); - loadControl = config.loadControl; - mediaSourceBuilder = config.mediaSourceBuilder; - - DefaultRenderersFactory tempFactory = new DefaultRenderersFactory(this.toro.context); - tempFactory.setExtensionRendererMode(config.extensionMode); - renderersFactory = tempFactory; - - DataSource.Factory baseFactory = config.dataSourceFactory; - if (baseFactory == null) { - baseFactory = new DefaultHttpDataSourceFactory(toro.appName, config.meter); + final ToroExo toro; // per application + final Config config; + private final TrackSelector trackSelector; // 'maybe' stateless + private final LoadControl loadControl; // stateless + private final MediaSourceBuilder mediaSourceBuilder; // stateless + private final RenderersFactory renderersFactory; // stateless + private final DataSource.Factory mediaDataSourceFactory; // stateless + private final DataSource.Factory manifestDataSourceFactory; // stateless + + public DefaultExoCreator(@NonNull ToroExo toro, @NonNull Config config) { + this.toro = checkNotNull(toro); + this.config = checkNotNull(config); + trackSelector = new DefaultTrackSelector(); + loadControl = config.loadControl; + mediaSourceBuilder = config.mediaSourceBuilder; + + DefaultRenderersFactory tempFactory = new DefaultRenderersFactory(this.toro.context); + tempFactory.setExtensionRendererMode(config.extensionMode); + renderersFactory = tempFactory; + + DataSource.Factory baseFactory = config.dataSourceFactory; + if (baseFactory == null) { + baseFactory = new DefaultHttpDataSource.Factory().setUserAgent(APIUtils.getRedgifsUserAgent(getContext())); + } + DataSource.Factory factory = new DefaultDataSource.Factory(this.toro.context, baseFactory); + if (config.cache != null) + factory = new CacheDataSource.Factory().setCache(config.cache).setUpstreamDataSourceFactory(baseFactory); + mediaDataSourceFactory = factory; + manifestDataSourceFactory = new DefaultDataSource.Factory(this.toro.context); + } + + public DefaultExoCreator(Context context, Config config) { + this(with(context), config); + } + + @SuppressWarnings("SimplifiableIfStatement") + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DefaultExoCreator that = (DefaultExoCreator) o; + + if (!toro.equals(that.toro)) return false; + if (!trackSelector.equals(that.trackSelector)) return false; + if (!loadControl.equals(that.loadControl)) return false; + if (!mediaSourceBuilder.equals(that.mediaSourceBuilder)) return false; + if (!renderersFactory.equals(that.renderersFactory)) return false; + if (!mediaDataSourceFactory.equals(that.mediaDataSourceFactory)) return false; + return manifestDataSourceFactory.equals(that.manifestDataSourceFactory); + } + + @Override + public int hashCode() { + int result = toro.hashCode(); + result = 31 * result + trackSelector.hashCode(); + result = 31 * result + loadControl.hashCode(); + result = 31 * result + mediaSourceBuilder.hashCode(); + result = 31 * result + renderersFactory.hashCode(); + result = 31 * result + mediaDataSourceFactory.hashCode(); + result = 31 * result + manifestDataSourceFactory.hashCode(); + return result; + } + + final TrackSelector getTrackSelector() { + return trackSelector; + } + + @Nullable + @Override + public Context getContext() { + return toro.context; + } + + @NonNull + @Override + public ExoPlayer createPlayer() { + return new ToroExoPlayer(toro.context, renderersFactory, trackSelector, loadControl, + new DefaultBandwidthMeter.Builder(toro.context).build(), Util.getCurrentOrMainLooper()).getPlayer(); + } + + @NonNull + @Override + public MediaSource createMediaSource(@NonNull Uri uri, String fileExt) { + return mediaSourceBuilder.buildMediaSource(this.toro.context, uri, fileExt, new Handler(), + manifestDataSourceFactory, mediaDataSourceFactory, this); + } + + @NonNull + @Override // + public Playable createPlayable(@NonNull Uri uri, String fileExt) { + return new PlayableImpl(this, uri, fileExt); + } + + /// MediaSourceEventListener + + @Override + public void onLoadStarted(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + // no-ops + } + + @Override + public void onLoadCompleted(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + // no-ops + } + + @Override + public void onLoadCanceled(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + // no-ops + } + + @Override + public void onLoadError(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, + boolean wasCanceled) { + // no-ops + } + + @Override + public void onUpstreamDiscarded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, + MediaLoadData mediaLoadData) { + // no-ops + } + + @Override + public void onDownstreamFormatChanged(int windowIndex, + @Nullable MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { + // no-ops } - DataSource.Factory factory = new DefaultDataSourceFactory(this.toro.context, // - config.meter, baseFactory); - if (config.cache != null) factory = new CacheDataSourceFactory(config.cache, factory); - mediaDataSourceFactory = factory; - manifestDataSourceFactory = new DefaultDataSourceFactory(this.toro.context, this.toro.appName); - } - - public DefaultExoCreator(Context context, Config config) { - this(with(context), config); - } - - @SuppressWarnings("SimplifiableIfStatement") - @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - DefaultExoCreator that = (DefaultExoCreator) o; - - if (!toro.equals(that.toro)) return false; - if (!trackSelector.equals(that.trackSelector)) return false; - if (!loadControl.equals(that.loadControl)) return false; - if (!mediaSourceBuilder.equals(that.mediaSourceBuilder)) return false; - if (!renderersFactory.equals(that.renderersFactory)) return false; - if (!mediaDataSourceFactory.equals(that.mediaDataSourceFactory)) return false; - return manifestDataSourceFactory.equals(that.manifestDataSourceFactory); - } - - @Override public int hashCode() { - int result = toro.hashCode(); - result = 31 * result + trackSelector.hashCode(); - result = 31 * result + loadControl.hashCode(); - result = 31 * result + mediaSourceBuilder.hashCode(); - result = 31 * result + renderersFactory.hashCode(); - result = 31 * result + mediaDataSourceFactory.hashCode(); - result = 31 * result + manifestDataSourceFactory.hashCode(); - return result; - } - - final TrackSelector getTrackSelector() { - return trackSelector; - } - - @Nullable @Override public Context getContext() { - return toro.context; - } - - @NonNull @Override public SimpleExoPlayer createPlayer() { - return new ToroExoPlayer(toro.context, renderersFactory, trackSelector, loadControl, - new DefaultBandwidthMeter.Builder(toro.context).build(), config.drmSessionManager, - Util.getLooper()); - } - - @NonNull @Override public MediaSource createMediaSource(@NonNull Uri uri, String fileExt) { - return mediaSourceBuilder.buildMediaSource(this.toro.context, uri, fileExt, new Handler(), - manifestDataSourceFactory, mediaDataSourceFactory, this); - } - - @NonNull @Override // - public Playable createPlayable(@NonNull Uri uri, String fileExt) { - return new PlayableImpl(this, uri, fileExt); - } - - /// MediaSourceEventListener - - @Override - public void onLoadStarted(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { - // no-ops - } - - @Override - public void onLoadCompleted(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { - // no-ops - } - - @Override - public void onLoadCanceled(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { - // no-ops - } - - @Override - public void onLoadError(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, - LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, - boolean wasCanceled) { - // no-ops - } - - @Override public void onReadingStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { - // no-ops - } - - @Override - public void onUpstreamDiscarded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, - MediaLoadData mediaLoadData) { - // no-ops - } - - @Override public void onDownstreamFormatChanged(int windowIndex, - @Nullable MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { - // no-ops - } - - @Override - public void onMediaPeriodCreated(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { - // no-ops - } - - @Override - public void onMediaPeriodReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { - // no-ops - } } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoCreator.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoCreator.java index c4319493..d639c9d3 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoCreator.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoCreator.java @@ -22,6 +22,7 @@ import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.MediaSource; @@ -52,7 +53,8 @@ public interface ExoCreator { * * @return a new {@link SimpleExoPlayer} instance. */ - @NonNull SimpleExoPlayer createPlayer(); + @NonNull + ExoPlayer createPlayer(); /** * Create a {@link MediaSource} from media {@link Uri}. diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayable.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayable.java index a159ea17..9690d014 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayable.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayable.java @@ -17,7 +17,6 @@ package ml.docilealligator.infinityforreddit.videoautoplay; import static com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS; - import static ml.docilealligator.infinityforreddit.videoautoplay.ToroExo.toro; import android.net.Uri; @@ -28,16 +27,16 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.PlaybackException; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; -import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; +import com.google.android.exoplayer2.Tracks; import com.google.android.exoplayer2.source.BehindLiveWindowException; -import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.ui.PlayerView; +import com.google.common.collect.ImmutableList; import ml.docilealligator.infinityforreddit.R; @@ -52,154 +51,132 @@ import ml.docilealligator.infinityforreddit.R; @SuppressWarnings("WeakerAccess") public class ExoPlayable extends PlayableImpl { - @SuppressWarnings("unused") private static final String TAG = "ToroExo:Playable"; - - private EventListener listener; - - // Adapt from ExoPlayer demo. - protected boolean inErrorState = false; - protected TrackGroupArray lastSeenTrackGroupArray; - - /** - * Construct an instance of {@link ExoPlayable} from an {@link ExoCreator} and {@link Uri}. The - * {@link ExoCreator} is used to request {@link SimpleExoPlayer} instance, while {@link Uri} - * defines the media to play. - * - * @param creator the {@link ExoCreator} instance. - * @param uri the {@link Uri} of the media. - * @param fileExt the custom extension of the media Uri. - */ - public ExoPlayable(ExoCreator creator, Uri uri, String fileExt) { - super(creator, uri, fileExt); - } - - @Override public void prepare(boolean prepareSource) { - if (listener == null) { - listener = new Listener(); - super.addEventListener(listener); - } - super.prepare(prepareSource); - this.lastSeenTrackGroupArray = null; - this.inErrorState = false; - } - - @Override public void setPlayerView(@Nullable PlayerView playerView) { - // This will also clear these flags - if (playerView != this.playerView) { - this.lastSeenTrackGroupArray = null; - this.inErrorState = false; + @SuppressWarnings("unused") + private static final String TAG = "ToroExo:Playable"; + + private EventListener listener; + + // Adapt from ExoPlayer demo. + protected boolean inErrorState = false; + protected ImmutableList<Tracks.Group> lastSeenTrackGroupArray; + + /** + * Construct an instance of {@link ExoPlayable} from an {@link ExoCreator} and {@link Uri}. The + * {@link ExoCreator} is used to request {@link SimpleExoPlayer} instance, while {@link Uri} + * defines the media to play. + * + * @param creator the {@link ExoCreator} instance. + * @param uri the {@link Uri} of the media. + * @param fileExt the custom extension of the media Uri. + */ + public ExoPlayable(ExoCreator creator, Uri uri, String fileExt) { + super(creator, uri, fileExt); } - super.setPlayerView(playerView); - } - - @Override public void reset() { - super.reset(); - this.lastSeenTrackGroupArray = null; - this.inErrorState = false; - } - - @Override public void release() { - if (listener != null) { - super.removeEventListener(listener); - listener = null; + + @Override + public void prepare(boolean prepareSource) { + if (listener == null) { + listener = new Listener(); + super.addEventListener(listener); + } + super.prepare(prepareSource); + this.lastSeenTrackGroupArray = null; + this.inErrorState = false; } - super.release(); - this.lastSeenTrackGroupArray = null; - this.inErrorState = false; - } - - @SuppressWarnings({ "unused" }) // - protected void onErrorMessage(@NonNull String message) { - // Sub class can have custom reaction about the error here, including not to show this toast - // (by not calling super.onErrorMessage(message)). - if (this.errorListeners.size() > 0) { - this.errorListeners.onError(new RuntimeException(message)); - } else if (playerView != null) { - Toast.makeText(playerView.getContext(), message, Toast.LENGTH_SHORT).show(); + + @Override + public void setPlayerView(@Nullable PlayerView playerView) { + // This will also clear these flags + if (playerView != this.playerView) { + this.lastSeenTrackGroupArray = null; + this.inErrorState = false; + } + super.setPlayerView(playerView); } - } - class Listener extends DefaultEventListener { + @Override + public void reset() { + super.reset(); + this.lastSeenTrackGroupArray = null; + this.inErrorState = false; + } @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - super.onTracksChanged(trackGroups, trackSelections); - if (trackGroups == lastSeenTrackGroupArray) return; - lastSeenTrackGroupArray = trackGroups; - if (!(creator instanceof DefaultExoCreator)) return; - TrackSelector selector = ((DefaultExoCreator) creator).getTrackSelector(); - if (selector instanceof DefaultTrackSelector) { - MappedTrackInfo trackInfo = ((DefaultTrackSelector) selector).getCurrentMappedTrackInfo(); - if (trackInfo != null) { - if (trackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) == RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { - onErrorMessage(toro.getString(R.string.error_unsupported_video)); - } - - if (trackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO) == RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { - onErrorMessage(toro.getString(R.string.error_unsupported_audio)); - } + public void release() { + if (listener != null) { + super.removeEventListener(listener); + listener = null; } - } + super.release(); + this.lastSeenTrackGroupArray = null; + this.inErrorState = false; } - @Override public void onPlayerError(ExoPlaybackException error) { - /// Adapt from ExoPlayer Demo - String errorString = null; - if (error.type == ExoPlaybackException.TYPE_RENDERER) { - Exception cause = error.getRendererException(); - if (cause instanceof MediaCodecRenderer.DecoderInitializationException) { - // Special case for decoder initialization failures. - MediaCodecRenderer.DecoderInitializationException decoderInitializationException = - (MediaCodecRenderer.DecoderInitializationException) cause; - if (decoderInitializationException.decoderName == null) { - if (decoderInitializationException.getCause() instanceof MediaCodecUtil.DecoderQueryException) { - errorString = toro.getString(R.string.error_querying_decoders); - } else if (decoderInitializationException.secureDecoderRequired) { - errorString = toro.getString(R.string.error_no_secure_decoder, - decoderInitializationException.mimeType); - } else { - errorString = toro.getString(R.string.error_no_decoder, - decoderInitializationException.mimeType); + @SuppressWarnings({"unused"}) // + protected void onErrorMessage(@NonNull String message) { + // Sub class can have custom reaction about the error here, including not to show this toast + // (by not calling super.onErrorMessage(message)). + if (this.errorListeners.size() > 0) { + this.errorListeners.onError(new RuntimeException(message)); + } else if (playerView != null) { + Toast.makeText(playerView.getContext(), message, Toast.LENGTH_SHORT).show(); + } + } + + class Listener extends DefaultEventListener { + @Override + public void onTracksChanged(@NonNull Tracks tracks) { + ImmutableList<Tracks.Group> trackGroups = tracks.getGroups(); + if (trackGroups == lastSeenTrackGroupArray) return; + lastSeenTrackGroupArray = trackGroups; + if (!(creator instanceof DefaultExoCreator)) return; + TrackSelector selector = ((DefaultExoCreator) creator).getTrackSelector(); + if (selector instanceof DefaultTrackSelector) { + MappedTrackInfo trackInfo = ((DefaultTrackSelector) selector).getCurrentMappedTrackInfo(); + if (trackInfo != null) { + if (trackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) == RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + onErrorMessage(toro.getString(R.string.error_unsupported_video)); + } + + if (trackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO) == RENDERER_SUPPORT_UNSUPPORTED_TRACKS) { + onErrorMessage(toro.getString(R.string.error_unsupported_audio)); + } + } } - } else { - errorString = toro.getString(R.string.error_instantiating_decoder, - decoderInitializationException.decoderName); - } } - } - if (errorString != null) onErrorMessage(errorString); + @Override + public void onPlayerError(@NonNull PlaybackException error) { + inErrorState = true; + if (isBehindLiveWindow(error)) { + ExoPlayable.super.reset(); + } else { + ExoPlayable.super.updatePlaybackInfo(); + } - inErrorState = true; - if (isBehindLiveWindow(error)) { - ExoPlayable.super.reset(); - } else { - ExoPlayable.super.updatePlaybackInfo(); - } + super.onPlayerError(error); + } - super.onPlayerError(error); + @Override + public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, int reason) { + if (inErrorState) { + // Adapt from ExoPlayer demo. + // "This will only occur if the user has performed a seek whilst in the error state. Update + // the resume position so that if the user then retries, playback will resume from the + // position to which they seek." - ExoPlayer + ExoPlayable.super.updatePlaybackInfo(); + } + super.onPositionDiscontinuity(oldPosition, newPosition, reason); + } } - @Override public void onPositionDiscontinuity(int reason) { - if (inErrorState) { - // Adapt from ExoPlayer demo. - // "This will only occur if the user has performed a seek whilst in the error state. Update - // the resume position so that if the user then retries, playback will resume from the - // position to which they seek." - ExoPlayer - ExoPlayable.super.updatePlaybackInfo(); - } - - super.onPositionDiscontinuity(reason); - } - } - - static boolean isBehindLiveWindow(ExoPlaybackException error) { - if (error.type != ExoPlaybackException.TYPE_SOURCE) return false; - Throwable cause = error.getSourceException(); - while (cause != null) { - if (cause instanceof BehindLiveWindowException) return true; - cause = cause.getCause(); + static boolean isBehindLiveWindow(PlaybackException error) { + if (error instanceof ExoPlaybackException && ((ExoPlaybackException) error).type != ExoPlaybackException.TYPE_SOURCE) return false; + Throwable cause = error.getCause(); + while (cause != null) { + if (cause instanceof BehindLiveWindowException) return true; + cause = cause.getCause(); + } + return false; } - return false; - } } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayerDispatcher.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayerDispatcher.java deleted file mode 100644 index 031c1f3a..00000000 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ExoPlayerDispatcher.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2018 Nam Nguyen, nam@ene.im - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ml.docilealligator.infinityforreddit.videoautoplay; - -import com.google.android.exoplayer2.DefaultControlDispatcher; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.ui.PlayerView; - -import ml.docilealligator.infinityforreddit.videoautoplay.annotations.Beta; -import ml.docilealligator.infinityforreddit.videoautoplay.widget.PressablePlayerSelector; - -/** - * @author eneim (2018/08/18). - * @since 3.6.0.2802 - * - * Work with {@link PressablePlayerSelector} and {@link PlayerView} to handle user's custom playback - * interaction. A common use-case is when user clicks the Play button to manually start a playback. - * We should respect this by putting the {@link ToroPlayer}'s priority to highest, and request a - * refresh for all {@link ToroPlayer}. - * - * The same behaviour should be handled for the case user clicks the Pause button. - * - * All behaviour should be cleared once user scroll the selection out of playable region. This is - * already handled by {@link PressablePlayerSelector}. - */ -@Beta // -public class ExoPlayerDispatcher extends DefaultControlDispatcher { - - private final PressablePlayerSelector playerSelector; - private final ToroPlayer toroPlayer; - - public ExoPlayerDispatcher(PressablePlayerSelector playerSelector, ToroPlayer toroPlayer) { - this.playerSelector = playerSelector; - this.toroPlayer = toroPlayer; - } - - @Override public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) { - if (playWhenReady) { - // Container will handle the call to play. - return playerSelector.toPlay(toroPlayer.getPlayerOrder()); - } else { - player.setPlayWhenReady(false); - playerSelector.toPause(toroPlayer.getPlayerOrder()); - return true; - } - } -} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/MediaSourceBuilder.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/MediaSourceBuilder.java index 885319d0..d54798cf 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/MediaSourceBuilder.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/MediaSourceBuilder.java @@ -28,6 +28,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.ContentType; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.source.LoopingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; @@ -46,59 +47,62 @@ import com.google.android.exoplayer2.upstream.DataSource; public interface MediaSourceBuilder { - @NonNull MediaSource buildMediaSource(@NonNull Context context, @NonNull Uri uri, - @Nullable String fileExt, @Nullable Handler handler, - @NonNull DataSource.Factory manifestDataSourceFactory, - @NonNull DataSource.Factory mediaDataSourceFactory, - @Nullable MediaSourceEventListener listener); + @NonNull + MediaSource buildMediaSource(@NonNull Context context, @NonNull Uri uri, + @Nullable String fileExt, @Nullable Handler handler, + @NonNull DataSource.Factory manifestDataSourceFactory, + @NonNull DataSource.Factory mediaDataSourceFactory, + @Nullable MediaSourceEventListener listener); - MediaSourceBuilder DEFAULT = new MediaSourceBuilder() { - @NonNull @Override - public MediaSource buildMediaSource(@NonNull Context context, @NonNull Uri uri, - @Nullable String ext, @Nullable Handler handler, - @NonNull DataSource.Factory manifestDataSourceFactory, - @NonNull DataSource.Factory mediaDataSourceFactory, MediaSourceEventListener listener) { - @ContentType int type = isEmpty(ext) ? inferContentType(uri) : inferContentType("." + ext); - MediaSource result; - switch (type) { - case C.TYPE_SS: - result = new SsMediaSource.Factory( - new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory) - .createMediaSource(uri); - break; - case C.TYPE_DASH: - result = new DashMediaSource.Factory( - new DefaultDashChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory) - .createMediaSource(uri); - break; - case C.TYPE_HLS: - result = new HlsMediaSource.Factory(mediaDataSourceFactory) // - .createMediaSource(uri); - break; - case C.TYPE_OTHER: - result = new ProgressiveMediaSource.Factory(mediaDataSourceFactory) // - .createMediaSource(uri); - break; - default: - throw new IllegalStateException("Unsupported type: " + type); - } + MediaSourceBuilder DEFAULT = new MediaSourceBuilder() { + @NonNull + @Override + public MediaSource buildMediaSource(@NonNull Context context, @NonNull Uri uri, + @Nullable String ext, @Nullable Handler handler, + @NonNull DataSource.Factory manifestDataSourceFactory, + @NonNull DataSource.Factory mediaDataSourceFactory, MediaSourceEventListener listener) { + @ContentType int type = isEmpty(ext) ? inferContentType(uri) : inferContentType("." + ext); + MediaSource result; + switch (type) { + case C.CONTENT_TYPE_SS: + result = new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory) + .createMediaSource(MediaItem.fromUri(uri)); + break; + case C.CONTENT_TYPE_DASH: + result = new DashMediaSource.Factory( + new DefaultDashChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory) + .createMediaSource(MediaItem.fromUri(uri)); + break; + case C.CONTENT_TYPE_HLS: + result = new HlsMediaSource.Factory(mediaDataSourceFactory) // + .createMediaSource(MediaItem.fromUri(uri)); + break; + case C.CONTENT_TYPE_OTHER: + result = new ProgressiveMediaSource.Factory(mediaDataSourceFactory) // + .createMediaSource(MediaItem.fromUri(uri)); + break; + default: + throw new IllegalStateException("Unsupported type: " + type); + } - result.addEventListener(handler, listener); - return result; - } - }; + result.addEventListener(handler, listener); + return result; + } + }; - MediaSourceBuilder LOOPING = new MediaSourceBuilder() { + MediaSourceBuilder LOOPING = new MediaSourceBuilder() { - @NonNull @Override - public MediaSource buildMediaSource(@NonNull Context context, @NonNull Uri uri, - @Nullable String fileExt, @Nullable Handler handler, - @NonNull DataSource.Factory manifestDataSourceFactory, - @NonNull DataSource.Factory mediaDataSourceFactory, - @Nullable MediaSourceEventListener listener) { - return new LoopingMediaSource( - DEFAULT.buildMediaSource(context, uri, fileExt, handler, manifestDataSourceFactory, - mediaDataSourceFactory, listener)); - } - }; + @NonNull + @Override + public MediaSource buildMediaSource(@NonNull Context context, @NonNull Uri uri, + @Nullable String fileExt, @Nullable Handler handler, + @NonNull DataSource.Factory manifestDataSourceFactory, + @NonNull DataSource.Factory mediaDataSourceFactory, + @Nullable MediaSourceEventListener listener) { + return new LoopingMediaSource( + DEFAULT.buildMediaSource(context, uri, fileExt, handler, manifestDataSourceFactory, + mediaDataSourceFactory, listener)); + } + }; } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/Playable.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/Playable.java index a713b5a7..fb9e177e 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/Playable.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/Playable.java @@ -20,20 +20,20 @@ import androidx.annotation.FloatRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Tracks; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.CueGroup; import com.google.android.exoplayer2.text.TextOutput; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.exoplayer2.video.VideoListener; +import com.google.android.exoplayer2.video.VideoSize; import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; @@ -44,7 +44,7 @@ import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo; /** * Define an interface to control a playback, specific for {@link SimpleExoPlayer} and {@link PlayerView}. - * + * <p> * This interface is designed to be reused across Config change. Implementation must not hold any * strong reference to Activity, and if it supports any kind of that, make sure to implicitly clean * it up. @@ -56,310 +56,350 @@ import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo; @SuppressWarnings("unused") // public interface Playable { - /** - * Prepare the resource for a {@link SimpleExoPlayer}. This method should: - * - Request for new {@link SimpleExoPlayer} instance if there is not a usable one. - * - Configure {@link EventListener} for it. - * - If there is non-trivial PlaybackInfo, update it to the SimpleExoPlayer. - * - If client request to prepare MediaSource, then prepare it. - * - * This method must be called before {@link #setPlayerView(PlayerView)}. - * - * @param prepareSource if {@code true}, also prepare the MediaSource when preparing the Player, - * if {@code false} just do nothing for the MediaSource. - */ - void prepare(boolean prepareSource); - - /** - * Set the {@link PlayerView} for this Playable. It is expected that a playback doesn't require a - * UI, so this setup is optional. But it must be called after the SimpleExoPlayer is prepared, - * that is after {@link #prepare(boolean)} and before {@link #release()}. - * - * Changing the PlayerView during playback is expected, though not always recommended, especially - * on old Devices with low Android API. - * - * @param playerView the PlayerView to set to the SimpleExoPlayer. - */ - void setPlayerView(@Nullable PlayerView playerView); - - /** - * Get current {@link PlayerView} of this Playable. - * - * @return current PlayerView instance of this Playable. - */ - @Nullable PlayerView getPlayerView(); - - /** - * Start the playback. If the {@link MediaSource} is not prepared, then also prepare it. - */ - void play(); - - /** - * Pause the playback. - */ - void pause(); - - /** - * Reset all resource, so that the playback can start all over again. This is to cleanup the - * playback for reuse. The SimpleExoPlayer instance must be still usable without calling - * {@link #prepare(boolean)}. - */ - void reset(); - - /** - * Release all resource. After this, the SimpleExoPlayer is released to the Player pool and the - * Playable must call {@link #prepare(boolean)} again to use it again. - */ - void release(); - - /** - * Get current {@link PlaybackInfo} of the playback. - * - * @return current PlaybackInfo of the playback. - */ - @NonNull - PlaybackInfo getPlaybackInfo(); - - /** - * Set the custom {@link PlaybackInfo} for this playback. This could suggest a seek. - * - * @param playbackInfo the PlaybackInfo to set for this playback. - */ - void setPlaybackInfo(@NonNull PlaybackInfo playbackInfo); - - /** - * Add a new {@link EventListener} to this Playable. As calling {@link #prepare(boolean)} also - * triggers some internal events, this method should be called before {@link #prepare(boolean)} so - * that Client could received them all. - * - * @param listener the EventListener to add, must be not {@code null}. - */ - void addEventListener(@NonNull EventListener listener); - - /** - * Remove an {@link EventListener} from this Playable. - * - * @param listener the EventListener to be removed. If null, nothing happens. - */ - void removeEventListener(EventListener listener); - - /** - * !This must only work if the Player in use is a {@link ToroExoPlayer}. - */ - void addOnVolumeChangeListener(@NonNull ToroPlayer.OnVolumeChangeListener listener); - - void removeOnVolumeChangeListener(@Nullable ToroPlayer.OnVolumeChangeListener listener); - - /** - * Check if current Playable is playing or not. - * - * @return {@code true} if this Playable is playing, {@code false} otherwise. - */ - boolean isPlaying(); - - /** - * Change the volume of current playback. - * - * @param volume the volume value to be set. Must be a {@code float} of range from 0 to 1. - * @deprecated use {@link #setVolumeInfo(VolumeInfo)} instead. - */ - @RemoveIn(version = "3.6.0") @Deprecated // - void setVolume(@FloatRange(from = 0.0, to = 1.0) float volume); - - /** - * Obtain current volume value. The returned value is a {@code float} of range from 0 to 1. - * - * @return current volume value. - * @deprecated use {@link #getVolumeInfo()} instead. - */ - @RemoveIn(version = "3.6.0") @Deprecated // - @FloatRange(from = 0.0, to = 1.0) float getVolume(); - - /** - * Update playback's volume. - * - * @param volumeInfo the {@link VolumeInfo} to update to. - * @return {@code true} if current Volume info is updated, {@code false} otherwise. - */ - boolean setVolumeInfo(@NonNull VolumeInfo volumeInfo); - - /** - * Get current {@link VolumeInfo}. - */ - @NonNull VolumeInfo getVolumeInfo(); - - /** - * Same as {@link Player#setPlaybackParameters(PlaybackParameters)} - */ - void setParameters(@Nullable PlaybackParameters parameters); - - /** - * Same as {@link Player#getPlaybackParameters()} - */ - @Nullable PlaybackParameters getParameters(); - - void addErrorListener(@NonNull ToroPlayer.OnErrorListener listener); - - void removeErrorListener(@Nullable ToroPlayer.OnErrorListener listener); - - // Combine necessary interfaces. - interface EventListener extends Player.EventListener, VideoListener, TextOutput, MetadataOutput { - - } - - /** Default empty implementation */ - class DefaultEventListener implements EventListener { - - @Override public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { - - } - - @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - + /** + * Prepare the resource for a {@link SimpleExoPlayer}. This method should: + * - Request for new {@link SimpleExoPlayer} instance if there is not a usable one. + * - Configure {@link EventListener} for it. + * - If there is non-trivial PlaybackInfo, update it to the SimpleExoPlayer. + * - If client request to prepare MediaSource, then prepare it. + * <p> + * This method must be called before {@link #setPlayerView(PlayerView)}. + * + * @param prepareSource if {@code true}, also prepare the MediaSource when preparing the Player, + * if {@code false} just do nothing for the MediaSource. + */ + void prepare(boolean prepareSource); + + /** + * Set the {@link PlayerView} for this Playable. It is expected that a playback doesn't require a + * UI, so this setup is optional. But it must be called after the SimpleExoPlayer is prepared, + * that is after {@link #prepare(boolean)} and before {@link #release()}. + * <p> + * Changing the PlayerView during playback is expected, though not always recommended, especially + * on old Devices with low Android API. + * + * @param playerView the PlayerView to set to the SimpleExoPlayer. + */ + void setPlayerView(@Nullable PlayerView playerView); + + /** + * Get current {@link PlayerView} of this Playable. + * + * @return current PlayerView instance of this Playable. + */ + @Nullable + PlayerView getPlayerView(); + + /** + * Start the playback. If the {@link MediaSource} is not prepared, then also prepare it. + */ + void play(); + + /** + * Pause the playback. + */ + void pause(); + + /** + * Reset all resource, so that the playback can start all over again. This is to cleanup the + * playback for reuse. The SimpleExoPlayer instance must be still usable without calling + * {@link #prepare(boolean)}. + */ + void reset(); + + /** + * Release all resource. After this, the SimpleExoPlayer is released to the Player pool and the + * Playable must call {@link #prepare(boolean)} again to use it again. + */ + void release(); + + /** + * Get current {@link PlaybackInfo} of the playback. + * + * @return current PlaybackInfo of the playback. + */ + @NonNull + PlaybackInfo getPlaybackInfo(); + + /** + * Set the custom {@link PlaybackInfo} for this playback. This could suggest a seek. + * + * @param playbackInfo the PlaybackInfo to set for this playback. + */ + void setPlaybackInfo(@NonNull PlaybackInfo playbackInfo); + + /** + * Add a new {@link EventListener} to this Playable. As calling {@link #prepare(boolean)} also + * triggers some internal events, this method should be called before {@link #prepare(boolean)} so + * that Client could received them all. + * + * @param listener the EventListener to add, must be not {@code null}. + */ + void addEventListener(@NonNull EventListener listener); + + /** + * Remove an {@link EventListener} from this Playable. + * + * @param listener the EventListener to be removed. If null, nothing happens. + */ + void removeEventListener(EventListener listener); + + /** + * !This must only work if the Player in use is a {@link ToroExoPlayer}. + */ + void addOnVolumeChangeListener(@NonNull ToroPlayer.OnVolumeChangeListener listener); + + void removeOnVolumeChangeListener(@Nullable ToroPlayer.OnVolumeChangeListener listener); + + /** + * Check if current Playable is playing or not. + * + * @return {@code true} if this Playable is playing, {@code false} otherwise. + */ + boolean isPlaying(); + + /** + * Change the volume of current playback. + * + * @param volume the volume value to be set. Must be a {@code float} of range from 0 to 1. + * @deprecated use {@link #setVolumeInfo(VolumeInfo)} instead. + */ + @RemoveIn(version = "3.6.0") + @Deprecated + // + void setVolume(@FloatRange(from = 0.0, to = 1.0) float volume); + + /** + * Obtain current volume value. The returned value is a {@code float} of range from 0 to 1. + * + * @return current volume value. + * @deprecated use {@link #getVolumeInfo()} instead. + */ + @RemoveIn(version = "3.6.0") + @Deprecated // + @FloatRange(from = 0.0, to = 1.0) + float getVolume(); + + /** + * Update playback's volume. + * + * @param volumeInfo the {@link VolumeInfo} to update to. + * @return {@code true} if current Volume info is updated, {@code false} otherwise. + */ + boolean setVolumeInfo(@NonNull VolumeInfo volumeInfo); + + /** + * Get current {@link VolumeInfo}. + */ + @NonNull + VolumeInfo getVolumeInfo(); + + /** + * Same as {@link Player#setPlaybackParameters(PlaybackParameters)} + */ + void setParameters(@Nullable PlaybackParameters parameters); + + /** + * Same as {@link Player#getPlaybackParameters()} + */ + @Nullable + PlaybackParameters getParameters(); + + void addErrorListener(@NonNull ToroPlayer.OnErrorListener listener); + + void removeErrorListener(@Nullable ToroPlayer.OnErrorListener listener); + + // Combine necessary interfaces. + interface EventListener extends Player.Listener, TextOutput, MetadataOutput { + + @Override + default void onCues(@NonNull List<Cue> cues) { + + } + + @Override + default void onCues(@NonNull CueGroup cueGroup) { + + } + + @Override + default void onMetadata(@NonNull Metadata metadata) { + + } } - @Override public void onLoadingChanged(boolean isLoading) { + /** + * Default empty implementation + */ + class DefaultEventListener implements EventListener { + @Override + public void onTimelineChanged(@NonNull Timeline timeline, int reason) { - } + } - @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - - } - - @Override public void onRepeatModeChanged(int repeatMode) { - - } + @Override + public void onTracksChanged(@NonNull Tracks tracks) { - @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + } - } + @Override + public void onIsLoadingChanged(boolean isLoading) { - @Override public void onPlayerError(ExoPlaybackException error) { + } - } + @Override + public void onPlaybackStateChanged(int playbackState) { - @Override public void onPositionDiscontinuity(int reason) { + } - } + @Override + public void onRepeatModeChanged(int repeatMode) { - @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + } - } + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - @Override public void onSeekProcessed() { + } - } + @Override + public void onPlayerError(@NonNull PlaybackException error) { - @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio) { + } - } + @Override + public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, int reason) { - @Override public void onRenderedFirstFrame() { + } - } + @Override + public void onPlaybackParametersChanged(@NonNull PlaybackParameters playbackParameters) { - @Override public void onCues(List<Cue> cues) { + } - } + @Override + public void onSeekProcessed() { - @Override public void onMetadata(Metadata metadata) { + } - } - } + @Override + public void onVideoSizeChanged(@NonNull VideoSize videoSize) { + EventListener.super.onVideoSizeChanged(videoSize); + } - /** List of EventListener */ - class EventListeners extends CopyOnWriteArraySet<EventListener> implements EventListener { + @Override + public void onRenderedFirstFrame() { - EventListeners() { - } + } - @Override public void onVideoSizeChanged(int width, int height, int unAppliedRotationDegrees, - float pixelWidthHeightRatio) { - for (EventListener eventListener : this) { - eventListener.onVideoSizeChanged(width, height, unAppliedRotationDegrees, - pixelWidthHeightRatio); - } - } + @Override + public void onCues(@NonNull CueGroup cueGroup) { + EventListener.super.onCues(cueGroup); + } - @Override public void onRenderedFirstFrame() { - for (EventListener eventListener : this) { - eventListener.onRenderedFirstFrame(); - } - } - - @Override public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { - for (EventListener eventListener : this) { - eventListener.onTimelineChanged(timeline, manifest, reason); - } - } - - @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - for (EventListener eventListener : this) { - eventListener.onTracksChanged(trackGroups, trackSelections); - } - } - - @Override public void onLoadingChanged(boolean isLoading) { - for (EventListener eventListener : this) { - eventListener.onLoadingChanged(isLoading); - } - } - - @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - for (EventListener eventListener : this) { - eventListener.onPlayerStateChanged(playWhenReady, playbackState); - } - } - - @Override public void onRepeatModeChanged(int repeatMode) { - for (EventListener eventListener : this) { - eventListener.onRepeatModeChanged(repeatMode); - } - } - - @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - for (EventListener eventListener : this) { - eventListener.onShuffleModeEnabledChanged(shuffleModeEnabled); - } - } - - @Override public void onPlayerError(ExoPlaybackException error) { - for (EventListener eventListener : this) { - eventListener.onPlayerError(error); - } - } - - @Override public void onPositionDiscontinuity(int reason) { - for (EventListener eventListener : this) { - eventListener.onPositionDiscontinuity(reason); - } - } - - @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - for (EventListener eventListener : this) { - eventListener.onPlaybackParametersChanged(playbackParameters); - } - } - - @Override public void onSeekProcessed() { - for (EventListener eventListener : this) { - eventListener.onSeekProcessed(); - } - } + @Override + public void onMetadata(@NonNull Metadata metadata) { - @Override public void onCues(List<Cue> cues) { - for (EventListener eventListener : this) { - eventListener.onCues(cues); - } + } } - @Override public void onMetadata(Metadata metadata) { - for (EventListener eventListener : this) { - eventListener.onMetadata(metadata); - } + /** + * List of EventListener + */ + class EventListeners extends CopyOnWriteArraySet<EventListener> implements EventListener { + + EventListeners() { + } + + @Override + public void onVideoSizeChanged(@NonNull VideoSize videoSize) { + for (EventListener eventListener : this) { + eventListener.onVideoSizeChanged(videoSize); + } + } + + @Override + public void onRenderedFirstFrame() { + for (EventListener eventListener : this) { + eventListener.onRenderedFirstFrame(); + } + } + + @Override + public void onTimelineChanged(@NonNull Timeline timeline, int reason) { + for (EventListener eventListener : this) { + eventListener.onTimelineChanged(timeline, reason); + } + } + + @Override + public void onTracksChanged(@NonNull Tracks tracks) { + for (EventListener eventListener : this) { + eventListener.onTracksChanged(tracks); + } + } + + @Override + public void onIsLoadingChanged(boolean isLoading) { + for (EventListener eventListener : this) { + eventListener.onIsLoadingChanged(isLoading); + } + } + + @Override + public void onPlaybackStateChanged(int playbackState) { + for (EventListener eventListener : this) { + eventListener.onPlaybackStateChanged(playbackState); + } + } + + @Override + public void onRepeatModeChanged(int repeatMode) { + for (EventListener eventListener : this) { + eventListener.onRepeatModeChanged(repeatMode); + } + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + for (EventListener eventListener : this) { + eventListener.onShuffleModeEnabledChanged(shuffleModeEnabled); + } + } + + @Override + public void onPlayerError(@NonNull PlaybackException error) { + for (EventListener eventListener : this) { + eventListener.onPlayerError(error); + } + } + + @Override + public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, int reason) { + for (EventListener eventListener : this) { + eventListener.onPositionDiscontinuity(oldPosition, newPosition, reason); + } + } + + @Override + public void onPlaybackParametersChanged(@NonNull PlaybackParameters playbackParameters) { + for (EventListener eventListener : this) { + eventListener.onPlaybackParametersChanged(playbackParameters); + } + } + + @Override + public void onCues(@NonNull CueGroup cueGroup) { + for (EventListener eventListener : this) { + eventListener.onCues(cueGroup); + } + } + + @Override + public void onMetadata(@NonNull Metadata metadata) { + for (EventListener eventListener : this) { + eventListener.onMetadata(metadata); + } + } } - } } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/PlayableImpl.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/PlayableImpl.java index d1d47c07..a3f762e9 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/PlayableImpl.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/PlayableImpl.java @@ -30,7 +30,6 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.ui.PlayerView; @@ -39,9 +38,9 @@ import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo; /** * [20180225] - * + * <p> * Default implementation of {@link Playable}. - * + * <p> * Instance of {@link Playable} should be reusable. Retaining instance of Playable across config * change must guarantee that all {@link EventListener} are cleaned up on config change. * @@ -50,238 +49,264 @@ import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo; @SuppressWarnings("WeakerAccess") // class PlayableImpl implements Playable { - private final PlaybackInfo playbackInfo = new PlaybackInfo(); // never expose to outside. + private final PlaybackInfo playbackInfo = new PlaybackInfo(); // never expose to outside. + + protected final EventListeners listeners = new EventListeners(); // original listener. + protected final ToroPlayer.VolumeChangeListeners volumeChangeListeners = new ToroPlayer.VolumeChangeListeners(); + protected final ToroPlayer.ErrorListeners errorListeners = new ToroPlayer.ErrorListeners(); + + protected final Uri mediaUri; // immutable, parcelable + protected final String fileExt; + protected final ExoCreator creator; // required, cached + + protected ToroExoPlayer player; // on-demand, cached + protected MediaSource mediaSource; // on-demand, since we do not reuse MediaSource now. + protected PlayerView playerView; // on-demand, not always required. + + private boolean sourcePrepared = false; + private boolean listenerApplied = false; + + PlayableImpl(ExoCreator creator, Uri uri, String fileExt) { + this.creator = creator; + this.mediaUri = uri; + this.fileExt = fileExt; + } - protected final EventListeners listeners = new EventListeners(); // original listener. - protected final ToroPlayer.VolumeChangeListeners volumeChangeListeners = new ToroPlayer.VolumeChangeListeners(); - protected final ToroPlayer.ErrorListeners errorListeners = new ToroPlayer.ErrorListeners(); + @CallSuper + @Override + public void prepare(boolean prepareSource) { + if (prepareSource) { + ensureMediaSource(); + ensurePlayerView(); + } + } - protected final Uri mediaUri; // immutable, parcelable - protected final String fileExt; - protected final ExoCreator creator; // required, cached + @CallSuper + @Override + public void setPlayerView(@Nullable PlayerView playerView) { + if (this.playerView == playerView) return; + if (playerView == null) { + this.playerView.setPlayer(null); + } else { + if (this.player != null) { + PlayerView.switchTargetView(this.player.getPlayer(), this.playerView, playerView); + } + } - protected SimpleExoPlayer player; // on-demand, cached - protected MediaSource mediaSource; // on-demand, since we do not reuse MediaSource now. - protected PlayerView playerView; // on-demand, not always required. + this.playerView = playerView; + } + + @Override + public final PlayerView getPlayerView() { + return this.playerView; + } - private boolean sourcePrepared = false; - private boolean listenerApplied = false; + @CallSuper + @Override + public void play() { + ensureMediaSource(); + ensurePlayerView(); + checkNotNull(player, "Playable#play(): Player is null!"); + player.getPlayer().setPlayWhenReady(true); + } + + @CallSuper + @Override + public void pause() { + // Player is not required to be non-null here. + if (player != null) player.getPlayer().setPlayWhenReady(false); + } + + @CallSuper + @Override + public void reset() { + this.playbackInfo.reset(); + if (player != null) { + // reset volume to default + ToroExo.setVolumeInfo(this.player, new VolumeInfo(false, 1.f)); + player.getPlayer().stop(); + player.getPlayer().clearMediaItems(); + } + this.mediaSource = null; // so it will be re-prepared when play() is called. + this.sourcePrepared = false; + } + + @CallSuper + @Override + public void release() { + this.setPlayerView(null); + if (this.player != null) { + // reset volume to default + ToroExo.setVolumeInfo(this.player, new VolumeInfo(false, 1.f)); + player.getPlayer().stop(); + player.getPlayer().clearMediaItems(); + if (listenerApplied) { + player.getPlayer().removeListener(listeners); + if (this.player != null) { + this.player.removeOnVolumeChangeListener(this.volumeChangeListeners); + } + listenerApplied = false; + } + with(checkNotNull(creator.getContext(), "ExoCreator has no Context")) // + .releasePlayer(this.creator, this.player.getPlayer()); + } + this.player = null; + this.mediaSource = null; + this.sourcePrepared = false; + } + + @CallSuper + @NonNull + @Override + public PlaybackInfo getPlaybackInfo() { + updatePlaybackInfo(); + return new PlaybackInfo(playbackInfo.getResumeWindow(), playbackInfo.getResumePosition(), + playbackInfo.getVolumeInfo()); + } + + @CallSuper + @Override + public void setPlaybackInfo(@NonNull PlaybackInfo playbackInfo) { + this.playbackInfo.setResumeWindow(playbackInfo.getResumeWindow()); + this.playbackInfo.setResumePosition(playbackInfo.getResumePosition()); + this.setVolumeInfo(playbackInfo.getVolumeInfo()); + + if (player != null) { + ToroExo.setVolumeInfo(player, this.playbackInfo.getVolumeInfo()); + boolean haveResumePosition = this.playbackInfo.getResumeWindow() != INDEX_UNSET; + if (haveResumePosition) { + player.getPlayer().seekTo(this.playbackInfo.getResumeWindow(), this.playbackInfo.getResumePosition()); + } + } + } - PlayableImpl(ExoCreator creator, Uri uri, String fileExt) { - this.creator = creator; - this.mediaUri = uri; - this.fileExt = fileExt; - } + @Override + public final void addEventListener(@NonNull EventListener listener) { + //noinspection ConstantConditions + if (listener != null) this.listeners.add(listener); + } - @CallSuper @Override public void prepare(boolean prepareSource) { - if (prepareSource) { - ensureMediaSource(); - ensurePlayerView(); + @Override + public final void removeEventListener(EventListener listener) { + this.listeners.remove(listener); } - } - - @CallSuper @Override public void setPlayerView(@Nullable PlayerView playerView) { - if (this.playerView == playerView) return; - if (playerView == null) { - this.playerView.setPlayer(null); - } else { - if (this.player != null) { - PlayerView.switchTargetView(this.player, this.playerView, playerView); - } + + @CallSuper + @Override + public void setVolume(float volume) { + checkNotNull(player, "Playable#setVolume(): Player is null!"); + playbackInfo.getVolumeInfo().setTo(volume == 0, volume); + ToroExo.setVolumeInfo(player, this.playbackInfo.getVolumeInfo()); } - this.playerView = playerView; - } - - @Override public final PlayerView getPlayerView() { - return this.playerView; - } - - @CallSuper @Override public void play() { - ensureMediaSource(); - ensurePlayerView(); - checkNotNull(player, "Playable#play(): Player is null!"); - player.setPlayWhenReady(true); - } - - @CallSuper @Override public void pause() { - // Player is not required to be non-null here. - if (player != null) player.setPlayWhenReady(false); - } - - @CallSuper @Override public void reset() { - this.playbackInfo.reset(); - if (player != null) { - // reset volume to default - ToroExo.setVolumeInfo(this.player, new VolumeInfo(false, 1.f)); - player.stop(true); + @CallSuper + @Override + public float getVolume() { + return checkNotNull(player.getPlayer(), "Playable#getVolume(): Player is null!").getVolume(); } - this.mediaSource = null; // so it will be re-prepared when play() is called. - this.sourcePrepared = false; - } - - @CallSuper @Override public void release() { - this.setPlayerView(null); - if (this.player != null) { - // reset volume to default - ToroExo.setVolumeInfo(this.player, new VolumeInfo(false, 1.f)); - this.player.stop(true); - if (listenerApplied) { - player.removeListener(listeners); - player.removeVideoListener(listeners); - player.removeTextOutput(listeners); - player.removeMetadataOutput(listeners); - if (this.player instanceof ToroExoPlayer) { - ((ToroExoPlayer) this.player).removeOnVolumeChangeListener(this.volumeChangeListeners); + + @Override + public boolean setVolumeInfo(@NonNull VolumeInfo volumeInfo) { + boolean changed = !this.playbackInfo.getVolumeInfo().equals(checkNotNull(volumeInfo)); + if (changed) { + this.playbackInfo.getVolumeInfo().setTo(volumeInfo.isMute(), volumeInfo.getVolume()); + if (player != null) ToroExo.setVolumeInfo(player, this.playbackInfo.getVolumeInfo()); } - listenerApplied = false; - } - with(checkNotNull(creator.getContext(), "ExoCreator has no Context")) // - .releasePlayer(this.creator, this.player); + return changed; + } + + @NonNull + @Override + public VolumeInfo getVolumeInfo() { + return this.playbackInfo.getVolumeInfo(); + } + + @Override + public void setParameters(@Nullable PlaybackParameters parameters) { + checkNotNull(player.getPlayer(), "Playable#setParameters(PlaybackParameters): Player is null") // + .setPlaybackParameters(parameters); } - this.player = null; - this.mediaSource = null; - this.sourcePrepared = false; - } - - @CallSuper @NonNull @Override public PlaybackInfo getPlaybackInfo() { - updatePlaybackInfo(); - return new PlaybackInfo(playbackInfo.getResumeWindow(), playbackInfo.getResumePosition(), - playbackInfo.getVolumeInfo()); - } - - @CallSuper @Override public void setPlaybackInfo(@NonNull PlaybackInfo playbackInfo) { - this.playbackInfo.setResumeWindow(playbackInfo.getResumeWindow()); - this.playbackInfo.setResumePosition(playbackInfo.getResumePosition()); - this.setVolumeInfo(playbackInfo.getVolumeInfo()); - - if (player != null) { - ToroExo.setVolumeInfo(player, this.playbackInfo.getVolumeInfo()); - boolean haveResumePosition = this.playbackInfo.getResumeWindow() != INDEX_UNSET; - if (haveResumePosition) { - player.seekTo(this.playbackInfo.getResumeWindow(), this.playbackInfo.getResumePosition()); - } + + @Override + public PlaybackParameters getParameters() { + return checkNotNull(player.getPlayer(), "Playable#getParameters(): Player is null").getPlaybackParameters(); } - } - - @Override public final void addEventListener(@NonNull EventListener listener) { - //noinspection ConstantConditions - if (listener != null) this.listeners.add(listener); - } - - @Override public final void removeEventListener(EventListener listener) { - this.listeners.remove(listener); - } - - @CallSuper @Override public void setVolume(float volume) { - checkNotNull(player, "Playable#setVolume(): Player is null!"); - playbackInfo.getVolumeInfo().setTo(volume == 0, volume); - ToroExo.setVolumeInfo(player, this.playbackInfo.getVolumeInfo()); - } - - @CallSuper @Override public float getVolume() { - return checkNotNull(player, "Playable#getVolume(): Player is null!").getVolume(); - } - - @Override public boolean setVolumeInfo(@NonNull VolumeInfo volumeInfo) { - boolean changed = !this.playbackInfo.getVolumeInfo().equals(checkNotNull(volumeInfo)); - if (changed) { - this.playbackInfo.getVolumeInfo().setTo(volumeInfo.isMute(), volumeInfo.getVolume()); - if (player != null) ToroExo.setVolumeInfo(player, this.playbackInfo.getVolumeInfo()); + + @Override + public void addOnVolumeChangeListener(@NonNull ToroPlayer.OnVolumeChangeListener listener) { + volumeChangeListeners.add(checkNotNull(listener)); } - return changed; - } - - @NonNull @Override public VolumeInfo getVolumeInfo() { - return this.playbackInfo.getVolumeInfo(); - } - - @Override public void setParameters(@Nullable PlaybackParameters parameters) { - checkNotNull(player, "Playable#setParameters(PlaybackParameters): Player is null") // - .setPlaybackParameters(parameters); - } - - @Override public PlaybackParameters getParameters() { - return checkNotNull(player, "Playable#getParameters(): Player is null").getPlaybackParameters(); - } - - @Override - public void addOnVolumeChangeListener(@NonNull ToroPlayer.OnVolumeChangeListener listener) { - volumeChangeListeners.add(checkNotNull(listener)); - } - - @Override - public void removeOnVolumeChangeListener(@Nullable ToroPlayer.OnVolumeChangeListener listener) { - volumeChangeListeners.remove(listener); - } - - @Override public boolean isPlaying() { - return player != null && player.getPlayWhenReady(); - } - - @Override public void addErrorListener(@NonNull ToroPlayer.OnErrorListener listener) { - this.errorListeners.add(checkNotNull(listener)); - } - - @Override public void removeErrorListener(@Nullable ToroPlayer.OnErrorListener listener) { - this.errorListeners.remove(listener); - } - - final void updatePlaybackInfo() { - if (player == null || player.getPlaybackState() == Player.STATE_IDLE) return; - playbackInfo.setResumeWindow(player.getCurrentWindowIndex()); - playbackInfo.setResumePosition(player.isCurrentWindowSeekable() ? // - Math.max(0, player.getCurrentPosition()) : TIME_UNSET); - playbackInfo.setVolumeInfo(ToroExo.getVolumeInfo(player)); - } - - private void ensurePlayerView() { - if (playerView != null && playerView.getPlayer() != player) playerView.setPlayer(player); - } - - // TODO [20180822] Double check this. - private void ensureMediaSource() { - if (mediaSource == null) { // Only actually prepare the source when play() is called. - sourcePrepared = false; - mediaSource = creator.createMediaSource(mediaUri, fileExt); + + @Override + public void removeOnVolumeChangeListener(@Nullable ToroPlayer.OnVolumeChangeListener listener) { + volumeChangeListeners.remove(listener); } - if (!sourcePrepared) { - ensurePlayer(); // sourcePrepared is set to false only when player is null. - beforePrepareMediaSource(); - player.prepare(mediaSource, playbackInfo.getResumeWindow() == C.INDEX_UNSET, false); - sourcePrepared = true; + @Override + public boolean isPlaying() { + return player != null && player.getPlayer().getPlayWhenReady(); } - } - - private void ensurePlayer() { - if (player == null) { - sourcePrepared = false; - player = with(checkNotNull(creator.getContext(), "ExoCreator has no Context")) // - .requestPlayer(creator); - listenerApplied = false; + + @Override + public void addErrorListener(@NonNull ToroPlayer.OnErrorListener listener) { + this.errorListeners.add(checkNotNull(listener)); + } + + @Override + public void removeErrorListener(@Nullable ToroPlayer.OnErrorListener listener) { + this.errorListeners.remove(listener); } - if (!listenerApplied) { - if (player instanceof ToroExoPlayer) { - ((ToroExoPlayer) player).addOnVolumeChangeListener(volumeChangeListeners); - } - player.addListener(listeners); - player.addVideoListener(listeners); - player.addTextOutput(listeners); - player.addMetadataOutput(listeners); - listenerApplied = true; + final void updatePlaybackInfo() { + if (player == null || player.getPlayer().getPlaybackState() == Player.STATE_IDLE) return; + playbackInfo.setResumeWindow(player.getPlayer().getCurrentWindowIndex()); + playbackInfo.setResumePosition(player.getPlayer().isCurrentWindowSeekable() ? // + Math.max(0, player.getPlayer().getCurrentPosition()) : TIME_UNSET); + playbackInfo.setVolumeInfo(ToroExo.getVolumeInfo(player)); } - ToroExo.setVolumeInfo(player, this.playbackInfo.getVolumeInfo()); - boolean haveResumePosition = playbackInfo.getResumeWindow() != C.INDEX_UNSET; - if (haveResumePosition) { - player.seekTo(playbackInfo.getResumeWindow(), playbackInfo.getResumePosition()); + private void ensurePlayerView() { + if (playerView != null && playerView.getPlayer() != player.getPlayer()) playerView.setPlayer(player.getPlayer()); } - } - // Trick to inject to the Player creation event. - // Required for AdsLoader to set Player. - protected void beforePrepareMediaSource() { - } + // TODO [20180822] Double check this. + private void ensureMediaSource() { + if (mediaSource == null) { // Only actually prepare the source when play() is called. + sourcePrepared = false; + mediaSource = creator.createMediaSource(mediaUri, fileExt); + } + + if (!sourcePrepared) { + ensurePlayer(); // sourcePrepared is set to false only when player is null. + beforePrepareMediaSource(); + player.getPlayer().prepare(mediaSource, playbackInfo.getResumeWindow() == C.INDEX_UNSET, false); + sourcePrepared = true; + } + } + + private void ensurePlayer() { + if (player == null) { + sourcePrepared = false; + player = with(checkNotNull(creator.getContext(), "ExoCreator has no Context")) // + .requestPlayer(creator); + listenerApplied = false; + } + + if (!listenerApplied) { + player.addOnVolumeChangeListener(volumeChangeListeners); + player.getPlayer().addListener(listeners); + listenerApplied = true; + } + + ToroExo.setVolumeInfo(player, this.playbackInfo.getVolumeInfo()); + boolean haveResumePosition = playbackInfo.getResumeWindow() != C.INDEX_UNSET; + if (haveResumePosition) { + player.getPlayer().seekTo(playbackInfo.getResumeWindow(), playbackInfo.getResumePosition()); + } + } + + // Trick to inject to the Player creation event. + // Required for AdsLoader to set Player. + protected void beforePrepareMediaSource() { + } } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroExo.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroExo.java index 147157b6..5d49f0b8 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroExo.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroExo.java @@ -16,34 +16,21 @@ package ml.docilealligator.infinityforreddit.videoautoplay; -import static android.widget.Toast.LENGTH_SHORT; -import static com.google.android.exoplayer2.drm.UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME; -import static com.google.android.exoplayer2.util.Util.getDrmUuid; import static java.lang.Runtime.getRuntime; import static ml.docilealligator.infinityforreddit.videoautoplay.ToroUtil.checkNotNull; import android.annotation.SuppressLint; import android.app.Application; import android.content.Context; -import android.text.TextUtils; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.annotation.StringRes; import androidx.core.util.Pools; +import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; -import com.google.android.exoplayer2.drm.FrameworkMediaDrm; -import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; -import com.google.android.exoplayer2.drm.UnsupportedDrmException; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.Util; import java.net.CookieHandler; @@ -52,18 +39,15 @@ import java.net.CookiePolicy; import java.util.HashMap; import java.util.Iterator; import java.util.Map; -import java.util.UUID; -import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.utils.APIUtils; -import ml.docilealligator.infinityforreddit.videoautoplay.media.DrmMedia; import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo; /** * Global helper class to manage {@link ExoCreator} and {@link SimpleExoPlayer} instances. * In this setup, {@link ExoCreator} and SimpleExoPlayer pools are cached. A {@link Config} * is a key for each {@link ExoCreator}. - * + * <p> * A suggested usage is as below: * <pre><code> * ExoCreator creator = ToroExo.with(this).getDefaultCreator(); @@ -78,220 +62,152 @@ import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo; public final class ToroExo { - private static final String TAG = "ToroExo"; + private static final String TAG = "ToroExo"; - // Magic number: Build.VERSION.SDK_INT / 6 --> API 16 ~ 18 will set pool size to 2, etc. - @SuppressWarnings("WeakerAccess") // - static final int MAX_POOL_SIZE = Math.max(Util.SDK_INT / 6, getRuntime().availableProcessors()); - @SuppressLint("StaticFieldLeak") // - static volatile ToroExo toro; + // Magic number: Build.VERSION.SDK_INT / 6 --> API 16 ~ 18 will set pool size to 2, etc. + @SuppressWarnings("WeakerAccess") // + static final int MAX_POOL_SIZE = Math.max(Util.SDK_INT / 6, getRuntime().availableProcessors()); + @SuppressLint("StaticFieldLeak") // + static volatile ToroExo toro; - public static ToroExo with(Context context) { - if (toro == null) { - synchronized (ToroExo.class) { - if (toro == null) toro = new ToroExo(context.getApplicationContext()); - } + public static ToroExo with(Context context) { + if (toro == null) { + synchronized (ToroExo.class) { + if (toro == null) toro = new ToroExo(context.getApplicationContext()); + } + } + return toro; } - return toro; - } - - @NonNull final String appName; - @NonNull final Context context; // Application context - @NonNull private final Map<Config, ExoCreator> creators; - @NonNull private final Map<ExoCreator, Pools.Pool<SimpleExoPlayer>> playerPools; - private Config defaultConfig; // will be created on the first time it is used. + @NonNull + final String appName; + @NonNull + final Context context; // Application context + @NonNull + private final Map<Config, ExoCreator> creators; + @NonNull + private final Map<ExoCreator, Pools.Pool<ExoPlayer>> playerPools; + + private Config defaultConfig; // will be created on the first time it is used. + + private ToroExo(@NonNull Context context /* Application context */) { + this.context = context; + this.appName = getUserAgent(); + this.playerPools = new HashMap<>(); + this.creators = new HashMap<>(); + + // Adapt from ExoPlayer demo app. Start this on demand. + CookieManager cookieManager = new CookieManager(); + cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); + if (CookieHandler.getDefault() != cookieManager) { + CookieHandler.setDefault(cookieManager); + } + } - private ToroExo(@NonNull Context context /* Application context */) { - this.context = context; - this.appName = getUserAgent(); - this.playerPools = new HashMap<>(); - this.creators = new HashMap<>(); + /** + * Utility method to produce {@link ExoCreator} instance from a {@link Config}. + */ + public final ExoCreator getCreator(Config config) { + ExoCreator creator = this.creators.get(config); + if (creator == null) { + creator = new DefaultExoCreator(this, config); + this.creators.put(config, creator); + } - // Adapt from ExoPlayer demo app. Start this on demand. - CookieManager cookieManager = new CookieManager(); - cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); - if (CookieHandler.getDefault() != cookieManager) { - CookieHandler.setDefault(cookieManager); + return creator; } - } - /** - * Utility method to produce {@link ExoCreator} instance from a {@link Config}. - */ - public final ExoCreator getCreator(Config config) { - ExoCreator creator = this.creators.get(config); - if (creator == null) { - creator = new DefaultExoCreator(this, config); - this.creators.put(config, creator); + @SuppressWarnings("WeakerAccess") + public final Config getDefaultConfig() { + if (defaultConfig == null) defaultConfig = new Config.Builder(context).build(); + return defaultConfig; } - return creator; - } - - @SuppressWarnings("WeakerAccess") public final Config getDefaultConfig() { - if (defaultConfig == null) defaultConfig = new Config.Builder(context).build(); - return defaultConfig; - } - - /** - * Get the default {@link ExoCreator}. This ExoCreator is configured by {@link #defaultConfig}. - */ - public final ExoCreator getDefaultCreator() { - return getCreator(getDefaultConfig()); - } - - /** - * Request an instance of {@link SimpleExoPlayer}. It can be an existing instance cached by Pool - * or new one. - * - * The creator may or may not be the one created by either {@link #getCreator(Config)} or - * {@link #getDefaultCreator()}. - * - * @param creator the {@link ExoCreator} that is scoped to the {@link SimpleExoPlayer} config. - * @return an usable {@link SimpleExoPlayer} instance. - */ - @NonNull // - public final SimpleExoPlayer requestPlayer(@NonNull ExoCreator creator) { - SimpleExoPlayer player = getPool(checkNotNull(creator)).acquire(); - if (player == null) player = creator.createPlayer(); - return player; - } - - /** - * Release player to Pool attached to the creator. - * - * @param creator the {@link ExoCreator} that created the player. - * @param player the {@link SimpleExoPlayer} to be released back to the Pool - * @return true if player is released to relevant Pool, false otherwise. - */ - @SuppressWarnings({ "WeakerAccess", "UnusedReturnValue" }) // - public final boolean releasePlayer(@NonNull ExoCreator creator, @NonNull SimpleExoPlayer player) { - return getPool(checkNotNull(creator)).release(player); - } - - /** - * Release and clear all current cached ExoPlayer instances. This should be called when - * client Application runs out of memory ({@link Application#onTrimMemory(int)} for example). - */ - public final void cleanUp() { - // TODO [2018/03/07] Test this. Ref: https://stackoverflow.com/a/1884916/1553254 - for (Iterator<Map.Entry<ExoCreator, Pools.Pool<SimpleExoPlayer>>> it = - playerPools.entrySet().iterator(); it.hasNext(); ) { - Pools.Pool<SimpleExoPlayer> pool = it.next().getValue(); - SimpleExoPlayer item; - while ((item = pool.acquire()) != null) item.release(); - it.remove(); + /** + * Get the default {@link ExoCreator}. This ExoCreator is configured by {@link #defaultConfig}. + */ + public final ExoCreator getDefaultCreator() { + return getCreator(getDefaultConfig()); } - } - /// internal APIs - private Pools.Pool<SimpleExoPlayer> getPool(ExoCreator creator) { - Pools.Pool<SimpleExoPlayer> pool = playerPools.get(creator); - if (pool == null) { - pool = new Pools.SimplePool<>(MAX_POOL_SIZE); - playerPools.put(creator, pool); + /** + * Request an instance of {@link SimpleExoPlayer}. It can be an existing instance cached by Pool + * or new one. + * <p> + * The creator may or may not be the one created by either {@link #getCreator(Config)} or + * {@link #getDefaultCreator()}. + * + * @param creator the {@link ExoCreator} that is scoped to the {@link SimpleExoPlayer} config. + * @return an usable {@link SimpleExoPlayer} instance. + */ + @NonNull // + public final ToroExoPlayer requestPlayer(@NonNull ExoCreator creator) { + ExoPlayer player = getPool(checkNotNull(creator)).acquire(); + if (player == null) player = creator.createPlayer(); + return new ToroExoPlayer(player); } - return pool; - } - - /** - * Get a possibly-non-localized String from existing resourceId. - */ - /* pkg */ String getString(@StringRes int resId, @Nullable Object... params) { - return params == null || params.length < 1 ? // - this.context.getString(resId) : this.context.getString(resId, params); - } + /** + * Release player to Pool attached to the creator. + * + * @param creator the {@link ExoCreator} that created the player. + * @param player the {@link SimpleExoPlayer} to be released back to the Pool + * @return true if player is released to relevant Pool, false otherwise. + */ + @SuppressWarnings({"WeakerAccess", "UnusedReturnValue"}) // + public final boolean releasePlayer(@NonNull ExoCreator creator, @NonNull ExoPlayer player) { + return getPool(checkNotNull(creator)).release(player); + } - /** - * Utility method to build a {@link DrmSessionManager} that can be used in {@link Config} - * - * Usage: - * <pre><code> - * DrmSessionManager manager = ToroExo.with(context).createDrmSessionManager(mediaDrm); - * Config config = new Config.Builder().setDrmSessionManager(manager); - * ExoCreator creator = ToroExo.with(context).getCreator(config); - * </code></pre> - */ - @SuppressWarnings("unused") @RequiresApi(18) @Nullable // - public DrmSessionManager<FrameworkMediaCrypto> createDrmSessionManager(@NonNull DrmMedia drm) { - DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null; - int errorStringId = R.string.error_drm_unknown; - String subString = null; - if (Util.SDK_INT < 18) { - errorStringId = R.string.error_drm_not_supported; - } else { - UUID drmSchemeUuid = getDrmUuid(checkNotNull(drm).getType()); - if (drmSchemeUuid == null) { - errorStringId = R.string.error_drm_unsupported_scheme; - } else { - HttpDataSource.Factory factory = new DefaultHttpDataSourceFactory(appName); - try { - drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drm.getLicenseUrl(), - drm.getKeyRequestPropertiesArray(), drm.multiSession(), factory); - } catch (UnsupportedDrmException e) { - e.printStackTrace(); - errorStringId = e.reason == REASON_UNSUPPORTED_SCHEME ? // - R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown; - if (e.reason == REASON_UNSUPPORTED_SCHEME) { - subString = drm.getType(); - } + /** + * Release and clear all current cached ExoPlayer instances. This should be called when + * client Application runs out of memory ({@link Application#onTrimMemory(int)} for example). + */ + public final void cleanUp() { + // TODO [2018/03/07] Test this. Ref: https://stackoverflow.com/a/1884916/1553254 + for (Iterator<Map.Entry<ExoCreator, Pools.Pool<ExoPlayer>>> it = + playerPools.entrySet().iterator(); it.hasNext(); ) { + Pools.Pool<ExoPlayer> pool = it.next().getValue(); + ExoPlayer item; + while ((item = pool.acquire()) != null) item.release(); + it.remove(); } - } } - if (drmSessionManager == null) { - String error = TextUtils.isEmpty(subString) ? context.getString(errorStringId) - : context.getString(errorStringId) + ": " + subString; - Toast.makeText(context, error, LENGTH_SHORT).show(); - } + /// internal APIs + private Pools.Pool<ExoPlayer> getPool(ExoCreator creator) { + Pools.Pool<ExoPlayer> pool = playerPools.get(creator); + if (pool == null) { + pool = new Pools.SimplePool<>(MAX_POOL_SIZE); + playerPools.put(creator, pool); + } - return drmSessionManager; - } + return pool; + } - @RequiresApi(18) private static DrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18( - @NonNull UUID uuid, @Nullable String licenseUrl, @Nullable String[] keyRequestPropertiesArray, - boolean multiSession, @NonNull HttpDataSource.Factory httpDataSourceFactory) - throws UnsupportedDrmException { - HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory); - if (keyRequestPropertiesArray != null) { - for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { - drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], - keyRequestPropertiesArray[i + 1]); - } + /** + * Get a possibly-non-localized String from existing resourceId. + */ + /* pkg */ String getString(@StringRes int resId, @Nullable Object... params) { + return params == null || params.length < 1 ? // + this.context.getString(resId) : this.context.getString(resId, params); } - return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, - null, multiSession); - } - // Share the code of setting Volume. For use inside library only. - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // - public static void setVolumeInfo(@NonNull SimpleExoPlayer player, - @NonNull VolumeInfo volumeInfo) { - if (player instanceof ToroExoPlayer) { - ((ToroExoPlayer) player).setVolumeInfo(volumeInfo); - } else { - if (volumeInfo.isMute()) { - player.setVolume(0f); - } else { - player.setVolume(volumeInfo.getVolume()); - } + // Share the code of setting Volume. For use inside library only. + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // + public static void setVolumeInfo(@NonNull ToroExoPlayer player, + @NonNull VolumeInfo volumeInfo) { + player.setVolumeInfo(volumeInfo); } - } - @SuppressWarnings("WeakerAccess") @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // - public static VolumeInfo getVolumeInfo(SimpleExoPlayer player) { - if (player instanceof ToroExoPlayer) { - return new VolumeInfo(((ToroExoPlayer) player).getVolumeInfo()); - } else { - float volume = player.getVolume(); - return new VolumeInfo(volume == 0, volume); + @SuppressWarnings("WeakerAccess") + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // + public static VolumeInfo getVolumeInfo(ToroExoPlayer player) { + return new VolumeInfo(player.getVolumeInfo()); } - } - @SuppressWarnings("SameParameterValue") - private static String getUserAgent() { - return APIUtils.USER_AGENT; - } + @SuppressWarnings("SameParameterValue") + private static String getUserAgent() { + return APIUtils.USER_AGENT; + } }
\ No newline at end of file diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroExoPlayer.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroExoPlayer.java index 2fe92569..ea8bf638 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroExoPlayer.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroExoPlayer.java @@ -23,13 +23,11 @@ import android.os.Looper; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; +import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.BandwidthMeter; @@ -41,53 +39,65 @@ import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo; * @author eneim (2018/03/27). */ @SuppressWarnings("WeakerAccess") // -public class ToroExoPlayer extends SimpleExoPlayer { - - protected ToroExoPlayer(Context context, RenderersFactory renderersFactory, - TrackSelector trackSelector, LoadControl loadControl, BandwidthMeter bandwidthMeter, - @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, Looper looper) { - super(context, renderersFactory, trackSelector, loadControl, bandwidthMeter, drmSessionManager, - looper); - } - - private ToroPlayer.VolumeChangeListeners listeners; - - public final void addOnVolumeChangeListener(@NonNull ToroPlayer.OnVolumeChangeListener listener) { - if (this.listeners == null) this.listeners = new ToroPlayer.VolumeChangeListeners(); - this.listeners.add(checkNotNull(listener)); - } - - public final void removeOnVolumeChangeListener(ToroPlayer.OnVolumeChangeListener listener) { - if (this.listeners != null) this.listeners.remove(listener); - } - - public final void clearOnVolumeChangeListener() { - if (this.listeners != null) this.listeners.clear(); - } - - @CallSuper @Override public void setVolume(float audioVolume) { - this.setVolumeInfo(new VolumeInfo(audioVolume == 0, audioVolume)); - } - - private final VolumeInfo volumeInfo = new VolumeInfo(false, 1f); - - @SuppressWarnings("UnusedReturnValue") - public final boolean setVolumeInfo(@NonNull VolumeInfo volumeInfo) { - boolean changed = !this.volumeInfo.equals(volumeInfo); - if (changed) { - this.volumeInfo.setTo(volumeInfo.isMute(), volumeInfo.getVolume()); - super.setVolume(volumeInfo.isMute() ? 0 : volumeInfo.getVolume()); - if (listeners != null) { - for (ToroPlayer.OnVolumeChangeListener listener : this.listeners) { - listener.onVolumeChanged(volumeInfo); +public class ToroExoPlayer { + + private ExoPlayer player; + + public ToroExoPlayer(Context context, RenderersFactory renderersFactory, + TrackSelector trackSelector, LoadControl loadControl, BandwidthMeter bandwidthMeter, + Looper looper) { + player = new ExoPlayer.Builder(context).setRenderersFactory(renderersFactory).setTrackSelector(trackSelector).setLoadControl(loadControl).setBandwidthMeter(bandwidthMeter).setLooper(looper).build(); + } + + public ToroExoPlayer(ExoPlayer exoPlayer) { + this.player = exoPlayer; + } + + private ToroPlayer.VolumeChangeListeners listeners; + + public final void addOnVolumeChangeListener(@NonNull ToroPlayer.OnVolumeChangeListener listener) { + if (this.listeners == null) this.listeners = new ToroPlayer.VolumeChangeListeners(); + this.listeners.add(checkNotNull(listener)); + } + + public final void removeOnVolumeChangeListener(ToroPlayer.OnVolumeChangeListener listener) { + if (this.listeners != null) this.listeners.remove(listener); + } + + public final void clearOnVolumeChangeListener() { + if (this.listeners != null) this.listeners.clear(); + } + + @CallSuper + public void setVolume(float audioVolume) { + this.setVolumeInfo(new VolumeInfo(audioVolume == 0, audioVolume)); + } + + private final VolumeInfo volumeInfo = new VolumeInfo(false, 1f); + + @SuppressWarnings("UnusedReturnValue") + public final boolean setVolumeInfo(@NonNull VolumeInfo volumeInfo) { + boolean changed = !this.volumeInfo.equals(volumeInfo); + if (changed) { + this.volumeInfo.setTo(volumeInfo.isMute(), volumeInfo.getVolume()); + player.setVolume(volumeInfo.isMute() ? 0 : volumeInfo.getVolume()); + if (listeners != null) { + for (ToroPlayer.OnVolumeChangeListener listener : this.listeners) { + listener.onVolumeChanged(volumeInfo); + } + } } - } + + return changed; } - return changed; - } + @SuppressWarnings("unused") + @NonNull + public final VolumeInfo getVolumeInfo() { + return volumeInfo; + } - @SuppressWarnings("unused") @NonNull public final VolumeInfo getVolumeInfo() { - return volumeInfo; - } + public ExoPlayer getPlayer() { + return player; + } } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroUtil.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroUtil.java index ecfc7d1d..563d5094 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroUtil.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ToroUtil.java @@ -34,82 +34,85 @@ import ml.docilealligator.infinityforreddit.videoautoplay.widget.Container; public final class ToroUtil { - @SuppressWarnings("unused") private static final String TAG = "ToroLib:Util"; + @SuppressWarnings("unused") + private static final String TAG = "ToroLib:Util"; - private ToroUtil() { - throw new RuntimeException("Meh!"); - } + private ToroUtil() { + throw new RuntimeException("Meh!"); + } - /** - * Get the ratio in range of 0.0 ~ 1.0 the visible area of a {@link ToroPlayer}'s playerView. - * - * @param player the {@link ToroPlayer} need to investigate. - * @param container the {@link ViewParent} that holds the {@link ToroPlayer}. If {@code null} - * then this method must returns 0.0f; - * @return the value in range of 0.0 ~ 1.0 of the visible area. - */ - @FloatRange(from = 0.0, to = 1.0) // - public static float visibleAreaOffset(@NonNull ToroPlayer player, ViewParent container) { - if (container == null) return 0.0f; + /** + * Get the ratio in range of 0.0 ~ 1.0 the visible area of a {@link ToroPlayer}'s playerView. + * + * @param player the {@link ToroPlayer} need to investigate. + * @param container the {@link ViewParent} that holds the {@link ToroPlayer}. If {@code null} + * then this method must returns 0.0f; + * @return the value in range of 0.0 ~ 1.0 of the visible area. + */ + @FloatRange(from = 0.0, to = 1.0) // + public static float visibleAreaOffset(@NonNull ToroPlayer player, ViewParent container) { + if (container == null) return 0.0f; - View playerView = player.getPlayerView(); - Rect drawRect = new Rect(); - playerView.getDrawingRect(drawRect); - int drawArea = drawRect.width() * drawRect.height(); + View playerView = player.getPlayerView(); + Rect drawRect = new Rect(); + playerView.getDrawingRect(drawRect); + int drawArea = drawRect.width() * drawRect.height(); - Rect playerRect = new Rect(); - boolean visible = playerView.getGlobalVisibleRect(playerRect, new Point()); + Rect playerRect = new Rect(); + boolean visible = playerView.getGlobalVisibleRect(playerRect, new Point()); - float offset = 0.f; - if (visible && drawArea > 0) { - int visibleArea = playerRect.height() * playerRect.width(); - offset = visibleArea / (float) drawArea; + float offset = 0.f; + if (visible && drawArea > 0) { + int visibleArea = playerRect.height() * playerRect.width(); + offset = visibleArea / (float) drawArea; + } + return offset; } - return offset; - } - /** - * Ensures that an object reference passed as a parameter to the calling - * method is not null. - * - * @param reference an object reference - * @return the non-null reference that was validated - * @throws NullPointerException if {@code reference} is null - */ - public static @NonNull <T> T checkNotNull(final T reference) { - if (reference == null) { - throw new NullPointerException(); + /** + * Ensures that an object reference passed as a parameter to the calling + * method is not null. + * + * @param reference an object reference + * @return the non-null reference that was validated + * @throws NullPointerException if {@code reference} is null + */ + public static @NonNull + <T> T checkNotNull(final T reference) { + if (reference == null) { + throw new NullPointerException(); + } + return reference; } - return reference; - } - /** - * Ensures that an object reference passed as a parameter to the calling - * method is not null. - * - * @param reference an object reference - * @param errorMessage the exception message to use if the check fails; will - * be converted to a string using {@link String#valueOf(Object)} - * @return the non-null reference that was validated - * @throws NullPointerException if {@code reference} is null - */ - public static @NonNull <T> T checkNotNull(final T reference, final Object errorMessage) { - if (reference == null) { - throw new NullPointerException(String.valueOf(errorMessage)); + /** + * Ensures that an object reference passed as a parameter to the calling + * method is not null. + * + * @param reference an object reference + * @param errorMessage the exception message to use if the check fails; will + * be converted to a string using {@link String#valueOf(Object)} + * @return the non-null reference that was validated + * @throws NullPointerException if {@code reference} is null + */ + public static @NonNull + <T> T checkNotNull(final T reference, final Object errorMessage) { + if (reference == null) { + throw new NullPointerException(String.valueOf(errorMessage)); + } + return reference; } - return reference; - } - @SuppressWarnings("unchecked") // - public static void wrapParamBehavior(@NonNull final Container container, - final Container.BehaviorCallback callback) { - container.setBehaviorCallback(callback); - ViewGroup.LayoutParams params = container.getLayoutParams(); - if (params instanceof CoordinatorLayout.LayoutParams) { - CoordinatorLayout.Behavior temp = ((CoordinatorLayout.LayoutParams) params).getBehavior(); - if (temp != null) { - ((CoordinatorLayout.LayoutParams) params).setBehavior(new Container.Behavior(temp)); - } + @SuppressWarnings("unchecked") // + public static void wrapParamBehavior(@NonNull final Container container, + final Container.BehaviorCallback callback) { + container.setBehaviorCallback(callback); + ViewGroup.LayoutParams params = container.getLayoutParams(); + if (params instanceof CoordinatorLayout.LayoutParams) { + CoordinatorLayout.Behavior temp = ((CoordinatorLayout.LayoutParams) params).getBehavior(); + if (temp != null) { + ((CoordinatorLayout.LayoutParams) params).setBehavior(new Container.Behavior(temp)); + } + } } - } } diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ui/ToroControlView.java b/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ui/ToroControlView.java deleted file mode 100644 index 0ab474db..00000000 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/videoautoplay/ui/ToroControlView.java +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright (c) 2018 Nam Nguyen, nam@ene.im - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ml.docilealligator.infinityforreddit.videoautoplay.ui; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.core.view.ViewCompat; - -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.ui.PlayerControlView; -import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.exoplayer2.ui.TimeBar; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -import ml.docilealligator.infinityforreddit.R; -import ml.docilealligator.infinityforreddit.videoautoplay.ToroExo; -import ml.docilealligator.infinityforreddit.videoautoplay.ToroExoPlayer; -import ml.docilealligator.infinityforreddit.videoautoplay.ToroPlayer; -import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo; - -/** - * An extension of {@link PlayerControlView} that adds Volume control buttons. It works on-par - * with {@link PlayerView}. Will be automatically inflated when client uses {@link R.layout.toro_exo_player_view} - * for {@link PlayerView} layout. - * - * @author eneim (2018/08/20). - * @since 3.6.0.2802 - */ -public class ToroControlView extends PlayerControlView { - - @SuppressWarnings("unused") static final String TAG = "ToroExo:Control"; - - // Statically obtain from super class. - protected static Method hideAfterTimeoutMethod; // from parent ... - protected static boolean hideMethodFetched; - protected static Field hideActionField; - protected static boolean hideActionFetched; - - final ComponentListener componentListener; - final View volumeUpButton; - final View volumeOffButton; - final TimeBar volumeBar; - final VolumeInfo volumeInfo = new VolumeInfo(false, 1); - - public ToroControlView(Context context) { - this(context, null); - } - - public ToroControlView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public ToroControlView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - volumeOffButton = findViewById(R.id.exo_volume_off); - volumeUpButton = findViewById(R.id.exo_volume_up); - volumeBar = findViewById(R.id.volume_bar); - componentListener = new ComponentListener(); - } - - @Override public void onAttachedToWindow() { - super.onAttachedToWindow(); - if (volumeUpButton != null) volumeUpButton.setOnClickListener(componentListener); - if (volumeOffButton != null) volumeOffButton.setOnClickListener(componentListener); - if (volumeBar != null) volumeBar.addListener(componentListener); - - updateVolumeButtons(); - } - - @Override public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - if (volumeUpButton != null) volumeUpButton.setOnClickListener(null); - if (volumeOffButton != null) volumeOffButton.setOnClickListener(null); - if (volumeBar != null) volumeBar.removeListener(componentListener); - this.setPlayer(null); - } - - @SuppressLint("ClickableViewAccessibility") @Override - public boolean onTouchEvent(MotionEvent event) { - // After processing all children' touch event, this View will just stop it here. - // User can click to PlayerView to show/hide this view, but since this View's height is not - // significantly large, clicking to show/hide may disturb other actions like clicking to button, - // seeking the bars, etc. This extension will stop the touch event here so that PlayerView has - // nothing to do when User touch this View. - return true; - } - - @Override public void setPlayer(Player player) { - Player current = super.getPlayer(); - if (current == player) return; - - if (current instanceof ToroExoPlayer) { - ((ToroExoPlayer) current).removeOnVolumeChangeListener(componentListener); - } - - super.setPlayer(player); - current = super.getPlayer(); - @NonNull final VolumeInfo tempVol; - if (current instanceof ToroExoPlayer) { - tempVol = ((ToroExoPlayer) current).getVolumeInfo(); - ((ToroExoPlayer) current).addOnVolumeChangeListener(componentListener); - } else if (current instanceof SimpleExoPlayer) { - float volume = ((SimpleExoPlayer) current).getVolume(); - tempVol = new VolumeInfo(volume == 0, volume); - } else { - tempVol = new VolumeInfo(false, 1f); - } - - this.volumeInfo.setTo(tempVol.isMute(), tempVol.getVolume()); - updateVolumeButtons(); - } - - @Override protected void onVisibilityChanged(@NonNull View changedView, int visibility) { - super.onVisibilityChanged(changedView, visibility); - if (changedView == this) updateVolumeButtons(); - } - - @SuppressWarnings("ConstantConditions") // - void updateVolumeButtons() { - if (!isVisible() || !ViewCompat.isAttachedToWindow(this)) { - return; - } - boolean requestButtonFocus = false; - // if muted then show volumeOffButton, or else show volumeUpButton - boolean muted = volumeInfo.isMute(); - if (volumeOffButton != null) { - requestButtonFocus |= muted && volumeOffButton.isFocused(); - volumeOffButton.setVisibility(muted ? View.VISIBLE : View.GONE); - } - if (volumeUpButton != null) { - requestButtonFocus |= !muted && volumeUpButton.isFocused(); - volumeUpButton.setVisibility(!muted ? View.VISIBLE : View.GONE); - } - - if (volumeBar != null) { - volumeBar.setDuration(100); - volumeBar.setPosition(muted ? 0 : (long) (volumeInfo.getVolume() * 100)); - } - - if (requestButtonFocus) { - requestButtonFocus(); - } - - // A hack to access PlayerControlView's hideAfterTimeout. Don't want to re-implement it. - // Reflection happens once for all instances, so it should not affect the performance. - if (!hideMethodFetched) { - try { - hideAfterTimeoutMethod = PlayerControlView.class.getDeclaredMethod("hideAfterTimeout"); - hideAfterTimeoutMethod.setAccessible(true); - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } - hideMethodFetched = true; - } - - if (hideAfterTimeoutMethod != null) { - try { - hideAfterTimeoutMethod.invoke(this); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } - } - } - - private void requestButtonFocus() { - boolean muted = volumeInfo.isMute(); - if (!muted && volumeUpButton != null) { - volumeUpButton.requestFocus(); - } else if (muted && volumeOffButton != null) { - volumeOffButton.requestFocus(); - } - } - - void dispatchOnScrubStart() { - // Fetch the 'hideAction' Runnable from super class. We need this to synchronize the show/hide - // behaviour when user does something. - if (!hideActionFetched) { - try { - hideActionField = PlayerControlView.class.getDeclaredField("hideAction"); - hideActionField.setAccessible(true); - } catch (NoSuchFieldException e) { - e.printStackTrace(); - } - hideActionFetched = true; - } - - if (hideActionField != null) { - try { - removeCallbacks((Runnable) hideActionField.get(this)); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - } - } - - // Scrub Move will always modify actual Volume, there is no 'mute-with-non-zero-volume' state. - void dispatchOnScrubMove(long position) { - if (position > 100) position = 100; - if (position < 0) position = 0; - - float actualVolume = position / (float) 100; - this.volumeInfo.setTo(actualVolume == 0, actualVolume); - if (getPlayer() instanceof SimpleExoPlayer) { - ToroExo.setVolumeInfo((SimpleExoPlayer) getPlayer(), this.volumeInfo); - } - - updateVolumeButtons(); - } - - void dispatchOnScrubStop(long position) { - this.dispatchOnScrubMove(position); - } - - private class ComponentListener - implements OnClickListener, TimeBar.OnScrubListener, ToroPlayer.OnVolumeChangeListener { - - ComponentListener() { - } - - @Override public void onClick(View v) { - Player player = ToroControlView.super.getPlayer(); - if (!(player instanceof SimpleExoPlayer)) return; - if (v == volumeOffButton) { // click to vol Off --> unmute - volumeInfo.setTo(false, volumeInfo.getVolume()); - } else if (v == volumeUpButton) { // click to vol Up --> mute - volumeInfo.setTo(true, volumeInfo.getVolume()); - } - ToroExo.setVolumeInfo((SimpleExoPlayer) player, volumeInfo); - updateVolumeButtons(); - } - - /// TimeBar.OnScrubListener - - @Override public void onScrubStart(TimeBar timeBar, long position) { - dispatchOnScrubStart(); - } - - @Override public void onScrubMove(TimeBar timeBar, long position) { - dispatchOnScrubMove(position); - } - - @Override public void onScrubStop(TimeBar timeBar, long position, boolean canceled) { - dispatchOnScrubStop(position); - } - - /// ToroPlayer.OnVolumeChangeListener - - @Override public void onVolumeChanged(@NonNull VolumeInfo volumeInfo) { - ToroControlView.this.volumeInfo.setTo(volumeInfo.isMute(), volumeInfo.getVolume()); - updateVolumeButtons(); - } - } -} |