Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
fa2b226b9e
39 changed files with 1960 additions and 1018 deletions
13
README.md
13
README.md
|
@ -59,18 +59,17 @@ NewPipe does not use any Google framework libraries, or the YouTube API. It only
|
||||||
* Search/Watch Playlists
|
* Search/Watch Playlists
|
||||||
* Watch as queues Playlists
|
* Watch as queues Playlists
|
||||||
* Queuing videos
|
* Queuing videos
|
||||||
|
* Local playlists
|
||||||
|
* Subtitles
|
||||||
|
* Multi-service support (eg. SoundCloud in NewPipe Beta)
|
||||||
|
|
||||||
### Coming Features
|
### Coming Features
|
||||||
|
|
||||||
* Multiservice support (eg. SoundCloud)
|
* Livestream support
|
||||||
* Bookmarks
|
* Cast to UPnP and Cast
|
||||||
* Subtitles support
|
* Show comments
|
||||||
* livestream support
|
|
||||||
* ... and many more
|
* ... and many more
|
||||||
|
|
||||||
### Multiservice support
|
|
||||||
Although NewPipe only supports YouTube at the moment, it's designed to support many more streaming services. The plan is, that NewPipe will get such support by the version 2.0.
|
|
||||||
|
|
||||||
## Contribution
|
## Contribution
|
||||||
Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.
|
Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.
|
||||||
The more is done the better it gets!
|
The more is done the better it gets!
|
||||||
|
|
|
@ -55,7 +55,7 @@ dependencies {
|
||||||
exclude module: 'support-annotations'
|
exclude module: 'support-annotations'
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:86db415b181'
|
implementation 'com.github.TeamNewPipe:NewPipeExtractor:b1130629bb'
|
||||||
|
|
||||||
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'
|
||||||
|
|
|
@ -16,6 +16,7 @@ import android.support.v4.content.ContextCompat;
|
||||||
import android.support.v4.view.animation.FastOutSlowInInterpolator;
|
import android.support.v4.view.animation.FastOutSlowInInterpolator;
|
||||||
import android.support.v7.app.ActionBar;
|
import android.support.v7.app.ActionBar;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
|
import android.support.v7.widget.Toolbar;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
@ -56,6 +57,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;
|
||||||
|
@ -138,6 +140,7 @@ public class VideoDetailFragment
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
private Menu menu;
|
private Menu menu;
|
||||||
|
|
||||||
|
private Toolbar toolbar;
|
||||||
private Spinner spinnerToolbar;
|
private Spinner spinnerToolbar;
|
||||||
|
|
||||||
private ParallaxScrollView parallaxScrollRootView;
|
private ParallaxScrollView parallaxScrollRootView;
|
||||||
|
@ -321,7 +324,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);
|
||||||
|
@ -459,7 +462,8 @@ public class VideoDetailFragment
|
||||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
spinnerToolbar = activity.findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner);
|
toolbar = activity.findViewById(R.id.toolbar);
|
||||||
|
spinnerToolbar = toolbar.findViewById(R.id.toolbar_spinner);
|
||||||
|
|
||||||
parallaxScrollRootView = rootView.findViewById(R.id.detail_main_content);
|
parallaxScrollRootView = rootView.findViewById(R.id.detail_main_content);
|
||||||
|
|
||||||
|
@ -1192,11 +1196,21 @@ public class VideoDetailFragment
|
||||||
0);
|
0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.video_streams.isEmpty() && info.video_only_streams.isEmpty()) {
|
switch (info.getStreamType()) {
|
||||||
detailControlsBackground.setVisibility(View.GONE);
|
case LIVE_STREAM:
|
||||||
detailControlsPopup.setVisibility(View.GONE);
|
case AUDIO_LIVE_STREAM:
|
||||||
spinnerToolbar.setVisibility(View.GONE);
|
detailControlsDownload.setVisibility(View.GONE);
|
||||||
thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp);
|
spinnerToolbar.setVisibility(View.GONE);
|
||||||
|
toolbar.setTitle(R.string.live);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (!info.video_streams.isEmpty() || !info.video_only_streams.isEmpty()) break;
|
||||||
|
|
||||||
|
detailControlsBackground.setVisibility(View.GONE);
|
||||||
|
detailControlsPopup.setVisibility(View.GONE);
|
||||||
|
spinnerToolbar.setVisibility(View.GONE);
|
||||||
|
thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (autoPlayEnabled) {
|
if (autoPlayEnabled) {
|
||||||
|
@ -1216,8 +1230,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 ||
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
searchEditText == null || disposables == null) return;
|
||||||
.subscribe(
|
final String query = item.query;
|
||||||
howManyDeleted -> suggestionPublisher
|
|
||||||
.onNext(searchEditText.getText().toString()),
|
|
||||||
|
|
||||||
throwable -> showSnackBarError(throwable,
|
|
||||||
UserAction.SOMETHING_ELSE, "none",
|
|
||||||
"Deleting item failed", R.string.general_error)
|
|
||||||
);
|
|
||||||
|
|
||||||
new AlertDialog.Builder(activity)
|
new AlertDialog.Builder(activity)
|
||||||
.setTitle(item.query)
|
.setTitle(query)
|
||||||
.setMessage(R.string.delete_item_search_history)
|
.setMessage(R.string.delete_item_search_history)
|
||||||
.setCancelable(true)
|
.setCancelable(true)
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.delete, (dialog, which) -> disposables.add(onDelete))
|
.setPositiveButton(R.string.delete, (dialog, which) -> {
|
||||||
|
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
howManyDeleted -> suggestionPublisher
|
||||||
|
.onNext(searchEditText.getText().toString()),
|
||||||
|
throwable -> showSnackBarError(throwable,
|
||||||
|
UserAction.SOMETHING_ELSE, "none",
|
||||||
|
"Deleting item failed", R.string.general_error)
|
||||||
|
);
|
||||||
|
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
|
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||||
public void onClick(View view) {
|
itemBuilder.getOnStreamSelectedListener().selected(item);
|
||||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
|
||||||
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
|
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||||
public boolean onLongClick(View view) {
|
itemBuilder.getOnStreamSelectedListener().held(item);
|
||||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
|
||||||
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) {
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Thumbnail Loading
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
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) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "buildMediaSource() called with: url = [" + url + "], overrideExtension = [" + overrideExtension + "]");
|
Log.d(TAG, "buildLiveMediaSource() called with: url = [" + sourceUrl +
|
||||||
|
"], content type = [" + type + "]");
|
||||||
}
|
}
|
||||||
Uri uri = Uri.parse(url);
|
if (dataSource == null) return null;
|
||||||
int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
|
|
||||||
MediaSource mediaSource;
|
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:
|
throw new IllegalStateException("Unsupported type: " + type);
|
||||||
mediaSource = new ExtractorMediaSource(uri, cacheDataSourceFactory, extractorsFactory, null, null);
|
}
|
||||||
break;
|
}
|
||||||
default: {
|
|
||||||
|
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);
|
throw new IllegalStateException("Unsupported type: " + type);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return mediaSource;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -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()) {
|
// Progress Updates
|
||||||
playQueue.unshuffle();
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
} else {
|
|
||||||
playQueue.shuffle();
|
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 " +
|
||||||
currentItem = item;
|
(info != null ? "available" : "null") + " info, " +
|
||||||
currentInfo = info;
|
"item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]");
|
||||||
|
|
||||||
if (DEBUG) Log.d(TAG, "Syncing...");
|
final boolean hasPlayQueueItemChanged = currentItem != item;
|
||||||
if (simpleExoPlayer == null) return;
|
final boolean hasStreamInfoChanged = currentInfo != info;
|
||||||
|
if (!hasPlayQueueItemChanged && !hasStreamInfoChanged) {
|
||||||
// Check if on wrong window
|
return; // Nothing to synchronize
|
||||||
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();
|
currentItem = item;
|
||||||
initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url);
|
currentInfo = info;
|
||||||
|
if (hasPlayQueueItemChanged) {
|
||||||
|
// updates only to the stream info should not trigger another view count
|
||||||
|
registerView();
|
||||||
|
initThumbnail(info == null ? item.getThumbnailUrl() : info.getThumbnailUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
final int currentPlayQueueIndex = playQueue.indexOf(item);
|
||||||
|
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));
|
||||||
playerImpl.handleIntent(intent);
|
|
||||||
|
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);
|
||||||
|
} 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 {
|
||||||
);
|
visibility = View.STATUS_BAR_VISIBLE;
|
||||||
} else getWindow().getDecorView().setSystemUiVisibility(0);
|
}
|
||||||
|
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,27 +325,53 @@ 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);
|
||||||
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
|
playbackLiveSync.setVisibility(View.GONE);
|
||||||
info.video_streams, info.video_only_streams, false);
|
|
||||||
availableStreams = new ArrayList<>(videos);
|
|
||||||
if (playbackQuality == null) {
|
|
||||||
selectedStreamIndex = getDefaultResolutionIndex(videos);
|
|
||||||
} else {
|
|
||||||
selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality());
|
|
||||||
}
|
|
||||||
|
|
||||||
buildQualityMenu();
|
final StreamType streamType = info == null ? StreamType.NONE : info.getStreamType();
|
||||||
qualityTextView.setVisibility(View.VISIBLE);
|
|
||||||
surfaceView.setVisibility(View.VISIBLE);
|
switch (streamType) {
|
||||||
} else {
|
case AUDIO_STREAM:
|
||||||
surfaceView.setVisibility(View.GONE);
|
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,
|
||||||
|
info.video_streams, info.video_only_streams, false);
|
||||||
|
availableStreams = new ArrayList<>(videos);
|
||||||
|
if (playbackQuality == null) {
|
||||||
|
selectedStreamIndex = getDefaultResolutionIndex(videos);
|
||||||
|
} else {
|
||||||
|
selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality());
|
||||||
|
}
|
||||||
|
|
||||||
|
buildQualityMenu();
|
||||||
|
qualityTextView.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
surfaceView.setVisibility(View.VISIBLE);
|
||||||
|
default:
|
||||||
|
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();
|
|
||||||
resetSources();
|
final MediaSource mediaSource = sources.getMediaSource(playQueue.getIndex());
|
||||||
isBlocked = true;
|
final PlayQueueItem playQueueItem = playQueue.getItem();
|
||||||
return true;
|
|
||||||
|
if (mediaSource instanceof LoadedMediaSource) {
|
||||||
|
return playQueueItem == ((LoadedMediaSource) mediaSource).getStream();
|
||||||
|
} else if (mediaSource instanceof FailedMediaSource) {
|
||||||
|
return playQueueItem == ((FailedMediaSource) mediaSource).getStream();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean tryUnblock() {
|
private void maybeBlock() {
|
||||||
if (isPlayQueueReady() && isBlocked && sources != null) {
|
if (DEBUG) Log.d(TAG, "maybeBlock() called.");
|
||||||
isBlocked = false;
|
|
||||||
playbackListener.unblock(sources);
|
if (isBlocked.get()) return;
|
||||||
return true;
|
|
||||||
}
|
playbackListener.onPlaybackBlock();
|
||||||
return false;
|
resetSources();
|
||||||
|
|
||||||
|
isBlocked.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sync() {
|
private void maybeUnblock() {
|
||||||
|
if (DEBUG) Log.d(TAG, "maybeUnblock() called.");
|
||||||
|
|
||||||
|
if (isPlayQueueReady() && isPlaybackReady() && isBlocked.get()) {
|
||||||
|
isBlocked.set(false);
|
||||||
|
playbackListener.onPlaybackUnblock(sources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// 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) {
|
final Disposable sync = currentItem.getStream()
|
||||||
syncedItem = currentItem;
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
final Disposable sync = currentItem.getStream()
|
.subscribe(onSuccess, onError);
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
syncReactor.set(sync);
|
||||||
.subscribe(onSuccess, onError);
|
}
|
||||||
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);
|
for (final PlayQueueItem item : items) {
|
||||||
}
|
maybeLoadItem(item);
|
||||||
|
|
||||||
private void loadItem(@Nullable final PlayQueueItem item) {
|
|
||||||
if (item == null) return;
|
|
||||||
|
|
||||||
final int index = playQueue.indexOf(item);
|
|
||||||
if (index > sources.getSize() - 1) return;
|
|
||||||
|
|
||||||
final DeferredMediaSource mediaSource = (DeferredMediaSource) sources.getMediaSource(playQueue.indexOf(item));
|
|
||||||
if (mediaSource.state() == DeferredMediaSource.STATE_PREPARED) mediaSource.load();
|
|
||||||
|
|
||||||
tryUnblock();
|
|
||||||
if (!isBlocked) sync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void resetSources() {
|
|
||||||
if (this.sources != null) this.sources.releaseSource();
|
|
||||||
this.sources = new DynamicConcatenatingMediaSource();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void populateSources() {
|
|
||||||
if (sources == null) return;
|
|
||||||
|
|
||||||
for (final PlayQueueItem item : playQueue.getStreams()) {
|
|
||||||
insert(playQueue.indexOf(item), new DeferredMediaSource(item, sourceBuilder));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Disposable getDebouncedLoader() {
|
private void maybeLoadItem(@NonNull final PlayQueueItem item) {
|
||||||
return debouncedLoadSignal
|
if (DEBUG) Log.d(TAG, "maybeLoadItem() called.");
|
||||||
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
|
if (playQueue.indexOf(item) >= sources.getSize()) return;
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(timestamp -> loadImmediate());
|
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);
|
||||||
|
if (index == -1 || index >= sources.getSize()) return false;
|
||||||
|
|
||||||
|
final ManagedMediaSource mediaSource = (ManagedMediaSource) sources.getMediaSource(index);
|
||||||
|
|
||||||
|
if (index == playQueue.getIndex() && mediaSource instanceof LoadedMediaSource) {
|
||||||
|
return item != ((LoadedMediaSource) mediaSource).getStream();
|
||||||
|
} else {
|
||||||
|
return mediaSource.canReplace(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Media Source List Manipulation
|
// MediaSource Playlist Helpers
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
private void resetSources() {
|
||||||
|
if (DEBUG) Log.d(TAG, "resetSources() called.");
|
||||||
|
|
||||||
|
this.sources.releaseSource();
|
||||||
|
this.sources = new DynamicConcatenatingMediaSource(false,
|
||||||
|
new ShuffleOrder.UnshuffledShuffleOrder(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populateSources() {
|
||||||
|
if (DEBUG) Log.d(TAG, "populateSources() called.");
|
||||||
|
if (sources.getSize() >= playQueue.size()) return;
|
||||||
|
|
||||||
|
for (int index = sources.getSize() - 1; index < playQueue.size(); index++) {
|
||||||
|
emplace(index, new PlaceholderMediaSource());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// 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);
|
|
||||||
lruCache.put(keyOf(info), data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeInfo(@NonNull Info info) {
|
final long expirationMillis;
|
||||||
if (DEBUG) Log.d(TAG, "removeInfo() called with: info = [" + info + "]");
|
if (info.getServiceId() == SoundCloud.getServiceId()) {
|
||||||
|
expirationMillis = TimeUnit.MILLISECONDS.convert(15, TimeUnit.MINUTES);
|
||||||
|
} else {
|
||||||
|
expirationMillis = TimeUnit.MILLISECONDS.convert(DEFAULT_TIMEOUT_HOURS, TimeUnit.HOURS);
|
||||||
|
}
|
||||||
|
|
||||||
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>
|
||||||
|
@ -109,6 +113,7 @@
|
||||||
<string name="show_age_restricted_content_title">Show age restricted content</string>
|
<string name="show_age_restricted_content_title">Show age restricted content</string>
|
||||||
<string name="video_is_age_restricted">Age Restricted Video. Allowing such material is possible from Settings.</string>
|
<string name="video_is_age_restricted">Age Restricted Video. Allowing such material is possible from Settings.</string>
|
||||||
<string name="duration_live">live</string>
|
<string name="duration_live">live</string>
|
||||||
|
<string name="live">LIVE</string>
|
||||||
<string name="downloads">Downloads</string>
|
<string name="downloads">Downloads</string>
|
||||||
<string name="downloads_title">Downloads</string>
|
<string name="downloads_title">Downloads</string>
|
||||||
<string name="error_report_title">Error report</string>
|
<string name="error_report_title">Error report</string>
|
||||||
|
@ -413,6 +418,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>
|
||||||
|
|
Loading…
Reference in a new issue