local player improvements
This commit is contained in:
parent
c0e9b673db
commit
14e1abc28e
4 changed files with 423 additions and 245 deletions
|
@ -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"
|
||||||
|
|
|
@ -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() {
|
int visibility;
|
||||||
if (!isProgressLoopRunning()) {
|
|
||||||
startProgressLoop();
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onPlaying() {
|
if (!isInMultiWindow()) {
|
||||||
if (!isProgressLoopRunning()) {
|
visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN;
|
||||||
startProgressLoop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onBuffering() {
|
getWindow().getDecorView().setSystemUiVisibility(visibility);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && (isInMultiWindow())) {
|
||||||
|
getWindow().setStatusBarColor(Color.TRANSPARENT);
|
||||||
|
getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
||||||
|
}
|
||||||
|
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onPaused() {
|
private boolean isInMultiWindow() {
|
||||||
if (isProgressLoopRunning()) {
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode();
|
||||||
stopProgressLoop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onPausedSeek() {
|
boolean isLandscape() {
|
||||||
}
|
final DisplayMetrics metrics = getResources().getDisplayMetrics();
|
||||||
|
return metrics.heightPixels < metrics.widthPixels;
|
||||||
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);
|
|
||||||
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(
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void seekTo(final long positionMillis) {
|
|
||||||
if (simpleExoPlayer != null) {
|
|
||||||
// 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) {
|
|
||||||
if (segments == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final VideoSegment segment : segments) {
|
|
||||||
if (progress < segment.startTime) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progress > segment.endTime) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return segment;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
304
app/src/main/java/org/schabi/newpipe/player/LocalPlayer.java
Normal file
304
app/src/main/java/org/schabi/newpipe/player/LocalPlayer.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
Loading…
Reference in a new issue