SponsorBlock: started work on a local player implementation

This commit is contained in:
polymorphicshade 2021-01-26 17:01:27 -07:00
parent 991c12c539
commit 420f99f317
16 changed files with 460 additions and 26 deletions

View file

@ -22,8 +22,8 @@
android:label="@string/app_name" android:label="@string/app_name"
android:logo="@mipmap/ic_launcher" android:logo="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:theme="@style/OpeningTheme"
android:resizeableActivity="true" android:resizeableActivity="true"
android:theme="@style/OpeningTheme"
tools:ignore="AllowBackup"> tools:ignore="AllowBackup">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
@ -56,11 +56,9 @@
android:name=".player.BackgroundPlayerActivity" android:name=".player.BackgroundPlayerActivity"
android:label="@string/title_activity_play_queue" android:label="@string/title_activity_play_queue"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
<activity <activity
android:name=".settings.SettingsActivity" android:name=".settings.SettingsActivity"
android:label="@string/settings" /> android:label="@string/settings" />
<activity <activity
android:name=".about.AboutActivity" android:name=".about.AboutActivity"
android:label="@string/title_activity_about" /> android:label="@string/title_activity_about" />
@ -80,14 +78,11 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".ExitActivity" android:name=".ExitActivity"
android:label="@string/general_error" android:label="@string/general_error"
android:theme="@android:style/Theme.NoDisplay" /> android:theme="@android:style/Theme.NoDisplay" />
<activity android:name=".report.ErrorActivity" /> <activity android:name=".report.ErrorActivity" /> <!-- giga get related -->
<!-- giga get related -->
<activity <activity
android:name=".download.DownloadActivity" android:name=".download.DownloadActivity"
android:label="@string/app_name" android:label="@string/app_name"
@ -101,10 +96,10 @@
android:theme="@style/FilePickerThemeDark"> android:theme="@style/FilePickerThemeDark">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.GET_CONTENT" /> <action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".ReCaptchaActivity" android:name=".ReCaptchaActivity"
android:label="@string/recaptcha" /> android:label="@string/recaptcha" />
@ -262,7 +257,9 @@
<!-- Share filter --> <!-- Share filter -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" /> <data android:mimeType="text/plain" />
</intent-filter> </intent-filter>
@ -280,7 +277,7 @@
<data android:host="media.ccc.de" /> <data android:host="media.ccc.de" />
<!-- video prefix --> <!-- video prefix -->
<data android:pathPrefix="/v/" /> <data android:pathPrefix="/v/" />
<!-- channel prefix--> <!-- channel prefix -->
<data android:pathPrefix="/c/" /> <data android:pathPrefix="/c/" />
<data android:pathPrefix="/b/" /> <data android:pathPrefix="/b/" />
</intent-filter> </intent-filter>
@ -296,7 +293,6 @@
<data android:scheme="http" /> <data android:scheme="http" />
<data android:scheme="https" /> <data android:scheme="https" />
<data android:host="framatube.org" /> <data android:host="framatube.org" />
<data android:host="media.assassinate-you.net" /> <data android:host="media.assassinate-you.net" />
<data android:host="peertube.co.uk" /> <data android:host="peertube.co.uk" />
@ -308,20 +304,28 @@
<data android:host="video.ploud.fr" /> <data android:host="video.ploud.fr" />
<data android:host="video.lqdn.fr" /> <data android:host="video.lqdn.fr" />
<data android:host="skeptikon.fr" /> <data android:host="skeptikon.fr" />
<data android:pathPrefix="/videos/" /> <!-- it contains playlists --> <data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
<data android:pathPrefix="/accounts/" /> <data android:pathPrefix="/accounts/" />
<data android:pathPrefix="/video-channels/" /> <data android:pathPrefix="/video-channels/" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".LocalPlayerActivity"
android:label="@string/app_name"
android:launchMode="singleTask"
android:foregroundServiceType="mediaPlayback" />
<service <service
android:name=".RouterActivity$FetcherService" android:name=".RouterActivity$FetcherService"
android:exported="false" /> android:exported="false" /> <!-- see https://github.com/TeamNewPipe/NewPipe/issues/3947 -->
<!-- see https://github.com/TeamNewPipe/NewPipe/issues/3947 -->
<!-- Version < 3.0. DeX Mode and Screen Mirroring support --> <!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
<meta-data android:name="com.samsung.android.keepalive.density" android:value="true"/> <meta-data
<!-- Version >= 3.0. DeX Dual Mode support --> android:name="com.samsung.android.keepalive.density"
<meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true"/> android:value="true" /> <!-- Version >= 3.0. DeX Dual Mode support -->
<meta-data
android:name="com.samsung.android.multidisplay.keep_process_alive"
android:value="true" />
</application> </application>
</manifest>
</manifest>

View file

@ -0,0 +1,311 @@
package org.schabi.newpipe;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager;
import com.google.android.exoplayer2.Player;
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.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.util.VideoSegment;
import java.util.ArrayList;
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.BasePlayer.STATE_BLOCKED;
import static org.schabi.newpipe.player.BasePlayer.STATE_BUFFERING;
import static org.schabi.newpipe.player.BasePlayer.STATE_COMPLETED;
import static org.schabi.newpipe.player.BasePlayer.STATE_PAUSED;
import static org.schabi.newpipe.player.BasePlayer.STATE_PAUSED_SEEK;
import static org.schabi.newpipe.player.BasePlayer.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
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_local_player);
this.mPrefs = PreferenceManager.getDefaultSharedPreferences(App.getApp());
initPlayer();
initSegments();
}
private void initPlayer() {
final String uri = getIntent().getDataString();
if (uri == null) {
return;
}
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);
playerView.setPlayer(simpleExoPlayer);
this.progressUpdateReactor = new SerialDisposable();
}
private void initSegments() {
final String segmentsJson = getIntent().getStringExtra("segments");
if (segmentsJson != null && segmentsJson.length() > 0) {
try {
final ArrayList<VideoSegment> segmentsArrayList = new ArrayList<>();
final JsonObject obj = JsonParser.object().from(segmentsJson);
for (final Object item : obj.getArray("segments")) {
final JsonObject itemObject = (JsonObject) item;
final double startTime = itemObject.getDouble("start");
final double endTime = itemObject.getDouble("end");
final String category = itemObject.getString("category");
final VideoSegment segment = new VideoSegment(startTime, endTime, category);
segmentsArrayList.add(segment);
}
segments = segmentsArrayList.toArray(new VideoSegment[0]);
} catch (final Exception e) {
Log.e(TAG, "Error initializing segments", e);
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
simpleExoPlayer.removeListener(this);
simpleExoPlayer.stop();
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) {
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();
}
}
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);
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;
}
}

View file

@ -60,6 +60,7 @@ import org.schabi.newpipe.util.SecondaryStreamHelper;
import org.schabi.newpipe.util.StreamItemAdapter; import org.schabi.newpipe.util.StreamItemAdapter;
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.VideoSegment;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -124,6 +125,8 @@ public class DownloadDialog extends DialogFragment
private SharedPreferences prefs; private SharedPreferences prefs;
private VideoSegment[] segments;
public static DownloadDialog newInstance(final StreamInfo info) { public static DownloadDialog newInstance(final StreamInfo info) {
final DownloadDialog dialog = new DownloadDialog(); final DownloadDialog dialog = new DownloadDialog();
dialog.setInfo(info); dialog.setInfo(info);
@ -190,6 +193,10 @@ public class DownloadDialog extends DialogFragment
this.selectedSubtitleIndex = ssi; this.selectedSubtitleIndex = ssi;
} }
public void setVideoSegments(final VideoSegment[] seg) {
this.segments = seg;
}
@Override @Override
public void onCreate(@Nullable final Bundle savedInstanceState) { public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -942,7 +949,8 @@ public class DownloadDialog extends DialogFragment
} }
DownloadManagerService.startMission(context, urls, storage, kind, threads, DownloadManagerService.startMission(context, urls, storage, kind, threads,
currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo); currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo,
segments);
dismiss(); dismiss();
} }

View file

@ -1678,6 +1678,7 @@ public final class VideoDetailFragment
downloadDialog.setAudioStreams(currentInfo.getAudioStreams()); downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles()); downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
downloadDialog.setVideoSegments(player.getVideoSegments());
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
} catch (final Exception e) { } catch (final Exception e) {

View file

@ -1,6 +1,8 @@
package org.schabi.newpipe.util; package org.schabi.newpipe.util;
public class VideoSegment { import java.io.Serializable;
public class VideoSegment implements Serializable {
public double startTime; public double startTime;
public double endTime; public double endTime;
public String category; public String category;

View file

@ -13,6 +13,7 @@ public class FinishedMission extends Mission {
timestamp = mission.timestamp; timestamp = mission.timestamp;
kind = mission.kind; kind = mission.kind;
storage = mission.storage; storage = mission.storage;
segmentsJson = mission.segmentsJson;
} }
} }

View file

@ -2,6 +2,8 @@ package us.shandian.giga.get;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.schabi.newpipe.util.VideoSegment;
import java.io.Serializable; import java.io.Serializable;
import java.util.Calendar; import java.util.Calendar;
@ -35,6 +37,8 @@ public abstract class Mission implements Serializable {
*/ */
public StoredFileHelper storage; public StoredFileHelper storage;
public String segmentsJson;
public long getTimestamp() { public long getTimestamp() {
return timestamp; return timestamp;
} }

View file

@ -26,7 +26,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
// TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?) // TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?)
private static final String DATABASE_NAME = "downloads.db"; private static final String DATABASE_NAME = "downloads.db";
private static final int DATABASE_VERSION = 4; private static final int DATABASE_VERSION = 5;
/** /**
* The table name of download missions (old) * The table name of download missions (old)
@ -55,6 +55,8 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
private static final String KEY_PATH = "path"; private static final String KEY_PATH = "path";
private static final String KEY_SEGMENTS = "segments";
/** /**
* The statement to create the table * The statement to create the table
*/ */
@ -65,6 +67,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
KEY_DONE + " INTEGER NOT NULL, " + KEY_DONE + " INTEGER NOT NULL, " +
KEY_TIMESTAMP + " INTEGER NOT NULL, " + KEY_TIMESTAMP + " INTEGER NOT NULL, " +
KEY_KIND + " TEXT NOT NULL, " + KEY_KIND + " TEXT NOT NULL, " +
KEY_SEGMENTS + " TEXT, " +
" UNIQUE(" + KEY_TIMESTAMP + ", " + KEY_PATH + "));"; " UNIQUE(" + KEY_TIMESTAMP + ", " + KEY_PATH + "));";
@ -120,6 +123,11 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
cursor.close(); cursor.close();
db.execSQL("DROP TABLE " + MISSIONS_TABLE_NAME_v2); db.execSQL("DROP TABLE " + MISSIONS_TABLE_NAME_v2);
oldVersion++;
}
if (oldVersion == 4) {
db.execSQL("ALTER TABLE " + FINISHED_TABLE_NAME + " ADD COLUMN " + KEY_SEGMENTS + " TEXT;");
} }
} }
@ -136,6 +144,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
values.put(KEY_DONE, downloadMission.length); values.put(KEY_DONE, downloadMission.length);
values.put(KEY_TIMESTAMP, downloadMission.timestamp); values.put(KEY_TIMESTAMP, downloadMission.timestamp);
values.put(KEY_KIND, String.valueOf(downloadMission.kind)); values.put(KEY_KIND, String.valueOf(downloadMission.kind));
values.put(KEY_SEGMENTS, downloadMission.segmentsJson);
return values; return values;
} }
@ -153,6 +162,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE)); mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE));
mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP)); mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP));
mission.kind = kind.charAt(0); mission.kind = kind.charAt(0);
mission.segmentsJson = cursor.getString(cursor.getColumnIndexOrThrow(KEY_SEGMENTS));
try { try {
mission.storage = new StoredFileHelper(context,null, Uri.parse(path), ""); mission.storage = new StoredFileHelper(context,null, Uri.parse(path), "");

View file

@ -38,9 +38,13 @@ import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationCompat.Builder; import androidx.core.app.NotificationCompat.Builder;
import com.grack.nanojson.JsonStringWriter;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.download.DownloadActivity; import org.schabi.newpipe.download.DownloadActivity;
import org.schabi.newpipe.player.helper.LockManager; import org.schabi.newpipe.player.helper.LockManager;
import org.schabi.newpipe.util.VideoSegment;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -80,6 +84,7 @@ public class DownloadManagerService extends Service {
private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath"; private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath";
private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag"; private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag";
private static final String EXTRA_RECOVERY_INFO = "DownloadManagerService.extra.recoveryInfo"; private static final String EXTRA_RECOVERY_INFO = "DownloadManagerService.extra.recoveryInfo";
private static final String EXTRA_SEGMENTS = "DownloadManagerService.extra.segments";
private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished"; private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished";
private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished"; private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished";
@ -388,7 +393,8 @@ public class DownloadManagerService extends Service {
*/ */
public static void startMission(Context context, String[] urls, StoredFileHelper storage, public static void startMission(Context context, String[] urls, StoredFileHelper storage,
char kind, int threads, String source, String psName, char kind, int threads, String source, String psName,
String[] psArgs, long nearLength, MissionRecoveryInfo[] recoveryInfo) { String[] psArgs, long nearLength, MissionRecoveryInfo[] recoveryInfo,
VideoSegment[] segments) {
Intent intent = new Intent(context, DownloadManagerService.class); Intent intent = new Intent(context, DownloadManagerService.class);
intent.setAction(Intent.ACTION_RUN); intent.setAction(Intent.ACTION_RUN);
intent.putExtra(EXTRA_URLS, urls); intent.putExtra(EXTRA_URLS, urls);
@ -403,6 +409,7 @@ public class DownloadManagerService extends Service {
intent.putExtra(EXTRA_PARENT_PATH, storage.getParentUri()); intent.putExtra(EXTRA_PARENT_PATH, storage.getParentUri());
intent.putExtra(EXTRA_PATH, storage.getUri()); intent.putExtra(EXTRA_PATH, storage.getUri());
intent.putExtra(EXTRA_STORAGE_TAG, storage.getTag()); intent.putExtra(EXTRA_STORAGE_TAG, storage.getTag());
intent.putExtra(EXTRA_SEGMENTS, segments);
context.startService(intent); context.startService(intent);
} }
@ -419,6 +426,7 @@ public class DownloadManagerService extends Service {
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
String tag = intent.getStringExtra(EXTRA_STORAGE_TAG); String tag = intent.getStringExtra(EXTRA_STORAGE_TAG);
Parcelable[] parcelRecovery = intent.getParcelableArrayExtra(EXTRA_RECOVERY_INFO); Parcelable[] parcelRecovery = intent.getParcelableArrayExtra(EXTRA_RECOVERY_INFO);
VideoSegment[] segments = (VideoSegment[]) intent.getSerializableExtra(EXTRA_SEGMENTS);
StoredFileHelper storage; StoredFileHelper storage;
try { try {
@ -443,6 +451,25 @@ public class DownloadManagerService extends Service {
mission.nearLength = nearLength; mission.nearLength = nearLength;
mission.recoveryInfo = recovery; mission.recoveryInfo = recovery;
if (segments != null && segments.length > 0) {
try {
final JsonStringWriter writer = JsonWriter.string()
.object()
.array("segments");
for (final VideoSegment segment : segments) {
writer.object()
.value("start", segment.startTime)
.value("end", segment.endTime)
.value("category", segment.category)
.end();
}
writer.end().end();
mission.segmentsJson = writer.done();
} catch (final Exception e) {
e.printStackTrace();
}
}
if (ps != null) if (ps != null)
ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this)); ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this));

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color; import android.graphics.Color;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
@ -30,6 +31,7 @@ import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider; import androidx.core.content.FileProvider;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.Adapter; import androidx.recyclerview.widget.RecyclerView.Adapter;
@ -37,7 +39,9 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import org.schabi.newpipe.App;
import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.LocalPlayerActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
@ -122,8 +126,12 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
private final CompositeDisposable compositeDisposable = new CompositeDisposable(); private final CompositeDisposable compositeDisposable = new CompositeDisposable();
private SharedPreferences mPrefs;
public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage, View root) { public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage, View root) {
mContext = context; mContext = context;
mPrefs = PreferenceManager.getDefaultSharedPreferences(App.getApp());
mDownloadManager = downloadManager; mDownloadManager = downloadManager;
mInflater = LayoutInflater.from(mContext); mInflater = LayoutInflater.from(mContext);
@ -338,7 +346,25 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
} }
} }
private void viewWithFileProvider(Mission mission) { private void open(Mission mission) {
if (checkInvalidFile(mission)) return;
String mimeType = resolveMimeType(mission);
if (BuildConfig.DEBUG)
Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider");
Uri uri = resolveShareableUri(mission);
Intent intent = new Intent(mContext, LocalPlayerActivity.class);
intent.setDataAndType(uri, mimeType);
intent.putExtra("segments", mission.segmentsJson);
intent.setFlags(FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
mContext.startActivity(intent);
}
private void openExternally(Mission mission) {
if (checkInvalidFile(mission)) return; if (checkInvalidFile(mission)) return;
String mimeType = resolveMimeType(mission); String mimeType = resolveMimeType(mission);
@ -679,6 +705,9 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
applyChanges(); applyChanges();
checkMasterButtonsVisibility(); checkMasterButtonsVisibility();
return true; return true;
case R.id.open_externally:
openExternally(h.item.mission);
return true;
case R.id.md5: case R.id.md5:
case R.id.sha1: case R.id.sha1:
final NotificationManager notificationManager final NotificationManager notificationManager
@ -896,8 +925,14 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
itemView.setHapticFeedbackEnabled(true); itemView.setHapticFeedbackEnabled(true);
itemView.setOnClickListener(v -> { itemView.setOnClickListener(v -> {
if (item.mission instanceof FinishedMission) if (item.mission instanceof FinishedMission) {
viewWithFileProvider(item.mission); if (mPrefs.getBoolean(mContext
.getString(R.string.enable_local_player_key), false)) {
open(item.mission);
} else {
openExternally(item.mission);
}
}
}); });
itemView.setOnLongClickListener(v -> { itemView.setOnLongClickListener(v -> {

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
tools:context=".LocalPlayerActivity">
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:show_buffering="when_playing"
app:show_shuffle_button="true"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -29,6 +29,10 @@
android:id="@+id/delete" android:id="@+id/delete"
android:title="@string/delete" /> android:title="@string/delete" />
<item
android:id="@+id/open_externally"
android:title="@string/open_with" />
<item <item
android:id="@+id/error_message_view" android:id="@+id/error_message_view"
android:title="@string/show_error" /> android:title="@string/show_error" />

View file

@ -364,6 +364,7 @@
<string name="sponsor_block_clear_whitelist_key" translatable="false">sponsor_block_clear_whitelist</string> <string name="sponsor_block_clear_whitelist_key" translatable="false">sponsor_block_clear_whitelist</string>
<!-- Extras --> <!-- Extras -->
<string name="enable_local_player_key" translatable="false">enable_local_player</string>
<string name="disable_tablet_ui_key" translatable="false">disable_tablet_ui</string> <string name="disable_tablet_ui_key" translatable="false">disable_tablet_ui</string>
<string name="disable_tv_ui_key" translatable="false">disable_tv_ui</string> <string name="disable_tv_ui_key" translatable="false">disable_tv_ui</string>

View file

@ -360,6 +360,7 @@
<string name="checksum">Checksum</string> <string name="checksum">Checksum</string>
<string name="dismiss">Dismiss</string> <string name="dismiss">Dismiss</string>
<string name="rename">Rename</string> <string name="rename">Rename</string>
<string name="open_with">Open With...</string>
<!-- Fragment --> <!-- Fragment -->
<string name="add">New mission</string> <string name="add">New mission</string>
<string name="finish">OK</string> <string name="finish">OK</string>
@ -750,6 +751,8 @@
<string name="extras">Extras</string> <string name="extras">Extras</string>
<string name="extras_todo_summary">Tweaks, workarounds, and other miscellaneous settings belong here.</string> <string name="extras_todo_summary">Tweaks, workarounds, and other miscellaneous settings belong here.</string>
<string name="experimental_settings">Experimental Settings</string> <string name="experimental_settings">Experimental Settings</string>
<string name="enable_local_player_title">Enable Local Player (alpha)</string>
<string name="enable_local_player_summary">Use a built-in player for local playback. This is still in early development so there will probably be a lot of issues, including conflicts with the existing player.</string>
<string name="disable_tablet_ui_title">Disable Tablet UI</string> <string name="disable_tablet_ui_title">Disable Tablet UI</string>
<string name="disable_tablet_ui_summary">Ignore tablet layouts. This is intended for specific workarounds. You may need to restart the app to see the effects.</string> <string name="disable_tablet_ui_summary">Ignore tablet layouts. This is intended for specific workarounds. You may need to restart the app to see the effects.</string>
<string name="disable_tv_ui_title">Disable TV UI</string> <string name="disable_tv_ui_title">Disable TV UI</string>

View file

@ -13,6 +13,13 @@
android:layout="@layout/settings_category_header_layout" android:layout="@layout/settings_category_header_layout"
android:title="@string/experimental_settings"> android:title="@string/experimental_settings">
<SwitchPreference
app:iconSpaceReserved="false"
android:defaultValue="false"
android:key="@string/enable_local_player_key"
android:summary="@string/enable_local_player_summary"
android:title="@string/enable_local_player_title"/>
<SwitchPreference <SwitchPreference
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
android:defaultValue="false" android:defaultValue="false"

View file

@ -7,7 +7,7 @@ buildscript {
google() google()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.1.1' classpath 'com.android.tools.build:gradle:4.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong