Merge branch 'exoplayer-update' of https://github.com/karyogamy/NewPipe into live
This commit is contained in:
commit
e6e812fdb0
@ -55,7 +55,7 @@ dependencies {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:86db415b181'
|
||||
implementation 'com.github.karyogamy:NewPipeExtractor:b4206479cb'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||
@ -73,7 +73,7 @@ dependencies {
|
||||
implementation 'de.hdodenhof:circleimageview:2.2.0'
|
||||
implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1'
|
||||
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-urlconnection:1.5.0'
|
||||
|
||||
@ -56,6 +56,7 @@ import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
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.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
@ -321,7 +322,7 @@ public class VideoDetailFragment
|
||||
if (serializable instanceof StreamInfo) {
|
||||
//noinspection unchecked
|
||||
currentInfo = (StreamInfo) serializable;
|
||||
InfoCache.getInstance().putInfo(currentInfo);
|
||||
InfoCache.getInstance().putInfo(serviceId, url, currentInfo);
|
||||
}
|
||||
|
||||
serializable = savedState.getSerializable(STACK_KEY);
|
||||
@ -1192,11 +1193,20 @@ public class VideoDetailFragment
|
||||
0);
|
||||
}
|
||||
|
||||
if (info.video_streams.isEmpty() && info.video_only_streams.isEmpty()) {
|
||||
detailControlsBackground.setVisibility(View.GONE);
|
||||
detailControlsPopup.setVisibility(View.GONE);
|
||||
spinnerToolbar.setVisibility(View.GONE);
|
||||
thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp);
|
||||
switch (info.getStreamType()) {
|
||||
case LIVE_STREAM:
|
||||
case AUDIO_LIVE_STREAM:
|
||||
detailControlsDownload.setVisibility(View.GONE);
|
||||
spinnerToolbar.setVisibility(View.GONE);
|
||||
break;
|
||||
default:
|
||||
if (!info.video_streams.isEmpty() || !info.video_only_streams.isEmpty()) break;
|
||||
|
||||
detailControlsBackground.setVisibility(View.GONE);
|
||||
detailControlsPopup.setVisibility(View.GONE);
|
||||
spinnerToolbar.setVisibility(View.GONE);
|
||||
thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp);
|
||||
break;
|
||||
}
|
||||
|
||||
if (autoPlayEnabled) {
|
||||
@ -1216,8 +1226,6 @@ public class VideoDetailFragment
|
||||
|
||||
if (exception instanceof YoutubeStreamExtractor.GemaException) {
|
||||
onBlockedByGemaError();
|
||||
} else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) {
|
||||
showError(getString(R.string.live_streams_not_supported), false);
|
||||
} else if (exception instanceof ContentNotAvailableException) {
|
||||
showError(getString(R.string.content_not_available), false);
|
||||
} else {
|
||||
|
||||
@ -527,23 +527,26 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||
}
|
||||
|
||||
private void showDeleteSuggestionDialog(final SuggestionItem item) {
|
||||
final Disposable onDelete = historyRecordManager.deleteSearchHistory(item.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)
|
||||
);
|
||||
|
||||
if (activity == null || historyRecordManager == null || suggestionPublisher == null ||
|
||||
searchEditText == null || disposables == null) return;
|
||||
final String query = item.query;
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(item.query)
|
||||
.setTitle(query)
|
||||
.setMessage(R.string.delete_item_search_history)
|
||||
.setCancelable(true)
|
||||
.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();
|
||||
}
|
||||
|
||||
@ -701,19 +704,8 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||
searchDisposable = ExtractorHelper.searchFor(serviceId, searchQuery, currentPage, contentCountry, filter)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<SearchResult>() {
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
});
|
||||
.doOnEvent((searchResult, throwable) -> isLoading.set(false))
|
||||
.subscribe(this::handleResult, this::onError);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -725,19 +717,8 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||
searchDisposable = ExtractorHelper.getMoreSearchItems(serviceId, searchQuery, currentNextPage, contentCountry, filter)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<ListExtractor.InfoItemPage>() {
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
});
|
||||
.doOnEvent((nextItemsResult, throwable) -> isLoading.set(false))
|
||||
.subscribe(this::handleNextItems, this::onError);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -59,23 +59,20 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.thumbnail_url, itemThumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
|
||||
itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||
itemBuilder.getOnStreamSelectedListener().selected(item);
|
||||
}
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||
itemBuilder.getOnStreamSelectedListener().selected(item);
|
||||
}
|
||||
});
|
||||
|
||||
switch (item.stream_type) {
|
||||
case AUDIO_STREAM:
|
||||
case VIDEO_STREAM:
|
||||
case FILE:
|
||||
enableLongClick(item);
|
||||
break;
|
||||
case LIVE_STREAM:
|
||||
case AUDIO_LIVE_STREAM:
|
||||
enableLongClick(item);
|
||||
break;
|
||||
case FILE:
|
||||
case NONE:
|
||||
default:
|
||||
disableLongClick();
|
||||
@ -85,14 +82,11 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
|
||||
private void enableLongClick(final StreamInfoItem item) {
|
||||
itemView.setLongClickable(true);
|
||||
itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||
itemBuilder.getOnStreamSelectedListener().held(item);
|
||||
}
|
||||
return true;
|
||||
itemView.setOnLongClickListener(view -> {
|
||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||
itemBuilder.getOnStreamSelectedListener().held(item);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@ import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
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.player.event.PlayerEventListener;
|
||||
import org.schabi.newpipe.player.helper.LockManager;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
@ -291,15 +293,15 @@ public final class BackgroundPlayer extends Service {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onThumbnailReceived(Bitmap thumbnail) {
|
||||
super.onThumbnailReceived(thumbnail);
|
||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||
super.onLoadingComplete(imageUri, view, loadedImage);
|
||||
|
||||
if (thumbnail != null) {
|
||||
if (loadedImage != null) {
|
||||
// rebuild notification here since remote view does not release bitmaps, causing memory leaks
|
||||
resetNotification();
|
||||
|
||||
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail);
|
||||
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
|
||||
|
||||
updateNotification(-1);
|
||||
}
|
||||
@ -378,29 +380,34 @@ public final class BackgroundPlayer extends Service {
|
||||
// Playback Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) {
|
||||
if (currentItem == item && currentInfo == info) return;
|
||||
super.sync(item, info);
|
||||
|
||||
resetNotification();
|
||||
updateNotification(-1);
|
||||
updateMetadata();
|
||||
protected void onMetadataChanged(@NonNull final PlayQueueItem item,
|
||||
@Nullable final StreamInfo info,
|
||||
final int newPlayQueueIndex,
|
||||
final boolean hasPlayQueueItemChanged) {
|
||||
if (shouldUpdateOnProgress || hasPlayQueueItemChanged) {
|
||||
resetNotification();
|
||||
updateNotification(-1);
|
||||
updateMetadata();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
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);
|
||||
if (index < 0 || index >= info.audio_streams.size()) return null;
|
||||
|
||||
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
|
||||
public void shutdown() {
|
||||
super.shutdown();
|
||||
public void onPlaybackShutdown() {
|
||||
super.onPlaybackShutdown();
|
||||
onClose();
|
||||
}
|
||||
|
||||
@ -429,7 +436,8 @@ public final class BackgroundPlayer extends Service {
|
||||
|
||||
private void updatePlayback() {
|
||||
if (activityListener != null && simpleExoPlayer != null && playQueue != null) {
|
||||
activityListener.onPlaybackUpdate(currentState, getRepeatMode(), playQueue.isShuffled(), getPlaybackParameters());
|
||||
activityListener.onPlaybackUpdate(currentState, getRepeatMode(),
|
||||
playQueue.isShuffled(), getPlaybackParameters());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -70,6 +70,9 @@ import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
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.util.AnimationUtils.animateView;
|
||||
|
||||
@ -419,13 +422,15 @@ public final class PopupVideoPlayer extends Service {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onThumbnailReceived(Bitmap thumbnail) {
|
||||
super.onThumbnailReceived(thumbnail);
|
||||
if (thumbnail != null) {
|
||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||
super.onLoadingComplete(imageUri, view, loadedImage);
|
||||
if (loadedImage != null) {
|
||||
// rebuild notification here since remote view does not release bitmaps, causing memory leaks
|
||||
notBuilder = createNotification();
|
||||
|
||||
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail);
|
||||
if (notRemoteView != null) {
|
||||
notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
|
||||
}
|
||||
|
||||
updateNotification(-1);
|
||||
}
|
||||
@ -533,7 +538,8 @@ public final class PopupVideoPlayer extends Service {
|
||||
|
||||
private void updatePlayback() {
|
||||
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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void sync(@NonNull PlayQueueItem item, @Nullable StreamInfo info) {
|
||||
if (currentItem == item && currentInfo == info) return;
|
||||
super.sync(item, info);
|
||||
protected void onMetadataChanged(@NonNull final PlayQueueItem item,
|
||||
@Nullable final StreamInfo info,
|
||||
final int newPlayQueueIndex,
|
||||
final boolean hasPlayQueueItemChanged) {
|
||||
super.onMetadataChanged(item, info, newPlayQueueIndex, false);
|
||||
updateMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
super.shutdown();
|
||||
public void onPlaybackShutdown() {
|
||||
super.onPlaybackShutdown();
|
||||
onClose();
|
||||
}
|
||||
|
||||
@ -646,6 +653,8 @@ public final class PopupVideoPlayer extends Service {
|
||||
super.onPlaying();
|
||||
updateNotification(R.drawable.ic_pause_white);
|
||||
lockManager.acquireWifiAndCpu();
|
||||
|
||||
hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -778,8 +787,8 @@ public final class PopupVideoPlayer extends Service {
|
||||
private void onScrollEnd() {
|
||||
if (DEBUG) Log.d(TAG, "onScrollEnd() called");
|
||||
if (playerImpl == null) return;
|
||||
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == BasePlayer.STATE_PLAYING) {
|
||||
playerImpl.hideControls(300, VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME);
|
||||
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
|
||||
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -76,6 +76,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
private SeekBar progressSeekBar;
|
||||
private TextView progressCurrentTime;
|
||||
private TextView progressEndTime;
|
||||
private TextView progressLiveSync;
|
||||
private TextView seekDisplay;
|
||||
|
||||
private ImageButton repeatButton;
|
||||
@ -294,9 +295,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
progressCurrentTime = rootView.findViewById(R.id.current_time);
|
||||
progressSeekBar = rootView.findViewById(R.id.seek_bar);
|
||||
progressEndTime = rootView.findViewById(R.id.end_time);
|
||||
progressLiveSync = rootView.findViewById(R.id.live_sync);
|
||||
seekDisplay = rootView.findViewById(R.id.seek_display);
|
||||
|
||||
progressSeekBar.setOnSeekBarChangeListener(this);
|
||||
progressLiveSync.setOnClickListener(this);
|
||||
}
|
||||
|
||||
private void buildControls() {
|
||||
@ -513,6 +516,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
} else if (view.getId() == metadata.getId()) {
|
||||
scrollToSelected();
|
||||
|
||||
} else if (view.getId() == progressLiveSync.getId()) {
|
||||
player.seekToDefault();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -574,6 +580,19 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
if (info != null) {
|
||||
metadataTitle.setText(info.getName());
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,21 +50,21 @@ import android.widget.TextView;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
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.MergingMediaSource;
|
||||
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.ui.SubtitleView;
|
||||
import com.google.android.exoplayer2.video.VideoListener;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
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.StreamType;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
@ -87,7 +87,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
*/
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
public abstract class VideoPlayer extends BasePlayer
|
||||
implements SimpleExoPlayer.VideoListener,
|
||||
implements VideoListener,
|
||||
SeekBar.OnSeekBarChangeListener,
|
||||
View.OnClickListener,
|
||||
Player.EventListener,
|
||||
@ -101,6 +101,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
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
|
||||
|
||||
private ArrayList<VideoStream> availableStreams;
|
||||
@ -131,6 +132,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
private SeekBar playbackSeekBar;
|
||||
private TextView playbackCurrentTime;
|
||||
private TextView playbackEndTime;
|
||||
private TextView playbackLiveSync;
|
||||
private TextView playbackSpeedTextView;
|
||||
|
||||
private View topControlsRoot;
|
||||
@ -159,7 +161,6 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
public VideoPlayer(String debugTag, Context context) {
|
||||
super(context);
|
||||
this.TAG = debugTag;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void setup(View rootView) {
|
||||
@ -180,6 +181,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
this.playbackSeekBar = rootView.findViewById(R.id.playbackSeekBar);
|
||||
this.playbackCurrentTime = rootView.findViewById(R.id.playbackCurrentTime);
|
||||
this.playbackEndTime = rootView.findViewById(R.id.playbackEndTime);
|
||||
this.playbackLiveSync = rootView.findViewById(R.id.playbackLiveSync);
|
||||
this.playbackSpeedTextView = rootView.findViewById(R.id.playbackSpeed);
|
||||
this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls);
|
||||
this.topControlsRoot = rootView.findViewById(R.id.topControls);
|
||||
@ -221,6 +223,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
qualityTextView.setOnClickListener(this);
|
||||
captionTextView.setOnClickListener(this);
|
||||
resizeView.setOnClickListener(this);
|
||||
playbackLiveSync.setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -261,7 +264,8 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId);
|
||||
for (int i = 0; i < availableStreams.size(); 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) {
|
||||
qualityTextView.setText(getSelectedVideoStream().resolution);
|
||||
@ -305,8 +309,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
captionItem.setOnMenuItemClickListener(menuItem -> {
|
||||
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
|
||||
if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) {
|
||||
trackSelector.setParameters(trackSelector.getParameters()
|
||||
.withPreferredTextLanguage(captionLanguage));
|
||||
trackSelector.setPreferredTextLanguage(captionLanguage);
|
||||
trackSelector.setRendererDisabled(textRendererIndex, false);
|
||||
}
|
||||
return true;
|
||||
@ -322,27 +325,53 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
|
||||
protected abstract int getOverrideResolutionIndex(final List<VideoStream> sortedVideos, final String playbackQuality);
|
||||
|
||||
@Override
|
||||
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) {
|
||||
super.sync(item, info);
|
||||
protected void onMetadataChanged(@NonNull final PlayQueueItem item,
|
||||
@Nullable final StreamInfo info,
|
||||
final int newPlayQueueIndex,
|
||||
final boolean hasPlayQueueItemChanged) {
|
||||
qualityTextView.setVisibility(View.GONE);
|
||||
playbackSpeedTextView.setVisibility(View.GONE);
|
||||
|
||||
if (info != null && info.video_streams.size() + info.video_only_streams.size() > 0) {
|
||||
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());
|
||||
}
|
||||
playbackEndTime.setVisibility(View.GONE);
|
||||
playbackLiveSync.setVisibility(View.GONE);
|
||||
|
||||
buildQualityMenu();
|
||||
qualityTextView.setVisibility(View.VISIBLE);
|
||||
surfaceView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
surfaceView.setVisibility(View.GONE);
|
||||
final StreamType streamType = info == null ? StreamType.NONE : info.getStreamType();
|
||||
|
||||
switch (streamType) {
|
||||
case AUDIO_STREAM:
|
||||
surfaceView.setVisibility(View.GONE);
|
||||
playbackEndTime.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
|
||||
case AUDIO_LIVE_STREAM:
|
||||
surfaceView.setVisibility(View.GONE);
|
||||
playbackLiveSync.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
|
||||
case LIVE_STREAM:
|
||||
surfaceView.setVisibility(View.VISIBLE);
|
||||
playbackLiveSync.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
|
||||
case VIDEO_STREAM:
|
||||
if (info.video_streams.size() + info.video_only_streams.size() == 0) break;
|
||||
|
||||
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
|
||||
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();
|
||||
@ -352,6 +381,9 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
@Override
|
||||
@Nullable
|
||||
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<>();
|
||||
|
||||
// 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;
|
||||
if (video != null) {
|
||||
final MediaSource streamSource = buildMediaSource(video.getUrl(),
|
||||
PlayerHelper.cacheKeyOf(info, video),
|
||||
MediaFormat.getSuffixById(video.getFormatId()));
|
||||
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
|
||||
if (audio != null && ((video != null && video.isVideoOnly) || video == null)) {
|
||||
final MediaSource audioSource = buildMediaSource(audio.getUrl(),
|
||||
PlayerHelper.cacheKeyOf(info, audio),
|
||||
MediaFormat.getSuffixById(audio.getFormatId()));
|
||||
mediaSources.add(audioSource);
|
||||
}
|
||||
@ -395,8 +429,8 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
|
||||
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
|
||||
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
|
||||
final MediaSource textSource = new SingleSampleMediaSource(
|
||||
Uri.parse(subtitle.getURL()), cacheDataSourceFactory, textFormat, TIME_UNSET);
|
||||
final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
|
||||
.createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
|
||||
mediaSources.add(textSource);
|
||||
}
|
||||
|
||||
@ -417,7 +451,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
super.onBlocked();
|
||||
|
||||
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
||||
animateView(controlsRoot, false, 300);
|
||||
animateView(controlsRoot, false, DEFAULT_CONTROLS_DURATION);
|
||||
|
||||
playbackSeekBar.setEnabled(false);
|
||||
// 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);
|
||||
|
||||
loadingPanel.setVisibility(View.GONE);
|
||||
showControlsThenHide();
|
||||
|
||||
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200);
|
||||
animateView(endScreen, false, 0);
|
||||
}
|
||||
@ -529,26 +563,15 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
}
|
||||
|
||||
// Normalize mismatching language strings
|
||||
final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
final String preferredLanguage = trackSelector.getPreferredTextLanguage();
|
||||
|
||||
// Build UI
|
||||
buildCaptionMenu(availableLanguages);
|
||||
if (trackSelector.getRendererDisabled(textRenderer) || formattedPreferredLanguage == null ||
|
||||
!availableLanguages.contains(formattedPreferredLanguage)) {
|
||||
if (trackSelector.getRendererDisabled(textRenderer) || preferredLanguage == null ||
|
||||
!availableLanguages.contains(preferredLanguage)) {
|
||||
captionTextView.setText(R.string.caption_none);
|
||||
} else {
|
||||
captionTextView.setText(formattedPreferredLanguage);
|
||||
captionTextView.setText(preferredLanguage);
|
||||
}
|
||||
captionTextView.setVisibility(availableLanguages.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
@ -595,9 +618,9 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onThumbnailReceived(Bitmap thumbnail) {
|
||||
super.onThumbnailReceived(thumbnail);
|
||||
if (thumbnail != null) endScreen.setImageBitmap(thumbnail);
|
||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||
super.onLoadingComplete(imageUri, view, loadedImage);
|
||||
if (loadedImage != null) endScreen.setImageBitmap(loadedImage);
|
||||
}
|
||||
|
||||
protected void onFullScreenButtonClicked() {
|
||||
@ -633,6 +656,8 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
onResizeClicked();
|
||||
} else if (v.getId() == captionTextView.getId()) {
|
||||
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");
|
||||
qualityPopupMenu.show();
|
||||
isSomePopupMenuVisible = true;
|
||||
showControls(300);
|
||||
showControls(DEFAULT_CONTROLS_DURATION);
|
||||
|
||||
final VideoStream videoStream = getSelectedVideoStream();
|
||||
if (videoStream != null) {
|
||||
@ -699,14 +724,14 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
if (DEBUG) Log.d(TAG, "onPlaybackSpeedClicked() called");
|
||||
playbackSpeedPopupMenu.show();
|
||||
isSomePopupMenuVisible = true;
|
||||
showControls(300);
|
||||
showControls(DEFAULT_CONTROLS_DURATION);
|
||||
}
|
||||
|
||||
private void onCaptionClicked() {
|
||||
if (DEBUG) Log.d(TAG, "onCaptionClicked() called");
|
||||
captionPopupMenu.show();
|
||||
isSomePopupMenuVisible = true;
|
||||
showControls(300);
|
||||
showControls(DEFAULT_CONTROLS_DURATION);
|
||||
}
|
||||
|
||||
private void onResizeClicked() {
|
||||
@ -739,7 +764,8 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
if (isPlaying()) simpleExoPlayer.setPlayWhenReady(false);
|
||||
|
||||
showControls(0);
|
||||
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, true, 300);
|
||||
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, true,
|
||||
DEFAULT_CONTROLS_DURATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -795,7 +821,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1f),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1f)
|
||||
).setDuration(300);
|
||||
).setDuration(DEFAULT_CONTROLS_DURATION);
|
||||
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
@ -837,12 +863,8 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
|
||||
public void showControlsThenHide() {
|
||||
if (DEBUG) Log.d(TAG, "showControlsThenHide() called");
|
||||
animateView(controlsRoot, true, 300, 0, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
hideControls(300, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
}
|
||||
});
|
||||
animateView(controlsRoot, true, DEFAULT_CONTROLS_DURATION, 0,
|
||||
() -> hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME));
|
||||
}
|
||||
|
||||
public void showControls(long duration) {
|
||||
@ -854,12 +876,8 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
public void hideControls(final long duration, long delay) {
|
||||
if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]");
|
||||
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
||||
controlsVisibilityHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
animateView(controlsRoot, false, duration);
|
||||
}
|
||||
}, delay);
|
||||
controlsVisibilityHandler.postDelayed(
|
||||
() -> animateView(controlsRoot, false, duration), delay);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@ -4,11 +4,14 @@ import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||
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.TransferListener;
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
|
||||
@ -18,7 +21,7 @@ import org.schabi.newpipe.Downloader;
|
||||
|
||||
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 CACHE_FOLDER_NAME = "exoplayer";
|
||||
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?
|
||||
private static SimpleCache cache;
|
||||
|
||||
public CacheFactory(@NonNull final Context context) {
|
||||
this(context, PlayerHelper.getPreferredCacheSize(context), PlayerHelper.getPreferredFileSize(context));
|
||||
public CacheFactory(@NonNull final Context 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) {
|
||||
super();
|
||||
private CacheFactory(@NonNull final Context context,
|
||||
@NonNull final String userAgent,
|
||||
@NonNull final TransferListener<? super DataSource> transferListener,
|
||||
final long maxCacheSize,
|
||||
final long maxFileSize) {
|
||||
this.maxFileSize = maxFileSize;
|
||||
|
||||
final String userAgent = Downloader.USER_AGENT;
|
||||
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
dataSourceFactory = new DefaultDataSourceFactory(context, userAgent, bandwidthMeter);
|
||||
|
||||
dataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener);
|
||||
cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
|
||||
if (!cacheDir.exists()) {
|
||||
//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.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 static final String TAG = "LoadController";
|
||||
|
||||
private final long initialPlaybackBufferUs;
|
||||
private final LoadControl internalLoadControl;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@ -24,19 +26,25 @@ public class LoadController implements LoadControl {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public LoadController(final Context context) {
|
||||
this(PlayerHelper.getMinBufferMs(context),
|
||||
PlayerHelper.getMaxBufferMs(context),
|
||||
PlayerHelper.getBufferForPlaybackMs(context));
|
||||
this(PlayerHelper.getPlaybackStartBufferMs(context),
|
||||
PlayerHelper.getPlaybackMinimumBufferMs(context),
|
||||
PlayerHelper.getPlaybackOptimalBufferMs(context));
|
||||
}
|
||||
|
||||
public LoadController(final int minBufferMs,
|
||||
final int maxBufferMs,
|
||||
final int bufferForPlaybackMs) {
|
||||
private LoadController(final int initialPlaybackBufferMs,
|
||||
final int minimumPlaybackbufferMs,
|
||||
final int optimalPlaybackBufferMs) {
|
||||
this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000;
|
||||
|
||||
final DefaultAllocator allocator = new DefaultAllocator(true,
|
||||
C.DEFAULT_BUFFER_SEGMENT_SIZE);
|
||||
|
||||
internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs,
|
||||
bufferForPlaybackMs, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
|
||||
internalLoadControl = new DefaultLoadControl(allocator,
|
||||
/*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
|
||||
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroupArray, TrackSelectionArray trackSelectionArray) {
|
||||
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroupArray,
|
||||
TrackSelectionArray trackSelectionArray) {
|
||||
internalLoadControl.onTracksSelected(renderers, trackGroupArray, trackSelectionArray);
|
||||
}
|
||||
|
||||
@ -69,12 +78,27 @@ public class LoadController implements LoadControl {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldStartPlayback(long l, boolean b) {
|
||||
return internalLoadControl.shouldStartPlayback(l, b);
|
||||
public long getBackBufferDurationUs() {
|
||||
return internalLoadControl.getBackBufferDurationUs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldContinueLoading(long l) {
|
||||
return internalLoadControl.shouldContinueLoading(l);
|
||||
public boolean retainBackBufferFromKeyframe() {
|
||||
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.preference.PreferenceManager;
|
||||
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.util.MimeTypes;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
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.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.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Formatter;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
@ -69,10 +84,10 @@ public class PlayerHelper {
|
||||
public static String captionLanguageOf(@NonNull final Context context,
|
||||
@NonNull final Subtitles subtitles) {
|
||||
final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale());
|
||||
return displayName + (subtitles.isAutoGenerated() ?
|
||||
" (" + context.getString(R.string.caption_auto_generated)+ ")" : "");
|
||||
return displayName + (subtitles.isAutoGenerated() ? " (" + context.getString(R.string.caption_auto_generated)+ ")" : "");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String resizeTypeOf(@NonNull final Context context,
|
||||
@AspectRatioFrameLayout.ResizeMode final int 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) {
|
||||
return isResumeAfterAudioFocusGain(context, false);
|
||||
}
|
||||
@ -99,6 +166,16 @@ public class PlayerHelper {
|
||||
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) {
|
||||
return 64 * 1024 * 1024L;
|
||||
}
|
||||
@ -107,16 +184,27 @@ public class PlayerHelper {
|
||||
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) {
|
||||
@ -155,4 +243,12 @@ public class PlayerHelper {
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -18,7 +18,7 @@ public interface PlaybackListener {
|
||||
*
|
||||
* May be called at any time.
|
||||
* */
|
||||
void block();
|
||||
void onPlaybackBlock();
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* */
|
||||
void unblock(final MediaSource mediaSource);
|
||||
void onPlaybackUnblock(final MediaSource mediaSource);
|
||||
|
||||
/**
|
||||
* Called when the queue index is refreshed.
|
||||
* Signals to the listener to synchronize the player's window to the manager's
|
||||
* window.
|
||||
*
|
||||
* Occurs once only per play queue item change.
|
||||
*
|
||||
* May be called only after unblock is called.
|
||||
* May be called anytime at any amount once 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
|
||||
@ -55,5 +53,5 @@ public interface PlaybackListener {
|
||||
*
|
||||
* 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() {
|
||||
super.dispose();
|
||||
if (fetchReactor != null) fetchReactor.dispose();
|
||||
fetchReactor = null;
|
||||
}
|
||||
|
||||
private static List<PlayQueueItem> extractListItems(final List<InfoItem> infos) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package org.schabi.newpipe.playlist;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
@ -44,7 +45,7 @@ public abstract class PlayQueue implements Serializable {
|
||||
|
||||
private ArrayList<PlayQueueItem> backup;
|
||||
private ArrayList<PlayQueueItem> streams;
|
||||
private final AtomicInteger queueIndex;
|
||||
@NonNull private final AtomicInteger queueIndex;
|
||||
|
||||
private transient BehaviorSubject<PlayQueueEvent> eventBroadcast;
|
||||
private transient Flowable<PlayQueueEvent> broadcastReceiver;
|
||||
@ -83,6 +84,7 @@ public abstract class PlayQueue implements Serializable {
|
||||
if (eventBroadcast != null) eventBroadcast.onComplete();
|
||||
if (reportingReactor != null) reportingReactor.cancel();
|
||||
|
||||
eventBroadcast = null;
|
||||
broadcastReceiver = null;
|
||||
reportingReactor = null;
|
||||
}
|
||||
@ -131,7 +133,7 @@ public abstract class PlayQueue implements Serializable {
|
||||
* Returns the index of the given item using referential equality.
|
||||
* 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
|
||||
// todo: better than this
|
||||
return streams.indexOf(item);
|
||||
@ -170,7 +172,7 @@ public abstract class PlayQueue implements Serializable {
|
||||
* Returns the play queue's update broadcast.
|
||||
* May be null if the play queue message bus is not initialized.
|
||||
* */
|
||||
@NonNull
|
||||
@Nullable
|
||||
public Flowable<PlayQueueEvent> getBroadcastReceiver() {
|
||||
return broadcastReceiver;
|
||||
}
|
||||
@ -211,7 +213,7 @@ public abstract class PlayQueue implements Serializable {
|
||||
*
|
||||
* @see #append(List items)
|
||||
* */
|
||||
public synchronized void append(final PlayQueueItem... items) {
|
||||
public synchronized void append(@NonNull final PlayQueueItem... items) {
|
||||
append(Arrays.asList(items));
|
||||
}
|
||||
|
||||
@ -223,7 +225,7 @@ public abstract class PlayQueue implements Serializable {
|
||||
*
|
||||
* 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);
|
||||
|
||||
if (isShuffled()) {
|
||||
@ -349,6 +351,7 @@ public abstract class PlayQueue implements Serializable {
|
||||
if (backup == null) {
|
||||
backup = new ArrayList<>(streams);
|
||||
}
|
||||
final int originIndex = getIndex();
|
||||
final PlayQueueItem current = getItem();
|
||||
Collections.shuffle(streams);
|
||||
|
||||
@ -358,7 +361,7 @@ public abstract class PlayQueue implements Serializable {
|
||||
}
|
||||
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() {
|
||||
if (backup == null) return;
|
||||
final int originIndex = getIndex();
|
||||
final PlayQueueItem current = getItem();
|
||||
|
||||
streams.clear();
|
||||
@ -384,14 +388,14 @@ public abstract class PlayQueue implements Serializable {
|
||||
queueIndex.set(0);
|
||||
}
|
||||
|
||||
broadcast(new ReorderEvent());
|
||||
broadcast(new ReorderEvent(originIndex, queueIndex.get()));
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Rx Broadcast
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void broadcast(final PlayQueueEvent event) {
|
||||
private void broadcast(@NonNull final PlayQueueEvent event) {
|
||||
if (eventBroadcast != null) {
|
||||
eventBroadcast.onNext(event);
|
||||
}
|
||||
|
||||
@ -63,22 +63,18 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
||||
}
|
||||
|
||||
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.playQueue = playQueue;
|
||||
|
||||
startReactor();
|
||||
playQueue.getBroadcastReceiver().toObservable().subscribe(getReactor());
|
||||
}
|
||||
|
||||
public void setSelectedListener(final PlayQueueItemBuilder.OnSelectedListener listener) {
|
||||
playQueueItemBuilder.setOnSelectedListener(listener);
|
||||
}
|
||||
|
||||
public void unsetSelectedListener() {
|
||||
playQueueItemBuilder.setOnSelectedListener(null);
|
||||
}
|
||||
|
||||
private void startReactor() {
|
||||
final Observer<PlayQueueEvent> observer = new Observer<PlayQueueEvent>() {
|
||||
private Observer<PlayQueueEvent> getReactor() {
|
||||
return new Observer<PlayQueueEvent>() {
|
||||
@Override
|
||||
public void onSubscribe(@NonNull Disposable d) {
|
||||
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) {
|
||||
@ -146,6 +141,14 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
||||
playQueueReactor = null;
|
||||
}
|
||||
|
||||
public void setSelectedListener(final PlayQueueItemBuilder.OnSelectedListener listener) {
|
||||
playQueueItemBuilder.setOnSelectedListener(listener);
|
||||
}
|
||||
|
||||
public void unsetSelectedListener() {
|
||||
playQueueItemBuilder.setOnSelectedListener(null);
|
||||
}
|
||||
|
||||
public void setFooter(View footer) {
|
||||
this.footer = footer;
|
||||
notifyItemChanged(playQueue.size());
|
||||
|
||||
@ -1,12 +1,24 @@
|
||||
package org.schabi.newpipe.playlist.events;
|
||||
|
||||
public class ReorderEvent implements PlayQueueEvent {
|
||||
private final int fromSelectedIndex;
|
||||
private final int toSelectedIndex;
|
||||
|
||||
@Override
|
||||
public PlayQueueEventType type() {
|
||||
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,
|
||||
Single<I> loadFromNetwork) {
|
||||
checkServiceId(serviceId);
|
||||
loadFromNetwork = loadFromNetwork.doOnSuccess((@NonNull I i) -> cache.putInfo(i));
|
||||
loadFromNetwork = loadFromNetwork.doOnSuccess(info -> cache.putInfo(serviceId, url, info));
|
||||
|
||||
Single<I> load;
|
||||
if (forceLoad) {
|
||||
@ -224,8 +224,6 @@ public final class ExtractorHelper {
|
||||
Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show();
|
||||
} else if (exception instanceof YoutubeStreamExtractor.GemaException) {
|
||||
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) {
|
||||
Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
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;
|
||||
|
||||
@ -29,6 +30,8 @@ import org.schabi.newpipe.extractor.Info;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
|
||||
|
||||
|
||||
public final class InfoCache {
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
@ -52,6 +55,7 @@ public final class InfoCache {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Info getFromKey(int serviceId, @NonNull String url) {
|
||||
if (DEBUG) Log.d(TAG, "getFromKey() called with: serviceId = [" + serviceId + "], url = [" + url + "]");
|
||||
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 + "]");
|
||||
synchronized (lruCache) {
|
||||
final CacheData data = new CacheData(info, DEFAULT_TIMEOUT_HOURS, TimeUnit.HOURS);
|
||||
lruCache.put(keyOf(info), data);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeInfo(@NonNull Info info) {
|
||||
if (DEBUG) Log.d(TAG, "removeInfo() called with: info = [" + info + "]");
|
||||
final long expirationMillis;
|
||||
if (info.getServiceId() == SoundCloud.getServiceId()) {
|
||||
expirationMillis = TimeUnit.MILLISECONDS.convert(15, TimeUnit.MINUTES);
|
||||
} else {
|
||||
expirationMillis = TimeUnit.MILLISECONDS.convert(DEFAULT_TIMEOUT_HOURS, TimeUnit.HOURS);
|
||||
}
|
||||
|
||||
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) {
|
||||
return keyOf(info.getServiceId(), info.getUrl());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String keyOf(final int serviceId, @NonNull final String url) {
|
||||
return serviceId + url;
|
||||
}
|
||||
@ -119,6 +121,7 @@ public final class InfoCache {
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Info getInfo(@NonNull final LruCache<String, CacheData> cache,
|
||||
@NonNull final String key) {
|
||||
final CacheData data = cache.get(key);
|
||||
@ -136,12 +139,8 @@ public final class InfoCache {
|
||||
final private long expireTimestamp;
|
||||
final private Info info;
|
||||
|
||||
private CacheData(@NonNull final Info info,
|
||||
final long timeout,
|
||||
@NonNull final TimeUnit timeUnit) {
|
||||
this.expireTimestamp = System.currentTimeMillis() +
|
||||
TimeUnit.MILLISECONDS.convert(timeout, timeUnit);
|
||||
|
||||
private CacheData(@NonNull final Info info, final long timeoutMillis) {
|
||||
this.expireTimestamp = System.currentTimeMillis() + timeoutMillis;
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,8 @@ import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
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.FragmentManager;
|
||||
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.playlist.PlaylistFragment;
|
||||
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.MostPlayedFragment;
|
||||
import org.schabi.newpipe.fragments.local.bookmark.LastPlayedFragment;
|
||||
import org.schabi.newpipe.history.HistoryActivity;
|
||||
import org.schabi.newpipe.player.BackgroundPlayer;
|
||||
import org.schabi.newpipe.player.BackgroundPlayerActivity;
|
||||
@ -59,39 +61,45 @@ public class NavigationHelper {
|
||||
// Players
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static Intent getPlayerIntent(final Context context,
|
||||
final Class targetClazz,
|
||||
final PlayQueue playQueue,
|
||||
final String quality) {
|
||||
Intent intent = new Intent(context, targetClazz)
|
||||
.putExtra(VideoPlayer.PLAY_QUEUE, playQueue);
|
||||
@NonNull
|
||||
public static Intent getPlayerIntent(@NonNull final Context context,
|
||||
@NonNull final Class targetClazz,
|
||||
@NonNull final PlayQueue playQueue,
|
||||
@Nullable final String quality) {
|
||||
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);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static Intent getPlayerIntent(final Context context,
|
||||
final Class targetClazz,
|
||||
final PlayQueue playQueue) {
|
||||
@NonNull
|
||||
public static Intent getPlayerIntent(@NonNull final Context context,
|
||||
@NonNull final Class targetClazz,
|
||||
@NonNull final PlayQueue playQueue) {
|
||||
return getPlayerIntent(context, targetClazz, playQueue, null);
|
||||
}
|
||||
|
||||
public static Intent getPlayerEnqueueIntent(final Context context,
|
||||
final Class targetClazz,
|
||||
final PlayQueue playQueue,
|
||||
@NonNull
|
||||
public static Intent getPlayerEnqueueIntent(@NonNull final Context context,
|
||||
@NonNull final Class targetClazz,
|
||||
@NonNull final PlayQueue playQueue,
|
||||
final boolean selectOnAppend) {
|
||||
return getPlayerIntent(context, targetClazz, playQueue)
|
||||
.putExtra(BasePlayer.APPEND_ONLY, true)
|
||||
.putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend);
|
||||
}
|
||||
|
||||
public static Intent getPlayerIntent(final Context context,
|
||||
final Class targetClazz,
|
||||
final PlayQueue playQueue,
|
||||
@NonNull
|
||||
public static Intent getPlayerIntent(@NonNull final Context context,
|
||||
@NonNull final Class targetClazz,
|
||||
@NonNull final PlayQueue playQueue,
|
||||
final int repeatMode,
|
||||
final float playbackSpeed,
|
||||
final float playbackPitch,
|
||||
final String playbackQuality) {
|
||||
@Nullable final String playbackQuality) {
|
||||
return getPlayerIntent(context, targetClazz, playQueue, playbackQuality)
|
||||
.putExtra(BasePlayer.REPEAT_MODE, repeatMode)
|
||||
.putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed)
|
||||
@ -131,12 +139,12 @@ public class NavigationHelper {
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
@ -150,7 +158,8 @@ public class NavigationHelper {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -159,7 +168,16 @@ public class NavigationHelper {
|
||||
|
||||
public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend) {
|
||||
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"
|
||||
tools:ignore="HardcodedText"
|
||||
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>
|
||||
</RelativeLayout>
|
||||
@ -134,6 +134,7 @@
|
||||
tools:visibility="visible">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/playbackWindowRoot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
@ -397,6 +398,17 @@
|
||||
android:textColor="@android:color/white"
|
||||
tools:ignore="HardcodedText"
|
||||
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>
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
@ -146,6 +146,16 @@
|
||||
android:textColor="?attr/colorAccent"
|
||||
tools:ignore="HardcodedText"
|
||||
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>
|
||||
|
||||
<RelativeLayout
|
||||
|
||||
@ -190,6 +190,17 @@
|
||||
android:textColor="@android:color/white"
|
||||
tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry"
|
||||
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>
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
@ -20,6 +20,8 @@
|
||||
<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="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_value" translatable="false">360p</string>
|
||||
|
||||
@ -72,6 +72,10 @@
|
||||
<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_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_summary">Use gestures to control the brightness and volume of the player</string>
|
||||
<string name="show_search_suggestions_title">Search suggestions</string>
|
||||
@ -413,6 +417,8 @@
|
||||
<string name="normal_caption_font_size">Normal Font</string>
|
||||
<string name="larger_caption_font_size">Larger Font</string>
|
||||
|
||||
<string name="live_sync">SYNC</string>
|
||||
|
||||
<!-- Debug Settings -->
|
||||
<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>
|
||||
|
||||
@ -30,6 +30,13 @@
|
||||
android:key="@string/show_search_suggestions_key"
|
||||
android:summary="@string/show_search_suggestions_summary"
|
||||
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
|
||||
android:defaultValue="@string/kiosk_page_key"
|
||||
android:entries="@array/main_page_content_names"
|
||||
|
||||
@ -100,5 +100,10 @@
|
||||
android:summary="@string/popup_remember_size_pos_summary"
|
||||
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>
|
||||
</PreferenceScreen>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user