Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
Weblate 2018-03-24 18:40:13 +01:00
commit 22c404a667
74 changed files with 1943 additions and 683 deletions

View file

@ -77,19 +77,24 @@ The more is done the better it gets!
If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md). If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
## Donate ## Donate
If you like NewPipe we'd be happy about a donation. You can either donate via Bitcoin or BountySource. For further information about donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate/). If you like NewPipe we'd be happy about a donation. You can either donate via Bitcoin, Bountysource or Liberapay. For further information about donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate).
<table> <table>
<tr> <tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin" /></td> <td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin" /></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR Code" width="100px"/></td> <td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR Code" width="100px"/></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td> <td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr> </tr>
<tr> <tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alz="Bountysource" width="190px" /></a></td> <td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" /></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"/></a></td> <td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"/></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn." /></a></td> <td><a href="https://liberapay.com/TeamNewPipe/donate/"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px" /></a></td>
</tr> </tr>
<tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px" /></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"/></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn." /></a></td>
</tr>
</table> </table>
## License ## License

View file

@ -48,7 +48,13 @@ android {
} }
ext { ext {
supportLibVersion = '27.0.2' supportLibVersion = '27.1.0'
exoPlayerLibVersion = '2.7.1'
roomDbLibVersion = '1.0.0'
leakCanaryLibVersion = '1.5.4'
okHttpLibVersion = '1.5.0'
icepickLibVersion = '3.2.0'
stethoLibVersion = '1.5.0'
} }
dependencies { dependencies {
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') { androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') {
@ -73,27 +79,28 @@ dependencies {
implementation 'de.hdodenhof:circleimageview:2.2.0' implementation 'de.hdodenhof:circleimageview:2.2.0'
implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1' implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1'
implementation 'com.nononsenseapps:filepicker:4.2.1' implementation 'com.nononsenseapps:filepicker:4.2.1'
implementation 'com.google.android.exoplayer:exoplayer:2.7.0' implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion"
implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion"
debugImplementation 'com.facebook.stetho:stetho:1.5.0' debugImplementation "com.facebook.stetho:stetho:$stethoLibVersion"
debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0' debugImplementation "com.facebook.stetho:stetho-urlconnection:$stethoLibVersion"
debugImplementation 'com.android.support:multidex:1.0.2' debugImplementation 'com.android.support:multidex:1.0.3'
implementation 'io.reactivex.rxjava2:rxjava:2.1.7' implementation 'io.reactivex.rxjava2:rxjava:2.1.10'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0' implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
implementation 'android.arch.persistence.room:runtime:1.0.0' implementation "android.arch.persistence.room:runtime:$roomDbLibVersion"
implementation 'android.arch.persistence.room:rxjava2:1.0.0' implementation "android.arch.persistence.room:rxjava2:$roomDbLibVersion"
annotationProcessor 'android.arch.persistence.room:compiler:1.0.0' annotationProcessor "android.arch.persistence.room:compiler:$roomDbLibVersion"
implementation 'frankiesardo:icepick:3.2.0' implementation "frankiesardo:icepick:$icepickLibVersion"
annotationProcessor 'frankiesardo:icepick-processor:3.2.0' annotationProcessor "frankiesardo:icepick-processor:$icepickLibVersion"
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4' debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryLibVersion"
betaImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' betaImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion"
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion"
implementation 'com.squareup.okhttp3:okhttp:3.9.1' implementation 'com.squareup.okhttp3:okhttp:3.9.1'
debugImplementation 'com.facebook.stetho:stetho-okhttp3:1.5.0' debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpLibVersion"
} }

View file

@ -28,6 +28,12 @@
</intent-filter> </intent-filter>
</activity> </activity>
<receiver android:name="android.support.v4.media.session.MediaButtonReceiver" >
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<activity <activity
android:name=".player.old.PlayVideoActivity" android:name=".player.old.PlayVideoActivity"
android:configChanges="orientation|keyboardHidden|screenSize" android:configChanges="orientation|keyboardHidden|screenSize"
@ -43,12 +49,6 @@
android:launchMode="singleTask" android:launchMode="singleTask"
android:label="@string/title_activity_background_player"/> android:label="@string/title_activity_background_player"/>
<receiver android:name="org.schabi.newpipe.player.BackgroundPlayer$MediaButtonReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<activity <activity
android:name=".player.PopupVideoPlayerActivity" android:name=".player.PopupVideoPlayerActivity"
android:launchMode="singleTask" android:launchMode="singleTask"

View file

@ -8,9 +8,7 @@ import android.support.v7.app.AppCompatActivity;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
import com.squareup.leakcanary.RefWatcher; import com.squareup.leakcanary.RefWatcher;
import icepick.Icepick; import icepick.Icepick;
@ -94,35 +92,4 @@ public abstract class BaseFragment extends Fragment {
activity.getSupportActionBar().setTitle(title); activity.getSupportActionBar().setTitle(title);
} }
} }
/*//////////////////////////////////////////////////////////////////////////
// DisplayImageOptions default configurations
//////////////////////////////////////////////////////////////////////////*/
public static final DisplayImageOptions BASE_OPTIONS =
new DisplayImageOptions.Builder().cacheInMemory(true).build();
public static final DisplayImageOptions DISPLAY_AVATAR_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_OPTIONS)
.showImageOnLoading(R.drawable.buddy)
.showImageForEmptyUri(R.drawable.buddy)
.showImageOnFail(R.drawable.buddy)
.build();
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_OPTIONS)
.displayer(new FadeInBitmapDisplayer(250))
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
.showImageOnFail(R.drawable.dummy_thumbnail)
.build();
public static final DisplayImageOptions DISPLAY_BANNER_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_OPTIONS)
.showImageOnLoading(R.drawable.channel_banner)
.showImageForEmptyUri(R.drawable.channel_banner)
.showImageOnFail(R.drawable.channel_banner)
.build();
} }

View file

@ -1,6 +1,10 @@
package org.schabi.newpipe; package org.schabi.newpipe;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.preference.PreferenceManager;
import com.nostra13.universalimageloader.core.download.BaseImageDownloader; import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
@ -10,16 +14,33 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
public class ImageDownloader extends BaseImageDownloader { public class ImageDownloader extends BaseImageDownloader {
private final Resources resources;
private final SharedPreferences preferences;
private final String downloadThumbnailKey;
public ImageDownloader(Context context) { public ImageDownloader(Context context) {
super(context); super(context);
this.resources = context.getResources();
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
this.downloadThumbnailKey = context.getString(R.string.download_thumbnail_key);
} }
public ImageDownloader(Context context, int connectTimeout, int readTimeout) { private boolean isDownloadingThumbnail() {
super(context, connectTimeout, readTimeout); return preferences.getBoolean(downloadThumbnailKey, true);
}
@SuppressLint("ResourceType")
@Override
public InputStream getStream(String imageUri, Object extra) throws IOException {
if (isDownloadingThumbnail()) {
return super.getStream(imageUri, extra);
} else {
return resources.openRawResource(R.drawable.dummy_thumbnail_dark);
}
} }
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException { protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
Downloader downloader = (Downloader) NewPipe.getDownloader(); final Downloader downloader = (Downloader) NewPipe.getDownloader();
return downloader.stream(imageUri); return downloader.stream(imageUri);
} }
} }

View file

@ -43,6 +43,7 @@ import android.widget.Toast;
import com.nirhart.parallaxscroll.views.ParallaxScrollView; import com.nirhart.parallaxscroll.views.ParallaxScrollView;
import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@ -73,6 +74,7 @@ import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.InfoCache; import org.schabi.newpipe.util.InfoCache;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
@ -581,30 +583,25 @@ public class VideoDetailFragment
}; };
} }
private void initThumbnailViews(StreamInfo info) { private void initThumbnailViews(@NonNull StreamInfo info) {
thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
if (!TextUtils.isEmpty(info.getThumbnailUrl())) { if (!TextUtils.isEmpty(info.getThumbnailUrl())) {
imageLoader.displayImage( final String infoServiceName = NewPipe.getNameOfService(info.getServiceId());
info.getThumbnailUrl(), final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() {
thumbnailImageView,
DISPLAY_THUMBNAIL_OPTIONS, new SimpleImageLoadingListener() {
@Override @Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) { public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
ErrorActivity.reportError( showSnackBarError(failReason.getCause(), UserAction.LOAD_IMAGE,
activity, infoServiceName, imageUri, R.string.could_not_load_thumbnails);
failReason.getCause(),
null,
activity.findViewById(android.R.id.content),
ErrorActivity.ErrorInfo.make(UserAction.LOAD_IMAGE,
NewPipe.getNameOfService(currentInfo.getServiceId()),
imageUri,
R.string.could_not_load_thumbnails));
} }
}); };
imageLoader.displayImage(info.getThumbnailUrl(), thumbnailImageView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener);
} }
if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) { if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) {
imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb, DISPLAY_AVATAR_OPTIONS); imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
} }
} }

View file

@ -44,6 +44,7 @@ import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.subscription.SubscriptionService; import org.schabi.newpipe.subscription.SubscriptionService;
import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
@ -419,8 +420,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
super.handleResult(result); super.handleResult(result);
headerRootLayout.setVisibility(View.VISIBLE); headerRootLayout.setVisibility(View.VISIBLE);
imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner, DISPLAY_BANNER_OPTIONS); imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner,
imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView, DISPLAY_AVATAR_OPTIONS); ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
if (result.getSubscriberCount() != -1) { if (result.getSubscriberCount() != -1) {
headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount())); headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount()));

View file

@ -37,6 +37,7 @@ import org.schabi.newpipe.playlist.PlaylistPlayQueue;
import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
@ -271,7 +272,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
playlistCtrl.setVisibility(View.VISIBLE); playlistCtrl.setVisibility(View.VISIBLE);
imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, DISPLAY_AVATAR_OPTIONS); imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos, headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos,
(int) result.getStreamCount(), (int) result.getStreamCount())); (int) result.getStreamCount(), (int) result.getStreamCount()));

View file

@ -1,12 +1,10 @@
package org.schabi.newpipe.fragments.local; package org.schabi.newpipe.fragments.local;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap;
import android.widget.ImageView; import android.widget.ImageView;
import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.process.BitmapProcessor;
import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;

View file

@ -151,7 +151,10 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
@Override @Override
public void showListFooter(final boolean show) { public void showListFooter(final boolean show) {
itemsList.post(() -> itemListAdapter.showFooter(show)); if (itemsList == null) return;
itemsList.post(() -> {
if (itemListAdapter != null) itemListAdapter.showFooter(show);
});
} }
@Override @Override

View file

@ -1,14 +1,8 @@
package org.schabi.newpipe.fragments.local.holder; package org.schabi.newpipe.fragments.local.holder;
import android.graphics.Bitmap;
import android.support.annotation.DimenRes;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.process.BitmapProcessor;
import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.fragments.local.LocalItemBuilder; import org.schabi.newpipe.fragments.local.LocalItemBuilder;
@ -45,19 +39,4 @@ public abstract class LocalItemHolder extends RecyclerView.ViewHolder {
} }
public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat); public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat);
/*//////////////////////////////////////////////////////////////////////////
// ImageLoaderOptions
//////////////////////////////////////////////////////////////////////////*/
/**
* Base display options
*/
public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS =
new DisplayImageOptions.Builder()
.cacheInMemory(true)
.cacheOnDisk(true)
.bitmapConfig(Bitmap.Config.RGB_565)
.resetViewBeforeLoading(false)
.build();
} }

View file

@ -2,15 +2,11 @@ package org.schabi.newpipe.fragments.local.holder;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.fragments.local.LocalItemBuilder; import org.schabi.newpipe.fragments.local.LocalItemBuilder;
import org.schabi.newpipe.util.ImageDisplayConstants;
import java.text.DateFormat; import java.text.DateFormat;
@ -29,7 +25,8 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
itemStreamCountView.setText(String.valueOf(item.streamCount)); itemStreamCountView.setText(String.valueOf(item.streamCount));
itemUploaderView.setVisibility(View.INVISIBLE); itemUploaderView.setVisibility(View.INVISIBLE);
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
super.updateFromItem(localItem, dateFormat); super.updateFromItem(localItem, dateFormat);
} }

View file

@ -1,6 +1,5 @@
package org.schabi.newpipe.fragments.local.holder; package org.schabi.newpipe.fragments.local.holder;
import android.graphics.Bitmap;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
@ -8,14 +7,12 @@ import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.fragments.local.LocalItemBuilder; import org.schabi.newpipe.fragments.local.LocalItemBuilder;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import java.text.DateFormat; import java.text.DateFormat;
@ -61,7 +58,8 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
} }
// Default thumbnail is shown on error, while loading and if the url is empty // Default thumbnail is shown on error, while loading and if the url is empty
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) { if (itemBuilder.getOnItemSelectedListener() != null) {
@ -92,15 +90,4 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
return false; return false;
}; };
} }
/**
* Display options for stream thumbnails
*/
private static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageOnFail(R.drawable.dummy_thumbnail)
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
.showImageOnLoading(R.drawable.dummy_thumbnail)
.build();
} }

View file

@ -6,13 +6,12 @@ import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.fragments.local.LocalItemBuilder; import org.schabi.newpipe.fragments.local.LocalItemBuilder;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import java.text.DateFormat; import java.text.DateFormat;
@ -84,7 +83,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat)); itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat));
// Default thumbnail is shown on error, while loading and if the url is empty // Default thumbnail is shown on error, while loading and if the url is empty
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) { if (itemBuilder.getOnItemSelectedListener() != null) {
@ -100,15 +100,4 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
return true; return true;
}); });
} }
/**
* Display options for stream thumbnails
*/
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageOnFail(R.drawable.dummy_thumbnail)
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
.showImageOnLoading(R.drawable.dummy_thumbnail)
.build();
} }

View file

@ -4,8 +4,6 @@ import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.fragments.local.LocalItemBuilder; import org.schabi.newpipe.fragments.local.LocalItemBuilder;
@ -48,15 +46,4 @@ public abstract class PlaylistItemHolder extends LocalItemHolder {
return true; return true;
}); });
} }
/**
* Display options for playlist thumbnails
*/
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageOnLoading(R.drawable.dummy_thumbnail_playlist)
.showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist)
.showImageOnFail(R.drawable.dummy_thumbnail_playlist)
.build();
} }

View file

@ -6,6 +6,7 @@ import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.fragments.local.LocalItemBuilder; import org.schabi.newpipe.fragments.local.LocalItemBuilder;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import java.text.DateFormat; import java.text.DateFormat;
@ -26,7 +27,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
NewPipe.getNameOfService(item.getServiceId()))); NewPipe.getNameOfService(item.getServiceId())));
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView, itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
DISPLAY_THUMBNAIL_OPTIONS); ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
super.updateFromItem(localItem, dateFormat); super.updateFromItem(localItem, dateFormat);
} }

View file

@ -20,6 +20,7 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.history.model.StreamHistoryEntry; import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
@ -147,7 +148,7 @@ public class WatchHistoryFragment extends HistoryFragment<StreamHistoryEntry> {
holder.uploader.setText(entry.uploader); holder.uploader.setText(entry.uploader);
holder.duration.setText(Localization.getDurationString(entry.duration)); holder.duration.setText(Localization.getDurationString(entry.duration));
ImageLoader.getInstance().displayImage(entry.thumbnailUrl, holder.thumbnailView, ImageLoader.getInstance().displayImage(entry.thumbnailUrl, holder.thumbnailView,
StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
} }
} }

View file

@ -1,15 +1,13 @@
package org.schabi.newpipe.info_list.holder; package org.schabi.newpipe.info_list.holder;
import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import de.hdodenhof.circleimageview.CircleImageView; import de.hdodenhof.circleimageview.CircleImageView;
@ -42,7 +40,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
itemBuilder.getImageLoader() itemBuilder.getImageLoader()
.displayImage(item.getThumbnailUrl(), .displayImage(item.getThumbnailUrl(),
itemThumbnailView, itemThumbnailView,
ChannelInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnChannelSelectedListener() != null) { if (itemBuilder.getOnChannelSelectedListener() != null) {
@ -59,15 +57,4 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
} }
return details; return details;
} }
/**
* Display options for channel thumbnails
*/
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageOnLoading(R.drawable.buddy_channel_item)
.showImageForEmptyUri(R.drawable.buddy_channel_item)
.showImageOnFail(R.drawable.buddy_channel_item)
.build();
} }

View file

@ -4,8 +4,6 @@ import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemBuilder;
@ -38,16 +36,4 @@ public abstract class InfoItemHolder extends RecyclerView.ViewHolder {
} }
public abstract void updateFromItem(final InfoItem infoItem); public abstract void updateFromItem(final InfoItem infoItem);
/*//////////////////////////////////////////////////////////////////////////
// ImageLoaderOptions
//////////////////////////////////////////////////////////////////////////*/
/**
* Base display options
*/
public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS =
new DisplayImageOptions.Builder()
.cacheInMemory(true)
.build();
} }

View file

@ -4,12 +4,11 @@ import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.util.ImageDisplayConstants;
public class PlaylistMiniInfoItemHolder extends InfoItemHolder { public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
public final ImageView itemThumbnailView; public final ImageView itemThumbnailView;
@ -40,7 +39,8 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
itemUploaderView.setText(item.getUploaderName()); itemUploaderView.setText(item.getUploaderName());
itemBuilder.getImageLoader() itemBuilder.getImageLoader()
.displayImage(item.getThumbnailUrl(), itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS); .displayImage(item.getThumbnailUrl(), itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnPlaylistSelectedListener() != null) { if (itemBuilder.getOnPlaylistSelectedListener() != null) {
@ -56,15 +56,4 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
return true; return true;
}); });
} }
/**
* Display options for playlist thumbnails
*/
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageOnLoading(R.drawable.dummy_thumbnail_playlist)
.showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist)
.showImageOnFail(R.drawable.dummy_thumbnail_playlist)
.build();
} }

View file

@ -6,13 +6,12 @@ import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
public class StreamMiniInfoItemHolder extends InfoItemHolder { public class StreamMiniInfoItemHolder extends InfoItemHolder {
@ -61,7 +60,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
itemBuilder.getImageLoader() itemBuilder.getImageLoader()
.displayImage(item.getThumbnailUrl(), .displayImage(item.getThumbnailUrl(),
itemThumbnailView, itemThumbnailView,
StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnStreamSelectedListener() != null) { if (itemBuilder.getOnStreamSelectedListener() != null) {
@ -98,15 +97,4 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
itemView.setLongClickable(false); itemView.setLongClickable(false);
itemView.setOnLongClickListener(null); itemView.setOnLongClickListener(null);
} }
/**
* Display options for stream thumbnails
*/
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageOnFail(R.drawable.dummy_thumbnail)
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
.showImageOnLoading(R.drawable.dummy_thumbnail)
.build();
} }

View file

@ -22,8 +22,6 @@ package org.schabi.newpipe.player;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
@ -35,7 +33,6 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.util.Log; import android.util.Log;
import android.view.KeyEvent;
import android.view.View; import android.view.View;
import android.widget.RemoteViews; import android.widget.RemoteViews;
@ -81,8 +78,6 @@ public final class BackgroundPlayer extends Service {
private BasePlayerImpl basePlayerImpl; private BasePlayerImpl basePlayerImpl;
private LockManager lockManager; private LockManager lockManager;
private ComponentName mReceiverComponent;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Service-Activity Binder // Service-Activity Binder
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -119,9 +114,6 @@ public final class BackgroundPlayer extends Service {
mBinder = new PlayerServiceBinder(basePlayerImpl); mBinder = new PlayerServiceBinder(basePlayerImpl);
shouldUpdateOnProgress = true; shouldUpdateOnProgress = true;
mReceiverComponent = new ComponentName(this, MediaButtonReceiver.class);
basePlayerImpl.audioReactor.registerMediaButtonEventReceiver(mReceiverComponent);
} }
@Override @Override
@ -152,7 +144,6 @@ public final class BackgroundPlayer extends Service {
lockManager.releaseWifiAndCpu(); lockManager.releaseWifiAndCpu();
} }
if (basePlayerImpl != null) { if (basePlayerImpl != null) {
basePlayerImpl.audioReactor.unregisterMediaButtonEventReceiver(mReceiverComponent);
basePlayerImpl.stopActivityBinding(); basePlayerImpl.stopActivityBinding();
basePlayerImpl.destroy(); basePlayerImpl.destroy();
} }
@ -495,7 +486,7 @@ public final class BackgroundPlayer extends Service {
onClose(); onClose();
break; break;
case ACTION_PLAY_PAUSE: case ACTION_PLAY_PAUSE:
onVideoPlayPause(); onPlayPause();
break; break;
case ACTION_REPEAT: case ACTION_REPEAT:
onRepeatClicked(); onRepeatClicked();
@ -573,41 +564,4 @@ public final class BackgroundPlayer extends Service {
lockManager.releaseWifiAndCpu(); lockManager.releaseWifiAndCpu();
} }
} }
public static class MediaButtonReceiver extends BroadcastReceiver {
public MediaButtonReceiver() {
super();
}
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
if (event.getAction() == KeyEvent.ACTION_UP) {
int keycode = event.getKeyCode();
PendingIntent pendingIntent = null;
if (keycode == KeyEvent.KEYCODE_MEDIA_NEXT) {
pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT);
} else if (keycode == KeyEvent.KEYCODE_MEDIA_PREVIOUS) {
pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT);
} else if (keycode == KeyEvent.KEYCODE_HEADSETHOOK || keycode == KeyEvent.KEYCODE_MEDIA_PAUSE || keycode == KeyEvent.KEYCODE_MEDIA_PLAY) {
pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT);
} else if (keycode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) {
pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT);
} else if (keycode == KeyEvent.KEYCODE_MEDIA_REWIND) {
pendingIntent = PendingIntent.getBroadcast(context, NOTIFICATION_ID, new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT);
}
if (pendingIntent != null) {
try {
pendingIntent.send();
} catch (Exception e) {
Log.e(TAG, "Error Sending intent MediaButtonReceiver", e);
}
}
}
}
}
}
} }

View file

@ -57,11 +57,14 @@ import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import org.schabi.newpipe.Downloader; import org.schabi.newpipe.Downloader;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.history.HistoryRecordManager;
import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.AudioReactor;
import org.schabi.newpipe.player.helper.LoadController; import org.schabi.newpipe.player.helper.LoadController;
import org.schabi.newpipe.player.helper.MediaSessionManager;
import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.playback.BasePlayerMediaSession;
import org.schabi.newpipe.player.playback.CustomTrackSelector; import org.schabi.newpipe.player.playback.CustomTrackSelector;
import org.schabi.newpipe.player.playback.MediaSourceManager; import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.player.playback.PlaybackListener;
@ -147,8 +150,10 @@ public abstract class BasePlayer implements
protected SimpleExoPlayer simpleExoPlayer; protected SimpleExoPlayer simpleExoPlayer;
protected AudioReactor audioReactor; protected AudioReactor audioReactor;
protected MediaSessionManager mediaSessionManager;
protected boolean isPrepared = false; private boolean isPrepared = false;
private boolean isSynchronizing = false;
protected Disposable progressUpdateReactor; protected Disposable progressUpdateReactor;
protected CompositeDisposable databaseUpdateReactor; protected CompositeDisposable databaseUpdateReactor;
@ -193,11 +198,13 @@ public abstract class BasePlayer implements
final LoadControl loadControl = new LoadController(context); final LoadControl loadControl = new LoadController(context);
final RenderersFactory renderFactory = new DefaultRenderersFactory(context); final RenderersFactory renderFactory = new DefaultRenderersFactory(context);
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl); simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl);
audioReactor = new AudioReactor(context, simpleExoPlayer);
simpleExoPlayer.addListener(this); simpleExoPlayer.addListener(this);
simpleExoPlayer.setPlayWhenReady(true); simpleExoPlayer.setPlayWhenReady(true);
simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context)); simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context));
audioReactor = new AudioReactor(context, simpleExoPlayer);
mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer,
new BasePlayerMediaSession(this));
} }
public void initListeners() {} public void initListeners() {}
@ -244,6 +251,7 @@ public abstract class BasePlayer implements
playQueue = queue; playQueue = queue;
playQueue.init(); playQueue.init();
if (playbackManager != null) playbackManager.dispose();
playbackManager = new MediaSourceManager(this, playQueue); playbackManager = new MediaSourceManager(this, playQueue);
if (playQueueAdapter != null) playQueueAdapter.dispose(); if (playQueueAdapter != null) playQueueAdapter.dispose();
@ -259,8 +267,8 @@ public abstract class BasePlayer implements
} }
if (isProgressLoopRunning()) stopProgressLoop(); if (isProgressLoopRunning()) stopProgressLoop();
if (playQueue != null) playQueue.dispose(); if (playQueue != null) playQueue.dispose();
if (audioReactor != null) audioReactor.dispose();
if (playbackManager != null) playbackManager.dispose(); if (playbackManager != null) playbackManager.dispose();
if (audioReactor != null) audioReactor.abandonAudioFocus();
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
if (playQueueAdapter != null) { if (playQueueAdapter != null) {
@ -272,11 +280,11 @@ public abstract class BasePlayer implements
public void destroy() { public void destroy() {
if (DEBUG) Log.d(TAG, "destroy() called"); if (DEBUG) Log.d(TAG, "destroy() called");
destroyPlayer(); destroyPlayer();
clearThumbnailCache();
unregisterBroadcastReceiver(); unregisterBroadcastReceiver();
trackSelector = null; trackSelector = null;
simpleExoPlayer = null; simpleExoPlayer = null;
mediaSessionManager = null;
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -314,11 +322,6 @@ public abstract class BasePlayer implements
if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " + if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " +
"imageUri = [" + imageUri + "], view = [" + view + "]"); "imageUri = [" + imageUri + "], view = [" + view + "]");
} }
protected void clearThumbnailCache() {
ImageLoader.getInstance().clearMemoryCache();
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// MediaSource Building // MediaSource Building
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -389,7 +392,7 @@ public abstract class BasePlayer implements
if (intent == null || intent.getAction() == null) return; if (intent == null || intent.getAction() == null) return;
switch (intent.getAction()) { switch (intent.getAction()) {
case AudioManager.ACTION_AUDIO_BECOMING_NOISY: case AudioManager.ACTION_AUDIO_BECOMING_NOISY:
if (isPlaying()) onVideoPlayPause(); onPause();
break; break;
} }
} }
@ -406,6 +409,7 @@ public abstract class BasePlayer implements
// States Implementation // States Implementation
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public static final int STATE_PREFLIGHT = -1;
public static final int STATE_BLOCKED = 123; public static final int STATE_BLOCKED = 123;
public static final int STATE_PLAYING = 124; public static final int STATE_PLAYING = 124;
public static final int STATE_BUFFERING = 125; public static final int STATE_BUFFERING = 125;
@ -413,7 +417,7 @@ public abstract class BasePlayer implements
public static final int STATE_PAUSED_SEEK = 127; public static final int STATE_PAUSED_SEEK = 127;
public static final int STATE_COMPLETED = 128; public static final int STATE_COMPLETED = 128;
protected int currentState = -1; protected int currentState = STATE_PREFLIGHT;
public void changeState(int state) { public void changeState(int state) {
if (DEBUG) Log.d(TAG, "changeState() called with: state = [" + state + "]"); if (DEBUG) Log.d(TAG, "changeState() called with: state = [" + state + "]");
@ -448,7 +452,6 @@ public abstract class BasePlayer implements
public void onPlaying() { public void onPlaying() {
if (DEBUG) Log.d(TAG, "onPlaying() called"); if (DEBUG) Log.d(TAG, "onPlaying() called");
if (!isProgressLoopRunning()) startProgressLoop(); if (!isProgressLoopRunning()) startProgressLoop();
if (!isCurrentWindowValid()) seekToDefault();
} }
public void onBuffering() {} public void onBuffering() {}
@ -522,11 +525,9 @@ public abstract class BasePlayer implements
); );
} }
private Disposable getProgressReactor() { private Disposable getProgressReactor() {
return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS) return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.filter(ignored -> isProgressLoopRunning())
.subscribe(ignored -> triggerProgressUpdate()); .subscribe(ignored -> triggerProgressUpdate());
} }
@ -541,16 +542,21 @@ public abstract class BasePlayer implements
(manifest == null ? "no manifest" : "available manifest") + ", " + (manifest == null ? "no manifest" : "available manifest") + ", " +
"timeline size = [" + timeline.getWindowCount() + "], " + "timeline size = [" + timeline.getWindowCount() + "], " +
"reason = [" + reason + "]"); "reason = [" + reason + "]");
if (playQueue == null) return;
switch (reason) { switch (reason) {
case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block
case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock
case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes
if (playQueue != null && playbackManager != null && // Ensures MediaSourceManager#update is complete
// ensures MediaSourceManager#update is complete final boolean isPlaylistStable = timeline.getWindowCount() == playQueue.size();
timeline.getWindowCount() == playQueue.size()) { // Ensure dynamic/livestream timeline changes does not cause negative position
playbackManager.load(); if (isPlaylistStable && !isCurrentWindowValid() && !isSynchronizing) {
if (DEBUG) Log.d(TAG, "Playback - negative time position reached, " +
"clamping position to 0ms.");
seekTo(/*clampToTime=*/0);
} }
break;
} }
} }
@ -600,49 +606,54 @@ public abstract class BasePlayer implements
} }
break; break;
case Player.STATE_READY: //3 case Player.STATE_READY: //3
maybeRecover(); maybeCorrectSeekPosition();
if (!isPrepared) { if (!isPrepared) {
isPrepared = true; isPrepared = true;
onPrepared(playWhenReady); onPrepared(playWhenReady);
break; break;
} }
if (currentState == STATE_PAUSED_SEEK) break;
changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED);
break; break;
case Player.STATE_ENDED: // 4 case Player.STATE_ENDED: // 4
// Ensure the current window has actually ended changeState(STATE_COMPLETED);
// since single windows that are still loading may produce an ended state isPrepared = false;
if (isCurrentWindowValid() &&
simpleExoPlayer.getCurrentPosition() >= simpleExoPlayer.getDuration()) {
changeState(STATE_COMPLETED);
isPrepared = false;
}
break; break;
} }
} }
private void maybeRecover() { private void maybeCorrectSeekPosition() {
if (playQueue == null || simpleExoPlayer == null || currentInfo == null) return;
final int currentSourceIndex = playQueue.getIndex(); final int currentSourceIndex = playQueue.getIndex();
final PlayQueueItem currentSourceItem = playQueue.getItem(); final PlayQueueItem currentSourceItem = playQueue.getItem();
if (currentSourceItem == null) return;
// Check if already playing correct window final long recoveryPositionMillis = currentSourceItem.getRecoveryPosition();
final boolean isCurrentPeriodCorrect = final boolean isCurrentWindowCorrect =
simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex; simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex;
final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000;
// Check if recovering if (recoveryPositionMillis != PlayQueueItem.RECOVERY_UNSET && isCurrentWindowCorrect) {
if (isCurrentPeriodCorrect && currentSourceItem != null) { // Is recovering previous playback?
/* Recovering with sub-second position may cause a long buffer delay in ExoPlayer, if (DEBUG) Log.d(TAG, "Playback - Rewinding to recovery time=" +
* rounding this position to the nearest second will help alleviate this.*/ "[" + getTimeString((int)recoveryPositionMillis) + "]");
final long position = currentSourceItem.getRecoveryPosition(); seekTo(recoveryPositionMillis);
/* Skip recovering if the recovery position is not set.*/
if (position == PlayQueueItem.RECOVERY_UNSET) return;
if (DEBUG) Log.d(TAG, "Rewinding to recovery window: " + currentSourceIndex +
" at: " + getTimeString((int)position));
simpleExoPlayer.seekTo(currentSourceItem.getRecoveryPosition());
playQueue.unsetRecovery(currentSourceIndex); playQueue.unsetRecovery(currentSourceIndex);
} else if (isSynchronizing && simpleExoPlayer.isCurrentWindowDynamic()) {
if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time");
// Is still synchronizing?
seekToDefault();
} else if (isSynchronizing && presetStartPositionMillis != 0L) {
if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " +
"position=[" + presetStartPositionMillis + "]");
// Has another start position?
seekTo(presetStartPositionMillis);
currentInfo.setStartPosition(0);
} }
isSynchronizing = false;
} }
/** /**
@ -775,6 +786,16 @@ public abstract class BasePlayer implements
// Playback Listener // Playback Listener
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override
public boolean isNearPlaybackEdge(final long timeToEndMillis) {
// If live, then not near playback edge
if (simpleExoPlayer == null || simpleExoPlayer.isCurrentWindowDynamic()) return false;
final long currentPositionMillis = simpleExoPlayer.getCurrentPosition();
final long currentDurationMillis = simpleExoPlayer.getDuration();
return currentDurationMillis - currentPositionMillis < timeToEndMillis;
}
@Override @Override
public void onPlaybackBlock() { public void onPlaybackBlock() {
if (simpleExoPlayer == null) return; if (simpleExoPlayer == null) return;
@ -796,7 +817,6 @@ public abstract class BasePlayer implements
if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING); if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING);
simpleExoPlayer.prepare(mediaSource); simpleExoPlayer.prepare(mediaSource);
seekToDefault();
} }
@Override @Override
@ -805,11 +825,26 @@ public abstract class BasePlayer implements
if (DEBUG) Log.d(TAG, "Playback - onPlaybackSynchronize() called with " + if (DEBUG) Log.d(TAG, "Playback - onPlaybackSynchronize() called with " +
(info != null ? "available" : "null") + " info, " + (info != null ? "available" : "null") + " info, " +
"item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]");
if (simpleExoPlayer == null || playQueue == null) return;
final boolean onPlaybackInitial = currentItem == null;
final boolean hasPlayQueueItemChanged = currentItem != item; final boolean hasPlayQueueItemChanged = currentItem != item;
final boolean hasStreamInfoChanged = currentInfo != info; final boolean hasStreamInfoChanged = currentInfo != info;
final int currentPlayQueueIndex = playQueue.indexOf(item);
final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex();
final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount();
// when starting playback on the last item when not repeating, maybe auto queue
if (info != null && currentPlayQueueIndex == playQueue.size() - 1 &&
getRepeatMode() == Player.REPEAT_MODE_OFF &&
PlayerHelper.isAutoQueueEnabled(context)) {
final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams());
if (autoQueue != null) playQueue.append(autoQueue.getStreams());
}
// If nothing to synchronize
if (!hasPlayQueueItemChanged && !hasStreamInfoChanged) { if (!hasPlayQueueItemChanged && !hasStreamInfoChanged) {
return; // Nothing to synchronize return;
} }
currentItem = item; currentItem = item;
@ -819,34 +854,31 @@ public abstract class BasePlayer implements
registerView(); registerView();
initThumbnail(info == null ? item.getThumbnailUrl() : info.getThumbnailUrl()); initThumbnail(info == null ? item.getThumbnailUrl() : info.getThumbnailUrl());
} }
final int currentPlayQueueIndex = playQueue.indexOf(item);
onMetadataChanged(item, info, currentPlayQueueIndex, hasPlayQueueItemChanged); onMetadataChanged(item, info, currentPlayQueueIndex, hasPlayQueueItemChanged);
if (simpleExoPlayer == null) return;
final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex();
// Check if on wrong window // Check if on wrong window
if (currentPlayQueueIndex != playQueue.getIndex()) { if (currentPlayQueueIndex != playQueue.getIndex()) {
Log.e(TAG, "Play Queue may be desynchronized: item " + Log.e(TAG, "Playback - Play Queue may be desynchronized: item " +
"index=[" + currentPlayQueueIndex + "], " + "index=[" + currentPlayQueueIndex + "], " +
"queue index=[" + playQueue.getIndex() + "]"); "queue index=[" + playQueue.getIndex() + "]");
// on metadata changed // Check if bad seek position
} else if (currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) { } else if ((currentPlaylistSize > 0 && currentPlayQueueIndex >= currentPlaylistSize) ||
final long startPos = info != null ? info.getStartPosition() : C.TIME_UNSET; currentPlayQueueIndex < 0) {
if (DEBUG) Log.d(TAG, "Rewinding to correct" + Log.e(TAG, "Playback - Trying to seek to invalid " +
" window=[" + currentPlayQueueIndex + "]," + "index=[" + currentPlayQueueIndex + "] with " +
" at=[" + getTimeString((int)startPos) + "]," + "playlist length=[" + currentPlaylistSize + "]");
" from=[" + simpleExoPlayer.getCurrentWindowIndex() + "].");
simpleExoPlayer.seekTo(currentPlayQueueIndex, startPos);
}
// when starting playback on the last item when not repeating, maybe auto queue // If not playing correct stream, change window position and sets flag
if (info != null && currentPlayQueueIndex == playQueue.size() - 1 && // for synchronizing once window position is corrected
getRepeatMode() == Player.REPEAT_MODE_OFF && // @see maybeCorrectSeekPosition()
PlayerHelper.isAutoQueueEnabled(context)) { } else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial ||
final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams()); !isPlaying()) {
if (autoQueue != null) playQueue.append(autoQueue.getStreams()); if (DEBUG) Log.d(TAG, "Playback - Rewinding to correct" +
" index=[" + currentPlayQueueIndex + "]," +
" from=[" + currentPlaylistIndex + "], size=[" + currentPlaylistSize + "].");
isSynchronizing = true;
simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex);
} }
} }
@ -858,6 +890,11 @@ public abstract class BasePlayer implements
@Nullable @Nullable
@Override @Override
public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) { public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) {
final StreamType streamType = info.getStreamType();
if (!(streamType == StreamType.AUDIO_LIVE_STREAM || streamType == StreamType.LIVE_STREAM)) {
return null;
}
if (!info.getHlsUrl().isEmpty()) { if (!info.getHlsUrl().isEmpty()) {
return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS); return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS);
} else if (!info.getDashMpdUrl().isEmpty()) { } else if (!info.getDashMpdUrl().isEmpty()) {
@ -911,14 +948,11 @@ public abstract class BasePlayer implements
changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED);
} }
public void onVideoPlayPause() { public void onPlay() {
if (DEBUG) Log.d(TAG, "onVideoPlayPause() called"); if (DEBUG) Log.d(TAG, "onPlay() called");
if (audioReactor == null || playQueue == null || simpleExoPlayer == null) return;
if (!isPlaying()) { audioReactor.requestAudioFocus();
audioReactor.requestAudioFocus();
} else {
audioReactor.abandonAudioFocus();
}
if (getCurrentState() == STATE_COMPLETED) { if (getCurrentState() == STATE_COMPLETED) {
if (playQueue.getIndex() == 0) { if (playQueue.getIndex() == 0) {
@ -928,7 +962,25 @@ public abstract class BasePlayer implements
} }
} }
simpleExoPlayer.setPlayWhenReady(!isPlaying()); simpleExoPlayer.setPlayWhenReady(true);
}
public void onPause() {
if (DEBUG) Log.d(TAG, "onPause() called");
if (audioReactor == null || simpleExoPlayer == null) return;
audioReactor.abandonAudioFocus();
simpleExoPlayer.setPlayWhenReady(false);
}
public void onPlayPause() {
if (DEBUG) Log.d(TAG, "onPlayPause() called");
if (!isPlaying()) {
onPlay();
} else {
onPause();
}
} }
public void onFastRewind() { public void onFastRewind() {
@ -945,14 +997,15 @@ public abstract class BasePlayer implements
if (simpleExoPlayer == null || playQueue == null) return; if (simpleExoPlayer == null || playQueue == null) return;
if (DEBUG) Log.d(TAG, "onPlayPrevious() called"); if (DEBUG) Log.d(TAG, "onPlayPrevious() called");
savePlaybackState(); /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds,
* restart current track. Also restart the track if the current track
/* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, restart current track. * is the first in a queue.*/
* Also restart the track if the current track is the first in a queue.*/ if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT ||
if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || playQueue.getIndex() == 0) { playQueue.getIndex() == 0) {
final long startPos = currentInfo == null ? 0 : currentInfo.getStartPosition(); seekToDefault();
simpleExoPlayer.seekTo(startPos); playQueue.offsetIndex(0);
} else { } else {
savePlaybackState();
playQueue.offsetIndex(-1); playQueue.offsetIndex(-1);
} }
} }
@ -962,7 +1015,6 @@ public abstract class BasePlayer implements
if (DEBUG) Log.d(TAG, "onPlayNext() called"); if (DEBUG) Log.d(TAG, "onPlayNext() called");
savePlaybackState(); savePlaybackState();
playQueue.offsetIndex(+1); playQueue.offsetIndex(+1);
} }
@ -975,20 +1027,21 @@ public abstract class BasePlayer implements
if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) { if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) {
seekToDefault(); seekToDefault();
} else { } else {
playQueue.setIndex(index); savePlaybackState();
} }
playQueue.setIndex(index);
} }
public void seekBy(int milliSeconds) { public void seekTo(long positionMillis) {
if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]"); if (DEBUG) Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]");
if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || if (simpleExoPlayer == null || positionMillis < 0 ||
((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) { positionMillis > simpleExoPlayer.getDuration()) return;
return; simpleExoPlayer.seekTo(positionMillis);
} }
int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds); public void seekBy(long offsetMillis) {
if (progress < 0) progress = 0; if (DEBUG) Log.d(TAG, "seekBy() called with: offsetMillis = [" + offsetMillis + "]");
simpleExoPlayer.seekTo(progress); seekTo(simpleExoPlayer.getCurrentPosition() + offsetMillis);
} }
public boolean isCurrentWindowValid() { public boolean isCurrentWindowValid() {
@ -1015,8 +1068,11 @@ public abstract class BasePlayer implements
protected void reload() { protected void reload() {
if (playbackManager != null) { if (playbackManager != null) {
playbackManager.reset(); playbackManager.dispose();
playbackManager.load(); }
if (playQueue != null) {
playbackManager = new MediaSourceManager(this, playQueue);
} }
} }
@ -1069,8 +1125,22 @@ public abstract class BasePlayer implements
return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUploader(); return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUploader();
} }
public boolean isCompleted() { /** Checks if the current playback is a livestream AND is playing at or beyond the live edge */
return simpleExoPlayer != null && simpleExoPlayer.getPlaybackState() == Player.STATE_ENDED; public boolean isLiveEdge() {
if (simpleExoPlayer == null) return false;
final boolean isLive = simpleExoPlayer.isCurrentWindowDynamic();
if (!isLive) return false;
final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline();
final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
if (currentTimeline.isEmpty() || currentWindowIndex < 0 ||
currentWindowIndex >= currentTimeline.getWindowCount()) {
return false;
}
Timeline.Window timelineWindow = new Timeline.Window();
currentTimeline.getWindow(currentWindowIndex, timelineWindow);
return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition();
} }
public boolean isPlaying() { public boolean isPlaying() {
@ -1123,8 +1193,8 @@ public abstract class BasePlayer implements
return playQueueAdapter; return playQueueAdapter;
} }
public boolean isPlayerReady() { public boolean isPrepared() {
return currentState == STATE_PLAYING || currentState == STATE_COMPLETED || currentState == STATE_PAUSED; return isPrepared;
} }
public boolean isProgressLoopRunning() { public boolean isProgressLoopRunning() {

View file

@ -19,7 +19,6 @@
package org.schabi.newpipe.player; package org.schabi.newpipe.player;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -33,6 +32,7 @@ import android.preference.PreferenceManager;
import android.provider.Settings; import android.provider.Settings;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper; import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
@ -57,11 +57,13 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder; import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder; import org.schabi.newpipe.playlist.PlayQueueItemHolder;
import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback;
import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
@ -76,6 +78,8 @@ import java.util.UUID;
import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; 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_DURATION;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME;
import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA;
import static org.schabi.newpipe.util.AnimationUtils.animateRotation;
import static org.schabi.newpipe.util.AnimationUtils.animateView; import static org.schabi.newpipe.util.AnimationUtils.animateView;
import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE; import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE;
@ -84,7 +88,8 @@ import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE;
* *
* @author mauriciocolli * @author mauriciocolli
*/ */
public final class MainVideoPlayer extends Activity implements StateSaver.WriteRead { public final class MainVideoPlayer extends AppCompatActivity
implements StateSaver.WriteRead, PlaybackParameterDialog.Callback {
private static final String TAG = ".MainVideoPlayer"; private static final String TAG = ".MainVideoPlayer";
private static final boolean DEBUG = BasePlayer.DEBUG; private static final boolean DEBUG = BasePlayer.DEBUG;
@ -110,7 +115,7 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK);
setVolumeControlStream(AudioManager.STREAM_MUSIC); setVolumeControlStream(AudioManager.STREAM_MUSIC);
changeSystemUi(); hideSystemUi();
setContentView(R.layout.activity_main_player); setContentView(R.layout.activity_main_player);
playerImpl = new VideoPlayerImpl(this); playerImpl = new VideoPlayerImpl(this);
playerImpl.setup(findViewById(android.R.id.content)); playerImpl.setup(findViewById(android.R.id.content));
@ -147,7 +152,7 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
if (DEBUG) Log.d(TAG, "onResume() called"); if (DEBUG) Log.d(TAG, "onResume() called");
if (playerImpl.getPlayer() != null && activityPaused && playerImpl.wasPlaying() if (playerImpl.getPlayer() != null && activityPaused && playerImpl.wasPlaying()
&& !playerImpl.isPlaying()) { && !playerImpl.isPlaying()) {
playerImpl.onVideoPlayPause(); playerImpl.onPlay();
} }
activityPaused = false; activityPaused = false;
@ -182,7 +187,7 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
if (playerImpl != null && playerImpl.getPlayer() != null && !activityPaused) { if (playerImpl != null && playerImpl.getPlayer() != null && !activityPaused) {
playerImpl.wasPlaying = playerImpl.isPlaying(); playerImpl.wasPlaying = playerImpl.isPlaying();
if (playerImpl.isPlaying()) playerImpl.onVideoPlayPause(); playerImpl.onPause();
} }
activityPaused = true; activityPaused = true;
} }
@ -337,6 +342,15 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
} }
} }
////////////////////////////////////////////////////////////////////////////
// Playback Parameters Listener
////////////////////////////////////////////////////////////////////////////
@Override
public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) {
if (playerImpl != null) playerImpl.setPlaybackParameters(playbackTempo, playbackPitch);
}
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@SuppressWarnings({"unused", "WeakerAccess"}) @SuppressWarnings({"unused", "WeakerAccess"})
@ -548,7 +562,7 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
public void onClick(View v) { public void onClick(View v) {
super.onClick(v); super.onClick(v);
if (v.getId() == playPauseButton.getId()) { if (v.getId() == playPauseButton.getId()) {
onVideoPlayPause(); onPlayPause();
} else if (v.getId() == playPreviousButton.getId()) { } else if (v.getId() == playPreviousButton.getId()) {
onPlayPrevious(); onPlayPrevious();
@ -597,28 +611,27 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
updatePlaybackButtons(); updatePlaybackButtons();
getControlsRoot().setVisibility(View.INVISIBLE); getControlsRoot().setVisibility(View.INVISIBLE);
queueLayout.setVisibility(View.VISIBLE); animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/true,
DEFAULT_CONTROLS_DURATION);
itemsList.scrollToPosition(playQueue.getIndex()); itemsList.scrollToPosition(playQueue.getIndex());
} }
private void onQueueClosed() { private void onQueueClosed() {
queueLayout.setVisibility(View.GONE); animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/false,
DEFAULT_CONTROLS_DURATION);
queueVisible = false; queueVisible = false;
} }
private void onMoreOptionsClicked() { private void onMoreOptionsClicked() {
if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called"); if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called");
if (secondaryControls.getVisibility() == View.VISIBLE) { final boolean isMoreControlsVisible = secondaryControls.getVisibility() == View.VISIBLE;
moreOptionsButton.setImageDrawable(getResources().getDrawable(
R.drawable.ic_expand_more_white_24dp)); animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION,
animateView(secondaryControls, false, 200); isMoreControlsVisible ? 0 : 180);
} else { animateView(secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible,
moreOptionsButton.setImageDrawable(getResources().getDrawable( DEFAULT_CONTROLS_DURATION);
R.drawable.ic_expand_less_white_24dp));
animateView(secondaryControls, true, 200);
}
showControls(DEFAULT_CONTROLS_DURATION); showControls(DEFAULT_CONTROLS_DURATION);
} }
@ -628,6 +641,12 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
showControlsThenHide(); showControlsThenHide();
} }
@Override
public void onPlaybackSpeedClicked() {
PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch())
.show(getSupportFragmentManager(), TAG);
}
@Override @Override
public void onStopTrackingTouch(SeekBar seekBar) { public void onStopTrackingTouch(SeekBar seekBar) {
super.onStopTrackingTouch(seekBar); super.onStopTrackingTouch(seekBar);
@ -638,6 +657,7 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
public void onDismiss(PopupMenu menu) { public void onDismiss(PopupMenu menu) {
super.onDismiss(menu); super.onDismiss(menu);
if (isPlaying()) hideControls(DEFAULT_CONTROLS_DURATION, 0); if (isPlaying()) hideControls(DEFAULT_CONTROLS_DURATION, 0);
hideSystemUi();
} }
@Override @Override
@ -696,7 +716,6 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
animatePlayButtons(true, 200); animatePlayButtons(true, 200);
}); });
changeSystemUi();
getRootView().setKeepScreenOn(true); getRootView().setKeepScreenOn(true);
} }
@ -798,31 +817,11 @@ public final class MainVideoPlayer extends Activity implements StateSaver.WriteR
} }
private ItemTouchHelper.SimpleCallback getItemTouchCallback() { private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { return new PlayQueueItemTouchCallback() {
@Override @Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) { public void onMove(int sourceIndex, int targetIndex) {
if (source.getItemViewType() != target.getItemViewType()) { if (playQueue != null) playQueue.move(sourceIndex, targetIndex);
return false;
}
final int sourceIndex = source.getLayoutPosition();
final int targetIndex = target.getLayoutPosition();
playQueue.move(sourceIndex, targetIndex);
return true;
} }
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
}; };
} }

View file

@ -618,7 +618,7 @@ public final class PopupVideoPlayer extends Service {
onClose(); onClose();
break; break;
case ACTION_PLAY_PAUSE: case ACTION_PLAY_PAUSE:
onVideoPlayPause(); onPlayPause();
break; break;
case ACTION_REPEAT: case ACTION_REPEAT:
onRepeatClicked(); onRepeatClicked();
@ -716,7 +716,7 @@ public final class PopupVideoPlayer extends Service {
public boolean onDoubleTap(MotionEvent e) { public boolean onDoubleTap(MotionEvent e) {
if (DEBUG) if (DEBUG)
Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
if (playerImpl == null || !playerImpl.isPlaying() || !playerImpl.isPlayerReady()) return false; if (playerImpl == null || !playerImpl.isPlaying()) return false;
if (e.getX() > popupWidth / 2) { if (e.getX() > popupWidth / 2) {
playerImpl.onFastForward(); playerImpl.onFastForward();
@ -731,7 +731,7 @@ public final class PopupVideoPlayer extends Service {
public boolean onSingleTapConfirmed(MotionEvent e) { public boolean onSingleTapConfirmed(MotionEvent e) {
if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
if (playerImpl == null || playerImpl.getPlayer() == null) return false; if (playerImpl == null || playerImpl.getPlayer() == null) return false;
playerImpl.onVideoPlayPause(); playerImpl.onPlayPause();
return true; return true;
} }

View file

@ -31,9 +31,11 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder; import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder; import org.schabi.newpipe.playlist.PlayQueueItemHolder;
import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
@ -42,7 +44,8 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.formatPitch;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
public abstract class ServicePlayerActivity extends AppCompatActivity public abstract class ServicePlayerActivity extends AppCompatActivity
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener { implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
View.OnClickListener, PlaybackParameterDialog.Callback {
private boolean serviceBound; private boolean serviceBound;
private ServiceConnection serviceConnection; private ServiceConnection serviceConnection;
@ -56,14 +59,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47; private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
private static final int PLAYBACK_SPEED_POPUP_MENU_GROUP_ID = 61;
private static final int PLAYBACK_PITCH_POPUP_MENU_GROUP_ID = 97;
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10;
private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25;
private View rootView; private View rootView;
private RecyclerView itemsList; private RecyclerView itemsList;
@ -87,9 +85,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private ProgressBar progressBar; private ProgressBar progressBar;
private TextView playbackSpeedButton; private TextView playbackSpeedButton;
private PopupMenu playbackSpeedPopupMenu;
private TextView playbackPitchButton; private TextView playbackPitchButton;
private PopupMenu playbackPitchPopupMenu;
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Abstracts // Abstracts
@ -319,45 +315,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
shuffleButton.setOnClickListener(this); shuffleButton.setOnClickListener(this);
playbackSpeedButton.setOnClickListener(this); playbackSpeedButton.setOnClickListener(this);
playbackPitchButton.setOnClickListener(this); playbackPitchButton.setOnClickListener(this);
playbackSpeedPopupMenu = new PopupMenu(this, playbackSpeedButton);
playbackPitchPopupMenu = new PopupMenu(this, playbackPitchButton);
buildPlaybackSpeedMenu();
buildPlaybackPitchMenu();
}
private void buildPlaybackSpeedMenu() {
if (playbackSpeedPopupMenu == null) return;
playbackSpeedPopupMenu.getMenu().removeGroup(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID);
for (int i = 0; i < BasePlayer.PLAYBACK_SPEEDS.length; i++) {
final float playbackSpeed = BasePlayer.PLAYBACK_SPEEDS[i];
final String formattedSpeed = formatSpeed(playbackSpeed);
final MenuItem item = playbackSpeedPopupMenu.getMenu().add(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedSpeed);
item.setOnMenuItemClickListener(menuItem -> {
if (player == null) return false;
player.setPlaybackSpeed(playbackSpeed);
return true;
});
}
}
private void buildPlaybackPitchMenu() {
if (playbackPitchPopupMenu == null) return;
playbackPitchPopupMenu.getMenu().removeGroup(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID);
for (int i = 0; i < BasePlayer.PLAYBACK_PITCHES.length; i++) {
final float playbackPitch = BasePlayer.PLAYBACK_PITCHES[i];
final String formattedPitch = formatPitch(playbackPitch);
final MenuItem item = playbackPitchPopupMenu.getMenu().add(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedPitch);
item.setOnMenuItemClickListener(menuItem -> {
if (player == null) return false;
player.setPlaybackPitch(playbackPitch);
return true;
});
}
} }
private void buildItemPopupMenu(final PlayQueueItem item, final View view) { private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
@ -398,43 +355,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
} }
private ItemTouchHelper.SimpleCallback getItemTouchCallback() { private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { return new PlayQueueItemTouchCallback() {
@Override @Override
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, public void onMove(int sourceIndex, int targetIndex) {
int viewSizeOutOfBounds, int totalSize,
long msSinceStartScroll) {
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize,
viewSizeOutOfBounds, totalSize, msSinceStartScroll);
final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY));
return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType()) {
return false;
}
final int sourceIndex = source.getLayoutPosition();
final int targetIndex = target.getLayoutPosition();
if (player != null) player.getPlayQueue().move(sourceIndex, targetIndex); if (player != null) player.getPlayQueue().move(sourceIndex, targetIndex);
return true;
} }
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
}; };
} }
@ -499,7 +424,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
player.onPlayPrevious(); player.onPlayPrevious();
} else if (view.getId() == playPauseButton.getId()) { } else if (view.getId() == playPauseButton.getId()) {
player.onVideoPlayPause(); player.onPlayPause();
} else if (view.getId() == forwardButton.getId()) { } else if (view.getId() == forwardButton.getId()) {
player.onPlayNext(); player.onPlayNext();
@ -508,10 +433,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
player.onShuffleClicked(); player.onShuffleClicked();
} else if (view.getId() == playbackSpeedButton.getId()) { } else if (view.getId() == playbackSpeedButton.getId()) {
playbackSpeedPopupMenu.show(); openPlaybackParameterDialog();
} else if (view.getId() == playbackPitchButton.getId()) { } else if (view.getId() == playbackPitchButton.getId()) {
playbackPitchPopupMenu.show(); openPlaybackParameterDialog();
} else if (view.getId() == metadata.getId()) { } else if (view.getId() == metadata.getId()) {
scrollToSelected(); scrollToSelected();
@ -522,6 +447,21 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
} }
} }
////////////////////////////////////////////////////////////////////////////
// Playback Parameters
////////////////////////////////////////////////////////////////////////////
private void openPlaybackParameterDialog() {
if (player == null) return;
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(),
player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag());
}
@Override
public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) {
if (player != null) player.setPlaybackParameters(playbackTempo, playbackPitch);
}
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Seekbar Listener // Seekbar Listener
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@ -543,7 +483,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
@Override @Override
public void onStopTrackingTouch(SeekBar seekBar) { public void onStopTrackingTouch(SeekBar seekBar) {
if (player != null) player.simpleExoPlayer.seekTo(seekBar.getProgress()); if (player != null) player.seekTo(seekBar.getProgress());
seekDisplay.setVisibility(View.GONE); seekDisplay.setVisibility(View.GONE);
seeking = false; seeking = false;
} }
@ -573,6 +513,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
progressSeekBar.setProgress(currentProgress); progressSeekBar.setProgress(currentProgress);
progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000)); progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000));
} }
if (player != null) {
progressLiveSync.setClickable(!player.isLiveEdge());
}
} }
@Override @Override

View file

@ -49,6 +49,7 @@ import android.widget.TextView;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.MergingMediaSource;
@ -425,7 +426,7 @@ public abstract class VideoPlayer extends BasePlayer
// Create subtitle sources // Create subtitle sources
for (final Subtitles subtitle : info.getSubtitles()) { for (final Subtitles subtitle : info.getSubtitles()) {
final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType()); final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType());
if (mimeType == null || context == null) continue; if (mimeType == null) continue;
final Format textFormat = Format.createTextSampleFormat(null, mimeType, final Format textFormat = Format.createTextSampleFormat(null, mimeType,
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
@ -523,6 +524,12 @@ public abstract class VideoPlayer extends BasePlayer
onTextTrackUpdate(); onTextTrackUpdate();
} }
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
super.onPlaybackParametersChanged(playbackParameters);
playbackSpeedTextView.setText(formatSpeed(playbackParameters.speed));
}
@Override @Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
if (DEBUG) { if (DEBUG) {
@ -599,7 +606,7 @@ public abstract class VideoPlayer extends BasePlayer
@Override @Override
public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) { public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) {
if (!isPrepared) return; if (!isPrepared()) return;
if (duration != playbackSeekBar.getMax()) { if (duration != playbackSeekBar.getMax()) {
playbackEndTime.setText(getTimeString(duration)); playbackEndTime.setText(getTimeString(duration));
@ -615,6 +622,7 @@ public abstract class VideoPlayer extends BasePlayer
if (DEBUG && bufferPercent % 20 == 0) { //Limit log if (DEBUG && bufferPercent % 20 == 0) { //Limit log
Log.d(TAG, "updateProgress() called with: isVisible = " + isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]"); Log.d(TAG, "updateProgress() called with: isVisible = " + isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]");
} }
playbackLiveSync.setClickable(!isLiveEdge());
} }
@Override @Override
@ -624,8 +632,6 @@ public abstract class VideoPlayer extends BasePlayer
} }
protected void onFullScreenButtonClicked() { protected void onFullScreenButtonClicked() {
if (!isPlayerReady()) return;
changeState(STATE_BLOCKED); changeState(STATE_BLOCKED);
} }
@ -720,7 +726,7 @@ public abstract class VideoPlayer extends BasePlayer
wasPlaying = simpleExoPlayer.getPlayWhenReady(); wasPlaying = simpleExoPlayer.getPlayWhenReady();
} }
private void onPlaybackSpeedClicked() { public void onPlaybackSpeedClicked() {
if (DEBUG) Log.d(TAG, "onPlaybackSpeedClicked() called"); if (DEBUG) Log.d(TAG, "onPlaybackSpeedClicked() called");
playbackSpeedPopupMenu.show(); playbackSpeedPopupMenu.show();
isSomePopupMenuVisible = true; isSomePopupMenuVisible = true;
@ -735,7 +741,7 @@ public abstract class VideoPlayer extends BasePlayer
} }
private void onResizeClicked() { private void onResizeClicked() {
if (getAspectRatioFrameLayout() != null && context != null) { if (getAspectRatioFrameLayout() != null) {
final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode();
final int newResizeMode = nextResizeMode(currentResizeMode); final int newResizeMode = nextResizeMode(currentResizeMode);
getAspectRatioFrameLayout().setResizeMode(newResizeMode); getAspectRatioFrameLayout().setResizeMode(newResizeMode);
@ -772,7 +778,7 @@ public abstract class VideoPlayer extends BasePlayer
public void onStopTrackingTouch(SeekBar seekBar) { public void onStopTrackingTouch(SeekBar seekBar) {
if (DEBUG) Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]"); if (DEBUG) Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]");
simpleExoPlayer.seekTo(seekBar.getProgress()); seekTo(seekBar.getProgress());
if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) simpleExoPlayer.setPlayWhenReady(true); if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) simpleExoPlayer.setPlayWhenReady(true);
playbackCurrentTime.setText(getTimeString(seekBar.getProgress())); playbackCurrentTime.setText(getTimeString(seekBar.getProgress()));

View file

@ -3,7 +3,6 @@ package org.schabi.newpipe.player.helper;
import android.animation.Animator; import android.animation.Animator;
import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.media.AudioFocusRequest; import android.media.AudioFocusRequest;
@ -18,10 +17,14 @@ import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderCounters;
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AudioRendererEventListener { public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
AudioRendererEventListener {
private static final String TAG = "AudioFocusReactor"; private static final String TAG = "AudioFocusReactor";
private static final boolean SHOULD_BUILD_FOCUS_REQUEST =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
private static final int DUCK_DURATION = 1500; private static final int DUCK_DURATION = 1500;
private static final float DUCK_AUDIO_TO = .2f; private static final float DUCK_AUDIO_TO = .2f;
@ -34,13 +37,14 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
private final AudioFocusRequest request; private final AudioFocusRequest request;
public AudioReactor(@NonNull final Context context, @NonNull final SimpleExoPlayer player) { public AudioReactor(@NonNull final Context context,
@NonNull final SimpleExoPlayer player) {
this.player = player; this.player = player;
this.context = context; this.context = context;
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
player.setAudioDebugListener(this); player.addAudioDebugListener(this);
if (shouldBuildFocusRequest()) { if (SHOULD_BUILD_FOCUS_REQUEST) {
request = new AudioFocusRequest.Builder(FOCUS_GAIN_TYPE) request = new AudioFocusRequest.Builder(FOCUS_GAIN_TYPE)
.setAcceptsDelayedFocusGain(true) .setAcceptsDelayedFocusGain(true)
.setWillPauseWhenDucked(true) .setWillPauseWhenDucked(true)
@ -51,12 +55,17 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
} }
} }
public void dispose() {
abandonAudioFocus();
player.removeAudioDebugListener(this);
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Audio Manager // Audio Manager
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public void requestAudioFocus() { public void requestAudioFocus() {
if (shouldBuildFocusRequest()) { if (SHOULD_BUILD_FOCUS_REQUEST) {
audioManager.requestAudioFocus(request); audioManager.requestAudioFocus(request);
} else { } else {
audioManager.requestAudioFocus(this, STREAM_TYPE, FOCUS_GAIN_TYPE); audioManager.requestAudioFocus(this, STREAM_TYPE, FOCUS_GAIN_TYPE);
@ -64,7 +73,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
} }
public void abandonAudioFocus() { public void abandonAudioFocus() {
if (shouldBuildFocusRequest()) { if (SHOULD_BUILD_FOCUS_REQUEST) {
audioManager.abandonAudioFocusRequest(request); audioManager.abandonAudioFocusRequest(request);
} else { } else {
audioManager.abandonAudioFocus(this); audioManager.abandonAudioFocus(this);
@ -83,26 +92,6 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
audioManager.setStreamVolume(STREAM_TYPE, volume, 0); audioManager.setStreamVolume(STREAM_TYPE, volume, 0);
} }
private boolean shouldBuildFocusRequest() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
}
public void registerMediaButtonEventReceiver(ComponentName componentName) {
if (android.os.Build.VERSION.SDK_INT > 27) {
Log.e(TAG, "registerMediaButtonEventReceiver has been deprecated and maybe not supported anymore.");
return;
}
audioManager.registerMediaButtonEventReceiver(componentName);
}
public void unregisterMediaButtonEventReceiver(ComponentName componentName) {
if (android.os.Build.VERSION.SDK_INT > 27) {
Log.e(TAG, "unregisterMediaButtonEventReceiver has been deprecated and maybe not supported anymore.");
return;
}
audioManager.unregisterMediaButtonEventReceiver(componentName);
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// AudioFocus // AudioFocus
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -165,12 +154,8 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
player.setVolume(to); player.setVolume(to);
} }
}); });
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { valueAnimator.addUpdateListener(animation ->
@Override player.setVolume(((float) animation.getAnimatedValue())));
public void onAnimationUpdate(ValueAnimator animation) {
player.setVolume(((float) animation.getAnimatedValue()));
}
});
valueAnimator.start(); valueAnimator.start();
} }

View file

@ -0,0 +1,38 @@
package org.schabi.newpipe.player.helper;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.v4.media.session.MediaSessionCompat;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import org.schabi.newpipe.player.mediasession.DummyPlaybackPreparer;
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
import org.schabi.newpipe.player.mediasession.PlayQueueNavigator;
import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController;
public class MediaSessionManager {
private static final String TAG = "MediaSessionManager";
private final MediaSessionCompat mediaSession;
private final MediaSessionConnector sessionConnector;
public MediaSessionManager(@NonNull final Context context,
@NonNull final Player player,
@NonNull final MediaSessionCallback callback) {
this.mediaSession = new MediaSessionCompat(context, TAG);
this.sessionConnector = new MediaSessionConnector(mediaSession,
new PlayQueuePlaybackController(callback));
this.sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback));
this.sessionConnector.setPlayer(player, new DummyPlaybackPreparer());
}
public MediaSessionCompat getMediaSession() {
return mediaSession;
}
public MediaSessionConnector getSessionConnector() {
return sessionConnector;
}
}

View file

@ -0,0 +1,379 @@
package org.schabi.newpipe.player.helper;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.view.View;
import android.widget.CheckBox;
import android.widget.SeekBar;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.SliderStrategy;
import static org.schabi.newpipe.player.BasePlayer.DEBUG;
public class PlaybackParameterDialog extends DialogFragment {
@NonNull private static final String TAG = "PlaybackParameterDialog";
public static final double MINIMUM_PLAYBACK_VALUE = 0.25f;
public static final double MAXIMUM_PLAYBACK_VALUE = 3.00f;
public static final char STEP_UP_SIGN = '+';
public static final char STEP_DOWN_SIGN = '-';
public static final double PLAYBACK_STEP_VALUE = 0.05f;
public static final double NIGHTCORE_TEMPO = 1.20f;
public static final double NIGHTCORE_PITCH_LOWER = 1.15f;
public static final double NIGHTCORE_PITCH_UPPER = 1.25f;
public static final double DEFAULT_TEMPO = 1.00f;
public static final double DEFAULT_PITCH = 1.00f;
@NonNull private static final String INITIAL_TEMPO_KEY = "initial_tempo_key";
@NonNull private static final String INITIAL_PITCH_KEY = "initial_pitch_key";
public interface Callback {
void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch);
}
@Nullable private Callback callback;
@NonNull private final SliderStrategy strategy = new SliderStrategy.Quadratic(
MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE,
/*centerAt=*/1.00f, /*sliderGranularity=*/10000);
private double initialTempo = DEFAULT_TEMPO;
private double initialPitch = DEFAULT_PITCH;
@Nullable private SeekBar tempoSlider;
@Nullable private TextView tempoMinimumText;
@Nullable private TextView tempoMaximumText;
@Nullable private TextView tempoCurrentText;
@Nullable private TextView tempoStepDownText;
@Nullable private TextView tempoStepUpText;
@Nullable private SeekBar pitchSlider;
@Nullable private TextView pitchMinimumText;
@Nullable private TextView pitchMaximumText;
@Nullable private TextView pitchCurrentText;
@Nullable private TextView pitchStepDownText;
@Nullable private TextView pitchStepUpText;
@Nullable private CheckBox unhookingCheckbox;
@Nullable private TextView nightCorePresetText;
@Nullable private TextView resetPresetText;
public static PlaybackParameterDialog newInstance(final double playbackTempo,
final double playbackPitch) {
PlaybackParameterDialog dialog = new PlaybackParameterDialog();
dialog.initialTempo = playbackTempo;
dialog.initialPitch = playbackPitch;
return dialog;
}
/*//////////////////////////////////////////////////////////////////////////
// Lifecycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context != null && context instanceof Callback) {
callback = (Callback) context;
} else {
dismiss();
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO);
initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putDouble(INITIAL_TEMPO_KEY, initialTempo);
outState.putDouble(INITIAL_PITCH_KEY, initialPitch);
}
/*//////////////////////////////////////////////////////////////////////////
// Dialog
//////////////////////////////////////////////////////////////////////////*/
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null);
setupControlViews(view);
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
.setTitle(R.string.playback_speed_control)
.setView(view)
.setCancelable(true)
.setNegativeButton(R.string.cancel, (dialogInterface, i) ->
setPlaybackParameters(initialTempo, initialPitch))
.setPositiveButton(R.string.finish, (dialogInterface, i) ->
setCurrentPlaybackParameters());
return dialogBuilder.create();
}
/*//////////////////////////////////////////////////////////////////////////
// Control Views
//////////////////////////////////////////////////////////////////////////*/
private void setupControlViews(@NonNull View rootView) {
setupHookingControl(rootView);
setupTempoControl(rootView);
setupPitchControl(rootView);
setupPresetControl(rootView);
}
private void setupTempoControl(@NonNull View rootView) {
tempoSlider = rootView.findViewById(R.id.tempoSeekbar);
tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText);
tempoMaximumText = rootView.findViewById(R.id.tempoMaximumText);
tempoCurrentText = rootView.findViewById(R.id.tempoCurrentText);
tempoStepUpText = rootView.findViewById(R.id.tempoStepUp);
tempoStepDownText = rootView.findViewById(R.id.tempoStepDown);
if (tempoCurrentText != null)
tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo));
if (tempoMaximumText != null)
tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE));
if (tempoMinimumText != null)
tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE));
if (tempoStepUpText != null) {
tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE));
tempoStepUpText.setOnClickListener(view -> {
onTempoSliderUpdated(getCurrentTempo() + PLAYBACK_STEP_VALUE);
setCurrentPlaybackParameters();
});
}
if (tempoStepDownText != null) {
tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE));
tempoStepDownText.setOnClickListener(view -> {
onTempoSliderUpdated(getCurrentTempo() - PLAYBACK_STEP_VALUE);
setCurrentPlaybackParameters();
});
}
if (tempoSlider != null) {
tempoSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE));
tempoSlider.setProgress(strategy.progressOf(initialTempo));
tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener());
}
}
private void setupPitchControl(@NonNull View rootView) {
pitchSlider = rootView.findViewById(R.id.pitchSeekbar);
pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText);
pitchMaximumText = rootView.findViewById(R.id.pitchMaximumText);
pitchCurrentText = rootView.findViewById(R.id.pitchCurrentText);
pitchStepDownText = rootView.findViewById(R.id.pitchStepDown);
pitchStepUpText = rootView.findViewById(R.id.pitchStepUp);
if (pitchCurrentText != null)
pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch));
if (pitchMaximumText != null)
pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE));
if (pitchMinimumText != null)
pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE));
if (pitchStepUpText != null) {
pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE));
pitchStepUpText.setOnClickListener(view -> {
onPitchSliderUpdated(getCurrentPitch() + PLAYBACK_STEP_VALUE);
setCurrentPlaybackParameters();
});
}
if (pitchStepDownText != null) {
pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE));
pitchStepDownText.setOnClickListener(view -> {
onPitchSliderUpdated(getCurrentPitch() - PLAYBACK_STEP_VALUE);
setCurrentPlaybackParameters();
});
}
if (pitchSlider != null) {
pitchSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE));
pitchSlider.setProgress(strategy.progressOf(initialPitch));
pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener());
}
}
private void setupHookingControl(@NonNull View rootView) {
unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox);
if (unhookingCheckbox != null) {
unhookingCheckbox.setChecked(initialPitch != initialTempo);
unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
if (isChecked) return;
// When unchecked, slide back to the minimum of current tempo or pitch
final double minimum = Math.min(getCurrentPitch(), getCurrentTempo());
setSliders(minimum);
setCurrentPlaybackParameters();
});
}
}
private void setupPresetControl(@NonNull View rootView) {
nightCorePresetText = rootView.findViewById(R.id.presetNightcore);
if (nightCorePresetText != null) {
nightCorePresetText.setOnClickListener(view -> {
final double randomPitch = NIGHTCORE_PITCH_LOWER +
Math.random() * (NIGHTCORE_PITCH_UPPER - NIGHTCORE_PITCH_LOWER);
setTempoSlider(NIGHTCORE_TEMPO);
setPitchSlider(randomPitch);
setCurrentPlaybackParameters();
});
}
resetPresetText = rootView.findViewById(R.id.presetReset);
if (resetPresetText != null) {
resetPresetText.setOnClickListener(view -> {
setTempoSlider(DEFAULT_TEMPO);
setPitchSlider(DEFAULT_PITCH);
setCurrentPlaybackParameters();
});
}
}
/*//////////////////////////////////////////////////////////////////////////
// Sliders
//////////////////////////////////////////////////////////////////////////*/
private SeekBar.OnSeekBarChangeListener getOnTempoChangedListener() {
return new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
final double currentTempo = strategy.valueOf(progress);
if (fromUser) {
onTempoSliderUpdated(currentTempo);
setCurrentPlaybackParameters();
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// Do Nothing.
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// Do Nothing.
}
};
}
private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() {
return new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
final double currentPitch = strategy.valueOf(progress);
if (fromUser) { // this change is first in chain
onPitchSliderUpdated(currentPitch);
setCurrentPlaybackParameters();
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// Do Nothing.
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// Do Nothing.
}
};
}
private void onTempoSliderUpdated(final double newTempo) {
if (unhookingCheckbox == null) return;
if (!unhookingCheckbox.isChecked()) {
setSliders(newTempo);
} else {
setTempoSlider(newTempo);
}
}
private void onPitchSliderUpdated(final double newPitch) {
if (unhookingCheckbox == null) return;
if (!unhookingCheckbox.isChecked()) {
setSliders(newPitch);
} else {
setPitchSlider(newPitch);
}
}
private void setSliders(final double newValue) {
setTempoSlider(newValue);
setPitchSlider(newValue);
}
private void setTempoSlider(final double newTempo) {
if (tempoSlider == null) return;
tempoSlider.setProgress(strategy.progressOf(newTempo));
}
private void setPitchSlider(final double newPitch) {
if (pitchSlider == null) return;
pitchSlider.setProgress(strategy.progressOf(newPitch));
}
/*//////////////////////////////////////////////////////////////////////////
// Helper
//////////////////////////////////////////////////////////////////////////*/
private void setCurrentPlaybackParameters() {
setPlaybackParameters(getCurrentTempo(), getCurrentPitch());
}
private void setPlaybackParameters(final double tempo, final double pitch) {
if (callback != null && tempoCurrentText != null && pitchCurrentText != null) {
if (DEBUG) Log.d(TAG, "Setting playback parameters to " +
"tempo=[" + tempo + "], " +
"pitch=[" + pitch + "]");
tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo));
pitchCurrentText.setText(PlayerHelper.formatPitch(pitch));
callback.onPlaybackParameterChanged((float) tempo, (float) pitch);
}
}
private double getCurrentTempo() {
return tempoSlider == null ? initialTempo : strategy.valueOf(
tempoSlider.getProgress());
}
private double getCurrentPitch() {
return pitchSlider == null ? initialPitch : strategy.valueOf(
pitchSlider.getProgress());
}
@NonNull
private static String getStepUpPercentString(final double percent) {
return STEP_UP_SIGN + PlayerHelper.formatPitch(percent);
}
@NonNull
private static String getStepDownPercentString(final double percent) {
return STEP_DOWN_SIGN + PlayerHelper.formatPitch(percent);
}
}

View file

@ -60,11 +60,11 @@ public class PlayerHelper {
: stringFormatter.format("%02d:%02d", minutes, seconds).toString(); : stringFormatter.format("%02d:%02d", minutes, seconds).toString();
} }
public static String formatSpeed(float speed) { public static String formatSpeed(double speed) {
return speedFormatter.format(speed); return speedFormatter.format(speed);
} }
public static String formatPitch(float pitch) { public static String formatPitch(double pitch) {
return pitchFormatter.format(pitch); return pitchFormatter.format(pitch);
} }

View file

@ -0,0 +1,45 @@
package org.schabi.newpipe.player.mediasession;
import android.net.Uri;
import android.os.Bundle;
import android.os.ResultReceiver;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
public class DummyPlaybackPreparer implements MediaSessionConnector.PlaybackPreparer {
@Override
public long getSupportedPrepareActions() {
return 0;
}
@Override
public void onPrepare() {
}
@Override
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
}
@Override
public void onPrepareFromSearch(String query, Bundle extras) {
}
@Override
public void onPrepareFromUri(Uri uri, Bundle extras) {
}
@Override
public String[] getCommands() {
return new String[0];
}
@Override
public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
}
}

View file

@ -0,0 +1,17 @@
package org.schabi.newpipe.player.mediasession;
import android.support.v4.media.MediaDescriptionCompat;
public interface MediaSessionCallback {
void onSkipToPrevious();
void onSkipToNext();
void onSkipToIndex(final int index);
int getCurrentPlayingIndex();
int getQueueSize();
MediaDescriptionCompat getQueueMetadata(final int index);
void onPlay();
void onPause();
void onSetShuffle(final boolean isShuffled);
}

View file

@ -0,0 +1,111 @@
package org.schabi.newpipe.player.mediasession;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.media.session.MediaSessionCompat;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator {
public static final int DEFAULT_MAX_QUEUE_SIZE = 10;
private final MediaSessionCompat mediaSession;
private final MediaSessionCallback callback;
private final int maxQueueSize;
private long activeQueueItemId;
public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession,
@NonNull final MediaSessionCallback callback) {
this.mediaSession = mediaSession;
this.callback = callback;
this.maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
this.activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
}
@Override
public long getSupportedQueueNavigatorActions(@Nullable Player player) {
return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM;
}
@Override
public void onTimelineChanged(Player player) {
publishFloatingQueueWindow();
}
@Override
public void onCurrentWindowIndexChanged(Player player) {
if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID
|| player.getCurrentTimeline().getWindowCount() > maxQueueSize) {
publishFloatingQueueWindow();
} else if (!player.getCurrentTimeline().isEmpty()) {
activeQueueItemId = player.getCurrentWindowIndex();
}
}
@Override
public long getActiveQueueItemId(@Nullable Player player) {
return callback.getCurrentPlayingIndex();
}
@Override
public void onSkipToPrevious(Player player) {
callback.onSkipToPrevious();
}
@Override
public void onSkipToQueueItem(Player player, long id) {
callback.onSkipToIndex((int) id);
}
@Override
public void onSkipToNext(Player player) {
callback.onSkipToNext();
}
private void publishFloatingQueueWindow() {
if (callback.getQueueSize() == 0) {
mediaSession.setQueue(Collections.<MediaSessionCompat.QueueItem>emptyList());
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
return;
}
// Yes this is almost a copypasta, got a problem with that? =\
int windowCount = callback.getQueueSize();
int currentWindowIndex = callback.getCurrentPlayingIndex();
int queueSize = Math.min(maxQueueSize, windowCount);
int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0,
windowCount - queueSize);
List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
for (int i = startIndex; i < startIndex + queueSize; i++) {
queue.add(new MediaSessionCompat.QueueItem(callback.getQueueMetadata(i), i));
}
mediaSession.setQueue(queue);
activeQueueItemId = currentWindowIndex;
}
@Override
public String[] getCommands() {
return new String[0];
}
@Override
public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
}
}

View file

@ -0,0 +1,31 @@
package org.schabi.newpipe.player.mediasession;
import android.support.v4.media.session.PlaybackStateCompat;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.DefaultPlaybackController;
public class PlayQueuePlaybackController extends DefaultPlaybackController {
private final MediaSessionCallback callback;
public PlayQueuePlaybackController(final MediaSessionCallback callback) {
super();
this.callback = callback;
}
@Override
public void onPlay(Player player) {
callback.onPlay();
}
@Override
public void onPause(Player player) {
callback.onPause();
}
@Override
public void onSetShuffleMode(Player player, int shuffleMode) {
callback.onSetShuffle(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL
|| shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP);
}
}

View file

@ -72,7 +72,13 @@ public class FailedMediaSource implements ManagedMediaSource {
public void releaseSource() {} public void releaseSource() {}
@Override @Override
public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,
final boolean isInterruptable) {
return newIdentity != playQueueItem || canRetry(); return newIdentity != playQueueItem || canRetry();
} }
@Override
public boolean isStreamEqual(@NonNull PlayQueueItem stream) {
return playQueueItem == stream;
}
} }

View file

@ -59,7 +59,13 @@ public class LoadedMediaSource implements ManagedMediaSource {
} }
@Override @Override
public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity,
return newIdentity != stream || isExpired(); final boolean isInterruptable) {
return newIdentity != stream || (isInterruptable && isExpired());
}
@Override
public boolean isStreamEqual(@NonNull PlayQueueItem stream) {
return this.stream == stream;
} }
} }

View file

@ -7,5 +7,21 @@ import com.google.android.exoplayer2.source.MediaSource;
import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItem;
public interface ManagedMediaSource extends MediaSource { public interface ManagedMediaSource extends MediaSource {
boolean canReplace(@NonNull final PlayQueueItem newIdentity); /**
* Determines whether or not this {@link ManagedMediaSource} can be replaced.
*
* @param newIdentity a stream the {@link ManagedMediaSource} should encapsulate over, if
* it is different from the existing stream in the
* {@link ManagedMediaSource}, then it should be replaced.
* @param isInterruptable specifies if this {@link ManagedMediaSource} potentially
* being played.
* */
boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,
final boolean isInterruptable);
/**
* Determines if the {@link PlayQueueItem} is the one the
* {@link ManagedMediaSource} encapsulates over.
* */
boolean isStreamEqual(@NonNull final PlayQueueItem stream);
} }

View file

@ -19,7 +19,13 @@ public class PlaceholderMediaSource implements ManagedMediaSource {
@Override public void releaseSource() {} @Override public void releaseSource() {}
@Override @Override
public boolean canReplace(@NonNull final PlayQueueItem newIdentity) { public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity,
final boolean isInterruptable) {
return true; return true;
} }
@Override
public boolean isStreamEqual(@NonNull PlayQueueItem stream) {
return false;
}
} }

View file

@ -0,0 +1,77 @@
package org.schabi.newpipe.player.playback;
import android.net.Uri;
import android.support.v4.media.MediaDescriptionCompat;
import org.schabi.newpipe.player.BasePlayer;
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
import org.schabi.newpipe.playlist.PlayQueueItem;
public class BasePlayerMediaSession implements MediaSessionCallback {
private BasePlayer player;
public BasePlayerMediaSession(final BasePlayer player) {
this.player = player;
}
@Override
public void onSkipToPrevious() {
player.onPlayPrevious();
}
@Override
public void onSkipToNext() {
player.onPlayNext();
}
@Override
public void onSkipToIndex(int index) {
if (player.getPlayQueue() == null) return;
player.onSelected(player.getPlayQueue().getItem(index));
}
@Override
public int getCurrentPlayingIndex() {
if (player.getPlayQueue() == null) return -1;
return player.getPlayQueue().getIndex();
}
@Override
public int getQueueSize() {
if (player.getPlayQueue() == null) return -1;
return player.getPlayQueue().size();
}
@Override
public MediaDescriptionCompat getQueueMetadata(int index) {
if (player.getPlayQueue() == null || player.getPlayQueue().getItem(index) == null) {
return null;
}
final PlayQueueItem item = player.getPlayQueue().getItem(index);
MediaDescriptionCompat.Builder descriptionBuilder = new MediaDescriptionCompat.Builder()
.setMediaId(String.valueOf(index))
.setTitle(item.getTitle())
.setSubtitle(item.getUploader());
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
if (thumbnailUri != null) descriptionBuilder.setIconUri(thumbnailUri);
return descriptionBuilder.build();
}
@Override
public void onPlay() {
player.onPlay();
}
@Override
public void onPause() {
player.onPause();
}
@Override
public void onSetShuffle(boolean isShuffled) {
player.onShuffleModeEnabledChanged(isShuffled);
}
}

View file

@ -21,15 +21,15 @@ import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueEvent; import org.schabi.newpipe.playlist.events.PlayQueueEvent;
import org.schabi.newpipe.playlist.events.RemoveEvent; import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.ReorderEvent; import org.schabi.newpipe.playlist.events.ReorderEvent;
import org.schabi.newpipe.util.ServiceHelper;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
@ -42,7 +42,7 @@ import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.playlist.PlayQueue.DEBUG; import static org.schabi.newpipe.playlist.PlayQueue.DEBUG;
public class MediaSourceManager { public class MediaSourceManager {
@NonNull private final static String TAG = "MediaSourceManager"; @NonNull private final String TAG = "MediaSourceManager@" + hashCode();
/** /**
* Determines how many streams before and after the current stream should be loaded. * Determines how many streams before and after the current stream should be loaded.
@ -60,17 +60,18 @@ public class MediaSourceManager {
@NonNull private final PlayQueue playQueue; @NonNull private final PlayQueue playQueue;
/** /**
* Determines how long NEIGHBOURING {@link LoadedMediaSource} window of a currently playing * Determines the gap time between the playback position and the playback duration which
* {@link MediaSource} is allowed to stay in the playlist timeline. This is to ensure * the {@link #getEdgeIntervalSignal()} begins to request loading.
* the {@link StreamInfo} used in subsequent playback is up-to-date.
* <br><br>
* Once a {@link LoadedMediaSource} has expired, a new source will be reloaded to
* replace the expired one on whereupon {@link #loadImmediate()} is called.
* *
* @see #loadImmediate() * @see #progressUpdateIntervalMillis
* @see #isCorrectionNeeded(PlayQueueItem)
* */ * */
private final long windowRefreshTimeMillis; private final long playbackNearEndGapMillis;
/**
* Determines the interval which the {@link #getEdgeIntervalSignal()} waits for between
* each request for loading, once {@link #playbackNearEndGapMillis} has reached.
* */
private final long progressUpdateIntervalMillis;
@NonNull private final Observable<Long> nearEndIntervalSignal;
/** /**
* Process only the last load order when receiving a stream of load orders (lessens I/O). * Process only the last load order when receiving a stream of load orders (lessens I/O).
@ -106,23 +107,31 @@ public class MediaSourceManager {
public MediaSourceManager(@NonNull final PlaybackListener listener, public MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue) { @NonNull final PlayQueue playQueue) {
this(listener, playQueue, this(listener, playQueue, /*loadDebounceMillis=*/400L,
/*loadDebounceMillis=*/400L, /*playbackNearEndGapMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.SECONDS),
/*windowRefreshTimeMillis=*/TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES)); /*progressUpdateIntervalMillis*/TimeUnit.MILLISECONDS.convert(2, TimeUnit.SECONDS));
} }
private MediaSourceManager(@NonNull final PlaybackListener listener, private MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue, @NonNull final PlayQueue playQueue,
final long loadDebounceMillis, final long loadDebounceMillis,
final long windowRefreshTimeMillis) { final long playbackNearEndGapMillis,
final long progressUpdateIntervalMillis) {
if (playQueue.getBroadcastReceiver() == null) { if (playQueue.getBroadcastReceiver() == null) {
throw new IllegalArgumentException("Play Queue has not been initialized."); throw new IllegalArgumentException("Play Queue has not been initialized.");
} }
if (playbackNearEndGapMillis < progressUpdateIntervalMillis) {
throw new IllegalArgumentException("Playback end gap=[" + playbackNearEndGapMillis +
" ms] must be longer than update interval=[ " + progressUpdateIntervalMillis +
" ms] for them to be useful.");
}
this.playbackListener = listener; this.playbackListener = listener;
this.playQueue = playQueue; this.playQueue = playQueue;
this.windowRefreshTimeMillis = windowRefreshTimeMillis; this.playbackNearEndGapMillis = playbackNearEndGapMillis;
this.progressUpdateIntervalMillis = progressUpdateIntervalMillis;
this.nearEndIntervalSignal = getEdgeIntervalSignal();
this.loadDebounceMillis = loadDebounceMillis; this.loadDebounceMillis = loadDebounceMillis;
this.debouncedSignal = PublishSubject.create(); this.debouncedSignal = PublishSubject.create();
@ -161,28 +170,6 @@ public class MediaSourceManager {
sources.releaseSource(); sources.releaseSource();
} }
/**
* Loads the current playing stream and the streams within its windowSize bound.
*
* Unblocks the player once the item at the current index is loaded.
* */
public void load() {
if (DEBUG) Log.d(TAG, "load() called.");
loadDebounced();
}
/**
* Blocks the player and repopulate the sources.
*
* Does not ensure the player is unblocked and should be done explicitly
* through {@link #load() load}.
* */
public void reset() {
if (DEBUG) Log.d(TAG, "reset() called.");
maybeBlock();
populateSources();
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Event Reactor // Event Reactor
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -219,11 +206,13 @@ public class MediaSourceManager {
switch (event.type()) { switch (event.type()) {
case INIT: case INIT:
case ERROR: case ERROR:
reset(); maybeBlock();
break;
case APPEND: case APPEND:
populateSources(); populateSources();
break; break;
case SELECT:
maybeRenewCurrentIndex();
break;
case REMOVE: case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) event; final RemoveEvent removeEvent = (RemoveEvent) event;
remove(removeEvent.getRemoveIndex()); remove(removeEvent.getRemoveIndex());
@ -238,7 +227,6 @@ public class MediaSourceManager {
final ReorderEvent reorderEvent = (ReorderEvent) event; final ReorderEvent reorderEvent = (ReorderEvent) event;
move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex()); move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex());
break; break;
case SELECT:
case RECOVERY: case RECOVERY:
default: default:
break; break;
@ -280,15 +268,10 @@ public class MediaSourceManager {
private boolean isPlaybackReady() { private boolean isPlaybackReady() {
if (sources.getSize() != playQueue.size()) return false; if (sources.getSize() != playQueue.size()) return false;
final MediaSource mediaSource = sources.getMediaSource(playQueue.getIndex()); final ManagedMediaSource mediaSource =
(ManagedMediaSource) sources.getMediaSource(playQueue.getIndex());
final PlayQueueItem playQueueItem = playQueue.getItem(); final PlayQueueItem playQueueItem = playQueue.getItem();
return mediaSource.isStreamEqual(playQueueItem);
if (mediaSource instanceof LoadedMediaSource) {
return playQueueItem == ((LoadedMediaSource) mediaSource).getStream();
} else if (mediaSource instanceof FailedMediaSource) {
return playQueueItem == ((FailedMediaSource) mediaSource).getStream();
}
return false;
} }
private void maybeBlock() { private void maybeBlock() {
@ -319,7 +302,7 @@ public class MediaSourceManager {
if (DEBUG) Log.d(TAG, "onPlaybackSynchronize() called."); if (DEBUG) Log.d(TAG, "onPlaybackSynchronize() called.");
final PlayQueueItem currentItem = playQueue.getItem(); final PlayQueueItem currentItem = playQueue.getItem();
if (isBlocked.get() || currentItem == null) return; if (isBlocked.get() || !isPlaybackReady() || currentItem == null) return;
final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info); final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info);
final Consumer<Throwable> onError = throwable -> syncInternal(currentItem, null); final Consumer<Throwable> onError = throwable -> syncInternal(currentItem, null);
@ -347,8 +330,13 @@ public class MediaSourceManager {
// MediaSource Loading // MediaSource Loading
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private Observable<Long> getEdgeIntervalSignal() {
return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS)
.filter(ignored -> playbackListener.isNearPlaybackEdge(playbackNearEndGapMillis));
}
private Disposable getDebouncedLoader() { private Disposable getDebouncedLoader() {
return debouncedSignal return debouncedSignal.mergeWith(nearEndIntervalSignal)
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(timestamp -> loadImmediate()); .subscribe(timestamp -> loadImmediate());
@ -359,13 +347,14 @@ public class MediaSourceManager {
} }
private void loadImmediate() { private void loadImmediate() {
if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called");
// The current item has higher priority // The current item has higher priority
final int currentIndex = playQueue.getIndex(); final int currentIndex = playQueue.getIndex();
final PlayQueueItem currentItem = playQueue.getItem(currentIndex); final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
if (currentItem == null) return; if (currentItem == null) return;
// Evict the items being loaded to free up memory // Evict the items being loaded to free up memory
if (!loadingItems.contains(currentItem) && loaderReactor.size() > MAXIMUM_LOADER_SIZE) { if (loaderReactor.size() > MAXIMUM_LOADER_SIZE) {
loaderReactor.clear(); loaderReactor.clear();
loadingItems.clear(); loadingItems.clear();
} }
@ -377,7 +366,7 @@ public class MediaSourceManager {
final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE); final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE);
final int rightLimit = currentIndex + WINDOW_SIZE + 1; final int rightLimit = currentIndex + WINDOW_SIZE + 1;
final int rightBound = Math.min(playQueue.size(), rightLimit); final int rightBound = Math.min(playQueue.size(), rightLimit);
final List<PlayQueueItem> items = new ArrayList<>( final Set<PlayQueueItem> items = new HashSet<>(
playQueue.getStreams().subList(leftBound,rightBound)); playQueue.getStreams().subList(leftBound,rightBound));
// Do a round robin // Do a round robin
@ -385,6 +374,7 @@ public class MediaSourceManager {
if (excess >= 0) { if (excess >= 0) {
items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
} }
items.remove(currentItem);
for (final PlayQueueItem item : items) { for (final PlayQueueItem item : items) {
maybeLoadItem(item); maybeLoadItem(item);
@ -406,8 +396,6 @@ public class MediaSourceManager {
.subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource)); .subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource));
loaderReactor.add(loader); loaderReactor.add(loader);
} }
maybeSynchronizePlayer();
} }
private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) { private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) {
@ -423,7 +411,8 @@ public class MediaSourceManager {
return new FailedMediaSource(stream, exception); return new FailedMediaSource(stream, exception);
} }
final long expiration = System.currentTimeMillis() + windowRefreshTimeMillis; final long expiration = System.currentTimeMillis() +
ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId());
return new LoadedMediaSource(source, stream, expiration); return new LoadedMediaSource(source, stream, expiration);
}).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable)); }).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable));
} }
@ -459,14 +448,37 @@ public class MediaSourceManager {
if (index == -1 || index >= sources.getSize()) return false; if (index == -1 || index >= sources.getSize()) return false;
final ManagedMediaSource mediaSource = (ManagedMediaSource) sources.getMediaSource(index); final ManagedMediaSource mediaSource = (ManagedMediaSource) sources.getMediaSource(index);
return mediaSource.shouldBeReplacedWith(item,
if (index == playQueue.getIndex() && mediaSource instanceof LoadedMediaSource) { /*mightBeInProgress=*/index != playQueue.getIndex());
return item != ((LoadedMediaSource) mediaSource).getStream();
} else {
return mediaSource.canReplace(item);
}
} }
/**
* Checks if the current playing index contains an expired {@link ManagedMediaSource}.
* If so, the expired source is replaced by a {@link PlaceholderMediaSource} and
* {@link #loadImmediate()} is called to reload the current item.
* <br><br>
* If not, then the media source at the current index is ready for playback, and
* {@link #maybeSynchronizePlayer()} is called.
* <br><br>
* Under both cases, {@link #maybeSync()} will be called to ensure the listener
* is up-to-date.
* */
private void maybeRenewCurrentIndex() {
final int currentIndex = playQueue.getIndex();
if (sources.getSize() <= currentIndex) return;
final ManagedMediaSource currentSource =
(ManagedMediaSource) sources.getMediaSource(currentIndex);
final PlayQueueItem currentItem = playQueue.getItem();
if (!currentSource.shouldBeReplacedWith(currentItem, /*canInterruptOnRenew=*/true)) {
maybeSynchronizePlayer();
return;
}
if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " +
"index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]");
update(currentIndex, new PlaceholderMediaSource(), this::loadImmediate);
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// MediaSource Playlist Helpers // MediaSource Playlist Helpers
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -476,6 +488,7 @@ public class MediaSourceManager {
this.sources.releaseSource(); this.sources.releaseSource();
this.sources = new DynamicConcatenatingMediaSource(false, this.sources = new DynamicConcatenatingMediaSource(false,
// Shuffling is done on PlayQueue, thus no need to use ExoPlayer's shuffle order
new ShuffleOrder.UnshuffledShuffleOrder(0)); new ShuffleOrder.UnshuffledShuffleOrder(0));
} }

View file

@ -11,6 +11,16 @@ import org.schabi.newpipe.playlist.PlayQueueItem;
import java.util.List; import java.util.List;
public interface PlaybackListener { public interface PlaybackListener {
/**
* Called to check if the currently playing stream is close to the end of its playback.
* Implementation should return true when the current playback position is within
* timeToEndMillis or less until its playback completes or transitions.
*
* May be called at any time.
* */
boolean isNearPlaybackEdge(final long timeToEndMillis);
/** /**
* Called when the stream at the current queue index is not ready yet. * Called when the stream at the current queue index is not ready yet.
* Signals to the listener to block the player from playing anything and notify the source * Signals to the listener to block the player from playing anything and notify the source

View file

@ -11,20 +11,19 @@ import org.schabi.newpipe.util.ExtractorHelper;
import java.io.Serializable; import java.io.Serializable;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
public class PlayQueueItem implements Serializable { public class PlayQueueItem implements Serializable {
final public static long RECOVERY_UNSET = Long.MIN_VALUE; public final static long RECOVERY_UNSET = Long.MIN_VALUE;
private final static String EMPTY_STRING = "";
final private String title; @NonNull final private String title;
final private String url; @NonNull final private String url;
final private int serviceId; final private int serviceId;
final private long duration; final private long duration;
final private String thumbnailUrl; @NonNull final private String thumbnailUrl;
final private String uploader; @NonNull final private String uploader;
final private StreamType streamType; @NonNull final private StreamType streamType;
private long recoveryPosition; private long recoveryPosition;
private Throwable error; private Throwable error;
@ -42,15 +41,16 @@ public class PlayQueueItem implements Serializable {
item.getThumbnailUrl(), item.getUploaderName(), item.getStreamType()); item.getThumbnailUrl(), item.getUploaderName(), item.getStreamType());
} }
private PlayQueueItem(final String name, final String url, final int serviceId, private PlayQueueItem(@Nullable final String name, @Nullable final String url,
final long duration, final String thumbnailUrl, final String uploader, final int serviceId, final long duration,
final StreamType streamType) { @Nullable final String thumbnailUrl, @Nullable final String uploader,
this.title = name; @NonNull final StreamType streamType) {
this.url = url; this.title = name != null ? name : EMPTY_STRING;
this.url = url != null ? url : EMPTY_STRING;
this.serviceId = serviceId; this.serviceId = serviceId;
this.duration = duration; this.duration = duration;
this.thumbnailUrl = thumbnailUrl; this.thumbnailUrl = thumbnailUrl != null ? thumbnailUrl : EMPTY_STRING;
this.uploader = uploader; this.uploader = uploader != null ? uploader : EMPTY_STRING;
this.streamType = streamType; this.streamType = streamType;
this.recoveryPosition = RECOVERY_UNSET; this.recoveryPosition = RECOVERY_UNSET;
@ -84,6 +84,7 @@ public class PlayQueueItem implements Serializable {
return uploader; return uploader;
} }
@NonNull
public StreamType getStreamType() { public StreamType getStreamType() {
return streamType; return streamType;
} }

View file

@ -1,28 +1,22 @@
package org.schabi.newpipe.playlist; package org.schabi.newpipe.playlist;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import com.nostra13.universalimageloader.core.process.BitmapProcessor;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
public class PlayQueueItemBuilder { public class PlayQueueItemBuilder {
private static final String TAG = PlayQueueItemBuilder.class.toString(); private static final String TAG = PlayQueueItemBuilder.class.toString();
private final int thumbnailWidthPx;
private final int thumbnailHeightPx;
private final DisplayImageOptions imageOptions;
public interface OnSelectedListener { public interface OnSelectedListener {
void selected(PlayQueueItem item, View view); void selected(PlayQueueItem item, View view);
void held(PlayQueueItem item, View view); void held(PlayQueueItem item, View view);
@ -31,11 +25,7 @@ public class PlayQueueItemBuilder {
private OnSelectedListener onItemClickListener; private OnSelectedListener onItemClickListener;
public PlayQueueItemBuilder(final Context context) { public PlayQueueItemBuilder(final Context context) {}
thumbnailWidthPx = context.getResources().getDimensionPixelSize(R.dimen.play_queue_thumbnail_width);
thumbnailHeightPx = context.getResources().getDimensionPixelSize(R.dimen.play_queue_thumbnail_height);
imageOptions = buildImageOptions(thumbnailWidthPx, thumbnailHeightPx);
}
public void setOnSelectedListener(OnSelectedListener listener) { public void setOnSelectedListener(OnSelectedListener listener) {
this.onItemClickListener = listener; this.onItemClickListener = listener;
@ -43,7 +33,8 @@ public class PlayQueueItemBuilder {
public void buildStreamInfoItem(final PlayQueueItemHolder holder, final PlayQueueItem item) { public void buildStreamInfoItem(final PlayQueueItemHolder holder, final PlayQueueItem item) {
if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle()); if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle());
if (!TextUtils.isEmpty(item.getUploader())) holder.itemAdditionalDetailsView.setText(item.getUploader()); holder.itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.getUploader(),
NewPipe.getNameOfService(item.getServiceId())));
if (item.getDuration() > 0) { if (item.getDuration() > 0) {
holder.itemDurationView.setText(Localization.getDurationString(item.getDuration())); holder.itemDurationView.setText(Localization.getDurationString(item.getDuration()));
@ -51,7 +42,8 @@ public class PlayQueueItemBuilder {
holder.itemDurationView.setVisibility(View.GONE); holder.itemDurationView.setVisibility(View.GONE);
} }
ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, imageOptions); ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
holder.itemRoot.setOnClickListener(view -> { holder.itemRoot.setOnClickListener(view -> {
if (onItemClickListener != null) { if (onItemClickListener != null) {
@ -81,23 +73,4 @@ public class PlayQueueItemBuilder {
return false; return false;
}; };
} }
private DisplayImageOptions buildImageOptions(final int widthPx, final int heightPx) {
final BitmapProcessor bitmapProcessor = bitmap -> {
final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, widthPx, heightPx, false);
bitmap.recycle();
return resizedBitmap;
};
return new DisplayImageOptions.Builder()
.showImageOnFail(R.drawable.dummy_thumbnail)
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
.showImageOnLoading(R.drawable.dummy_thumbnail)
.bitmapConfig(Bitmap.Config.RGB_565) // Users won't be able to see much anyways
.preProcessor(bitmapProcessor)
.imageScaleType(ImageScaleType.EXACTLY)
.cacheInMemory(true)
.cacheOnDisk(true)
.build();
}
} }

View file

@ -0,0 +1,52 @@
package org.schabi.newpipe.playlist;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleCallback {
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10;
private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25;
public PlayQueueItemTouchCallback() {
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
}
public abstract void onMove(final int sourceIndex, final int targetIndex);
@Override
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
int viewSizeOutOfBounds, int totalSize,
long msSinceStartScroll) {
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize,
viewSizeOutOfBounds, totalSize, msSinceStartScroll);
final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY));
return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType()) {
return false;
}
final int sourceIndex = source.getLayoutPosition();
final int targetIndex = target.getLayoutPosition();
onMove(sourceIndex, targetIndex);
return true;
}
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
}

View file

@ -6,12 +6,14 @@ import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.preference.ListPreference; import android.support.v7.preference.ListPreference;
import android.support.v7.preference.Preference; import android.support.v7.preference.Preference;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
import com.nononsenseapps.filepicker.Utils; import com.nononsenseapps.filepicker.Utils;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
@ -47,6 +49,29 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
private File newpipe_db; private File newpipe_db;
private File newpipe_db_journal; private File newpipe_db_journal;
private String thumbnailLoadToggleKey;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key);
}
@Override
public boolean onPreferenceTreeClick(Preference preference) {
if (preference.getKey().equals(thumbnailLoadToggleKey)) {
final ImageLoader imageLoader = ImageLoader.getInstance();
imageLoader.stop();
imageLoader.clearDiskCache();
imageLoader.clearMemoryCache();
imageLoader.resume();
Toast.makeText(preference.getContext(), R.string.thumbnail_cache_wipe_complete_notice,
Toast.LENGTH_SHORT).show();
}
return super.onPreferenceTreeClick(preference);
}
@Override @Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {

View file

@ -1,12 +1,35 @@
package org.schabi.newpipe.settings; package org.schabi.newpipe.settings;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.preference.Preference;
import android.widget.Toast;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.util.InfoCache;
public class HistorySettingsFragment extends BasePreferenceFragment { public class HistorySettingsFragment extends BasePreferenceFragment {
private String cacheWipeKey;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
cacheWipeKey = getString(R.string.metadata_cache_wipe_key);
}
@Override @Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.history_settings); addPreferencesFromResource(R.xml.history_settings);
} }
@Override
public boolean onPreferenceTreeClick(Preference preference) {
if (preference.getKey().equals(cacheWipeKey)) {
InfoCache.getInstance().clearCache();
Toast.makeText(preference.getContext(), R.string.metadata_cache_wipe_complete_notice,
Toast.LENGTH_SHORT).show();
}
return super.onPreferenceTreeClick(preference);
}
} }

View file

@ -0,0 +1,58 @@
package org.schabi.newpipe.util;
import android.graphics.Bitmap;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
import org.schabi.newpipe.R;
public class ImageDisplayConstants {
private static final int BITMAP_FADE_IN_DURATION_MILLIS = 250;
/**
* Base display options
*/
private static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS =
new DisplayImageOptions.Builder()
.cacheInMemory(true)
.cacheOnDisk(true)
.resetViewBeforeLoading(true)
.bitmapConfig(Bitmap.Config.RGB_565)
.imageScaleType(ImageScaleType.EXACTLY)
.displayer(new FadeInBitmapDisplayer(BITMAP_FADE_IN_DURATION_MILLIS))
.build();
/*//////////////////////////////////////////////////////////////////////////
// DisplayImageOptions default configurations
//////////////////////////////////////////////////////////////////////////*/
public static final DisplayImageOptions DISPLAY_AVATAR_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageForEmptyUri(R.drawable.buddy)
.showImageOnFail(R.drawable.buddy)
.build();
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
.showImageOnFail(R.drawable.dummy_thumbnail)
.build();
public static final DisplayImageOptions DISPLAY_BANNER_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageForEmptyUri(R.drawable.channel_banner)
.showImageOnFail(R.drawable.channel_banner)
.build();
public static final DisplayImageOptions DISPLAY_PLAYLIST_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist)
.showImageOnFail(R.drawable.dummy_thumbnail_playlist)
.build();
}

View file

@ -43,7 +43,6 @@ public final class InfoCache {
* Trim the cache to this size * Trim the cache to this size
*/ */
private static final int TRIM_CACHE_TO = 30; private static final int TRIM_CACHE_TO = 30;
private static final int DEFAULT_TIMEOUT_HOURS = 4;
private static final LruCache<String, CacheData> lruCache = new LruCache<>(MAX_ITEMS_ON_CACHE); private static final LruCache<String, CacheData> lruCache = new LruCache<>(MAX_ITEMS_ON_CACHE);
@ -66,13 +65,7 @@ public final class InfoCache {
public void putInfo(int serviceId, @NonNull String url, @NonNull Info info) { public void putInfo(int serviceId, @NonNull String url, @NonNull Info info) {
if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]"); if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]");
final long expirationMillis; final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId());
if (info.getServiceId() == SoundCloud.getServiceId()) {
expirationMillis = TimeUnit.MILLISECONDS.convert(15, TimeUnit.MINUTES);
} else {
expirationMillis = TimeUnit.MILLISECONDS.convert(DEFAULT_TIMEOUT_HOURS, TimeUnit.HOURS);
}
synchronized (lruCache) { synchronized (lruCache) {
final CacheData data = new CacheData(info, expirationMillis); final CacheData data = new CacheData(info, expirationMillis);
lruCache.put(keyOf(serviceId, url), data); lruCache.put(keyOf(serviceId, url), data);

View file

@ -12,6 +12,10 @@ import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import java.util.concurrent.TimeUnit;
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
public class ServiceHelper { public class ServiceHelper {
private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube; private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube;
@ -98,4 +102,12 @@ public class ServiceHelper {
PreferenceManager.getDefaultSharedPreferences(context).edit(). PreferenceManager.getDefaultSharedPreferences(context).edit().
putString(context.getString(R.string.current_service_key), serviceName).apply(); putString(context.getString(R.string.current_service_key), serviceName).apply();
} }
public static long getCacheExpirationMillis(final int serviceId) {
if (serviceId == SoundCloud.getServiceId()) {
return TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
} else {
return TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
}
}
} }

View file

@ -0,0 +1,73 @@
package org.schabi.newpipe.util;
public interface SliderStrategy {
/**
* Converts from zeroed double with a minimum offset to the nearest rounded slider
* equivalent integer
* */
int progressOf(final double value);
/**
* Converts from slider integer value to an equivalent double value with a given
* minimum offset
* */
double valueOf(final int progress);
// TODO: also implement linear strategy when needed
final class Quadratic implements SliderStrategy {
private final double leftGap;
private final double rightGap;
private final double center;
private final int centerProgress;
/**
* Quadratic slider strategy that scales the value of a slider given how far the slider
* progress is from the center of the slider. The further away from the center,
* the faster the interpreted value changes, and vice versa.
*
* @param minimum the minimum value of the interpreted value of the slider.
* @param maximum the maximum value of the interpreted value of the slider.
* @param center center of the interpreted value between the minimum and maximum, which
* will be used as the center value on the slider progress. Doesn't need
* to be the average of the minimum and maximum values, but must be in
* between the two.
* @param maxProgress the maximum possible progress of the slider, this is the
* value that is shown for the UI and controls the granularity of
* the slider. Should be as large as possible to avoid floating
* point round-off error. Using odd number is recommended.
* */
public Quadratic(double minimum, double maximum, double center, int maxProgress) {
if (center < minimum || center > maximum) {
throw new IllegalArgumentException("Center must be in between minimum and maximum");
}
this.leftGap = minimum - center;
this.rightGap = maximum - center;
this.center = center;
this.centerProgress = maxProgress / 2;
}
@Override
public int progressOf(double value) {
final double difference = value - center;
final double root = difference >= 0 ?
Math.sqrt(difference / rightGap) :
-Math.sqrt(Math.abs(difference / leftGap));
final double offset = Math.round(root * centerProgress);
return (int) (centerProgress + offset);
}
@Override
public double valueOf(int progress) {
final int offset = progress - centerProgress;
final double square = Math.pow(((double) offset) / ((double) centerProgress), 2);
final double difference = square * (offset >= 0 ? rightGap : leftGap);
return difference + center;
}
}
}

View file

@ -301,9 +301,13 @@
android:id="@+id/live_sync" android:id="@+id/live_sync"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:gravity="center" android:gravity="center"
android:text="@string/live_sync" android:text="@string/duration_live"
android:textAllCaps="true"
android:textColor="?attr/colorAccent" android:textColor="?attr/colorAccent"
android:maxLength="4"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:visibility="gone"/> android:visibility="gone"/>
</LinearLayout> </LinearLayout>

View file

@ -52,7 +52,7 @@
android:id="@+id/playQueuePanel" android:id="@+id/playQueuePanel"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone" android:visibility="invisible"
android:background="?attr/queue_background_color" android:background="?attr/queue_background_color"
tools:visibility="visible"> tools:visibility="visible">
@ -254,7 +254,7 @@
android:focusable="true" android:focusable="true"
android:scaleType="fitXY" android:scaleType="fitXY"
android:src="@drawable/ic_expand_more_white_24dp" android:src="@drawable/ic_expand_more_white_24dp"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackgroundBorderless"
tools:ignore="ContentDescription,RtlHardcoded"/> tools:ignore="ContentDescription,RtlHardcoded"/>
</RelativeLayout> </RelativeLayout>
@ -266,7 +266,7 @@
android:gravity="top" android:gravity="top"
android:paddingLeft="5dp" android:paddingLeft="5dp"
android:paddingRight="5dp" android:paddingRight="5dp"
android:visibility="gone" android:visibility="invisible"
tools:ignore="RtlHardcoded" tools:ignore="RtlHardcoded"
tools:visibility="visible"> tools:visibility="visible">
@ -308,7 +308,7 @@
android:id="@+id/toggleOrientation" android:id="@+id/toggleOrientation"
android:layout_width="30dp" android:layout_width="30dp"
android:layout_height="30dp" android:layout_height="30dp"
android:layout_marginLeft="2dp" android:layout_marginLeft="4dp"
android:layout_marginRight="2dp" android:layout_marginRight="2dp"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_centerVertical="true" android:layout_centerVertical="true"
@ -325,8 +325,8 @@
android:id="@+id/switchPopup" android:id="@+id/switchPopup"
android:layout_width="30dp" android:layout_width="30dp"
android:layout_height="30dp" android:layout_height="30dp"
android:layout_marginLeft="2dp" android:layout_marginLeft="4dp"
android:layout_marginRight="2dp" android:layout_marginRight="4dp"
android:layout_toLeftOf="@id/toggleOrientation" android:layout_toLeftOf="@id/toggleOrientation"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:clickable="true" android:clickable="true"
@ -341,8 +341,8 @@
android:id="@+id/switchBackground" android:id="@+id/switchBackground"
android:layout_width="30dp" android:layout_width="30dp"
android:layout_height="30dp" android:layout_height="30dp"
android:layout_marginLeft="2dp" android:layout_marginLeft="4dp"
android:layout_marginRight="2dp" android:layout_marginRight="4dp"
android:layout_toLeftOf="@id/switchPopup" android:layout_toLeftOf="@id/switchPopup"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:clickable="true" android:clickable="true"
@ -403,9 +403,13 @@
android:id="@+id/playbackLiveSync" android:id="@+id/playbackLiveSync"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:gravity="center" android:gravity="center"
android:text="@string/live_sync" android:text="@string/duration_live"
android:textAllCaps="true"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:maxLength="4"
android:visibility="gone" android:visibility="gone"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" /> tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" />

View file

@ -151,9 +151,13 @@
android:id="@+id/live_sync" android:id="@+id/live_sync"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:gravity="center" android:gravity="center"
android:text="@string/live_sync" android:text="@string/duration_live"
android:textAllCaps="true"
android:textColor="?attr/colorAccent" android:textColor="?attr/colorAccent"
android:maxLength="4"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:visibility="gone"/> android:visibility="gone"/>
</LinearLayout> </LinearLayout>

View file

@ -0,0 +1,313 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="false"
android:paddingLeft="@dimen/video_item_search_padding"
android:paddingRight="@dimen/video_item_search_padding"
android:paddingTop="@dimen/video_item_search_padding">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
android:scrollbarAlwaysDrawVerticalTrack="true">
<!-- START HERE -->
<TextView
android:id="@+id/tempoControlText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_centerHorizontal="true"
android:text="@string/playback_tempo"
android:textStyle="bold"
android:textColor="?attr/colorAccent"
android:layout_alignParentTop="true"/>
<RelativeLayout
android:id="@+id/tempoControl"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal"
android:layout_marginTop="4dp"
android:layout_below="@id/tempoControlText">
<TextView
android:id="@+id/tempoStepDown"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:layout_centerVertical="true"
android:clickable="true"
android:focusable="true"
android:text="--%"
android:textStyle="bold"
android:textColor="?attr/colorAccent"
android:background="?attr/selectableItemBackground"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
tools:ignore="HardcodedText"
tools:text="-5%"/>
<RelativeLayout
android:id="@+id/tempoDisplay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_toRightOf="@id/tempoStepDown"
android:layout_toEndOf="@id/tempoStepDown"
android:layout_toLeftOf="@id/tempoStepUp"
android:layout_toStartOf="@id/tempoStepUp">
<TextView
android:id="@+id/tempoMinimumText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="-.--x"
android:textColor="?attr/colorAccent"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
tools:ignore="HardcodedText"
tools:text="1.00x"/>
<TextView
android:id="@+id/tempoCurrentText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="---%"
android:textColor="?attr/colorAccent"
android:layout_centerHorizontal="true"
android:textStyle="bold"
tools:ignore="HardcodedText"
tools:text="100%"/>
<TextView
android:id="@+id/tempoMaximumText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="---%"
android:textColor="?attr/colorAccent"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="4dp"
android:layout_marginEnd="4dp"
tools:ignore="HardcodedText"
tools:text="300%"/>
<android.support.v7.widget.AppCompatSeekBar
android:id="@+id/tempoSeekbar"
style="@style/Widget.AppCompat.SeekBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/tempoCurrentText"
android:paddingBottom="4dp"
tools:progress="50"/>
</RelativeLayout>
<TextView
android:id="@+id/tempoStepUp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:clickable="true"
android:focusable="true"
android:text="+-%"
android:textStyle="bold"
android:textColor="?attr/colorAccent"
android:background="?attr/selectableItemBackground"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="4dp"
android:layout_marginEnd="4dp"
tools:ignore="HardcodedText"
tools:text="+5%"/>
</RelativeLayout>
<View
android:id="@+id/separatorPitch"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@id/tempoControl"
android:layout_margin="@dimen/video_item_search_padding"
android:background="?attr/separator_color"/>
<TextView
android:id="@+id/pitchControlText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_centerHorizontal="true"
android:text="@string/playback_pitch"
android:textStyle="bold"
android:textColor="?attr/colorAccent"
android:layout_below="@id/separatorPitch"/>
<RelativeLayout
android:id="@+id/pitchControl"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal"
android:layout_marginTop="4dp"
android:layout_below="@id/pitchControlText">
<TextView
android:id="@+id/pitchStepDown"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:layout_centerVertical="true"
android:clickable="true"
android:focusable="true"
android:text="--%"
android:textStyle="bold"
android:textColor="?attr/colorAccent"
android:background="?attr/selectableItemBackground"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
tools:ignore="HardcodedText"
tools:text="-5%"/>
<RelativeLayout
android:id="@+id/pitchDisplay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_toRightOf="@+id/pitchStepDown"
android:layout_toEndOf="@+id/pitchStepDown"
android:layout_toLeftOf="@+id/pitchStepUp"
android:layout_toStartOf="@+id/pitchStepUp">
<TextView
android:id="@+id/pitchMinimumText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="---%"
android:textColor="?attr/colorAccent"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
tools:ignore="HardcodedText"
tools:text="25%"/>
<TextView
android:id="@+id/pitchCurrentText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="---%"
android:textColor="?attr/colorAccent"
android:layout_centerHorizontal="true"
android:textStyle="bold"
tools:ignore="HardcodedText"
tools:text="100%"/>
<TextView
android:id="@+id/pitchMaximumText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="---%"
android:textColor="?attr/colorAccent"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="4dp"
android:layout_marginEnd="4dp"
tools:ignore="HardcodedText"
tools:text="300%"/>
<android.support.v7.widget.AppCompatSeekBar
android:id="@+id/pitchSeekbar"
style="@style/Widget.AppCompat.SeekBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/pitchCurrentText"
android:paddingBottom="4dp"
tools:progress="50"/>
</RelativeLayout>
<TextView
android:id="@+id/pitchStepUp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:clickable="true"
android:focusable="true"
android:text="+-%"
android:textStyle="bold"
android:textColor="?attr/colorAccent"
android:background="?attr/selectableItemBackground"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="4dp"
android:layout_marginEnd="4dp"
tools:ignore="HardcodedText"
tools:text="+5%"/>
</RelativeLayout>
<View
android:id="@+id/separatorCheckbox"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/pitchControl"
android:layout_margin="@dimen/video_item_search_padding"
android:background="?attr/separator_color"/>
<CheckBox
android:id="@+id/unhookCheckbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="false"
android:clickable="true"
android:focusable="true"
android:text="@string/unhook_checkbox"
android:maxLines="1"
android:layout_centerHorizontal="true"
android:layout_below="@id/separatorCheckbox"/>
<LinearLayout
android:id="@+id/presetSelector"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal"
android:layout_below="@id/unhookCheckbox">
<TextView
android:id="@+id/presetNightcore"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="@string/playback_nightcore"
android:background="?attr/selectableItemBackground"
android:textColor="?attr/colorAccent"/>
<TextView
android:id="@+id/presetReset"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="@string/playback_default"
android:background="?attr/selectableItemBackground"
android:textColor="?attr/colorAccent"/>
</LinearLayout>
<!-- END HERE -->
</RelativeLayout>
</ScrollView>

View file

@ -19,7 +19,7 @@
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:layout_marginRight="@dimen/video_item_search_image_right_margin" android:layout_marginRight="@dimen/video_item_search_image_right_margin"
android:contentDescription="@string/list_thumbnail_view_description" android:contentDescription="@string/list_thumbnail_view_description"
android:scaleType="fitEnd" android:scaleType="centerCrop"
android:src="@drawable/dummy_thumbnail_playlist" android:src="@drawable/dummy_thumbnail_playlist"
tools:ignore="RtlHardcoded"/> tools:ignore="RtlHardcoded"/>

View file

@ -195,9 +195,13 @@
android:id="@+id/playbackLiveSync" android:id="@+id/playbackLiveSync"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:text="@string/live_sync" android:text="@string/duration_live"
android:textAllCaps="true"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:maxLength="4"
android:visibility="gone" android:visibility="gone"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" /> tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" />

View file

@ -160,6 +160,10 @@
<string name="import_data">import_data</string> <string name="import_data">import_data</string>
<string name="export_data">export_data</string> <string name="export_data">export_data</string>
<string name="download_thumbnail_key" translatable="false">download_thumbnail_key</string>
<string name="metadata_cache_wipe_key" translatable="false">cache_wipe_key</string>
<!-- FileName Downloads --> <!-- FileName Downloads -->
<string name="settings_file_charset_key" translatable="false">file_rename</string> <string name="settings_file_charset_key" translatable="false">file_rename</string>
<string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string> <string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string>

View file

@ -74,6 +74,12 @@
<string name="popup_remember_size_pos_summary">Remember last size and position of popup</string> <string name="popup_remember_size_pos_summary">Remember last size and position of popup</string>
<string name="use_inexact_seek_title">Use fast inexact seek</string> <string name="use_inexact_seek_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="use_inexact_seek_summary">Inexact seek allows the player to seek to positions faster with reduced precision</string>
<string name="download_thumbnail_title">Load thumbnails</string>
<string name="download_thumbnail_summary">Disable to stop all thumbnails from loading and save on data and memory usage. Changing this will clear both in-memory and on-disk image cache.</string>
<string name="thumbnail_cache_wipe_complete_notice">Image cache wiped</string>
<string name="metadata_cache_wipe_title">Wipe cached metadata</string>
<string name="metadata_cache_wipe_summary">Remove all cached webpage data</string>
<string name="metadata_cache_wipe_complete_notice">Metadata cache wiped</string>
<string name="auto_queue_title">Auto-queue next stream</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="auto_queue_summary">Automatically append a related stream when playback starts on the last stream in a non-repeating play queue.</string>
<string name="player_gesture_controls_title">Player gesture controls</string> <string name="player_gesture_controls_title">Player gesture controls</string>
@ -89,7 +95,7 @@
<string name="download_dialog_title">Download</string> <string name="download_dialog_title">Download</string>
<string name="next_video_title">Next video</string> <string name="next_video_title">Next video</string>
<string name="show_next_and_similar_title">Show next and similar videos</string> <string name="show_next_and_similar_title">Show next and similar videos</string>
<string name="show_hold_to_append_title">Show Hold to Append Tip</string> <string name="show_hold_to_append_title">Show hold to append tip</string>
<string name="show_hold_to_append_summary">Show tip when background or popup button is pressed on video details page</string> <string name="show_hold_to_append_summary">Show tip when background or popup button is pressed on video details page</string>
<string name="url_not_supported_toast">URL not supported</string> <string name="url_not_supported_toast">URL not supported</string>
<string name="default_content_country_title">Default content country</string> <string name="default_content_country_title">Default content country</string>
@ -98,7 +104,7 @@
<string name="settings_category_player_title">Player</string> <string name="settings_category_player_title">Player</string>
<string name="settings_category_player_behavior_title">Behavior</string> <string name="settings_category_player_behavior_title">Behavior</string>
<string name="settings_category_video_audio_title">Video &amp; Audio</string> <string name="settings_category_video_audio_title">Video &amp; Audio</string>
<string name="settings_category_history_title">History</string> <string name="settings_category_history_title">History &amp; Cache</string>
<string name="settings_category_popup_title">Popup</string> <string name="settings_category_popup_title">Popup</string>
<string name="settings_category_appearance_title">Appearance</string> <string name="settings_category_appearance_title">Appearance</string>
<string name="settings_category_other_title">Other</string> <string name="settings_category_other_title">Other</string>
@ -418,18 +424,16 @@
<string name="resize_zoom">ZOOM</string> <string name="resize_zoom">ZOOM</string>
<string name="caption_auto_generated">Auto-generated</string> <string name="caption_auto_generated">Auto-generated</string>
<string name="caption_font_size_settings_title">Caption Font Size</string> <string name="caption_font_size_settings_title">Caption font size</string>
<string name="smaller_caption_font_size">Smaller Font</string> <string name="smaller_caption_font_size">Smaller font</string>
<string name="normal_caption_font_size">Normal Font</string> <string name="normal_caption_font_size">Normal font</string>
<string name="larger_caption_font_size">Larger Font</string> <string name="larger_caption_font_size">Larger font</string>
<string name="live_sync">SYNC</string>
<!-- Debug Settings --> <!-- Debug Settings -->
<string name="enable_leak_canary_title">Enable LeakCanary</string> <string name="enable_leak_canary_title">Enable LeakCanary</string>
<string name="enable_leak_canary_summary">Memory leak monitoring may cause app to become unresponsive when heap dumping</string> <string name="enable_leak_canary_summary">Memory leak monitoring may cause app to become unresponsive when heap dumping</string>
<string name="enable_disposed_exceptions_title">Report Out-of-Lifecycle Errors</string> <string name="enable_disposed_exceptions_title">Report Out-of-lifecycle errors</string>
<string name="enable_disposed_exceptions_summary">Force reporting of undeliverable Rx exceptions occurring outside of fragment or activity lifecycle after dispose</string> <string name="enable_disposed_exceptions_summary">Force reporting of undeliverable Rx exceptions occurring outside of fragment or activity lifecycle after dispose</string>
<!-- Subscriptions import/export --> <!-- Subscriptions import/export -->
@ -452,4 +456,12 @@
<string name="import_soundcloud_instructions_hint">yourid, soundcloud.com/yourid</string> <string name="import_soundcloud_instructions_hint">yourid, soundcloud.com/yourid</string>
<string name="import_network_expensive_warning">Keep in mind that this operation can be network expensive.\n\nDo you want to continue?</string> <string name="import_network_expensive_warning">Keep in mind that this operation can be network expensive.\n\nDo you want to continue?</string>
<!-- Playback Parameters -->
<string name="playback_speed_control">Playback Speed Control</string>
<string name="playback_tempo">Tempo</string>
<string name="playback_pitch">Pitch</string>
<string name="unhook_checkbox">Unhook (may cause distortion)</string>
<string name="playback_nightcore">Nightcore</string>
<string name="playback_default">Default</string>
</resources> </resources>

View file

@ -37,6 +37,12 @@
android:summary="@string/auto_queue_summary" android:summary="@string/auto_queue_summary"
android:title="@string/auto_queue_title"/> android:title="@string/auto_queue_title"/>
<SwitchPreference
android:defaultValue="true"
android:key="@string/download_thumbnail_key"
android:title="@string/download_thumbnail_title"
android:summary="@string/download_thumbnail_summary"/>
<ListPreference <ListPreference
android:defaultValue="@string/kiosk_page_key" android:defaultValue="@string/kiosk_page_key"
android:entries="@array/main_page_content_names" android:entries="@array/main_page_content_names"

View file

@ -16,4 +16,9 @@
android:summary="@string/enable_search_history_summary" android:summary="@string/enable_search_history_summary"
android:title="@string/enable_search_history_title"/> android:title="@string/enable_search_history_title"/>
<Preference
android:key="@string/metadata_cache_wipe_key"
android:summary="@string/metadata_cache_wipe_summary"
android:title="@string/metadata_cache_wipe_title"/>
</PreferenceScreen> </PreferenceScreen>

View file

@ -0,0 +1,86 @@
package org.schabi.newpipe.util;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class QuadraticSliderStrategyTest {
private final static int STEP = 100;
private final static float DELTA = 1f / (float) STEP;
private final SliderStrategy.Quadratic standard =
new SliderStrategy.Quadratic(0f, 100f, 50f, STEP);
@Test
public void testLeftBound() throws Exception {
assertEquals(standard.progressOf(0), 0);
assertEquals(standard.valueOf(0), 0f, DELTA);
}
@Test
public void testCenter() throws Exception {
assertEquals(standard.progressOf(50), 50);
assertEquals(standard.valueOf(50), 50f, DELTA);
}
@Test
public void testRightBound() throws Exception {
assertEquals(standard.progressOf(100), 100);
assertEquals(standard.valueOf(100), 100f, DELTA);
}
@Test
public void testLeftRegion() throws Exception {
final int leftProgress = standard.progressOf(25);
final double leftValue = standard.valueOf(25);
assertTrue(leftProgress > 0 && leftProgress < 50);
assertTrue(leftValue > 0f && leftValue < 50);
}
@Test
public void testRightRegion() throws Exception {
final int leftProgress = standard.progressOf(75);
final double leftValue = standard.valueOf(75);
assertTrue(leftProgress > 50 && leftProgress < 100);
assertTrue(leftValue > 50f && leftValue < 100);
}
@Test
public void testConversion() throws Exception {
assertEquals(standard.progressOf(standard.valueOf(0)), 0);
assertEquals(standard.progressOf(standard.valueOf(25)), 25);
assertEquals(standard.progressOf(standard.valueOf(50)), 50);
assertEquals(standard.progressOf(standard.valueOf(75)), 75);
assertEquals(standard.progressOf(standard.valueOf(100)), 100);
}
@Test
public void testReverseConversion() throws Exception {
// Need a larger delta since step size / granularity is too small and causes
// floating point round-off errors during conversion
final float largeDelta = 1f;
assertEquals(standard.valueOf(standard.progressOf(0)), 0f, largeDelta);
assertEquals(standard.valueOf(standard.progressOf(25)), 25f, largeDelta);
assertEquals(standard.valueOf(standard.progressOf(50)), 50f, largeDelta);
assertEquals(standard.valueOf(standard.progressOf(75)), 75f, largeDelta);
assertEquals(standard.valueOf(standard.progressOf(100)), 100f, largeDelta);
}
@Test
public void testQuadraticPropertyLeftRegion() throws Exception {
final double differenceCloserToCenter =
Math.abs(standard.valueOf(40) - standard.valueOf(45));
final double differenceFurtherFromCenter =
Math.abs(standard.valueOf(10) - standard.valueOf(15));
assertTrue(differenceCloserToCenter < differenceFurtherFromCenter);
}
@Test
public void testQuadraticPropertyRightRegion() throws Exception {
final double differenceCloserToCenter =
Math.abs(standard.valueOf(75) - standard.valueOf(70));
final double differenceFurtherFromCenter =
Math.abs(standard.valueOf(95) - standard.valueOf(90));
assertTrue(differenceCloserToCenter < differenceFurtherFromCenter);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 784 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 801 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="30"><link xmlns="" type="text/css" id="dark-mode" rel="stylesheet" href=""/><style xmlns="" type="text/css" id="dark-mode-custom-style"/><rect id="back" fill="#f6c915" x="1" y=".5" width="82" height="29" rx="4"/><svg viewBox="0 0 80 80" height="16" width="16" x="7" y="7"><g transform="translate(-78.37-208.06)" fill="#1a171b"><path d="m104.28 271.1c-3.571 0-6.373-.466-8.41-1.396-2.037-.93-3.495-2.199-4.375-3.809-.88-1.609-1.308-3.457-1.282-5.544.025-2.086.313-4.311.868-6.675l9.579-40.05 11.69-1.81-10.484 43.44c-.202.905-.314 1.735-.339 2.489-.026.754.113 1.421.415 1.999.302.579.817 1.044 1.546 1.395.729.353 1.747.579 3.055.679l-2.263 9.278"/><path d="m146.52 246.14c0 3.671-.604 7.03-1.811 10.07-1.207 3.043-2.879 5.669-5.01 7.881-2.138 2.213-4.702 3.935-7.693 5.167-2.992 1.231-6.248 1.848-9.767 1.848-1.71 0-3.42-.151-5.129-.453l-3.394 13.651h-11.162l12.52-52.19c2.01-.603 4.311-1.143 6.901-1.622 2.589-.477 5.393-.716 8.41-.716 2.815 0 5.242.428 7.278 1.282 2.037.855 3.708 2.024 5.02 3.507 1.307 1.484 2.274 3.219 2.904 5.205.627 1.987.942 4.11.942 6.373m-27.378 15.461c.854.202 1.91.302 3.167.302 1.961 0 3.746-.364 5.355-1.094 1.609-.728 2.979-1.747 4.111-3.055 1.131-1.307 2.01-2.877 2.64-4.714.628-1.835.943-3.858.943-6.071 0-2.161-.479-3.998-1.433-5.506-.956-1.508-2.615-2.263-4.978-2.263-1.61 0-3.118.151-4.525.453l-5.28 21.948"/></g></svg><text fill="#1a171b" text-anchor="middle" font-family="Helvetica Neue,Helvetica,Arial,sans-serif" font-weight="700" font-size="14" x="50" y="20">Donate</text></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 19 KiB