mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 15:23:00 +00:00 
			
		
		
		
	Merge branch 'exoplayer-update' of https://github.com/karyogamy/NewPipe into live
This commit is contained in:
		| @@ -55,7 +55,7 @@ dependencies { | |||||||
|         exclude module: 'support-annotations' |         exclude module: 'support-annotations' | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     implementation 'com.github.TeamNewPipe:NewPipeExtractor:86db415b181' |     implementation 'com.github.karyogamy:NewPipeExtractor:b4206479cb' | ||||||
|  |  | ||||||
|     testImplementation 'junit:junit:4.12' |     testImplementation 'junit:junit:4.12' | ||||||
|     testImplementation 'org.mockito:mockito-core:1.10.19' |     testImplementation 'org.mockito:mockito-core:1.10.19' | ||||||
| @@ -73,7 +73,7 @@ dependencies { | |||||||
|     implementation 'de.hdodenhof:circleimageview:2.2.0' |     implementation 'de.hdodenhof:circleimageview:2.2.0' | ||||||
|     implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1' |     implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1' | ||||||
|     implementation 'com.nononsenseapps:filepicker:3.0.1' |     implementation 'com.nononsenseapps:filepicker:3.0.1' | ||||||
|     implementation 'com.google.android.exoplayer:exoplayer:2.6.0' |     implementation 'com.google.android.exoplayer:exoplayer:2.7.0' | ||||||
|  |  | ||||||
|     debugImplementation 'com.facebook.stetho:stetho:1.5.0' |     debugImplementation 'com.facebook.stetho:stetho:1.5.0' | ||||||
|     debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0' |     debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0' | ||||||
|   | |||||||
| @@ -56,6 +56,7 @@ import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; | |||||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | import org.schabi.newpipe.extractor.stream.AudioStream; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||||
|  | import org.schabi.newpipe.extractor.stream.StreamType; | ||||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | import org.schabi.newpipe.extractor.stream.VideoStream; | ||||||
| import org.schabi.newpipe.fragments.BackPressable; | import org.schabi.newpipe.fragments.BackPressable; | ||||||
| import org.schabi.newpipe.fragments.BaseStateFragment; | import org.schabi.newpipe.fragments.BaseStateFragment; | ||||||
| @@ -321,7 +322,7 @@ public class VideoDetailFragment | |||||||
|         if (serializable instanceof StreamInfo) { |         if (serializable instanceof StreamInfo) { | ||||||
|             //noinspection unchecked |             //noinspection unchecked | ||||||
|             currentInfo = (StreamInfo) serializable; |             currentInfo = (StreamInfo) serializable; | ||||||
|             InfoCache.getInstance().putInfo(currentInfo); |             InfoCache.getInstance().putInfo(serviceId, url, currentInfo); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         serializable = savedState.getSerializable(STACK_KEY); |         serializable = savedState.getSerializable(STACK_KEY); | ||||||
| @@ -1192,11 +1193,20 @@ public class VideoDetailFragment | |||||||
|                     0); |                     0); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (info.video_streams.isEmpty() && info.video_only_streams.isEmpty()) { |         switch (info.getStreamType()) { | ||||||
|  |             case LIVE_STREAM: | ||||||
|  |             case AUDIO_LIVE_STREAM: | ||||||
|  |                 detailControlsDownload.setVisibility(View.GONE); | ||||||
|  |                 spinnerToolbar.setVisibility(View.GONE); | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 if (!info.video_streams.isEmpty() || !info.video_only_streams.isEmpty()) break; | ||||||
|  |  | ||||||
|                 detailControlsBackground.setVisibility(View.GONE); |                 detailControlsBackground.setVisibility(View.GONE); | ||||||
|                 detailControlsPopup.setVisibility(View.GONE); |                 detailControlsPopup.setVisibility(View.GONE); | ||||||
|                 spinnerToolbar.setVisibility(View.GONE); |                 spinnerToolbar.setVisibility(View.GONE); | ||||||
|                 thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp); |                 thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp); | ||||||
|  |                 break; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (autoPlayEnabled) { |         if (autoPlayEnabled) { | ||||||
| @@ -1216,8 +1226,6 @@ public class VideoDetailFragment | |||||||
|  |  | ||||||
|         if (exception instanceof YoutubeStreamExtractor.GemaException) { |         if (exception instanceof YoutubeStreamExtractor.GemaException) { | ||||||
|             onBlockedByGemaError(); |             onBlockedByGemaError(); | ||||||
|         } else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) { |  | ||||||
|             showError(getString(R.string.live_streams_not_supported), false); |  | ||||||
|         } else if (exception instanceof ContentNotAvailableException) { |         } else if (exception instanceof ContentNotAvailableException) { | ||||||
|             showError(getString(R.string.content_not_available), false); |             showError(getString(R.string.content_not_available), false); | ||||||
|         } else { |         } else { | ||||||
|   | |||||||
| @@ -527,23 +527,26 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void showDeleteSuggestionDialog(final SuggestionItem item) { |     private void showDeleteSuggestionDialog(final SuggestionItem item) { | ||||||
|         final Disposable onDelete = historyRecordManager.deleteSearchHistory(item.query) |         if (activity == null || historyRecordManager == null || suggestionPublisher == null || | ||||||
|  |                 searchEditText == null || disposables == null) return; | ||||||
|  |         final String query = item.query; | ||||||
|  |         new AlertDialog.Builder(activity) | ||||||
|  |                 .setTitle(query) | ||||||
|  |                 .setMessage(R.string.delete_item_search_history) | ||||||
|  |                 .setCancelable(true) | ||||||
|  |                 .setNegativeButton(R.string.cancel, null) | ||||||
|  |                 .setPositiveButton(R.string.delete, (dialog, which) -> { | ||||||
|  |                     final Disposable onDelete = historyRecordManager.deleteSearchHistory(query) | ||||||
|                             .observeOn(AndroidSchedulers.mainThread()) |                             .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                             .subscribe( |                             .subscribe( | ||||||
|                                     howManyDeleted -> suggestionPublisher |                                     howManyDeleted -> suggestionPublisher | ||||||
|                                             .onNext(searchEditText.getText().toString()), |                                             .onNext(searchEditText.getText().toString()), | ||||||
|  |  | ||||||
|                                     throwable -> showSnackBarError(throwable, |                                     throwable -> showSnackBarError(throwable, | ||||||
|                                             UserAction.SOMETHING_ELSE, "none", |                                             UserAction.SOMETHING_ELSE, "none", | ||||||
|                                             "Deleting item failed", R.string.general_error) |                                             "Deleting item failed", R.string.general_error) | ||||||
|                             ); |                             ); | ||||||
|  |                     disposables.add(onDelete); | ||||||
|         new AlertDialog.Builder(activity) |                 }) | ||||||
|                 .setTitle(item.query) |  | ||||||
|                 .setMessage(R.string.delete_item_search_history) |  | ||||||
|                 .setCancelable(true) |  | ||||||
|                 .setNegativeButton(R.string.cancel, null) |  | ||||||
|                 .setPositiveButton(R.string.delete, (dialog, which) -> disposables.add(onDelete)) |  | ||||||
|                 .show(); |                 .show(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -701,19 +704,8 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor | |||||||
|         searchDisposable = ExtractorHelper.searchFor(serviceId, searchQuery, currentPage, contentCountry, filter) |         searchDisposable = ExtractorHelper.searchFor(serviceId, searchQuery, currentPage, contentCountry, filter) | ||||||
|                 .subscribeOn(Schedulers.io()) |                 .subscribeOn(Schedulers.io()) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe(new Consumer<SearchResult>() { |                 .doOnEvent((searchResult, throwable) -> isLoading.set(false)) | ||||||
|                     @Override |                 .subscribe(this::handleResult, this::onError); | ||||||
|                     public void accept(@NonNull SearchResult result) throws Exception { |  | ||||||
|                         isLoading.set(false); |  | ||||||
|                         handleResult(result); |  | ||||||
|                     } |  | ||||||
|                 }, new Consumer<Throwable>() { |  | ||||||
|                     @Override |  | ||||||
|                     public void accept(@NonNull Throwable throwable) throws Exception { |  | ||||||
|                         isLoading.set(false); |  | ||||||
|                         onError(throwable); |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -725,19 +717,8 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor | |||||||
|         searchDisposable = ExtractorHelper.getMoreSearchItems(serviceId, searchQuery, currentNextPage, contentCountry, filter) |         searchDisposable = ExtractorHelper.getMoreSearchItems(serviceId, searchQuery, currentNextPage, contentCountry, filter) | ||||||
|                 .subscribeOn(Schedulers.io()) |                 .subscribeOn(Schedulers.io()) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe(new Consumer<ListExtractor.InfoItemPage>() { |                 .doOnEvent((nextItemsResult, throwable) -> isLoading.set(false)) | ||||||
|                     @Override |                 .subscribe(this::handleNextItems, this::onError); | ||||||
|                     public void accept(@NonNull ListExtractor.InfoItemPage result) throws Exception { |  | ||||||
|                         isLoading.set(false); |  | ||||||
|                         handleNextItems(result); |  | ||||||
|                     } |  | ||||||
|                 }, new Consumer<Throwable>() { |  | ||||||
|                     @Override |  | ||||||
|                     public void accept(@NonNull Throwable throwable) throws Exception { |  | ||||||
|                         isLoading.set(false); |  | ||||||
|                         onError(throwable); |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|   | |||||||
| @@ -59,23 +59,20 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { | |||||||
|         itemBuilder.getImageLoader() |         itemBuilder.getImageLoader() | ||||||
|                 .displayImage(item.thumbnail_url, itemThumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); |                 .displayImage(item.thumbnail_url, itemThumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); | ||||||
|  |  | ||||||
|         itemView.setOnClickListener(new View.OnClickListener() { |         itemView.setOnClickListener(view -> { | ||||||
|             @Override |  | ||||||
|             public void onClick(View view) { |  | ||||||
|             if (itemBuilder.getOnStreamSelectedListener() != null) { |             if (itemBuilder.getOnStreamSelectedListener() != null) { | ||||||
|                 itemBuilder.getOnStreamSelectedListener().selected(item); |                 itemBuilder.getOnStreamSelectedListener().selected(item); | ||||||
|             } |             } | ||||||
|             } |  | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         switch (item.stream_type) { |         switch (item.stream_type) { | ||||||
|             case AUDIO_STREAM: |             case AUDIO_STREAM: | ||||||
|             case VIDEO_STREAM: |             case VIDEO_STREAM: | ||||||
|             case FILE: |  | ||||||
|                 enableLongClick(item); |  | ||||||
|                 break; |  | ||||||
|             case LIVE_STREAM: |             case LIVE_STREAM: | ||||||
|             case AUDIO_LIVE_STREAM: |             case AUDIO_LIVE_STREAM: | ||||||
|  |                 enableLongClick(item); | ||||||
|  |                 break; | ||||||
|  |             case FILE: | ||||||
|             case NONE: |             case NONE: | ||||||
|             default: |             default: | ||||||
|                 disableLongClick(); |                 disableLongClick(); | ||||||
| @@ -85,14 +82,11 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { | |||||||
|  |  | ||||||
|     private void enableLongClick(final StreamInfoItem item) { |     private void enableLongClick(final StreamInfoItem item) { | ||||||
|         itemView.setLongClickable(true); |         itemView.setLongClickable(true); | ||||||
|         itemView.setOnLongClickListener(new View.OnLongClickListener() { |         itemView.setOnLongClickListener(view -> { | ||||||
|             @Override |  | ||||||
|             public boolean onLongClick(View view) { |  | ||||||
|             if (itemBuilder.getOnStreamSelectedListener() != null) { |             if (itemBuilder.getOnStreamSelectedListener() != null) { | ||||||
|                 itemBuilder.getOnStreamSelectedListener().held(item); |                 itemBuilder.getOnStreamSelectedListener().held(item); | ||||||
|             } |             } | ||||||
|             return true; |             return true; | ||||||
|             } |  | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -33,6 +33,7 @@ import android.support.annotation.NonNull; | |||||||
| import android.support.annotation.Nullable; | import android.support.annotation.Nullable; | ||||||
| import android.support.v4.app.NotificationCompat; | import android.support.v4.app.NotificationCompat; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
|  | import android.view.View; | ||||||
| import android.widget.RemoteViews; | import android.widget.RemoteViews; | ||||||
|  |  | ||||||
| import com.google.android.exoplayer2.PlaybackParameters; | import com.google.android.exoplayer2.PlaybackParameters; | ||||||
| @@ -46,6 +47,7 @@ import org.schabi.newpipe.extractor.stream.AudioStream; | |||||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||||
| import org.schabi.newpipe.player.event.PlayerEventListener; | import org.schabi.newpipe.player.event.PlayerEventListener; | ||||||
| import org.schabi.newpipe.player.helper.LockManager; | import org.schabi.newpipe.player.helper.LockManager; | ||||||
|  | import org.schabi.newpipe.player.helper.PlayerHelper; | ||||||
| import org.schabi.newpipe.playlist.PlayQueueItem; | import org.schabi.newpipe.playlist.PlayQueueItem; | ||||||
| import org.schabi.newpipe.util.ListHelper; | import org.schabi.newpipe.util.ListHelper; | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
| @@ -291,15 +293,15 @@ public final class BackgroundPlayer extends Service { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public void onThumbnailReceived(Bitmap thumbnail) { |         public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { | ||||||
|             super.onThumbnailReceived(thumbnail); |             super.onLoadingComplete(imageUri, view, loadedImage); | ||||||
|  |  | ||||||
|             if (thumbnail != null) { |             if (loadedImage != null) { | ||||||
|                 // rebuild notification here since remote view does not release bitmaps, causing memory leaks |                 // rebuild notification here since remote view does not release bitmaps, causing memory leaks | ||||||
|                 resetNotification(); |                 resetNotification(); | ||||||
|  |  | ||||||
|                 if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail); |                 if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); | ||||||
|                 if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail); |                 if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); | ||||||
|  |  | ||||||
|                 updateNotification(-1); |                 updateNotification(-1); | ||||||
|             } |             } | ||||||
| @@ -378,29 +380,34 @@ public final class BackgroundPlayer extends Service { | |||||||
|         // Playback Listener |         // Playback Listener | ||||||
|         //////////////////////////////////////////////////////////////////////////*/ |         //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|         @Override |         protected void onMetadataChanged(@NonNull final PlayQueueItem item, | ||||||
|         public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) { |                                          @Nullable final StreamInfo info, | ||||||
|             if (currentItem == item && currentInfo == info) return; |                                          final int newPlayQueueIndex, | ||||||
|             super.sync(item, info); |                                          final boolean hasPlayQueueItemChanged) { | ||||||
|  |             if (shouldUpdateOnProgress || hasPlayQueueItemChanged) { | ||||||
|                 resetNotification(); |                 resetNotification(); | ||||||
|                 updateNotification(-1); |                 updateNotification(-1); | ||||||
|                 updateMetadata(); |                 updateMetadata(); | ||||||
|             } |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         @Nullable |         @Nullable | ||||||
|         public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { |         public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { | ||||||
|  |             final MediaSource liveSource = super.sourceOf(item, info); | ||||||
|  |             if (liveSource != null) return liveSource; | ||||||
|  |  | ||||||
|             final int index = ListHelper.getDefaultAudioFormat(context, info.audio_streams); |             final int index = ListHelper.getDefaultAudioFormat(context, info.audio_streams); | ||||||
|             if (index < 0 || index >= info.audio_streams.size()) return null; |             if (index < 0 || index >= info.audio_streams.size()) return null; | ||||||
|  |  | ||||||
|             final AudioStream audio = info.audio_streams.get(index); |             final AudioStream audio = info.audio_streams.get(index); | ||||||
|             return buildMediaSource(audio.getUrl(), MediaFormat.getSuffixById(audio.getFormatId())); |             return buildMediaSource(audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio), | ||||||
|  |                     MediaFormat.getSuffixById(audio.getFormatId())); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public void shutdown() { |         public void onPlaybackShutdown() { | ||||||
|             super.shutdown(); |             super.onPlaybackShutdown(); | ||||||
|             onClose(); |             onClose(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -429,7 +436,8 @@ public final class BackgroundPlayer extends Service { | |||||||
|  |  | ||||||
|         private void updatePlayback() { |         private void updatePlayback() { | ||||||
|             if (activityListener != null && simpleExoPlayer != null && playQueue != null) { |             if (activityListener != null && simpleExoPlayer != null && playQueue != null) { | ||||||
|                 activityListener.onPlaybackUpdate(currentState, getRepeatMode(), playQueue.isShuffled(), getPlaybackParameters()); |                 activityListener.onPlaybackUpdate(currentState, getRepeatMode(), | ||||||
|  |                         playQueue.isShuffled(), getPlaybackParameters()); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -43,37 +43,35 @@ import com.google.android.exoplayer2.Player; | |||||||
| import com.google.android.exoplayer2.RenderersFactory; | import com.google.android.exoplayer2.RenderersFactory; | ||||||
| import com.google.android.exoplayer2.SimpleExoPlayer; | import com.google.android.exoplayer2.SimpleExoPlayer; | ||||||
| import com.google.android.exoplayer2.Timeline; | import com.google.android.exoplayer2.Timeline; | ||||||
| import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; | import com.google.android.exoplayer2.source.BehindLiveWindowException; | ||||||
| import com.google.android.exoplayer2.source.ExtractorMediaSource; |  | ||||||
| import com.google.android.exoplayer2.source.MediaSource; | import com.google.android.exoplayer2.source.MediaSource; | ||||||
| import com.google.android.exoplayer2.source.TrackGroupArray; | import com.google.android.exoplayer2.source.TrackGroupArray; | ||||||
| import com.google.android.exoplayer2.source.dash.DashMediaSource; |  | ||||||
| import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; |  | ||||||
| import com.google.android.exoplayer2.source.hls.HlsMediaSource; |  | ||||||
| import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; |  | ||||||
| import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; |  | ||||||
| import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; | import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; | ||||||
| import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; |  | ||||||
| import com.google.android.exoplayer2.trackselection.TrackSelectionArray; | import com.google.android.exoplayer2.trackselection.TrackSelectionArray; | ||||||
| import com.google.android.exoplayer2.upstream.DataSource; |  | ||||||
| import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; | import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; | ||||||
| import com.google.android.exoplayer2.util.Util; | import com.google.android.exoplayer2.util.Util; | ||||||
| import com.nostra13.universalimageloader.core.ImageLoader; | import com.nostra13.universalimageloader.core.ImageLoader; | ||||||
| import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; | import com.nostra13.universalimageloader.core.assist.FailReason; | ||||||
|  | import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.Downloader; | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||||
| import org.schabi.newpipe.history.HistoryRecordManager; | import org.schabi.newpipe.history.HistoryRecordManager; | ||||||
| import org.schabi.newpipe.player.helper.AudioReactor; | import org.schabi.newpipe.player.helper.AudioReactor; | ||||||
| import org.schabi.newpipe.player.helper.CacheFactory; |  | ||||||
| import org.schabi.newpipe.player.helper.LoadController; | import org.schabi.newpipe.player.helper.LoadController; | ||||||
|  | import org.schabi.newpipe.player.helper.PlayerDataSource; | ||||||
|  | import org.schabi.newpipe.player.helper.PlayerHelper; | ||||||
|  | import org.schabi.newpipe.player.playback.CustomTrackSelector; | ||||||
| import org.schabi.newpipe.player.playback.MediaSourceManager; | import org.schabi.newpipe.player.playback.MediaSourceManager; | ||||||
| import org.schabi.newpipe.player.playback.PlaybackListener; | import org.schabi.newpipe.player.playback.PlaybackListener; | ||||||
| import org.schabi.newpipe.playlist.PlayQueue; | import org.schabi.newpipe.playlist.PlayQueue; | ||||||
| import org.schabi.newpipe.playlist.PlayQueueAdapter; | import org.schabi.newpipe.playlist.PlayQueueAdapter; | ||||||
| import org.schabi.newpipe.playlist.PlayQueueItem; | import org.schabi.newpipe.playlist.PlayQueueItem; | ||||||
|  | import org.schabi.newpipe.util.SerializedCache; | ||||||
|  |  | ||||||
| import java.io.Serializable; | import java.io.IOException; | ||||||
|  | import java.net.UnknownHostException; | ||||||
| import java.util.concurrent.TimeUnit; | import java.util.concurrent.TimeUnit; | ||||||
|  |  | ||||||
| import io.reactivex.Observable; | import io.reactivex.Observable; | ||||||
| @@ -93,17 +91,18 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; | |||||||
|  * @author mauriciocolli |  * @author mauriciocolli | ||||||
|  */ |  */ | ||||||
| @SuppressWarnings({"WeakerAccess"}) | @SuppressWarnings({"WeakerAccess"}) | ||||||
| public abstract class BasePlayer implements Player.EventListener, PlaybackListener { | public abstract class BasePlayer implements | ||||||
|  |         Player.EventListener, PlaybackListener, ImageLoadingListener { | ||||||
|  |  | ||||||
|     public static final boolean DEBUG = true; |     public static final boolean DEBUG = true; | ||||||
|     public static final String TAG = "BasePlayer"; |     @NonNull public static final String TAG = "BasePlayer"; | ||||||
|  |  | ||||||
|     protected Context context; |     @NonNull final protected Context context; | ||||||
|  |  | ||||||
|     protected BroadcastReceiver broadcastReceiver; |     @NonNull final protected BroadcastReceiver broadcastReceiver; | ||||||
|     protected IntentFilter intentFilter; |     @NonNull final protected IntentFilter intentFilter; | ||||||
|  |  | ||||||
|     protected PlayQueueAdapter playQueueAdapter; |     @NonNull final protected HistoryRecordManager recordManager; | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Intent |     // Intent | ||||||
| @@ -113,7 +112,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|     public static final String PLAYBACK_PITCH = "playback_pitch"; |     public static final String PLAYBACK_PITCH = "playback_pitch"; | ||||||
|     public static final String PLAYBACK_SPEED = "playback_speed"; |     public static final String PLAYBACK_SPEED = "playback_speed"; | ||||||
|     public static final String PLAYBACK_QUALITY = "playback_quality"; |     public static final String PLAYBACK_QUALITY = "playback_quality"; | ||||||
|     public static final String PLAY_QUEUE = "play_queue"; |     public static final String PLAY_QUEUE_KEY = "play_queue_key"; | ||||||
|     public static final String APPEND_ONLY = "append_only"; |     public static final String APPEND_ONLY = "append_only"; | ||||||
|     public static final String SELECT_ON_APPEND = "select_on_append"; |     public static final String SELECT_ON_APPEND = "select_on_append"; | ||||||
|  |  | ||||||
| @@ -124,8 +123,10 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|     protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f}; |     protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f}; | ||||||
|     protected static final float[] PLAYBACK_PITCHES = {0.8f, 0.9f, 0.95f, 1f, 1.05f, 1.1f, 1.2f}; |     protected static final float[] PLAYBACK_PITCHES = {0.8f, 0.9f, 0.95f, 1f, 1.05f, 1.1f, 1.2f}; | ||||||
|  |  | ||||||
|     protected MediaSourceManager playbackManager; |  | ||||||
|     protected PlayQueue playQueue; |     protected PlayQueue playQueue; | ||||||
|  |     protected PlayQueueAdapter playQueueAdapter; | ||||||
|  |  | ||||||
|  |     protected MediaSourceManager playbackManager; | ||||||
|  |  | ||||||
|     protected StreamInfo currentInfo; |     protected StreamInfo currentInfo; | ||||||
|     protected PlayQueueItem currentItem; |     protected PlayQueueItem currentItem; | ||||||
| @@ -141,23 +142,20 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|     protected final static int PROGRESS_LOOP_INTERVAL = 500; |     protected final static int PROGRESS_LOOP_INTERVAL = 500; | ||||||
|     protected final static int RECOVERY_SKIP_THRESHOLD = 3000; // 3 seconds |     protected final static int RECOVERY_SKIP_THRESHOLD = 3000; // 3 seconds | ||||||
|  |  | ||||||
|  |     protected CustomTrackSelector trackSelector; | ||||||
|  |     protected PlayerDataSource dataSource; | ||||||
|  |  | ||||||
|     protected SimpleExoPlayer simpleExoPlayer; |     protected SimpleExoPlayer simpleExoPlayer; | ||||||
|     protected AudioReactor audioReactor; |     protected AudioReactor audioReactor; | ||||||
|  |  | ||||||
|     protected boolean isPrepared = false; |     protected boolean isPrepared = false; | ||||||
|  |  | ||||||
|     protected DefaultTrackSelector trackSelector; |  | ||||||
|     protected DataSource.Factory cacheDataSourceFactory; |  | ||||||
|     protected DefaultExtractorsFactory extractorsFactory; |  | ||||||
|  |  | ||||||
|     protected Disposable progressUpdateReactor; |     protected Disposable progressUpdateReactor; | ||||||
|     protected CompositeDisposable databaseUpdateReactor; |     protected CompositeDisposable databaseUpdateReactor; | ||||||
|  |  | ||||||
|     protected HistoryRecordManager recordManager; |  | ||||||
|  |  | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     public BasePlayer(Context context) { |     public BasePlayer(@NonNull final Context context) { | ||||||
|         this.context = context; |         this.context = context; | ||||||
|  |  | ||||||
|         this.broadcastReceiver = new BroadcastReceiver() { |         this.broadcastReceiver = new BroadcastReceiver() { | ||||||
| @@ -169,6 +167,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|         this.intentFilter = new IntentFilter(); |         this.intentFilter = new IntentFilter(); | ||||||
|         setupBroadcastReceiver(intentFilter); |         setupBroadcastReceiver(intentFilter); | ||||||
|         context.registerReceiver(broadcastReceiver, intentFilter); |         context.registerReceiver(broadcastReceiver, intentFilter); | ||||||
|  |  | ||||||
|  |         this.recordManager = new HistoryRecordManager(context); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void setup() { |     public void setup() { | ||||||
| @@ -179,51 +179,46 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|     public void initPlayer() { |     public void initPlayer() { | ||||||
|         if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); |         if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); | ||||||
|  |  | ||||||
|         if (recordManager == null) recordManager = new HistoryRecordManager(context); |  | ||||||
|         if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); |         if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); | ||||||
|         databaseUpdateReactor = new CompositeDisposable(); |         databaseUpdateReactor = new CompositeDisposable(); | ||||||
|  |  | ||||||
|  |         final String userAgent = Downloader.USER_AGENT; | ||||||
|         final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); |         final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); | ||||||
|         final AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter); |         dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter); | ||||||
|  |  | ||||||
|  |         final AdaptiveTrackSelection.Factory trackSelectionFactory = | ||||||
|  |                 new AdaptiveTrackSelection.Factory(bandwidthMeter); | ||||||
|  |         trackSelector = new CustomTrackSelector(trackSelectionFactory); | ||||||
|  |  | ||||||
|         final LoadControl loadControl = new LoadController(context); |         final LoadControl loadControl = new LoadController(context); | ||||||
|         final RenderersFactory renderFactory = new DefaultRenderersFactory(context); |         final RenderersFactory renderFactory = new DefaultRenderersFactory(context); | ||||||
|  |  | ||||||
|         trackSelector = new DefaultTrackSelector(trackSelectionFactory); |  | ||||||
|         extractorsFactory = new DefaultExtractorsFactory(); |  | ||||||
|         cacheDataSourceFactory = new CacheFactory(context); |  | ||||||
|  |  | ||||||
|         simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl); |         simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl); | ||||||
|         audioReactor = new AudioReactor(context, simpleExoPlayer); |         audioReactor = new AudioReactor(context, simpleExoPlayer); | ||||||
|  |  | ||||||
|         simpleExoPlayer.addListener(this); |         simpleExoPlayer.addListener(this); | ||||||
|         simpleExoPlayer.setPlayWhenReady(true); |         simpleExoPlayer.setPlayWhenReady(true); | ||||||
|  |         simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void initListeners() {} |     public void initListeners() {} | ||||||
|  |  | ||||||
|     private Disposable getProgressReactor() { |  | ||||||
|         return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) |  | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |  | ||||||
|                 .filter(ignored -> isProgressLoopRunning()) |  | ||||||
|                 .subscribe(ignored -> triggerProgressUpdate()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void handleIntent(Intent intent) { |     public void handleIntent(Intent intent) { | ||||||
|         if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); |         if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); | ||||||
|         if (intent == null) return; |         if (intent == null) return; | ||||||
|  |  | ||||||
|         // Resolve play queue |         // Resolve play queue | ||||||
|         if (!intent.hasExtra(PLAY_QUEUE)) return; |         if (!intent.hasExtra(PLAY_QUEUE_KEY)) return; | ||||||
|         final Serializable playQueueCandidate = intent.getSerializableExtra(PLAY_QUEUE); |         final String intentCacheKey = intent.getStringExtra(PLAY_QUEUE_KEY); | ||||||
|         if (!(playQueueCandidate instanceof PlayQueue)) return; |         final PlayQueue queue = SerializedCache.getInstance().take(intentCacheKey, PlayQueue.class); | ||||||
|         final PlayQueue queue = (PlayQueue) playQueueCandidate; |         if (queue == null) return; | ||||||
|  |  | ||||||
|         // Resolve append intents |         // Resolve append intents | ||||||
|         if (intent.getBooleanExtra(APPEND_ONLY, false) && playQueue != null) { |         if (intent.getBooleanExtra(APPEND_ONLY, false) && playQueue != null) { | ||||||
|             int sizeBeforeAppend = playQueue.size(); |             int sizeBeforeAppend = playQueue.size(); | ||||||
|             playQueue.append(queue.getStreams()); |             playQueue.append(queue.getStreams()); | ||||||
|  |  | ||||||
|             if (intent.getBooleanExtra(SELECT_ON_APPEND, false) && queue.getStreams().size() > 0) { |             if (intent.getBooleanExtra(SELECT_ON_APPEND, false) && | ||||||
|  |                     queue.getStreams().size() > 0) { | ||||||
|                 playQueue.setIndex(sizeBeforeAppend); |                 playQueue.setIndex(sizeBeforeAppend); | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -234,17 +229,19 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|         final float playbackSpeed = intent.getFloatExtra(PLAYBACK_SPEED, getPlaybackSpeed()); |         final float playbackSpeed = intent.getFloatExtra(PLAYBACK_SPEED, getPlaybackSpeed()); | ||||||
|         final float playbackPitch = intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch()); |         final float playbackPitch = intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch()); | ||||||
|  |  | ||||||
|         // Re-initialization |         // Good to go... | ||||||
|  |         initPlayback(queue, repeatMode, playbackSpeed, playbackPitch); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected void initPlayback(@NonNull final PlayQueue queue, | ||||||
|  |                                 @Player.RepeatMode final int repeatMode, | ||||||
|  |                                 final float playbackSpeed, | ||||||
|  |                                 final float playbackPitch) { | ||||||
|         destroyPlayer(); |         destroyPlayer(); | ||||||
|         initPlayer(); |         initPlayer(); | ||||||
|         setRepeatMode(repeatMode); |         setRepeatMode(repeatMode); | ||||||
|         setPlaybackParameters(playbackSpeed, playbackPitch); |         setPlaybackParameters(playbackSpeed, playbackPitch); | ||||||
|  |  | ||||||
|         // Good to go... |  | ||||||
|         initPlayback(queue); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     protected void initPlayback(final PlayQueue queue) { |  | ||||||
|         playQueue = queue; |         playQueue = queue; | ||||||
|         playQueue.init(); |         playQueue.init(); | ||||||
|         playbackManager = new MediaSourceManager(this, playQueue); |         playbackManager = new MediaSourceManager(this, playQueue); | ||||||
| @@ -253,24 +250,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|         playQueueAdapter = new PlayQueueAdapter(context, playQueue); |         playQueueAdapter = new PlayQueueAdapter(context, playQueue); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void initThumbnail(final String url) { |  | ||||||
|         if (DEBUG) Log.d(TAG, "initThumbnail() called"); |  | ||||||
|         if (url == null || url.isEmpty()) return; |  | ||||||
|         ImageLoader.getInstance().resume(); |  | ||||||
|         ImageLoader.getInstance().loadImage(url, new SimpleImageLoadingListener() { |  | ||||||
|             @Override |  | ||||||
|             public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { |  | ||||||
|                 if (simpleExoPlayer == null) return; |  | ||||||
|                 if (DEBUG) Log.d(TAG, "onLoadingComplete() called with: imageUri = [" + imageUri + "], view = [" + view + "], loadedImage = [" + loadedImage + "]"); |  | ||||||
|                 onThumbnailReceived(loadedImage); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void onThumbnailReceived(Bitmap thumbnail) { |  | ||||||
|         if (DEBUG) Log.d(TAG, "onThumbnailReceived() called with: thumbnail = [" + thumbnail + "]"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void destroyPlayer() { |     public void destroyPlayer() { | ||||||
|         if (DEBUG) Log.d(TAG, "destroyPlayer() called"); |         if (DEBUG) Log.d(TAG, "destroyPlayer() called"); | ||||||
|         if (simpleExoPlayer != null) { |         if (simpleExoPlayer != null) { | ||||||
| @@ -298,34 +277,99 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|  |  | ||||||
|         trackSelector = null; |         trackSelector = null; | ||||||
|         simpleExoPlayer = null; |         simpleExoPlayer = null; | ||||||
|         recordManager = null; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public MediaSource buildMediaSource(String url, String overrideExtension) { |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|         if (DEBUG) { |     // Thumbnail Loading | ||||||
|             Log.d(TAG, "buildMediaSource() called with: url = [" + url + "], overrideExtension = [" + overrideExtension + "]"); |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     public void initThumbnail(final String url) { | ||||||
|  |         if (DEBUG) Log.d(TAG, "Thumbnail - initThumbnail() called"); | ||||||
|  |         if (url == null || url.isEmpty()) return; | ||||||
|  |         ImageLoader.getInstance().resume(); | ||||||
|  |         ImageLoader.getInstance().loadImage(url, this); | ||||||
|     } |     } | ||||||
|         Uri uri = Uri.parse(url); |  | ||||||
|         int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); |     @Override | ||||||
|         MediaSource mediaSource; |     public void onLoadingStarted(String imageUri, View view) { | ||||||
|  |         if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingStarted() called on: " + | ||||||
|  |                 "imageUri = [" + imageUri + "], view = [" + view + "]"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onLoadingFailed(String imageUri, View view, FailReason failReason) { | ||||||
|  |         Log.e(TAG, "Thumbnail - onLoadingFailed() called on imageUri = [" + imageUri + "]", | ||||||
|  |                 failReason.getCause()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { | ||||||
|  |         if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingComplete() called with: " + | ||||||
|  |                 "imageUri = [" + imageUri + "], view = [" + view + "], " + | ||||||
|  |                 "loadedImage = [" + loadedImage + "]"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onLoadingCancelled(String imageUri, View view) { | ||||||
|  |         if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " + | ||||||
|  |                 "imageUri = [" + imageUri + "], view = [" + view + "]"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected void clearThumbnailCache() { | ||||||
|  |         ImageLoader.getInstance().clearMemoryCache(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // MediaSource Building | ||||||
|  |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     public MediaSource buildLiveMediaSource(@NonNull final String sourceUrl, | ||||||
|  |                                             @C.ContentType final int type) { | ||||||
|  |         if (DEBUG) { | ||||||
|  |             Log.d(TAG, "buildLiveMediaSource() called with: url = [" + sourceUrl + | ||||||
|  |                     "], content type = [" + type + "]"); | ||||||
|  |         } | ||||||
|  |         if (dataSource == null) return null; | ||||||
|  |  | ||||||
|  |         final Uri uri = Uri.parse(sourceUrl); | ||||||
|         switch (type) { |         switch (type) { | ||||||
|             case C.TYPE_SS: |             case C.TYPE_SS: | ||||||
|                 mediaSource = new SsMediaSource(uri, cacheDataSourceFactory, new DefaultSsChunkSource.Factory(cacheDataSourceFactory), null, null); |                 return dataSource.getLiveSsMediaSourceFactory().createMediaSource(uri); | ||||||
|                 break; |  | ||||||
|             case C.TYPE_DASH: |             case C.TYPE_DASH: | ||||||
|                 mediaSource = new DashMediaSource(uri, cacheDataSourceFactory, new DefaultDashChunkSource.Factory(cacheDataSourceFactory), null, null); |                 return dataSource.getLiveDashMediaSourceFactory().createMediaSource(uri); | ||||||
|                 break; |  | ||||||
|             case C.TYPE_HLS: |             case C.TYPE_HLS: | ||||||
|                 mediaSource = new HlsMediaSource(uri, cacheDataSourceFactory, null, null); |                 return dataSource.getLiveHlsMediaSourceFactory().createMediaSource(uri); | ||||||
|                 break; |             default: | ||||||
|             case C.TYPE_OTHER: |  | ||||||
|                 mediaSource = new ExtractorMediaSource(uri, cacheDataSourceFactory, extractorsFactory, null, null); |  | ||||||
|                 break; |  | ||||||
|             default: { |  | ||||||
|                 throw new IllegalStateException("Unsupported type: " + type); |                 throw new IllegalStateException("Unsupported type: " + type); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|         return mediaSource; |  | ||||||
|  |     public MediaSource buildMediaSource(@NonNull final String sourceUrl, | ||||||
|  |                                         @NonNull final String cacheKey, | ||||||
|  |                                         @NonNull final String overrideExtension) { | ||||||
|  |         if (DEBUG) { | ||||||
|  |             Log.d(TAG, "buildMediaSource() called with: url = [" + sourceUrl + | ||||||
|  |                     "], cacheKey = [" + cacheKey + "]" + | ||||||
|  |                     "], overrideExtension = [" + overrideExtension + "]"); | ||||||
|  |         } | ||||||
|  |         if (dataSource == null) return null; | ||||||
|  |  | ||||||
|  |         final Uri uri = Uri.parse(sourceUrl); | ||||||
|  |         @C.ContentType final int type = TextUtils.isEmpty(overrideExtension) ? | ||||||
|  |                 Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); | ||||||
|  |  | ||||||
|  |         switch (type) { | ||||||
|  |             case C.TYPE_SS: | ||||||
|  |                 return dataSource.getLiveSsMediaSourceFactory().createMediaSource(uri); | ||||||
|  |             case C.TYPE_DASH: | ||||||
|  |                 return dataSource.getDashMediaSourceFactory().createMediaSource(uri); | ||||||
|  |             case C.TYPE_HLS: | ||||||
|  |                 return dataSource.getHlsMediaSourceFactory().createMediaSource(uri); | ||||||
|  |             case C.TYPE_OTHER: | ||||||
|  |                 return dataSource.getExtractorMediaSourceFactory(cacheKey).createMediaSource(uri); | ||||||
|  |             default: | ||||||
|  |                 throw new IllegalStateException("Unsupported type: " + type); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
| @@ -345,15 +389,16 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|         if (intent == null || intent.getAction() == null) return; |         if (intent == null || intent.getAction() == null) return; | ||||||
|         switch (intent.getAction()) { |         switch (intent.getAction()) { | ||||||
|             case AudioManager.ACTION_AUDIO_BECOMING_NOISY: |             case AudioManager.ACTION_AUDIO_BECOMING_NOISY: | ||||||
|                 if (isPlaying()) simpleExoPlayer.setPlayWhenReady(false); |                 if (isPlaying()) onVideoPlayPause(); | ||||||
|                 break; |                 break; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void unregisterBroadcastReceiver() { |     public void unregisterBroadcastReceiver() { | ||||||
|         if (broadcastReceiver != null && context != null) { |         try { | ||||||
|             context.unregisterReceiver(broadcastReceiver); |             context.unregisterReceiver(broadcastReceiver); | ||||||
|             broadcastReceiver = null; |         } catch (final IllegalArgumentException unregisteredException) { | ||||||
|  |             Log.e(TAG, "Broadcast receiver already unregistered.", unregisteredException); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -403,17 +448,16 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|     public void onPlaying() { |     public void onPlaying() { | ||||||
|         if (DEBUG) Log.d(TAG, "onPlaying() called"); |         if (DEBUG) Log.d(TAG, "onPlaying() called"); | ||||||
|         if (!isProgressLoopRunning()) startProgressLoop(); |         if (!isProgressLoopRunning()) startProgressLoop(); | ||||||
|  |         if (!isCurrentWindowValid()) seekToDefault(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void onBuffering() { |     public void onBuffering() {} | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void onPaused() { |     public void onPaused() { | ||||||
|         if (isProgressLoopRunning()) stopProgressLoop(); |         if (isProgressLoopRunning()) stopProgressLoop(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void onPausedSeek() { |     public void onPausedSeek() {} | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void onCompleted() { |     public void onCompleted() { | ||||||
|         if (DEBUG) Log.d(TAG, "onCompleted() called"); |         if (DEBUG) Log.d(TAG, "onCompleted() called"); | ||||||
| @@ -450,21 +494,134 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|     public void onShuffleClicked() { |     public void onShuffleClicked() { | ||||||
|         if (DEBUG) Log.d(TAG, "onShuffleClicked() called"); |         if (DEBUG) Log.d(TAG, "onShuffleClicked() called"); | ||||||
|  |  | ||||||
|         if (playQueue == null) return; |         if (simpleExoPlayer == null) return; | ||||||
|  |         simpleExoPlayer.setShuffleModeEnabled(!simpleExoPlayer.getShuffleModeEnabled()); | ||||||
|         setRecovery(); |  | ||||||
|         if (playQueue.isShuffled()) { |  | ||||||
|             playQueue.unshuffle(); |  | ||||||
|         } else { |  | ||||||
|             playQueue.shuffle(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Progress Updates | ||||||
|  |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     public abstract void onUpdateProgress(int currentProgress, int duration, int bufferPercent); | ||||||
|  |  | ||||||
|  |     protected void startProgressLoop() { | ||||||
|  |         if (progressUpdateReactor != null) progressUpdateReactor.dispose(); | ||||||
|  |         progressUpdateReactor = getProgressReactor(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected void stopProgressLoop() { | ||||||
|  |         if (progressUpdateReactor != null) progressUpdateReactor.dispose(); | ||||||
|  |         progressUpdateReactor = null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void triggerProgressUpdate() { | ||||||
|  |         onUpdateProgress( | ||||||
|  |                 (int) simpleExoPlayer.getCurrentPosition(), | ||||||
|  |                 (int) simpleExoPlayer.getDuration(), | ||||||
|  |                 simpleExoPlayer.getBufferedPercentage() | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     private Disposable getProgressReactor() { | ||||||
|  |         return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) | ||||||
|  |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                 .filter(ignored -> isProgressLoopRunning()) | ||||||
|  |                 .subscribe(ignored -> triggerProgressUpdate()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // ExoPlayer Listener |     // ExoPlayer Listener | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     private void recover() { |     @Override | ||||||
|  |     public void onTimelineChanged(Timeline timeline, Object manifest, | ||||||
|  |                                   @Player.TimelineChangeReason final int reason) { | ||||||
|  |         if (DEBUG) Log.d(TAG, "ExoPlayer - onTimelineChanged() called with " + | ||||||
|  |                 (manifest == null ? "no manifest" : "available manifest") + ", " + | ||||||
|  |                 "timeline size = [" + timeline.getWindowCount() + "], " + | ||||||
|  |                 "reason = [" + reason + "]"); | ||||||
|  |  | ||||||
|  |         switch (reason) { | ||||||
|  |             case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block | ||||||
|  |             case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock | ||||||
|  |             case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes | ||||||
|  |                 if (playQueue != null && playbackManager != null && | ||||||
|  |                         // ensures MediaSourceManager#update is complete | ||||||
|  |                         timeline.getWindowCount() == playQueue.size()) { | ||||||
|  |                     playbackManager.load(); | ||||||
|  |                 } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { | ||||||
|  |         if (DEBUG) Log.d(TAG, "ExoPlayer - onTracksChanged(), " + | ||||||
|  |                 "track group size = " + trackGroups.length); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { | ||||||
|  |         if (DEBUG) Log.d(TAG, "ExoPlayer - playbackParameters(), " + | ||||||
|  |                 "speed: " + playbackParameters.speed + ", " + | ||||||
|  |                 "pitch: " + playbackParameters.pitch); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onLoadingChanged(final boolean isLoading) { | ||||||
|  |         if (DEBUG) Log.d(TAG, "ExoPlayer - onLoadingChanged() called with: " + | ||||||
|  |                 "isLoading = [" + isLoading + "]"); | ||||||
|  |  | ||||||
|  |         if (!isLoading && getCurrentState() == STATE_PAUSED && isProgressLoopRunning()) { | ||||||
|  |             stopProgressLoop(); | ||||||
|  |         } else if (isLoading && !isProgressLoopRunning()) { | ||||||
|  |             startProgressLoop(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { | ||||||
|  |         if (DEBUG) Log.d(TAG, "ExoPlayer - onPlayerStateChanged() called with: " + | ||||||
|  |                 "playWhenReady = [" + playWhenReady + "], " + | ||||||
|  |                 "playbackState = [" + playbackState + "]"); | ||||||
|  |  | ||||||
|  |         if (getCurrentState() == STATE_PAUSED_SEEK) { | ||||||
|  |             if (DEBUG) Log.d(TAG, "ExoPlayer - onPlayerStateChanged() is currently blocked"); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         switch (playbackState) { | ||||||
|  |             case Player.STATE_IDLE: // 1 | ||||||
|  |                 isPrepared = false; | ||||||
|  |                 break; | ||||||
|  |             case Player.STATE_BUFFERING: // 2 | ||||||
|  |                 if (isPrepared) { | ||||||
|  |                     changeState(STATE_BUFFERING); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case Player.STATE_READY: //3 | ||||||
|  |                 maybeRecover(); | ||||||
|  |                 if (!isPrepared) { | ||||||
|  |                     isPrepared = true; | ||||||
|  |                     onPrepared(playWhenReady); | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |                 if (currentState == STATE_PAUSED_SEEK) break; | ||||||
|  |                 changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); | ||||||
|  |                 break; | ||||||
|  |             case Player.STATE_ENDED: // 4 | ||||||
|  |                 // Ensure the current window has actually ended | ||||||
|  |                 // since single windows that are still loading may produce an ended state | ||||||
|  |                 if (isCurrentWindowValid() && | ||||||
|  |                         simpleExoPlayer.getCurrentPosition() >= simpleExoPlayer.getDuration()) { | ||||||
|  |                     changeState(STATE_COMPLETED); | ||||||
|  |                     isPrepared = false; | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void maybeRecover() { | ||||||
|         final int currentSourceIndex = playQueue.getIndex(); |         final int currentSourceIndex = playQueue.getIndex(); | ||||||
|         final PlayQueueItem currentSourceItem = playQueue.getItem(); |         final PlayQueueItem currentSourceItem = playQueue.getItem(); | ||||||
|  |  | ||||||
| @@ -488,90 +645,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onTimelineChanged(Timeline timeline, Object manifest) { |  | ||||||
|         if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount()); |  | ||||||
|  |  | ||||||
|         if (playbackManager != null) { |  | ||||||
|             playbackManager.load(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { |  | ||||||
|         if (DEBUG) Log.d(TAG, "onTracksChanged(), track group size = " + trackGroups.length); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { |  | ||||||
|         if (DEBUG) Log.d(TAG, "playbackParameters(), speed: " + playbackParameters.speed + ", pitch: " + playbackParameters.pitch); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onLoadingChanged(boolean isLoading) { |  | ||||||
|         if (DEBUG) Log.d(TAG, "onLoadingChanged() called with: isLoading = [" + isLoading + "]"); |  | ||||||
|  |  | ||||||
|         if (!isLoading && getCurrentState() == STATE_PAUSED && isProgressLoopRunning()) stopProgressLoop(); |  | ||||||
|         else if (isLoading && !isProgressLoopRunning()) startProgressLoop(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { |  | ||||||
|         if (DEBUG) |  | ||||||
|             Log.d(TAG, "onPlayerStateChanged() called with: playWhenReady = [" + playWhenReady + "], playbackState = [" + playbackState + "]"); |  | ||||||
|         if (getCurrentState() == STATE_PAUSED_SEEK) { |  | ||||||
|             if (DEBUG) Log.d(TAG, "onPlayerStateChanged() is currently blocked"); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         switch (playbackState) { |  | ||||||
|             case Player.STATE_IDLE: // 1 |  | ||||||
|                 isPrepared = false; |  | ||||||
|                 break; |  | ||||||
|             case Player.STATE_BUFFERING: // 2 |  | ||||||
|                 if (isPrepared) { |  | ||||||
|                     changeState(STATE_BUFFERING); |  | ||||||
|                 } |  | ||||||
|                 break; |  | ||||||
|             case Player.STATE_READY: //3 |  | ||||||
|                 recover(); |  | ||||||
|                 if (!isPrepared) { |  | ||||||
|                     isPrepared = true; |  | ||||||
|                     onPrepared(playWhenReady); |  | ||||||
|                     break; |  | ||||||
|                 } |  | ||||||
|                 if (currentState == STATE_PAUSED_SEEK) break; |  | ||||||
|                 changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); |  | ||||||
|                 break; |  | ||||||
|             case Player.STATE_ENDED: // 4 |  | ||||||
|                 // Ensure the current window has actually ended |  | ||||||
|                 // since single windows that are still loading may produce an ended state |  | ||||||
|                 if (isCurrentWindowValid() && simpleExoPlayer.getCurrentPosition() >= simpleExoPlayer.getDuration()) { |  | ||||||
|                     changeState(STATE_COMPLETED); |  | ||||||
|                     isPrepared = false; |  | ||||||
|                 } |  | ||||||
|                 break; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Processes the exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}. |      * Processes the exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}. | ||||||
|      * There are multiple types of errors: <br><br> |      * There are multiple types of errors: <br><br> | ||||||
|      * |      * | ||||||
|      * {@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}: <br><br> |      * {@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}: <br><br> | ||||||
|      * If the current {@link com.google.android.exoplayer2.Timeline.Window window} is valid, |  | ||||||
|      * then we know the error is produced by transitioning into a bad window, therefore we report |  | ||||||
|      * an error to the play queue based on if the current error can be skipped. |  | ||||||
|      * |  | ||||||
|      * This is done because ExoPlayer reports the source exceptions before window is |  | ||||||
|      * transitioned on seamless playback. Because player error causes ExoPlayer to go |  | ||||||
|      * back to {@link Player#STATE_IDLE STATE_IDLE}, we reset and prepare the media source |  | ||||||
|      * again to resume playback. |  | ||||||
|      * |  | ||||||
|      * In the event that this error is produced during a valid stream playback, we save the |  | ||||||
|      * current position so the playback may be recovered and resumed manually by the user. This |  | ||||||
|      * happens only if the playback is {@link #RECOVERY_SKIP_THRESHOLD} milliseconds until complete. |  | ||||||
|      * <br><br> |  | ||||||
|      * |      * | ||||||
|      * {@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}: <br><br> |      * {@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}: <br><br> | ||||||
|      * If a runtime error occurred, then we can try to recover it by restarting the playback |      * If a runtime error occurred, then we can try to recover it by restarting the playback | ||||||
| @@ -580,11 +658,13 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|      * {@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER}: <br><br> |      * {@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER}: <br><br> | ||||||
|      * If the renderer failed, treat the error as unrecoverable. |      * If the renderer failed, treat the error as unrecoverable. | ||||||
|      * |      * | ||||||
|  |      * @see #processSourceError(IOException) | ||||||
|      * @see Player.EventListener#onPlayerError(ExoPlaybackException) |      * @see Player.EventListener#onPlayerError(ExoPlaybackException) | ||||||
|      *  */ |      *  */ | ||||||
|     @Override |     @Override | ||||||
|     public void onPlayerError(ExoPlaybackException error) { |     public void onPlayerError(ExoPlaybackException error) { | ||||||
|         if (DEBUG) Log.d(TAG, "onPlayerError() called with: error = [" + error + "]"); |         if (DEBUG) Log.d(TAG, "ExoPlayer - onPlayerError() called with: " + | ||||||
|  |                 "error = [" + error + "]"); | ||||||
|         if (errorToast != null) { |         if (errorToast != null) { | ||||||
|             errorToast.cancel(); |             errorToast.cancel(); | ||||||
|             errorToast = null; |             errorToast = null; | ||||||
| @@ -594,11 +674,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|  |  | ||||||
|         switch (error.type) { |         switch (error.type) { | ||||||
|             case ExoPlaybackException.TYPE_SOURCE: |             case ExoPlaybackException.TYPE_SOURCE: | ||||||
|                 if (simpleExoPlayer.getCurrentPosition() < |                 processSourceError(error.getSourceException()); | ||||||
|                         simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD) { |  | ||||||
|                     setRecovery(); |  | ||||||
|                 } |  | ||||||
|                 playQueue.error(isCurrentWindowValid()); |  | ||||||
|                 showStreamError(error); |                 showStreamError(error); | ||||||
|                 break; |                 break; | ||||||
|             case ExoPlaybackException.TYPE_UNEXPECTED: |             case ExoPlaybackException.TYPE_UNEXPECTED: | ||||||
| @@ -608,14 +684,53 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|                 break; |                 break; | ||||||
|             default: |             default: | ||||||
|                 showUnrecoverableError(error); |                 showUnrecoverableError(error); | ||||||
|                 shutdown(); |                 onPlaybackShutdown(); | ||||||
|                 break; |                 break; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Processes {@link ExoPlaybackException} tagged with {@link ExoPlaybackException#TYPE_SOURCE}. | ||||||
|  |      * <br><br> | ||||||
|  |      * If the current {@link com.google.android.exoplayer2.Timeline.Window window} is valid, | ||||||
|  |      * then we know the error is produced by transitioning into a bad window, therefore we report | ||||||
|  |      * an error to the play queue based on if the current error can be skipped. | ||||||
|  |      * <br><br> | ||||||
|  |      * This is done because ExoPlayer reports the source exceptions before window is | ||||||
|  |      * transitioned on seamless playback. Because player error causes ExoPlayer to go | ||||||
|  |      * back to {@link Player#STATE_IDLE STATE_IDLE}, we reset and prepare the media source | ||||||
|  |      * again to resume playback. | ||||||
|  |      * <br><br> | ||||||
|  |      * In the event that this error is produced during a valid stream playback, we save the | ||||||
|  |      * current position so the playback may be recovered and resumed manually by the user. This | ||||||
|  |      * happens only if the playback is {@link #RECOVERY_SKIP_THRESHOLD} milliseconds until complete. | ||||||
|  |      * <br><br> | ||||||
|  |      * In the event of livestreaming being lagged behind for any reason, most notably pausing for | ||||||
|  |      * too long, a {@link BehindLiveWindowException} will be produced. This will trigger a reload | ||||||
|  |      * instead of skipping or removal. | ||||||
|  |      * */ | ||||||
|  |     private void processSourceError(final IOException error) { | ||||||
|  |         if (simpleExoPlayer == null || playQueue == null) return; | ||||||
|  |  | ||||||
|  |         if (simpleExoPlayer.getCurrentPosition() < | ||||||
|  |                 simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD) { | ||||||
|  |             setRecovery(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         final Throwable cause = error.getCause(); | ||||||
|  |         if (cause instanceof BehindLiveWindowException) { | ||||||
|  |             reload(); | ||||||
|  |         } else if (cause instanceof UnknownHostException) { | ||||||
|  |             playQueue.error(/*isNetworkProblem=*/true); | ||||||
|  |         } else { | ||||||
|  |             playQueue.error(isCurrentWindowValid()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onPositionDiscontinuity(int reason) { |     public void onPositionDiscontinuity(@Player.DiscontinuityReason final int reason) { | ||||||
|         if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with reason = [" + reason + "]"); |         if (DEBUG) Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " + | ||||||
|  |                 "reason = [" + reason + "]"); | ||||||
|         // Refresh the playback if there is a transition to the next video |         // Refresh the playback if there is a transition to the next video | ||||||
|         final int newPeriodIndex = simpleExoPlayer.getCurrentPeriodIndex(); |         final int newPeriodIndex = simpleExoPlayer.getCurrentPeriodIndex(); | ||||||
|  |  | ||||||
| @@ -627,39 +742,43 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|                 } else { |                 } else { | ||||||
|                     playQueue.offsetIndex(+1); |                     playQueue.offsetIndex(+1); | ||||||
|                 } |                 } | ||||||
|                 break; |  | ||||||
|             case DISCONTINUITY_REASON_SEEK: |             case DISCONTINUITY_REASON_SEEK: | ||||||
|             case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: |             case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: | ||||||
|             case DISCONTINUITY_REASON_INTERNAL: |             case DISCONTINUITY_REASON_INTERNAL: | ||||||
|             default: |  | ||||||
|                 break; |                 break; | ||||||
|         } |         } | ||||||
|         playbackManager.load(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onRepeatModeChanged(int i) { |     public void onRepeatModeChanged(@Player.RepeatMode final int reason) { | ||||||
|         if (DEBUG) Log.d(TAG, "onRepeatModeChanged() called with: mode = [" + i + "]"); |         if (DEBUG) Log.d(TAG, "ExoPlayer - onRepeatModeChanged() called with: " + | ||||||
|  |                 "mode = [" + reason + "]"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { |     public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) { | ||||||
|         if (DEBUG) Log.d(TAG, "onShuffleModeEnabledChanged() called with: " + |         if (DEBUG) Log.d(TAG, "ExoPlayer - onShuffleModeEnabledChanged() called with: " + | ||||||
|                 "mode = [" + shuffleModeEnabled + "]"); |                 "mode = [" + shuffleModeEnabled + "]"); | ||||||
|  |         if (playQueue == null) return; | ||||||
|  |         if (shuffleModeEnabled) { | ||||||
|  |             playQueue.shuffle(); | ||||||
|  |         } else { | ||||||
|  |             playQueue.unshuffle(); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onSeekProcessed() { |     public void onSeekProcessed() { | ||||||
|         if (DEBUG) Log.d(TAG, "onSeekProcessed() called"); |         if (DEBUG) Log.d(TAG, "ExoPlayer - onSeekProcessed() called"); | ||||||
|     } |     } | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Playback Listener |     // Playback Listener | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void block() { |     public void onPlaybackBlock() { | ||||||
|         if (simpleExoPlayer == null) return; |         if (simpleExoPlayer == null) return; | ||||||
|         if (DEBUG) Log.d(TAG, "Blocking..."); |         if (DEBUG) Log.d(TAG, "Playback - onPlaybackBlock() called"); | ||||||
|  |  | ||||||
|         currentItem = null; |         currentItem = null; | ||||||
|         currentInfo = null; |         currentInfo = null; | ||||||
| @@ -670,44 +789,86 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void unblock(final MediaSource mediaSource) { |     public void onPlaybackUnblock(final MediaSource mediaSource) { | ||||||
|         if (simpleExoPlayer == null) return; |         if (simpleExoPlayer == null) return; | ||||||
|         if (DEBUG) Log.d(TAG, "Unblocking..."); |         if (DEBUG) Log.d(TAG, "Playback - onPlaybackUnblock() called"); | ||||||
|  |  | ||||||
|         if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING); |         if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING); | ||||||
|  |  | ||||||
|         simpleExoPlayer.prepare(mediaSource); |         simpleExoPlayer.prepare(mediaSource); | ||||||
|         simpleExoPlayer.seekToDefaultPosition(); |         seekToDefault(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void sync(@NonNull final PlayQueueItem item, |     public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, | ||||||
|                                       @Nullable final StreamInfo info) { |                                       @Nullable final StreamInfo info) { | ||||||
|         if (currentItem == item && currentInfo == info) return; |         if (DEBUG) Log.d(TAG, "Playback - onPlaybackSynchronize() called with " + | ||||||
|  |                 (info != null ? "available" : "null") + " info, " + | ||||||
|  |                 "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); | ||||||
|  |  | ||||||
|  |         final boolean hasPlayQueueItemChanged = currentItem != item; | ||||||
|  |         final boolean hasStreamInfoChanged = currentInfo != info; | ||||||
|  |         if (!hasPlayQueueItemChanged && !hasStreamInfoChanged) { | ||||||
|  |             return; // Nothing to synchronize | ||||||
|  |         } | ||||||
|  |  | ||||||
|         currentItem = item; |         currentItem = item; | ||||||
|         currentInfo = info; |         currentInfo = info; | ||||||
|  |         if (hasPlayQueueItemChanged) { | ||||||
|         if (DEBUG) Log.d(TAG, "Syncing..."); |             // updates only to the stream info should not trigger another view count | ||||||
|         if (simpleExoPlayer == null) return; |             registerView(); | ||||||
|  |             initThumbnail(info == null ? item.getThumbnailUrl() : info.getThumbnailUrl()); | ||||||
|         // Check if on wrong window |  | ||||||
|         final int currentSourceIndex = playQueue.indexOf(item); |  | ||||||
|         if (currentSourceIndex != playQueue.getIndex()) { |  | ||||||
|             Log.e(TAG, "Play Queue may be desynchronized: item index=[" + currentSourceIndex + |  | ||||||
|                     "], queue index=[" + playQueue.getIndex() + "]"); |  | ||||||
|         } else if (simpleExoPlayer.getCurrentPeriodIndex() != currentSourceIndex || !isPlaying()) { |  | ||||||
|             final long startPos = info != null ? info.start_position : 0; |  | ||||||
|             if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + |  | ||||||
|                     " at: " + getTimeString((int)startPos)); |  | ||||||
|             simpleExoPlayer.seekTo(currentSourceIndex, startPos); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         registerView(); |         final int currentPlayQueueIndex = playQueue.indexOf(item); | ||||||
|         initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url); |         onMetadataChanged(item, info, currentPlayQueueIndex, hasPlayQueueItemChanged); | ||||||
|  |  | ||||||
|  |         if (simpleExoPlayer == null) return; | ||||||
|  |         final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); | ||||||
|  |         // Check if on wrong window | ||||||
|  |         if (currentPlayQueueIndex != playQueue.getIndex()) { | ||||||
|  |             Log.e(TAG, "Play Queue may be desynchronized: item " + | ||||||
|  |                     "index=[" + currentPlayQueueIndex + "], " + | ||||||
|  |                     "queue index=[" + playQueue.getIndex() + "]"); | ||||||
|  |  | ||||||
|  |             // on metadata changed | ||||||
|  |         } else if (currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) { | ||||||
|  |             final long startPos = info != null ? info.start_position : C.TIME_UNSET; | ||||||
|  |             if (DEBUG) Log.d(TAG, "Rewinding to correct" + | ||||||
|  |                     " window=[" + currentPlayQueueIndex + "]," + | ||||||
|  |                     " at=[" + getTimeString((int)startPos) + "]," + | ||||||
|  |                     " from=[" + simpleExoPlayer.getCurrentWindowIndex() + "]."); | ||||||
|  |             simpleExoPlayer.seekTo(currentPlayQueueIndex, startPos); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // when starting playback on the last item when not repeating, maybe auto queue | ||||||
|  |         if (info != null && currentPlayQueueIndex == playQueue.size() - 1 && | ||||||
|  |                 getRepeatMode() == Player.REPEAT_MODE_OFF && | ||||||
|  |                 PlayerHelper.isAutoQueueEnabled(context)) { | ||||||
|  |             final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams()); | ||||||
|  |             if (autoQueue != null) playQueue.append(autoQueue.getStreams()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     abstract protected void onMetadataChanged(@NonNull final PlayQueueItem item, | ||||||
|  |                                               @Nullable final StreamInfo info, | ||||||
|  |                                               final int newPlayQueueIndex, | ||||||
|  |                                               final boolean hasPlayQueueItemChanged); | ||||||
|  |  | ||||||
|  |     @Nullable | ||||||
|  |     @Override | ||||||
|  |     public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { | ||||||
|  |         if (!info.getHlsUrl().isEmpty()) { | ||||||
|  |             return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS); | ||||||
|  |         } else if (!info.getDashMpdUrl().isEmpty()) { | ||||||
|  |             return buildLiveMediaSource(info.getDashMpdUrl(), C.TYPE_DASH); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void shutdown() { |     public void onPlaybackShutdown() { | ||||||
|         if (DEBUG) Log.d(TAG, "Shutting down..."); |         if (DEBUG) Log.d(TAG, "Shutting down..."); | ||||||
|         destroy(); |         destroy(); | ||||||
|     } |     } | ||||||
| @@ -750,8 +911,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|         changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); |         changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public abstract void onUpdateProgress(int currentProgress, int duration, int bufferPercent); |  | ||||||
|  |  | ||||||
|     public void onVideoPlayPause() { |     public void onVideoPlayPause() { | ||||||
|         if (DEBUG) Log.d(TAG, "onVideoPlayPause() called"); |         if (DEBUG) Log.d(TAG, "onVideoPlayPause() called"); | ||||||
|  |  | ||||||
| @@ -763,7 +922,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|  |  | ||||||
|         if (getCurrentState() == STATE_COMPLETED) { |         if (getCurrentState() == STATE_COMPLETED) { | ||||||
|             if (playQueue.getIndex() == 0) { |             if (playQueue.getIndex() == 0) { | ||||||
|                 simpleExoPlayer.seekToDefaultPosition(); |                 seekToDefault(); | ||||||
|             } else { |             } else { | ||||||
|                 playQueue.setIndex(0); |                 playQueue.setIndex(0); | ||||||
|             } |             } | ||||||
| @@ -808,11 +967,13 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void onSelected(final PlayQueueItem item) { |     public void onSelected(final PlayQueueItem item) { | ||||||
|  |         if (playQueue == null || simpleExoPlayer == null) return; | ||||||
|  |  | ||||||
|         final int index = playQueue.indexOf(item); |         final int index = playQueue.indexOf(item); | ||||||
|         if (index == -1) return; |         if (index == -1) return; | ||||||
|  |  | ||||||
|         if (playQueue.getIndex() == index) { |         if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) { | ||||||
|             simpleExoPlayer.seekToDefaultPosition(); |             seekToDefault(); | ||||||
|         } else { |         } else { | ||||||
|             playQueue.setIndex(index); |             playQueue.setIndex(index); | ||||||
|         } |         } | ||||||
| @@ -820,8 +981,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|  |  | ||||||
|     public void seekBy(int milliSeconds) { |     public void seekBy(int milliSeconds) { | ||||||
|         if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]"); |         if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]"); | ||||||
|         if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) |         if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || | ||||||
|  |                 ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) { | ||||||
|             return; |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds); |         int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds); | ||||||
|         if (progress < 0) progress = 0; |         if (progress < 0) progress = 0; | ||||||
|         simpleExoPlayer.seekTo(progress); |         simpleExoPlayer.seekTo(progress); | ||||||
| @@ -832,12 +996,16 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|                 && simpleExoPlayer.getCurrentPosition() >= 0; |                 && simpleExoPlayer.getCurrentPosition() >= 0; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public void seekToDefault() { | ||||||
|  |         if (simpleExoPlayer != null) simpleExoPlayer.seekToDefaultPosition(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Utils |     // Utils | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     private void registerView() { |     private void registerView() { | ||||||
|         if (databaseUpdateReactor == null || recordManager == null || currentInfo == null) return; |         if (databaseUpdateReactor == null || currentInfo == null) return; | ||||||
|         databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete() |         databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete() | ||||||
|                 .subscribe( |                 .subscribe( | ||||||
|                         ignored -> {/* successful */}, |                         ignored -> {/* successful */}, | ||||||
| @@ -852,30 +1020,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     protected void clearThumbnailCache() { |  | ||||||
|         ImageLoader.getInstance().clearMemoryCache(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     protected void startProgressLoop() { |  | ||||||
|         if (progressUpdateReactor != null) progressUpdateReactor.dispose(); |  | ||||||
|         progressUpdateReactor = getProgressReactor(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     protected void stopProgressLoop() { |  | ||||||
|         if (progressUpdateReactor != null) progressUpdateReactor.dispose(); |  | ||||||
|         progressUpdateReactor = null; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void triggerProgressUpdate() { |  | ||||||
|         onUpdateProgress( |  | ||||||
|                 (int) simpleExoPlayer.getCurrentPosition(), |  | ||||||
|                 (int) simpleExoPlayer.getDuration(), |  | ||||||
|                 simpleExoPlayer.getBufferedPercentage() |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     protected void savePlaybackState(final StreamInfo info, final long progress) { |     protected void savePlaybackState(final StreamInfo info, final long progress) { | ||||||
|         if (context == null || info == null || databaseUpdateReactor == null) return; |         if (info == null || databaseUpdateReactor == null) return; | ||||||
|         final Disposable stateSaver = recordManager.saveStreamState(info, progress) |         final Disposable stateSaver = recordManager.saveStreamState(info, progress) | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .onErrorComplete() |                 .onErrorComplete() | ||||||
| @@ -928,14 +1074,17 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     public boolean isPlaying() { |     public boolean isPlaying() { | ||||||
|         return simpleExoPlayer.getPlaybackState() == Player.STATE_READY && simpleExoPlayer.getPlayWhenReady(); |         final int state = simpleExoPlayer.getPlaybackState(); | ||||||
|  |         return (state == Player.STATE_READY || state == Player.STATE_BUFFERING) | ||||||
|  |                 && simpleExoPlayer.getPlayWhenReady(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Player.RepeatMode | ||||||
|     public int getRepeatMode() { |     public int getRepeatMode() { | ||||||
|         return simpleExoPlayer.getRepeatMode(); |         return simpleExoPlayer.getRepeatMode(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void setRepeatMode(final int repeatMode) { |     public void setRepeatMode(@Player.RepeatMode final int repeatMode) { | ||||||
|         simpleExoPlayer.setRepeatMode(repeatMode); |         simpleExoPlayer.setRepeatMode(repeatMode); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -58,6 +58,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; | |||||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | import org.schabi.newpipe.extractor.stream.VideoStream; | ||||||
| import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; | import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; | ||||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | import org.schabi.newpipe.player.helper.PlayerHelper; | ||||||
|  | import org.schabi.newpipe.playlist.PlayQueue; | ||||||
| import org.schabi.newpipe.playlist.PlayQueueItem; | import org.schabi.newpipe.playlist.PlayQueueItem; | ||||||
| import org.schabi.newpipe.playlist.PlayQueueItemBuilder; | import org.schabi.newpipe.playlist.PlayQueueItemBuilder; | ||||||
| import org.schabi.newpipe.playlist.PlayQueueItemHolder; | import org.schabi.newpipe.playlist.PlayQueueItemHolder; | ||||||
| @@ -65,21 +66,27 @@ import org.schabi.newpipe.util.AnimationUtils; | |||||||
| import org.schabi.newpipe.util.ListHelper; | import org.schabi.newpipe.util.ListHelper; | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
| import org.schabi.newpipe.util.PermissionHelper; | import org.schabi.newpipe.util.PermissionHelper; | ||||||
|  | import org.schabi.newpipe.util.StateSaver; | ||||||
| import org.schabi.newpipe.util.ThemeHelper; | import org.schabi.newpipe.util.ThemeHelper; | ||||||
|  |  | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  | import java.util.Queue; | ||||||
|  | import java.util.UUID; | ||||||
|  |  | ||||||
|  | import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; | ||||||
|  | import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; | ||||||
|  | import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; | ||||||
| import static org.schabi.newpipe.util.AnimationUtils.animateView; | import static org.schabi.newpipe.util.AnimationUtils.animateView; | ||||||
|  | import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Activity Player implementing VideoPlayer |  * Activity Player implementing VideoPlayer | ||||||
|  * |  * | ||||||
|  * @author mauriciocolli |  * @author mauriciocolli | ||||||
|  */ |  */ | ||||||
| public final class MainVideoPlayer extends Activity { | public final class MainVideoPlayer extends Activity implements StateSaver.WriteRead { | ||||||
|     private static final String TAG = ".MainVideoPlayer"; |     private static final String TAG = ".MainVideoPlayer"; | ||||||
|     private static final boolean DEBUG = BasePlayer.DEBUG; |     private static final boolean DEBUG = BasePlayer.DEBUG; | ||||||
|     private static final String PLAYER_STATE_INTENT = "player_state_intent"; |  | ||||||
|  |  | ||||||
|     private GestureDetector gestureDetector; |     private GestureDetector gestureDetector; | ||||||
|  |  | ||||||
| @@ -88,6 +95,8 @@ public final class MainVideoPlayer extends Activity { | |||||||
|  |  | ||||||
|     private SharedPreferences defaultPreferences; |     private SharedPreferences defaultPreferences; | ||||||
|  |  | ||||||
|  |     @Nullable private StateSaver.SavedState savedState; | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Activity LifeCycle |     // Activity LifeCycle | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -101,41 +110,28 @@ public final class MainVideoPlayer extends Activity { | |||||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK); |         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK); | ||||||
|         setVolumeControlStream(AudioManager.STREAM_MUSIC); |         setVolumeControlStream(AudioManager.STREAM_MUSIC); | ||||||
|  |  | ||||||
|         final Intent intent; |         changeSystemUi(); | ||||||
|         if (savedInstanceState != null && savedInstanceState.getParcelable(PLAYER_STATE_INTENT) != null) { |  | ||||||
|             intent = savedInstanceState.getParcelable(PLAYER_STATE_INTENT); |  | ||||||
|         } else { |  | ||||||
|             intent = getIntent(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (intent == null) { |  | ||||||
|             Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show(); |  | ||||||
|             finish(); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         showSystemUi(); |  | ||||||
|         setContentView(R.layout.activity_main_player); |         setContentView(R.layout.activity_main_player); | ||||||
|         playerImpl = new VideoPlayerImpl(this); |         playerImpl = new VideoPlayerImpl(this); | ||||||
|         playerImpl.setup(findViewById(android.R.id.content)); |         playerImpl.setup(findViewById(android.R.id.content)); | ||||||
|  |  | ||||||
|  |         if (savedInstanceState != null && savedInstanceState.get(KEY_SAVED_STATE) != null) { | ||||||
|  |             return; // We have saved states, stop here to restore it | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         final Intent intent = getIntent(); | ||||||
|  |         if (intent != null) { | ||||||
|             playerImpl.handleIntent(intent); |             playerImpl.handleIntent(intent); | ||||||
|  |         } else { | ||||||
|  |             Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show(); | ||||||
|  |             finish(); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     protected void onSaveInstanceState(Bundle outState) { |     protected void onRestoreInstanceState(@NonNull Bundle bundle) { | ||||||
|         super.onSaveInstanceState(outState); |         super.onRestoreInstanceState(bundle); | ||||||
|         if (this.playerImpl == null) return; |         savedState = StateSaver.tryToRestore(bundle, this); | ||||||
|  |  | ||||||
|         final Intent intent = NavigationHelper.getPlayerIntent( |  | ||||||
|                 getApplicationContext(), |  | ||||||
|                 this.getClass(), |  | ||||||
|                 this.playerImpl.getPlayQueue(), |  | ||||||
|                 this.playerImpl.getRepeatMode(), |  | ||||||
|                 this.playerImpl.getPlaybackSpeed(), |  | ||||||
|                 this.playerImpl.getPlaybackPitch(), |  | ||||||
|                 this.playerImpl.getPlaybackQuality() |  | ||||||
|         ); |  | ||||||
|         outState.putParcelable(PLAYER_STATE_INTENT, intent); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -145,6 +141,23 @@ public final class MainVideoPlayer extends Activity { | |||||||
|         playerImpl.handleIntent(intent); |         playerImpl.handleIntent(intent); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void onResume() { | ||||||
|  |         super.onResume(); | ||||||
|  |         if (DEBUG) Log.d(TAG, "onResume() called"); | ||||||
|  |         if (playerImpl.getPlayer() != null && activityPaused && playerImpl.wasPlaying() | ||||||
|  |                 && !playerImpl.isPlaying()) { | ||||||
|  |             playerImpl.onVideoPlayPause(); | ||||||
|  |         } | ||||||
|  |         activityPaused = false; | ||||||
|  |  | ||||||
|  |         if(globalScreenOrientationLocked()) { | ||||||
|  |             boolean lastOrientationWasLandscape | ||||||
|  |                     = defaultPreferences.getBoolean(getString(R.string.last_orientation_landscape_key), false); | ||||||
|  |             setLandscape(lastOrientationWasLandscape); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onBackPressed() { |     public void onBackPressed() { | ||||||
|         if (DEBUG) Log.d(TAG, "onBackPressed() called"); |         if (DEBUG) Log.d(TAG, "onBackPressed() called"); | ||||||
| @@ -152,46 +165,6 @@ public final class MainVideoPlayer extends Activity { | |||||||
|         if (playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(false); |         if (playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(false); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected void onStop() { |  | ||||||
|         super.onStop(); |  | ||||||
|         if (DEBUG) Log.d(TAG, "onStop() called"); |  | ||||||
|         activityPaused = true; |  | ||||||
|  |  | ||||||
|         if (playerImpl.getPlayer() != null) { |  | ||||||
|             playerImpl.wasPlaying = playerImpl.getPlayer().getPlayWhenReady(); |  | ||||||
|             playerImpl.setRecovery(); |  | ||||||
|             playerImpl.destroyPlayer(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected void onResume() { |  | ||||||
|         super.onResume(); |  | ||||||
|         if (DEBUG) Log.d(TAG, "onResume() called"); |  | ||||||
|         if (activityPaused) { |  | ||||||
|             playerImpl.initPlayer(); |  | ||||||
|             playerImpl.getPlayPauseButton().setImageResource(R.drawable.ic_play_arrow_white); |  | ||||||
|  |  | ||||||
|             playerImpl.getPlayer().setPlayWhenReady(playerImpl.wasPlaying); |  | ||||||
|             playerImpl.initPlayback(playerImpl.playQueue); |  | ||||||
|  |  | ||||||
|             activityPaused = false; |  | ||||||
|         } |  | ||||||
|         if(globalScreenOrientationLocked()) { |  | ||||||
|             boolean lastOrientationWasLandscape |  | ||||||
|                     = defaultPreferences.getBoolean(getString(R.string.last_orientation_landscape_key), false); |  | ||||||
|             setLandScape(lastOrientationWasLandscape); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected void onDestroy() { |  | ||||||
|         super.onDestroy(); |  | ||||||
|         if (DEBUG) Log.d(TAG, "onDestroy() called"); |  | ||||||
|         if (playerImpl != null) playerImpl.destroy(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onConfigurationChanged(Configuration newConfig) { |     public void onConfigurationChanged(Configuration newConfig) { | ||||||
|         super.onConfigurationChanged(newConfig); |         super.onConfigurationChanged(newConfig); | ||||||
| @@ -202,49 +175,134 @@ public final class MainVideoPlayer extends Activity { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void onPause() { | ||||||
|  |         super.onPause(); | ||||||
|  |         if (DEBUG) Log.d(TAG, "onPause() called"); | ||||||
|  |  | ||||||
|  |         if (playerImpl != null && playerImpl.getPlayer() != null && !activityPaused) { | ||||||
|  |             playerImpl.wasPlaying = playerImpl.isPlaying(); | ||||||
|  |             if (playerImpl.isPlaying()) playerImpl.onVideoPlayPause(); | ||||||
|  |         } | ||||||
|  |         activityPaused = true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void onSaveInstanceState(Bundle outState) { | ||||||
|  |         super.onSaveInstanceState(outState); | ||||||
|  |         if (playerImpl == null) return; | ||||||
|  |  | ||||||
|  |         playerImpl.setRecovery(); | ||||||
|  |         savedState = StateSaver.tryToSave(isChangingConfigurations(), savedState, | ||||||
|  |                 outState, this); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void onDestroy() { | ||||||
|  |         super.onDestroy(); | ||||||
|  |         if (DEBUG) Log.d(TAG, "onDestroy() called"); | ||||||
|  |         if (playerImpl != null) playerImpl.destroy(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Utils |     // State Saving | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public String generateSuffix() { | ||||||
|  |         return "." + UUID.randomUUID().toString() + ".player"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void writeTo(Queue<Object> objectsToSave) { | ||||||
|  |         if (objectsToSave == null) return; | ||||||
|  |         objectsToSave.add(playerImpl.getPlayQueue()); | ||||||
|  |         objectsToSave.add(playerImpl.getRepeatMode()); | ||||||
|  |         objectsToSave.add(playerImpl.getPlaybackSpeed()); | ||||||
|  |         objectsToSave.add(playerImpl.getPlaybackPitch()); | ||||||
|  |         objectsToSave.add(playerImpl.getPlaybackQuality()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     @SuppressWarnings("unchecked") | ||||||
|  |     public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception { | ||||||
|  |         @NonNull final PlayQueue queue = (PlayQueue) savedObjects.poll(); | ||||||
|  |         final int repeatMode = (int) savedObjects.poll(); | ||||||
|  |         final float playbackSpeed = (float) savedObjects.poll(); | ||||||
|  |         final float playbackPitch = (float) savedObjects.poll(); | ||||||
|  |         @NonNull final String playbackQuality = (String) savedObjects.poll(); | ||||||
|  |  | ||||||
|  |         playerImpl.setPlaybackQuality(playbackQuality); | ||||||
|  |         playerImpl.initPlayback(queue, repeatMode, playbackSpeed, playbackPitch); | ||||||
|  |  | ||||||
|  |         StateSaver.onDestroy(savedState); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // View | ||||||
|  |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Prior to Kitkat, hiding system ui causes the player view to be overlaid and require two | ||||||
|  |      * clicks to get rid of that invisible overlay. By showing the system UI on actions/events, | ||||||
|  |      * that overlay is removed and the player view is put to the foreground. | ||||||
|  |      * | ||||||
|  |      * Post Kitkat, navbar and status bar can be pulled out by swiping the edge of | ||||||
|  |      * screen, therefore, we can do nothing or hide the UI on actions/events. | ||||||
|  |      * */ | ||||||
|  |     private void changeSystemUi() { | ||||||
|  |         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { | ||||||
|  |             showSystemUi(); | ||||||
|  |         } else { | ||||||
|  |             hideSystemUi(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     private void showSystemUi() { |     private void showSystemUi() { | ||||||
|         if (DEBUG) Log.d(TAG, "showSystemUi() called"); |         if (DEBUG) Log.d(TAG, "showSystemUi() called"); | ||||||
|         if (playerImpl != null && playerImpl.queueVisible) return; |         if (playerImpl != null && playerImpl.queueVisible) return; | ||||||
|  |  | ||||||
|  |         final int visibility; | ||||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { |         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { | ||||||
|             getWindow().getDecorView().setSystemUiVisibility( |             visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | ||||||
|                     View.SYSTEM_UI_FLAG_LAYOUT_STABLE |  | ||||||
|                     | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |                     | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | ||||||
|                             | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |                     | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; | ||||||
|             ); |         } else { | ||||||
|         } else getWindow().getDecorView().setSystemUiVisibility(0); |             visibility = View.STATUS_BAR_VISIBLE; | ||||||
|  |         } | ||||||
|  |         getWindow().getDecorView().setSystemUiVisibility(visibility); | ||||||
|         getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); |         getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void hideSystemUi() { |     private void hideSystemUi() { | ||||||
|         if (DEBUG) Log.d(TAG, "hideSystemUi() called"); |         if (DEBUG) Log.d(TAG, "hideSystemUi() called"); | ||||||
|         if (android.os.Build.VERSION.SDK_INT >= 16) { |         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { | ||||||
|             int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE |             int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | ||||||
|                     | View.SYSTEM_UI_FLAG_FULLSCREEN |                     | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | ||||||
|                     | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |  | ||||||
|                     | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |                     | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | ||||||
|                     | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; |                     | View.SYSTEM_UI_FLAG_FULLSCREEN | ||||||
|             if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) visibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; |                     | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; | ||||||
|  |             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { | ||||||
|  |                 visibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; | ||||||
|  |             } | ||||||
|             getWindow().getDecorView().setSystemUiVisibility(visibility); |             getWindow().getDecorView().setSystemUiVisibility(visibility); | ||||||
|         } |         } | ||||||
|         getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); |         getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, | ||||||
|  |                 WindowManager.LayoutParams.FLAG_FULLSCREEN); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void toggleOrientation() { |     private void toggleOrientation() { | ||||||
|         setLandScape(!isLandScape()); |         setLandscape(!isLandscape()); | ||||||
|         defaultPreferences.edit() |         defaultPreferences.edit() | ||||||
|                 .putBoolean(getString(R.string.last_orientation_landscape_key), !isLandScape()) |                 .putBoolean(getString(R.string.last_orientation_landscape_key), !isLandscape()) | ||||||
|                 .apply(); |                 .apply(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private boolean isLandScape() { |     private boolean isLandscape() { | ||||||
|         return getResources().getDisplayMetrics().heightPixels < getResources().getDisplayMetrics().widthPixels; |         return getResources().getDisplayMetrics().heightPixels < getResources().getDisplayMetrics().widthPixels; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void setLandScape(boolean v) { |     private void setLandscape(boolean v) { | ||||||
|         setRequestedOrientation(v |         setRequestedOrientation(v | ||||||
|                 ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE |                 ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE | ||||||
|                 : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); |                 : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); | ||||||
| @@ -307,6 +365,7 @@ public final class MainVideoPlayer extends Activity { | |||||||
|         private ImageButton switchPopupButton; |         private ImageButton switchPopupButton; | ||||||
|         private ImageButton switchBackgroundButton; |         private ImageButton switchBackgroundButton; | ||||||
|  |  | ||||||
|  |         private RelativeLayout windowRootLayout; | ||||||
|         private View secondaryControls; |         private View secondaryControls; | ||||||
|  |  | ||||||
|         VideoPlayerImpl(final Context context) { |         VideoPlayerImpl(final Context context) { | ||||||
| @@ -334,6 +393,19 @@ public final class MainVideoPlayer extends Activity { | |||||||
|             this.switchBackgroundButton = rootView.findViewById(R.id.switchBackground); |             this.switchBackgroundButton = rootView.findViewById(R.id.switchBackground); | ||||||
|             this.switchPopupButton = rootView.findViewById(R.id.switchPopup); |             this.switchPopupButton = rootView.findViewById(R.id.switchPopup); | ||||||
|  |  | ||||||
|  |             this.queueLayout = findViewById(R.id.playQueuePanel); | ||||||
|  |             this.itemsListCloseButton = findViewById(R.id.playQueueClose); | ||||||
|  |             this.itemsList = findViewById(R.id.playQueue); | ||||||
|  |  | ||||||
|  |             this.windowRootLayout = rootView.findViewById(R.id.playbackWindowRoot); | ||||||
|  |             // Prior to Kitkat, there is no way of setting translucent navbar programmatically. | ||||||
|  |             // Thus, fit system windows is opted instead. | ||||||
|  |             // See https://stackoverflow.com/questions/29069070/completely-transparent-status-bar-and-navigation-bar-on-lollipop | ||||||
|  |             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { | ||||||
|  |                 windowRootLayout.setFitsSystemWindows(false); | ||||||
|  |                 windowRootLayout.invalidate(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|             titleTextView.setSelected(true); |             titleTextView.setSelected(true); | ||||||
|             channelTextView.setSelected(true); |             channelTextView.setSelected(true); | ||||||
|  |  | ||||||
| @@ -391,31 +463,32 @@ public final class MainVideoPlayer extends Activity { | |||||||
|             updatePlaybackButtons(); |             updatePlaybackButtons(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         /*////////////////////////////////////////////////////////////////////////// |  | ||||||
|         // Playback Listener |  | ||||||
|         //////////////////////////////////////////////////////////////////////////*/ |  | ||||||
|  |  | ||||||
|         @Override |  | ||||||
|         public void shutdown() { |  | ||||||
|             super.shutdown(); |  | ||||||
|             finish(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         @Override |  | ||||||
|         public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) { |  | ||||||
|             super.sync(item, info); |  | ||||||
|             titleTextView.setText(getVideoTitle()); |  | ||||||
|             channelTextView.setText(getUploaderName()); |  | ||||||
|  |  | ||||||
|             //playPauseButton.setImageResource(R.drawable.ic_pause_white); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public void onShuffleClicked() { |         public void onShuffleClicked() { | ||||||
|             super.onShuffleClicked(); |             super.onShuffleClicked(); | ||||||
|             updatePlaybackButtons(); |             updatePlaybackButtons(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /*////////////////////////////////////////////////////////////////////////// | ||||||
|  |         // Playback Listener | ||||||
|  |         //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |         protected void onMetadataChanged(@NonNull final PlayQueueItem item, | ||||||
|  |                                          @Nullable final StreamInfo info, | ||||||
|  |                                          final int newPlayQueueIndex, | ||||||
|  |                                          final boolean hasPlayQueueItemChanged) { | ||||||
|  |             super.onMetadataChanged(item, info, newPlayQueueIndex, false); | ||||||
|  |  | ||||||
|  |             titleTextView.setText(getVideoTitle()); | ||||||
|  |             channelTextView.setText(getUploaderName()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @Override | ||||||
|  |         public void onPlaybackShutdown() { | ||||||
|  |             super.onPlaybackShutdown(); | ||||||
|  |             finish(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         /*////////////////////////////////////////////////////////////////////////// |         /*////////////////////////////////////////////////////////////////////////// | ||||||
|         // Player Overrides |         // Player Overrides | ||||||
|         //////////////////////////////////////////////////////////////////////////*/ |         //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -508,9 +581,9 @@ public final class MainVideoPlayer extends Activity { | |||||||
|  |  | ||||||
|             if (getCurrentState() != STATE_COMPLETED) { |             if (getCurrentState() != STATE_COMPLETED) { | ||||||
|                 getControlsVisibilityHandler().removeCallbacksAndMessages(null); |                 getControlsVisibilityHandler().removeCallbacksAndMessages(null); | ||||||
|                 animateView(getControlsRoot(), true, 300, 0, () -> { |                 animateView(getControlsRoot(), true, DEFAULT_CONTROLS_DURATION, 0, () -> { | ||||||
|                     if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) { |                     if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) { | ||||||
|                         hideControls(300, DEFAULT_CONTROLS_HIDE_TIME); |                         hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); | ||||||
|                     } |                     } | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
| @@ -546,7 +619,7 @@ public final class MainVideoPlayer extends Activity { | |||||||
|                         R.drawable.ic_expand_less_white_24dp)); |                         R.drawable.ic_expand_less_white_24dp)); | ||||||
|                 animateView(secondaryControls, true, 200); |                 animateView(secondaryControls, true, 200); | ||||||
|             } |             } | ||||||
|             showControls(300); |             showControls(DEFAULT_CONTROLS_DURATION); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         private void onScreenRotationClicked() { |         private void onScreenRotationClicked() { | ||||||
| @@ -558,15 +631,13 @@ public final class MainVideoPlayer extends Activity { | |||||||
|         @Override |         @Override | ||||||
|         public void onStopTrackingTouch(SeekBar seekBar) { |         public void onStopTrackingTouch(SeekBar seekBar) { | ||||||
|             super.onStopTrackingTouch(seekBar); |             super.onStopTrackingTouch(seekBar); | ||||||
|             if (wasPlaying()) { |             if (wasPlaying()) showControlsThenHide(); | ||||||
|                 hideControls(100, 0); |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public void onDismiss(PopupMenu menu) { |         public void onDismiss(PopupMenu menu) { | ||||||
|             super.onDismiss(menu); |             super.onDismiss(menu); | ||||||
|             if (isPlaying()) hideControls(300, 0); |             if (isPlaying()) hideControls(DEFAULT_CONTROLS_DURATION, 0); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
| @@ -624,7 +695,8 @@ public final class MainVideoPlayer extends Activity { | |||||||
|                 playPauseButton.setImageResource(R.drawable.ic_pause_white); |                 playPauseButton.setImageResource(R.drawable.ic_pause_white); | ||||||
|                 animatePlayButtons(true, 200); |                 animatePlayButtons(true, 200); | ||||||
|             }); |             }); | ||||||
|             showSystemUi(); |  | ||||||
|  |             changeSystemUi(); | ||||||
|             getRootView().setKeepScreenOn(true); |             getRootView().setKeepScreenOn(true); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -636,7 +708,7 @@ public final class MainVideoPlayer extends Activity { | |||||||
|                 animatePlayButtons(true, 200); |                 animatePlayButtons(true, 200); | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|             showSystemUi(); |             changeSystemUi(); | ||||||
|             getRootView().setKeepScreenOn(false); |             getRootView().setKeepScreenOn(false); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -650,10 +722,9 @@ public final class MainVideoPlayer extends Activity { | |||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public void onCompleted() { |         public void onCompleted() { | ||||||
|             showSystemUi(); |  | ||||||
|             animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> { |             animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> { | ||||||
|                 playPauseButton.setImageResource(R.drawable.ic_replay_white); |                 playPauseButton.setImageResource(R.drawable.ic_replay_white); | ||||||
|                 animatePlayButtons(true, 300); |                 animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|             getRootView().setKeepScreenOn(false); |             getRootView().setKeepScreenOn(false); | ||||||
| @@ -683,8 +754,9 @@ public final class MainVideoPlayer extends Activity { | |||||||
|             if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); |             if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); | ||||||
|             getControlsVisibilityHandler().removeCallbacksAndMessages(null); |             getControlsVisibilityHandler().removeCallbacksAndMessages(null); | ||||||
|             getControlsVisibilityHandler().postDelayed(() -> |             getControlsVisibilityHandler().postDelayed(() -> | ||||||
|                     animateView(getControlsRoot(), false, duration, 0, MainVideoPlayer.this::hideSystemUi), |                     animateView(getControlsRoot(), false, duration, 0, | ||||||
|                     delay |                             MainVideoPlayer.this::hideSystemUi), | ||||||
|  |                     /*delayMillis=*/delay | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -697,11 +769,6 @@ public final class MainVideoPlayer extends Activity { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         private void buildQueue() { |         private void buildQueue() { | ||||||
|             queueLayout = findViewById(R.id.playQueuePanel); |  | ||||||
|  |  | ||||||
|             itemsListCloseButton = findViewById(R.id.playQueueClose); |  | ||||||
|  |  | ||||||
|             itemsList = findViewById(R.id.playQueue); |  | ||||||
|             itemsList.setAdapter(playQueueAdapter); |             itemsList.setAdapter(playQueueAdapter); | ||||||
|             itemsList.setClickable(true); |             itemsList.setClickable(true); | ||||||
|             itemsList.setLongClickable(true); |             itemsList.setLongClickable(true); | ||||||
| @@ -830,14 +897,22 @@ public final class MainVideoPlayer extends Activity { | |||||||
|             if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); |             if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); | ||||||
|             if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) return true; |             if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) return true; | ||||||
|  |  | ||||||
|             if (playerImpl.isControlsVisible()) playerImpl.hideControls(150, 0); |             if (playerImpl.isControlsVisible()) { | ||||||
|             else { |                 playerImpl.hideControls(150, 0); | ||||||
|  |             } else { | ||||||
|                 playerImpl.showControlsThenHide(); |                 playerImpl.showControlsThenHide(); | ||||||
|                 showSystemUi(); |                 changeSystemUi(); | ||||||
|             } |             } | ||||||
|             return true; |             return true; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         @Override | ||||||
|  |         public boolean onDown(MotionEvent e) { | ||||||
|  |             if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]"); | ||||||
|  |  | ||||||
|  |             return super.onDown(e); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         private final boolean isPlayerGestureEnabled = PlayerHelper.isPlayerGestureEnabled(getApplicationContext()); |         private final boolean isPlayerGestureEnabled = PlayerHelper.isPlayerGestureEnabled(getApplicationContext()); | ||||||
|  |  | ||||||
|         private final float stepsBrightness = 15, stepBrightness = (1f / stepsBrightness), minBrightness = .01f; |         private final float stepsBrightness = 15, stepBrightness = (1f / stepsBrightness), minBrightness = .01f; | ||||||
| @@ -916,11 +991,15 @@ public final class MainVideoPlayer extends Activity { | |||||||
|             eventsNum = 0; |             eventsNum = 0; | ||||||
|             /* if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE); |             /* if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE); | ||||||
|             if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE);*/ |             if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE);*/ | ||||||
|             if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) animateView(playerImpl.getVolumeTextView(), false, 200, 200); |             if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) { | ||||||
|             if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) animateView(playerImpl.getBrightnessTextView(), false, 200, 200); |                 animateView(playerImpl.getVolumeTextView(), false, 200, 200); | ||||||
|  |             } | ||||||
|  |             if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) { | ||||||
|  |                 animateView(playerImpl.getBrightnessTextView(), false, 200, 200); | ||||||
|  |             } | ||||||
|  |  | ||||||
|             if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == BasePlayer.STATE_PLAYING) { |             if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { | ||||||
|                 playerImpl.hideControls(300, VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME); |                 playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -70,6 +70,9 @@ import org.schabi.newpipe.util.ThemeHelper; | |||||||
|  |  | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  |  | ||||||
|  | import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; | ||||||
|  | import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; | ||||||
|  | import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; | ||||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.isUsingOldPlayer; | import static org.schabi.newpipe.player.helper.PlayerHelper.isUsingOldPlayer; | ||||||
| import static org.schabi.newpipe.util.AnimationUtils.animateView; | import static org.schabi.newpipe.util.AnimationUtils.animateView; | ||||||
|  |  | ||||||
| @@ -419,13 +422,15 @@ public final class PopupVideoPlayer extends Service { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public void onThumbnailReceived(Bitmap thumbnail) { |         public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { | ||||||
|             super.onThumbnailReceived(thumbnail); |             super.onLoadingComplete(imageUri, view, loadedImage); | ||||||
|             if (thumbnail != null) { |             if (loadedImage != null) { | ||||||
|                 // rebuild notification here since remote view does not release bitmaps, causing memory leaks |                 // rebuild notification here since remote view does not release bitmaps, causing memory leaks | ||||||
|                 notBuilder = createNotification(); |                 notBuilder = createNotification(); | ||||||
|  |  | ||||||
|                 if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail); |                 if (notRemoteView != null) { | ||||||
|  |                     notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|                 updateNotification(-1); |                 updateNotification(-1); | ||||||
|             } |             } | ||||||
| @@ -533,7 +538,8 @@ public final class PopupVideoPlayer extends Service { | |||||||
|  |  | ||||||
|         private void updatePlayback() { |         private void updatePlayback() { | ||||||
|             if (activityListener != null && simpleExoPlayer != null && playQueue != null) { |             if (activityListener != null && simpleExoPlayer != null && playQueue != null) { | ||||||
|                 activityListener.onPlaybackUpdate(currentState, getRepeatMode(), playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters()); |                 activityListener.onPlaybackUpdate(currentState, getRepeatMode(), | ||||||
|  |                         playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters()); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -572,16 +578,17 @@ public final class PopupVideoPlayer extends Service { | |||||||
|         // Playback Listener |         // Playback Listener | ||||||
|         //////////////////////////////////////////////////////////////////////////*/ |         //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|         @Override |         protected void onMetadataChanged(@NonNull final PlayQueueItem item, | ||||||
|         public void sync(@NonNull PlayQueueItem item, @Nullable StreamInfo info) { |                                          @Nullable final StreamInfo info, | ||||||
|             if (currentItem == item && currentInfo == info) return; |                                          final int newPlayQueueIndex, | ||||||
|             super.sync(item, info); |                                          final boolean hasPlayQueueItemChanged) { | ||||||
|  |             super.onMetadataChanged(item, info, newPlayQueueIndex, false); | ||||||
|             updateMetadata(); |             updateMetadata(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public void shutdown() { |         public void onPlaybackShutdown() { | ||||||
|             super.shutdown(); |             super.onPlaybackShutdown(); | ||||||
|             onClose(); |             onClose(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -646,6 +653,8 @@ public final class PopupVideoPlayer extends Service { | |||||||
|             super.onPlaying(); |             super.onPlaying(); | ||||||
|             updateNotification(R.drawable.ic_pause_white); |             updateNotification(R.drawable.ic_pause_white); | ||||||
|             lockManager.acquireWifiAndCpu(); |             lockManager.acquireWifiAndCpu(); | ||||||
|  |  | ||||||
|  |             hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
| @@ -778,8 +787,8 @@ public final class PopupVideoPlayer extends Service { | |||||||
|         private void onScrollEnd() { |         private void onScrollEnd() { | ||||||
|             if (DEBUG) Log.d(TAG, "onScrollEnd() called"); |             if (DEBUG) Log.d(TAG, "onScrollEnd() called"); | ||||||
|             if (playerImpl == null) return; |             if (playerImpl == null) return; | ||||||
|             if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == BasePlayer.STATE_PLAYING) { |             if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { | ||||||
|                 playerImpl.hideControls(300, VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME); |                 playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -76,6 +76,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | |||||||
|     private SeekBar progressSeekBar; |     private SeekBar progressSeekBar; | ||||||
|     private TextView progressCurrentTime; |     private TextView progressCurrentTime; | ||||||
|     private TextView progressEndTime; |     private TextView progressEndTime; | ||||||
|  |     private TextView progressLiveSync; | ||||||
|     private TextView seekDisplay; |     private TextView seekDisplay; | ||||||
|  |  | ||||||
|     private ImageButton repeatButton; |     private ImageButton repeatButton; | ||||||
| @@ -294,9 +295,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | |||||||
|         progressCurrentTime = rootView.findViewById(R.id.current_time); |         progressCurrentTime = rootView.findViewById(R.id.current_time); | ||||||
|         progressSeekBar = rootView.findViewById(R.id.seek_bar); |         progressSeekBar = rootView.findViewById(R.id.seek_bar); | ||||||
|         progressEndTime = rootView.findViewById(R.id.end_time); |         progressEndTime = rootView.findViewById(R.id.end_time); | ||||||
|  |         progressLiveSync = rootView.findViewById(R.id.live_sync); | ||||||
|         seekDisplay = rootView.findViewById(R.id.seek_display); |         seekDisplay = rootView.findViewById(R.id.seek_display); | ||||||
|  |  | ||||||
|         progressSeekBar.setOnSeekBarChangeListener(this); |         progressSeekBar.setOnSeekBarChangeListener(this); | ||||||
|  |         progressLiveSync.setOnClickListener(this); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void buildControls() { |     private void buildControls() { | ||||||
| @@ -513,6 +516,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | |||||||
|         } else if (view.getId() == metadata.getId()) { |         } else if (view.getId() == metadata.getId()) { | ||||||
|             scrollToSelected(); |             scrollToSelected(); | ||||||
|  |  | ||||||
|  |         } else if (view.getId() == progressLiveSync.getId()) { | ||||||
|  |             player.seekToDefault(); | ||||||
|  |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -574,6 +580,19 @@ public abstract class ServicePlayerActivity extends AppCompatActivity | |||||||
|         if (info != null) { |         if (info != null) { | ||||||
|             metadataTitle.setText(info.getName()); |             metadataTitle.setText(info.getName()); | ||||||
|             metadataArtist.setText(info.uploader_name); |             metadataArtist.setText(info.uploader_name); | ||||||
|  |  | ||||||
|  |             progressEndTime.setVisibility(View.GONE); | ||||||
|  |             progressLiveSync.setVisibility(View.GONE); | ||||||
|  |             switch (info.getStreamType()) { | ||||||
|  |                 case LIVE_STREAM: | ||||||
|  |                 case AUDIO_LIVE_STREAM: | ||||||
|  |                     progressLiveSync.setVisibility(View.VISIBLE); | ||||||
|  |                     break; | ||||||
|  |                 default: | ||||||
|  |                     progressEndTime.setVisibility(View.VISIBLE); | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  |  | ||||||
|             scrollToSelected(); |             scrollToSelected(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -50,21 +50,21 @@ import android.widget.TextView; | |||||||
| import com.google.android.exoplayer2.C; | import com.google.android.exoplayer2.C; | ||||||
| import com.google.android.exoplayer2.Format; | import com.google.android.exoplayer2.Format; | ||||||
| import com.google.android.exoplayer2.Player; | import com.google.android.exoplayer2.Player; | ||||||
| import com.google.android.exoplayer2.SimpleExoPlayer; |  | ||||||
| import com.google.android.exoplayer2.source.MediaSource; | import com.google.android.exoplayer2.source.MediaSource; | ||||||
| import com.google.android.exoplayer2.source.MergingMediaSource; | import com.google.android.exoplayer2.source.MergingMediaSource; | ||||||
| import com.google.android.exoplayer2.source.SingleSampleMediaSource; |  | ||||||
| import com.google.android.exoplayer2.source.TrackGroup; | import com.google.android.exoplayer2.source.TrackGroup; | ||||||
| import com.google.android.exoplayer2.source.TrackGroupArray; | import com.google.android.exoplayer2.source.TrackGroupArray; | ||||||
| import com.google.android.exoplayer2.trackselection.TrackSelectionArray; | import com.google.android.exoplayer2.trackselection.TrackSelectionArray; | ||||||
| import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; | import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; | ||||||
| import com.google.android.exoplayer2.ui.SubtitleView; | import com.google.android.exoplayer2.ui.SubtitleView; | ||||||
|  | import com.google.android.exoplayer2.video.VideoListener; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.extractor.MediaFormat; | import org.schabi.newpipe.extractor.MediaFormat; | ||||||
| import org.schabi.newpipe.extractor.Subtitles; | import org.schabi.newpipe.extractor.Subtitles; | ||||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | import org.schabi.newpipe.extractor.stream.AudioStream; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||||
|  | import org.schabi.newpipe.extractor.stream.StreamType; | ||||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | import org.schabi.newpipe.extractor.stream.VideoStream; | ||||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | import org.schabi.newpipe.player.helper.PlayerHelper; | ||||||
| import org.schabi.newpipe.playlist.PlayQueueItem; | import org.schabi.newpipe.playlist.PlayQueueItem; | ||||||
| @@ -87,7 +87,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView; | |||||||
|  */ |  */ | ||||||
| @SuppressWarnings({"WeakerAccess", "unused"}) | @SuppressWarnings({"WeakerAccess", "unused"}) | ||||||
| public abstract class VideoPlayer extends BasePlayer | public abstract class VideoPlayer extends BasePlayer | ||||||
|         implements SimpleExoPlayer.VideoListener, |         implements VideoListener, | ||||||
|         SeekBar.OnSeekBarChangeListener, |         SeekBar.OnSeekBarChangeListener, | ||||||
|         View.OnClickListener, |         View.OnClickListener, | ||||||
|         Player.EventListener, |         Player.EventListener, | ||||||
| @@ -101,6 +101,7 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     protected static final int RENDERER_UNAVAILABLE = -1; |     protected static final int RENDERER_UNAVAILABLE = -1; | ||||||
|  |     public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis | ||||||
|     public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000;  // 2 Seconds |     public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000;  // 2 Seconds | ||||||
|  |  | ||||||
|     private ArrayList<VideoStream> availableStreams; |     private ArrayList<VideoStream> availableStreams; | ||||||
| @@ -131,6 +132,7 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|     private SeekBar playbackSeekBar; |     private SeekBar playbackSeekBar; | ||||||
|     private TextView playbackCurrentTime; |     private TextView playbackCurrentTime; | ||||||
|     private TextView playbackEndTime; |     private TextView playbackEndTime; | ||||||
|  |     private TextView playbackLiveSync; | ||||||
|     private TextView playbackSpeedTextView; |     private TextView playbackSpeedTextView; | ||||||
|  |  | ||||||
|     private View topControlsRoot; |     private View topControlsRoot; | ||||||
| @@ -159,7 +161,6 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|     public VideoPlayer(String debugTag, Context context) { |     public VideoPlayer(String debugTag, Context context) { | ||||||
|         super(context); |         super(context); | ||||||
|         this.TAG = debugTag; |         this.TAG = debugTag; | ||||||
|         this.context = context; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void setup(View rootView) { |     public void setup(View rootView) { | ||||||
| @@ -180,6 +181,7 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|         this.playbackSeekBar = rootView.findViewById(R.id.playbackSeekBar); |         this.playbackSeekBar = rootView.findViewById(R.id.playbackSeekBar); | ||||||
|         this.playbackCurrentTime = rootView.findViewById(R.id.playbackCurrentTime); |         this.playbackCurrentTime = rootView.findViewById(R.id.playbackCurrentTime); | ||||||
|         this.playbackEndTime = rootView.findViewById(R.id.playbackEndTime); |         this.playbackEndTime = rootView.findViewById(R.id.playbackEndTime); | ||||||
|  |         this.playbackLiveSync = rootView.findViewById(R.id.playbackLiveSync); | ||||||
|         this.playbackSpeedTextView = rootView.findViewById(R.id.playbackSpeed); |         this.playbackSpeedTextView = rootView.findViewById(R.id.playbackSpeed); | ||||||
|         this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls); |         this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls); | ||||||
|         this.topControlsRoot = rootView.findViewById(R.id.topControls); |         this.topControlsRoot = rootView.findViewById(R.id.topControls); | ||||||
| @@ -221,6 +223,7 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|         qualityTextView.setOnClickListener(this); |         qualityTextView.setOnClickListener(this); | ||||||
|         captionTextView.setOnClickListener(this); |         captionTextView.setOnClickListener(this); | ||||||
|         resizeView.setOnClickListener(this); |         resizeView.setOnClickListener(this); | ||||||
|  |         playbackLiveSync.setOnClickListener(this); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -261,7 +264,8 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|         qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId); |         qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId); | ||||||
|         for (int i = 0; i < availableStreams.size(); i++) { |         for (int i = 0; i < availableStreams.size(); i++) { | ||||||
|             VideoStream videoStream = availableStreams.get(i); |             VideoStream videoStream = availableStreams.get(i); | ||||||
|             qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution); |             qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, | ||||||
|  |                     MediaFormat.getNameById(videoStream.getFormatId()) + " " + videoStream.resolution); | ||||||
|         } |         } | ||||||
|         if (getSelectedVideoStream() != null) { |         if (getSelectedVideoStream() != null) { | ||||||
|             qualityTextView.setText(getSelectedVideoStream().resolution); |             qualityTextView.setText(getSelectedVideoStream().resolution); | ||||||
| @@ -305,8 +309,7 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|             captionItem.setOnMenuItemClickListener(menuItem -> { |             captionItem.setOnMenuItemClickListener(menuItem -> { | ||||||
|                 final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); |                 final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); | ||||||
|                 if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) { |                 if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) { | ||||||
|                     trackSelector.setParameters(trackSelector.getParameters() |                     trackSelector.setPreferredTextLanguage(captionLanguage); | ||||||
|                             .withPreferredTextLanguage(captionLanguage)); |  | ||||||
|                     trackSelector.setRendererDisabled(textRendererIndex, false); |                     trackSelector.setRendererDisabled(textRendererIndex, false); | ||||||
|                 } |                 } | ||||||
|                 return true; |                 return true; | ||||||
| @@ -322,13 +325,37 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|  |  | ||||||
|     protected abstract int getOverrideResolutionIndex(final List<VideoStream> sortedVideos, final String playbackQuality); |     protected abstract int getOverrideResolutionIndex(final List<VideoStream> sortedVideos, final String playbackQuality); | ||||||
|  |  | ||||||
|     @Override |     protected void onMetadataChanged(@NonNull final PlayQueueItem item, | ||||||
|     public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) { |                                      @Nullable final StreamInfo info, | ||||||
|         super.sync(item, info); |                                      final int newPlayQueueIndex, | ||||||
|  |                                      final boolean hasPlayQueueItemChanged) { | ||||||
|         qualityTextView.setVisibility(View.GONE); |         qualityTextView.setVisibility(View.GONE); | ||||||
|         playbackSpeedTextView.setVisibility(View.GONE); |         playbackSpeedTextView.setVisibility(View.GONE); | ||||||
|  |  | ||||||
|         if (info != null && info.video_streams.size() + info.video_only_streams.size() > 0) { |         playbackEndTime.setVisibility(View.GONE); | ||||||
|  |         playbackLiveSync.setVisibility(View.GONE); | ||||||
|  |  | ||||||
|  |         final StreamType streamType = info == null ? StreamType.NONE : info.getStreamType(); | ||||||
|  |  | ||||||
|  |         switch (streamType) { | ||||||
|  |             case AUDIO_STREAM: | ||||||
|  |                 surfaceView.setVisibility(View.GONE); | ||||||
|  |                 playbackEndTime.setVisibility(View.VISIBLE); | ||||||
|  |                 break; | ||||||
|  |  | ||||||
|  |             case AUDIO_LIVE_STREAM: | ||||||
|  |                 surfaceView.setVisibility(View.GONE); | ||||||
|  |                 playbackLiveSync.setVisibility(View.VISIBLE); | ||||||
|  |                 break; | ||||||
|  |  | ||||||
|  |             case LIVE_STREAM: | ||||||
|  |                 surfaceView.setVisibility(View.VISIBLE); | ||||||
|  |                 playbackLiveSync.setVisibility(View.VISIBLE); | ||||||
|  |                 break; | ||||||
|  |  | ||||||
|  |             case VIDEO_STREAM: | ||||||
|  |                 if (info.video_streams.size() + info.video_only_streams.size() == 0) break; | ||||||
|  |  | ||||||
|                 final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, |                 final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, | ||||||
|                         info.video_streams, info.video_only_streams, false); |                         info.video_streams, info.video_only_streams, false); | ||||||
|                 availableStreams = new ArrayList<>(videos); |                 availableStreams = new ArrayList<>(videos); | ||||||
| @@ -340,9 +367,11 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|  |  | ||||||
|                 buildQualityMenu(); |                 buildQualityMenu(); | ||||||
|                 qualityTextView.setVisibility(View.VISIBLE); |                 qualityTextView.setVisibility(View.VISIBLE); | ||||||
|  |  | ||||||
|                 surfaceView.setVisibility(View.VISIBLE); |                 surfaceView.setVisibility(View.VISIBLE); | ||||||
|         } else { |             default: | ||||||
|             surfaceView.setVisibility(View.GONE); |                 playbackEndTime.setVisibility(View.VISIBLE); | ||||||
|  |                 break; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         buildPlaybackSpeedMenu(); |         buildPlaybackSpeedMenu(); | ||||||
| @@ -352,6 +381,9 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|     @Override |     @Override | ||||||
|     @Nullable |     @Nullable | ||||||
|     public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { |     public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { | ||||||
|  |         final MediaSource liveSource = super.sourceOf(item, info); | ||||||
|  |         if (liveSource != null) return liveSource; | ||||||
|  |  | ||||||
|         List<MediaSource> mediaSources = new ArrayList<>(); |         List<MediaSource> mediaSources = new ArrayList<>(); | ||||||
|  |  | ||||||
|         // Create video stream source |         // Create video stream source | ||||||
| @@ -368,6 +400,7 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|         final VideoStream video = index >= 0 && index < videos.size() ? videos.get(index) : null; |         final VideoStream video = index >= 0 && index < videos.size() ? videos.get(index) : null; | ||||||
|         if (video != null) { |         if (video != null) { | ||||||
|             final MediaSource streamSource = buildMediaSource(video.getUrl(), |             final MediaSource streamSource = buildMediaSource(video.getUrl(), | ||||||
|  |                     PlayerHelper.cacheKeyOf(info, video), | ||||||
|                     MediaFormat.getSuffixById(video.getFormatId())); |                     MediaFormat.getSuffixById(video.getFormatId())); | ||||||
|             mediaSources.add(streamSource); |             mediaSources.add(streamSource); | ||||||
|         } |         } | ||||||
| @@ -380,6 +413,7 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|         // Merge with audio stream in case if video does not contain audio |         // Merge with audio stream in case if video does not contain audio | ||||||
|         if (audio != null && ((video != null && video.isVideoOnly) || video == null)) { |         if (audio != null && ((video != null && video.isVideoOnly) || video == null)) { | ||||||
|             final MediaSource audioSource = buildMediaSource(audio.getUrl(), |             final MediaSource audioSource = buildMediaSource(audio.getUrl(), | ||||||
|  |                     PlayerHelper.cacheKeyOf(info, audio), | ||||||
|                     MediaFormat.getSuffixById(audio.getFormatId())); |                     MediaFormat.getSuffixById(audio.getFormatId())); | ||||||
|             mediaSources.add(audioSource); |             mediaSources.add(audioSource); | ||||||
|         } |         } | ||||||
| @@ -395,8 +429,8 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|  |  | ||||||
|             final Format textFormat = Format.createTextSampleFormat(null, mimeType, |             final Format textFormat = Format.createTextSampleFormat(null, mimeType, | ||||||
|                     SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); |                     SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); | ||||||
|             final MediaSource textSource = new SingleSampleMediaSource( |             final MediaSource textSource = dataSource.getSampleMediaSourceFactory() | ||||||
|                     Uri.parse(subtitle.getURL()), cacheDataSourceFactory, textFormat, TIME_UNSET); |                     .createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET); | ||||||
|             mediaSources.add(textSource); |             mediaSources.add(textSource); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -417,7 +451,7 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|         super.onBlocked(); |         super.onBlocked(); | ||||||
|  |  | ||||||
|         controlsVisibilityHandler.removeCallbacksAndMessages(null); |         controlsVisibilityHandler.removeCallbacksAndMessages(null); | ||||||
|         animateView(controlsRoot, false, 300); |         animateView(controlsRoot, false, DEFAULT_CONTROLS_DURATION); | ||||||
|  |  | ||||||
|         playbackSeekBar.setEnabled(false); |         playbackSeekBar.setEnabled(false); | ||||||
|         // Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again |         // Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again | ||||||
| @@ -442,7 +476,7 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|             playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); |             playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); | ||||||
|  |  | ||||||
|         loadingPanel.setVisibility(View.GONE); |         loadingPanel.setVisibility(View.GONE); | ||||||
|         showControlsThenHide(); |  | ||||||
|         animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); |         animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); | ||||||
|         animateView(endScreen, false, 0); |         animateView(endScreen, false, 0); | ||||||
|     } |     } | ||||||
| @@ -529,26 +563,15 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Normalize mismatching language strings |         // Normalize mismatching language strings | ||||||
|         final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage; |         final String preferredLanguage = trackSelector.getPreferredTextLanguage(); | ||||||
|         // Because ExoPlayer normalizes the preferred language string but not the text track |  | ||||||
|         // language strings, some preferred language string will have the language name in lowercase |  | ||||||
|         String formattedPreferredLanguage = null; |  | ||||||
|         if (preferredLanguage != null) { |  | ||||||
|             for (final String language : availableLanguages) { |  | ||||||
|                 if (language.compareToIgnoreCase(preferredLanguage) == 0) { |  | ||||||
|                     formattedPreferredLanguage = language; |  | ||||||
|                     break; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Build UI |         // Build UI | ||||||
|         buildCaptionMenu(availableLanguages); |         buildCaptionMenu(availableLanguages); | ||||||
|         if (trackSelector.getRendererDisabled(textRenderer) || formattedPreferredLanguage == null || |         if (trackSelector.getRendererDisabled(textRenderer) || preferredLanguage == null || | ||||||
|                 !availableLanguages.contains(formattedPreferredLanguage)) { |                 !availableLanguages.contains(preferredLanguage)) { | ||||||
|             captionTextView.setText(R.string.caption_none); |             captionTextView.setText(R.string.caption_none); | ||||||
|         } else { |         } else { | ||||||
|             captionTextView.setText(formattedPreferredLanguage); |             captionTextView.setText(preferredLanguage); | ||||||
|         } |         } | ||||||
|         captionTextView.setVisibility(availableLanguages.isEmpty() ? View.GONE : View.VISIBLE); |         captionTextView.setVisibility(availableLanguages.isEmpty() ? View.GONE : View.VISIBLE); | ||||||
|     } |     } | ||||||
| @@ -595,9 +618,9 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onThumbnailReceived(Bitmap thumbnail) { |     public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { | ||||||
|         super.onThumbnailReceived(thumbnail); |         super.onLoadingComplete(imageUri, view, loadedImage); | ||||||
|         if (thumbnail != null) endScreen.setImageBitmap(thumbnail); |         if (loadedImage != null) endScreen.setImageBitmap(loadedImage); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     protected void onFullScreenButtonClicked() { |     protected void onFullScreenButtonClicked() { | ||||||
| @@ -633,6 +656,8 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|             onResizeClicked(); |             onResizeClicked(); | ||||||
|         } else if (v.getId() == captionTextView.getId()) { |         } else if (v.getId() == captionTextView.getId()) { | ||||||
|             onCaptionClicked(); |             onCaptionClicked(); | ||||||
|  |         } else if (v.getId() == playbackLiveSync.getId()) { | ||||||
|  |             seekToDefault(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -683,7 +708,7 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|         if (DEBUG) Log.d(TAG, "onQualitySelectorClicked() called"); |         if (DEBUG) Log.d(TAG, "onQualitySelectorClicked() called"); | ||||||
|         qualityPopupMenu.show(); |         qualityPopupMenu.show(); | ||||||
|         isSomePopupMenuVisible = true; |         isSomePopupMenuVisible = true; | ||||||
|         showControls(300); |         showControls(DEFAULT_CONTROLS_DURATION); | ||||||
|  |  | ||||||
|         final VideoStream videoStream = getSelectedVideoStream(); |         final VideoStream videoStream = getSelectedVideoStream(); | ||||||
|         if (videoStream != null) { |         if (videoStream != null) { | ||||||
| @@ -699,14 +724,14 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|         if (DEBUG) Log.d(TAG, "onPlaybackSpeedClicked() called"); |         if (DEBUG) Log.d(TAG, "onPlaybackSpeedClicked() called"); | ||||||
|         playbackSpeedPopupMenu.show(); |         playbackSpeedPopupMenu.show(); | ||||||
|         isSomePopupMenuVisible = true; |         isSomePopupMenuVisible = true; | ||||||
|         showControls(300); |         showControls(DEFAULT_CONTROLS_DURATION); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void onCaptionClicked() { |     private void onCaptionClicked() { | ||||||
|         if (DEBUG) Log.d(TAG, "onCaptionClicked() called"); |         if (DEBUG) Log.d(TAG, "onCaptionClicked() called"); | ||||||
|         captionPopupMenu.show(); |         captionPopupMenu.show(); | ||||||
|         isSomePopupMenuVisible = true; |         isSomePopupMenuVisible = true; | ||||||
|         showControls(300); |         showControls(DEFAULT_CONTROLS_DURATION); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void onResizeClicked() { |     private void onResizeClicked() { | ||||||
| @@ -739,7 +764,8 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|         if (isPlaying()) simpleExoPlayer.setPlayWhenReady(false); |         if (isPlaying()) simpleExoPlayer.setPlayWhenReady(false); | ||||||
|  |  | ||||||
|         showControls(0); |         showControls(0); | ||||||
|         animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, true, 300); |         animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, true, | ||||||
|  |                 DEFAULT_CONTROLS_DURATION); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -795,7 +821,7 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|                         PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f), |                         PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f), | ||||||
|                         PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1f), |                         PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1f), | ||||||
|                         PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1f) |                         PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1f) | ||||||
|                 ).setDuration(300); |                 ).setDuration(DEFAULT_CONTROLS_DURATION); | ||||||
|                 controlViewAnimator.addListener(new AnimatorListenerAdapter() { |                 controlViewAnimator.addListener(new AnimatorListenerAdapter() { | ||||||
|                     @Override |                     @Override | ||||||
|                     public void onAnimationEnd(Animator animation) { |                     public void onAnimationEnd(Animator animation) { | ||||||
| @@ -837,12 +863,8 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|  |  | ||||||
|     public void showControlsThenHide() { |     public void showControlsThenHide() { | ||||||
|         if (DEBUG) Log.d(TAG, "showControlsThenHide() called"); |         if (DEBUG) Log.d(TAG, "showControlsThenHide() called"); | ||||||
|         animateView(controlsRoot, true, 300, 0, new Runnable() { |         animateView(controlsRoot, true, DEFAULT_CONTROLS_DURATION, 0, | ||||||
|             @Override |                 () -> hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME)); | ||||||
|             public void run() { |  | ||||||
|                 hideControls(300, DEFAULT_CONTROLS_HIDE_TIME); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void showControls(long duration) { |     public void showControls(long duration) { | ||||||
| @@ -854,12 +876,8 @@ public abstract class VideoPlayer extends BasePlayer | |||||||
|     public void hideControls(final long duration, long delay) { |     public void hideControls(final long duration, long delay) { | ||||||
|         if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); |         if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); | ||||||
|         controlsVisibilityHandler.removeCallbacksAndMessages(null); |         controlsVisibilityHandler.removeCallbacksAndMessages(null); | ||||||
|         controlsVisibilityHandler.postDelayed(new Runnable() { |         controlsVisibilityHandler.postDelayed( | ||||||
|             @Override |                 () -> animateView(controlsRoot, false, duration), delay); | ||||||
|             public void run() { |  | ||||||
|                 animateView(controlsRoot, false, duration); |  | ||||||
|             } |  | ||||||
|         }, delay); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|   | |||||||
| @@ -4,11 +4,14 @@ import android.content.Context; | |||||||
| import android.support.annotation.NonNull; | import android.support.annotation.NonNull; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
|  |  | ||||||
|  | import com.google.android.exoplayer2.upstream.BandwidthMeter; | ||||||
| import com.google.android.exoplayer2.upstream.DataSource; | import com.google.android.exoplayer2.upstream.DataSource; | ||||||
| import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; | import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; | ||||||
| import com.google.android.exoplayer2.upstream.DefaultDataSource; | import com.google.android.exoplayer2.upstream.DefaultDataSource; | ||||||
| import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; | import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; | ||||||
|  | import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; | ||||||
| import com.google.android.exoplayer2.upstream.FileDataSource; | import com.google.android.exoplayer2.upstream.FileDataSource; | ||||||
|  | import com.google.android.exoplayer2.upstream.TransferListener; | ||||||
| import com.google.android.exoplayer2.upstream.cache.CacheDataSink; | import com.google.android.exoplayer2.upstream.cache.CacheDataSink; | ||||||
| import com.google.android.exoplayer2.upstream.cache.CacheDataSource; | import com.google.android.exoplayer2.upstream.cache.CacheDataSource; | ||||||
| import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor; | import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor; | ||||||
| @@ -18,7 +21,7 @@ import org.schabi.newpipe.Downloader; | |||||||
|  |  | ||||||
| import java.io.File; | import java.io.File; | ||||||
|  |  | ||||||
| public class CacheFactory implements DataSource.Factory { | /* package-private */ class CacheFactory implements DataSource.Factory { | ||||||
|     private static final String TAG = "CacheFactory"; |     private static final String TAG = "CacheFactory"; | ||||||
|     private static final String CACHE_FOLDER_NAME = "exoplayer"; |     private static final String CACHE_FOLDER_NAME = "exoplayer"; | ||||||
|     private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR; |     private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR; | ||||||
| @@ -33,18 +36,21 @@ public class CacheFactory implements DataSource.Factory { | |||||||
|     // todo: make this a singleton? |     // todo: make this a singleton? | ||||||
|     private static SimpleCache cache; |     private static SimpleCache cache; | ||||||
|  |  | ||||||
|     public CacheFactory(@NonNull final Context context) { |     public CacheFactory(@NonNull final Context context, | ||||||
|         this(context, PlayerHelper.getPreferredCacheSize(context), PlayerHelper.getPreferredFileSize(context)); |                         @NonNull final String userAgent, | ||||||
|  |                         @NonNull final TransferListener<? super DataSource> transferListener) { | ||||||
|  |         this(context, userAgent, transferListener, PlayerHelper.getPreferredCacheSize(context), | ||||||
|  |                 PlayerHelper.getPreferredFileSize(context)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     CacheFactory(@NonNull final Context context, final long maxCacheSize, final long maxFileSize) { |     private CacheFactory(@NonNull final Context context, | ||||||
|         super(); |                          @NonNull final String userAgent, | ||||||
|  |                          @NonNull final TransferListener<? super DataSource> transferListener, | ||||||
|  |                          final long maxCacheSize, | ||||||
|  |                          final long maxFileSize) { | ||||||
|         this.maxFileSize = maxFileSize; |         this.maxFileSize = maxFileSize; | ||||||
|  |  | ||||||
|         final String userAgent = Downloader.USER_AGENT; |         dataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener); | ||||||
|         final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); |  | ||||||
|         dataSourceFactory = new DefaultDataSourceFactory(context, userAgent, bandwidthMeter); |  | ||||||
|  |  | ||||||
|         cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME); |         cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME); | ||||||
|         if (!cacheDir.exists()) { |         if (!cacheDir.exists()) { | ||||||
|             //noinspection ResultOfMethodCallIgnored |             //noinspection ResultOfMethodCallIgnored | ||||||
|   | |||||||
| @@ -11,12 +11,14 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; | |||||||
| import com.google.android.exoplayer2.upstream.Allocator; | import com.google.android.exoplayer2.upstream.Allocator; | ||||||
| import com.google.android.exoplayer2.upstream.DefaultAllocator; | import com.google.android.exoplayer2.upstream.DefaultAllocator; | ||||||
|  |  | ||||||
| import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; | import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS; | ||||||
|  | import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_TARGET_BUFFER_BYTES; | ||||||
|  |  | ||||||
| public class LoadController implements LoadControl { | public class LoadController implements LoadControl { | ||||||
|  |  | ||||||
|     public static final String TAG = "LoadController"; |     public static final String TAG = "LoadController"; | ||||||
|  |  | ||||||
|  |     private final long initialPlaybackBufferUs; | ||||||
|     private final LoadControl internalLoadControl; |     private final LoadControl internalLoadControl; | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
| @@ -24,19 +26,25 @@ public class LoadController implements LoadControl { | |||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     public LoadController(final Context context) { |     public LoadController(final Context context) { | ||||||
|         this(PlayerHelper.getMinBufferMs(context), |         this(PlayerHelper.getPlaybackStartBufferMs(context), | ||||||
|                 PlayerHelper.getMaxBufferMs(context), |                 PlayerHelper.getPlaybackMinimumBufferMs(context), | ||||||
|                 PlayerHelper.getBufferForPlaybackMs(context)); |                 PlayerHelper.getPlaybackOptimalBufferMs(context)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public LoadController(final int minBufferMs, |     private LoadController(final int initialPlaybackBufferMs, | ||||||
|                           final int maxBufferMs, |                            final int minimumPlaybackbufferMs, | ||||||
|                           final int bufferForPlaybackMs) { |                            final int optimalPlaybackBufferMs) { | ||||||
|  |         this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000; | ||||||
|  |  | ||||||
|         final DefaultAllocator allocator = new DefaultAllocator(true, |         final DefaultAllocator allocator = new DefaultAllocator(true, | ||||||
|                 C.DEFAULT_BUFFER_SEGMENT_SIZE); |                 C.DEFAULT_BUFFER_SEGMENT_SIZE); | ||||||
|  |  | ||||||
|         internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs, |         internalLoadControl = new DefaultLoadControl(allocator, | ||||||
|                 bufferForPlaybackMs, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS); |                 /*minBufferMs=*/minimumPlaybackbufferMs, | ||||||
|  |                 /*maxBufferMs=*/optimalPlaybackBufferMs, | ||||||
|  |                 /*bufferForPlaybackMs=*/initialPlaybackBufferMs, | ||||||
|  |                 /*bufferForPlaybackAfterRebufferMs=*/initialPlaybackBufferMs, | ||||||
|  |                 DEFAULT_TARGET_BUFFER_BYTES, DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
| @@ -49,7 +57,8 @@ public class LoadController implements LoadControl { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroupArray, TrackSelectionArray trackSelectionArray) { |     public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroupArray, | ||||||
|  |                                  TrackSelectionArray trackSelectionArray) { | ||||||
|         internalLoadControl.onTracksSelected(renderers, trackGroupArray, trackSelectionArray); |         internalLoadControl.onTracksSelected(renderers, trackGroupArray, trackSelectionArray); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -69,12 +78,27 @@ public class LoadController implements LoadControl { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public boolean shouldStartPlayback(long l, boolean b) { |     public long getBackBufferDurationUs() { | ||||||
|         return internalLoadControl.shouldStartPlayback(l, b); |         return internalLoadControl.getBackBufferDurationUs(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public boolean shouldContinueLoading(long l) { |     public boolean retainBackBufferFromKeyframe() { | ||||||
|         return internalLoadControl.shouldContinueLoading(l); |         return internalLoadControl.retainBackBufferFromKeyframe(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { | ||||||
|  |         return internalLoadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, | ||||||
|  |                                        boolean rebuffering) { | ||||||
|  |         final boolean isInitialPlaybackBufferFilled = bufferedDurationUs >= | ||||||
|  |                 this.initialPlaybackBufferUs * playbackSpeed; | ||||||
|  |         final boolean isInternalStartingPlayback = internalLoadControl.shouldStartPlayback( | ||||||
|  |                 bufferedDurationUs, playbackSpeed, rebuffering); | ||||||
|  |         return isInitialPlaybackBufferFilled || isInternalStartingPlayback; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,78 @@ | |||||||
|  | package org.schabi.newpipe.player.helper; | ||||||
|  |  | ||||||
|  | import android.content.Context; | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  |  | ||||||
|  | import com.google.android.exoplayer2.source.ExtractorMediaSource; | ||||||
|  | import com.google.android.exoplayer2.source.SingleSampleMediaSource; | ||||||
|  | import com.google.android.exoplayer2.source.dash.DashMediaSource; | ||||||
|  | import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; | ||||||
|  | import com.google.android.exoplayer2.source.hls.HlsMediaSource; | ||||||
|  | import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; | ||||||
|  | import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; | ||||||
|  | import com.google.android.exoplayer2.upstream.DataSource; | ||||||
|  | import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; | ||||||
|  | import com.google.android.exoplayer2.upstream.TransferListener; | ||||||
|  |  | ||||||
|  | public class PlayerDataSource { | ||||||
|  |     private static final int MANIFEST_MINIMUM_RETRY = 5; | ||||||
|  |     private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE; | ||||||
|  |     private static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000; | ||||||
|  |  | ||||||
|  |     private final DataSource.Factory cacheDataSourceFactory; | ||||||
|  |     private final DataSource.Factory cachelessDataSourceFactory; | ||||||
|  |  | ||||||
|  |     public PlayerDataSource(@NonNull final Context context, | ||||||
|  |                             @NonNull final String userAgent, | ||||||
|  |                             @NonNull final TransferListener<? super DataSource> transferListener) { | ||||||
|  |         cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener); | ||||||
|  |         cachelessDataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public SsMediaSource.Factory getLiveSsMediaSourceFactory() { | ||||||
|  |         return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory( | ||||||
|  |                 cachelessDataSourceFactory), cachelessDataSourceFactory) | ||||||
|  |                 .setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY) | ||||||
|  |                 .setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() { | ||||||
|  |         return new HlsMediaSource.Factory(cachelessDataSourceFactory) | ||||||
|  |                 .setAllowChunklessPreparation(true) | ||||||
|  |                 .setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public DashMediaSource.Factory getLiveDashMediaSourceFactory() { | ||||||
|  |         return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory( | ||||||
|  |                 cachelessDataSourceFactory), cachelessDataSourceFactory) | ||||||
|  |                 .setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY) | ||||||
|  |                 .setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public SsMediaSource.Factory getSsMediaSourceFactory() { | ||||||
|  |         return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory( | ||||||
|  |                 cacheDataSourceFactory), cacheDataSourceFactory); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public HlsMediaSource.Factory getHlsMediaSourceFactory() { | ||||||
|  |         return new HlsMediaSource.Factory(cacheDataSourceFactory); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public DashMediaSource.Factory getDashMediaSourceFactory() { | ||||||
|  |         return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory( | ||||||
|  |                 cacheDataSourceFactory), cacheDataSourceFactory); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public ExtractorMediaSource.Factory getExtractorMediaSourceFactory() { | ||||||
|  |         return new ExtractorMediaSource.Factory(cacheDataSourceFactory) | ||||||
|  |                 .setMinLoadableRetryCount(EXTRACTOR_MINIMUM_RETRY); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public ExtractorMediaSource.Factory getExtractorMediaSourceFactory(@NonNull final String key) { | ||||||
|  |         return getExtractorMediaSourceFactory().setCustomCacheKey(key); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() { | ||||||
|  |         return new SingleSampleMediaSource.Factory(cacheDataSourceFactory); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -4,18 +4,33 @@ import android.content.Context; | |||||||
| import android.content.SharedPreferences; | import android.content.SharedPreferences; | ||||||
| import android.preference.PreferenceManager; | import android.preference.PreferenceManager; | ||||||
| import android.support.annotation.NonNull; | import android.support.annotation.NonNull; | ||||||
|  | import android.support.annotation.Nullable; | ||||||
|  |  | ||||||
|  | import com.google.android.exoplayer2.SeekParameters; | ||||||
| import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; | import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; | ||||||
| import com.google.android.exoplayer2.util.MimeTypes; | import com.google.android.exoplayer2.util.MimeTypes; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.extractor.InfoItem; | ||||||
| import org.schabi.newpipe.extractor.Subtitles; | import org.schabi.newpipe.extractor.Subtitles; | ||||||
|  | import org.schabi.newpipe.extractor.stream.AudioStream; | ||||||
|  | import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||||
|  | import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||||
| import org.schabi.newpipe.extractor.stream.SubtitlesFormat; | import org.schabi.newpipe.extractor.stream.SubtitlesFormat; | ||||||
|  | import org.schabi.newpipe.extractor.stream.VideoStream; | ||||||
|  | import org.schabi.newpipe.playlist.PlayQueue; | ||||||
|  | import org.schabi.newpipe.playlist.PlayQueueItem; | ||||||
|  | import org.schabi.newpipe.playlist.SinglePlayQueue; | ||||||
|  |  | ||||||
| import java.text.DecimalFormat; | import java.text.DecimalFormat; | ||||||
| import java.text.NumberFormat; | import java.text.NumberFormat; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Collections; | ||||||
| import java.util.Formatter; | import java.util.Formatter; | ||||||
|  | import java.util.HashSet; | ||||||
|  | import java.util.List; | ||||||
| import java.util.Locale; | import java.util.Locale; | ||||||
|  | import java.util.Set; | ||||||
|  |  | ||||||
| import javax.annotation.Nonnull; | import javax.annotation.Nonnull; | ||||||
|  |  | ||||||
| @@ -69,10 +84,10 @@ public class PlayerHelper { | |||||||
|     public static String captionLanguageOf(@NonNull final Context context, |     public static String captionLanguageOf(@NonNull final Context context, | ||||||
|                                            @NonNull final Subtitles subtitles) { |                                            @NonNull final Subtitles subtitles) { | ||||||
|         final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale()); |         final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale()); | ||||||
|         return displayName + (subtitles.isAutoGenerated() ? |         return displayName + (subtitles.isAutoGenerated() ? " (" + context.getString(R.string.caption_auto_generated)+ ")" : ""); | ||||||
|                 " (" + context.getString(R.string.caption_auto_generated)+ ")" : ""); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|     public static String resizeTypeOf(@NonNull final Context context, |     public static String resizeTypeOf(@NonNull final Context context, | ||||||
|                                       @AspectRatioFrameLayout.ResizeMode final int resizeMode) { |                                       @AspectRatioFrameLayout.ResizeMode final int resizeMode) { | ||||||
|         switch (resizeMode) { |         switch (resizeMode) { | ||||||
| @@ -83,6 +98,58 @@ public class PlayerHelper { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull VideoStream video) { | ||||||
|  |         return info.getUrl() + video.getResolution() + video.getFormat().getName(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull AudioStream audio) { | ||||||
|  |         return info.getUrl() + audio.getAverageBitrate() + audio.getFormat().getName(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Given a {@link StreamInfo} and the existing queue items, provide the | ||||||
|  |      * {@link SinglePlayQueue} consisting of the next video for auto queuing. | ||||||
|  |      * <br><br> | ||||||
|  |      * This method detects and prevents cycle by naively checking if a | ||||||
|  |      * candidate next video's url already exists in the existing items. | ||||||
|  |      * <br><br> | ||||||
|  |      * To select the next video, {@link StreamInfo#getNextVideo()} is first | ||||||
|  |      * checked. If it is nonnull and is not part of the existing items, then | ||||||
|  |      * it will be used as the next video. Otherwise, an random item with | ||||||
|  |      * non-repeating url will be selected from the {@link StreamInfo#getRelatedStreams()}. | ||||||
|  |      * */ | ||||||
|  |     @Nullable | ||||||
|  |     public static PlayQueue autoQueueOf(@NonNull final StreamInfo info, | ||||||
|  |                                         @NonNull final List<PlayQueueItem> existingItems) { | ||||||
|  |         Set<String> urls = new HashSet<>(existingItems.size()); | ||||||
|  |         for (final PlayQueueItem item : existingItems) { | ||||||
|  |             urls.add(item.getUrl()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         final StreamInfoItem nextVideo = info.getNextVideo(); | ||||||
|  |         if (nextVideo != null && !urls.contains(nextVideo.getUrl())) { | ||||||
|  |             return new SinglePlayQueue(nextVideo); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         final List<InfoItem> relatedItems = info.getRelatedStreams(); | ||||||
|  |         if (relatedItems == null) return null; | ||||||
|  |  | ||||||
|  |         List<StreamInfoItem> autoQueueItems = new ArrayList<>(); | ||||||
|  |         for (final InfoItem item : info.getRelatedStreams()) { | ||||||
|  |             if (item instanceof StreamInfoItem && !urls.contains(item.getUrl())) { | ||||||
|  |                 autoQueueItems.add((StreamInfoItem) item); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         Collections.shuffle(autoQueueItems); | ||||||
|  |         return autoQueueItems.isEmpty() ? null : new SinglePlayQueue(autoQueueItems.get(0)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     //////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Settings Resolution | ||||||
|  |     //////////////////////////////////////////////////////////////////////////// | ||||||
|  |  | ||||||
|     public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) { |     public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) { | ||||||
|         return isResumeAfterAudioFocusGain(context, false); |         return isResumeAfterAudioFocusGain(context, false); | ||||||
|     } |     } | ||||||
| @@ -99,6 +166,16 @@ public class PlayerHelper { | |||||||
|         return isRememberingPopupDimensions(context, true); |         return isRememberingPopupDimensions(context, true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public static boolean isAutoQueueEnabled(@NonNull final Context context) { | ||||||
|  |         return isAutoQueueEnabled(context, false); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     public static SeekParameters getSeekParameters(@NonNull final Context context) { | ||||||
|  |         return isUsingInexactSeek(context, false) ? | ||||||
|  |                 SeekParameters.CLOSEST_SYNC : SeekParameters.EXACT; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public static long getPreferredCacheSize(@NonNull final Context context) { |     public static long getPreferredCacheSize(@NonNull final Context context) { | ||||||
|         return 64 * 1024 * 1024L; |         return 64 * 1024 * 1024L; | ||||||
|     } |     } | ||||||
| @@ -107,16 +184,27 @@ public class PlayerHelper { | |||||||
|         return 512 * 1024L; |         return 512 * 1024L; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static int getMinBufferMs(@NonNull final Context context) { |     /** | ||||||
|         return 15000; |      * Returns the number of milliseconds the player buffers for before starting playback. | ||||||
|  |      * */ | ||||||
|  |     public static int getPlaybackStartBufferMs(@NonNull final Context context) { | ||||||
|  |         return 500; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static int getMaxBufferMs(@NonNull final Context context) { |     /** | ||||||
|         return 30000; |      * Returns the minimum number of milliseconds the player always buffers to after starting | ||||||
|  |      * playback. | ||||||
|  |      * */ | ||||||
|  |     public static int getPlaybackMinimumBufferMs(@NonNull final Context context) { | ||||||
|  |         return 25000; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static int getBufferForPlaybackMs(@NonNull final Context context) { |     /** | ||||||
|         return 2500; |      * Returns the maximum/optimal number of milliseconds the player will buffer to once the buffer | ||||||
|  |      * hits the point of {@link #getPlaybackMinimumBufferMs(Context)}. | ||||||
|  |      * */ | ||||||
|  |     public static int getPlaybackOptimalBufferMs(@NonNull final Context context) { | ||||||
|  |         return 60000; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static boolean isUsingDSP(@NonNull final Context context) { |     public static boolean isUsingDSP(@NonNull final Context context) { | ||||||
| @@ -155,4 +243,12 @@ public class PlayerHelper { | |||||||
|     private static boolean isRememberingPopupDimensions(@Nonnull final Context context, final boolean b) { |     private static boolean isRememberingPopupDimensions(@Nonnull final Context context, final boolean b) { | ||||||
|         return getPreferences(context).getBoolean(context.getString(R.string.popup_remember_size_pos_key), b); |         return getPreferences(context).getBoolean(context.getString(R.string.popup_remember_size_pos_key), b); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private static boolean isUsingInexactSeek(@NonNull final Context context, final boolean b) { | ||||||
|  |         return getPreferences(context).getBoolean(context.getString(R.string.use_inexact_seek_key), b); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static boolean isAutoQueueEnabled(@NonNull final Context context, final boolean b) { | ||||||
|  |         return getPreferences(context).getBoolean(context.getString(R.string.auto_queue_key), b); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,78 @@ | |||||||
|  | package org.schabi.newpipe.player.mediasource; | ||||||
|  |  | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  | import android.util.Log; | ||||||
|  |  | ||||||
|  | import com.google.android.exoplayer2.ExoPlayer; | ||||||
|  | import com.google.android.exoplayer2.source.MediaPeriod; | ||||||
|  | import com.google.android.exoplayer2.upstream.Allocator; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.playlist.PlayQueueItem; | ||||||
|  |  | ||||||
|  | import java.io.IOException; | ||||||
|  |  | ||||||
|  | public class FailedMediaSource implements ManagedMediaSource { | ||||||
|  |     private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode()); | ||||||
|  |  | ||||||
|  |     private final PlayQueueItem playQueueItem; | ||||||
|  |     private final Throwable error; | ||||||
|  |  | ||||||
|  |     private final long retryTimestamp; | ||||||
|  |  | ||||||
|  |     public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem, | ||||||
|  |                              @NonNull final Throwable error, | ||||||
|  |                              final long retryTimestamp) { | ||||||
|  |         this.playQueueItem = playQueueItem; | ||||||
|  |         this.error = error; | ||||||
|  |         this.retryTimestamp = retryTimestamp; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Permanently fail the play queue item associated with this source, with no hope of retrying. | ||||||
|  |      * The error will always be propagated to ExoPlayer. | ||||||
|  |      * */ | ||||||
|  |     public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem, | ||||||
|  |                              @NonNull final Throwable error) { | ||||||
|  |         this.playQueueItem = playQueueItem; | ||||||
|  |         this.error = error; | ||||||
|  |         this.retryTimestamp = Long.MAX_VALUE; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public PlayQueueItem getStream() { | ||||||
|  |         return playQueueItem; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public Throwable getError() { | ||||||
|  |         return error; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private boolean canRetry() { | ||||||
|  |         return System.currentTimeMillis() >= retryTimestamp; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { | ||||||
|  |         Log.e(TAG, "Loading failed source: ", error); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void maybeThrowSourceInfoRefreshError() throws IOException { | ||||||
|  |         throw new IOException(error); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void releasePeriod(MediaPeriod mediaPeriod) {} | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void releaseSource() {} | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { | ||||||
|  |         return newIdentity != playQueueItem || canRetry(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,65 @@ | |||||||
|  | package org.schabi.newpipe.player.mediasource; | ||||||
|  |  | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  |  | ||||||
|  | import com.google.android.exoplayer2.ExoPlayer; | ||||||
|  | import com.google.android.exoplayer2.source.MediaPeriod; | ||||||
|  | import com.google.android.exoplayer2.source.MediaSource; | ||||||
|  | import com.google.android.exoplayer2.upstream.Allocator; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.playlist.PlayQueueItem; | ||||||
|  |  | ||||||
|  | import java.io.IOException; | ||||||
|  |  | ||||||
|  | public class LoadedMediaSource implements ManagedMediaSource { | ||||||
|  |  | ||||||
|  |     private final MediaSource source; | ||||||
|  |     private final PlayQueueItem stream; | ||||||
|  |     private final long expireTimestamp; | ||||||
|  |  | ||||||
|  |     public LoadedMediaSource(@NonNull final MediaSource source, | ||||||
|  |                              @NonNull final PlayQueueItem stream, | ||||||
|  |                              final long expireTimestamp) { | ||||||
|  |         this.source = source; | ||||||
|  |         this.stream = stream; | ||||||
|  |         this.expireTimestamp = expireTimestamp; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public PlayQueueItem getStream() { | ||||||
|  |         return stream; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private boolean isExpired() { | ||||||
|  |         return System.currentTimeMillis() >= expireTimestamp; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { | ||||||
|  |         source.prepareSource(player, isTopLevelSource, listener); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void maybeThrowSourceInfoRefreshError() throws IOException { | ||||||
|  |         source.maybeThrowSourceInfoRefreshError(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { | ||||||
|  |         return source.createPeriod(id, allocator); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void releasePeriod(MediaPeriod mediaPeriod) { | ||||||
|  |         source.releasePeriod(mediaPeriod); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void releaseSource() { | ||||||
|  |         source.releaseSource(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { | ||||||
|  |         return newIdentity != stream || isExpired(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,11 @@ | |||||||
|  | package org.schabi.newpipe.player.mediasource; | ||||||
|  |  | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  |  | ||||||
|  | import com.google.android.exoplayer2.source.MediaSource; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.playlist.PlayQueueItem; | ||||||
|  |  | ||||||
|  | public interface ManagedMediaSource extends MediaSource { | ||||||
|  |     boolean canReplace(@NonNull final PlayQueueItem newIdentity); | ||||||
|  | } | ||||||
| @@ -0,0 +1,25 @@ | |||||||
|  | package org.schabi.newpipe.player.mediasource; | ||||||
|  |  | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  |  | ||||||
|  | import com.google.android.exoplayer2.ExoPlayer; | ||||||
|  | import com.google.android.exoplayer2.source.MediaPeriod; | ||||||
|  | import com.google.android.exoplayer2.upstream.Allocator; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.playlist.PlayQueueItem; | ||||||
|  |  | ||||||
|  | import java.io.IOException; | ||||||
|  |  | ||||||
|  | public class PlaceholderMediaSource implements ManagedMediaSource { | ||||||
|  |     // Do nothing, so this will stall the playback | ||||||
|  |     @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {} | ||||||
|  |     @Override public void maybeThrowSourceInfoRefreshError() throws IOException {} | ||||||
|  |     @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { return null; } | ||||||
|  |     @Override public void releasePeriod(MediaPeriod mediaPeriod) {} | ||||||
|  |     @Override public void releaseSource() {} | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,114 @@ | |||||||
|  | package org.schabi.newpipe.player.playback; | ||||||
|  |  | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  | import android.text.TextUtils; | ||||||
|  |  | ||||||
|  | import com.google.android.exoplayer2.C; | ||||||
|  | import com.google.android.exoplayer2.ExoPlaybackException; | ||||||
|  | import com.google.android.exoplayer2.Format; | ||||||
|  | import com.google.android.exoplayer2.source.TrackGroup; | ||||||
|  | import com.google.android.exoplayer2.source.TrackGroupArray; | ||||||
|  | import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; | ||||||
|  | import com.google.android.exoplayer2.trackselection.FixedTrackSelection; | ||||||
|  | import com.google.android.exoplayer2.trackselection.TrackSelection; | ||||||
|  | import com.google.android.exoplayer2.util.Assertions; | ||||||
|  | import com.google.android.exoplayer2.util.Util; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This class allows irregular text language labels for use when selecting text captions and | ||||||
|  |  * is mostly a copy-paste from {@link DefaultTrackSelector}. | ||||||
|  |  * | ||||||
|  |  * This is a hack and should be removed once ExoPlayer fixes language normalization to accept | ||||||
|  |  * a broader set of languages.  | ||||||
|  |  * */ | ||||||
|  | public class CustomTrackSelector extends DefaultTrackSelector { | ||||||
|  |     private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000; | ||||||
|  |  | ||||||
|  |     private String preferredTextLanguage; | ||||||
|  |  | ||||||
|  |     public CustomTrackSelector(TrackSelection.Factory adaptiveTrackSelectionFactory) { | ||||||
|  |         super(adaptiveTrackSelectionFactory); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getPreferredTextLanguage() { | ||||||
|  |         return preferredTextLanguage; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setPreferredTextLanguage(@NonNull final String label) { | ||||||
|  |         Assertions.checkNotNull(label); | ||||||
|  |         if (!label.equals(preferredTextLanguage)) { | ||||||
|  |             preferredTextLanguage = label; | ||||||
|  |             invalidate(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @see DefaultTrackSelector#formatHasLanguage(Format, String)*/ | ||||||
|  |     protected static boolean formatHasLanguage(Format format, String language) { | ||||||
|  |         return language != null && TextUtils.equals(language, format.language); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @see DefaultTrackSelector#formatHasNoLanguage(Format)*/ | ||||||
|  |     protected static boolean formatHasNoLanguage(Format format) { | ||||||
|  |         return TextUtils.isEmpty(format.language) || formatHasLanguage(format, C.LANGUAGE_UNDETERMINED); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @see DefaultTrackSelector#selectTextTrack(TrackGroupArray, int[][], Parameters) */ | ||||||
|  |     @Override | ||||||
|  |     protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatSupport, | ||||||
|  |                                              Parameters params) throws ExoPlaybackException { | ||||||
|  |         TrackGroup selectedGroup = null; | ||||||
|  |         int selectedTrackIndex = 0; | ||||||
|  |         int selectedTrackScore = 0; | ||||||
|  |         for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { | ||||||
|  |             TrackGroup trackGroup = groups.get(groupIndex); | ||||||
|  |             int[] trackFormatSupport = formatSupport[groupIndex]; | ||||||
|  |             for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { | ||||||
|  |                 if (isSupported(trackFormatSupport[trackIndex], | ||||||
|  |                         params.exceedRendererCapabilitiesIfNecessary)) { | ||||||
|  |                     Format format = trackGroup.getFormat(trackIndex); | ||||||
|  |                     int maskedSelectionFlags = | ||||||
|  |                             format.selectionFlags & ~params.disabledTextTrackSelectionFlags; | ||||||
|  |                     boolean isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; | ||||||
|  |                     boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; | ||||||
|  |                     int trackScore; | ||||||
|  |                     boolean preferredLanguageFound = formatHasLanguage(format, preferredTextLanguage); | ||||||
|  |                     if (preferredLanguageFound | ||||||
|  |                             || (params.selectUndeterminedTextLanguage && formatHasNoLanguage(format))) { | ||||||
|  |                         if (isDefault) { | ||||||
|  |                             trackScore = 8; | ||||||
|  |                         } else if (!isForced) { | ||||||
|  |                             // Prefer non-forced to forced if a preferred text language has been specified. Where | ||||||
|  |                             // both are provided the non-forced track will usually contain the forced subtitles as | ||||||
|  |                             // a subset. | ||||||
|  |                             trackScore = 6; | ||||||
|  |                         } else { | ||||||
|  |                             trackScore = 4; | ||||||
|  |                         } | ||||||
|  |                         trackScore += preferredLanguageFound ? 1 : 0; | ||||||
|  |                     } else if (isDefault) { | ||||||
|  |                         trackScore = 3; | ||||||
|  |                     } else if (isForced) { | ||||||
|  |                         if (formatHasLanguage(format, params.preferredAudioLanguage)) { | ||||||
|  |                             trackScore = 2; | ||||||
|  |                         } else { | ||||||
|  |                             trackScore = 1; | ||||||
|  |                         } | ||||||
|  |                     } else { | ||||||
|  |                         // Track should not be selected. | ||||||
|  |                         continue; | ||||||
|  |                     } | ||||||
|  |                     if (isSupported(trackFormatSupport[trackIndex], false)) { | ||||||
|  |                         trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; | ||||||
|  |                     } | ||||||
|  |                     if (trackScore > selectedTrackScore) { | ||||||
|  |                         selectedGroup = trackGroup; | ||||||
|  |                         selectedTrackIndex = trackIndex; | ||||||
|  |                         selectedTrackScore = trackScore; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return selectedGroup == null ? null | ||||||
|  |                 : new FixedTrackSelection(selectedGroup, selectedTrackIndex); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,216 +0,0 @@ | |||||||
| package org.schabi.newpipe.player.playback; |  | ||||||
|  |  | ||||||
| import android.support.annotation.NonNull; |  | ||||||
| import android.util.Log; |  | ||||||
|  |  | ||||||
| import com.google.android.exoplayer2.ExoPlayer; |  | ||||||
| import com.google.android.exoplayer2.source.MediaPeriod; |  | ||||||
| import com.google.android.exoplayer2.source.MediaSource; |  | ||||||
| import com.google.android.exoplayer2.upstream.Allocator; |  | ||||||
|  |  | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; |  | ||||||
| import org.schabi.newpipe.playlist.PlayQueueItem; |  | ||||||
|  |  | ||||||
| import java.io.IOException; |  | ||||||
|  |  | ||||||
| import io.reactivex.android.schedulers.AndroidSchedulers; |  | ||||||
| import io.reactivex.disposables.Disposable; |  | ||||||
| import io.reactivex.functions.Consumer; |  | ||||||
| import io.reactivex.functions.Function; |  | ||||||
| import io.reactivex.schedulers.Schedulers; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * DeferredMediaSource is specifically designed to allow external control over when |  | ||||||
|  * the source metadata are loaded while being compatible with ExoPlayer's playlists. |  | ||||||
|  * |  | ||||||
|  * This media source follows the structure of how NewPipeExtractor's |  | ||||||
|  * {@link org.schabi.newpipe.extractor.stream.StreamInfoItem} is converted into |  | ||||||
|  * {@link org.schabi.newpipe.extractor.stream.StreamInfo}. Once conversion is complete, |  | ||||||
|  * this media source behaves identically as any other native media sources. |  | ||||||
|  * */ |  | ||||||
| public final class DeferredMediaSource implements MediaSource { |  | ||||||
|     private final String TAG = "DeferredMediaSource@" + Integer.toHexString(hashCode()); |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * This state indicates the {@link DeferredMediaSource} has just been initialized or reset. |  | ||||||
|      * The source must be prepared and loaded again before playback. |  | ||||||
|      * */ |  | ||||||
|     public final static int STATE_INIT = 0; |  | ||||||
|     /** |  | ||||||
|      * This state indicates the {@link DeferredMediaSource} has been prepared and is ready to load. |  | ||||||
|      * */ |  | ||||||
|     public final static int STATE_PREPARED = 1; |  | ||||||
|     /** |  | ||||||
|      * This state indicates the {@link DeferredMediaSource} has been loaded without errors and |  | ||||||
|      * is ready for playback. |  | ||||||
|      * */ |  | ||||||
|     public final static int STATE_LOADED = 2; |  | ||||||
|  |  | ||||||
|     public interface Callback { |  | ||||||
|         /** |  | ||||||
|          * Player-specific {@link com.google.android.exoplayer2.source.MediaSource} resolution |  | ||||||
|          * from a given StreamInfo. |  | ||||||
|          * */ |  | ||||||
|         MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private PlayQueueItem stream; |  | ||||||
|     private Callback callback; |  | ||||||
|     private int state; |  | ||||||
|  |  | ||||||
|     private MediaSource mediaSource; |  | ||||||
|  |  | ||||||
|     /* Custom internal objects */ |  | ||||||
|     private Disposable loader; |  | ||||||
|     private ExoPlayer exoPlayer; |  | ||||||
|     private Listener listener; |  | ||||||
|     private Throwable error; |  | ||||||
|  |  | ||||||
|     public DeferredMediaSource(@NonNull final PlayQueueItem stream, |  | ||||||
|                                @NonNull final Callback callback) { |  | ||||||
|         this.stream = stream; |  | ||||||
|         this.callback = callback; |  | ||||||
|         this.state = STATE_INIT; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Returns the current state of the {@link DeferredMediaSource}. |  | ||||||
|      * |  | ||||||
|      * @see DeferredMediaSource#STATE_INIT |  | ||||||
|      * @see DeferredMediaSource#STATE_PREPARED |  | ||||||
|      * @see DeferredMediaSource#STATE_LOADED |  | ||||||
|      * */ |  | ||||||
|     public int state() { |  | ||||||
|         return state; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Parameters are kept in the class for delayed preparation. |  | ||||||
|      * */ |  | ||||||
|     @Override |  | ||||||
|     public void prepareSource(ExoPlayer exoPlayer, boolean isTopLevelSource, Listener listener) { |  | ||||||
|         this.exoPlayer = exoPlayer; |  | ||||||
|         this.listener = listener; |  | ||||||
|         this.state = STATE_PREPARED; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Externally controlled loading. This method fully prepares the source to be used |  | ||||||
|      * like any other native {@link com.google.android.exoplayer2.source.MediaSource}. |  | ||||||
|      * |  | ||||||
|      * Ideally, this should be called after this source has entered PREPARED state and |  | ||||||
|      * called once only. |  | ||||||
|      * |  | ||||||
|      * If loading fails here, an error will be propagated out and result in an |  | ||||||
|      * {@link com.google.android.exoplayer2.ExoPlaybackException ExoPlaybackException}, |  | ||||||
|      * which is delegated to the player. |  | ||||||
|      * */ |  | ||||||
|     public synchronized void load() { |  | ||||||
|         if (stream == null) { |  | ||||||
|             Log.e(TAG, "Stream Info missing, media source loading terminated."); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         if (state != STATE_PREPARED || loader != null) return; |  | ||||||
|  |  | ||||||
|         Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl()); |  | ||||||
|  |  | ||||||
|         loader = stream.getStream() |  | ||||||
|                 .map(streamInfo -> onStreamInfoReceived(stream, streamInfo)) |  | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |  | ||||||
|                 .subscribe(this::onMediaSourceReceived, this::onStreamInfoError); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private MediaSource onStreamInfoReceived(@NonNull final PlayQueueItem item, |  | ||||||
|                                              @NonNull final StreamInfo info) throws Exception { |  | ||||||
|         if (callback == null) { |  | ||||||
|             throw new Exception("No available callback for resolving stream info."); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         final MediaSource mediaSource = callback.sourceOf(item, info); |  | ||||||
|  |  | ||||||
|         if (mediaSource == null) { |  | ||||||
|             throw new Exception("Unable to resolve source from stream info. URL: " + stream.getUrl() + |  | ||||||
|                     ", audio count: " + info.audio_streams.size() + |  | ||||||
|                     ", video count: " + info.video_only_streams.size() + info.video_streams.size()); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return mediaSource; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void onMediaSourceReceived(final MediaSource mediaSource) throws Exception { |  | ||||||
|         if (exoPlayer == null || listener == null || mediaSource == null) { |  | ||||||
|             throw new Exception("MediaSource loading failed. URL: " + stream.getUrl()); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl()); |  | ||||||
|         state = STATE_LOADED; |  | ||||||
|  |  | ||||||
|         this.mediaSource = mediaSource; |  | ||||||
|         this.mediaSource.prepareSource(exoPlayer, false, listener); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void onStreamInfoError(final Throwable throwable) { |  | ||||||
|         Log.e(TAG, "Loading error:", throwable); |  | ||||||
|         error = throwable; |  | ||||||
|         state = STATE_LOADED; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Delegate all errors to the player after {@link #load() load} is complete. |  | ||||||
|      * |  | ||||||
|      * Specifically, this method is called after an exception has occurred during loading or |  | ||||||
|      * {@link com.google.android.exoplayer2.source.MediaSource#prepareSource(ExoPlayer, boolean, Listener) prepareSource}. |  | ||||||
|      * */ |  | ||||||
|     @Override |  | ||||||
|     public void maybeThrowSourceInfoRefreshError() throws IOException { |  | ||||||
|         if (error != null) { |  | ||||||
|             throw new IOException(error); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (mediaSource != null) { |  | ||||||
|             mediaSource.maybeThrowSourceInfoRefreshError(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public MediaPeriod createPeriod(MediaPeriodId mediaPeriodId, Allocator allocator) { |  | ||||||
|         return mediaSource.createPeriod(mediaPeriodId, allocator); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Releases the media period (buffers). |  | ||||||
|      * |  | ||||||
|      * This may be called after {@link #releaseSource releaseSource}. |  | ||||||
|      * */ |  | ||||||
|     @Override |  | ||||||
|     public void releasePeriod(MediaPeriod mediaPeriod) { |  | ||||||
|         mediaSource.releasePeriod(mediaPeriod); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Cleans up all internal custom objects creating during loading. |  | ||||||
|      * |  | ||||||
|      * This method is called when the parent {@link com.google.android.exoplayer2.source.MediaSource} |  | ||||||
|      * is released or when the player is stopped. |  | ||||||
|      * |  | ||||||
|      * This method should not release or set null the resources passed in through the constructor. |  | ||||||
|      * This method should not set null the internal {@link com.google.android.exoplayer2.source.MediaSource}. |  | ||||||
|      * */ |  | ||||||
|     @Override |  | ||||||
|     public void releaseSource() { |  | ||||||
|         if (mediaSource != null) { |  | ||||||
|             mediaSource.releaseSource(); |  | ||||||
|         } |  | ||||||
|         if (loader != null) { |  | ||||||
|             loader.dispose(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /* Do not set mediaSource as null here as it may be called through releasePeriod */ |  | ||||||
|         loader = null; |  | ||||||
|         exoPlayer = null; |  | ||||||
|         listener = null; |  | ||||||
|         error = null; |  | ||||||
|  |  | ||||||
|         state = STATE_INIT; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,96 +1,148 @@ | |||||||
| package org.schabi.newpipe.player.playback; | package org.schabi.newpipe.player.playback; | ||||||
|  |  | ||||||
|  | import android.support.annotation.NonNull; | ||||||
| import android.support.annotation.Nullable; | import android.support.annotation.Nullable; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
|  |  | ||||||
| import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; | import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; | ||||||
|  | import com.google.android.exoplayer2.source.MediaSource; | ||||||
|  | import com.google.android.exoplayer2.source.ShuffleOrder; | ||||||
|  |  | ||||||
| import org.reactivestreams.Subscriber; | import org.reactivestreams.Subscriber; | ||||||
| import org.reactivestreams.Subscription; | import org.reactivestreams.Subscription; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||||
|  | import org.schabi.newpipe.player.mediasource.FailedMediaSource; | ||||||
|  | import org.schabi.newpipe.player.mediasource.LoadedMediaSource; | ||||||
|  | import org.schabi.newpipe.player.mediasource.ManagedMediaSource; | ||||||
|  | import org.schabi.newpipe.player.mediasource.PlaceholderMediaSource; | ||||||
| import org.schabi.newpipe.playlist.PlayQueue; | import org.schabi.newpipe.playlist.PlayQueue; | ||||||
| import org.schabi.newpipe.playlist.PlayQueueItem; | import org.schabi.newpipe.playlist.PlayQueueItem; | ||||||
| import org.schabi.newpipe.playlist.events.MoveEvent; | import org.schabi.newpipe.playlist.events.MoveEvent; | ||||||
| import org.schabi.newpipe.playlist.events.PlayQueueEvent; | import org.schabi.newpipe.playlist.events.PlayQueueEvent; | ||||||
| import org.schabi.newpipe.playlist.events.RemoveEvent; | import org.schabi.newpipe.playlist.events.RemoveEvent; | ||||||
|  | import org.schabi.newpipe.playlist.events.ReorderEvent; | ||||||
|  |  | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.HashSet; | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  | import java.util.Set; | ||||||
| import java.util.concurrent.TimeUnit; | import java.util.concurrent.TimeUnit; | ||||||
|  | import java.util.concurrent.atomic.AtomicBoolean; | ||||||
|  |  | ||||||
|  | import io.reactivex.Single; | ||||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | import io.reactivex.android.schedulers.AndroidSchedulers; | ||||||
| import io.reactivex.annotations.NonNull; |  | ||||||
| import io.reactivex.disposables.CompositeDisposable; | import io.reactivex.disposables.CompositeDisposable; | ||||||
| import io.reactivex.disposables.Disposable; | import io.reactivex.disposables.Disposable; | ||||||
| import io.reactivex.disposables.SerialDisposable; | import io.reactivex.disposables.SerialDisposable; | ||||||
| import io.reactivex.functions.Consumer; | import io.reactivex.functions.Consumer; | ||||||
|  | import io.reactivex.internal.subscriptions.EmptySubscription; | ||||||
| import io.reactivex.subjects.PublishSubject; | import io.reactivex.subjects.PublishSubject; | ||||||
|  |  | ||||||
|  | import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; | ||||||
|  |  | ||||||
| public class MediaSourceManager { | public class MediaSourceManager { | ||||||
|     private final String TAG = "MediaSourceManager@" + Integer.toHexString(hashCode()); |     @NonNull private final static String TAG = "MediaSourceManager"; | ||||||
|     // One-side rolling window size for default loading |  | ||||||
|     // Effectively loads windowSize * 2 + 1 streams per call to load, must be greater than 0 |  | ||||||
|     private final int windowSize; |  | ||||||
|     private final PlaybackListener playbackListener; |  | ||||||
|     private final PlayQueue playQueue; |  | ||||||
|  |  | ||||||
|     // Process only the last load order when receiving a stream of load orders (lessens I/O) |     /** | ||||||
|     // The higher it is, the less loading occurs during rapid noncritical timeline changes |      * Determines how many streams before and after the current stream should be loaded. | ||||||
|     // Not recommended to go below 100ms |      * The default value (1) ensures seamless playback under typical network settings. | ||||||
|  |      * <br><br> | ||||||
|  |      * The streams after the current will be loaded into the playlist timeline while the | ||||||
|  |      * streams before will only be cached for future usage. | ||||||
|  |      * | ||||||
|  |      * @see #onMediaSourceReceived(PlayQueueItem, ManagedMediaSource) | ||||||
|  |      * @see #update(int, MediaSource, Runnable) | ||||||
|  |      * */ | ||||||
|  |     private final static int WINDOW_SIZE = 1; | ||||||
|  |  | ||||||
|  |     @NonNull private final PlaybackListener playbackListener; | ||||||
|  |     @NonNull private final PlayQueue playQueue; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Determines how long NEIGHBOURING {@link LoadedMediaSource} window of a currently playing | ||||||
|  |      * {@link MediaSource} is allowed to stay in the playlist timeline. This is to ensure | ||||||
|  |      * the {@link StreamInfo} used in subsequent playback is up-to-date. | ||||||
|  |      * <br><br> | ||||||
|  |      * Once a {@link LoadedMediaSource} has expired, a new source will be reloaded to | ||||||
|  |      * replace the expired one on whereupon {@link #loadImmediate()} is called. | ||||||
|  |      * | ||||||
|  |      * @see #loadImmediate() | ||||||
|  |      * @see #isCorrectionNeeded(PlayQueueItem) | ||||||
|  |      * */ | ||||||
|  |     private final long windowRefreshTimeMillis; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Process only the last load order when receiving a stream of load orders (lessens I/O). | ||||||
|  |      * <br><br> | ||||||
|  |      * The higher it is, the less loading occurs during rapid noncritical timeline changes. | ||||||
|  |      * <br><br> | ||||||
|  |      * Not recommended to go below 100ms. | ||||||
|  |      * | ||||||
|  |      * @see #loadDebounced() | ||||||
|  |      * */ | ||||||
|     private final long loadDebounceMillis; |     private final long loadDebounceMillis; | ||||||
|     private final PublishSubject<Long> debouncedLoadSignal; |     @NonNull private final Disposable debouncedLoader; | ||||||
|     private final Disposable debouncedLoader; |     @NonNull private final PublishSubject<Long> debouncedSignal; | ||||||
|  |  | ||||||
|     private final DeferredMediaSource.Callback sourceBuilder; |     @NonNull private Subscription playQueueReactor; | ||||||
|  |  | ||||||
|     private DynamicConcatenatingMediaSource sources; |     /** | ||||||
|  |      * Determines the maximum number of disposables allowed in the {@link #loaderReactor}. | ||||||
|  |      * Once exceeded, new calls to {@link #loadImmediate()} will evict all disposables in the | ||||||
|  |      * {@link #loaderReactor} in order to load a new set of items. | ||||||
|  |      * | ||||||
|  |      * @see #loadImmediate() | ||||||
|  |      * @see #maybeLoadItem(PlayQueueItem) | ||||||
|  |      * */ | ||||||
|  |     private final static int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1; | ||||||
|  |     @NonNull private final CompositeDisposable loaderReactor; | ||||||
|  |     @NonNull private final Set<PlayQueueItem> loadingItems; | ||||||
|  |     @NonNull private final SerialDisposable syncReactor; | ||||||
|  |  | ||||||
|     private Subscription playQueueReactor; |     @NonNull private final AtomicBoolean isBlocked; | ||||||
|     private SerialDisposable syncReactor; |  | ||||||
|  |  | ||||||
|     private PlayQueueItem syncedItem; |     @NonNull private DynamicConcatenatingMediaSource sources; | ||||||
|  |  | ||||||
|     private boolean isBlocked; |  | ||||||
|  |  | ||||||
|     public MediaSourceManager(@NonNull final PlaybackListener listener, |     public MediaSourceManager(@NonNull final PlaybackListener listener, | ||||||
|                               @NonNull final PlayQueue playQueue) { |                               @NonNull final PlayQueue playQueue) { | ||||||
|         this(listener, playQueue, 1, 400L); |         this(listener, playQueue, | ||||||
|  |                 /*loadDebounceMillis=*/400L, | ||||||
|  |                 /*windowRefreshTimeMillis=*/TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private MediaSourceManager(@NonNull final PlaybackListener listener, |     private MediaSourceManager(@NonNull final PlaybackListener listener, | ||||||
|                                @NonNull final PlayQueue playQueue, |                                @NonNull final PlayQueue playQueue, | ||||||
|                                final int windowSize, |                                final long loadDebounceMillis, | ||||||
|                                final long loadDebounceMillis) { |                                final long windowRefreshTimeMillis) { | ||||||
|         if (windowSize <= 0) { |         if (playQueue.getBroadcastReceiver() == null) { | ||||||
|             throw new UnsupportedOperationException("MediaSourceManager window size must be greater than 0"); |             throw new IllegalArgumentException("Play Queue has not been initialized."); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         this.playbackListener = listener; |         this.playbackListener = listener; | ||||||
|         this.playQueue = playQueue; |         this.playQueue = playQueue; | ||||||
|         this.windowSize = windowSize; |  | ||||||
|         this.loadDebounceMillis = loadDebounceMillis; |  | ||||||
|  |  | ||||||
|         this.syncReactor = new SerialDisposable(); |         this.windowRefreshTimeMillis = windowRefreshTimeMillis; | ||||||
|         this.debouncedLoadSignal = PublishSubject.create(); |  | ||||||
|  |         this.loadDebounceMillis = loadDebounceMillis; | ||||||
|  |         this.debouncedSignal = PublishSubject.create(); | ||||||
|         this.debouncedLoader = getDebouncedLoader(); |         this.debouncedLoader = getDebouncedLoader(); | ||||||
|  |  | ||||||
|         this.sourceBuilder = getSourceBuilder(); |         this.playQueueReactor = EmptySubscription.INSTANCE; | ||||||
|  |         this.loaderReactor = new CompositeDisposable(); | ||||||
|  |         this.syncReactor = new SerialDisposable(); | ||||||
|  |  | ||||||
|  |         this.isBlocked = new AtomicBoolean(false); | ||||||
|  |  | ||||||
|         this.sources = new DynamicConcatenatingMediaSource(); |         this.sources = new DynamicConcatenatingMediaSource(); | ||||||
|  |  | ||||||
|  |         this.loadingItems = Collections.synchronizedSet(new HashSet<>()); | ||||||
|  |  | ||||||
|         playQueue.getBroadcastReceiver() |         playQueue.getBroadcastReceiver() | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe(getReactor()); |                 .subscribe(getReactor()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |  | ||||||
|     // DeferredMediaSource listener |  | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |  | ||||||
|  |  | ||||||
|     private DeferredMediaSource.Callback getSourceBuilder() { |  | ||||||
|         return playbackListener::sourceOf; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Exposed Methods |     // Exposed Methods | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -98,16 +150,15 @@ public class MediaSourceManager { | |||||||
|      * Dispose the manager and releases all message buses and loaders. |      * Dispose the manager and releases all message buses and loaders. | ||||||
|      * */ |      * */ | ||||||
|     public void dispose() { |     public void dispose() { | ||||||
|         if (debouncedLoadSignal != null) debouncedLoadSignal.onComplete(); |         if (DEBUG) Log.d(TAG, "dispose() called."); | ||||||
|         if (debouncedLoader != null) debouncedLoader.dispose(); |  | ||||||
|         if (playQueueReactor != null) playQueueReactor.cancel(); |  | ||||||
|         if (syncReactor != null) syncReactor.dispose(); |  | ||||||
|         if (sources != null) sources.releaseSource(); |  | ||||||
|  |  | ||||||
|         playQueueReactor = null; |         debouncedSignal.onComplete(); | ||||||
|         syncReactor = null; |         debouncedLoader.dispose(); | ||||||
|         syncedItem = null; |  | ||||||
|         sources = null; |         playQueueReactor.cancel(); | ||||||
|  |         loaderReactor.dispose(); | ||||||
|  |         syncReactor.dispose(); | ||||||
|  |         sources.releaseSource(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -116,18 +167,20 @@ public class MediaSourceManager { | |||||||
|      * Unblocks the player once the item at the current index is loaded. |      * Unblocks the player once the item at the current index is loaded. | ||||||
|      * */ |      * */ | ||||||
|     public void load() { |     public void load() { | ||||||
|  |         if (DEBUG) Log.d(TAG, "load() called."); | ||||||
|         loadDebounced(); |         loadDebounced(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Blocks the player and repopulate the sources. |      * Blocks the player and repopulate the sources. | ||||||
|      * |      * | ||||||
|      * Does not ensure the player is unblocked and should be done explicitly through {@link #load() load}. |      * Does not ensure the player is unblocked and should be done explicitly | ||||||
|  |      * through {@link #load() load}. | ||||||
|      * */ |      * */ | ||||||
|     public void reset() { |     public void reset() { | ||||||
|         tryBlock(); |         if (DEBUG) Log.d(TAG, "reset() called."); | ||||||
|  |  | ||||||
|         syncedItem = null; |         maybeBlock(); | ||||||
|         populateSources(); |         populateSources(); | ||||||
|     } |     } | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
| @@ -138,14 +191,14 @@ public class MediaSourceManager { | |||||||
|         return new Subscriber<PlayQueueEvent>() { |         return new Subscriber<PlayQueueEvent>() { | ||||||
|             @Override |             @Override | ||||||
|             public void onSubscribe(@NonNull Subscription d) { |             public void onSubscribe(@NonNull Subscription d) { | ||||||
|                 if (playQueueReactor != null) playQueueReactor.cancel(); |                 playQueueReactor.cancel(); | ||||||
|                 playQueueReactor = d; |                 playQueueReactor = d; | ||||||
|                 playQueueReactor.request(1); |                 playQueueReactor.request(1); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
|             public void onNext(@NonNull PlayQueueEvent playQueueMessage) { |             public void onNext(@NonNull PlayQueueEvent playQueueMessage) { | ||||||
|                 if (playQueueReactor != null) onPlayQueueChanged(playQueueMessage); |                 onPlayQueueChanged(playQueueMessage); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             @Override |             @Override | ||||||
| @@ -158,14 +211,13 @@ public class MediaSourceManager { | |||||||
|  |  | ||||||
|     private void onPlayQueueChanged(final PlayQueueEvent event) { |     private void onPlayQueueChanged(final PlayQueueEvent event) { | ||||||
|         if (playQueue.isEmpty() && playQueue.isComplete()) { |         if (playQueue.isEmpty() && playQueue.isComplete()) { | ||||||
|             playbackListener.shutdown(); |             playbackListener.onPlaybackShutdown(); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Event specific action |         // Event specific action | ||||||
|         switch (event.type()) { |         switch (event.type()) { | ||||||
|             case INIT: |             case INIT: | ||||||
|             case REORDER: |  | ||||||
|             case ERROR: |             case ERROR: | ||||||
|                 reset(); |                 reset(); | ||||||
|                 break; |                 break; | ||||||
| @@ -180,6 +232,12 @@ public class MediaSourceManager { | |||||||
|                 final MoveEvent moveEvent = (MoveEvent) event; |                 final MoveEvent moveEvent = (MoveEvent) event; | ||||||
|                 move(moveEvent.getFromIndex(), moveEvent.getToIndex()); |                 move(moveEvent.getFromIndex(), moveEvent.getToIndex()); | ||||||
|                 break; |                 break; | ||||||
|  |             case REORDER: | ||||||
|  |                 // Need to move to ensure the playing index from play queue matches that of | ||||||
|  |                 // the source timeline, and then window correction can take care of the rest | ||||||
|  |                 final ReorderEvent reorderEvent = (ReorderEvent) event; | ||||||
|  |                 move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex()); | ||||||
|  |                 break; | ||||||
|             case SELECT: |             case SELECT: | ||||||
|             case RECOVERY: |             case RECOVERY: | ||||||
|             default: |             default: | ||||||
| @@ -191,11 +249,11 @@ public class MediaSourceManager { | |||||||
|             case INIT: |             case INIT: | ||||||
|             case REORDER: |             case REORDER: | ||||||
|             case ERROR: |             case ERROR: | ||||||
|  |             case SELECT: | ||||||
|                 loadImmediate(); // low frequency, critical events |                 loadImmediate(); // low frequency, critical events | ||||||
|                 break; |                 break; | ||||||
|             case APPEND: |             case APPEND: | ||||||
|             case REMOVE: |             case REMOVE: | ||||||
|             case SELECT: |  | ||||||
|             case MOVE: |             case MOVE: | ||||||
|             case RECOVERY: |             case RECOVERY: | ||||||
|             default: |             default: | ||||||
| @@ -204,69 +262,100 @@ public class MediaSourceManager { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (!isPlayQueueReady()) { |         if (!isPlayQueueReady()) { | ||||||
|             tryBlock(); |             maybeBlock(); | ||||||
|             playQueue.fetch(); |             playQueue.fetch(); | ||||||
|         } |         } | ||||||
|         if (playQueueReactor != null) playQueueReactor.request(1); |         playQueueReactor.request(1); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Internal Helpers |     // Playback Locking | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     private boolean isPlayQueueReady() { |     private boolean isPlayQueueReady() { | ||||||
|         return playQueue.isComplete() || playQueue.size() - playQueue.getIndex() > windowSize; |         final boolean isWindowLoaded = playQueue.size() - playQueue.getIndex() > WINDOW_SIZE; | ||||||
|  |         return playQueue.isComplete() || isWindowLoaded; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private boolean tryBlock() { |     private boolean isPlaybackReady() { | ||||||
|         if (!isBlocked) { |         if (sources.getSize() != playQueue.size()) return false; | ||||||
|             playbackListener.block(); |  | ||||||
|  |         final MediaSource mediaSource = sources.getMediaSource(playQueue.getIndex()); | ||||||
|  |         final PlayQueueItem playQueueItem = playQueue.getItem(); | ||||||
|  |  | ||||||
|  |         if (mediaSource instanceof LoadedMediaSource) { | ||||||
|  |             return playQueueItem == ((LoadedMediaSource) mediaSource).getStream(); | ||||||
|  |         } else if (mediaSource instanceof FailedMediaSource) { | ||||||
|  |             return playQueueItem == ((FailedMediaSource) mediaSource).getStream(); | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void maybeBlock() { | ||||||
|  |         if (DEBUG) Log.d(TAG, "maybeBlock() called."); | ||||||
|  |  | ||||||
|  |         if (isBlocked.get()) return; | ||||||
|  |  | ||||||
|  |         playbackListener.onPlaybackBlock(); | ||||||
|         resetSources(); |         resetSources(); | ||||||
|             isBlocked = true; |  | ||||||
|             return true; |         isBlocked.set(true); | ||||||
|         } |  | ||||||
|         return false; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private boolean tryUnblock() { |     private void maybeUnblock() { | ||||||
|         if (isPlayQueueReady() && isBlocked && sources != null) { |         if (DEBUG) Log.d(TAG, "maybeUnblock() called."); | ||||||
|             isBlocked = false; |  | ||||||
|             playbackListener.unblock(sources); |         if (isPlayQueueReady() && isPlaybackReady() && isBlocked.get()) { | ||||||
|             return true; |             isBlocked.set(false); | ||||||
|  |             playbackListener.onPlaybackUnblock(sources); | ||||||
|         } |         } | ||||||
|         return false; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void sync() { |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Metadata Synchronization | ||||||
|  |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     private void maybeSync() { | ||||||
|  |         if (DEBUG) Log.d(TAG, "onPlaybackSynchronize() called."); | ||||||
|  |  | ||||||
|         final PlayQueueItem currentItem = playQueue.getItem(); |         final PlayQueueItem currentItem = playQueue.getItem(); | ||||||
|         if (currentItem == null) return; |         if (isBlocked.get() || currentItem == null) return; | ||||||
|  |  | ||||||
|         final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info); |         final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info); | ||||||
|         final Consumer<Throwable> onError = throwable -> { |         final Consumer<Throwable> onError = throwable -> syncInternal(currentItem, null); | ||||||
|             Log.e(TAG, "Sync error:", throwable); |  | ||||||
|             syncInternal(currentItem, null); |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         if (syncedItem != currentItem) { |  | ||||||
|             syncedItem = currentItem; |  | ||||||
|         final Disposable sync = currentItem.getStream() |         final Disposable sync = currentItem.getStream() | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe(onSuccess, onError); |                 .subscribe(onSuccess, onError); | ||||||
|         syncReactor.set(sync); |         syncReactor.set(sync); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private void syncInternal(@NonNull final PlayQueueItem item, | ||||||
|  |                               @Nullable final StreamInfo info) { | ||||||
|  |         // Ensure the current item is up to date with the play queue | ||||||
|  |         if (playQueue.getItem() == item) { | ||||||
|  |             playbackListener.onPlaybackSynchronize(item, info); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void syncInternal(@android.support.annotation.NonNull final PlayQueueItem item, |     private void maybeSynchronizePlayer() { | ||||||
|                               @Nullable final StreamInfo info) { |         maybeUnblock(); | ||||||
|         if (playQueue == null || playbackListener == null) return; |         maybeSync(); | ||||||
|         // Ensure the current item is up to date with the play queue |  | ||||||
|         if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) { |  | ||||||
|             playbackListener.sync(syncedItem,info); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // MediaSource Loading | ||||||
|  |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     private Disposable getDebouncedLoader() { | ||||||
|  |         return debouncedSignal | ||||||
|  |                 .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) | ||||||
|  |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                 .subscribe(timestamp -> loadImmediate()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void loadDebounced() { |     private void loadDebounced() { | ||||||
|         debouncedLoadSignal.onNext(System.currentTimeMillis()); |         debouncedSignal.onNext(System.currentTimeMillis()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void loadImmediate() { |     private void loadImmediate() { | ||||||
| @@ -274,87 +363,182 @@ public class MediaSourceManager { | |||||||
|         final int currentIndex = playQueue.getIndex(); |         final int currentIndex = playQueue.getIndex(); | ||||||
|         final PlayQueueItem currentItem = playQueue.getItem(currentIndex); |         final PlayQueueItem currentItem = playQueue.getItem(currentIndex); | ||||||
|         if (currentItem == null) return; |         if (currentItem == null) return; | ||||||
|         loadItem(currentItem); |  | ||||||
|  |         // Evict the items being loaded to free up memory | ||||||
|  |         if (!loadingItems.contains(currentItem) && loaderReactor.size() > MAXIMUM_LOADER_SIZE) { | ||||||
|  |             loaderReactor.clear(); | ||||||
|  |             loadingItems.clear(); | ||||||
|  |         } | ||||||
|  |         maybeLoadItem(currentItem); | ||||||
|  |  | ||||||
|         // The rest are just for seamless playback |         // The rest are just for seamless playback | ||||||
|         final int leftBound = Math.max(0, currentIndex - windowSize); |         // Although timeline is not updated prior to the current index, these sources are still | ||||||
|         final int rightLimit = currentIndex + windowSize + 1; |         // loaded into the cache for faster retrieval at a potentially later time. | ||||||
|  |         final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE); | ||||||
|  |         final int rightLimit = currentIndex + WINDOW_SIZE + 1; | ||||||
|         final int rightBound = Math.min(playQueue.size(), rightLimit); |         final int rightBound = Math.min(playQueue.size(), rightLimit); | ||||||
|         final List<PlayQueueItem> items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound)); |         final List<PlayQueueItem> items = new ArrayList<>( | ||||||
|  |                 playQueue.getStreams().subList(leftBound,rightBound)); | ||||||
|  |  | ||||||
|         // Do a round robin |         // Do a round robin | ||||||
|         final int excess = rightLimit - playQueue.size(); |         final int excess = rightLimit - playQueue.size(); | ||||||
|         if (excess >= 0) items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); |         if (excess >= 0) { | ||||||
|  |             items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); | ||||||
|         for (final PlayQueueItem item: items) loadItem(item); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     private void loadItem(@Nullable final PlayQueueItem item) { |         for (final PlayQueueItem item : items) { | ||||||
|         if (item == null) return; |             maybeLoadItem(item); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void maybeLoadItem(@NonNull final PlayQueueItem item) { | ||||||
|  |         if (DEBUG) Log.d(TAG, "maybeLoadItem() called."); | ||||||
|  |         if (playQueue.indexOf(item) >= sources.getSize()) return; | ||||||
|  |  | ||||||
|  |         if (!loadingItems.contains(item) && isCorrectionNeeded(item)) { | ||||||
|  |             if (DEBUG) Log.d(TAG, "MediaSource - Loading=[" + item.getTitle() + | ||||||
|  |                     "] with url=[" + item.getUrl() + "]"); | ||||||
|  |  | ||||||
|  |             loadingItems.add(item); | ||||||
|  |             final Disposable loader = getLoadedMediaSource(item) | ||||||
|  |                     .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                     /* No exception handling since getLoadedMediaSource guarantees nonnull return */ | ||||||
|  |                     .subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource)); | ||||||
|  |             loaderReactor.add(loader); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         maybeSynchronizePlayer(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) { | ||||||
|  |         return stream.getStream().map(streamInfo -> { | ||||||
|  |             final MediaSource source = playbackListener.sourceOf(stream, streamInfo); | ||||||
|  |             if (source == null) { | ||||||
|  |                 final Exception exception = new IllegalStateException( | ||||||
|  |                         "Unable to resolve source from stream info." + | ||||||
|  |                                 " URL: " + stream.getUrl() + | ||||||
|  |                                 ", audio count: " + streamInfo.audio_streams.size() + | ||||||
|  |                                 ", video count: " + streamInfo.video_only_streams.size() + | ||||||
|  |                                 streamInfo.video_streams.size()); | ||||||
|  |                 return new FailedMediaSource(stream, exception); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             final long expiration = System.currentTimeMillis() + windowRefreshTimeMillis; | ||||||
|  |             return new LoadedMediaSource(source, stream, expiration); | ||||||
|  |         }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void onMediaSourceReceived(@NonNull final PlayQueueItem item, | ||||||
|  |                                        @NonNull final ManagedMediaSource mediaSource) { | ||||||
|  |         if (DEBUG) Log.d(TAG, "MediaSource - Loaded=[" + item.getTitle() + | ||||||
|  |                 "] with url=[" + item.getUrl() + "]"); | ||||||
|  |  | ||||||
|  |         loadingItems.remove(item); | ||||||
|  |  | ||||||
|  |         final int itemIndex = playQueue.indexOf(item); | ||||||
|  |         // Only update the playlist timeline for items at the current index or after. | ||||||
|  |         if (itemIndex >= playQueue.getIndex() && isCorrectionNeeded(item)) { | ||||||
|  |             if (DEBUG) Log.d(TAG, "MediaSource - Updating index=[" + itemIndex + "] with " + | ||||||
|  |                     "title=[" + item.getTitle() + "] at url=[" + item.getUrl() + "]"); | ||||||
|  |             update(itemIndex, mediaSource, this::maybeSynchronizePlayer); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Checks if the corresponding MediaSource in {@link DynamicConcatenatingMediaSource} | ||||||
|  |      * for a given {@link PlayQueueItem} needs replacement, either due to gapless playback | ||||||
|  |      * readiness or playlist desynchronization. | ||||||
|  |      * <br><br> | ||||||
|  |      * If the given {@link PlayQueueItem} is currently being played and is already loaded, | ||||||
|  |      * then correction is not only needed if the playlist is desynchronized. Otherwise, the | ||||||
|  |      * check depends on the status (e.g. expiration or placeholder) of the | ||||||
|  |      * {@link ManagedMediaSource}. | ||||||
|  |      * */ | ||||||
|  |     private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) { | ||||||
|         final int index = playQueue.indexOf(item); |         final int index = playQueue.indexOf(item); | ||||||
|         if (index > sources.getSize() - 1) return; |         if (index == -1 || index >= sources.getSize()) return false; | ||||||
|  |  | ||||||
|         final DeferredMediaSource mediaSource = (DeferredMediaSource) sources.getMediaSource(playQueue.indexOf(item)); |         final ManagedMediaSource mediaSource = (ManagedMediaSource) sources.getMediaSource(index); | ||||||
|         if (mediaSource.state() == DeferredMediaSource.STATE_PREPARED) mediaSource.load(); |  | ||||||
|  |  | ||||||
|         tryUnblock(); |         if (index == playQueue.getIndex() && mediaSource instanceof LoadedMediaSource) { | ||||||
|         if (!isBlocked) sync(); |             return item != ((LoadedMediaSource) mediaSource).getStream(); | ||||||
|  |         } else { | ||||||
|  |             return mediaSource.canReplace(item); | ||||||
|         } |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // MediaSource Playlist Helpers | ||||||
|  |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     private void resetSources() { |     private void resetSources() { | ||||||
|         if (this.sources != null) this.sources.releaseSource(); |         if (DEBUG) Log.d(TAG, "resetSources() called."); | ||||||
|         this.sources = new DynamicConcatenatingMediaSource(); |  | ||||||
|  |         this.sources.releaseSource(); | ||||||
|  |         this.sources = new DynamicConcatenatingMediaSource(false, | ||||||
|  |                 new ShuffleOrder.UnshuffledShuffleOrder(0)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void populateSources() { |     private void populateSources() { | ||||||
|         if (sources == null) return; |         if (DEBUG) Log.d(TAG, "populateSources() called."); | ||||||
|  |         if (sources.getSize() >= playQueue.size()) return; | ||||||
|  |  | ||||||
|         for (final PlayQueueItem item : playQueue.getStreams()) { |         for (int index = sources.getSize() - 1; index < playQueue.size(); index++) { | ||||||
|             insert(playQueue.indexOf(item), new DeferredMediaSource(item, sourceBuilder)); |             emplace(index, new PlaceholderMediaSource()); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private Disposable getDebouncedLoader() { |  | ||||||
|         return debouncedLoadSignal |  | ||||||
|                 .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) |  | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |  | ||||||
|                 .subscribe(timestamp -> loadImmediate()); |  | ||||||
|     } |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Media Source List Manipulation |     // MediaSource Playlist Manipulation | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Inserts a source into {@link DynamicConcatenatingMediaSource} with position |      * Places a {@link MediaSource} into the {@link DynamicConcatenatingMediaSource} | ||||||
|      * in respect to the play queue. |      * with position in respect to the play queue only if no {@link MediaSource} | ||||||
|      * |      * already exists at the given index. | ||||||
|      * If the play queue index already exists, then the insert is ignored. |  | ||||||
|      * */ |      * */ | ||||||
|     private void insert(final int queueIndex, final DeferredMediaSource source) { |     private synchronized void emplace(final int index, @NonNull final MediaSource source) { | ||||||
|         if (sources == null) return; |         if (index < sources.getSize()) return; | ||||||
|         if (queueIndex < 0 || queueIndex < sources.getSize()) return; |  | ||||||
|  |  | ||||||
|         sources.addMediaSource(queueIndex, source); |         sources.addMediaSource(index, source); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Removes a source from {@link DynamicConcatenatingMediaSource} with the given play queue index. |      * Removes a {@link MediaSource} from {@link DynamicConcatenatingMediaSource} | ||||||
|      * |      * at the given index. If this index is out of bound, then the removal is ignored. | ||||||
|      * If the play queue index does not exist, the removal is ignored. |  | ||||||
|      * */ |      * */ | ||||||
|     private void remove(final int queueIndex) { |     private synchronized void remove(final int index) { | ||||||
|         if (sources == null) return; |         if (index < 0 || index > sources.getSize()) return; | ||||||
|         if (queueIndex < 0 || queueIndex > sources.getSize()) return; |  | ||||||
|  |  | ||||||
|         sources.removeMediaSource(queueIndex); |         sources.removeMediaSource(index); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void move(final int source, final int target) { |     /** | ||||||
|         if (sources == null) return; |      * Moves a {@link MediaSource} in {@link DynamicConcatenatingMediaSource} | ||||||
|  |      * from the given source index to the target index. If either index is out of bound, | ||||||
|  |      * then the call is ignored. | ||||||
|  |      * */ | ||||||
|  |     private synchronized void move(final int source, final int target) { | ||||||
|         if (source < 0 || target < 0) return; |         if (source < 0 || target < 0) return; | ||||||
|         if (source >= sources.getSize() || target >= sources.getSize()) return; |         if (source >= sources.getSize() || target >= sources.getSize()) return; | ||||||
|  |  | ||||||
|         sources.moveMediaSource(source, target); |         sources.moveMediaSource(source, target); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Updates the {@link MediaSource} in {@link DynamicConcatenatingMediaSource} | ||||||
|  |      * at the given index with a given {@link MediaSource}. If the index is out of bound, | ||||||
|  |      * then the replacement is ignored. | ||||||
|  |      * <br><br> | ||||||
|  |      * Not recommended to use on indices LESS THAN the currently playing index, since | ||||||
|  |      * this will modify the playback timeline prior to the index and may cause desynchronization | ||||||
|  |      * on the playing item between {@link PlayQueue} and {@link DynamicConcatenatingMediaSource}. | ||||||
|  |      * */ | ||||||
|  |     private synchronized void update(final int index, @NonNull final MediaSource source, | ||||||
|  |                                      @Nullable final Runnable finalizingAction) { | ||||||
|  |         if (index < 0 || index >= sources.getSize()) return; | ||||||
|  |  | ||||||
|  |         sources.addMediaSource(index + 1, source, () -> | ||||||
|  |                 sources.removeMediaSource(index, finalizingAction)); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ public interface PlaybackListener { | |||||||
|      * |      * | ||||||
|      * May be called at any time. |      * May be called at any time. | ||||||
|      * */ |      * */ | ||||||
|     void block(); |     void onPlaybackBlock(); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Called when the stream at the current queue index is ready. |      * Called when the stream at the current queue index is ready. | ||||||
| @@ -26,18 +26,16 @@ public interface PlaybackListener { | |||||||
|      * |      * | ||||||
|      * May be called only when the player is blocked. |      * May be called only when the player is blocked. | ||||||
|      * */ |      * */ | ||||||
|     void unblock(final MediaSource mediaSource); |     void onPlaybackUnblock(final MediaSource mediaSource); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Called when the queue index is refreshed. |      * Called when the queue index is refreshed. | ||||||
|      * Signals to the listener to synchronize the player's window to the manager's |      * Signals to the listener to synchronize the player's window to the manager's | ||||||
|      * window. |      * window. | ||||||
|      * |      * | ||||||
|      * Occurs once only per play queue item change. |      * May be called anytime at any amount once unblock is called. | ||||||
|      * |  | ||||||
|      * May be called only after unblock is called. |  | ||||||
|      * */ |      * */ | ||||||
|     void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info); |     void onPlaybackSynchronize(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Requests the listener to resolve a stream info into a media source |      * Requests the listener to resolve a stream info into a media source | ||||||
| @@ -55,5 +53,5 @@ public interface PlaybackListener { | |||||||
|      * |      * | ||||||
|      * May be called at any time. |      * May be called at any time. | ||||||
|      * */ |      * */ | ||||||
|     void shutdown(); |     void onPlaybackShutdown(); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -118,6 +118,7 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext | |||||||
|     public void dispose() { |     public void dispose() { | ||||||
|         super.dispose(); |         super.dispose(); | ||||||
|         if (fetchReactor != null) fetchReactor.dispose(); |         if (fetchReactor != null) fetchReactor.dispose(); | ||||||
|  |         fetchReactor = null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private static List<PlayQueueItem> extractListItems(final List<InfoItem> infos) { |     private static List<PlayQueueItem> extractListItems(final List<InfoItem> infos) { | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| package org.schabi.newpipe.playlist; | package org.schabi.newpipe.playlist; | ||||||
|  |  | ||||||
| import android.support.annotation.NonNull; | import android.support.annotation.NonNull; | ||||||
|  | import android.support.annotation.Nullable; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
|  |  | ||||||
| import org.reactivestreams.Subscriber; | import org.reactivestreams.Subscriber; | ||||||
| @@ -44,7 +45,7 @@ public abstract class PlayQueue implements Serializable { | |||||||
|  |  | ||||||
|     private ArrayList<PlayQueueItem> backup; |     private ArrayList<PlayQueueItem> backup; | ||||||
|     private ArrayList<PlayQueueItem> streams; |     private ArrayList<PlayQueueItem> streams; | ||||||
|     private final AtomicInteger queueIndex; |     @NonNull private final AtomicInteger queueIndex; | ||||||
|  |  | ||||||
|     private transient BehaviorSubject<PlayQueueEvent> eventBroadcast; |     private transient BehaviorSubject<PlayQueueEvent> eventBroadcast; | ||||||
|     private transient Flowable<PlayQueueEvent> broadcastReceiver; |     private transient Flowable<PlayQueueEvent> broadcastReceiver; | ||||||
| @@ -83,6 +84,7 @@ public abstract class PlayQueue implements Serializable { | |||||||
|         if (eventBroadcast != null) eventBroadcast.onComplete(); |         if (eventBroadcast != null) eventBroadcast.onComplete(); | ||||||
|         if (reportingReactor != null) reportingReactor.cancel(); |         if (reportingReactor != null) reportingReactor.cancel(); | ||||||
|  |  | ||||||
|  |         eventBroadcast = null; | ||||||
|         broadcastReceiver = null; |         broadcastReceiver = null; | ||||||
|         reportingReactor = null; |         reportingReactor = null; | ||||||
|     } |     } | ||||||
| @@ -131,7 +133,7 @@ public abstract class PlayQueue implements Serializable { | |||||||
|      * Returns the index of the given item using referential equality. |      * Returns the index of the given item using referential equality. | ||||||
|      * May be null despite play queue contains identical item. |      * May be null despite play queue contains identical item. | ||||||
|      * */ |      * */ | ||||||
|     public int indexOf(final PlayQueueItem item) { |     public int indexOf(@NonNull final PlayQueueItem item) { | ||||||
|         // referential equality, can't think of a better way to do this |         // referential equality, can't think of a better way to do this | ||||||
|         // todo: better than this |         // todo: better than this | ||||||
|         return streams.indexOf(item); |         return streams.indexOf(item); | ||||||
| @@ -170,7 +172,7 @@ public abstract class PlayQueue implements Serializable { | |||||||
|      * Returns the play queue's update broadcast. |      * Returns the play queue's update broadcast. | ||||||
|      * May be null if the play queue message bus is not initialized. |      * May be null if the play queue message bus is not initialized. | ||||||
|      * */ |      * */ | ||||||
|     @NonNull |     @Nullable | ||||||
|     public Flowable<PlayQueueEvent> getBroadcastReceiver() { |     public Flowable<PlayQueueEvent> getBroadcastReceiver() { | ||||||
|         return broadcastReceiver; |         return broadcastReceiver; | ||||||
|     } |     } | ||||||
| @@ -211,7 +213,7 @@ public abstract class PlayQueue implements Serializable { | |||||||
|      * |      * | ||||||
|      * @see #append(List items) |      * @see #append(List items) | ||||||
|      * */ |      * */ | ||||||
|     public synchronized void append(final PlayQueueItem... items) { |     public synchronized void append(@NonNull final PlayQueueItem... items) { | ||||||
|         append(Arrays.asList(items)); |         append(Arrays.asList(items)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -223,7 +225,7 @@ public abstract class PlayQueue implements Serializable { | |||||||
|      * |      * | ||||||
|      * Will emit a {@link AppendEvent} on any given context. |      * Will emit a {@link AppendEvent} on any given context. | ||||||
|      * */ |      * */ | ||||||
|     public synchronized void append(final List<PlayQueueItem> items) { |     public synchronized void append(@NonNull final List<PlayQueueItem> items) { | ||||||
|         List<PlayQueueItem> itemList = new ArrayList<>(items); |         List<PlayQueueItem> itemList = new ArrayList<>(items); | ||||||
|  |  | ||||||
|         if (isShuffled()) { |         if (isShuffled()) { | ||||||
| @@ -349,6 +351,7 @@ public abstract class PlayQueue implements Serializable { | |||||||
|         if (backup == null) { |         if (backup == null) { | ||||||
|             backup = new ArrayList<>(streams); |             backup = new ArrayList<>(streams); | ||||||
|         } |         } | ||||||
|  |         final int originIndex = getIndex(); | ||||||
|         final PlayQueueItem current = getItem(); |         final PlayQueueItem current = getItem(); | ||||||
|         Collections.shuffle(streams); |         Collections.shuffle(streams); | ||||||
|  |  | ||||||
| @@ -358,7 +361,7 @@ public abstract class PlayQueue implements Serializable { | |||||||
|         } |         } | ||||||
|         queueIndex.set(0); |         queueIndex.set(0); | ||||||
|  |  | ||||||
|         broadcast(new ReorderEvent()); |         broadcast(new ReorderEvent(originIndex, queueIndex.get())); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -371,6 +374,7 @@ public abstract class PlayQueue implements Serializable { | |||||||
|      * */ |      * */ | ||||||
|     public synchronized void unshuffle() { |     public synchronized void unshuffle() { | ||||||
|         if (backup == null) return; |         if (backup == null) return; | ||||||
|  |         final int originIndex = getIndex(); | ||||||
|         final PlayQueueItem current = getItem(); |         final PlayQueueItem current = getItem(); | ||||||
|  |  | ||||||
|         streams.clear(); |         streams.clear(); | ||||||
| @@ -384,14 +388,14 @@ public abstract class PlayQueue implements Serializable { | |||||||
|             queueIndex.set(0); |             queueIndex.set(0); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         broadcast(new ReorderEvent()); |         broadcast(new ReorderEvent(originIndex, queueIndex.get())); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Rx Broadcast |     // Rx Broadcast | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     private void broadcast(final PlayQueueEvent event) { |     private void broadcast(@NonNull final PlayQueueEvent event) { | ||||||
|         if (eventBroadcast != null) { |         if (eventBroadcast != null) { | ||||||
|             eventBroadcast.onNext(event); |             eventBroadcast.onNext(event); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -63,22 +63,18 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     public PlayQueueAdapter(final Context context, final PlayQueue playQueue) { |     public PlayQueueAdapter(final Context context, final PlayQueue playQueue) { | ||||||
|  |         if (playQueue.getBroadcastReceiver() == null) { | ||||||
|  |             throw new IllegalStateException("Play Queue has not been initialized."); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         this.playQueueItemBuilder = new PlayQueueItemBuilder(context); |         this.playQueueItemBuilder = new PlayQueueItemBuilder(context); | ||||||
|         this.playQueue = playQueue; |         this.playQueue = playQueue; | ||||||
|  |  | ||||||
|         startReactor(); |         playQueue.getBroadcastReceiver().toObservable().subscribe(getReactor()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void setSelectedListener(final PlayQueueItemBuilder.OnSelectedListener listener) { |     private Observer<PlayQueueEvent> getReactor() { | ||||||
|         playQueueItemBuilder.setOnSelectedListener(listener); |         return new Observer<PlayQueueEvent>() { | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void unsetSelectedListener() { |  | ||||||
|         playQueueItemBuilder.setOnSelectedListener(null); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void startReactor() { |  | ||||||
|         final Observer<PlayQueueEvent> observer = new Observer<PlayQueueEvent>() { |  | ||||||
|             @Override |             @Override | ||||||
|             public void onSubscribe(@NonNull Disposable d) { |             public void onSubscribe(@NonNull Disposable d) { | ||||||
|                 if (playQueueReactor != null) playQueueReactor.dispose(); |                 if (playQueueReactor != null) playQueueReactor.dispose(); | ||||||
| @@ -99,7 +95,6 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold | |||||||
|             } |             } | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         playQueue.getBroadcastReceiver().toObservable().subscribe(observer); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void onPlayQueueChanged(final PlayQueueEvent message) { |     private void onPlayQueueChanged(final PlayQueueEvent message) { | ||||||
| @@ -146,6 +141,14 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold | |||||||
|         playQueueReactor = null; |         playQueueReactor = null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public void setSelectedListener(final PlayQueueItemBuilder.OnSelectedListener listener) { | ||||||
|  |         playQueueItemBuilder.setOnSelectedListener(listener); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void unsetSelectedListener() { | ||||||
|  |         playQueueItemBuilder.setOnSelectedListener(null); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public void setFooter(View footer) { |     public void setFooter(View footer) { | ||||||
|         this.footer = footer; |         this.footer = footer; | ||||||
|         notifyItemChanged(playQueue.size()); |         notifyItemChanged(playQueue.size()); | ||||||
|   | |||||||
| @@ -1,12 +1,24 @@ | |||||||
| package org.schabi.newpipe.playlist.events; | package org.schabi.newpipe.playlist.events; | ||||||
|  |  | ||||||
| public class ReorderEvent implements PlayQueueEvent { | public class ReorderEvent implements PlayQueueEvent { | ||||||
|  |     private final int fromSelectedIndex; | ||||||
|  |     private final int toSelectedIndex; | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public PlayQueueEventType type() { |     public PlayQueueEventType type() { | ||||||
|         return PlayQueueEventType.REORDER; |         return PlayQueueEventType.REORDER; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public ReorderEvent() { |     public ReorderEvent(final int fromSelectedIndex, final int toSelectedIndex) { | ||||||
|  |         this.fromSelectedIndex = fromSelectedIndex; | ||||||
|  |         this.toSelectedIndex = toSelectedIndex; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public int getFromSelectedIndex() { | ||||||
|  |         return fromSelectedIndex; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public int getToSelectedIndex() { | ||||||
|  |         return toSelectedIndex; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -172,7 +172,7 @@ public final class ExtractorHelper { | |||||||
|                                                          String url, |                                                          String url, | ||||||
|                                                          Single<I> loadFromNetwork) { |                                                          Single<I> loadFromNetwork) { | ||||||
|         checkServiceId(serviceId); |         checkServiceId(serviceId); | ||||||
|         loadFromNetwork = loadFromNetwork.doOnSuccess((@NonNull I i) -> cache.putInfo(i)); |         loadFromNetwork = loadFromNetwork.doOnSuccess(info -> cache.putInfo(serviceId, url, info)); | ||||||
|  |  | ||||||
|         Single<I> load; |         Single<I> load; | ||||||
|         if (forceLoad) { |         if (forceLoad) { | ||||||
| @@ -224,8 +224,6 @@ public final class ExtractorHelper { | |||||||
|                 Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show(); |                 Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show(); | ||||||
|             } else if (exception instanceof YoutubeStreamExtractor.GemaException) { |             } else if (exception instanceof YoutubeStreamExtractor.GemaException) { | ||||||
|                 Toast.makeText(context, R.string.blocked_by_gema, Toast.LENGTH_LONG).show(); |                 Toast.makeText(context, R.string.blocked_by_gema, Toast.LENGTH_LONG).show(); | ||||||
|             } else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) { |  | ||||||
|                 Toast.makeText(context, R.string.live_streams_not_supported, Toast.LENGTH_LONG).show(); |  | ||||||
|             } else if (exception instanceof ContentNotAvailableException) { |             } else if (exception instanceof ContentNotAvailableException) { | ||||||
|                 Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show(); |                 Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show(); | ||||||
|             } else { |             } else { | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ | |||||||
| package org.schabi.newpipe.util; | package org.schabi.newpipe.util; | ||||||
|  |  | ||||||
| import android.support.annotation.NonNull; | import android.support.annotation.NonNull; | ||||||
|  | import android.support.annotation.Nullable; | ||||||
| import android.support.v4.util.LruCache; | import android.support.v4.util.LruCache; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
|  |  | ||||||
| @@ -29,6 +30,8 @@ import org.schabi.newpipe.extractor.Info; | |||||||
| import java.util.Map; | import java.util.Map; | ||||||
| import java.util.concurrent.TimeUnit; | import java.util.concurrent.TimeUnit; | ||||||
|  |  | ||||||
|  | import static org.schabi.newpipe.extractor.ServiceList.SoundCloud; | ||||||
|  |  | ||||||
|  |  | ||||||
| public final class InfoCache { | public final class InfoCache { | ||||||
|     private static final boolean DEBUG = MainActivity.DEBUG; |     private static final boolean DEBUG = MainActivity.DEBUG; | ||||||
| @@ -52,6 +55,7 @@ public final class InfoCache { | |||||||
|         return instance; |         return instance; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Nullable | ||||||
|     public Info getFromKey(int serviceId, @NonNull String url) { |     public Info getFromKey(int serviceId, @NonNull String url) { | ||||||
|         if (DEBUG) Log.d(TAG, "getFromKey() called with: serviceId = [" + serviceId + "], url = [" + url + "]"); |         if (DEBUG) Log.d(TAG, "getFromKey() called with: serviceId = [" + serviceId + "], url = [" + url + "]"); | ||||||
|         synchronized (lruCache) { |         synchronized (lruCache) { | ||||||
| @@ -59,18 +63,19 @@ public final class InfoCache { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void putInfo(@NonNull Info info) { |     public void putInfo(int serviceId, @NonNull String url, @NonNull Info info) { | ||||||
|         if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]"); |         if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]"); | ||||||
|         synchronized (lruCache) { |  | ||||||
|             final CacheData data = new CacheData(info, DEFAULT_TIMEOUT_HOURS, TimeUnit.HOURS); |         final long expirationMillis; | ||||||
|             lruCache.put(keyOf(info), data); |         if (info.getServiceId() == SoundCloud.getServiceId()) { | ||||||
|         } |             expirationMillis = TimeUnit.MILLISECONDS.convert(15, TimeUnit.MINUTES); | ||||||
|  |         } else { | ||||||
|  |             expirationMillis = TimeUnit.MILLISECONDS.convert(DEFAULT_TIMEOUT_HOURS, TimeUnit.HOURS); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     public void removeInfo(@NonNull Info info) { |  | ||||||
|         if (DEBUG) Log.d(TAG, "removeInfo() called with: info = [" + info + "]"); |  | ||||||
|         synchronized (lruCache) { |         synchronized (lruCache) { | ||||||
|             lruCache.remove(keyOf(info)); |             final CacheData data = new CacheData(info, expirationMillis); | ||||||
|  |             lruCache.put(keyOf(serviceId, url), data); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -102,10 +107,7 @@ public final class InfoCache { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private static String keyOf(@NonNull final Info info) { |     @NonNull | ||||||
|         return keyOf(info.getServiceId(), info.getUrl()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private static String keyOf(final int serviceId, @NonNull final String url) { |     private static String keyOf(final int serviceId, @NonNull final String url) { | ||||||
|         return serviceId + url; |         return serviceId + url; | ||||||
|     } |     } | ||||||
| @@ -119,6 +121,7 @@ public final class InfoCache { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Nullable | ||||||
|     private static Info getInfo(@NonNull final LruCache<String, CacheData> cache, |     private static Info getInfo(@NonNull final LruCache<String, CacheData> cache, | ||||||
|                                 @NonNull final String key) { |                                 @NonNull final String key) { | ||||||
|         final CacheData data = cache.get(key); |         final CacheData data = cache.get(key); | ||||||
| @@ -136,12 +139,8 @@ public final class InfoCache { | |||||||
|         final private long expireTimestamp; |         final private long expireTimestamp; | ||||||
|         final private Info info; |         final private Info info; | ||||||
|  |  | ||||||
|         private CacheData(@NonNull final Info info, |         private CacheData(@NonNull final Info info, final long timeoutMillis) { | ||||||
|                           final long timeout, |             this.expireTimestamp = System.currentTimeMillis() + timeoutMillis; | ||||||
|                           @NonNull final TimeUnit timeUnit) { |  | ||||||
|             this.expireTimestamp = System.currentTimeMillis() + |  | ||||||
|                     TimeUnit.MILLISECONDS.convert(timeout, timeUnit); |  | ||||||
|  |  | ||||||
|             this.info = info; |             this.info = info; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,6 +7,8 @@ import android.content.Intent; | |||||||
| import android.net.Uri; | import android.net.Uri; | ||||||
| import android.os.Build; | import android.os.Build; | ||||||
| import android.preference.PreferenceManager; | import android.preference.PreferenceManager; | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  | import android.support.annotation.Nullable; | ||||||
| import android.support.v4.app.Fragment; | import android.support.v4.app.Fragment; | ||||||
| import android.support.v4.app.FragmentManager; | import android.support.v4.app.FragmentManager; | ||||||
| import android.support.v7.app.AlertDialog; | import android.support.v7.app.AlertDialog; | ||||||
| @@ -33,9 +35,9 @@ import org.schabi.newpipe.fragments.list.feed.FeedFragment; | |||||||
| import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; | import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; | ||||||
| import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; | import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; | ||||||
| import org.schabi.newpipe.fragments.list.search.SearchFragment; | import org.schabi.newpipe.fragments.list.search.SearchFragment; | ||||||
|  | import org.schabi.newpipe.fragments.local.bookmark.LastPlayedFragment; | ||||||
| import org.schabi.newpipe.fragments.local.bookmark.LocalPlaylistFragment; | import org.schabi.newpipe.fragments.local.bookmark.LocalPlaylistFragment; | ||||||
| import org.schabi.newpipe.fragments.local.bookmark.MostPlayedFragment; | import org.schabi.newpipe.fragments.local.bookmark.MostPlayedFragment; | ||||||
| import org.schabi.newpipe.fragments.local.bookmark.LastPlayedFragment; |  | ||||||
| import org.schabi.newpipe.history.HistoryActivity; | import org.schabi.newpipe.history.HistoryActivity; | ||||||
| import org.schabi.newpipe.player.BackgroundPlayer; | import org.schabi.newpipe.player.BackgroundPlayer; | ||||||
| import org.schabi.newpipe.player.BackgroundPlayerActivity; | import org.schabi.newpipe.player.BackgroundPlayerActivity; | ||||||
| @@ -59,39 +61,45 @@ public class NavigationHelper { | |||||||
|     // Players |     // Players | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     public static Intent getPlayerIntent(final Context context, |     @NonNull | ||||||
|                                          final Class targetClazz, |     public static Intent getPlayerIntent(@NonNull final Context context, | ||||||
|                                          final PlayQueue playQueue, |                                          @NonNull final Class targetClazz, | ||||||
|                                          final String quality) { |                                          @NonNull final PlayQueue playQueue, | ||||||
|         Intent intent = new Intent(context, targetClazz) |                                          @Nullable final String quality) { | ||||||
|                 .putExtra(VideoPlayer.PLAY_QUEUE, playQueue); |         Intent intent = new Intent(context, targetClazz); | ||||||
|  |  | ||||||
|  |         final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class); | ||||||
|  |         if (cacheKey != null) intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey); | ||||||
|         if (quality != null) intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality); |         if (quality != null) intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality); | ||||||
|  |  | ||||||
|         return intent; |         return intent; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static Intent getPlayerIntent(final Context context, |     @NonNull | ||||||
|                                          final Class targetClazz, |     public static Intent getPlayerIntent(@NonNull final Context context, | ||||||
|                                          final PlayQueue playQueue) { |                                          @NonNull final Class targetClazz, | ||||||
|  |                                          @NonNull final PlayQueue playQueue) { | ||||||
|         return getPlayerIntent(context, targetClazz, playQueue, null); |         return getPlayerIntent(context, targetClazz, playQueue, null); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static Intent getPlayerEnqueueIntent(final Context context, |     @NonNull | ||||||
|                                                 final Class targetClazz, |     public static Intent getPlayerEnqueueIntent(@NonNull final Context context, | ||||||
|                                                 final PlayQueue playQueue, |                                                 @NonNull final Class targetClazz, | ||||||
|  |                                                 @NonNull final PlayQueue playQueue, | ||||||
|                                                 final boolean selectOnAppend) { |                                                 final boolean selectOnAppend) { | ||||||
|         return getPlayerIntent(context, targetClazz, playQueue) |         return getPlayerIntent(context, targetClazz, playQueue) | ||||||
|                 .putExtra(BasePlayer.APPEND_ONLY, true) |                 .putExtra(BasePlayer.APPEND_ONLY, true) | ||||||
|                 .putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend); |                 .putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static Intent getPlayerIntent(final Context context, |     @NonNull | ||||||
|                                          final Class targetClazz, |     public static Intent getPlayerIntent(@NonNull final Context context, | ||||||
|                                          final PlayQueue playQueue, |                                          @NonNull final Class targetClazz, | ||||||
|  |                                          @NonNull final PlayQueue playQueue, | ||||||
|                                          final int repeatMode, |                                          final int repeatMode, | ||||||
|                                          final float playbackSpeed, |                                          final float playbackSpeed, | ||||||
|                                          final float playbackPitch, |                                          final float playbackPitch, | ||||||
|                                          final String playbackQuality) { |                                          @Nullable final String playbackQuality) { | ||||||
|         return getPlayerIntent(context, targetClazz, playQueue, playbackQuality) |         return getPlayerIntent(context, targetClazz, playQueue, playbackQuality) | ||||||
|                 .putExtra(BasePlayer.REPEAT_MODE, repeatMode) |                 .putExtra(BasePlayer.REPEAT_MODE, repeatMode) | ||||||
|                 .putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed) |                 .putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed) | ||||||
| @@ -131,12 +139,12 @@ public class NavigationHelper { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); |         Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); | ||||||
|         context.startService(getPlayerIntent(context, PopupVideoPlayer.class, queue)); |         startService(context, getPlayerIntent(context, PopupVideoPlayer.class, queue)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue) { |     public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue) { | ||||||
|         Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show(); |         Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show(); | ||||||
|         context.startService(getPlayerIntent(context, BackgroundPlayer.class, queue)); |         startService(context, getPlayerIntent(context, BackgroundPlayer.class, queue)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue) { |     public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue) { | ||||||
| @@ -150,7 +158,8 @@ public class NavigationHelper { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         Toast.makeText(context, R.string.popup_playing_append, Toast.LENGTH_SHORT).show(); |         Toast.makeText(context, R.string.popup_playing_append, Toast.LENGTH_SHORT).show(); | ||||||
|         context.startService(getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue, selectOnAppend)); |         startService(context, | ||||||
|  |                 getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue, selectOnAppend)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue) { |     public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue) { | ||||||
| @@ -159,7 +168,16 @@ public class NavigationHelper { | |||||||
|  |  | ||||||
|     public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend) { |     public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend) { | ||||||
|         Toast.makeText(context, R.string.background_player_append, Toast.LENGTH_SHORT).show(); |         Toast.makeText(context, R.string.background_player_append, Toast.LENGTH_SHORT).show(); | ||||||
|         context.startService(getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue, selectOnAppend)); |         startService(context, | ||||||
|  |                 getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue, selectOnAppend)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static void startService(@NonNull final Context context, @NonNull final Intent intent) { | ||||||
|  |         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||||
|  |             context.startForegroundService(intent); | ||||||
|  |         } else { | ||||||
|  |             context.startService(intent); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|   | |||||||
							
								
								
									
										112
									
								
								app/src/main/java/org/schabi/newpipe/util/SerializedCache.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								app/src/main/java/org/schabi/newpipe/util/SerializedCache.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | |||||||
|  | package org.schabi.newpipe.util; | ||||||
|  |  | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  | import android.support.annotation.Nullable; | ||||||
|  | import android.support.v4.util.LruCache; | ||||||
|  | import android.util.Log; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.MainActivity; | ||||||
|  |  | ||||||
|  | import java.io.ByteArrayInputStream; | ||||||
|  | import java.io.ByteArrayOutputStream; | ||||||
|  | import java.io.ObjectInputStream; | ||||||
|  | import java.io.ObjectOutputStream; | ||||||
|  | import java.io.Serializable; | ||||||
|  | import java.util.UUID; | ||||||
|  |  | ||||||
|  | public class SerializedCache { | ||||||
|  |     private static final boolean DEBUG = MainActivity.DEBUG; | ||||||
|  |     private final String TAG = getClass().getSimpleName(); | ||||||
|  |  | ||||||
|  |     private static final SerializedCache instance = new SerializedCache(); | ||||||
|  |     private static final int MAX_ITEMS_ON_CACHE = 5; | ||||||
|  |  | ||||||
|  |     private static final LruCache<String, CacheData> lruCache = | ||||||
|  |             new LruCache<>(MAX_ITEMS_ON_CACHE); | ||||||
|  |  | ||||||
|  |     private SerializedCache() { | ||||||
|  |         //no instance | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static SerializedCache getInstance() { | ||||||
|  |         return instance; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Nullable | ||||||
|  |     public <T> T take(@NonNull final String key, @NonNull final Class<T> type) { | ||||||
|  |         if (DEBUG) Log.d(TAG, "take() called with: key = [" + key + "]"); | ||||||
|  |         synchronized (lruCache) { | ||||||
|  |             return lruCache.get(key) != null ? getItem(lruCache.remove(key), type) : null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Nullable | ||||||
|  |     public <T> T get(@NonNull final String key, @NonNull final Class<T> type) { | ||||||
|  |         if (DEBUG) Log.d(TAG, "get() called with: key = [" + key + "]"); | ||||||
|  |         synchronized (lruCache) { | ||||||
|  |             final CacheData data = lruCache.get(key); | ||||||
|  |             return data != null ? getItem(data, type) : null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Nullable | ||||||
|  |     public <T extends Serializable> String put(@NonNull T item, @NonNull final Class<T> type) { | ||||||
|  |         final String key = UUID.randomUUID().toString(); | ||||||
|  |         return put(key, item, type) ? key : null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public <T extends Serializable> boolean put(@NonNull final String key, @NonNull T item, | ||||||
|  |                                                 @NonNull final Class<T> type) { | ||||||
|  |         if (DEBUG) Log.d(TAG, "put() called with: key = [" + key + "], item = [" + item + "]"); | ||||||
|  |         synchronized (lruCache) { | ||||||
|  |             try { | ||||||
|  |                 lruCache.put(key, new CacheData<>(clone(item, type), type)); | ||||||
|  |                 return true; | ||||||
|  |             } catch (final Exception error) { | ||||||
|  |                 Log.e(TAG, "Serialization failed for: ", error); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void clear() { | ||||||
|  |         if (DEBUG) Log.d(TAG, "clear() called"); | ||||||
|  |         synchronized (lruCache) { | ||||||
|  |             lruCache.evictAll(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public long size() { | ||||||
|  |         synchronized (lruCache) { | ||||||
|  |             return lruCache.size(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Nullable | ||||||
|  |     private <T> T getItem(@NonNull final CacheData data, @NonNull final Class<T> type) { | ||||||
|  |         return type.isAssignableFrom(data.type) ? type.cast(data.item) : null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     private <T extends Serializable> T clone(@NonNull T item, | ||||||
|  |                                              @NonNull final Class<T> type) throws Exception { | ||||||
|  |         final ByteArrayOutputStream bytesOutput = new ByteArrayOutputStream(); | ||||||
|  |         try (final ObjectOutputStream objectOutput = new ObjectOutputStream(bytesOutput)) { | ||||||
|  |             objectOutput.writeObject(item); | ||||||
|  |             objectOutput.flush(); | ||||||
|  |         } | ||||||
|  |         final Object clone = new ObjectInputStream( | ||||||
|  |                 new ByteArrayInputStream(bytesOutput.toByteArray())).readObject(); | ||||||
|  |         return type.cast(clone); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     final private static class CacheData<T> { | ||||||
|  |         private final T item; | ||||||
|  |         private final Class<T> type; | ||||||
|  |  | ||||||
|  |         private CacheData(@NonNull final T item, @NonNull Class<T> type) { | ||||||
|  |             this.item = item; | ||||||
|  |             this.type = type; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -296,5 +296,15 @@ | |||||||
|             android:textColor="?attr/colorAccent" |             android:textColor="?attr/colorAccent" | ||||||
|             tools:ignore="HardcodedText" |             tools:ignore="HardcodedText" | ||||||
|             tools:text="1:23:49"/> |             tools:text="1:23:49"/> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/live_sync" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="match_parent" | ||||||
|  |             android:gravity="center" | ||||||
|  |             android:text="@string/live_sync" | ||||||
|  |             android:textColor="?attr/colorAccent" | ||||||
|  |             android:background="?attr/selectableItemBackground" | ||||||
|  |             android:visibility="gone"/> | ||||||
|     </LinearLayout> |     </LinearLayout> | ||||||
| </RelativeLayout> | </RelativeLayout> | ||||||
| @@ -134,6 +134,7 @@ | |||||||
|         tools:visibility="visible"> |         tools:visibility="visible"> | ||||||
|  |  | ||||||
|         <RelativeLayout |         <RelativeLayout | ||||||
|  |             android:id="@+id/playbackWindowRoot" | ||||||
|             android:layout_width="match_parent" |             android:layout_width="match_parent" | ||||||
|             android:layout_height="match_parent" |             android:layout_height="match_parent" | ||||||
|             android:fitsSystemWindows="true"> |             android:fitsSystemWindows="true"> | ||||||
| @@ -397,6 +398,17 @@ | |||||||
|                     android:textColor="@android:color/white" |                     android:textColor="@android:color/white" | ||||||
|                     tools:ignore="HardcodedText" |                     tools:ignore="HardcodedText" | ||||||
|                     tools:text="1:23:49"/> |                     tools:text="1:23:49"/> | ||||||
|  |  | ||||||
|  |                 <TextView | ||||||
|  |                     android:id="@+id/playbackLiveSync" | ||||||
|  |                     android:layout_width="wrap_content" | ||||||
|  |                     android:layout_height="match_parent" | ||||||
|  |                     android:gravity="center" | ||||||
|  |                     android:text="@string/live_sync" | ||||||
|  |                     android:textColor="@android:color/white" | ||||||
|  |                     android:visibility="gone" | ||||||
|  |                     android:background="?attr/selectableItemBackground" | ||||||
|  |                     tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" /> | ||||||
|             </LinearLayout> |             </LinearLayout> | ||||||
|         </RelativeLayout> |         </RelativeLayout> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -146,6 +146,16 @@ | |||||||
|             android:textColor="?attr/colorAccent" |             android:textColor="?attr/colorAccent" | ||||||
|             tools:ignore="HardcodedText" |             tools:ignore="HardcodedText" | ||||||
|             tools:text="1:23:49"/> |             tools:text="1:23:49"/> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/live_sync" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="match_parent" | ||||||
|  |             android:gravity="center" | ||||||
|  |             android:text="@string/live_sync" | ||||||
|  |             android:textColor="?attr/colorAccent" | ||||||
|  |             android:background="?attr/selectableItemBackground" | ||||||
|  |             android:visibility="gone"/> | ||||||
|     </LinearLayout> |     </LinearLayout> | ||||||
|  |  | ||||||
|     <RelativeLayout |     <RelativeLayout | ||||||
|   | |||||||
| @@ -190,6 +190,17 @@ | |||||||
|                 android:textColor="@android:color/white" |                 android:textColor="@android:color/white" | ||||||
|                 tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" |                 tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" | ||||||
|                 tools:text="1:23:49"/> |                 tools:text="1:23:49"/> | ||||||
|  |  | ||||||
|  |             <TextView | ||||||
|  |                 android:id="@+id/playbackLiveSync" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:gravity="center_vertical" | ||||||
|  |                 android:text="@string/live_sync" | ||||||
|  |                 android:textColor="@android:color/white" | ||||||
|  |                 android:visibility="gone" | ||||||
|  |                 android:background="?attr/selectableItemBackground" | ||||||
|  |                 tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" /> | ||||||
|         </LinearLayout> |         </LinearLayout> | ||||||
|     </RelativeLayout> |     </RelativeLayout> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,6 +20,8 @@ | |||||||
|     <string name="player_gesture_controls_key" translatable="false">player_gesture_controls</string> |     <string name="player_gesture_controls_key" translatable="false">player_gesture_controls</string> | ||||||
|     <string name="resume_on_audio_focus_gain_key" translatable="false">resume_on_audio_focus_gain</string> |     <string name="resume_on_audio_focus_gain_key" translatable="false">resume_on_audio_focus_gain</string> | ||||||
|     <string name="popup_remember_size_pos_key" translatable="false">popup_remember_size_pos_key</string> |     <string name="popup_remember_size_pos_key" translatable="false">popup_remember_size_pos_key</string> | ||||||
|  |     <string name="use_inexact_seek_key" translatable="false">use_inexact_seek_key</string> | ||||||
|  |     <string name="auto_queue_key" translatable="false">auto_queue_key</string> | ||||||
|  |  | ||||||
|     <string name="default_resolution_key" translatable="false">default_resolution</string> |     <string name="default_resolution_key" translatable="false">default_resolution</string> | ||||||
|     <string name="default_resolution_value" translatable="false">360p</string> |     <string name="default_resolution_value" translatable="false">360p</string> | ||||||
|   | |||||||
| @@ -72,6 +72,10 @@ | |||||||
|     <string name="black_theme_title">Black</string> |     <string name="black_theme_title">Black</string> | ||||||
|     <string name="popup_remember_size_pos_title">Remember popup size and position</string> |     <string name="popup_remember_size_pos_title">Remember popup size and position</string> | ||||||
|     <string name="popup_remember_size_pos_summary">Remember last size and position of popup</string> |     <string name="popup_remember_size_pos_summary">Remember last size and position of popup</string> | ||||||
|  |     <string name="use_inexact_seek_title">Use fast inexact seek</string> | ||||||
|  |     <string name="use_inexact_seek_summary">Inexact seek allows the player to seek to positions faster with reduced precision</string> | ||||||
|  |     <string name="auto_queue_title">Auto-queue next stream</string> | ||||||
|  |     <string name="auto_queue_summary">Automatically append a related stream when playback starts on the last stream in a non-repeating play queue.</string> | ||||||
|     <string name="player_gesture_controls_title">Player gesture controls</string> |     <string name="player_gesture_controls_title">Player gesture controls</string> | ||||||
|     <string name="player_gesture_controls_summary">Use gestures to control the brightness and volume of the player</string> |     <string name="player_gesture_controls_summary">Use gestures to control the brightness and volume of the player</string> | ||||||
|     <string name="show_search_suggestions_title">Search suggestions</string> |     <string name="show_search_suggestions_title">Search suggestions</string> | ||||||
| @@ -413,6 +417,8 @@ | |||||||
|     <string name="normal_caption_font_size">Normal Font</string> |     <string name="normal_caption_font_size">Normal Font</string> | ||||||
|     <string name="larger_caption_font_size">Larger Font</string> |     <string name="larger_caption_font_size">Larger Font</string> | ||||||
|  |  | ||||||
|  |     <string name="live_sync">SYNC</string> | ||||||
|  |  | ||||||
|     <!-- Debug Settings --> |     <!-- Debug Settings --> | ||||||
|     <string name="enable_leak_canary_title">Enable LeakCanary</string> |     <string name="enable_leak_canary_title">Enable LeakCanary</string> | ||||||
|     <string name="enable_leak_canary_summary">Memory leak monitoring may cause app to become unresponsive when heap dumping</string> |     <string name="enable_leak_canary_summary">Memory leak monitoring may cause app to become unresponsive when heap dumping</string> | ||||||
|   | |||||||
| @@ -30,6 +30,13 @@ | |||||||
|         android:key="@string/show_search_suggestions_key" |         android:key="@string/show_search_suggestions_key" | ||||||
|         android:summary="@string/show_search_suggestions_summary" |         android:summary="@string/show_search_suggestions_summary" | ||||||
|         android:title="@string/show_search_suggestions_title"/> |         android:title="@string/show_search_suggestions_title"/> | ||||||
|  |  | ||||||
|  |     <SwitchPreference | ||||||
|  |         android:defaultValue="false" | ||||||
|  |         android:key="@string/auto_queue_key" | ||||||
|  |         android:summary="@string/auto_queue_summary" | ||||||
|  |         android:title="@string/auto_queue_title"/> | ||||||
|  |  | ||||||
|     <ListPreference |     <ListPreference | ||||||
|         android:defaultValue="@string/kiosk_page_key" |         android:defaultValue="@string/kiosk_page_key" | ||||||
|         android:entries="@array/main_page_content_names" |         android:entries="@array/main_page_content_names" | ||||||
|   | |||||||
| @@ -100,5 +100,10 @@ | |||||||
|             android:summary="@string/popup_remember_size_pos_summary" |             android:summary="@string/popup_remember_size_pos_summary" | ||||||
|             android:title="@string/popup_remember_size_pos_title"/> |             android:title="@string/popup_remember_size_pos_title"/> | ||||||
|  |  | ||||||
|  |         <SwitchPreference | ||||||
|  |             android:defaultValue="false" | ||||||
|  |             android:key="@string/use_inexact_seek_key" | ||||||
|  |             android:summary="@string/use_inexact_seek_summary" | ||||||
|  |             android:title="@string/use_inexact_seek_title"/> | ||||||
|     </PreferenceCategory> |     </PreferenceCategory> | ||||||
| </PreferenceScreen> | </PreferenceScreen> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Schabi
					Schabi