local player improvements

This commit is contained in:
polymorphicshade 2021-02-23 13:50:44 -07:00
parent c0e9b673db
commit 14e1abc28e
4 changed files with 423 additions and 245 deletions

View file

@ -318,7 +318,8 @@
android:name=".LocalPlayerActivity" android:name=".LocalPlayerActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleTask" android:launchMode="singleTask"
android:foregroundServiceType="mediaPlayback" /> android:foregroundServiceType="mediaPlayback"
android:configChanges="orientation|screenSize|layoutDirection" />
<service <service
android:name=".RouterActivity$FetcherService" android:name=".RouterActivity$FetcherService"

View file

@ -1,91 +1,105 @@
package org.schabi.newpipe; package org.schabi.newpipe;
import android.content.SharedPreferences; import android.content.Intent;
import android.net.Uri; import android.content.res.Configuration;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.view.View;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser; import com.grack.nanojson.JsonParser;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.LocalPlayer;
import org.schabi.newpipe.player.LocalPlayerListener;
import org.schabi.newpipe.util.VideoSegment; import org.schabi.newpipe.util.VideoSegment;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; public class LocalPlayerActivity extends AppCompatActivity implements Player.EventListener,
import io.reactivex.rxjava3.core.Observable; LocalPlayerListener {
import io.reactivex.rxjava3.disposables.Disposable; private LocalPlayer localPlayer;
import io.reactivex.rxjava3.disposables.SerialDisposable; public static final String TAG = "LocalPlayerActivity";
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.schabi.newpipe.player.Player.STATE_BLOCKED;
import static org.schabi.newpipe.player.Player.STATE_BUFFERING;
import static org.schabi.newpipe.player.Player.STATE_COMPLETED;
import static org.schabi.newpipe.player.Player.STATE_PAUSED;
import static org.schabi.newpipe.player.Player.STATE_PAUSED_SEEK;
import static org.schabi.newpipe.player.Player.STATE_PLAYING;
public class LocalPlayerActivity extends AppCompatActivity implements Player.EventListener {
private SimpleExoPlayer simpleExoPlayer;
private SerialDisposable progressUpdateReactor;
private int lastCurrentProgress = -1;
protected static final int PROGRESS_LOOP_INTERVAL_MILLIS = 500;
public static final String TAG = "LocalPlayer";
private VideoSegment[] segments;
private SharedPreferences mPrefs;
@Override @Override
protected void onCreate(final Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_local_player); setContentView(R.layout.activity_local_player);
this.mPrefs = PreferenceManager.getDefaultSharedPreferences(App.getApp()); hideSystemUi(isLandscape());
initPlayer(); final Intent intent = getIntent();
initSegments();
}
private void initPlayer() { final String uri = intent.getDataString();
final String uri = getIntent().getDataString(); final VideoSegment[] segments = getSegmentsFromIntent(intent);
if (uri == null) { localPlayer = new LocalPlayer(this);
return; localPlayer.initPlayer(uri, segments);
}
simpleExoPlayer = new SimpleExoPlayer
.Builder(this)
.build();
simpleExoPlayer.addListener(this);
simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(this));
simpleExoPlayer.setHandleAudioBecomingNoisy(true);
final MediaSource videoSource = new ProgressiveMediaSource
.Factory(new DefaultDataSourceFactory(this, DownloaderImpl.USER_AGENT))
.createMediaSource(Uri.parse(uri));
simpleExoPlayer.prepare(videoSource);
final PlayerView playerView = findViewById(R.id.player_view); final PlayerView playerView = findViewById(R.id.player_view);
playerView.setPlayer(simpleExoPlayer); playerView.setPlayer(localPlayer.getPlayer());
this.progressUpdateReactor = new SerialDisposable();
} }
private void initSegments() { @Override
final String segmentsJson = getIntent().getStringExtra("segments"); protected void onDestroy() {
super.onDestroy();
localPlayer.destroy();
}
@Override
public void onConfigurationChanged(@NonNull final Configuration newConfig) {
super.onConfigurationChanged(newConfig);
hideSystemUi(isLandscape());
}
@Override
public void onBlocked(final SimpleExoPlayer player) {
}
@Override
public void onPlaying(final SimpleExoPlayer player) {
setKeepScreenOn(true);
}
@Override
public void onBuffering(final SimpleExoPlayer player) {
}
@Override
public void onPaused(final SimpleExoPlayer player) {
setKeepScreenOn(false);
}
@Override
public void onPausedSeek(final SimpleExoPlayer player) {
}
@Override
public void onCompleted(final SimpleExoPlayer player) {
setKeepScreenOn(false);
}
private static VideoSegment[] getSegmentsFromIntent(final Intent intent) {
final List<VideoSegment> result = new ArrayList<>();
final String segmentsJson = intent.getStringExtra("segments");
if (segmentsJson != null && segmentsJson.length() > 0) { if (segmentsJson != null && segmentsJson.length() > 0) {
try { try {
final ArrayList<VideoSegment> segmentsArrayList = new ArrayList<>();
final JsonObject obj = JsonParser.object().from(segmentsJson); final JsonObject obj = JsonParser.object().from(segmentsJson);
for (final Object item : obj.getArray("segments")) { for (final Object item : obj.getArray("segments")) {
@ -96,216 +110,63 @@ public class LocalPlayerActivity extends AppCompatActivity implements Player.Eve
final String category = itemObject.getString("category"); final String category = itemObject.getString("category");
final VideoSegment segment = new VideoSegment(startTime, endTime, category); final VideoSegment segment = new VideoSegment(startTime, endTime, category);
segmentsArrayList.add(segment); result.add(segment);
} }
segments = segmentsArrayList.toArray(new VideoSegment[0]);
} catch (final Exception e) { } catch (final Exception e) {
Log.e(TAG, "Error initializing segments", e); Log.e(TAG, "Error initializing segments", e);
} }
} }
return result.toArray(new VideoSegment[0]);
} }
@Override private void setKeepScreenOn(final boolean keepScreenOn) {
protected void onDestroy() { if (keepScreenOn) {
super.onDestroy(); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
simpleExoPlayer.removeListener(this); } else {
simpleExoPlayer.stop(); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
simpleExoPlayer.release();
progressUpdateReactor.set(null);
}
@Override
public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) {
switch (playbackState) {
case Player.STATE_IDLE:
break;
case Player.STATE_BUFFERING:
break;
case Player.STATE_READY:
changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED);
break;
case Player.STATE_ENDED:
changeState(STATE_COMPLETED);
break;
} }
} }
public void changeState(final int state) { private void hideSystemUi(final boolean isLandscape) {
switch (state) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
case STATE_BLOCKED: getWindow().getAttributes().layoutInDisplayCutoutMode =
onBlocked(); WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
break;
case STATE_PLAYING:
onPlaying();
break;
case STATE_BUFFERING:
onBuffering();
break;
case STATE_PAUSED:
onPaused();
break;
case STATE_PAUSED_SEEK:
onPausedSeek();
break;
case STATE_COMPLETED:
onCompleted();
break;
}
}
private void onBlocked() {
if (!isProgressLoopRunning()) {
startProgressLoop();
}
}
private void onPlaying() {
if (!isProgressLoopRunning()) {
startProgressLoop();
}
}
private void onBuffering() {
}
private void onPaused() {
if (isProgressLoopRunning()) {
stopProgressLoop();
}
}
private void onPausedSeek() {
}
private void onCompleted() {
if (isProgressLoopRunning()) {
stopProgressLoop();
}
}
public boolean isProgressLoopRunning() {
return progressUpdateReactor.get() != null;
}
protected void startProgressLoop() {
progressUpdateReactor.set(getProgressReactor());
}
protected void stopProgressLoop() {
progressUpdateReactor.set(null);
}
private Disposable getProgressReactor() {
return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, MILLISECONDS,
AndroidSchedulers.mainThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> triggerProgressUpdate(),
error -> Log.e(TAG, "Progress update failure: ", error));
}
private void triggerProgressUpdate() {
if (simpleExoPlayer == null) {
return;
}
final int currentProgress = Math.max((int) simpleExoPlayer.getCurrentPosition(), 0);
final boolean isRewind = currentProgress < lastCurrentProgress;
lastCurrentProgress = currentProgress;
if (!mPrefs.getBoolean(
this.getString(R.string.sponsor_block_enable_key), false)) {
return;
} }
final VideoSegment segment = getSkippableSegment(currentProgress); int visibility;
if (segment == null) {
return; if (isLandscape) {
visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
} else {
visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
} }
int skipTarget = isRewind if (!isInMultiWindow()) {
? (int) Math.ceil((segment.startTime)) - 1 visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN;
: (int) Math.ceil((segment.endTime));
if (skipTarget < 0) {
skipTarget = 0;
} }
// temporarily force EXACT seek parameters to prevent infinite skip looping getWindow().getDecorView().setSystemUiVisibility(visibility);
final SeekParameters seekParams = simpleExoPlayer.getSeekParameters();
simpleExoPlayer.setSeekParameters(SeekParameters.EXACT);
seekTo(skipTarget); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && (isInMultiWindow())) {
getWindow().setStatusBarColor(Color.TRANSPARENT);
simpleExoPlayer.setSeekParameters(seekParams); getWindow().setNavigationBarColor(Color.TRANSPARENT);
if (mPrefs.getBoolean(
this.getString(R.string.sponsor_block_notifications_key), false)) {
String toastText = "";
switch (segment.category) {
case "sponsor":
toastText = this
.getString(R.string.sponsor_block_skip_sponsor_toast);
break;
case "intro":
toastText = this
.getString(R.string.sponsor_block_skip_intro_toast);
break;
case "outro":
toastText = this
.getString(R.string.sponsor_block_skip_outro_toast);
break;
case "interaction":
toastText = this
.getString(R.string.sponsor_block_skip_interaction_toast);
break;
case "selfpromo":
toastText = this
.getString(R.string.sponsor_block_skip_self_promo_toast);
break;
case "music_offtopic":
toastText = this
.getString(R.string.sponsor_block_skip_non_music_toast);
break;
}
Toast.makeText(this, toastText, Toast.LENGTH_SHORT).show();
} }
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
} }
public void seekTo(final long positionMillis) { private boolean isInMultiWindow() {
if (simpleExoPlayer != null) { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode();
// prevent invalid positions when fast-forwarding/-rewinding
long normalizedPositionMillis = positionMillis;
if (normalizedPositionMillis < 0) {
normalizedPositionMillis = 0;
} else if (normalizedPositionMillis > simpleExoPlayer.getDuration()) {
normalizedPositionMillis = simpleExoPlayer.getDuration();
}
simpleExoPlayer.seekTo(normalizedPositionMillis);
}
} }
public VideoSegment getSkippableSegment(final int progress) { boolean isLandscape() {
if (segments == null) { final DisplayMetrics metrics = getResources().getDisplayMetrics();
return null; return metrics.heightPixels < metrics.widthPixels;
}
for (final VideoSegment segment : segments) {
if (progress < segment.startTime) {
continue;
}
if (progress > segment.endTime) {
continue;
}
return segment;
}
return null;
} }
} }

View file

@ -0,0 +1,304 @@
package org.schabi.newpipe.player;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.util.Log;
import android.widget.Toast;
import androidx.preference.PreferenceManager;
import com.google.android.exoplayer2.Player.EventListener;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.schabi.newpipe.App;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.util.VideoSegment;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.disposables.SerialDisposable;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.schabi.newpipe.player.Player.STATE_BLOCKED;
import static org.schabi.newpipe.player.Player.STATE_BUFFERING;
import static org.schabi.newpipe.player.Player.STATE_COMPLETED;
import static org.schabi.newpipe.player.Player.STATE_PAUSED;
import static org.schabi.newpipe.player.Player.STATE_PAUSED_SEEK;
import static org.schabi.newpipe.player.Player.STATE_PLAYING;
public class LocalPlayer implements EventListener {
private static final String TAG = "LocalPlayer";
private static final int PROGRESS_LOOP_INTERVAL_MILLIS = 500;
private final Context context;
private final SharedPreferences mPrefs;
private SimpleExoPlayer simpleExoPlayer;
private SerialDisposable progressUpdateReactor;
private VideoSegment[] videoSegments;
private LocalPlayerListener listener;
private int lastCurrentProgress = -1;
public LocalPlayer(final Context context) {
this.context = context;
this.mPrefs = PreferenceManager.getDefaultSharedPreferences(App.getApp());
}
public void initPlayer(final String uri, final VideoSegment[] segments) {
this.videoSegments = segments;
this.progressUpdateReactor = new SerialDisposable();
simpleExoPlayer = new SimpleExoPlayer
.Builder(context)
.build();
simpleExoPlayer.addListener(this);
simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context));
simpleExoPlayer.setHandleAudioBecomingNoisy(true);
if (uri == null || uri.length() == 0) {
return;
}
final MediaSource videoSource = new ProgressiveMediaSource
.Factory(new DefaultDataSourceFactory(context, DownloaderImpl.USER_AGENT))
.createMediaSource(Uri.parse(uri));
simpleExoPlayer.prepare(videoSource);
}
public SimpleExoPlayer getPlayer() {
return this.simpleExoPlayer;
}
public void setListener(final LocalPlayerListener listener) {
this.listener = listener;
}
public void destroy() {
simpleExoPlayer.removeListener(this);
simpleExoPlayer.stop();
simpleExoPlayer.release();
progressUpdateReactor.set(null);
}
@Override
public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) {
switch (playbackState) {
case com.google.android.exoplayer2.Player.STATE_IDLE:
break;
case com.google.android.exoplayer2.Player.STATE_BUFFERING:
break;
case com.google.android.exoplayer2.Player.STATE_READY:
changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED);
break;
case com.google.android.exoplayer2.Player.STATE_ENDED:
changeState(STATE_COMPLETED);
break;
}
}
private boolean isProgressLoopRunning() {
return progressUpdateReactor.get() != null;
}
private void startProgressLoop() {
progressUpdateReactor.set(getProgressReactor());
}
private void stopProgressLoop() {
progressUpdateReactor.set(null);
}
private Disposable getProgressReactor() {
return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, MILLISECONDS,
AndroidSchedulers.mainThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> triggerProgressUpdate(),
error -> Log.e(TAG, "Progress update failure: ", error));
}
private void changeState(final int state) {
switch (state) {
case STATE_BLOCKED:
onBlocked();
break;
case STATE_PLAYING:
onPlaying();
break;
case STATE_BUFFERING:
onBuffering();
break;
case STATE_PAUSED:
onPaused();
break;
case STATE_PAUSED_SEEK:
onPausedSeek();
break;
case STATE_COMPLETED:
onCompleted();
break;
}
}
private void onBlocked() {
if (!isProgressLoopRunning()) {
startProgressLoop();
}
if (listener != null) {
listener.onBlocked(simpleExoPlayer);
}
}
private void onPlaying() {
if (!isProgressLoopRunning()) {
startProgressLoop();
}
if (listener != null) {
listener.onPlaying(simpleExoPlayer);
}
}
private void onBuffering() {
if (listener != null) {
listener.onBuffering(simpleExoPlayer);
}
}
private void onPaused() {
if (isProgressLoopRunning()) {
stopProgressLoop();
}
if (listener != null) {
listener.onPaused(simpleExoPlayer);
}
}
private void onPausedSeek() {
if (listener != null) {
listener.onPausedSeek(simpleExoPlayer);
}
}
private void onCompleted() {
if (isProgressLoopRunning()) {
stopProgressLoop();
}
if (listener != null) {
listener.onCompleted(simpleExoPlayer);
}
}
private void triggerProgressUpdate() {
if (simpleExoPlayer == null) {
return;
}
final int currentProgress = Math.max((int) simpleExoPlayer.getCurrentPosition(), 0);
final boolean isRewind = currentProgress < lastCurrentProgress;
lastCurrentProgress = currentProgress;
if (!mPrefs.getBoolean(
context.getString(R.string.sponsor_block_enable_key), false)) {
return;
}
final VideoSegment segment = getSkippableSegment(currentProgress);
if (segment == null) {
return;
}
int skipTarget = isRewind
? (int) Math.ceil((segment.startTime)) - 1
: (int) Math.ceil((segment.endTime));
if (skipTarget < 0) {
skipTarget = 0;
}
// temporarily force EXACT seek parameters to prevent infinite skip looping
final SeekParameters seekParams = simpleExoPlayer.getSeekParameters();
simpleExoPlayer.setSeekParameters(SeekParameters.EXACT);
seekTo(skipTarget);
simpleExoPlayer.setSeekParameters(seekParams);
if (mPrefs.getBoolean(
context.getString(R.string.sponsor_block_notifications_key), false)) {
String toastText = "";
switch (segment.category) {
case "sponsor":
toastText = context
.getString(R.string.sponsor_block_skip_sponsor_toast);
break;
case "intro":
toastText = context
.getString(R.string.sponsor_block_skip_intro_toast);
break;
case "outro":
toastText = context
.getString(R.string.sponsor_block_skip_outro_toast);
break;
case "interaction":
toastText = context
.getString(R.string.sponsor_block_skip_interaction_toast);
break;
case "selfpromo":
toastText = context
.getString(R.string.sponsor_block_skip_self_promo_toast);
break;
case "music_offtopic":
toastText = context
.getString(R.string.sponsor_block_skip_non_music_toast);
break;
}
Toast.makeText(context, toastText, Toast.LENGTH_SHORT).show();
}
}
private void seekTo(final long positionMillis) {
if (simpleExoPlayer != null) {
long normalizedPositionMillis = positionMillis;
if (normalizedPositionMillis < 0) {
normalizedPositionMillis = 0;
} else if (normalizedPositionMillis > simpleExoPlayer.getDuration()) {
normalizedPositionMillis = simpleExoPlayer.getDuration();
}
simpleExoPlayer.seekTo(normalizedPositionMillis);
}
}
private VideoSegment getSkippableSegment(final int progress) {
if (videoSegments == null) {
return null;
}
for (final VideoSegment segment : videoSegments) {
if (progress < segment.startTime) {
continue;
}
if (progress > segment.endTime) {
continue;
}
return segment;
}
return null;
}
}

View file

@ -0,0 +1,12 @@
package org.schabi.newpipe.player;
import com.google.android.exoplayer2.SimpleExoPlayer;
public interface LocalPlayerListener {
void onBlocked(SimpleExoPlayer player);
void onPlaying(SimpleExoPlayer player);
void onBuffering(SimpleExoPlayer player);
void onPaused(SimpleExoPlayer player);
void onPausedSeek(SimpleExoPlayer player);
void onCompleted(SimpleExoPlayer player);
}