Merge branch 'dev' into defaultTrending
This commit is contained in:
commit
bd485937c4
189 changed files with 10034 additions and 7857 deletions
|
|
@ -14,6 +14,7 @@ import android.os.AsyncTask;
|
|||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
|
@ -47,6 +48,8 @@ import okhttp3.Response;
|
|||
*/
|
||||
public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
|
||||
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final String TAG = CheckForNewAppVersionTask.class.getSimpleName();
|
||||
private static final Application app = App.getApp();
|
||||
private static final String GITHUB_APK_SHA1 = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
|
||||
private static final String newPipeApiUrl = "https://newpipe.schabi.org/api/data.json";
|
||||
|
|
@ -90,9 +93,8 @@ public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
|
|||
Response response = client.newCall(request).execute();
|
||||
return response.body().string();
|
||||
} catch (IOException ex) {
|
||||
ErrorActivity.reportError(app, ex, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"app update API fail", R.string.app_ui_crash));
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex));
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
@ -117,9 +119,8 @@ public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
|
|||
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode);
|
||||
|
||||
} catch (JSONException ex) {
|
||||
ErrorActivity.reportError(app, ex, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"could not parse app update JSON data", R.string.app_ui_crash));
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -574,7 +574,7 @@ public class RouterActivity extends AppCompatActivity {
|
|||
playQueue = new SinglePlayQueue((StreamInfo) info);
|
||||
|
||||
if (playerChoice.equals(videoPlayerKey)) {
|
||||
NavigationHelper.playOnMainPlayer(this, playQueue);
|
||||
NavigationHelper.playOnMainPlayer(this, playQueue, true);
|
||||
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true);
|
||||
} else if (playerChoice.equals(popupPlayerKey)) {
|
||||
|
|
@ -587,11 +587,11 @@ public class RouterActivity extends AppCompatActivity {
|
|||
playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info);
|
||||
|
||||
if (playerChoice.equals(videoPlayerKey)) {
|
||||
NavigationHelper.playOnMainPlayer(this, playQueue);
|
||||
NavigationHelper.playOnMainPlayer(this, playQueue, true);
|
||||
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
||||
NavigationHelper.playOnBackgroundPlayer(this, playQueue);
|
||||
NavigationHelper.playOnBackgroundPlayer(this, playQueue, true);
|
||||
} else if (playerChoice.equals(popupPlayerKey)) {
|
||||
NavigationHelper.playOnPopupPlayer(this, playQueue);
|
||||
NavigationHelper.playOnPopupPlayer(this, playQueue, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
|
|||
" ORDER BY " + STREAM_ACCESS_DATE + " DESC")
|
||||
public abstract Flowable<List<StreamHistoryEntry>> getHistory();
|
||||
|
||||
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID +
|
||||
" = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1")
|
||||
@Nullable
|
||||
public abstract StreamHistoryEntity getLatestEntry(final long streamId);
|
||||
|
||||
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||
public abstract int deleteStreamHistory(final long streamId);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ package org.schabi.newpipe.database.stream.model;
|
|||
import android.arch.persistence.room.ColumnInfo;
|
||||
import android.arch.persistence.room.Entity;
|
||||
import android.arch.persistence.room.ForeignKey;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static android.arch.persistence.room.ForeignKey.CASCADE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||
|
|
@ -22,6 +25,12 @@ public class StreamStateEntity {
|
|||
final public static String JOIN_STREAM_ID = "stream_id";
|
||||
final public static String STREAM_PROGRESS_TIME = "progress_time";
|
||||
|
||||
|
||||
/** Playback state will not be saved, if playback time less than this threshold */
|
||||
private static final int PLAYBACK_SAVE_THRESHOLD_START_SECONDS = 5;
|
||||
/** Playback state will not be saved, if time left less than this threshold */
|
||||
private static final int PLAYBACK_SAVE_THRESHOLD_END_SECONDS = 10;
|
||||
|
||||
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||
private long streamUid;
|
||||
|
||||
|
|
@ -48,4 +57,18 @@ public class StreamStateEntity {
|
|||
public void setProgressTime(long progressTime) {
|
||||
this.progressTime = progressTime;
|
||||
}
|
||||
|
||||
public boolean isValid(int durationInSeconds) {
|
||||
final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(progressTime);
|
||||
return seconds > PLAYBACK_SAVE_THRESHOLD_START_SECONDS
|
||||
&& seconds < durationInSeconds - PLAYBACK_SAVE_THRESHOLD_END_SECONDS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (obj instanceof StreamStateEntity) {
|
||||
return ((StreamStateEntity) obj).streamUid == streamUid
|
||||
&& ((StreamStateEntity) obj).progressTime == progressTime;
|
||||
} else return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ public class DownloadActivity extends AppCompatActivity {
|
|||
private void updateFragments() {
|
||||
MissionsFragment fragment = new MissionsFragment();
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG)
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
.commit();
|
||||
|
|
|
|||
|
|
@ -1,14 +1,23 @@
|
|||
package org.schabi.newpipe.download;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.IdRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.provider.DocumentFile;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.view.menu.ActionMenuItemView;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
|
|
@ -34,7 +43,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.extractor.utils.Localization;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.FilenameUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
|
|
@ -43,19 +53,27 @@ import org.schabi.newpipe.util.StreamItemAdapter;
|
|||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
import us.shandian.giga.service.DownloadManager;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
|
||||
import us.shandian.giga.service.MissionState;
|
||||
|
||||
public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
||||
private static final String TAG = "DialogFragment";
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final int REQUEST_DOWNLOAD_PATH_SAF = 0x1230;
|
||||
|
||||
@State
|
||||
protected StreamInfo currentInfo;
|
||||
|
|
@ -80,7 +98,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
|
||||
private EditText nameEditText;
|
||||
private Spinner streamsSpinner;
|
||||
private RadioGroup radioVideoAudioGroup;
|
||||
private RadioGroup radioStreamsGroup;
|
||||
private TextView threadsCountTextView;
|
||||
private SeekBar threadsSeekBar;
|
||||
|
||||
|
|
@ -160,7 +178,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
return;
|
||||
}
|
||||
|
||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(getContext()));
|
||||
context = getContext();
|
||||
|
||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams = new SparseArray<>(4);
|
||||
|
|
@ -177,9 +197,59 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
}
|
||||
}
|
||||
|
||||
this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, secondaryStreams);
|
||||
this.audioStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedAudioStreams);
|
||||
this.subtitleStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedSubtitleStreams);
|
||||
this.videoStreamsAdapter = new StreamItemAdapter<>(context, wrappedVideoStreams, secondaryStreams);
|
||||
this.audioStreamsAdapter = new StreamItemAdapter<>(context, wrappedAudioStreams);
|
||||
this.subtitleStreamsAdapter = new StreamItemAdapter<>(context, wrappedSubtitleStreams);
|
||||
|
||||
Intent intent = new Intent(context, DownloadManagerService.class);
|
||||
context.startService(intent);
|
||||
|
||||
context.bindService(intent, new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName cname, IBinder service) {
|
||||
DownloadManagerBinder mgr = (DownloadManagerBinder) service;
|
||||
|
||||
mainStorageAudio = mgr.getMainStorageAudio();
|
||||
mainStorageVideo = mgr.getMainStorageVideo();
|
||||
downloadManager = mgr.getDownloadManager();
|
||||
askForSavePath = mgr.askForSavePath();
|
||||
|
||||
okButton.setEnabled(true);
|
||||
|
||||
context.unbindService(this);
|
||||
|
||||
// check of download paths are defined
|
||||
if (!askForSavePath) {
|
||||
String msg = "";
|
||||
if (mainStorageVideo == null) msg += getString(R.string.download_path_title);
|
||||
if (mainStorageAudio == null)
|
||||
msg += getString(R.string.download_path_audio_title);
|
||||
|
||||
if (!msg.isEmpty()) {
|
||||
String title;
|
||||
if (mainStorageVideo == null && mainStorageAudio == null) {
|
||||
title = getString(R.string.general_error);
|
||||
msg = getString(R.string.no_available_dir) + ":\n" + msg;
|
||||
} else {
|
||||
title = msg;
|
||||
msg = getString(R.string.no_available_dir);
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setTitle(title)
|
||||
.setMessage(msg)
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
// nothing to do
|
||||
}
|
||||
}, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -204,8 +274,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
threadsCountTextView = view.findViewById(R.id.threads_count);
|
||||
threadsSeekBar = view.findViewById(R.id.threads);
|
||||
|
||||
radioVideoAudioGroup = view.findViewById(R.id.video_audio_group);
|
||||
radioVideoAudioGroup.setOnCheckedChangeListener(this);
|
||||
radioStreamsGroup = view.findViewById(R.id.video_audio_group);
|
||||
radioStreamsGroup.setOnCheckedChangeListener(this);
|
||||
|
||||
initToolbar(view.findViewById(R.id.toolbar));
|
||||
setupDownloadOptions();
|
||||
|
|
@ -240,17 +310,17 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
disposables.clear();
|
||||
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams).subscribe(result -> {
|
||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) {
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.video_button) {
|
||||
setupVideoSpinner();
|
||||
}
|
||||
}));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams).subscribe(result -> {
|
||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
setupAudioSpinner();
|
||||
}
|
||||
}));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> {
|
||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
||||
setupSubtitleSpinner();
|
||||
}
|
||||
}));
|
||||
|
|
@ -263,22 +333,49 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == REQUEST_DOWNLOAD_PATH_SAF && resultCode == Activity.RESULT_OK) {
|
||||
if (data.getData() == null) {
|
||||
showFailedDialog(R.string.general_error);
|
||||
return;
|
||||
}
|
||||
|
||||
DocumentFile docFile = DocumentFile.fromSingleUri(context, data.getData());
|
||||
if (docFile == null) {
|
||||
showFailedDialog(R.string.general_error);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the selected file was previously used
|
||||
checkSelectedDownload(null, data.getData(), docFile.getName(), docFile.getType());
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Inits
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void initToolbar(Toolbar toolbar) {
|
||||
if (DEBUG) Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
|
||||
|
||||
boolean isLight = ThemeHelper.isLightThemeSelected(getActivity());
|
||||
|
||||
toolbar.setTitle(R.string.download_dialog_title);
|
||||
toolbar.setNavigationIcon(ThemeHelper.isLightThemeSelected(getActivity()) ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
|
||||
toolbar.setNavigationIcon(isLight ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
|
||||
toolbar.inflateMenu(R.menu.dialog_url);
|
||||
toolbar.setNavigationOnClickListener(v -> getDialog().dismiss());
|
||||
|
||||
okButton = toolbar.findViewById(R.id.okay);
|
||||
okButton.setEnabled(false);// disable until the download service connection is done
|
||||
|
||||
toolbar.setOnMenuItemClickListener(item -> {
|
||||
if (item.getItemId() == R.id.okay) {
|
||||
prepareSelectedDownload();
|
||||
|
|
@ -346,7 +443,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
|
||||
switch (radioVideoAudioGroup.getCheckedRadioButtonId()) {
|
||||
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
selectedAudioIndex = position;
|
||||
break;
|
||||
|
|
@ -370,9 +467,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
protected void setupDownloadOptions() {
|
||||
setRadioButtonsState(false);
|
||||
|
||||
final RadioButton audioButton = radioVideoAudioGroup.findViewById(R.id.audio_button);
|
||||
final RadioButton videoButton = radioVideoAudioGroup.findViewById(R.id.video_button);
|
||||
final RadioButton subtitleButton = radioVideoAudioGroup.findViewById(R.id.subtitle_button);
|
||||
final RadioButton audioButton = radioStreamsGroup.findViewById(R.id.audio_button);
|
||||
final RadioButton videoButton = radioStreamsGroup.findViewById(R.id.video_button);
|
||||
final RadioButton subtitleButton = radioStreamsGroup.findViewById(R.id.subtitle_button);
|
||||
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
|
||||
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
|
||||
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
|
||||
|
|
@ -397,9 +494,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
}
|
||||
|
||||
private void setRadioButtonsState(boolean enabled) {
|
||||
radioVideoAudioGroup.findViewById(R.id.audio_button).setEnabled(enabled);
|
||||
radioVideoAudioGroup.findViewById(R.id.video_button).setEnabled(enabled);
|
||||
radioVideoAudioGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
|
||||
radioStreamsGroup.findViewById(R.id.audio_button).setEnabled(enabled);
|
||||
radioStreamsGroup.findViewById(R.id.video_button).setEnabled(enabled);
|
||||
radioStreamsGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
|
||||
}
|
||||
|
||||
private int getSubtitleIndexBy(List<SubtitlesStream> streams) {
|
||||
|
|
@ -434,98 +531,297 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
return 0;
|
||||
}
|
||||
|
||||
StoredDirectoryHelper mainStorageAudio = null;
|
||||
StoredDirectoryHelper mainStorageVideo = null;
|
||||
DownloadManager downloadManager = null;
|
||||
ActionMenuItemView okButton = null;
|
||||
Context context;
|
||||
boolean askForSavePath;
|
||||
|
||||
private String getNameEditText() {
|
||||
String str = nameEditText.getText().toString().trim();
|
||||
|
||||
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
|
||||
}
|
||||
|
||||
private void showFailedDialog(@StringRes int msg) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.general_error)
|
||||
.setMessage(msg)
|
||||
.setNegativeButton(android.R.string.ok, null)
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showErrorActivity(Exception e) {
|
||||
ErrorActivity.reportError(
|
||||
context,
|
||||
Collections.singletonList(e),
|
||||
null,
|
||||
null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error)
|
||||
);
|
||||
}
|
||||
|
||||
private void prepareSelectedDownload() {
|
||||
final Context context = getContext();
|
||||
Stream stream;
|
||||
String location;
|
||||
char kind;
|
||||
StoredDirectoryHelper mainStorage;
|
||||
MediaFormat format;
|
||||
String mime;
|
||||
|
||||
String fileName = nameEditText.getText().toString().trim();
|
||||
if (fileName.isEmpty())
|
||||
fileName = FilenameUtils.createFilename(context, currentInfo.getName());
|
||||
// first, build the filename and get the output folder (if possible)
|
||||
// later, run a very very very large file checking logic
|
||||
|
||||
switch (radioVideoAudioGroup.getCheckedRadioButtonId()) {
|
||||
String filename = getNameEditText().concat(".");
|
||||
|
||||
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
stream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
||||
location = NewPipeSettings.getAudioDownloadPath(context);
|
||||
kind = 'a';
|
||||
mainStorage = mainStorageAudio;
|
||||
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
||||
mime = format.mimeType;
|
||||
filename += format.suffix;
|
||||
break;
|
||||
case R.id.video_button:
|
||||
stream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||
location = NewPipeSettings.getVideoDownloadPath(context);
|
||||
kind = 'v';
|
||||
mainStorage = mainStorageVideo;
|
||||
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
||||
mime = format.mimeType;
|
||||
filename += format.suffix;
|
||||
break;
|
||||
case R.id.subtitle_button:
|
||||
stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
||||
location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video files go together
|
||||
kind = 's';
|
||||
mainStorage = mainStorageVideo;// subtitle & video files go together
|
||||
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
||||
mime = format.mimeType;
|
||||
filename += format == MediaFormat.TTML ? MediaFormat.SRT.suffix : format.suffix;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("No stream selected");
|
||||
}
|
||||
|
||||
if (mainStorage == null || askForSavePath) {
|
||||
// This part is called if with SAF preferred:
|
||||
// * older android version running
|
||||
// * save path not defined (via download settings)
|
||||
// * the user as checked the "ask where to download" option
|
||||
|
||||
StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_PATH_SAF, filename, mime);
|
||||
return;
|
||||
}
|
||||
|
||||
// check for existing file with the same name
|
||||
checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime);
|
||||
}
|
||||
|
||||
private void checkSelectedDownload(StoredDirectoryHelper mainStorage, Uri targetFile, String filename, String mime) {
|
||||
StoredFileHelper storage;
|
||||
|
||||
try {
|
||||
if (mainStorage == null) {
|
||||
// using SAF on older android version
|
||||
storage = new StoredFileHelper(context, null, targetFile, "");
|
||||
} else if (targetFile == null) {
|
||||
// the file does not exist, but it is probably used in a pending download
|
||||
storage = new StoredFileHelper(mainStorage.getUri(), filename, mime, mainStorage.getTag());
|
||||
} else {
|
||||
// the target filename is already use, attempt to use it
|
||||
storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
showErrorActivity(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if is our file
|
||||
MissionState state = downloadManager.checkForExistingMission(storage);
|
||||
@StringRes int msgBtn;
|
||||
@StringRes int msgBody;
|
||||
|
||||
switch (state) {
|
||||
case Finished:
|
||||
msgBtn = R.string.overwrite;
|
||||
msgBody = R.string.overwrite_finished_warning;
|
||||
break;
|
||||
case Pending:
|
||||
msgBtn = R.string.overwrite;
|
||||
msgBody = R.string.download_already_pending;
|
||||
break;
|
||||
case PendingRunning:
|
||||
msgBtn = R.string.generate_unique_name;
|
||||
msgBody = R.string.download_already_running;
|
||||
break;
|
||||
case None:
|
||||
if (mainStorage == null) {
|
||||
// This part is called if:
|
||||
// * using SAF on older android version
|
||||
// * save path not defined
|
||||
continueSelectedDownload(storage);
|
||||
return;
|
||||
} else if (targetFile == null) {
|
||||
// This part is called if:
|
||||
// * the filename is not used in a pending/finished download
|
||||
// * the file does not exists, create
|
||||
|
||||
if (!mainStorage.mkdirs()) {
|
||||
showFailedDialog(R.string.error_path_creation);
|
||||
return;
|
||||
}
|
||||
|
||||
storage = mainStorage.createFile(filename, mime);
|
||||
if (storage == null || !storage.canWrite()) {
|
||||
showFailedDialog(R.string.error_file_creation);
|
||||
return;
|
||||
}
|
||||
|
||||
continueSelectedDownload(storage);
|
||||
return;
|
||||
}
|
||||
msgBtn = R.string.overwrite;
|
||||
msgBody = R.string.overwrite_unrelated_warning;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
int threads;
|
||||
|
||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
||||
threads = 1;// use unique thread for subtitles due small file size
|
||||
fileName += ".srt";// final subtitle format
|
||||
} else {
|
||||
threads = threadsSeekBar.getProgress() + 1;
|
||||
fileName += "." + stream.getFormat().getSuffix();
|
||||
AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.download_dialog_title)
|
||||
.setMessage(msgBody)
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
final StoredFileHelper finalStorage = storage;
|
||||
|
||||
|
||||
if (mainStorage == null) {
|
||||
// This part is called if:
|
||||
// * using SAF on older android version
|
||||
// * save path not defined
|
||||
switch (state) {
|
||||
case Pending:
|
||||
case Finished:
|
||||
askDialog.setPositiveButton(msgBtn, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
downloadManager.forgetMission(finalStorage);
|
||||
continueSelectedDownload(finalStorage);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
askDialog.create().show();
|
||||
return;
|
||||
}
|
||||
|
||||
final String finalFileName = fileName;
|
||||
askDialog.setPositiveButton(msgBtn, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
|
||||
DownloadManagerService.checkForRunningMission(context, location, fileName, (listed, finished) -> {
|
||||
if (listed) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.download_dialog_title)
|
||||
.setMessage(finished ? R.string.overwrite_warning : R.string.download_already_running)
|
||||
.setPositiveButton(
|
||||
finished ? R.string.overwrite : R.string.generate_unique_name,
|
||||
(dialog, which) -> downloadSelected(context, stream, location, finalFileName, kind, threads)
|
||||
)
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||
dialog.cancel();
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
} else {
|
||||
downloadSelected(context, stream, location, finalFileName, kind, threads);
|
||||
StoredFileHelper storageNew;
|
||||
switch (state) {
|
||||
case Finished:
|
||||
case Pending:
|
||||
downloadManager.forgetMission(finalStorage);
|
||||
case None:
|
||||
if (targetFile == null) {
|
||||
storageNew = mainStorage.createFile(filename, mime);
|
||||
} else {
|
||||
try {
|
||||
// try take (or steal) the file
|
||||
storageNew = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag());
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to take (or steal) the file in " + targetFile.toString());
|
||||
storageNew = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (storageNew != null && storageNew.canWrite())
|
||||
continueSelectedDownload(storageNew);
|
||||
else
|
||||
showFailedDialog(R.string.error_file_creation);
|
||||
break;
|
||||
case PendingRunning:
|
||||
storageNew = mainStorage.createUniqueFile(filename, mime);
|
||||
if (storageNew == null)
|
||||
showFailedDialog(R.string.error_file_creation);
|
||||
else
|
||||
continueSelectedDownload(storageNew);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
askDialog.create().show();
|
||||
}
|
||||
|
||||
private void downloadSelected(Context context, Stream selectedStream, String location, String fileName, char kind, int threads) {
|
||||
private void continueSelectedDownload(@NonNull StoredFileHelper storage) {
|
||||
if (!storage.canWrite()) {
|
||||
showFailedDialog(R.string.permission_denied);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the selected file has to be overwritten, by simply checking its length
|
||||
try {
|
||||
if (storage.length() > 0) storage.truncate();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "failed to overwrite the file: " + storage.getUri().toString(), e);
|
||||
showFailedDialog(R.string.overwrite_failed);
|
||||
return;
|
||||
}
|
||||
|
||||
Stream selectedStream;
|
||||
char kind;
|
||||
int threads = threadsSeekBar.getProgress() + 1;
|
||||
String[] urls;
|
||||
String psName = null;
|
||||
String[] psArgs = null;
|
||||
String secondaryStreamUrl = null;
|
||||
long nearLength = 0;
|
||||
|
||||
if (selectedStream instanceof VideoStream) {
|
||||
SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter
|
||||
.getAllSecondary()
|
||||
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
|
||||
// more download logic: select muxer, subtitle converter, etc.
|
||||
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
threads = 1;// use unique thread for subtitles due small file size
|
||||
kind = 'a';
|
||||
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
||||
|
||||
if (secondaryStream != null) {
|
||||
secondaryStreamUrl = secondaryStream.getStream().getUrl();
|
||||
psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER;
|
||||
psArgs = null;
|
||||
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
|
||||
|
||||
// set nearLength, only, if both sizes are fetched or known. this probably does not work on slow networks
|
||||
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
|
||||
nearLength = secondaryStream.getSizeInBytes() + videoSize;
|
||||
if (selectedStream.getFormat() == MediaFormat.M4A) {
|
||||
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
|
||||
}
|
||||
}
|
||||
} else if ((selectedStream instanceof SubtitlesStream) && selectedStream.getFormat() == MediaFormat.TTML) {
|
||||
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
|
||||
psArgs = new String[]{
|
||||
selectedStream.getFormat().getSuffix(),
|
||||
"false",// ignore empty frames
|
||||
"false",// detect youtube duplicate lines
|
||||
};
|
||||
break;
|
||||
case R.id.video_button:
|
||||
kind = 'v';
|
||||
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||
|
||||
SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter
|
||||
.getAllSecondary()
|
||||
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
|
||||
|
||||
if (secondaryStream != null) {
|
||||
secondaryStreamUrl = secondaryStream.getStream().getUrl();
|
||||
|
||||
if (selectedStream.getFormat() == MediaFormat.MPEG_4)
|
||||
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
|
||||
else
|
||||
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
|
||||
|
||||
psArgs = null;
|
||||
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
|
||||
|
||||
// set nearLength, only, if both sizes are fetched or known. This probably
|
||||
// does not work on slow networks but is later updated in the downloader
|
||||
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
|
||||
nearLength = secondaryStream.getSizeInBytes() + videoSize;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case R.id.subtitle_button:
|
||||
kind = 's';
|
||||
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
||||
|
||||
if (selectedStream.getFormat() == MediaFormat.TTML) {
|
||||
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
|
||||
psArgs = new String[]{
|
||||
selectedStream.getFormat().getSuffix(),
|
||||
"false",// ignore empty frames
|
||||
"false",// detect youtube duplicate lines
|
||||
};
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (secondaryStreamUrl == null) {
|
||||
|
|
@ -534,8 +830,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl};
|
||||
}
|
||||
|
||||
DownloadManagerService.startMission(context, urls, location, fileName, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);
|
||||
DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);
|
||||
|
||||
getDialog().dismiss();
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,21 +230,4 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
|||
ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView,
|
||||
ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId));
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void openUrlInBrowser(String url) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title)));
|
||||
}
|
||||
|
||||
protected void shareUrl(String subject, String url) {
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, url);
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentPagerAdapter;
|
||||
|
|
@ -61,6 +62,18 @@ public class TabAdaptor extends FragmentPagerAdapter {
|
|||
else return POSITION_NONE;
|
||||
}
|
||||
|
||||
public int getItemPositionByTitle(String title) {
|
||||
return mFragmentTitleList.indexOf(title);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getItemTitle(int position) {
|
||||
if (position < 0 || position >= mFragmentTitleList.size()) {
|
||||
return null;
|
||||
}
|
||||
return mFragmentTitleList.get(position);
|
||||
}
|
||||
|
||||
public void notifyDataSetUpdate(){
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,14 +86,17 @@ import org.schabi.newpipe.util.ListHelper;
|
|||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Single;
|
||||
|
|
@ -117,11 +120,12 @@ public class VideoDetailFragment
|
|||
private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1;
|
||||
private static final int RESOLUTIONS_MENU_UPDATE_FLAG = 0x2;
|
||||
private static final int TOOLBAR_ITEMS_UPDATE_FLAG = 0x4;
|
||||
private static final int COMMENTS_UPDATE_FLAG = 0x4;
|
||||
private static final int COMMENTS_UPDATE_FLAG = 0x8;
|
||||
|
||||
private boolean autoPlayEnabled;
|
||||
private boolean showRelatedStreams;
|
||||
private boolean showComments;
|
||||
private String selectedTabTag;
|
||||
|
||||
@State
|
||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||
|
|
@ -134,6 +138,8 @@ public class VideoDetailFragment
|
|||
private Disposable currentWorker;
|
||||
@NonNull
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
@Nullable
|
||||
private Disposable positionSubscriber = null;
|
||||
|
||||
private List<VideoStream> sortedVideoStreams;
|
||||
private int selectedVideoStreamIndex = -1;
|
||||
|
|
@ -151,6 +157,7 @@ public class VideoDetailFragment
|
|||
private View thumbnailBackgroundButton;
|
||||
private ImageView thumbnailImageView;
|
||||
private ImageView thumbnailPlayButton;
|
||||
private AnimatedProgressBar positionView;
|
||||
|
||||
private View videoTitleRoot;
|
||||
private TextView videoTitleTextView;
|
||||
|
|
@ -163,6 +170,7 @@ public class VideoDetailFragment
|
|||
private TextView detailControlsDownload;
|
||||
private TextView appendControlsDetail;
|
||||
private TextView detailDurationView;
|
||||
private TextView detailPositionView;
|
||||
|
||||
private LinearLayout videoDescriptionRootLayout;
|
||||
private TextView videoUploadDateView;
|
||||
|
|
@ -213,6 +221,9 @@ public class VideoDetailFragment
|
|||
showComments = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(getString(R.string.show_comments_key), true);
|
||||
|
||||
selectedTabTag = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getString(getString(R.string.stream_info_selected_tab_key), COMMENTS_TAB_TAG);
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
|
@ -226,6 +237,10 @@ public class VideoDetailFragment
|
|||
public void onPause() {
|
||||
super.onPause();
|
||||
if (currentWorker != null) currentWorker.dispose();
|
||||
PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||
.edit()
|
||||
.putString(getString(R.string.stream_info_selected_tab_key), pageAdapter.getItemTitle(viewPager.getCurrentItem()))
|
||||
.apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -250,6 +265,8 @@ public class VideoDetailFragment
|
|||
// Check if it was loading when the fragment was stopped/paused,
|
||||
if (wasLoading.getAndSet(false)) {
|
||||
selectAndLoadVideo(serviceId, url, name);
|
||||
} else if (currentInfo != null) {
|
||||
updateProgressInfo(currentInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -259,8 +276,10 @@ public class VideoDetailFragment
|
|||
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.unregisterOnSharedPreferenceChangeListener(this);
|
||||
|
||||
if (positionSubscriber != null) positionSubscriber.dispose();
|
||||
if (currentWorker != null) currentWorker.dispose();
|
||||
if (disposables != null) disposables.clear();
|
||||
positionSubscriber = null;
|
||||
currentWorker = null;
|
||||
disposables = null;
|
||||
}
|
||||
|
|
@ -453,6 +472,7 @@ public class VideoDetailFragment
|
|||
videoTitleTextView = rootView.findViewById(R.id.detail_video_title_view);
|
||||
videoTitleToggleArrow = rootView.findViewById(R.id.detail_toggle_description_view);
|
||||
videoCountView = rootView.findViewById(R.id.detail_view_count_view);
|
||||
positionView = rootView.findViewById(R.id.position_view);
|
||||
|
||||
detailControlsBackground = rootView.findViewById(R.id.detail_controls_background);
|
||||
detailControlsPopup = rootView.findViewById(R.id.detail_controls_popup);
|
||||
|
|
@ -460,6 +480,7 @@ public class VideoDetailFragment
|
|||
detailControlsDownload = rootView.findViewById(R.id.detail_controls_download);
|
||||
appendControlsDetail = rootView.findViewById(R.id.touch_append_detail);
|
||||
detailDurationView = rootView.findViewById(R.id.detail_duration_view);
|
||||
detailPositionView = rootView.findViewById(R.id.detail_position_view);
|
||||
|
||||
videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout);
|
||||
videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view);
|
||||
|
|
@ -527,10 +548,10 @@ public class VideoDetailFragment
|
|||
final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> {
|
||||
switch (i) {
|
||||
case 0:
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), true);
|
||||
break;
|
||||
case 1:
|
||||
NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item));
|
||||
NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item), true);
|
||||
break;
|
||||
case 2:
|
||||
if (getFragmentManager() != null) {
|
||||
|
|
@ -539,7 +560,7 @@ public class VideoDetailFragment
|
|||
}
|
||||
break;
|
||||
case 3:
|
||||
shareUrl(item.getName(), item.getUrl());
|
||||
ShareUtils.shareUrl(this.getContext(), item.getName(), item.getUrl());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
@ -628,13 +649,13 @@ public class VideoDetailFragment
|
|||
switch (id) {
|
||||
case R.id.menu_item_share: {
|
||||
if (currentInfo != null) {
|
||||
shareUrl(currentInfo.getName(), currentInfo.getOriginalUrl());
|
||||
ShareUtils.shareUrl(this.getContext(), currentInfo.getName(), currentInfo.getOriginalUrl());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_item_openInBrowser: {
|
||||
if (currentInfo != null) {
|
||||
openUrlInBrowser(currentInfo.getOriginalUrl());
|
||||
ShareUtils.openUrlInBrowser(this.getContext(), currentInfo.getOriginalUrl());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -815,6 +836,9 @@ public class VideoDetailFragment
|
|||
}
|
||||
|
||||
private void initTabs() {
|
||||
if (pageAdapter.getCount() != 0) {
|
||||
selectedTabTag = pageAdapter.getItemTitle(viewPager.getCurrentItem());
|
||||
}
|
||||
pageAdapter.clearAllItems();
|
||||
|
||||
if(shouldShowComments()){
|
||||
|
|
@ -835,6 +859,8 @@ public class VideoDetailFragment
|
|||
if(pageAdapter.getCount() < 2){
|
||||
tabLayout.setVisibility(View.GONE);
|
||||
}else{
|
||||
int position = pageAdapter.getItemPositionByTitle(selectedTabTag);
|
||||
if(position != -1) viewPager.setCurrentItem(position);
|
||||
tabLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
|
@ -876,11 +902,11 @@ public class VideoDetailFragment
|
|||
|
||||
final PlayQueue itemQueue = new SinglePlayQueue(currentInfo);
|
||||
if (append) {
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue);
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue, false);
|
||||
} else {
|
||||
Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
|
||||
final Intent intent = NavigationHelper.getPlayerIntent(
|
||||
activity, PopupVideoPlayer.class, itemQueue, getSelectedVideoStream().resolution
|
||||
activity, PopupVideoPlayer.class, itemQueue, getSelectedVideoStream().resolution, true
|
||||
);
|
||||
activity.startService(intent);
|
||||
}
|
||||
|
|
@ -900,9 +926,9 @@ public class VideoDetailFragment
|
|||
private void openNormalBackgroundPlayer(final boolean append) {
|
||||
final PlayQueue itemQueue = new SinglePlayQueue(currentInfo);
|
||||
if (append) {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, itemQueue);
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, itemQueue, false);
|
||||
} else {
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, itemQueue);
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, itemQueue, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -912,7 +938,7 @@ public class VideoDetailFragment
|
|||
mIntent = NavigationHelper.getPlayerIntent(activity,
|
||||
MainVideoPlayer.class,
|
||||
playQueue,
|
||||
getSelectedVideoStream().getResolution());
|
||||
getSelectedVideoStream().getResolution(), true);
|
||||
startActivity(mIntent);
|
||||
}
|
||||
|
||||
|
|
@ -1018,6 +1044,8 @@ public class VideoDetailFragment
|
|||
animateView(spinnerToolbar, false, 200);
|
||||
animateView(thumbnailPlayButton, false, 50);
|
||||
animateView(detailDurationView, false, 100);
|
||||
animateView(detailPositionView, false, 100);
|
||||
animateView(positionView, false, 50);
|
||||
|
||||
videoTitleTextView.setText(name != null ? name : "");
|
||||
videoTitleTextView.setMaxLines(1);
|
||||
|
|
@ -1132,6 +1160,7 @@ public class VideoDetailFragment
|
|||
videoUploadDateView.setText(Localization.localizeDate(activity, info.getUploadDate()));
|
||||
}
|
||||
prepareDescription(info.getDescription());
|
||||
updateProgressInfo(info);
|
||||
|
||||
animateView(spinnerToolbar, true, 500);
|
||||
setupActionBar(info);
|
||||
|
|
@ -1181,7 +1210,7 @@ public class VideoDetailFragment
|
|||
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
|
||||
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
|
||||
|
||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||
downloadDialog.show(getActivity().getSupportFragmentManager(), "downloadDialog");
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
|
||||
ServiceList.all()
|
||||
|
|
@ -1206,9 +1235,7 @@ public class VideoDetailFragment
|
|||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
if (exception instanceof YoutubeStreamExtractor.GemaException) {
|
||||
onBlockedByGemaError();
|
||||
} else if (exception instanceof ContentNotAvailableException) {
|
||||
else if (exception instanceof ContentNotAvailableException) {
|
||||
showError(getString(R.string.content_not_available), false);
|
||||
} else {
|
||||
int errorId = exception instanceof YoutubeStreamExtractor.DecryptException
|
||||
|
|
@ -1226,14 +1253,36 @@ public class VideoDetailFragment
|
|||
return true;
|
||||
}
|
||||
|
||||
public void onBlockedByGemaError() {
|
||||
thumbnailBackgroundButton.setOnClickListener((View v) -> {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(getString(R.string.c3s_url)));
|
||||
startActivity(intent);
|
||||
});
|
||||
|
||||
showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema);
|
||||
private void updateProgressInfo(@NonNull final StreamInfo info) {
|
||||
if (positionSubscriber != null) {
|
||||
positionSubscriber.dispose();
|
||||
}
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
final boolean playbackResumeEnabled =
|
||||
prefs.getBoolean(activity.getString(R.string.enable_watch_history_key), true)
|
||||
&& prefs.getBoolean(activity.getString(R.string.enable_playback_resume_key), true);
|
||||
if (!playbackResumeEnabled || info.getDuration() <= 0) {
|
||||
positionView.setVisibility(View.INVISIBLE);
|
||||
detailPositionView.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext());
|
||||
positionSubscriber = recordManager.loadStreamState(info)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.onErrorComplete()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(state -> {
|
||||
final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime());
|
||||
positionView.setMax((int) info.getDuration());
|
||||
positionView.setProgressAnimated(seconds);
|
||||
detailPositionView.setText(Localization.getDurationString(seconds));
|
||||
animateView(positionView, true, 500);
|
||||
animateView(detailPositionView, true, 500);
|
||||
}, e -> {
|
||||
if (DEBUG) e.printStackTrace();
|
||||
}, () -> {
|
||||
animateView(positionView, false, 500);
|
||||
animateView(detailPositionView, false, 500);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
|||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
||||
import java.util.Collections;
|
||||
|
|
@ -64,6 +65,12 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||
infoListAdapter = new InfoListAdapter(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
infoListAdapter.dispose();
|
||||
super.onDetach();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
|
@ -93,6 +100,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||
}
|
||||
updateFlags = 0;
|
||||
}
|
||||
|
||||
itemsList.post(infoListAdapter::updateStates);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -255,6 +264,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||
if (context == null || context.getResources() == null || getActivity() == null) return;
|
||||
|
||||
final String[] commands = new String[]{
|
||||
context.getResources().getString(R.string.direct_on_background),
|
||||
context.getResources().getString(R.string.enqueue_on_background),
|
||||
context.getResources().getString(R.string.enqueue_on_popup),
|
||||
context.getResources().getString(R.string.append_playlist),
|
||||
|
|
@ -264,19 +274,22 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||
switch (i) {
|
||||
case 0:
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
|
||||
NavigationHelper.playOnBackgroundPlayer(context, new SinglePlayQueue(item), true);
|
||||
break;
|
||||
case 1:
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), true);
|
||||
break;
|
||||
case 2:
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item), true);
|
||||
break;
|
||||
case 3:
|
||||
if (getFragmentManager() != null) {
|
||||
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
|
||||
.show(getFragmentManager(), TAG);
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
shareUrl(item.getName(), item.getUrl());
|
||||
case 4:
|
||||
ShareUtils.shareUrl(this.getContext(), item.getName(), item.getUrl());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
|||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
|
@ -169,19 +170,19 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
|
||||
switch (i) {
|
||||
case 0:
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), false);
|
||||
break;
|
||||
case 1:
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item), false);
|
||||
break;
|
||||
case 2:
|
||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
|
||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index), true);
|
||||
break;
|
||||
case 3:
|
||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
|
||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index), true);
|
||||
break;
|
||||
case 4:
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index), true);
|
||||
break;
|
||||
case 5:
|
||||
if (getFragmentManager() != null) {
|
||||
|
|
@ -190,7 +191,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||
}
|
||||
break;
|
||||
case 6:
|
||||
shareUrl(item.getName(), item.getUrl());
|
||||
ShareUtils.shareUrl(this.getContext(), item.getName(), item.getUrl());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
@ -233,10 +234,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||
openRssFeed();
|
||||
break;
|
||||
case R.id.menu_item_openInBrowser:
|
||||
openUrlInBrowser(currentInfo.getOriginalUrl());
|
||||
ShareUtils.openUrlInBrowser(this.getContext(), currentInfo.getOriginalUrl());
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
shareUrl(name, currentInfo.getOriginalUrl());
|
||||
ShareUtils.shareUrl(this.getContext(), name, currentInfo.getOriginalUrl());
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
|
|
@ -439,11 +440,11 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||
monitorSubscription(result);
|
||||
|
||||
headerPlayAllButton.setOnClickListener(
|
||||
view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||
view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
|
||||
headerPopupButton.setOnClickListener(
|
||||
view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
||||
view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||
headerBackgroundButton.setOnClickListener(
|
||||
view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
||||
view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue() {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import org.schabi.newpipe.report.UserAction;
|
|||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -153,22 +154,22 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
|
||||
switch (i) {
|
||||
case 0:
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), false);
|
||||
break;
|
||||
case 1:
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item), false);
|
||||
break;
|
||||
case 2:
|
||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
|
||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index), true);
|
||||
break;
|
||||
case 3:
|
||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
|
||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index), true);
|
||||
break;
|
||||
case 4:
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index), true);
|
||||
break;
|
||||
case 5:
|
||||
shareUrl(item.getName(), item.getUrl());
|
||||
ShareUtils.shareUrl(requireContext(), item.getName(), item.getUrl());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
@ -230,10 +231,10 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_item_openInBrowser:
|
||||
openUrlInBrowser(url);
|
||||
ShareUtils.openUrlInBrowser(this.getContext(), url);
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
shareUrl(name, url);
|
||||
ShareUtils.shareUrl(this.getContext(), name, url);
|
||||
break;
|
||||
case R.id.menu_item_bookmark:
|
||||
onBookmarkClicked();
|
||||
|
|
@ -300,19 +301,19 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||
.subscribe(getPlaylistBookmarkSubscriber());
|
||||
|
||||
headerPlayAllButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
|
||||
headerPopupButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||
headerBackgroundButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
|
||||
headerPopupButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue());
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
|
||||
return true;
|
||||
});
|
||||
|
||||
headerBackgroundButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue());
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ package org.schabi.newpipe.info_list;
|
|||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
|
|
@ -59,13 +61,14 @@ public class InfoItemBuilder {
|
|||
this.context = context;
|
||||
}
|
||||
|
||||
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem) {
|
||||
return buildView(parent, infoItem, false);
|
||||
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, @Nullable StreamStateEntity state) {
|
||||
return buildView(parent, infoItem, state, false);
|
||||
}
|
||||
|
||||
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, boolean useMiniVariant) {
|
||||
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem,
|
||||
@Nullable StreamStateEntity state, boolean useMiniVariant) {
|
||||
InfoItemHolder holder = holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
|
||||
holder.updateFromItem(infoItem);
|
||||
holder.updateFromItem(infoItem, state);
|
||||
return holder.itemView;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,25 @@
|
|||
package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
||||
|
|
@ -50,7 +53,7 @@ import java.util.List;
|
|||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
public class InfoListAdapter extends StateObjectsListAdapter {
|
||||
private static final String TAG = InfoListAdapter.class.getSimpleName();
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
|
|
@ -87,6 +90,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||
}
|
||||
|
||||
public InfoListAdapter(Activity a) {
|
||||
super(a.getApplicationContext());
|
||||
infoItemBuilder = new InfoItemBuilder(a);
|
||||
infoItemList = new ArrayList<>();
|
||||
}
|
||||
|
|
@ -115,50 +119,64 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||
this.useGridVariant = useGridVariant;
|
||||
}
|
||||
|
||||
public void addInfoItemList(List<InfoItem> data) {
|
||||
public void addInfoItemList(@Nullable final List<InfoItem> data) {
|
||||
if (data != null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + infoItemList.size() + ", data.size() = " + data.size());
|
||||
}
|
||||
|
||||
int offsetStart = sizeConsideringHeaderOffset();
|
||||
infoItemList.addAll(data);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
|
||||
}
|
||||
|
||||
notifyItemRangeInserted(offsetStart, data.size());
|
||||
|
||||
if (footer != null && showFooter) {
|
||||
int footerNow = sizeConsideringHeaderOffset();
|
||||
notifyItemMoved(offsetStart, footerNow);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart + " to " + footerNow);
|
||||
}
|
||||
loadStates(data, infoItemList.size(), () -> addInfoItemListImpl(data));
|
||||
}
|
||||
}
|
||||
|
||||
public void addInfoItem(InfoItem data) {
|
||||
private void addInfoItemListImpl(@NonNull List<InfoItem> data) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + infoItemList.size() + ", data.size() = " + data.size());
|
||||
}
|
||||
|
||||
int offsetStart = sizeConsideringHeaderOffset();
|
||||
infoItemList.addAll(data);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
|
||||
}
|
||||
|
||||
notifyItemRangeInserted(offsetStart, data.size());
|
||||
|
||||
if (footer != null && showFooter) {
|
||||
int footerNow = sizeConsideringHeaderOffset();
|
||||
notifyItemMoved(offsetStart, footerNow);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart + " to " + footerNow);
|
||||
}
|
||||
}
|
||||
|
||||
public void addInfoItem(@Nullable InfoItem data) {
|
||||
if (data != null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + infoItemList.size() + ", thread = " + Thread.currentThread());
|
||||
}
|
||||
loadState(data, infoItemList.size(), () -> addInfoItemImpl(data));
|
||||
}
|
||||
}
|
||||
|
||||
int positionInserted = sizeConsideringHeaderOffset();
|
||||
infoItemList.add(data);
|
||||
private void addInfoItemImpl(@NonNull InfoItem data) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + infoItemList.size() + ", thread = " + Thread.currentThread());
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
|
||||
}
|
||||
notifyItemInserted(positionInserted);
|
||||
int positionInserted = sizeConsideringHeaderOffset();
|
||||
infoItemList.add(data);
|
||||
|
||||
if (footer != null && showFooter) {
|
||||
int footerNow = sizeConsideringHeaderOffset();
|
||||
notifyItemMoved(positionInserted, footerNow);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
|
||||
}
|
||||
notifyItemInserted(positionInserted);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted + " to " + footerNow);
|
||||
}
|
||||
if (footer != null && showFooter) {
|
||||
int footerNow = sizeConsideringHeaderOffset();
|
||||
notifyItemMoved(positionInserted, footerNow);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted + " to " + footerNow);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateStates() {
|
||||
if (!infoItemList.isEmpty()) {
|
||||
updateAllStates(infoItemList);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -167,6 +185,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||
return;
|
||||
}
|
||||
infoItemList.clear();
|
||||
clearStates();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
|
|
@ -240,8 +259,9 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int type) {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onCreateViewHolder() called with: parent = [" + parent + "], type = [" + type + "]");
|
||||
switch (type) {
|
||||
|
|
@ -278,13 +298,13 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
if (DEBUG) Log.d(TAG, "onBindViewHolder() called with: holder = [" + holder.getClass().getSimpleName() + "], position = [" + position + "]");
|
||||
if (holder instanceof InfoItemHolder) {
|
||||
// If header isn't null, offset the items by -1
|
||||
if (header != null) position--;
|
||||
|
||||
((InfoItemHolder) holder).updateFromItem(infoItemList.get(position));
|
||||
((InfoItemHolder) holder).updateFromItem(infoItemList.get(position), getState(position));
|
||||
} else if (holder instanceof HFHolder && position == 0 && header != null) {
|
||||
((HFHolder) holder).view = header;
|
||||
} else if (holder instanceof HFHolder && position == sizeConsideringHeaderOffset() && footer != null && showFooter) {
|
||||
|
|
@ -292,6 +312,28 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
if (!payloads.isEmpty() && holder instanceof InfoItemHolder) {
|
||||
for (Object payload : payloads) {
|
||||
if (payload instanceof StreamStateEntity) {
|
||||
((InfoItemHolder) holder).updateState(infoItemList.get(header == null ? position : position - 1),
|
||||
(StreamStateEntity) payload);
|
||||
} else if (payload instanceof Boolean) {
|
||||
((InfoItemHolder) holder).updateState(infoItemList.get(header == null ? position : position - 1),
|
||||
null);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onBindViewHolder(holder, position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onItemStateChanged(int position, @Nullable StreamStateEntity state) {
|
||||
notifyItemChanged(header == null ? position : position + 1, state != null ? state : false);
|
||||
}
|
||||
|
||||
public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) {
|
||||
return new GridLayoutManager.SpanSizeLookup() {
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -0,0 +1,189 @@
|
|||
package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.SparseArrayUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
|
||||
public abstract class StateObjectsListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
private final SparseArray<StreamStateEntity> states;
|
||||
private final HistoryRecordManager recordManager;
|
||||
private final CompositeDisposable stateLoaders;
|
||||
private final Context context;
|
||||
|
||||
public StateObjectsListAdapter(Context context) {
|
||||
this.states = new SparseArray<>();
|
||||
this.recordManager = new HistoryRecordManager(context);
|
||||
this.context = context;
|
||||
this.stateLoaders = new CompositeDisposable();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public StreamStateEntity getState(int position) {
|
||||
return states.get(position);
|
||||
}
|
||||
|
||||
protected void clearStates() {
|
||||
states.clear();
|
||||
}
|
||||
|
||||
private void appendStates(List<StreamStateEntity> statesEntities, int offset) {
|
||||
for (int i = 0; i < statesEntities.size(); i++) {
|
||||
final StreamStateEntity state = statesEntities.get(i);
|
||||
if (state != null) {
|
||||
states.append(offset + i, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void appendState(StreamStateEntity statesEntity, int offset) {
|
||||
if (statesEntity != null) {
|
||||
states.append(offset, statesEntity);
|
||||
}
|
||||
}
|
||||
|
||||
protected void removeState(int index) {
|
||||
states.remove(index);
|
||||
}
|
||||
|
||||
protected void moveState(int from, int to) {
|
||||
final StreamStateEntity item = states.get(from);
|
||||
if (from < to) {
|
||||
SparseArrayUtils.shiftItemsDown(states, from, to);
|
||||
} else {
|
||||
SparseArrayUtils.shiftItemsUp(states, to, from);
|
||||
}
|
||||
states.put(to, item);
|
||||
}
|
||||
|
||||
protected void loadStates(List<InfoItem> list, int offset, Runnable callback) {
|
||||
if (isPlaybackStatesVisible()) {
|
||||
stateLoaders.add(
|
||||
recordManager.loadStreamStateBatch(list)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(streamStateEntities -> {
|
||||
appendStates(streamStateEntities, offset);
|
||||
callback.run();
|
||||
}, throwable -> {
|
||||
if (BuildConfig.DEBUG) throwable.printStackTrace();
|
||||
callback.run();
|
||||
})
|
||||
);
|
||||
} else {
|
||||
callback.run();
|
||||
}
|
||||
}
|
||||
|
||||
protected void loadState(InfoItem item, int offset, Runnable callback) {
|
||||
if (isPlaybackStatesVisible()) {
|
||||
stateLoaders.add(
|
||||
recordManager.loadStreamState(item)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(streamStateEntities -> {
|
||||
appendState(streamStateEntities[0], offset);
|
||||
callback.run();
|
||||
}, throwable -> {
|
||||
if (BuildConfig.DEBUG) throwable.printStackTrace();
|
||||
callback.run();
|
||||
})
|
||||
);
|
||||
} else {
|
||||
callback.run();
|
||||
}
|
||||
}
|
||||
|
||||
protected void loadStatesForLocal(List<? extends LocalItem> list, int offset, Runnable callback) {
|
||||
if (isPlaybackStatesVisible()) {
|
||||
stateLoaders.add(
|
||||
recordManager.loadLocalStreamStateBatch(list)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(streamStateEntities -> {
|
||||
appendStates(streamStateEntities, offset);
|
||||
callback.run();
|
||||
}, throwable -> {
|
||||
if (BuildConfig.DEBUG) throwable.printStackTrace();
|
||||
callback.run();
|
||||
})
|
||||
);
|
||||
} else {
|
||||
callback.run();
|
||||
}
|
||||
}
|
||||
|
||||
private void processStatesUpdates(List<StreamStateEntity> streamStateEntities) {
|
||||
for (int i = 0; i < streamStateEntities.size(); i++) {
|
||||
final StreamStateEntity newState = streamStateEntities.get(i);
|
||||
if (!Objects.equals(states.get(i), newState)) {
|
||||
if (newState == null) {
|
||||
states.remove(i);
|
||||
} else {
|
||||
states.put(i, newState);
|
||||
}
|
||||
onItemStateChanged(i, newState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateAllStates(List<InfoItem> list) {
|
||||
if (isPlaybackStatesVisible()) {
|
||||
stateLoaders.add(
|
||||
recordManager.loadStreamStateBatch(list)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::processStatesUpdates, throwable -> {
|
||||
if (BuildConfig.DEBUG) throwable.printStackTrace();
|
||||
})
|
||||
);
|
||||
} else {
|
||||
final int[] positions = SparseArrayUtils.getKeys(states);
|
||||
states.clear();
|
||||
for (int pos : positions) onItemStateChanged(pos, null);
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateAllLocalStates(List<? extends LocalItem> list) {
|
||||
if (isPlaybackStatesVisible()) {
|
||||
stateLoaders.add(
|
||||
recordManager.loadLocalStreamStateBatch(list)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::processStatesUpdates, throwable -> {
|
||||
if (BuildConfig.DEBUG) throwable.printStackTrace();
|
||||
})
|
||||
);
|
||||
} else {
|
||||
final int[] positions = SparseArrayUtils.getKeys(states);
|
||||
states.clear();
|
||||
for (int pos : positions) onItemStateChanged(pos, null);
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
stateLoaders.dispose();
|
||||
}
|
||||
|
||||
protected boolean isPlaybackStatesVisible() {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)
|
||||
&& prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true)
|
||||
&& prefs.getBoolean(context.getString(R.string.enable_playback_state_lists_key), true);
|
||||
}
|
||||
|
||||
protected abstract void onItemStateChanged(int position, @Nullable StreamStateEntity state);
|
||||
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
|
|
@ -38,8 +40,8 @@ public class ChannelInfoItemHolder extends ChannelMiniInfoItemHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem) {
|
||||
super.updateFromItem(infoItem);
|
||||
public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) {
|
||||
super.updateFromItem(infoItem, state);
|
||||
|
||||
if (!(infoItem instanceof ChannelInfoItem)) return;
|
||||
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
|
|
@ -30,7 +32,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem) {
|
||||
public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) {
|
||||
if (!(infoItem instanceof ChannelInfoItem)) return;
|
||||
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 12.02.17.
|
||||
|
|
@ -41,8 +41,8 @@ public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem) {
|
||||
super.updateFromItem(infoItem);
|
||||
public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) {
|
||||
super.updateFromItem(infoItem, state);
|
||||
|
||||
if (!(infoItem instanceof CommentsInfoItem)) return;
|
||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.text.util.Linkify;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.jsoup.helper.StringUtil;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
|
|
@ -45,7 +48,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
|||
if(hours != null) timestamp += (Integer.parseInt(hours.replace(":", ""))*3600);
|
||||
if(minutes != null) timestamp += (Integer.parseInt(minutes.replace(":", ""))*60);
|
||||
if(seconds != null) timestamp += (Integer.parseInt(seconds));
|
||||
return streamUrl + url.replace(match.group(0), "&t=" + String.valueOf(timestamp));
|
||||
return streamUrl + url.replace(match.group(0), "#timestamp=" + String.valueOf(timestamp));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -64,7 +67,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem) {
|
||||
public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) {
|
||||
if (!(infoItem instanceof CommentsInfoItem)) return;
|
||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||
|
||||
|
|
@ -76,6 +79,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
|||
itemThumbnailView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if(StringUtil.isBlank(item.getAuthorEndpoint())) return;
|
||||
try {
|
||||
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
|
||||
NavigationHelper.openChannelFragment(
|
||||
|
|
@ -91,15 +95,14 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
|||
|
||||
streamUrl = item.getUrl();
|
||||
|
||||
itemContentView.setMaxLines(commentDefaultLines);
|
||||
itemContentView.setLines(commentDefaultLines);
|
||||
commentText = item.getCommentText();
|
||||
itemContentView.setText(commentText);
|
||||
linkify();
|
||||
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
|
||||
|
||||
if(itemContentView.getLineCount() == 0){
|
||||
if (itemContentView.getLineCount() == 0) {
|
||||
itemContentView.post(() -> ellipsize());
|
||||
}else{
|
||||
} else {
|
||||
ellipsize();
|
||||
}
|
||||
|
||||
|
|
@ -119,15 +122,17 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
|||
private void ellipsize() {
|
||||
if (itemContentView.getLineCount() > commentDefaultLines){
|
||||
int endOfLastLine = itemContentView.getLayout().getLineEnd(commentDefaultLines - 1);
|
||||
String newVal = itemContentView.getText().subSequence(0, endOfLastLine - 3) + "...";
|
||||
int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine -2);
|
||||
if(end == -1) end = Math.max(endOfLastLine -2, 0);
|
||||
String newVal = itemContentView.getText().subSequence(0, end) + " …";
|
||||
itemContentView.setText(newVal);
|
||||
linkify();
|
||||
}
|
||||
linkify();
|
||||
}
|
||||
|
||||
private void toggleEllipsize() {
|
||||
if (itemContentView.getText().toString().equals(commentText)) {
|
||||
ellipsize();
|
||||
if (itemContentView.getLineCount() > commentDefaultLines) ellipsize();
|
||||
} else {
|
||||
expand();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
|
||||
|
|
@ -35,5 +37,8 @@ public abstract class InfoItemHolder extends RecyclerView.ViewHolder {
|
|||
this.itemBuilder = infoItemBuilder;
|
||||
}
|
||||
|
||||
public abstract void updateFromItem(final InfoItem infoItem);
|
||||
public abstract void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state);
|
||||
|
||||
public void updateState(final InfoItem infoItem, @Nullable final StreamStateEntity state) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
|
|
@ -30,7 +32,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem) {
|
||||
public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) {
|
||||
if (!(infoItem instanceof PlaylistInfoItem)) return;
|
||||
final PlaylistInfoItem item = (PlaylistInfoItem) infoItem;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
|
|
@ -40,8 +42,8 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem) {
|
||||
super.updateFromItem(infoItem);
|
||||
public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) {
|
||||
super.updateFromItem(infoItem, state);
|
||||
|
||||
if (!(infoItem instanceof StreamInfoItem)) return;
|
||||
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
|
@ -7,12 +8,17 @@ import android.widget.ImageView;
|
|||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
|
||||
|
|
@ -20,6 +26,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||
public final TextView itemVideoTitleView;
|
||||
public final TextView itemUploaderView;
|
||||
public final TextView itemDurationView;
|
||||
public final AnimatedProgressBar itemProgressView;
|
||||
|
||||
StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
|
||||
super(infoItemBuilder, layoutId, parent);
|
||||
|
|
@ -28,6 +35,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||
itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView);
|
||||
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
|
||||
itemDurationView = itemView.findViewById(R.id.itemDurationView);
|
||||
itemProgressView = itemView.findViewById(R.id.itemProgressView);
|
||||
}
|
||||
|
||||
public StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
|
|
@ -35,7 +43,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem) {
|
||||
public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) {
|
||||
if (!(infoItem instanceof StreamInfoItem)) return;
|
||||
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
||||
|
||||
|
|
@ -47,13 +55,22 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||
R.color.duration_background_color));
|
||||
itemDurationView.setVisibility(View.VISIBLE);
|
||||
if (state != null) {
|
||||
itemProgressView.setVisibility(View.VISIBLE);
|
||||
itemProgressView.setMax((int) item.getDuration());
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
} else if (item.getStreamType() == StreamType.LIVE_STREAM) {
|
||||
itemDurationView.setText(R.string.duration_live);
|
||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||
R.color.live_duration_background_color));
|
||||
itemDurationView.setVisibility(View.VISIBLE);
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
} else {
|
||||
itemDurationView.setVisibility(View.GONE);
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||
|
|
@ -83,6 +100,22 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateState(final InfoItem infoItem, @Nullable final StreamStateEntity state) {
|
||||
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
||||
if (state != null && item.getDuration() > 0 && item.getStreamType() != StreamType.LIVE_STREAM) {
|
||||
itemProgressView.setMax((int) item.getDuration());
|
||||
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||
AnimationUtils.animateView(itemProgressView, true, 500);
|
||||
}
|
||||
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
AnimationUtils.animateView(itemProgressView, false, 500);
|
||||
}
|
||||
}
|
||||
|
||||
private void enableLongClick(final StreamInfoItem item) {
|
||||
itemView.setLongClickable(true);
|
||||
itemView.setOnLongClickListener(view -> {
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
|||
}
|
||||
updateFlags = 0;
|
||||
}
|
||||
|
||||
itemsList.post(itemListAdapter::updateStates);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -150,6 +152,7 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
|||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
itemsList = null;
|
||||
itemListAdapter.dispose();
|
||||
itemListAdapter = null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package org.schabi.newpipe.local;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
|
|
@ -8,6 +10,8 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.info_list.StateObjectsListAdapter;
|
||||
import org.schabi.newpipe.local.holder.LocalItemHolder;
|
||||
import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder;
|
||||
import org.schabi.newpipe.local.holder.LocalPlaylistItemHolder;
|
||||
|
|
@ -45,7 +49,7 @@ import java.util.List;
|
|||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
public class LocalItemListAdapter extends StateObjectsListAdapter {
|
||||
|
||||
private static final String TAG = LocalItemListAdapter.class.getSimpleName();
|
||||
private static final boolean DEBUG = false;
|
||||
|
|
@ -72,6 +76,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||
private View footer = null;
|
||||
|
||||
public LocalItemListAdapter(Activity activity) {
|
||||
super(activity.getApplicationContext());
|
||||
localItemBuilder = new LocalItemBuilder(activity);
|
||||
localItems = new ArrayList<>();
|
||||
dateFormat = DateFormat.getDateInstance(DateFormat.SHORT,
|
||||
|
|
@ -86,39 +91,49 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||
localItemBuilder.setOnItemSelectedListener(null);
|
||||
}
|
||||
|
||||
public void addItems(List<? extends LocalItem> data) {
|
||||
public void addItems(@Nullable List<? extends LocalItem> data) {
|
||||
if (data != null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addItems() before > localItems.size() = " +
|
||||
localItems.size() + ", data.size() = " + data.size());
|
||||
}
|
||||
loadStatesForLocal(data, localItems.size(), () -> addItemsImpl(data));
|
||||
}
|
||||
}
|
||||
|
||||
int offsetStart = sizeConsideringHeader();
|
||||
localItems.addAll(data);
|
||||
private void addItemsImpl(@NonNull List<? extends LocalItem> data) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addItems() before > localItems.size() = " +
|
||||
localItems.size() + ", data.size() = " + data.size());
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addItems() after > offsetStart = " + offsetStart +
|
||||
", localItems.size() = " + localItems.size() +
|
||||
", header = " + header + ", footer = " + footer +
|
||||
", showFooter = " + showFooter);
|
||||
}
|
||||
int offsetStart = sizeConsideringHeader();
|
||||
localItems.addAll(data);
|
||||
|
||||
notifyItemRangeInserted(offsetStart, data.size());
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addItems() after > offsetStart = " + offsetStart +
|
||||
", localItems.size() = " + localItems.size() +
|
||||
", header = " + header + ", footer = " + footer +
|
||||
", showFooter = " + showFooter);
|
||||
}
|
||||
|
||||
if (footer != null && showFooter) {
|
||||
int footerNow = sizeConsideringHeader();
|
||||
notifyItemMoved(offsetStart, footerNow);
|
||||
notifyItemRangeInserted(offsetStart, data.size());
|
||||
|
||||
if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart +
|
||||
" to " + footerNow);
|
||||
}
|
||||
if (footer != null && showFooter) {
|
||||
int footerNow = sizeConsideringHeader();
|
||||
notifyItemMoved(offsetStart, footerNow);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart +
|
||||
" to " + footerNow);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateStates() {
|
||||
if (!localItems.isEmpty()) {
|
||||
updateAllLocalStates(localItems);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeItem(final LocalItem data) {
|
||||
final int index = localItems.indexOf(data);
|
||||
|
||||
localItems.remove(index);
|
||||
removeState(index);
|
||||
notifyItemRemoved(index + (header != null ? 1 : 0));
|
||||
}
|
||||
|
||||
|
|
@ -130,6 +145,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||
if (actualFrom >= localItems.size() || actualTo >= localItems.size()) return false;
|
||||
|
||||
localItems.add(actualTo, localItems.remove(actualFrom));
|
||||
moveState(actualFrom, actualTo);
|
||||
notifyItemMoved(fromAdapterPosition, toAdapterPosition);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -139,6 +155,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||
return;
|
||||
}
|
||||
localItems.clear();
|
||||
clearStates();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
|
|
@ -259,7 +276,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||
// If header isn't null, offset the items by -1
|
||||
if (header != null) position--;
|
||||
|
||||
((LocalItemHolder) holder).updateFromItem(localItems.get(position), dateFormat);
|
||||
((LocalItemHolder) holder).updateFromItem(localItems.get(position), getState(position), dateFormat);
|
||||
} else if (holder instanceof HeaderFooterHolder && position == 0 && header != null) {
|
||||
((HeaderFooterHolder) holder).view = header;
|
||||
} else if (holder instanceof HeaderFooterHolder && position == sizeConsideringHeader()
|
||||
|
|
@ -268,6 +285,28 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
if (!payloads.isEmpty() && holder instanceof LocalItemHolder) {
|
||||
for (Object payload : payloads) {
|
||||
if (payload instanceof StreamStateEntity) {
|
||||
((LocalItemHolder) holder).updateState(localItems.get(header == null ? position : position - 1),
|
||||
(StreamStateEntity) payload);
|
||||
} else if (payload instanceof Boolean) {
|
||||
((LocalItemHolder) holder).updateState(localItems.get(header == null ? position : position - 1),
|
||||
null);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onBindViewHolder(holder, position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onItemStateChanged(int position, @Nullable StreamStateEntity state) {
|
||||
notifyItemChanged(header == null ? position : position + 1, state != null ? state : false);
|
||||
}
|
||||
|
||||
public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) {
|
||||
return new GridLayoutManager.SpanSizeLookup() {
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -112,7 +112,10 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
|||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
if (playlistReactor != null) playlistReactor.dispose();
|
||||
if (playlistAdapter != null) playlistAdapter.unsetSelectedListener();
|
||||
if (playlistAdapter != null) {
|
||||
playlistAdapter.dispose();
|
||||
playlistAdapter.unsetSelectedListener();
|
||||
}
|
||||
|
||||
playlistReactor = null;
|
||||
playlistRecyclerView = null;
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ import org.schabi.newpipe.extractor.NewPipe;
|
|||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionService;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
|
|
@ -262,7 +262,7 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
|
|||
* If chosen feed already displayed, then we request another feed from another
|
||||
* subscription, until the subscription table runs out of new items.
|
||||
* <p>
|
||||
* This Observer is self-contained and will dispose itself when complete. However, this
|
||||
* This Observer is self-contained and will close itself when complete. However, this
|
||||
* does not obey the fragment lifecycle and may continue running in the background
|
||||
* until it is complete. This is done due to RxJava2 no longer propagate errors once
|
||||
* an observer is unsubscribed while the thread process is still running.
|
||||
|
|
|
|||
|
|
@ -26,23 +26,29 @@ import android.support.annotation.NonNull;
|
|||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.AppDatabase;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
||||
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
import org.schabi.newpipe.database.stream.dao.StreamDAO;
|
||||
import org.schabi.newpipe.database.stream.dao.StreamStateDAO;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Maybe;
|
||||
import io.reactivex.Single;
|
||||
|
|
@ -80,9 +86,9 @@ public class HistoryRecordManager {
|
|||
final Date currentTime = new Date();
|
||||
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
||||
final long streamId = streamTable.upsert(new StreamEntity(info));
|
||||
StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry();
|
||||
StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
|
||||
|
||||
if (latestEntry != null && latestEntry.getStreamUid() == streamId) {
|
||||
if (latestEntry != null) {
|
||||
streamHistoryTable.delete(latestEntry);
|
||||
latestEntry.setAccessDate(currentTime);
|
||||
latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1);
|
||||
|
|
@ -99,7 +105,7 @@ public class HistoryRecordManager {
|
|||
}
|
||||
|
||||
public Single<Integer> deleteWholeStreamHistory() {
|
||||
return Single.fromCallable(() -> streamHistoryTable.deleteAll())
|
||||
return Single.fromCallable(streamHistoryTable::deleteAll)
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +166,7 @@ public class HistoryRecordManager {
|
|||
}
|
||||
|
||||
public Single<Integer> deleteWholeSearchHistory() {
|
||||
return Single.fromCallable(() -> searchHistoryTable.deleteAll())
|
||||
return Single.fromCallable(searchHistoryTable::deleteAll)
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
|
|
@ -180,21 +186,104 @@ public class HistoryRecordManager {
|
|||
// Stream State History
|
||||
///////////////////////////////////////////////////////
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Maybe<StreamStateEntity> loadStreamState(final StreamInfo info) {
|
||||
return Maybe.fromCallable(() -> streamTable.upsert(new StreamEntity(info)))
|
||||
.flatMap(streamId -> streamStateTable.getState(streamId).firstElement())
|
||||
.flatMap(states -> states.isEmpty() ? Maybe.empty() : Maybe.just(states.get(0)))
|
||||
public Maybe<StreamHistoryEntity> getStreamHistory(final StreamInfo info) {
|
||||
return Maybe.fromCallable(() -> {
|
||||
final long streamId = streamTable.upsert(new StreamEntity(info));
|
||||
return streamHistoryTable.getLatestEntry(streamId);
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Maybe<StreamStateEntity> loadStreamState(final PlayQueueItem queueItem) {
|
||||
return queueItem.getStream()
|
||||
.map((info) -> streamTable.upsert(new StreamEntity(info)))
|
||||
.flatMapPublisher(streamStateTable::getState)
|
||||
.firstElement()
|
||||
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
|
||||
.filter(state -> state.isValid((int) queueItem.getDuration()))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Maybe<Long> saveStreamState(@NonNull final StreamInfo info, final long progressTime) {
|
||||
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
||||
public Maybe<StreamStateEntity> loadStreamState(final StreamInfo info) {
|
||||
return Single.fromCallable(() -> streamTable.upsert(new StreamEntity(info)))
|
||||
.flatMapPublisher(streamStateTable::getState)
|
||||
.firstElement()
|
||||
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
|
||||
.filter(state -> state.isValid((int) info.getDuration()))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Completable saveStreamState(@NonNull final StreamInfo info, final long progressTime) {
|
||||
return Completable.fromAction(() -> database.runInTransaction(() -> {
|
||||
final long streamId = streamTable.upsert(new StreamEntity(info));
|
||||
return streamStateTable.upsert(new StreamStateEntity(streamId, progressTime));
|
||||
final StreamStateEntity state = new StreamStateEntity(streamId, progressTime);
|
||||
if (state.isValid((int) info.getDuration())) {
|
||||
streamStateTable.upsert(state);
|
||||
} else {
|
||||
streamStateTable.deleteState(streamId);
|
||||
}
|
||||
})).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<StreamStateEntity[]> loadStreamState(final InfoItem info) {
|
||||
return Single.fromCallable(() -> {
|
||||
final List<StreamEntity> entities = streamTable.getStream(info.getServiceId(), info.getUrl()).blockingFirst();
|
||||
if (entities.isEmpty()) {
|
||||
return new StreamStateEntity[]{null};
|
||||
}
|
||||
final List<StreamStateEntity> states = streamStateTable.getState(entities.get(0).getUid()).blockingFirst();
|
||||
if (states.isEmpty()) {
|
||||
return new StreamStateEntity[]{null};
|
||||
}
|
||||
return new StreamStateEntity[]{states.get(0)};
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<List<StreamStateEntity>> loadStreamStateBatch(final List<InfoItem> infos) {
|
||||
return Single.fromCallable(() -> {
|
||||
final List<StreamStateEntity> result = new ArrayList<>(infos.size());
|
||||
for (InfoItem info : infos) {
|
||||
final List<StreamEntity> entities = streamTable.getStream(info.getServiceId(), info.getUrl()).blockingFirst();
|
||||
if (entities.isEmpty()) {
|
||||
result.add(null);
|
||||
continue;
|
||||
}
|
||||
final List<StreamStateEntity> states = streamStateTable.getState(entities.get(0).getUid()).blockingFirst();
|
||||
if (states.isEmpty()) {
|
||||
result.add(null);
|
||||
continue;
|
||||
}
|
||||
result.add(states.get(0));
|
||||
}
|
||||
return result;
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<List<StreamStateEntity>> loadLocalStreamStateBatch(final List<? extends LocalItem> items) {
|
||||
return Single.fromCallable(() -> {
|
||||
final List<StreamStateEntity> result = new ArrayList<>(items.size());
|
||||
for (LocalItem item : items) {
|
||||
long streamId;
|
||||
if (item instanceof StreamStatisticsEntry) {
|
||||
streamId = ((StreamStatisticsEntry) item).streamId;
|
||||
} else if (item instanceof PlaylistStreamEntity) {
|
||||
streamId = ((PlaylistStreamEntity) item).getStreamUid();
|
||||
} else if (item instanceof PlaylistStreamEntry) {
|
||||
streamId = ((PlaylistStreamEntry) item).streamId;
|
||||
} else {
|
||||
result.add(null);
|
||||
continue;
|
||||
}
|
||||
final List<StreamStateEntity> states = streamStateTable.getState(streamId).blockingFirst();
|
||||
if (states.isEmpty()) {
|
||||
result.add(null);
|
||||
continue;
|
||||
}
|
||||
result.add(states.get(0));
|
||||
}
|
||||
return result;
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////
|
||||
// Utility
|
||||
///////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import org.schabi.newpipe.report.UserAction;
|
|||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -309,11 +310,11 @@ public class StatisticsPlaylistFragment
|
|||
}
|
||||
|
||||
headerPlayAllButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
|
||||
headerPopupButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||
headerBackgroundButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
sortButton.setOnClickListener(view -> toggleSortMode());
|
||||
|
||||
hideLoading();
|
||||
|
|
@ -376,25 +377,25 @@ public class StatisticsPlaylistFragment
|
|||
final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0);
|
||||
switch (i) {
|
||||
case 0:
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(infoItem));
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(infoItem), false);
|
||||
break;
|
||||
case 1:
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(infoItem));
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(infoItem), false);
|
||||
break;
|
||||
case 2:
|
||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
|
||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index), true);
|
||||
break;
|
||||
case 3:
|
||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
|
||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index), true);
|
||||
break;
|
||||
case 4:
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index), true);
|
||||
break;
|
||||
case 5:
|
||||
deleteEntry(index);
|
||||
break;
|
||||
case 6:
|
||||
shareUrl(item.toStreamInfoItem().getName(), item.toStreamInfoItem().getUrl());
|
||||
ShareUtils.shareUrl(this.getContext(), item.toStreamInfoItem().getName(), item.toStreamInfoItem().getUrl());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
package org.schabi.newpipe.local.holder;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
|
||||
import java.text.DateFormat;
|
||||
|
|
@ -38,5 +40,8 @@ public abstract class LocalItemHolder extends RecyclerView.ViewHolder {
|
|||
this.itemBuilder = itemBuilder;
|
||||
}
|
||||
|
||||
public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat);
|
||||
public abstract void updateFromItem(final LocalItem item, @Nullable final StreamStateEntity state, final DateFormat dateFormat);
|
||||
|
||||
public void updateState(final LocalItem localItem, @Nullable final StreamStateEntity state) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
package org.schabi.newpipe.local.holder;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
|
||||
|
|
@ -21,7 +23,7 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
||||
public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) {
|
||||
if (!(localItem instanceof PlaylistMetadataEntry)) return;
|
||||
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
|
||||
|
||||
|
|
@ -32,6 +34,6 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
|
|||
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
||||
|
||||
super.updateFromItem(localItem, dateFormat);
|
||||
super.updateFromItem(localItem, state, dateFormat);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package org.schabi.newpipe.local.holder;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
|
@ -10,12 +11,16 @@ import android.widget.TextView;
|
|||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
|
||||
|
|
@ -24,6 +29,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
|||
public final TextView itemAdditionalDetailsView;
|
||||
public final TextView itemDurationView;
|
||||
public final View itemHandleView;
|
||||
public final AnimatedProgressBar itemProgressView;
|
||||
|
||||
LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
|
||||
super(infoItemBuilder, layoutId, parent);
|
||||
|
|
@ -33,6 +39,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
|||
itemAdditionalDetailsView = itemView.findViewById(R.id.itemAdditionalDetails);
|
||||
itemDurationView = itemView.findViewById(R.id.itemDurationView);
|
||||
itemHandleView = itemView.findViewById(R.id.itemHandle);
|
||||
itemProgressView = itemView.findViewById(R.id.itemProgressView);
|
||||
}
|
||||
|
||||
public LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
|
|
@ -40,7 +47,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
||||
public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) {
|
||||
if (!(localItem instanceof PlaylistStreamEntry)) return;
|
||||
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
|
||||
|
||||
|
|
@ -53,6 +60,13 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
|||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||
R.color.duration_background_color));
|
||||
itemDurationView.setVisibility(View.VISIBLE);
|
||||
if (state != null) {
|
||||
itemProgressView.setVisibility(View.VISIBLE);
|
||||
itemProgressView.setMax((int) item.duration);
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
itemDurationView.setVisibility(View.GONE);
|
||||
}
|
||||
|
|
@ -79,6 +93,23 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
|||
itemHandleView.setOnTouchListener(getOnTouchListener(item));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateState(LocalItem localItem, @Nullable StreamStateEntity state) {
|
||||
if (!(localItem instanceof PlaylistStreamEntry)) return;
|
||||
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
|
||||
if (state != null && item.duration > 0) {
|
||||
itemProgressView.setMax((int) item.duration);
|
||||
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||
AnimationUtils.animateView(itemProgressView, true, 500);
|
||||
}
|
||||
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
AnimationUtils.animateView(itemProgressView, false, 500);
|
||||
}
|
||||
}
|
||||
|
||||
private View.OnTouchListener getOnTouchListener(final PlaylistStreamEntry item) {
|
||||
return (view, motionEvent) -> {
|
||||
view.performClick();
|
||||
|
|
|
|||
|
|
@ -10,12 +10,16 @@ import android.widget.TextView;
|
|||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 01.08.16.
|
||||
|
|
@ -45,6 +49,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
|||
public final TextView itemDurationView;
|
||||
@Nullable
|
||||
public final TextView itemAdditionalDetails;
|
||||
public final AnimatedProgressBar itemProgressView;
|
||||
|
||||
public LocalStatisticStreamItemHolder(LocalItemBuilder itemBuilder, ViewGroup parent) {
|
||||
this(itemBuilder, R.layout.list_stream_item, parent);
|
||||
|
|
@ -58,6 +63,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
|||
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
|
||||
itemDurationView = itemView.findViewById(R.id.itemDurationView);
|
||||
itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails);
|
||||
itemProgressView = itemView.findViewById(R.id.itemProgressView);
|
||||
}
|
||||
|
||||
private String getStreamInfoDetailLine(final StreamStatisticsEntry entry,
|
||||
|
|
@ -70,7 +76,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
||||
public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) {
|
||||
if (!(localItem instanceof StreamStatisticsEntry)) return;
|
||||
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
|
||||
|
||||
|
|
@ -82,8 +88,16 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
|||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||
R.color.duration_background_color));
|
||||
itemDurationView.setVisibility(View.VISIBLE);
|
||||
if (state != null) {
|
||||
itemProgressView.setVisibility(View.VISIBLE);
|
||||
itemProgressView.setMax((int) item.duration);
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
itemDurationView.setVisibility(View.GONE);
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (itemAdditionalDetails != null) {
|
||||
|
|
@ -108,4 +122,21 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
|||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateState(LocalItem localItem, @Nullable StreamStateEntity state) {
|
||||
if (!(localItem instanceof StreamStatisticsEntry)) return;
|
||||
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
|
||||
if (state != null && item.duration > 0) {
|
||||
itemProgressView.setMax((int) item.duration);
|
||||
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||
AnimationUtils.animateView(itemProgressView, true, 500);
|
||||
}
|
||||
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
AnimationUtils.animateView(itemProgressView, false, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package org.schabi.newpipe.local.holder;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
|
||||
import java.text.DateFormat;
|
||||
|
|
@ -31,7 +33,7 @@ public abstract class PlaylistItemHolder extends LocalItemHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
||||
public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) {
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||
itemBuilder.getOnItemSelectedListener().selected(localItem);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package org.schabi.newpipe.local.holder;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
|
|
@ -21,7 +23,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
||||
public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) {
|
||||
if (!(localItem instanceof PlaylistRemoteEntity)) return;
|
||||
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
|
||||
|
||||
|
|
@ -33,6 +35,6 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
|||
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
||||
|
||||
super.updateFromItem(localItem, dateFormat);
|
||||
super.updateFromItem(localItem, state, dateFormat);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import org.schabi.newpipe.report.UserAction;
|
|||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
|
@ -318,11 +319,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
setVideoCount(itemListAdapter.getItemsList().size());
|
||||
|
||||
headerPlayAllButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
|
||||
headerPopupButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||
headerBackgroundButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
|
||||
hideLoading();
|
||||
}
|
||||
|
|
@ -533,20 +534,20 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
switch (i) {
|
||||
case 0:
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context,
|
||||
new SinglePlayQueue(infoItem));
|
||||
new SinglePlayQueue(infoItem), false);
|
||||
break;
|
||||
case 1:
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new
|
||||
SinglePlayQueue(infoItem));
|
||||
SinglePlayQueue(infoItem), false);
|
||||
break;
|
||||
case 2:
|
||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
|
||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index), true);
|
||||
break;
|
||||
case 3:
|
||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
|
||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index), true);
|
||||
break;
|
||||
case 4:
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index), true);
|
||||
break;
|
||||
case 5:
|
||||
changeThumbnailUrl(item.thumbnailUrl);
|
||||
|
|
@ -555,7 +556,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
deleteItem(item);
|
||||
break;
|
||||
case 7:
|
||||
shareUrl(item.toStreamInfoItem().getName(), item.toStreamInfoItem().getUrl());
|
||||
ShareUtils.shareUrl(this.getContext(), item.toStreamInfoItem().getName(), item.toStreamInfoItem().getUrl());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import android.support.annotation.Nullable;
|
|||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
|
|
@ -48,15 +47,14 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
|||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService;
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.CollapsibleView;
|
||||
|
||||
|
|
@ -130,6 +128,12 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
|||
subscriptionService = SubscriptionService.getInstance(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
infoListAdapter.dispose();
|
||||
super.onDetach();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||
|
|
@ -149,6 +153,8 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
|||
}
|
||||
updateFlags = 0;
|
||||
}
|
||||
|
||||
itemsList.post(infoListAdapter::updateStates);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -425,7 +431,7 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
|||
}
|
||||
|
||||
private void shareChannel (ChannelInfoItem selectedItem) {
|
||||
shareUrl(selectedItem.getName(), selectedItem.getUrl());
|
||||
ShareUtils.shareUrl(this.getContext(), selectedItem.getName(), selectedItem.getUrl());
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ public final class BackgroundPlayer extends Service {
|
|||
lockManager.releaseWifiAndCpu();
|
||||
}
|
||||
if (basePlayerImpl != null) {
|
||||
basePlayerImpl.savePlaybackState();
|
||||
basePlayerImpl.stopActivityBinding();
|
||||
basePlayerImpl.destroy();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,11 @@ import android.content.BroadcastReceiver;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.media.AudioManager;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
|
@ -44,7 +46,6 @@ import com.google.android.exoplayer2.Timeline;
|
|||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
|
|
@ -64,6 +65,7 @@ import org.schabi.newpipe.player.helper.PlayerDataSource;
|
|||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.mediasource.FailedMediaSource;
|
||||
import org.schabi.newpipe.player.playback.BasePlayerMediaSession;
|
||||
import org.schabi.newpipe.player.playback.CustomTrackSelector;
|
||||
import org.schabi.newpipe.player.playback.MediaSourceManager;
|
||||
import org.schabi.newpipe.player.playback.PlaybackListener;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
|
|
@ -113,7 +115,7 @@ public abstract class BasePlayer implements
|
|||
final protected HistoryRecordManager recordManager;
|
||||
|
||||
@NonNull
|
||||
final protected DefaultTrackSelector trackSelector;
|
||||
final protected CustomTrackSelector trackSelector;
|
||||
@NonNull
|
||||
final protected PlayerDataSource dataSource;
|
||||
|
||||
|
|
@ -145,6 +147,8 @@ public abstract class BasePlayer implements
|
|||
@NonNull
|
||||
public static final String APPEND_ONLY = "append_only";
|
||||
@NonNull
|
||||
public static final String RESUME_PLAYBACK = "resume_playback";
|
||||
@NonNull
|
||||
public static final String SELECT_ON_APPEND = "select_on_append";
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -208,7 +212,7 @@ public abstract class BasePlayer implements
|
|||
this.dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter);
|
||||
|
||||
final TrackSelection.Factory trackSelectionFactory = PlayerHelper.getQualitySelector(context);
|
||||
this.trackSelector = new DefaultTrackSelector(trackSelectionFactory);
|
||||
this.trackSelector = new CustomTrackSelector(trackSelectionFactory);
|
||||
|
||||
this.loadControl = new LoadController(context);
|
||||
this.renderFactory = new DefaultRenderersFactory(context);
|
||||
|
|
@ -269,6 +273,33 @@ public abstract class BasePlayer implements
|
|||
final boolean playbackSkipSilence = intent.getBooleanExtra(PLAYBACK_SKIP_SILENCE,
|
||||
getPlaybackSkipSilence());
|
||||
|
||||
// seek to timestamp if stream is already playing
|
||||
if (simpleExoPlayer != null
|
||||
&& queue.size() == 1
|
||||
&& playQueue != null
|
||||
&& playQueue.getItem() != null
|
||||
&& queue.getItem().getUrl().equals(playQueue.getItem().getUrl())
|
||||
&& queue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET
|
||||
) {
|
||||
simpleExoPlayer.seekTo(playQueue.getIndex(), queue.getItem().getRecoveryPosition());
|
||||
return;
|
||||
} else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) && isPlaybackResumeEnabled()) {
|
||||
final PlayQueueItem item = queue.getItem();
|
||||
if (item != null && item.getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET && isPlaybackResumeEnabled()) {
|
||||
final Disposable stateLoader = recordManager.loadStreamState(item)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doFinally(() -> initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence,
|
||||
/*playOnInit=*/true))
|
||||
.subscribe(
|
||||
state -> queue.setRecovery(queue.getIndex(), state.getProgressTime()),
|
||||
error -> {
|
||||
if (DEBUG) error.printStackTrace();
|
||||
}
|
||||
);
|
||||
databaseUpdateReactor.add(stateLoader);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Good to go...
|
||||
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence,
|
||||
/*playOnInit=*/true);
|
||||
|
|
@ -603,6 +634,9 @@ public abstract class BasePlayer implements
|
|||
break;
|
||||
case Player.STATE_ENDED: // 4
|
||||
changeState(STATE_COMPLETED);
|
||||
if (currentMetadata != null) {
|
||||
resetPlaybackState(currentMetadata.getMetadata());
|
||||
}
|
||||
isPrepared = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -709,6 +743,7 @@ public abstract class BasePlayer implements
|
|||
case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
|
||||
case DISCONTINUITY_REASON_INTERNAL:
|
||||
if (playQueue.getIndex() != newWindowIndex) {
|
||||
resetPlaybackState(playQueue.getItem());
|
||||
playQueue.setIndex(newWindowIndex);
|
||||
}
|
||||
break;
|
||||
|
|
@ -738,6 +773,9 @@ public abstract class BasePlayer implements
|
|||
@Override
|
||||
public void onSeekProcessed() {
|
||||
if (DEBUG) Log.d(TAG, "ExoPlayer - onSeekProcessed() called");
|
||||
if (isPrepared) {
|
||||
savePlaybackState();
|
||||
}
|
||||
}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback Listener
|
||||
|
|
@ -1005,27 +1043,40 @@ public abstract class BasePlayer implements
|
|||
}
|
||||
}
|
||||
|
||||
protected void savePlaybackState(final StreamInfo info, final long progress) {
|
||||
private void savePlaybackState(final StreamInfo info, final long progress) {
|
||||
if (info == null) return;
|
||||
if (DEBUG) Log.d(TAG, "savePlaybackState() called");
|
||||
final Disposable stateSaver = recordManager.saveStreamState(info, progress)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError((e) -> {
|
||||
if (DEBUG) e.printStackTrace();
|
||||
})
|
||||
.onErrorComplete()
|
||||
.subscribe(
|
||||
ignored -> {/* successful */},
|
||||
error -> Log.e(TAG, "savePlaybackState() failure: ", error)
|
||||
);
|
||||
.subscribe();
|
||||
databaseUpdateReactor.add(stateSaver);
|
||||
}
|
||||
|
||||
private void savePlaybackState() {
|
||||
private void resetPlaybackState(final PlayQueueItem queueItem) {
|
||||
if (queueItem == null) return;
|
||||
final Disposable stateSaver = queueItem.getStream()
|
||||
.flatMapCompletable(info -> recordManager.saveStreamState(info, 0))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError((e) -> {
|
||||
if (DEBUG) e.printStackTrace();
|
||||
})
|
||||
.onErrorComplete()
|
||||
.subscribe();
|
||||
databaseUpdateReactor.add(stateSaver);
|
||||
}
|
||||
|
||||
public void resetPlaybackState(final StreamInfo info) {
|
||||
savePlaybackState(info, 0);
|
||||
}
|
||||
|
||||
public void savePlaybackState() {
|
||||
if (simpleExoPlayer == null || currentMetadata == null) return;
|
||||
final StreamInfo currentInfo = currentMetadata.getMetadata();
|
||||
|
||||
if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD_MILLIS &&
|
||||
simpleExoPlayer.getCurrentPosition() <
|
||||
simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD_MILLIS) {
|
||||
savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition());
|
||||
}
|
||||
savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition());
|
||||
}
|
||||
|
||||
private void maybeUpdateCurrentMetadata() {
|
||||
|
|
@ -1213,4 +1264,10 @@ public abstract class BasePlayer implements
|
|||
public boolean gotDestroyed() {
|
||||
return simpleExoPlayer == null;
|
||||
}
|
||||
|
||||
private boolean isPlaybackResumeEnabled() {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)
|
||||
&& prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,11 +24,13 @@ import android.content.Intent;
|
|||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.database.ContentObserver;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.provider.Settings;
|
||||
import android.support.annotation.ColorInt;
|
||||
|
|
@ -46,6 +48,7 @@ import android.view.GestureDetector;
|
|||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
|
|
@ -75,6 +78,7 @@ import org.schabi.newpipe.util.AnimationUtils;
|
|||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
|
|
@ -111,6 +115,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
private boolean isInMultiWindow;
|
||||
private boolean isBackPressed;
|
||||
|
||||
private ContentObserver rotationObserver;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Activity LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
|
@ -145,6 +151,23 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
rotationObserver = new ContentObserver(new Handler()) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
super.onChange(selfChange);
|
||||
if (globalScreenOrientationLocked()) {
|
||||
final boolean lastOrientationWasLandscape = defaultPreferences.getBoolean(
|
||||
getString(R.string.last_orientation_landscape_key), false);
|
||||
setLandscape(lastOrientationWasLandscape);
|
||||
} else {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
}
|
||||
}
|
||||
};
|
||||
getContentResolver().registerContentObserver(
|
||||
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
|
||||
false, rotationObserver);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -237,6 +260,9 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
playerState = createPlayerState();
|
||||
playerImpl.destroy();
|
||||
|
||||
if (rotationObserver != null)
|
||||
getContentResolver().unregisterContentObserver(rotationObserver);
|
||||
|
||||
isInMultiWindow = false;
|
||||
isBackPressed = false;
|
||||
}
|
||||
|
|
@ -246,6 +272,12 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(newBase));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
playerImpl.savePlaybackState();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// State Saving
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
|
@ -283,8 +315,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
if (playerImpl != null && playerImpl.queueVisible) return;
|
||||
|
||||
final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
@ColorInt final int systenUiColor =
|
||||
|
|
@ -393,6 +425,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
private ImageButton playPauseButton;
|
||||
private ImageButton playPreviousButton;
|
||||
private ImageButton playNextButton;
|
||||
private Button closeButton;
|
||||
|
||||
private RelativeLayout queueLayout;
|
||||
private ImageButton itemsListCloseButton;
|
||||
|
|
@ -402,6 +435,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
private boolean queueVisible;
|
||||
|
||||
private ImageButton moreOptionsButton;
|
||||
private ImageButton shareButton;
|
||||
private ImageButton toggleOrientationButton;
|
||||
private ImageButton switchPopupButton;
|
||||
private ImageButton switchBackgroundButton;
|
||||
|
|
@ -433,9 +467,11 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
this.playPauseButton = rootView.findViewById(R.id.playPauseButton);
|
||||
this.playPreviousButton = rootView.findViewById(R.id.playPreviousButton);
|
||||
this.playNextButton = rootView.findViewById(R.id.playNextButton);
|
||||
this.closeButton = rootView.findViewById(R.id.closeButton);
|
||||
|
||||
this.moreOptionsButton = rootView.findViewById(R.id.moreOptionsButton);
|
||||
this.secondaryControls = rootView.findViewById(R.id.secondaryControls);
|
||||
this.shareButton = rootView.findViewById(R.id.share);
|
||||
this.toggleOrientationButton = rootView.findViewById(R.id.toggleOrientation);
|
||||
this.switchBackgroundButton = rootView.findViewById(R.id.switchBackground);
|
||||
this.switchPopupButton = rootView.findViewById(R.id.switchPopup);
|
||||
|
|
@ -479,8 +515,10 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
playPauseButton.setOnClickListener(this);
|
||||
playPreviousButton.setOnClickListener(this);
|
||||
playNextButton.setOnClickListener(this);
|
||||
closeButton.setOnClickListener(this);
|
||||
|
||||
moreOptionsButton.setOnClickListener(this);
|
||||
shareButton.setOnClickListener(this);
|
||||
toggleOrientationButton.setOnClickListener(this);
|
||||
switchBackgroundButton.setOnClickListener(this);
|
||||
switchPopupButton.setOnClickListener(this);
|
||||
|
|
@ -575,7 +613,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
this.getPlaybackSpeed(),
|
||||
this.getPlaybackPitch(),
|
||||
this.getPlaybackSkipSilence(),
|
||||
this.getPlaybackQuality()
|
||||
this.getPlaybackQuality(),
|
||||
false
|
||||
);
|
||||
context.startService(intent);
|
||||
|
||||
|
|
@ -597,7 +636,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
this.getPlaybackSpeed(),
|
||||
this.getPlaybackPitch(),
|
||||
this.getPlaybackSkipSilence(),
|
||||
this.getPlaybackQuality()
|
||||
this.getPlaybackQuality(),
|
||||
false
|
||||
);
|
||||
context.startService(intent);
|
||||
|
||||
|
|
@ -631,6 +671,9 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
} else if (v.getId() == moreOptionsButton.getId()) {
|
||||
onMoreOptionsClicked();
|
||||
|
||||
} else if (v.getId() == shareButton.getId()) {
|
||||
onShareClicked();
|
||||
|
||||
} else if (v.getId() == toggleOrientationButton.getId()) {
|
||||
onScreenRotationClicked();
|
||||
|
||||
|
|
@ -640,6 +683,9 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
} else if (v.getId() == switchBackgroundButton.getId()) {
|
||||
onPlayBackgroundButtonClicked();
|
||||
|
||||
} else if (v.getId() == closeButton.getId()) {
|
||||
onPlaybackShutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
if (getCurrentState() != STATE_COMPLETED) {
|
||||
|
|
@ -684,6 +730,13 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
showControls(DEFAULT_CONTROLS_DURATION);
|
||||
}
|
||||
|
||||
private void onShareClicked() {
|
||||
// share video at the current time (youtube.com/watch?v=ID&t=SECONDS)
|
||||
ShareUtils.shareUrl(MainVideoPlayer.this,
|
||||
playerImpl.getVideoTitle(),
|
||||
playerImpl.getVideoUrl() + "&t=" + String.valueOf(playerImpl.getPlaybackSeekBar().getProgress()/1000));
|
||||
}
|
||||
|
||||
private void onScreenRotationClicked() {
|
||||
if (DEBUG) Log.d(TAG, "onScreenRotationClicked() called");
|
||||
toggleOrientation();
|
||||
|
|
@ -766,6 +819,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
super.onBlocked();
|
||||
playPauseButton.setImageResource(R.drawable.ic_pause_white);
|
||||
animatePlayButtons(false, 100);
|
||||
animateView(closeButton, false, DEFAULT_CONTROLS_DURATION);
|
||||
getRootView().setKeepScreenOn(true);
|
||||
}
|
||||
|
||||
|
|
@ -781,6 +835,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> {
|
||||
playPauseButton.setImageResource(R.drawable.ic_pause_white);
|
||||
animatePlayButtons(true, 200);
|
||||
animateView(closeButton, false, DEFAULT_CONTROLS_DURATION);
|
||||
});
|
||||
|
||||
getRootView().setKeepScreenOn(true);
|
||||
|
|
@ -792,6 +847,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> {
|
||||
playPauseButton.setImageResource(R.drawable.ic_play_arrow_white);
|
||||
animatePlayButtons(true, 200);
|
||||
animateView(closeButton, false, DEFAULT_CONTROLS_DURATION);
|
||||
});
|
||||
|
||||
showSystemUi();
|
||||
|
|
@ -811,8 +867,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> {
|
||||
playPauseButton.setImageResource(R.drawable.ic_replay_white);
|
||||
animatePlayButtons(true, DEFAULT_CONTROLS_DURATION);
|
||||
animateView(closeButton, true, DEFAULT_CONTROLS_DURATION);
|
||||
});
|
||||
|
||||
getRootView().setKeepScreenOn(false);
|
||||
super.onCompleted();
|
||||
}
|
||||
|
|
@ -847,8 +903,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]");
|
||||
getControlsVisibilityHandler().removeCallbacksAndMessages(null);
|
||||
getControlsVisibilityHandler().postDelayed(() ->
|
||||
animateView(getControlsRoot(), false, duration, 0,
|
||||
MainVideoPlayer.this::hideSystemUi),
|
||||
animateView(getControlsRoot(), false, duration, 0,
|
||||
MainVideoPlayer.this::hideSystemUi),
|
||||
/*delayMillis=*/delay
|
||||
);
|
||||
}
|
||||
|
|
@ -1052,9 +1108,9 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
|
||||
final int resId =
|
||||
currentProgressPercent <= 0 ? R.drawable.ic_volume_off_white_72dp
|
||||
: currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_72dp
|
||||
: currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_72dp
|
||||
: R.drawable.ic_volume_up_white_72dp;
|
||||
: currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_72dp
|
||||
: currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_72dp
|
||||
: R.drawable.ic_volume_up_white_72dp;
|
||||
|
||||
playerImpl.getVolumeImageView().setImageDrawable(
|
||||
AppCompatResources.getDrawable(getApplicationContext(), resId)
|
||||
|
|
@ -1078,8 +1134,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
|
||||
final int resId =
|
||||
currentProgressPercent < 0.25 ? R.drawable.ic_brightness_low_white_72dp
|
||||
: currentProgressPercent < 0.75 ? R.drawable.ic_brightness_medium_white_72dp
|
||||
: R.drawable.ic_brightness_high_white_72dp;
|
||||
: currentProgressPercent < 0.75 ? R.drawable.ic_brightness_medium_white_72dp
|
||||
: R.drawable.ic_brightness_high_white_72dp;
|
||||
|
||||
playerImpl.getBrightnessImageView().setImageDrawable(
|
||||
AppCompatResources.getDrawable(getApplicationContext(), resId)
|
||||
|
|
|
|||
|
|
@ -325,6 +325,7 @@ public final class PopupVideoPlayer extends Service {
|
|||
isPopupClosing = true;
|
||||
|
||||
if (playerImpl != null) {
|
||||
playerImpl.savePlaybackState();
|
||||
if (playerImpl.getRootView() != null) {
|
||||
windowManager.removeView(playerImpl.getRootView());
|
||||
}
|
||||
|
|
@ -565,7 +566,8 @@ public final class PopupVideoPlayer extends Service {
|
|||
this.getPlaybackSpeed(),
|
||||
this.getPlaybackPitch(),
|
||||
this.getPlaybackSkipSilence(),
|
||||
this.getPlaybackQuality()
|
||||
this.getPlaybackQuality(),
|
||||
false
|
||||
);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
|
|
|
|||
|
|
@ -188,7 +188,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
|||
this.player.getPlaybackSpeed(),
|
||||
this.player.getPlaybackPitch(),
|
||||
this.player.getPlaybackSkipSilence(),
|
||||
null
|
||||
null,
|
||||
false
|
||||
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -305,9 +305,9 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
captionItem.setOnMenuItemClickListener(menuItem -> {
|
||||
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
|
||||
if (textRendererIndex != RENDERER_UNAVAILABLE) {
|
||||
trackSelector.setPreferredTextLanguage(captionLanguage);
|
||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||
.setRendererDisabled(textRendererIndex, false)
|
||||
.setPreferredTextLanguage(captionLanguage));
|
||||
.setRendererDisabled(textRendererIndex, false));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
|
@ -315,7 +315,6 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
captionPopupMenu.setOnDismissListener(this);
|
||||
}
|
||||
|
||||
|
||||
private void updateStreamRelatedViews() {
|
||||
if (getCurrentMetadata() == null) return;
|
||||
|
||||
|
|
@ -508,7 +507,7 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
}
|
||||
|
||||
// Normalize mismatching language strings
|
||||
final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage;
|
||||
final String preferredLanguage = trackSelector.getPreferredTextLanguage();
|
||||
// Build UI
|
||||
buildCaptionMenu(availableLanguages);
|
||||
if (trackSelector.getParameters().getRendererDisabled(textRenderer) ||
|
||||
|
|
@ -543,6 +542,11 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed()));
|
||||
|
||||
super.onPrepared(playWhenReady);
|
||||
|
||||
if (simpleExoPlayer.getCurrentPosition() != 0 && !isControlsVisible()) {
|
||||
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
||||
controlsVisibilityHandler.postDelayed(this::showControlsThenHide, DEFAULT_CONTROLS_DURATION);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -58,8 +58,11 @@ public class BasePlayerMediaSession implements MediaSessionCallback {
|
|||
|
||||
// set additional metadata for A2DP/AVRCP
|
||||
Bundle additionalMetadata = new Bundle();
|
||||
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, item.getTitle());
|
||||
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, item.getUploader());
|
||||
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration());
|
||||
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration() * 1000);
|
||||
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, index + 1);
|
||||
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
|
||||
descriptionBuilder.setExtras(additionalMetadata);
|
||||
|
||||
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
package org.schabi.newpipe.player.playback;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
||||
/**
|
||||
* This class allows irregular text language labels for use when selecting text captions and
|
||||
* is mostly a copy-paste from {@link DefaultTrackSelector}.
|
||||
*
|
||||
* This is a hack and should be removed once ExoPlayer fixes language normalization to accept
|
||||
* a broader set of languages.
|
||||
* */
|
||||
public class CustomTrackSelector extends DefaultTrackSelector {
|
||||
private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000;
|
||||
|
||||
private String preferredTextLanguage;
|
||||
|
||||
public CustomTrackSelector(TrackSelection.Factory adaptiveTrackSelectionFactory) {
|
||||
super(adaptiveTrackSelectionFactory);
|
||||
}
|
||||
|
||||
public String getPreferredTextLanguage() {
|
||||
return preferredTextLanguage;
|
||||
}
|
||||
|
||||
public void setPreferredTextLanguage(@NonNull final String label) {
|
||||
Assertions.checkNotNull(label);
|
||||
if (!label.equals(preferredTextLanguage)) {
|
||||
preferredTextLanguage = label;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/** @see DefaultTrackSelector#formatHasLanguage(Format, String)*/
|
||||
protected static boolean formatHasLanguage(Format format, String language) {
|
||||
return language != null && TextUtils.equals(language, format.language);
|
||||
}
|
||||
|
||||
/** @see DefaultTrackSelector#formatHasNoLanguage(Format)*/
|
||||
protected static boolean formatHasNoLanguage(Format format) {
|
||||
return TextUtils.isEmpty(format.language) || formatHasLanguage(format, C.LANGUAGE_UNDETERMINED);
|
||||
}
|
||||
|
||||
/** @see DefaultTrackSelector#selectTextTrack(TrackGroupArray, int[][], Parameters) */
|
||||
@Override
|
||||
protected Pair<TrackSelection, Integer> selectTextTrack(TrackGroupArray groups, int[][] formatSupport,
|
||||
Parameters params) {
|
||||
TrackGroup selectedGroup = null;
|
||||
int selectedTrackIndex = 0;
|
||||
int selectedTrackScore = 0;
|
||||
for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
|
||||
TrackGroup trackGroup = groups.get(groupIndex);
|
||||
int[] trackFormatSupport = formatSupport[groupIndex];
|
||||
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
|
||||
if (isSupported(trackFormatSupport[trackIndex],
|
||||
params.exceedRendererCapabilitiesIfNecessary)) {
|
||||
Format format = trackGroup.getFormat(trackIndex);
|
||||
int maskedSelectionFlags =
|
||||
format.selectionFlags & ~params.disabledTextTrackSelectionFlags;
|
||||
boolean isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
|
||||
boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0;
|
||||
int trackScore;
|
||||
boolean preferredLanguageFound = formatHasLanguage(format, preferredTextLanguage);
|
||||
if (preferredLanguageFound
|
||||
|| (params.selectUndeterminedTextLanguage && formatHasNoLanguage(format))) {
|
||||
if (isDefault) {
|
||||
trackScore = 8;
|
||||
} else if (!isForced) {
|
||||
// Prefer non-forced to forced if a preferred text language has been specified. Where
|
||||
// both are provided the non-forced track will usually contain the forced subtitles as
|
||||
// a subset.
|
||||
trackScore = 6;
|
||||
} else {
|
||||
trackScore = 4;
|
||||
}
|
||||
trackScore += preferredLanguageFound ? 1 : 0;
|
||||
} else if (isDefault) {
|
||||
trackScore = 3;
|
||||
} else if (isForced) {
|
||||
if (formatHasLanguage(format, params.preferredAudioLanguage)) {
|
||||
trackScore = 2;
|
||||
} else {
|
||||
trackScore = 1;
|
||||
}
|
||||
} else {
|
||||
// Track should not be selected.
|
||||
continue;
|
||||
}
|
||||
if (isSupported(trackFormatSupport[trackIndex], false)) {
|
||||
trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS;
|
||||
}
|
||||
if (trackScore > selectedTrackScore) {
|
||||
selectedGroup = trackGroup;
|
||||
selectedTrackIndex = trackIndex;
|
||||
selectedTrackScore = trackScore;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selectedGroup == null
|
||||
? null
|
||||
: Pair.create(
|
||||
new FixedTrackSelection(selectedGroup, selectedTrackIndex), selectedTrackScore);
|
||||
}
|
||||
}
|
||||
|
|
@ -158,7 +158,7 @@ public class MediaSourceManager {
|
|||
* Dispose the manager and releases all message buses and loaders.
|
||||
* */
|
||||
public void dispose() {
|
||||
if (DEBUG) Log.d(TAG, "dispose() called.");
|
||||
if (DEBUG) Log.d(TAG, "close() called.");
|
||||
|
||||
debouncedSignal.onComplete();
|
||||
debouncedLoader.dispose();
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ public final class SinglePlayQueue extends PlayQueue {
|
|||
super(0, Collections.singletonList(new PlayQueueItem(info)));
|
||||
}
|
||||
|
||||
public SinglePlayQueue(final StreamInfo info, final long startPosition) {
|
||||
super(0, Collections.singletonList(new PlayQueueItem(info)));
|
||||
getItem().setRecoveryPosition(startPosition);
|
||||
}
|
||||
|
||||
public SinglePlayQueue(final List<StreamInfoItem> items, final int index) {
|
||||
super(index, playQueueItemsOf(items));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ public enum UserAction {
|
|||
REQUESTED_KIOSK("requested kiosk"),
|
||||
REQUESTED_COMMENTS("requested comments"),
|
||||
DELETE_FROM_HISTORY("delete from history"),
|
||||
PLAY_STREAM("Play stream");
|
||||
PLAY_STREAM("Play stream"),
|
||||
DOWNLOAD_POSTPROCESSING("download post-processing"),
|
||||
DOWNLOAD_FAILED("download failed");
|
||||
|
||||
|
||||
private final String message;
|
||||
|
|
|
|||
|
|
@ -1,30 +1,77 @@
|
|||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.v7.preference.Preference;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
|
||||
public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||
private static final int REQUEST_DOWNLOAD_PATH = 0x1235;
|
||||
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
private String DOWNLOAD_PATH_PREFERENCE;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
|
||||
public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||
private static final int REQUEST_DOWNLOAD_VIDEO_PATH = 0x1235;
|
||||
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
|
||||
public static final boolean IGNORE_RELEASE_ON_OLD_PATH = true;
|
||||
|
||||
private String DOWNLOAD_PATH_VIDEO_PREFERENCE;
|
||||
private String DOWNLOAD_PATH_AUDIO_PREFERENCE;
|
||||
|
||||
private String DOWNLOAD_STORAGE_ASK;
|
||||
|
||||
private Preference prefPathVideo;
|
||||
private Preference prefPathAudio;
|
||||
private Preference prefStorageAsk;
|
||||
|
||||
private Context ctx;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
initKeys();
|
||||
DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key);
|
||||
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
|
||||
DOWNLOAD_STORAGE_ASK = getString(R.string.downloads_storage_ask);
|
||||
|
||||
prefPathVideo = findPreference(DOWNLOAD_PATH_VIDEO_PREFERENCE);
|
||||
prefPathAudio = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE);
|
||||
prefStorageAsk = findPreference(DOWNLOAD_STORAGE_ASK);
|
||||
|
||||
updatePreferencesSummary();
|
||||
updatePathPickers(!defaultPreferences.getBoolean(DOWNLOAD_STORAGE_ASK, false));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
prefStorageAsk.setSummary(R.string.downloads_storage_ask_summary);
|
||||
}
|
||||
|
||||
if (hasInvalidPath(DOWNLOAD_PATH_VIDEO_PREFERENCE) || hasInvalidPath(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||
Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_SHORT).show();
|
||||
updatePreferencesSummary();
|
||||
}
|
||||
|
||||
prefStorageAsk.setOnPreferenceChangeListener((preference, value) -> {
|
||||
updatePathPickers(!(boolean) value);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -32,52 +79,183 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
|||
addPreferencesFromResource(R.xml.download_settings);
|
||||
}
|
||||
|
||||
private void initKeys() {
|
||||
DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key);
|
||||
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
ctx = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
ctx = null;
|
||||
prefStorageAsk.setOnPreferenceChangeListener(null);
|
||||
}
|
||||
|
||||
private void updatePreferencesSummary() {
|
||||
findPreference(DOWNLOAD_PATH_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_PREFERENCE, getString(R.string.download_path_summary)));
|
||||
findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary)));
|
||||
showPathInSummary(DOWNLOAD_PATH_VIDEO_PREFERENCE, R.string.download_path_summary, prefPathVideo);
|
||||
showPathInSummary(DOWNLOAD_PATH_AUDIO_PREFERENCE, R.string.download_path_audio_summary, prefPathAudio);
|
||||
}
|
||||
|
||||
private void showPathInSummary(String prefKey, @StringRes int defaultString, Preference target) {
|
||||
String rawUri = defaultPreferences.getString(prefKey, null);
|
||||
if (rawUri == null || rawUri.isEmpty()) {
|
||||
target.setSummary(getString(defaultString));
|
||||
return;
|
||||
}
|
||||
|
||||
if (rawUri.charAt(0) == File.separatorChar) {
|
||||
target.setSummary(rawUri);
|
||||
return;
|
||||
}
|
||||
if (rawUri.startsWith(ContentResolver.SCHEME_FILE)) {
|
||||
target.setSummary(new File(URI.create(rawUri)).getPath());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
rawUri = URLDecoder.decode(rawUri, StandardCharsets.UTF_8.name());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
target.setSummary(rawUri);
|
||||
}
|
||||
|
||||
private boolean isFileUri(String path) {
|
||||
return path.charAt(0) == File.separatorChar || path.startsWith(ContentResolver.SCHEME_FILE);
|
||||
}
|
||||
|
||||
private boolean hasInvalidPath(String prefKey) {
|
||||
String value = defaultPreferences.getString(prefKey, null);
|
||||
return value == null || value.isEmpty();
|
||||
}
|
||||
|
||||
private void updatePathPickers(boolean enabled) {
|
||||
prefPathVideo.setEnabled(enabled);
|
||||
prefPathAudio.setEnabled(enabled);
|
||||
}
|
||||
|
||||
// FIXME: after releasing the old path, all downloads created on the folder becomes inaccessible
|
||||
private void forgetSAFTree(Context ctx, String oldPath) {
|
||||
if (IGNORE_RELEASE_ON_OLD_PATH) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldPath == null || oldPath.isEmpty() || isFileUri(oldPath)) return;
|
||||
|
||||
try {
|
||||
Uri uri = Uri.parse(oldPath);
|
||||
|
||||
ctx.getContentResolver().releasePersistableUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||
ctx.revokeUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||
|
||||
Log.i(TAG, "Revoke old path permissions success on " + oldPath);
|
||||
} catch (Exception err) {
|
||||
Log.e(TAG, "Error revoking old path permissions on " + oldPath, err);
|
||||
}
|
||||
}
|
||||
|
||||
private void showMessageDialog(@StringRes int title, @StringRes int message) {
|
||||
AlertDialog.Builder msg = new AlertDialog.Builder(ctx);
|
||||
msg.setTitle(title);
|
||||
msg.setMessage(message);
|
||||
msg.setPositiveButton(android.R.string.ok, null);
|
||||
msg.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceTreeClick(Preference preference) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]");
|
||||
Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]");
|
||||
}
|
||||
|
||||
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)
|
||||
|| preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||
Intent i = new Intent(getActivity(), FilePickerActivityHelper.class)
|
||||
String key = preference.getKey();
|
||||
int request;
|
||||
|
||||
if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE)) {
|
||||
request = REQUEST_DOWNLOAD_VIDEO_PATH;
|
||||
} else if (key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||
request = REQUEST_DOWNLOAD_AUDIO_PATH;
|
||||
} else {
|
||||
return super.onPreferenceTreeClick(preference);
|
||||
}
|
||||
|
||||
Intent i;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
.putExtra("android.content.extra.SHOW_ADVANCED", true)
|
||||
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||
} else {
|
||||
i = new Intent(getActivity(), FilePickerActivityHelper.class)
|
||||
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false)
|
||||
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true)
|
||||
.putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR);
|
||||
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)) {
|
||||
startActivityForResult(i, REQUEST_DOWNLOAD_PATH);
|
||||
} else if (preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||
startActivityForResult(i, REQUEST_DOWNLOAD_AUDIO_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
return super.onPreferenceTreeClick(preference);
|
||||
startActivityForResult(i, request);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]");
|
||||
Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], " +
|
||||
"resultCode = [" + resultCode + "], data = [" + data + "]"
|
||||
);
|
||||
}
|
||||
|
||||
if ((requestCode == REQUEST_DOWNLOAD_PATH || requestCode == REQUEST_DOWNLOAD_AUDIO_PATH)
|
||||
&& resultCode == Activity.RESULT_OK && data.getData() != null) {
|
||||
String key = getString(requestCode == REQUEST_DOWNLOAD_PATH ? R.string.download_path_key : R.string.download_path_audio_key);
|
||||
String path = Utils.getFileForUri(data.getData()).getAbsolutePath();
|
||||
if (resultCode != Activity.RESULT_OK) return;
|
||||
|
||||
defaultPreferences.edit().putString(key, path).apply();
|
||||
updatePreferencesSummary();
|
||||
String key;
|
||||
if (requestCode == REQUEST_DOWNLOAD_VIDEO_PATH)
|
||||
key = DOWNLOAD_PATH_VIDEO_PREFERENCE;
|
||||
else if (requestCode == REQUEST_DOWNLOAD_AUDIO_PATH)
|
||||
key = DOWNLOAD_PATH_AUDIO_PREFERENCE;
|
||||
else
|
||||
return;
|
||||
|
||||
Uri uri = data.getData();
|
||||
if (uri == null) {
|
||||
showMessageDialog(R.string.general_error, R.string.invalid_directory);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// steps:
|
||||
// 1. revoke permissions on the old save path
|
||||
// 2. acquire permissions on the new save path
|
||||
// 3. save the new path, if step(2) was successful
|
||||
final Context ctx = getContext();
|
||||
if (ctx == null) throw new NullPointerException("getContext()");
|
||||
|
||||
forgetSAFTree(ctx, defaultPreferences.getString(key, ""));
|
||||
|
||||
try {
|
||||
ctx.grantUriPermission(ctx.getPackageName(), uri, StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||
|
||||
StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, uri, null);
|
||||
Log.i(TAG, "Acquiring tree success from " + uri.toString());
|
||||
|
||||
if (!mainStorage.canWrite())
|
||||
throw new IOException("No write permissions on " + uri.toString());
|
||||
} catch (IOException err) {
|
||||
Log.e(TAG, "Error acquiring tree from " + uri.toString(), err);
|
||||
showMessageDialog(R.string.general_error, R.string.no_available_dir);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
File target = Utils.getFileForUri(data.getData());
|
||||
if (!target.canWrite()) {
|
||||
showMessageDialog(R.string.download_to_sdcard_error_title, R.string.download_to_sdcard_error_message);
|
||||
return;
|
||||
}
|
||||
uri = Uri.fromFile(target);
|
||||
}
|
||||
|
||||
defaultPreferences.edit().putString(key, uri.toString()).apply();
|
||||
updatePreferencesSummary();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,37 +70,23 @@ public class NewPipeSettings {
|
|||
getAudioDownloadFolder(context);
|
||||
}
|
||||
|
||||
public static File getVideoDownloadFolder(Context context) {
|
||||
return getDir(context, R.string.download_path_key, Environment.DIRECTORY_MOVIES);
|
||||
private static void getVideoDownloadFolder(Context context) {
|
||||
getDir(context, R.string.download_path_video_key, Environment.DIRECTORY_MOVIES);
|
||||
}
|
||||
|
||||
public static String getVideoDownloadPath(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String key = context.getString(R.string.download_path_key);
|
||||
return prefs.getString(key, Environment.DIRECTORY_MOVIES);
|
||||
private static void getAudioDownloadFolder(Context context) {
|
||||
getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC);
|
||||
}
|
||||
|
||||
public static File getAudioDownloadFolder(Context context) {
|
||||
return getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC);
|
||||
}
|
||||
|
||||
public static String getAudioDownloadPath(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String key = context.getString(R.string.download_path_audio_key);
|
||||
return prefs.getString(key, Environment.DIRECTORY_MUSIC);
|
||||
}
|
||||
|
||||
private static File getDir(Context context, int keyID, String defaultDirectoryName) {
|
||||
private static void getDir(Context context, int keyID, String defaultDirectoryName) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String key = context.getString(keyID);
|
||||
String downloadPath = prefs.getString(key, null);
|
||||
if ((downloadPath != null) && (!downloadPath.isEmpty())) return new File(downloadPath.trim());
|
||||
if ((downloadPath != null) && (!downloadPath.isEmpty())) return;
|
||||
|
||||
final File dir = getDir(defaultDirectoryName);
|
||||
SharedPreferences.Editor spEditor = prefs.edit();
|
||||
spEditor.putString(key, getNewPipeChildFolderPathForDir(dir));
|
||||
spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName)));
|
||||
spEditor.apply();
|
||||
return dir;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
|
@ -108,19 +94,7 @@ public class NewPipeSettings {
|
|||
return new File(Environment.getExternalStorageDirectory(), defaultDirectoryName);
|
||||
}
|
||||
|
||||
public static void resetDownloadFolders(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
resetDownloadFolder(prefs, context.getString(R.string.download_path_audio_key), Environment.DIRECTORY_MUSIC);
|
||||
resetDownloadFolder(prefs, context.getString(R.string.download_path_key), Environment.DIRECTORY_MOVIES);
|
||||
}
|
||||
|
||||
private static void resetDownloadFolder(SharedPreferences prefs, String key, String defaultDirectoryName) {
|
||||
SharedPreferences.Editor spEditor = prefs.edit();
|
||||
spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName)));
|
||||
spEditor.apply();
|
||||
}
|
||||
|
||||
private static String getNewPipeChildFolderPathForDir(File dir) {
|
||||
return new File(dir, "NewPipe").getAbsolutePath();
|
||||
return new File(dir, "NewPipe").toURI().toString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package org.schabi.newpipe.streams;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
|
|
@ -15,89 +16,239 @@ public class DataReader {
|
|||
public final static int INTEGER_SIZE = 4;
|
||||
public final static int FLOAT_SIZE = 4;
|
||||
|
||||
private long pos;
|
||||
public final SharpStream stream;
|
||||
private final boolean rewind;
|
||||
private final static int BUFFER_SIZE = 128 * 1024;// 128 KiB
|
||||
|
||||
private long position = 0;
|
||||
private final SharpStream stream;
|
||||
|
||||
private InputStream view;
|
||||
private int viewSize;
|
||||
|
||||
public DataReader(SharpStream stream) {
|
||||
this.rewind = stream.canRewind();
|
||||
this.stream = stream;
|
||||
this.pos = 0L;
|
||||
this.readOffset = this.readBuffer.length;
|
||||
}
|
||||
|
||||
public long position() {
|
||||
return pos;
|
||||
return position;
|
||||
}
|
||||
|
||||
public final int readInt() throws IOException {
|
||||
public int read() throws IOException {
|
||||
if (fillBuffer()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
position++;
|
||||
readCount--;
|
||||
|
||||
return readBuffer[readOffset++] & 0xFF;
|
||||
}
|
||||
|
||||
public long skipBytes(long amount) throws IOException {
|
||||
if (readCount < 0) {
|
||||
return 0;
|
||||
} else if (readCount == 0) {
|
||||
amount = stream.skip(amount);
|
||||
} else {
|
||||
if (readCount > amount) {
|
||||
readCount -= (int) amount;
|
||||
readOffset += (int) amount;
|
||||
} else {
|
||||
amount = readCount + stream.skip(amount - readCount);
|
||||
readCount = 0;
|
||||
readOffset = readBuffer.length;
|
||||
}
|
||||
}
|
||||
|
||||
position += amount;
|
||||
return amount;
|
||||
}
|
||||
|
||||
public int readInt() throws IOException {
|
||||
primitiveRead(INTEGER_SIZE);
|
||||
return primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
|
||||
}
|
||||
|
||||
public final int read() throws IOException {
|
||||
int value = stream.read();
|
||||
if (value == -1) {
|
||||
throw new EOFException();
|
||||
}
|
||||
|
||||
pos++;
|
||||
return value;
|
||||
public short readShort() throws IOException {
|
||||
primitiveRead(SHORT_SIZE);
|
||||
return (short) (primitive[0] << 8 | primitive[1]);
|
||||
}
|
||||
|
||||
public final long skipBytes(long amount) throws IOException {
|
||||
amount = stream.skip(amount);
|
||||
pos += amount;
|
||||
return amount;
|
||||
}
|
||||
|
||||
public final long readLong() throws IOException {
|
||||
public long readLong() throws IOException {
|
||||
primitiveRead(LONG_SIZE);
|
||||
long high = primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
|
||||
long low = primitive[4] << 24 | primitive[5] << 16 | primitive[6] << 8 | primitive[7];
|
||||
return high << 32 | low;
|
||||
}
|
||||
|
||||
public final short readShort() throws IOException {
|
||||
primitiveRead(SHORT_SIZE);
|
||||
return (short) (primitive[0] << 8 | primitive[1]);
|
||||
}
|
||||
|
||||
public final int read(byte[] buffer) throws IOException {
|
||||
public int read(byte[] buffer) throws IOException {
|
||||
return read(buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
public final int read(byte[] buffer, int offset, int count) throws IOException {
|
||||
int res = stream.read(buffer, offset, count);
|
||||
pos += res;
|
||||
public int read(byte[] buffer, int offset, int count) throws IOException {
|
||||
if (readCount < 0) {
|
||||
return -1;
|
||||
}
|
||||
int total = 0;
|
||||
|
||||
return res;
|
||||
if (count >= readBuffer.length) {
|
||||
if (readCount > 0) {
|
||||
System.arraycopy(readBuffer, readOffset, buffer, offset, readCount);
|
||||
readOffset += readCount;
|
||||
|
||||
offset += readCount;
|
||||
count -= readCount;
|
||||
|
||||
total = readCount;
|
||||
readCount = 0;
|
||||
}
|
||||
total += Math.max(stream.read(buffer, offset, count), 0);
|
||||
} else {
|
||||
while (count > 0 && !fillBuffer()) {
|
||||
int read = Math.min(readCount, count);
|
||||
System.arraycopy(readBuffer, readOffset, buffer, offset, read);
|
||||
|
||||
readOffset += read;
|
||||
readCount -= read;
|
||||
|
||||
offset += read;
|
||||
count -= read;
|
||||
|
||||
total += read;
|
||||
}
|
||||
}
|
||||
|
||||
position += total;
|
||||
return total;
|
||||
}
|
||||
|
||||
public final boolean available() {
|
||||
return stream.available() > 0;
|
||||
public boolean available() {
|
||||
return readCount > 0 || stream.available() > 0;
|
||||
}
|
||||
|
||||
public void rewind() throws IOException {
|
||||
stream.rewind();
|
||||
pos = 0;
|
||||
|
||||
if ((position - viewSize) > 0) {
|
||||
viewSize = 0;// drop view
|
||||
} else {
|
||||
viewSize += position;
|
||||
}
|
||||
|
||||
position = 0;
|
||||
readOffset = readBuffer.length;
|
||||
}
|
||||
|
||||
public boolean canRewind() {
|
||||
return rewind;
|
||||
return stream.canRewind();
|
||||
}
|
||||
|
||||
private short[] primitive = new short[LONG_SIZE];
|
||||
/**
|
||||
* Wraps this instance of {@code DataReader} into {@code InputStream}
|
||||
* object. Note: Any read in the {@code DataReader} will not modify
|
||||
* (decrease) the view size
|
||||
*
|
||||
* @param size the size of the view
|
||||
* @return the view
|
||||
*/
|
||||
public InputStream getView(int size) {
|
||||
if (view == null) {
|
||||
view = new InputStream() {
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (viewSize < 1) {
|
||||
return -1;
|
||||
}
|
||||
int res = DataReader.this.read();
|
||||
if (res > 0) {
|
||||
viewSize--;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer) throws IOException {
|
||||
return read(buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int offset, int count) throws IOException {
|
||||
if (viewSize < 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int res = DataReader.this.read(buffer, offset, Math.min(viewSize, count));
|
||||
viewSize -= res;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long amount) throws IOException {
|
||||
if (viewSize < 1) {
|
||||
return 0;
|
||||
}
|
||||
int res = (int) DataReader.this.skipBytes(Math.min(amount, viewSize));
|
||||
viewSize -= res;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() {
|
||||
return viewSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
viewSize = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
viewSize = size;
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private final short[] primitive = new short[LONG_SIZE];
|
||||
|
||||
private void primitiveRead(int amount) throws IOException {
|
||||
byte[] buffer = new byte[amount];
|
||||
int read = stream.read(buffer, 0, amount);
|
||||
pos += read;
|
||||
int read = read(buffer, 0, amount);
|
||||
|
||||
if (read != amount) {
|
||||
throw new EOFException("Truncated data, missing " + String.valueOf(amount - read) + " bytes");
|
||||
throw new EOFException("Truncated stream, missing " + String.valueOf(amount - read) + " bytes");
|
||||
}
|
||||
|
||||
for (int i = 0; i < buffer.length; i++) {
|
||||
primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" datatype is signed and is very annoying
|
||||
for (int i = 0; i < amount; i++) {
|
||||
primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" data type in java is signed and is very annoying
|
||||
}
|
||||
}
|
||||
|
||||
private final byte[] readBuffer = new byte[BUFFER_SIZE];
|
||||
private int readOffset;
|
||||
private int readCount;
|
||||
|
||||
private boolean fillBuffer() throws IOException {
|
||||
if (readCount < 0) {
|
||||
return true;
|
||||
}
|
||||
if (readOffset >= readBuffer.length) {
|
||||
readCount = stream.read(readBuffer);
|
||||
if (readCount < 1) {
|
||||
readCount = -1;
|
||||
return true;
|
||||
}
|
||||
readOffset = 0;
|
||||
}
|
||||
|
||||
return readCount < 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
package org.schabi.newpipe.streams;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
|
|
@ -35,14 +33,29 @@ public class Mp4DashReader {
|
|||
private static final int ATOM_TREX = 0x74726578;
|
||||
private static final int ATOM_TKHD = 0x746B6864;
|
||||
private static final int ATOM_MFRA = 0x6D667261;
|
||||
private static final int ATOM_TFRA = 0x74667261;
|
||||
private static final int ATOM_MDHD = 0x6D646864;
|
||||
private static final int ATOM_EDTS = 0x65647473;
|
||||
private static final int ATOM_ELST = 0x656C7374;
|
||||
private static final int ATOM_HDLR = 0x68646C72;
|
||||
private static final int ATOM_MINF = 0x6D696E66;
|
||||
private static final int ATOM_DINF = 0x64696E66;
|
||||
private static final int ATOM_STBL = 0x7374626C;
|
||||
private static final int ATOM_STSD = 0x73747364;
|
||||
private static final int ATOM_VMHD = 0x766D6864;
|
||||
private static final int ATOM_SMHD = 0x736D6864;
|
||||
|
||||
private static final int BRAND_DASH = 0x64617368;
|
||||
private static final int BRAND_ISO5 = 0x69736F35;
|
||||
|
||||
private static final int HANDLER_VIDE = 0x76696465;
|
||||
private static final int HANDLER_SOUN = 0x736F756E;
|
||||
private static final int HANDLER_SUBT = 0x73756274;
|
||||
// </editor-fold>
|
||||
|
||||
private final DataReader stream;
|
||||
|
||||
private Mp4Track[] tracks = null;
|
||||
private int[] brands = null;
|
||||
|
||||
private Box box;
|
||||
private Moof moof;
|
||||
|
|
@ -50,9 +63,10 @@ public class Mp4DashReader {
|
|||
private boolean chunkZero = false;
|
||||
|
||||
private int selectedTrack = -1;
|
||||
private Box backupBox = null;
|
||||
|
||||
public enum TrackKind {
|
||||
Audio, Video, Other
|
||||
Audio, Video, Subtitles, Other
|
||||
}
|
||||
|
||||
public Mp4DashReader(SharpStream source) {
|
||||
|
|
@ -65,8 +79,15 @@ public class Mp4DashReader {
|
|||
}
|
||||
|
||||
box = readBox(ATOM_FTYP);
|
||||
if (parse_ftyp() != BRAND_DASH) {
|
||||
throw new NoSuchElementException("Main Brand is not dash");
|
||||
brands = parse_ftyp(box);
|
||||
switch (brands[0]) {
|
||||
case BRAND_DASH:
|
||||
case BRAND_ISO5:// ¿why not?
|
||||
break;
|
||||
default:
|
||||
throw new NoSuchElementException(
|
||||
"Not a MPEG-4 DASH container, major brand is not 'dash' or 'iso5' is " + boxName(brands[0])
|
||||
);
|
||||
}
|
||||
|
||||
Moov moov = null;
|
||||
|
|
@ -84,8 +105,6 @@ public class Mp4DashReader {
|
|||
break;
|
||||
case ATOM_MFRA:
|
||||
break;
|
||||
case ATOM_MDAT:
|
||||
throw new IOException("Expected moof, found mdat");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,15 +126,26 @@ public class Mp4DashReader {
|
|||
}
|
||||
}
|
||||
|
||||
if (moov.trak[i].tkhd.bHeight == 0 && moov.trak[i].tkhd.bWidth == 0) {
|
||||
tracks[i].kind = moov.trak[i].tkhd.bVolume == 0 ? TrackKind.Other : TrackKind.Audio;
|
||||
} else {
|
||||
tracks[i].kind = TrackKind.Video;
|
||||
switch (moov.trak[i].mdia.hdlr.subType) {
|
||||
case HANDLER_VIDE:
|
||||
tracks[i].kind = TrackKind.Video;
|
||||
break;
|
||||
case HANDLER_SOUN:
|
||||
tracks[i].kind = TrackKind.Audio;
|
||||
break;
|
||||
case HANDLER_SUBT:
|
||||
tracks[i].kind = TrackKind.Subtitles;
|
||||
break;
|
||||
default:
|
||||
tracks[i].kind = TrackKind.Other;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
backupBox = box;
|
||||
}
|
||||
|
||||
public Mp4Track selectTrack(int index) {
|
||||
Mp4Track selectTrack(int index) {
|
||||
selectedTrack = index;
|
||||
return tracks[index];
|
||||
}
|
||||
|
|
@ -126,7 +156,7 @@ public class Mp4DashReader {
|
|||
* @return list with a basic info
|
||||
* @throws IOException if the source stream is not seekeable
|
||||
*/
|
||||
public int getFragmentsCount() throws IOException {
|
||||
int getFragmentsCount() throws IOException {
|
||||
if (selectedTrack < 0) {
|
||||
throw new IllegalStateException("track no selected");
|
||||
}
|
||||
|
|
@ -136,7 +166,6 @@ public class Mp4DashReader {
|
|||
|
||||
Box tmp;
|
||||
int count = 0;
|
||||
long orig_offset = stream.position();
|
||||
|
||||
if (box.type == ATOM_MOOF) {
|
||||
tmp = box;
|
||||
|
|
@ -162,17 +191,36 @@ public class Mp4DashReader {
|
|||
ensure(tmp);
|
||||
} while (stream.available() && (tmp = readBox()) != null);
|
||||
|
||||
stream.rewind();
|
||||
stream.skipBytes((int) orig_offset);
|
||||
rewind();
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public int[] getBrands() {
|
||||
if (brands == null) throw new IllegalStateException("Not parsed");
|
||||
return brands;
|
||||
}
|
||||
|
||||
public void rewind() throws IOException {
|
||||
if (!stream.canRewind()) {
|
||||
throw new IOException("The provided stream doesn't allow seek");
|
||||
}
|
||||
if (box == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
box = backupBox;
|
||||
chunkZero = false;
|
||||
|
||||
stream.rewind();
|
||||
stream.skipBytes(backupBox.offset + (DataReader.INTEGER_SIZE * 2));
|
||||
}
|
||||
|
||||
public Mp4Track[] getAvailableTracks() {
|
||||
return tracks;
|
||||
}
|
||||
|
||||
public Mp4TrackChunk getNextChunk() throws IOException {
|
||||
public Mp4DashChunk getNextChunk(boolean infoOnly) throws IOException {
|
||||
Mp4Track track = tracks[selectedTrack];
|
||||
|
||||
while (stream.available()) {
|
||||
|
|
@ -208,7 +256,7 @@ public class Mp4DashReader {
|
|||
if (hasFlag(moof.traf.tfhd.bFlags, 0x10)) {
|
||||
moof.traf.trun.chunkSize = moof.traf.tfhd.defaultSampleSize * moof.traf.trun.entryCount;
|
||||
} else {
|
||||
moof.traf.trun.chunkSize = box.size - 8;
|
||||
moof.traf.trun.chunkSize = (int) (box.size - 8);
|
||||
}
|
||||
}
|
||||
if (!hasFlag(moof.traf.trun.bFlags, 0x900) && moof.traf.trun.chunkDuration == 0) {
|
||||
|
|
@ -228,9 +276,12 @@ public class Mp4DashReader {
|
|||
continue;// find another chunk
|
||||
}
|
||||
|
||||
Mp4TrackChunk chunk = new Mp4TrackChunk();
|
||||
Mp4DashChunk chunk = new Mp4DashChunk();
|
||||
chunk.moof = moof;
|
||||
chunk.data = new TrackDataChunk(stream, moof.traf.trun.chunkSize);
|
||||
if (!infoOnly) {
|
||||
chunk.data = stream.getView(moof.traf.trun.chunkSize);
|
||||
}
|
||||
|
||||
moof = null;
|
||||
|
||||
stream.skipBytes(chunk.moof.traf.trun.dataOffset);
|
||||
|
|
@ -269,6 +320,10 @@ public class Mp4DashReader {
|
|||
b.size = stream.readInt();
|
||||
b.type = stream.readInt();
|
||||
|
||||
if (b.size == 1) {
|
||||
b.size = stream.readLong();
|
||||
}
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
|
|
@ -280,6 +335,25 @@ public class Mp4DashReader {
|
|||
return b;
|
||||
}
|
||||
|
||||
private byte[] readFullBox(Box ref) throws IOException {
|
||||
// full box reading is limited to 2 GiB, and should be enough
|
||||
int size = (int) ref.size;
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(size);
|
||||
buffer.putInt(size);
|
||||
buffer.putInt(ref.type);
|
||||
|
||||
int read = size - 8;
|
||||
|
||||
if (stream.read(buffer.array(), 8, read) != read) {
|
||||
throw new EOFException(
|
||||
String.format("EOF reached in box: type=%s offset=%s size=%s", boxName(ref.type), ref.offset, ref.size)
|
||||
);
|
||||
}
|
||||
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
private void ensure(Box ref) throws IOException {
|
||||
long skip = ref.offset + ref.size - stream.position();
|
||||
|
||||
|
|
@ -310,6 +384,14 @@ public class Mp4DashReader {
|
|||
return null;
|
||||
}
|
||||
|
||||
private Box untilAnyBox(Box ref) throws IOException {
|
||||
if (stream.position() >= (ref.offset + ref.size)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return readBox();
|
||||
}
|
||||
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultState="collapsed" desc="Box readers">
|
||||
|
|
@ -329,7 +411,7 @@ public class Mp4DashReader {
|
|||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
|
@ -397,14 +479,14 @@ public class Mp4DashReader {
|
|||
|
||||
private long parse_tfdt() throws IOException {
|
||||
int version = stream.read();
|
||||
stream.skipBytes(3);// flags
|
||||
stream.skipBytes(3);// flags
|
||||
return version == 0 ? readUint() : stream.readLong();
|
||||
}
|
||||
|
||||
private Trun parse_trun() throws IOException {
|
||||
Trun obj = new Trun();
|
||||
obj.bFlags = stream.readInt();
|
||||
obj.entryCount = stream.readInt();// unsigned int
|
||||
obj.entryCount = stream.readInt();// unsigned int
|
||||
|
||||
obj.entries_rowSize = 0;
|
||||
if (hasFlag(obj.bFlags, 0x0100)) {
|
||||
|
|
@ -448,11 +530,18 @@ public class Mp4DashReader {
|
|||
return obj;
|
||||
}
|
||||
|
||||
private int parse_ftyp() throws IOException {
|
||||
int brand = stream.readInt();
|
||||
private int[] parse_ftyp(Box ref) throws IOException {
|
||||
int i = 0;
|
||||
int[] list = new int[(int) ((ref.offset + ref.size - stream.position() - 4) / 4)];
|
||||
|
||||
list[i++] = stream.readInt();// major brand
|
||||
|
||||
stream.skipBytes(4);// minor version
|
||||
|
||||
return brand;
|
||||
for (; i < list.length; i++)
|
||||
list[i] = stream.readInt();// compatible brands
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private Mvhd parse_mvhd() throws IOException {
|
||||
|
|
@ -521,32 +610,66 @@ public class Mp4DashReader {
|
|||
trak.tkhd = parse_tkhd();
|
||||
ensure(b);
|
||||
|
||||
b = untilBox(ref, ATOM_MDIA);
|
||||
trak.mdia = new byte[b.size];
|
||||
while ((b = untilBox(ref, ATOM_MDIA, ATOM_EDTS)) != null) {
|
||||
switch (b.type) {
|
||||
case ATOM_MDIA:
|
||||
trak.mdia = parse_mdia(b);
|
||||
break;
|
||||
case ATOM_EDTS:
|
||||
trak.edst_elst = parse_edts(b);
|
||||
break;
|
||||
}
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(trak.mdia);
|
||||
buffer.putInt(b.size);
|
||||
buffer.putInt(ATOM_MDIA);
|
||||
stream.read(trak.mdia, 8, b.size - 8);
|
||||
|
||||
trak.mdia_mdhd_timeScale = parse_mdia(buffer);
|
||||
ensure(b);
|
||||
}
|
||||
|
||||
return trak;
|
||||
}
|
||||
|
||||
private int parse_mdia(ByteBuffer data) {
|
||||
while (data.hasRemaining()) {
|
||||
int end = data.position() + data.getInt();
|
||||
if (data.getInt() == ATOM_MDHD) {
|
||||
byte version = data.get();
|
||||
data.position(data.position() + 3 + ((version == 0 ? 4 : 8) * 2));
|
||||
return data.getInt();
|
||||
}
|
||||
private Mdia parse_mdia(Box ref) throws IOException {
|
||||
Mdia obj = new Mdia();
|
||||
|
||||
data.position(end);
|
||||
Box b;
|
||||
while ((b = untilBox(ref, ATOM_MDHD, ATOM_HDLR, ATOM_MINF)) != null) {
|
||||
switch (b.type) {
|
||||
case ATOM_MDHD:
|
||||
obj.mdhd = readFullBox(b);
|
||||
|
||||
// read time scale
|
||||
ByteBuffer buffer = ByteBuffer.wrap(obj.mdhd);
|
||||
byte version = buffer.get(8);
|
||||
buffer.position(12 + ((version == 0 ? 4 : 8) * 2));
|
||||
obj.mdhd_timeScale = buffer.getInt();
|
||||
break;
|
||||
case ATOM_HDLR:
|
||||
obj.hdlr = parse_hdlr(b);
|
||||
break;
|
||||
case ATOM_MINF:
|
||||
obj.minf = parse_minf(b);
|
||||
break;
|
||||
}
|
||||
ensure(b);
|
||||
}
|
||||
|
||||
return 0;// this NEVER should happen
|
||||
return obj;
|
||||
}
|
||||
|
||||
private Hdlr parse_hdlr(Box ref) throws IOException {
|
||||
// version
|
||||
// flags
|
||||
stream.skipBytes(4);
|
||||
|
||||
Hdlr obj = new Hdlr();
|
||||
obj.bReserved = new byte[12];
|
||||
|
||||
obj.type = stream.readInt();
|
||||
obj.subType = stream.readInt();
|
||||
stream.read(obj.bReserved);
|
||||
|
||||
// component name (is a ansi/ascii string)
|
||||
stream.skipBytes((ref.offset + ref.size) - stream.position());
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private Moov parse_moov(Box ref) throws IOException {
|
||||
|
|
@ -570,7 +693,7 @@ public class Mp4DashReader {
|
|||
ensure(b);
|
||||
}
|
||||
|
||||
moov.trak = tmp.toArray(new Trak[tmp.size()]);
|
||||
moov.trak = tmp.toArray(new Trak[0]);
|
||||
|
||||
return moov;
|
||||
}
|
||||
|
|
@ -584,7 +707,7 @@ public class Mp4DashReader {
|
|||
ensure(b);
|
||||
}
|
||||
|
||||
return tmp.toArray(new Trex[tmp.size()]);
|
||||
return tmp.toArray(new Trex[0]);
|
||||
}
|
||||
|
||||
private Trex parse_trex() throws IOException {
|
||||
|
|
@ -602,74 +725,74 @@ public class Mp4DashReader {
|
|||
return obj;
|
||||
}
|
||||
|
||||
private Tfra parse_tfra() throws IOException {
|
||||
int version = stream.read();
|
||||
|
||||
stream.skipBytes(3);// flags
|
||||
|
||||
Tfra tfra = new Tfra();
|
||||
tfra.trackId = stream.readInt();
|
||||
|
||||
stream.skipBytes(3);// reserved
|
||||
int bFlags = stream.read();
|
||||
int size_tts = ((bFlags >> 4) & 3) + ((bFlags >> 2) & 3) + (bFlags & 3);
|
||||
|
||||
tfra.entries_time = new int[stream.readInt()];
|
||||
|
||||
for (int i = 0; i < tfra.entries_time.length; i++) {
|
||||
tfra.entries_time[i] = version == 0 ? stream.readInt() : (int) stream.readLong();
|
||||
stream.skipBytes(size_tts + (version == 0 ? 4 : 8));
|
||||
private Elst parse_edts(Box ref) throws IOException {
|
||||
Box b = untilBox(ref, ATOM_ELST);
|
||||
if (b == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return tfra;
|
||||
}
|
||||
|
||||
private Sidx parse_sidx() throws IOException {
|
||||
int version = stream.read();
|
||||
Elst obj = new Elst();
|
||||
|
||||
boolean v1 = stream.read() == 1;
|
||||
stream.skipBytes(3);// flags
|
||||
|
||||
Sidx obj = new Sidx();
|
||||
obj.referenceId = stream.readInt();
|
||||
obj.timescale = stream.readInt();
|
||||
int entryCount = stream.readInt();
|
||||
if (entryCount < 1) {
|
||||
obj.bMediaRate = 0x00010000;// default media rate (1.0)
|
||||
return obj;
|
||||
}
|
||||
|
||||
// earliest presentation entries_time
|
||||
// first offset
|
||||
// reserved
|
||||
stream.skipBytes((2 * (version == 0 ? 4 : 8)) + 2);
|
||||
if (v1) {
|
||||
stream.skipBytes(DataReader.LONG_SIZE);// segment duration
|
||||
obj.MediaTime = stream.readLong();
|
||||
// ignore all remain entries
|
||||
stream.skipBytes((entryCount - 1) * (DataReader.LONG_SIZE * 2));
|
||||
} else {
|
||||
stream.skipBytes(DataReader.INTEGER_SIZE);// segment duration
|
||||
obj.MediaTime = stream.readInt();
|
||||
}
|
||||
|
||||
obj.entries_subsegmentDuration = new int[stream.readShort()];
|
||||
obj.bMediaRate = stream.readInt();
|
||||
|
||||
for (int i = 0; i < obj.entries_subsegmentDuration.length; i++) {
|
||||
// reference type
|
||||
// referenced size
|
||||
stream.skipBytes(4);
|
||||
obj.entries_subsegmentDuration[i] = stream.readInt();// unsigned int
|
||||
return obj;
|
||||
}
|
||||
|
||||
// starts with SAP
|
||||
// SAP type
|
||||
// SAP delta entries_time
|
||||
stream.skipBytes(4);
|
||||
private Minf parse_minf(Box ref) throws IOException {
|
||||
Minf obj = new Minf();
|
||||
|
||||
Box b;
|
||||
while ((b = untilAnyBox(ref)) != null) {
|
||||
|
||||
switch (b.type) {
|
||||
case ATOM_DINF:
|
||||
obj.dinf = readFullBox(b);
|
||||
break;
|
||||
case ATOM_STBL:
|
||||
obj.stbl_stsd = parse_stbl(b);
|
||||
break;
|
||||
case ATOM_VMHD:
|
||||
case ATOM_SMHD:
|
||||
obj.$mhd = readFullBox(b);
|
||||
break;
|
||||
|
||||
}
|
||||
ensure(b);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private Tfra[] parse_mfra(Box ref, int trackCount) throws IOException {
|
||||
ArrayList<Tfra> tmp = new ArrayList<>(trackCount);
|
||||
long limit = ref.offset + ref.size;
|
||||
/**
|
||||
* this only read the "stsd" box inside
|
||||
*/
|
||||
private byte[] parse_stbl(Box ref) throws IOException {
|
||||
Box b = untilBox(ref, ATOM_STSD);
|
||||
|
||||
while (stream.position() < limit) {
|
||||
box = readBox();
|
||||
|
||||
if (box.type == ATOM_TFRA) {
|
||||
tmp.add(parse_tfra());
|
||||
}
|
||||
|
||||
ensure(box);
|
||||
if (b == null) {
|
||||
return new byte[0];// this never should happens (missing codec startup data)
|
||||
}
|
||||
|
||||
return tmp.toArray(new Tfra[tmp.size()]);
|
||||
return readFullBox(b);
|
||||
}
|
||||
|
||||
// </editor-fold>
|
||||
|
|
@ -679,14 +802,7 @@ public class Mp4DashReader {
|
|||
|
||||
int type;
|
||||
long offset;
|
||||
int size;
|
||||
}
|
||||
|
||||
class Sidx {
|
||||
|
||||
int timescale;
|
||||
int referenceId;
|
||||
int[] entries_subsegmentDuration;
|
||||
long size;
|
||||
}
|
||||
|
||||
public class Moof {
|
||||
|
|
@ -711,12 +827,16 @@ public class Mp4DashReader {
|
|||
int defaultSampleFlags;
|
||||
}
|
||||
|
||||
public class TrunEntry {
|
||||
class TrunEntry {
|
||||
|
||||
int sampleDuration;
|
||||
int sampleSize;
|
||||
int sampleFlags;
|
||||
int sampleCompositionTimeOffset;
|
||||
|
||||
boolean hasCompositionTimeOffset;
|
||||
boolean isKeyframe;
|
||||
|
||||
public int sampleDuration;
|
||||
public int sampleSize;
|
||||
public int sampleFlags;
|
||||
public int sampleCompositionTimeOffset;
|
||||
}
|
||||
|
||||
public class Trun {
|
||||
|
|
@ -749,6 +869,31 @@ public class Mp4DashReader {
|
|||
entry.sampleCompositionTimeOffset = buffer.getInt();
|
||||
}
|
||||
|
||||
entry.hasCompositionTimeOffset = hasFlag(bFlags, 0x0800);
|
||||
entry.isKeyframe = !hasFlag(entry.sampleFlags, 0x10000);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
public TrunEntry getAbsoluteEntry(int i, Tfhd header) {
|
||||
TrunEntry entry = getEntry(i);
|
||||
|
||||
if (!hasFlag(bFlags, 0x0100) && hasFlag(header.bFlags, 0x20)) {
|
||||
entry.sampleFlags = header.defaultSampleFlags;
|
||||
}
|
||||
|
||||
if (!hasFlag(bFlags, 0x0200) && hasFlag(header.bFlags, 0x10)) {
|
||||
entry.sampleSize = header.defaultSampleSize;
|
||||
}
|
||||
|
||||
if (!hasFlag(bFlags, 0x0100) && hasFlag(header.bFlags, 0x08)) {
|
||||
entry.sampleDuration = header.defaultSampleDuration;
|
||||
}
|
||||
|
||||
if (i == 0 && hasFlag(bFlags, 0x0004)) {
|
||||
entry.sampleFlags = bFirstSampleFlags;
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
|
@ -768,9 +913,9 @@ public class Mp4DashReader {
|
|||
public class Trak {
|
||||
|
||||
public Tkhd tkhd;
|
||||
public int mdia_mdhd_timeScale;
|
||||
public Elst edst_elst;
|
||||
public Mdia mdia;
|
||||
|
||||
byte[] mdia;
|
||||
}
|
||||
|
||||
class Mvhd {
|
||||
|
|
@ -786,12 +931,6 @@ public class Mp4DashReader {
|
|||
Trex[] mvex_trex;
|
||||
}
|
||||
|
||||
class Tfra {
|
||||
|
||||
int trackId;
|
||||
int[] entries_time;
|
||||
}
|
||||
|
||||
public class Trex {
|
||||
|
||||
private int trackId;
|
||||
|
|
@ -801,6 +940,34 @@ public class Mp4DashReader {
|
|||
int defaultSampleFlags;
|
||||
}
|
||||
|
||||
public class Elst {
|
||||
|
||||
public long MediaTime;
|
||||
public int bMediaRate;
|
||||
}
|
||||
|
||||
public class Mdia {
|
||||
|
||||
public int mdhd_timeScale;
|
||||
public byte[] mdhd;
|
||||
public Hdlr hdlr;
|
||||
public Minf minf;
|
||||
}
|
||||
|
||||
public class Hdlr {
|
||||
|
||||
public int type;
|
||||
public int subType;
|
||||
public byte[] bReserved;
|
||||
}
|
||||
|
||||
public class Minf {
|
||||
|
||||
public byte[] dinf;
|
||||
public byte[] stbl_stsd;
|
||||
public byte[] $mhd;
|
||||
}
|
||||
|
||||
public class Mp4Track {
|
||||
|
||||
public TrackKind kind;
|
||||
|
|
@ -808,10 +975,43 @@ public class Mp4DashReader {
|
|||
public Trex trex;
|
||||
}
|
||||
|
||||
public class Mp4TrackChunk {
|
||||
public class Mp4DashChunk {
|
||||
|
||||
public InputStream data;
|
||||
public Moof moof;
|
||||
private int i = 0;
|
||||
|
||||
public TrunEntry getNextSampleInfo() {
|
||||
if (i >= moof.traf.trun.entryCount) {
|
||||
return null;
|
||||
}
|
||||
return moof.traf.trun.getAbsoluteEntry(i++, moof.traf.tfhd);
|
||||
}
|
||||
|
||||
public Mp4DashSample getNextSample() throws IOException {
|
||||
if (data == null) {
|
||||
throw new IllegalStateException("This chunk has info only");
|
||||
}
|
||||
if (i >= moof.traf.trun.entryCount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Mp4DashSample sample = new Mp4DashSample();
|
||||
sample.info = moof.traf.trun.getAbsoluteEntry(i++, moof.traf.tfhd);
|
||||
sample.data = new byte[sample.info.sampleSize];
|
||||
|
||||
if (data.read(sample.data) != sample.info.sampleSize) {
|
||||
throw new EOFException("EOF reached while reading a sample");
|
||||
}
|
||||
|
||||
return sample;
|
||||
}
|
||||
}
|
||||
|
||||
public class Mp4DashSample {
|
||||
|
||||
public TrunEntry info;
|
||||
public byte[] data;
|
||||
}
|
||||
//</editor-fold>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,623 +0,0 @@
|
|||
package org.schabi.newpipe.streams;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Mp4TrackChunk;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Trak;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Trex;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.schabi.newpipe.streams.Mp4DashReader.hasFlag;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class Mp4DashWriter {
|
||||
|
||||
private final static byte DIMENSIONAL_FIVE = 5;
|
||||
private final static byte DIMENSIONAL_TWO = 2;
|
||||
private final static short DEFAULT_TIMESCALE = 1000;
|
||||
private final static int BUFFER_SIZE = 8 * 1024;
|
||||
private final static byte DEFAULT_TREX_SIZE = 32;
|
||||
private final static byte[] TFRA_TTS_DEFAULT = new byte[]{0x01, 0x01, 0x01};
|
||||
private final static int EPOCH_OFFSET = 2082844800;
|
||||
|
||||
private Mp4Track[] infoTracks;
|
||||
private SharpStream[] sourceTracks;
|
||||
|
||||
private Mp4DashReader[] readers;
|
||||
private final long time;
|
||||
|
||||
private boolean done = false;
|
||||
private boolean parsed = false;
|
||||
|
||||
private long written = 0;
|
||||
private ArrayList<ArrayList<Integer>> chunkTimes;
|
||||
private ArrayList<Long> moofOffsets;
|
||||
private ArrayList<Integer> fragSizes;
|
||||
|
||||
public Mp4DashWriter(SharpStream... source) {
|
||||
sourceTracks = source;
|
||||
readers = new Mp4DashReader[sourceTracks.length];
|
||||
infoTracks = new Mp4Track[sourceTracks.length];
|
||||
time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET;
|
||||
}
|
||||
|
||||
public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException {
|
||||
if (!parsed) {
|
||||
throw new IllegalStateException("All sources must be parsed first");
|
||||
}
|
||||
|
||||
return readers[sourceIndex].getAvailableTracks();
|
||||
}
|
||||
|
||||
public void parseSources() throws IOException, IllegalStateException {
|
||||
if (done) {
|
||||
throw new IllegalStateException("already done");
|
||||
}
|
||||
if (parsed) {
|
||||
throw new IllegalStateException("already parsed");
|
||||
}
|
||||
|
||||
try {
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
readers[i] = new Mp4DashReader(sourceTracks[i]);
|
||||
readers[i].parse();
|
||||
}
|
||||
|
||||
} finally {
|
||||
parsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void selectTracks(int... trackIndex) throws IOException {
|
||||
if (done) {
|
||||
throw new IOException("already done");
|
||||
}
|
||||
if (chunkTimes != null) {
|
||||
throw new IOException("tracks already selected");
|
||||
}
|
||||
|
||||
try {
|
||||
chunkTimes = new ArrayList<>(readers.length);
|
||||
moofOffsets = new ArrayList<>(32);
|
||||
fragSizes = new ArrayList<>(32);
|
||||
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
infoTracks[i] = readers[i].selectTrack(trackIndex[i]);
|
||||
|
||||
chunkTimes.add(new ArrayList<Integer>(32));
|
||||
}
|
||||
|
||||
} finally {
|
||||
parsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public long getBytesWritten() {
|
||||
return written;
|
||||
}
|
||||
|
||||
public void build(SharpStream out) throws IOException, RuntimeException {
|
||||
if (done) {
|
||||
throw new RuntimeException("already done");
|
||||
}
|
||||
if (!out.canWrite()) {
|
||||
throw new IOException("the provided output is not writable");
|
||||
}
|
||||
|
||||
long sidxOffsets = -1;
|
||||
int maxFrags = 0;
|
||||
|
||||
for (SharpStream stream : sourceTracks) {
|
||||
if (!stream.canRewind()) {
|
||||
sidxOffsets = -2;// sidx not available
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
dump(make_ftyp(), out);
|
||||
dump(make_moov(), out);
|
||||
|
||||
if (sidxOffsets == -1 && out.canRewind()) {
|
||||
//<editor-fold defaultstate="collapsed" desc="calculate sidx">
|
||||
int reserved = 0;
|
||||
for (Mp4DashReader reader : readers) {
|
||||
int count = reader.getFragmentsCount();
|
||||
if (count > maxFrags) {
|
||||
maxFrags = count;
|
||||
}
|
||||
reserved += 12 + calcSidxBodySize(count);
|
||||
}
|
||||
if (maxFrags > 0xFFFF) {
|
||||
sidxOffsets = -3;// TODO: to many fragments, needs a multi-sidx implementation
|
||||
} else {
|
||||
sidxOffsets = written;
|
||||
dump(make_free(reserved), out);
|
||||
}
|
||||
//</editor-fold>
|
||||
}
|
||||
ArrayList<Mp4TrackChunk> chunks = new ArrayList<>(readers.length);
|
||||
chunks.add(null);
|
||||
|
||||
int read;
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int sequenceNumber = 1;
|
||||
|
||||
while (true) {
|
||||
chunks.clear();
|
||||
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
Mp4TrackChunk chunk = readers[i].getNextChunk();
|
||||
if (chunk == null || chunk.moof.traf.trun.chunkSize < 1) {
|
||||
continue;
|
||||
}
|
||||
chunk.moof.traf.tfhd.trackId = i + 1;
|
||||
chunks.add(chunk);
|
||||
|
||||
if (sequenceNumber == 1) {
|
||||
if (chunk.moof.traf.trun.entryCount > 0 && hasFlag(chunk.moof.traf.trun.bFlags, 0x0800)) {
|
||||
chunkTimes.get(i).add(chunk.moof.traf.trun.getEntry(0).sampleCompositionTimeOffset);
|
||||
} else {
|
||||
chunkTimes.get(i).add(0);
|
||||
}
|
||||
}
|
||||
|
||||
chunkTimes.get(i).add(chunk.moof.traf.trun.chunkDuration);
|
||||
}
|
||||
|
||||
if (chunks.size() < 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
long offset = written;
|
||||
moofOffsets.add(offset);
|
||||
|
||||
dump(make_moof(sequenceNumber++, chunks, offset), out);
|
||||
dump(make_mdat(chunks), out);
|
||||
|
||||
for (Mp4TrackChunk chunk : chunks) {
|
||||
while ((read = chunk.data.read(buffer)) > 0) {
|
||||
out.write(buffer, 0, read);
|
||||
written += read;
|
||||
}
|
||||
}
|
||||
|
||||
fragSizes.add((int) (written - offset));
|
||||
}
|
||||
|
||||
dump(make_mfra(), out);
|
||||
|
||||
if (sidxOffsets > 0 && moofOffsets.size() == maxFrags) {
|
||||
long len = written;
|
||||
|
||||
out.rewind();
|
||||
out.skip(sidxOffsets);
|
||||
|
||||
written = sidxOffsets;
|
||||
sidxOffsets = moofOffsets.get(0);
|
||||
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
dump(make_sidx(i, sidxOffsets - written), out);
|
||||
}
|
||||
|
||||
written = len;
|
||||
}
|
||||
} finally {
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return done;
|
||||
}
|
||||
|
||||
public boolean isParsed() {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
done = true;
|
||||
parsed = true;
|
||||
|
||||
for (SharpStream src : sourceTracks) {
|
||||
src.dispose();
|
||||
}
|
||||
|
||||
sourceTracks = null;
|
||||
readers = null;
|
||||
infoTracks = null;
|
||||
moofOffsets = null;
|
||||
chunkTimes = null;
|
||||
}
|
||||
|
||||
// <editor-fold defaultstate="collapsed" desc="Utils">
|
||||
private void dump(byte[][] buffer, SharpStream stream) throws IOException {
|
||||
for (byte[] buff : buffer) {
|
||||
stream.write(buff);
|
||||
written += buff.length;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[][] lengthFor(byte[][] buffer) {
|
||||
int length = 0;
|
||||
for (byte[] buff : buffer) {
|
||||
length += buff.length;
|
||||
}
|
||||
|
||||
ByteBuffer.wrap(buffer[0]).putInt(length);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private int calcSidxBodySize(int entryCount) {
|
||||
return 4 + 4 + 8 + 8 + 4 + (entryCount * 12);
|
||||
}
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultstate="collapsed" desc="Box makers">
|
||||
private byte[][] make_moof(int sequence, ArrayList<Mp4TrackChunk> chunks, long referenceOffset) {
|
||||
int pos = 2;
|
||||
TrunExtra[] extra = new TrunExtra[chunks.size()];
|
||||
|
||||
byte[][] buffer = new byte[pos + (extra.length * DIMENSIONAL_FIVE)][];
|
||||
buffer[0] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x66,// info header
|
||||
0x00, 0x00, 0x00, 0x10, 0x6D, 0x66, 0x68, 0x64, 0x00, 0x00, 0x00, 0x00//mfhd
|
||||
};
|
||||
buffer[1] = new byte[4];
|
||||
ByteBuffer.wrap(buffer[1]).putInt(sequence);
|
||||
|
||||
for (int i = 0; i < extra.length; i++) {
|
||||
extra[i] = new TrunExtra();
|
||||
for (byte[] buff : make_traf(chunks.get(i), extra[i], referenceOffset)) {
|
||||
buffer[pos++] = buff;
|
||||
}
|
||||
}
|
||||
|
||||
lengthFor(buffer);
|
||||
|
||||
int offset = 8 + ByteBuffer.wrap(buffer[0]).getInt();
|
||||
|
||||
for (int i = 0; i < extra.length; i++) {
|
||||
extra[i].byteBuffer.putInt(offset);
|
||||
offset += chunks.get(i).moof.traf.trun.chunkSize;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private byte[][] make_traf(Mp4TrackChunk chunk, TrunExtra extra, long moofOffset) {
|
||||
byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
|
||||
buffer[0] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x66,
|
||||
0x00, 0x00, 0x00, 0x00, 0x74, 0x66, 0x68, 0x64
|
||||
};
|
||||
|
||||
int flags = (chunk.moof.traf.tfhd.bFlags & 0x38) | 0x01;
|
||||
byte tfhdBodySize = 8 + 8;
|
||||
if (hasFlag(flags, 0x08)) {
|
||||
tfhdBodySize += 4;
|
||||
}
|
||||
if (hasFlag(flags, 0x10)) {
|
||||
tfhdBodySize += 4;
|
||||
}
|
||||
if (hasFlag(flags, 0x20)) {
|
||||
tfhdBodySize += 4;
|
||||
}
|
||||
buffer[1] = new byte[tfhdBodySize];
|
||||
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
|
||||
set.position(4);
|
||||
set.putInt(chunk.moof.traf.tfhd.trackId);
|
||||
set.putLong(moofOffset);
|
||||
if (hasFlag(flags, 0x08)) {
|
||||
set.putInt(chunk.moof.traf.tfhd.defaultSampleDuration);
|
||||
}
|
||||
if (hasFlag(flags, 0x10)) {
|
||||
set.putInt(chunk.moof.traf.tfhd.defaultSampleSize);
|
||||
}
|
||||
if (hasFlag(flags, 0x20)) {
|
||||
set.putInt(chunk.moof.traf.tfhd.defaultSampleFlags);
|
||||
}
|
||||
set.putInt(0, flags);
|
||||
ByteBuffer.wrap(buffer[0]).putInt(8, 8 + tfhdBodySize);
|
||||
|
||||
buffer[2] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x14,
|
||||
0x74, 0x66, 0x64, 0x74,
|
||||
0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
ByteBuffer.wrap(buffer[2]).putLong(12, chunk.moof.traf.tfdt);
|
||||
|
||||
buffer[3] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x75, 0x6E,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
buffer[4] = chunk.moof.traf.trun.bEntries;
|
||||
|
||||
lengthFor(buffer);
|
||||
|
||||
set = ByteBuffer.wrap(buffer[3]);
|
||||
set.putInt(buffer[3].length + buffer[4].length);
|
||||
set.position(8);
|
||||
set.putInt((chunk.moof.traf.trun.bFlags | 0x01) & 0x0F01);
|
||||
set.putInt(chunk.moof.traf.trun.entryCount);
|
||||
extra.byteBuffer = set;
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private byte[][] make_mdat(ArrayList<Mp4TrackChunk> chunks) {
|
||||
byte[][] buffer = new byte[][]{
|
||||
{
|
||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x61, 0x74
|
||||
}
|
||||
};
|
||||
|
||||
int length = 0;
|
||||
|
||||
for (Mp4TrackChunk chunk : chunks) {
|
||||
length += chunk.moof.traf.trun.chunkSize;
|
||||
}
|
||||
|
||||
ByteBuffer.wrap(buffer[0]).putInt(length + 8);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private byte[][] make_ftyp() {
|
||||
return new byte[][]{
|
||||
{
|
||||
0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x64, 0x61, 0x73, 0x68, 0x00, 0x00, 0x00, 0x00,
|
||||
0x6D, 0x70, 0x34, 0x31, 0x69, 0x73, 0x6F, 0x6D, 0x69, 0x73, 0x6F, 0x36, 0x69, 0x73, 0x6F, 0x32
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private byte[][] make_mvhd() {
|
||||
byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
|
||||
|
||||
buffer[0] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 0x00
|
||||
};
|
||||
buffer[1] = new byte[28];
|
||||
buffer[2] = new byte[]{
|
||||
0x00, 0x01, 0x00, 0x00, 0x01, 0x00,// default volume and rate
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,// reserved values
|
||||
// default matrix
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x40, 0x00, 0x00, 0x00
|
||||
};
|
||||
buffer[3] = new byte[24];// predefined
|
||||
buffer[4] = ByteBuffer.allocate(4).putInt(infoTracks.length + 1).array();
|
||||
|
||||
long longestTrack = 0;
|
||||
|
||||
for (Mp4Track track : infoTracks) {
|
||||
long tmp = (long) ((track.trak.tkhd.duration / (double) track.trak.mdia_mdhd_timeScale) * DEFAULT_TIMESCALE);
|
||||
if (tmp > longestTrack) {
|
||||
longestTrack = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuffer.wrap(buffer[1])
|
||||
.putLong(time)
|
||||
.putLong(time)
|
||||
.putInt(DEFAULT_TIMESCALE)
|
||||
.putLong(longestTrack);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private byte[][] make_trak(int trackId, Trak trak) throws RuntimeException {
|
||||
if (trak.tkhd.matrix.length != 36) {
|
||||
throw new RuntimeException("bad track matrix length (expected 36)");
|
||||
}
|
||||
|
||||
byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
|
||||
|
||||
buffer[0] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B,// trak header
|
||||
0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 // tkhd header
|
||||
};
|
||||
buffer[1] = new byte[48];
|
||||
buffer[2] = trak.tkhd.matrix;
|
||||
buffer[3] = new byte[8];
|
||||
buffer[4] = trak.mdia;
|
||||
|
||||
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
|
||||
set.putLong(time);
|
||||
set.putLong(time);
|
||||
set.putInt(trackId);
|
||||
set.position(24);
|
||||
set.putLong(trak.tkhd.duration);
|
||||
set.position(40);
|
||||
set.putShort(trak.tkhd.bLayer);
|
||||
set.putShort(trak.tkhd.bAlternateGroup);
|
||||
set.putShort(trak.tkhd.bVolume);
|
||||
|
||||
ByteBuffer.wrap(buffer[3])
|
||||
.putInt(trak.tkhd.bWidth)
|
||||
.putInt(trak.tkhd.bHeight);
|
||||
|
||||
return lengthFor(buffer);
|
||||
}
|
||||
|
||||
private byte[][] make_moov() throws RuntimeException {
|
||||
int pos = 1;
|
||||
byte[][] buffer = new byte[2 + (DIMENSIONAL_TWO * infoTracks.length) + (DIMENSIONAL_FIVE * infoTracks.length) + DIMENSIONAL_FIVE + 1][];
|
||||
|
||||
buffer[0] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x76
|
||||
};
|
||||
|
||||
for (byte[] buff : make_mvhd()) {
|
||||
buffer[pos++] = buff;
|
||||
}
|
||||
|
||||
for (int i = 0; i < infoTracks.length; i++) {
|
||||
for (byte[] buff : make_trak(i + 1, infoTracks[i].trak)) {
|
||||
buffer[pos++] = buff;
|
||||
}
|
||||
}
|
||||
|
||||
buffer[pos] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x76, 0x65, 0x78
|
||||
};
|
||||
|
||||
ByteBuffer.wrap(buffer[pos++]).putInt((infoTracks.length * DEFAULT_TREX_SIZE) + 8);
|
||||
|
||||
for (int i = 0; i < infoTracks.length; i++) {
|
||||
for (byte[] buff : make_trex(i + 1, infoTracks[i].trex)) {
|
||||
buffer[pos++] = buff;
|
||||
}
|
||||
}
|
||||
|
||||
// default udta
|
||||
buffer[pos] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x5C, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x54, 0x6D, 0x65, 0x74, 0x61,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00,
|
||||
0x1F, (byte) 0xA9, 0x63, 0x6D, 0x74, 0x00, 0x00, 0x00, 0x17, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00,
|
||||
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
|
||||
};
|
||||
|
||||
return lengthFor(buffer);
|
||||
}
|
||||
|
||||
private byte[][] make_trex(int trackId, Trex trex) {
|
||||
byte[][] buffer = new byte[][]{
|
||||
{
|
||||
0x00, 0x00, 0x00, 0x20, 0x74, 0x72, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00
|
||||
},
|
||||
new byte[20]
|
||||
};
|
||||
|
||||
ByteBuffer.wrap(buffer[1])
|
||||
.putInt(trackId)
|
||||
.putInt(trex.defaultSampleDescriptionIndex)
|
||||
.putInt(trex.defaultSampleDuration)
|
||||
.putInt(trex.defaultSampleSize)
|
||||
.putInt(trex.defaultSampleFlags);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private byte[][] make_tfra(int trackId, List<Integer> times, List<Long> moofOffsets) {
|
||||
int entryCount = times.size() - 1;
|
||||
byte[][] buffer = new byte[DIMENSIONAL_TWO][];
|
||||
buffer[0] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x74, 0x66, 0x72, 0x61, 0x01, 0x00, 0x00, 0x00
|
||||
};
|
||||
buffer[1] = new byte[12 + ((16 + TFRA_TTS_DEFAULT.length) * entryCount)];
|
||||
|
||||
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
|
||||
set.putInt(trackId);
|
||||
set.position(8);
|
||||
set.putInt(entryCount);
|
||||
|
||||
long decodeTime = 0;
|
||||
|
||||
for (int i = 0; i < entryCount; i++) {
|
||||
decodeTime += times.get(i);
|
||||
set.putLong(decodeTime);
|
||||
set.putLong(moofOffsets.get(i));
|
||||
set.put(TFRA_TTS_DEFAULT);// default values: traf number/trun number/sample number
|
||||
}
|
||||
|
||||
return lengthFor(buffer);
|
||||
}
|
||||
|
||||
private byte[][] make_mfra() {
|
||||
byte[][] buffer = new byte[2 + (DIMENSIONAL_TWO * infoTracks.length)][];
|
||||
buffer[0] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x66, 0x72, 0x61
|
||||
};
|
||||
int pos = 1;
|
||||
|
||||
for (int i = 0; i < infoTracks.length; i++) {
|
||||
for (byte[] buff : make_tfra(i + 1, chunkTimes.get(i), moofOffsets)) {
|
||||
buffer[pos++] = buff;
|
||||
}
|
||||
}
|
||||
|
||||
buffer[pos] = new byte[]{// mfro
|
||||
0x00, 0x00, 0x00, 0x10, 0x6D, 0x66, 0x72, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
lengthFor(buffer);
|
||||
|
||||
ByteBuffer set = ByteBuffer.wrap(buffer[pos]);
|
||||
set.position(12);
|
||||
set.put(buffer[0], 0, 4);
|
||||
|
||||
return buffer;
|
||||
|
||||
}
|
||||
|
||||
private byte[][] make_sidx(int internalTrackId, long firstOffset) {
|
||||
List<Integer> times = chunkTimes.get(internalTrackId);
|
||||
int count = times.size() - 1;// the first item is ignored (composition time)
|
||||
|
||||
if (count > 65535) {
|
||||
throw new OutOfMemoryError("to many fragments. sidx limit is 65535, found " + String.valueOf(count));
|
||||
}
|
||||
|
||||
byte[][] buffer = new byte[][]{
|
||||
new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x73, 0x69, 0x64, 0x78, 0x01, 0x00, 0x00, 0x00
|
||||
},
|
||||
new byte[calcSidxBodySize(count)]
|
||||
};
|
||||
|
||||
lengthFor(buffer);
|
||||
|
||||
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
|
||||
set.putInt(internalTrackId + 1);
|
||||
set.putInt(infoTracks[internalTrackId].trak.mdia_mdhd_timeScale);
|
||||
set.putLong(0);
|
||||
set.putLong(firstOffset - ByteBuffer.wrap(buffer[0]).getInt());
|
||||
set.putInt(0xFFFF & count);// unsigned
|
||||
|
||||
int i = 0;
|
||||
while (i < count) {
|
||||
set.putInt(fragSizes.get(i) & 0x7fffffff);// default reference type is 0
|
||||
set.putInt(times.get(i + 1));
|
||||
set.putInt(0x90000000);// default SAP settings
|
||||
i++;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private byte[][] make_free(int totalSize) {
|
||||
return lengthFor(new byte[][]{
|
||||
new byte[]{0x00, 0x00, 0x00, 0x00, 0x66, 0x72, 0x65, 0x65},
|
||||
new byte[totalSize - 8]// this is waste of RAM
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
//</editor-fold>
|
||||
|
||||
class TrunExtra {
|
||||
|
||||
ByteBuffer byteBuffer;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,810 @@
|
|||
package org.schabi.newpipe.streams;
|
||||
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Hdlr;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Mdia;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashChunk;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashSample;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.TrunEntry;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class Mp4FromDashWriter {
|
||||
|
||||
private final static int EPOCH_OFFSET = 2082844800;
|
||||
private final static short DEFAULT_TIMESCALE = 1000;
|
||||
private final static byte SAMPLES_PER_CHUNK_INIT = 2;
|
||||
private final static byte SAMPLES_PER_CHUNK = 6;// ffmpeg uses 2, basic uses 1 (with 60fps uses 21 or 22). NewPipe will use 6
|
||||
private final static long THRESHOLD_FOR_CO64 = 0xFFFEFFFFL;// near 3.999 GiB
|
||||
private final static int THRESHOLD_MOOV_LENGTH = (256 * 1024) + (2048 * 1024); // 2.2 MiB enough for: 1080p 60fps 00h35m00s
|
||||
|
||||
private final long time;
|
||||
|
||||
private ByteBuffer auxBuffer;
|
||||
private SharpStream outStream;
|
||||
|
||||
private long lastWriteOffset = -1;
|
||||
private long writeOffset;
|
||||
|
||||
private boolean moovSimulation = true;
|
||||
|
||||
private boolean done = false;
|
||||
private boolean parsed = false;
|
||||
|
||||
private Mp4Track[] tracks;
|
||||
private SharpStream[] sourceTracks;
|
||||
|
||||
private Mp4DashReader[] readers;
|
||||
private Mp4DashChunk[] readersChunks;
|
||||
|
||||
private int overrideMainBrand = 0x00;
|
||||
|
||||
public Mp4FromDashWriter(SharpStream... sources) throws IOException {
|
||||
for (SharpStream src : sources) {
|
||||
if (!src.canRewind() && !src.canRead()) {
|
||||
throw new IOException("All sources must be readable and allow rewind");
|
||||
}
|
||||
}
|
||||
|
||||
sourceTracks = sources;
|
||||
readers = new Mp4DashReader[sourceTracks.length];
|
||||
readersChunks = new Mp4DashChunk[readers.length];
|
||||
time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET;
|
||||
}
|
||||
|
||||
public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException {
|
||||
if (!parsed) {
|
||||
throw new IllegalStateException("All sources must be parsed first");
|
||||
}
|
||||
|
||||
return readers[sourceIndex].getAvailableTracks();
|
||||
}
|
||||
|
||||
public void parseSources() throws IOException, IllegalStateException {
|
||||
if (done) {
|
||||
throw new IllegalStateException("already done");
|
||||
}
|
||||
if (parsed) {
|
||||
throw new IllegalStateException("already parsed");
|
||||
}
|
||||
|
||||
try {
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
readers[i] = new Mp4DashReader(sourceTracks[i]);
|
||||
readers[i].parse();
|
||||
}
|
||||
|
||||
} finally {
|
||||
parsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void selectTracks(int... trackIndex) throws IOException {
|
||||
if (done) {
|
||||
throw new IOException("already done");
|
||||
}
|
||||
if (tracks != null) {
|
||||
throw new IOException("tracks already selected");
|
||||
}
|
||||
|
||||
try {
|
||||
tracks = new Mp4Track[readers.length];
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
tracks[i] = readers[i].selectTrack(trackIndex[i]);
|
||||
}
|
||||
} finally {
|
||||
parsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void setMainBrand(int brandId) {
|
||||
overrideMainBrand = brandId;
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return done;
|
||||
}
|
||||
|
||||
public boolean isParsed() {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
done = true;
|
||||
parsed = true;
|
||||
|
||||
for (SharpStream src : sourceTracks) {
|
||||
src.close();
|
||||
}
|
||||
|
||||
tracks = null;
|
||||
sourceTracks = null;
|
||||
|
||||
readers = null;
|
||||
readersChunks = null;
|
||||
|
||||
auxBuffer = null;
|
||||
outStream = null;
|
||||
}
|
||||
|
||||
public void build(SharpStream output) throws IOException {
|
||||
if (done) {
|
||||
throw new RuntimeException("already done");
|
||||
}
|
||||
if (!output.canWrite()) {
|
||||
throw new IOException("the provided output is not writable");
|
||||
}
|
||||
|
||||
//
|
||||
// WARNING: the muxer requires at least 8 samples of every track
|
||||
// not allowed for very short tracks (less than 0.5 seconds)
|
||||
//
|
||||
outStream = output;
|
||||
int read = 8;// mdat box header size
|
||||
long totalSampleSize = 0;
|
||||
int[] sampleExtra = new int[readers.length];
|
||||
int[] defaultMediaTime = new int[readers.length];
|
||||
int[] defaultSampleDuration = new int[readers.length];
|
||||
int[] sampleCount = new int[readers.length];
|
||||
|
||||
TablesInfo[] tablesInfo = new TablesInfo[tracks.length];
|
||||
for (int i = 0; i < tablesInfo.length; i++) {
|
||||
tablesInfo[i] = new TablesInfo();
|
||||
}
|
||||
|
||||
//<editor-fold defaultstate="expanded" desc="calculate stbl sample tables size and required moov values">
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
int samplesSize = 0;
|
||||
int sampleSizeChanges = 0;
|
||||
int compositionOffsetLast = -1;
|
||||
|
||||
Mp4DashChunk chunk;
|
||||
while ((chunk = readers[i].getNextChunk(true)) != null) {
|
||||
|
||||
if (defaultMediaTime[i] < 1 && chunk.moof.traf.tfhd.defaultSampleDuration > 0) {
|
||||
defaultMediaTime[i] = chunk.moof.traf.tfhd.defaultSampleDuration;
|
||||
}
|
||||
|
||||
read += chunk.moof.traf.trun.chunkSize;
|
||||
sampleExtra[i] += chunk.moof.traf.trun.chunkDuration;// calculate track duration
|
||||
|
||||
TrunEntry info;
|
||||
while ((info = chunk.getNextSampleInfo()) != null) {
|
||||
if (info.isKeyframe) {
|
||||
tablesInfo[i].stss++;
|
||||
}
|
||||
|
||||
if (info.sampleDuration > defaultSampleDuration[i]) {
|
||||
defaultSampleDuration[i] = info.sampleDuration;
|
||||
}
|
||||
|
||||
tablesInfo[i].stsz++;
|
||||
if (samplesSize != info.sampleSize) {
|
||||
samplesSize = info.sampleSize;
|
||||
sampleSizeChanges++;
|
||||
}
|
||||
|
||||
if (info.hasCompositionTimeOffset) {
|
||||
if (info.sampleCompositionTimeOffset != compositionOffsetLast) {
|
||||
tablesInfo[i].ctts++;
|
||||
compositionOffsetLast = info.sampleCompositionTimeOffset;
|
||||
}
|
||||
}
|
||||
|
||||
totalSampleSize += info.sampleSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultMediaTime[i] < 1) {
|
||||
defaultMediaTime[i] = defaultSampleDuration[i];
|
||||
}
|
||||
|
||||
readers[i].rewind();
|
||||
|
||||
int tmp = tablesInfo[i].stsz - SAMPLES_PER_CHUNK_INIT;
|
||||
tablesInfo[i].stco = (tmp / SAMPLES_PER_CHUNK) + 1;// +1 for samples in first chunk
|
||||
|
||||
tmp = tmp % SAMPLES_PER_CHUNK;
|
||||
if (tmp == 0) {
|
||||
tablesInfo[i].stsc = 2;// first chunk (init) and succesive chunks
|
||||
tablesInfo[i].stsc_bEntries = new int[]{
|
||||
1, SAMPLES_PER_CHUNK_INIT, 1,
|
||||
2, SAMPLES_PER_CHUNK, 1
|
||||
};
|
||||
} else {
|
||||
tablesInfo[i].stsc = 3;// first chunk (init) and succesive chunks and remain chunk
|
||||
tablesInfo[i].stsc_bEntries = new int[]{
|
||||
1, SAMPLES_PER_CHUNK_INIT, 1,
|
||||
2, SAMPLES_PER_CHUNK, 1,
|
||||
tablesInfo[i].stco + 1, tmp, 1
|
||||
};
|
||||
tablesInfo[i].stco++;
|
||||
}
|
||||
|
||||
sampleCount[i] = tablesInfo[i].stsz;
|
||||
|
||||
if (sampleSizeChanges == 1) {
|
||||
tablesInfo[i].stsz = 0;
|
||||
tablesInfo[i].stsz_default = samplesSize;
|
||||
} else {
|
||||
tablesInfo[i].stsz_default = 0;
|
||||
}
|
||||
|
||||
if (tablesInfo[i].stss == tablesInfo[i].stsz) {
|
||||
tablesInfo[i].stss = -1;// for audio tracks (all samples are keyframes)
|
||||
}
|
||||
|
||||
// ensure track duration
|
||||
if (tracks[i].trak.tkhd.duration < 1) {
|
||||
tracks[i].trak.tkhd.duration = sampleExtra[i];// this never should happen
|
||||
}
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
boolean is64 = read > THRESHOLD_FOR_CO64;
|
||||
|
||||
// calculate the moov size;
|
||||
int auxSize = make_moov(defaultMediaTime, tablesInfo, is64);
|
||||
|
||||
if (auxSize < THRESHOLD_MOOV_LENGTH) {
|
||||
auxBuffer = ByteBuffer.allocate(auxSize);// cache moov in the memory
|
||||
}
|
||||
|
||||
moovSimulation = false;
|
||||
writeOffset = 0;
|
||||
|
||||
final int ftyp_size = make_ftyp();
|
||||
|
||||
// reserve moov space in the output stream
|
||||
/*if (outStream.canSetLength()) {
|
||||
long length = writeOffset + auxSize;
|
||||
outStream.setLength(length);
|
||||
outSeek(length);
|
||||
} else {*/
|
||||
if (auxSize > 0) {
|
||||
int length = auxSize;
|
||||
byte[] buffer = new byte[8 * 1024];// 8 KiB
|
||||
while (length > 0) {
|
||||
int count = Math.min(length, buffer.length);
|
||||
outWrite(buffer, 0, count);
|
||||
length -= count;
|
||||
}
|
||||
}
|
||||
|
||||
if (auxBuffer == null) {
|
||||
outSeek(ftyp_size);
|
||||
}
|
||||
|
||||
// tablesInfo contais row counts
|
||||
// and after returning from make_moov() will contain table offsets
|
||||
make_moov(defaultMediaTime, tablesInfo, is64);
|
||||
|
||||
// write tables: stts stsc
|
||||
// reset for ctts table: sampleCount sampleExtra
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
writeEntryArray(tablesInfo[i].stts, 2, sampleCount[i], defaultSampleDuration[i]);
|
||||
writeEntryArray(tablesInfo[i].stsc, tablesInfo[i].stsc_bEntries.length, tablesInfo[i].stsc_bEntries);
|
||||
tablesInfo[i].stsc_bEntries = null;
|
||||
if (tablesInfo[i].ctts > 0) {
|
||||
sampleCount[i] = 1;// index is not base zero
|
||||
sampleExtra[i] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (auxBuffer == null) {
|
||||
outRestore();
|
||||
}
|
||||
|
||||
outWrite(make_mdat(totalSampleSize, is64));
|
||||
|
||||
int[] sampleIndex = new int[readers.length];
|
||||
int[] sizes = new int[SAMPLES_PER_CHUNK];
|
||||
int[] sync = new int[SAMPLES_PER_CHUNK];
|
||||
|
||||
int written = readers.length;
|
||||
while (written > 0) {
|
||||
written = 0;
|
||||
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
if (sampleIndex[i] < 0) {
|
||||
continue;// track is done
|
||||
}
|
||||
|
||||
long chunkOffset = writeOffset;
|
||||
int syncCount = 0;
|
||||
int limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK;
|
||||
|
||||
int j = 0;
|
||||
for (; j < limit; j++) {
|
||||
Mp4DashSample sample = getNextSample(i);
|
||||
|
||||
if (sample == null) {
|
||||
if (tablesInfo[i].ctts > 0 && sampleExtra[i] >= 0) {
|
||||
writeEntryArray(tablesInfo[i].ctts, 1, sampleCount[i], sampleExtra[i]);// flush last entries
|
||||
}
|
||||
sampleIndex[i] = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
sampleIndex[i]++;
|
||||
|
||||
if (tablesInfo[i].ctts > 0) {
|
||||
if (sample.info.sampleCompositionTimeOffset == sampleExtra[i]) {
|
||||
sampleCount[i]++;
|
||||
} else {
|
||||
if (sampleExtra[i] >= 0) {
|
||||
tablesInfo[i].ctts = writeEntryArray(tablesInfo[i].ctts, 2, sampleCount[i], sampleExtra[i]);
|
||||
outRestore();
|
||||
}
|
||||
sampleCount[i] = 1;
|
||||
sampleExtra[i] = sample.info.sampleCompositionTimeOffset;
|
||||
}
|
||||
}
|
||||
|
||||
if (tablesInfo[i].stss > 0 && sample.info.isKeyframe) {
|
||||
sync[syncCount++] = sampleIndex[i];
|
||||
}
|
||||
|
||||
if (tablesInfo[i].stsz > 0) {
|
||||
sizes[j] = sample.data.length;
|
||||
}
|
||||
|
||||
outWrite(sample.data, 0, sample.data.length);
|
||||
}
|
||||
|
||||
if (j > 0) {
|
||||
written++;
|
||||
|
||||
if (tablesInfo[i].stsz > 0) {
|
||||
tablesInfo[i].stsz = writeEntryArray(tablesInfo[i].stsz, j, sizes);
|
||||
}
|
||||
|
||||
if (syncCount > 0) {
|
||||
tablesInfo[i].stss = writeEntryArray(tablesInfo[i].stss, syncCount, sync);
|
||||
}
|
||||
|
||||
if (is64) {
|
||||
tablesInfo[i].stco = writeEntry64(tablesInfo[i].stco, chunkOffset);
|
||||
} else {
|
||||
tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, (int) chunkOffset);
|
||||
}
|
||||
|
||||
outRestore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (auxBuffer != null) {
|
||||
// dump moov
|
||||
outSeek(ftyp_size);
|
||||
outStream.write(auxBuffer.array(), 0, auxBuffer.capacity());
|
||||
auxBuffer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private Mp4DashSample getNextSample(int track) throws IOException {
|
||||
if (readersChunks[track] == null) {
|
||||
readersChunks[track] = readers[track].getNextChunk(false);
|
||||
if (readersChunks[track] == null) {
|
||||
return null;// EOF reached
|
||||
}
|
||||
}
|
||||
|
||||
Mp4DashSample sample = readersChunks[track].getNextSample();
|
||||
if (sample == null) {
|
||||
readersChunks[track] = null;
|
||||
return getNextSample(track);
|
||||
} else {
|
||||
return sample;
|
||||
}
|
||||
}
|
||||
|
||||
// <editor-fold defaultstate="expanded" desc="Stbl handling">
|
||||
private int writeEntry64(int offset, long value) throws IOException {
|
||||
outBackup();
|
||||
|
||||
auxSeek(offset);
|
||||
auxWrite(ByteBuffer.allocate(8).putLong(value).array());
|
||||
|
||||
return offset + 8;
|
||||
}
|
||||
|
||||
private int writeEntryArray(int offset, int count, int... values) throws IOException {
|
||||
outBackup();
|
||||
|
||||
auxSeek(offset);
|
||||
|
||||
int size = count * 4;
|
||||
ByteBuffer buffer = ByteBuffer.allocate(size);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
buffer.putInt(values[i]);
|
||||
}
|
||||
|
||||
auxWrite(buffer.array());
|
||||
|
||||
return offset + size;
|
||||
}
|
||||
|
||||
private void outBackup() {
|
||||
if (auxBuffer == null && lastWriteOffset < 0) {
|
||||
lastWriteOffset = writeOffset;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore to the previous position before the first call to writeEntry64()
|
||||
* or writeEntryArray() methods.
|
||||
*/
|
||||
private void outRestore() throws IOException {
|
||||
if (lastWriteOffset > 0) {
|
||||
outSeek(lastWriteOffset);
|
||||
lastWriteOffset = -1;
|
||||
}
|
||||
}
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultstate="expanded" desc="Utils">
|
||||
private void outWrite(byte[] buffer) throws IOException {
|
||||
outWrite(buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
private void outWrite(byte[] buffer, int offset, int count) throws IOException {
|
||||
writeOffset += count;
|
||||
outStream.write(buffer, offset, count);
|
||||
}
|
||||
|
||||
private void outSeek(long offset) throws IOException {
|
||||
if (outStream.canSeek()) {
|
||||
outStream.seek(offset);
|
||||
writeOffset = offset;
|
||||
} else if (outStream.canRewind()) {
|
||||
outStream.rewind();
|
||||
writeOffset = 0;
|
||||
outSkip(offset);
|
||||
} else {
|
||||
throw new IOException("cannot seek or rewind the output stream");
|
||||
}
|
||||
}
|
||||
|
||||
private void outSkip(long amount) throws IOException {
|
||||
outStream.skip(amount);
|
||||
writeOffset += amount;
|
||||
}
|
||||
|
||||
private int lengthFor(int offset) throws IOException {
|
||||
int size = auxOffset() - offset;
|
||||
|
||||
if (moovSimulation) {
|
||||
return size;
|
||||
}
|
||||
|
||||
auxSeek(offset);
|
||||
auxWrite(size);
|
||||
auxSkip(size - 4);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
private int make(int type, int extra, int columns, int rows) throws IOException {
|
||||
final byte base = 16;
|
||||
int size = columns * rows * 4;
|
||||
int total = size + base;
|
||||
int offset = auxOffset();
|
||||
|
||||
if (extra >= 0) {
|
||||
total += 4;
|
||||
}
|
||||
|
||||
auxWrite(ByteBuffer.allocate(12)
|
||||
.putInt(total)
|
||||
.putInt(type)
|
||||
.putInt(0x00)// default version & flags
|
||||
.array()
|
||||
);
|
||||
|
||||
if (extra >= 0) {
|
||||
//size += 4;// commented for auxiliar buffer !!!
|
||||
offset += 4;
|
||||
auxWrite(extra);
|
||||
}
|
||||
|
||||
auxWrite(rows);
|
||||
auxSkip(size);
|
||||
|
||||
return offset + base;
|
||||
}
|
||||
|
||||
private void auxWrite(int value) throws IOException {
|
||||
auxWrite(ByteBuffer.allocate(4)
|
||||
.putInt(value)
|
||||
.array()
|
||||
);
|
||||
}
|
||||
|
||||
private void auxWrite(byte[] buffer) throws IOException {
|
||||
if (moovSimulation) {
|
||||
writeOffset += buffer.length;
|
||||
} else if (auxBuffer == null) {
|
||||
outWrite(buffer, 0, buffer.length);
|
||||
} else {
|
||||
auxBuffer.put(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private void auxSeek(int offset) throws IOException {
|
||||
if (moovSimulation) {
|
||||
writeOffset = offset;
|
||||
} else if (auxBuffer == null) {
|
||||
outSeek(offset);
|
||||
} else {
|
||||
auxBuffer.position(offset);
|
||||
}
|
||||
}
|
||||
|
||||
private void auxSkip(int amount) throws IOException {
|
||||
if (moovSimulation) {
|
||||
writeOffset += amount;
|
||||
} else if (auxBuffer == null) {
|
||||
outSkip(amount);
|
||||
} else {
|
||||
auxBuffer.position(auxBuffer.position() + amount);
|
||||
}
|
||||
}
|
||||
|
||||
private int auxOffset() {
|
||||
return auxBuffer == null ? (int) writeOffset : auxBuffer.position();
|
||||
}
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultstate="expanded" desc="Box makers">
|
||||
private int make_ftyp() throws IOException {
|
||||
byte[] buffer = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70,// ftyp
|
||||
0x6D, 0x70, 0x34, 0x32,// mayor brand (mp42)
|
||||
0x00, 0x00, 0x02, 0x00,// default minor version (512)
|
||||
0x6D, 0x70, 0x34, 0x31, 0x69, 0x73, 0x6F, 0x6D, 0x69, 0x73, 0x6F, 0x32// compatible brands: mp41 isom iso2
|
||||
};
|
||||
|
||||
if (overrideMainBrand != 0)
|
||||
ByteBuffer.wrap(buffer).putInt(8, overrideMainBrand);
|
||||
|
||||
outWrite(buffer);
|
||||
|
||||
return buffer.length;
|
||||
}
|
||||
|
||||
private byte[] make_mdat(long refSize, boolean is64) {
|
||||
if (is64) {
|
||||
refSize += 16;
|
||||
} else {
|
||||
refSize += 8;
|
||||
}
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(is64 ? 16 : 8)
|
||||
.putInt(is64 ? 0x01 : (int) refSize)
|
||||
.putInt(0x6D646174);// mdat
|
||||
|
||||
if (is64) {
|
||||
buffer.putLong(refSize);
|
||||
}
|
||||
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
private void make_mvhd(long longestTrack) throws IOException {
|
||||
auxWrite(new byte[]{
|
||||
0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 0x00
|
||||
});
|
||||
auxWrite(ByteBuffer.allocate(28)
|
||||
.putLong(time)
|
||||
.putLong(time)
|
||||
.putInt(DEFAULT_TIMESCALE)
|
||||
.putLong(longestTrack)
|
||||
.array()
|
||||
);
|
||||
|
||||
auxWrite(new byte[]{
|
||||
0x00, 0x01, 0x00, 0x00, 0x01, 0x00,// default volume and rate
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,// reserved values
|
||||
// default matrix
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x40, 0x00, 0x00, 0x00
|
||||
});
|
||||
auxWrite(new byte[24]);// predefined
|
||||
auxWrite(ByteBuffer.allocate(4)
|
||||
.putInt(tracks.length + 1)
|
||||
.array()
|
||||
);
|
||||
}
|
||||
|
||||
private int make_moov(int[] defaultMediaTime, TablesInfo[] tablesInfo, boolean is64) throws RuntimeException, IOException {
|
||||
int start = auxOffset();
|
||||
|
||||
auxWrite(new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x76
|
||||
});
|
||||
|
||||
long longestTrack = 0;
|
||||
long[] durations = new long[tracks.length];
|
||||
|
||||
for (int i = 0; i < durations.length; i++) {
|
||||
durations[i] = (long) Math.ceil(
|
||||
((double) tracks[i].trak.tkhd.duration / tracks[i].trak.mdia.mdhd_timeScale) * DEFAULT_TIMESCALE
|
||||
);
|
||||
|
||||
if (durations[i] > longestTrack) {
|
||||
longestTrack = durations[i];
|
||||
}
|
||||
}
|
||||
|
||||
make_mvhd(longestTrack);
|
||||
|
||||
for (int i = 0; i < tracks.length; i++) {
|
||||
if (tracks[i].trak.tkhd.matrix.length != 36) {
|
||||
throw new RuntimeException("bad track matrix length (expected 36) in track n°" + i);
|
||||
}
|
||||
make_trak(i, durations[i], defaultMediaTime[i], tablesInfo[i], is64);
|
||||
}
|
||||
|
||||
// udta/meta/ilst/©too
|
||||
auxWrite(new byte[]{
|
||||
0x00, 0x00, 0x00, 0x5C, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x54, 0x6D, 0x65, 0x74, 0x61,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00,
|
||||
0x1F, (byte) 0xA9, 0x74, 0x6F, 0x6F, 0x00, 0x00, 0x00, 0x17, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00,
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
|
||||
});
|
||||
|
||||
return lengthFor(start);
|
||||
}
|
||||
|
||||
private void make_trak(int index, long duration, int defaultMediaTime, TablesInfo tables, boolean is64) throws IOException {
|
||||
int start = auxOffset();
|
||||
|
||||
auxWrite(new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B,// trak header
|
||||
0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 // tkhd header
|
||||
});
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(48);
|
||||
buffer.putLong(time);
|
||||
buffer.putLong(time);
|
||||
buffer.putInt(index + 1);
|
||||
buffer.position(24);
|
||||
buffer.putLong(duration);
|
||||
buffer.position(40);
|
||||
buffer.putShort(tracks[index].trak.tkhd.bLayer);
|
||||
buffer.putShort(tracks[index].trak.tkhd.bAlternateGroup);
|
||||
buffer.putShort(tracks[index].trak.tkhd.bVolume);
|
||||
auxWrite(buffer.array());
|
||||
|
||||
auxWrite(tracks[index].trak.tkhd.matrix);
|
||||
auxWrite(ByteBuffer.allocate(8)
|
||||
.putInt(tracks[index].trak.tkhd.bWidth)
|
||||
.putInt(tracks[index].trak.tkhd.bHeight)
|
||||
.array()
|
||||
);
|
||||
|
||||
auxWrite(new byte[]{
|
||||
0x00, 0x00, 0x00, 0x24, 0x65, 0x64, 0x74, 0x73,// edts header
|
||||
0x00, 0x00, 0x00, 0x1C, 0x65, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01// elst header
|
||||
});
|
||||
|
||||
int bMediaRate;
|
||||
int mediaTime;
|
||||
|
||||
if (tracks[index].trak.edst_elst == null) {
|
||||
// is a audio track ¿is edst/elst opcional for audio tracks?
|
||||
mediaTime = 0x00;// ffmpeg set this value as zero, instead of defaultMediaTime
|
||||
bMediaRate = 0x00010000;
|
||||
} else {
|
||||
mediaTime = (int) tracks[index].trak.edst_elst.MediaTime;
|
||||
bMediaRate = tracks[index].trak.edst_elst.bMediaRate;
|
||||
}
|
||||
|
||||
auxWrite(ByteBuffer
|
||||
.allocate(12)
|
||||
.putInt((int) duration)
|
||||
.putInt(mediaTime)
|
||||
.putInt(bMediaRate)
|
||||
.array()
|
||||
);
|
||||
|
||||
make_mdia(tracks[index].trak.mdia, tables, is64);
|
||||
|
||||
lengthFor(start);
|
||||
}
|
||||
|
||||
private void make_mdia(Mdia mdia, TablesInfo tablesInfo, boolean is64) throws IOException {
|
||||
|
||||
int start_mdia = auxOffset();
|
||||
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x61});// mdia
|
||||
auxWrite(mdia.mdhd);
|
||||
auxWrite(make_hdlr(mdia.hdlr));
|
||||
|
||||
int start_minf = auxOffset();
|
||||
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x69, 0x6E, 0x66});// minf
|
||||
auxWrite(mdia.minf.$mhd);
|
||||
auxWrite(mdia.minf.dinf);
|
||||
|
||||
int start_stbl = auxOffset();
|
||||
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x62, 0x6C});// stbl
|
||||
auxWrite(mdia.minf.stbl_stsd);
|
||||
|
||||
//
|
||||
// In audio tracks the following tables is not required: ssts ctts
|
||||
// And stsz can be empty if has a default sample size
|
||||
//
|
||||
if (moovSimulation) {
|
||||
make(0x73747473, -1, 2, 1);
|
||||
if (tablesInfo.stss > 0) {
|
||||
make(0x73747373, -1, 1, tablesInfo.stss);
|
||||
}
|
||||
if (tablesInfo.ctts > 0) {
|
||||
make(0x63747473, -1, 2, tablesInfo.ctts);
|
||||
}
|
||||
make(0x73747363, -1, 3, tablesInfo.stsc);
|
||||
make(0x7374737A, tablesInfo.stsz_default, 1, tablesInfo.stsz);
|
||||
make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco);
|
||||
} else {
|
||||
tablesInfo.stts = make(0x73747473, -1, 2, 1);
|
||||
if (tablesInfo.stss > 0) {
|
||||
tablesInfo.stss = make(0x73747373, -1, 1, tablesInfo.stss);
|
||||
}
|
||||
if (tablesInfo.ctts > 0) {
|
||||
tablesInfo.ctts = make(0x63747473, -1, 2, tablesInfo.ctts);
|
||||
}
|
||||
tablesInfo.stsc = make(0x73747363, -1, 3, tablesInfo.stsc);
|
||||
tablesInfo.stsz = make(0x7374737A, tablesInfo.stsz_default, 1, tablesInfo.stsz);
|
||||
tablesInfo.stco = make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco);
|
||||
}
|
||||
|
||||
lengthFor(start_stbl);
|
||||
lengthFor(start_minf);
|
||||
lengthFor(start_mdia);
|
||||
}
|
||||
|
||||
private byte[] make_hdlr(Hdlr hdlr) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(new byte[]{
|
||||
0x00, 0x00, 0x00, 0x77, 0x68, 0x64, 0x6C, 0x72,// hdlr
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// binary string "ISO Media file created in NewPipe (A libre lightweight streaming frontend for Android)."
|
||||
0x49, 0x53, 0x4F, 0x20, 0x4D, 0x65, 0x64, 0x69, 0x61, 0x20, 0x66, 0x69, 0x6C, 0x65, 0x20, 0x63,
|
||||
0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x20, 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70,
|
||||
0x65, 0x20, 0x28, 0x41, 0x20, 0x6C, 0x69, 0x62, 0x72, 0x65, 0x20, 0x6C, 0x69, 0x67, 0x68, 0x74,
|
||||
0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x20, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, 0x69, 0x6E, 0x67,
|
||||
0x20, 0x66, 0x72, 0x6F, 0x6E, 0x74, 0x65, 0x6E, 0x64, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x41, 0x6E,
|
||||
0x64, 0x72, 0x6F, 0x69, 0x64, 0x29, 0x2E
|
||||
});
|
||||
|
||||
buffer.position(12);
|
||||
buffer.putInt(hdlr.type);
|
||||
buffer.putInt(hdlr.subType);
|
||||
buffer.put(hdlr.bReserved);// always is a zero array
|
||||
|
||||
return buffer.array();
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
class TablesInfo {
|
||||
|
||||
public int stts;
|
||||
public int stsc;
|
||||
public int[] stsc_bEntries;
|
||||
public int ctts;
|
||||
public int stsz;
|
||||
public int stsz_default;
|
||||
public int stss;
|
||||
public int stco;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package org.schabi.newpipe.streams;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
|
|
@ -12,8 +13,6 @@ import java.nio.charset.Charset;
|
|||
import java.text.ParseException;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
|
@ -27,11 +26,11 @@ public class SubtitleConverter {
|
|||
|
||||
public void dumpTTML(SharpStream in, final SharpStream out, final boolean ignoreEmptyFrames, final boolean detectYoutubeDuplicateLines
|
||||
) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException {
|
||||
|
||||
|
||||
final FrameWriter callback = new FrameWriter() {
|
||||
int frameIndex = 0;
|
||||
final Charset charset = Charset.forName("utf-8");
|
||||
|
||||
|
||||
@Override
|
||||
public void yield(SubtitleFrame frame) throws IOException {
|
||||
if (ignoreEmptyFrames && frame.isEmptyText()) {
|
||||
|
|
@ -48,13 +47,13 @@ public class SubtitleConverter {
|
|||
out.write(NEW_LINE.getBytes(charset));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
read_xml_based(in, callback, detectYoutubeDuplicateLines,
|
||||
"tt", "xmlns", "http://www.w3.org/ns/ttml",
|
||||
new String[]{"timedtext", "head", "wp"},
|
||||
new String[]{"body", "div", "p"},
|
||||
"begin", "end", true
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
private void read_xml_based(SharpStream source, FrameWriter callback, boolean detectYoutubeDuplicateLines,
|
||||
|
|
@ -70,7 +69,7 @@ public class SubtitleConverter {
|
|||
* Language parsing is not supported
|
||||
*/
|
||||
|
||||
byte[] buffer = new byte[source.available()];
|
||||
byte[] buffer = new byte[(int) source.available()];
|
||||
source.read(buffer);
|
||||
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
|
|
@ -206,7 +205,7 @@ public class SubtitleConverter {
|
|||
}
|
||||
}
|
||||
|
||||
private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) throws XPathExpressionException {
|
||||
private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) {
|
||||
Element ref = xml.getDocumentElement();
|
||||
|
||||
for (int i = 0; i < path.length - 1; i++) {
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
package org.schabi.newpipe.streams;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class TrackDataChunk extends InputStream {
|
||||
|
||||
private final DataReader base;
|
||||
private int size;
|
||||
|
||||
public TrackDataChunk(DataReader base, int size) {
|
||||
this.base = base;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (size < 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int res = base.read();
|
||||
|
||||
if (res >= 0) {
|
||||
size--;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer) throws IOException {
|
||||
return read(buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int offset, int count) throws IOException {
|
||||
count = Math.min(size, count);
|
||||
int read = base.read(buffer, offset, count);
|
||||
size -= count;
|
||||
return read;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long amount) throws IOException {
|
||||
long res = base.skipBytes(Math.min(amount, size));
|
||||
size -= res;
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
size = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
package org.schabi.newpipe.streams;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -121,7 +122,7 @@ public class WebMReader {
|
|||
}
|
||||
|
||||
private String readString(Element parent) throws IOException {
|
||||
return new String(readBlob(parent), "utf-8");
|
||||
return new String(readBlob(parent), StandardCharsets.UTF_8);// or use "utf-8"
|
||||
}
|
||||
|
||||
private byte[] readBlob(Element parent) throws IOException {
|
||||
|
|
@ -193,6 +194,7 @@ public class WebMReader {
|
|||
return elem;
|
||||
}
|
||||
}
|
||||
|
||||
ensure(elem);
|
||||
}
|
||||
|
||||
|
|
@ -306,7 +308,7 @@ public class WebMReader {
|
|||
entry.trackNumber = readNumber(elem);
|
||||
break;
|
||||
case ID_TrackType:
|
||||
entry.trackType = (int)readNumber(elem);
|
||||
entry.trackType = (int) readNumber(elem);
|
||||
break;
|
||||
case ID_CodecID:
|
||||
entry.codecId = readString(elem);
|
||||
|
|
@ -445,7 +447,7 @@ public class WebMReader {
|
|||
|
||||
public class SimpleBlock {
|
||||
|
||||
public TrackDataChunk data;
|
||||
public InputStream data;
|
||||
|
||||
SimpleBlock(Element ref) {
|
||||
this.ref = ref;
|
||||
|
|
@ -492,7 +494,7 @@ public class WebMReader {
|
|||
|
||||
currentSimpleBlock = readSimpleBlock(elem);
|
||||
if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) {
|
||||
currentSimpleBlock.data = new TrackDataChunk(stream, (int) currentSimpleBlock.dataSize);
|
||||
currentSimpleBlock.data = stream.getView((int) currentSimpleBlock.dataSize);
|
||||
return currentSimpleBlock;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
package org.schabi.newpipe.streams;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.WebMReader.Cluster;
|
||||
import org.schabi.newpipe.streams.WebMReader.Segment;
|
||||
import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
|
||||
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class WebMWriter {
|
||||
|
|
@ -94,10 +94,6 @@ public class WebMWriter {
|
|||
}
|
||||
}
|
||||
|
||||
public long getBytesWritten() {
|
||||
return written;
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return done;
|
||||
}
|
||||
|
|
@ -111,7 +107,7 @@ public class WebMWriter {
|
|||
parsed = true;
|
||||
|
||||
for (SharpStream src : sourceTracks) {
|
||||
src.dispose();
|
||||
src.close();
|
||||
}
|
||||
|
||||
sourceTracks = null;
|
||||
|
|
@ -138,42 +134,42 @@ public class WebMWriter {
|
|||
|
||||
/* segment */
|
||||
listBuffer.add(new byte[]{
|
||||
0x18, 0x53, (byte) 0x80, 0x67, 0x01,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// segment content size
|
||||
0x18, 0x53, (byte) 0x80, 0x67, 0x01,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// segment content size
|
||||
});
|
||||
|
||||
long baseSegmentOffset = written + listBuffer.get(0).length;
|
||||
|
||||
/* seek head */
|
||||
listBuffer.add(new byte[]{
|
||||
0x11, 0x4d, (byte) 0x9b, 0x74, (byte) 0xbe,
|
||||
0x4d, (byte) 0xbb, (byte) 0x8b,
|
||||
0x53, (byte) 0xab, (byte) 0x84, 0x15, 0x49, (byte) 0xa9, 0x66, 0x53,
|
||||
(byte) 0xac, (byte) 0x81, /*info offset*/ 0x43,
|
||||
0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab,
|
||||
(byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81,
|
||||
/*tracks offset*/ 0x6a,
|
||||
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1f,
|
||||
0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84, /*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00,
|
||||
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1c, 0x53,
|
||||
(byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84, /*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00
|
||||
0x11, 0x4d, (byte) 0x9b, 0x74, (byte) 0xbe,
|
||||
0x4d, (byte) 0xbb, (byte) 0x8b,
|
||||
0x53, (byte) 0xab, (byte) 0x84, 0x15, 0x49, (byte) 0xa9, 0x66, 0x53,
|
||||
(byte) 0xac, (byte) 0x81, /*info offset*/ 0x43,
|
||||
0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab,
|
||||
(byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81,
|
||||
/*tracks offset*/ 0x6a,
|
||||
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1f,
|
||||
0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84, /*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00,
|
||||
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1c, 0x53,
|
||||
(byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84, /*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00
|
||||
});
|
||||
|
||||
/* info */
|
||||
listBuffer.add(new byte[]{
|
||||
0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0xa2, 0x2a, (byte) 0xd7, (byte) 0xb1
|
||||
0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0xa2, 0x2a, (byte) 0xd7, (byte) 0xb1
|
||||
});
|
||||
listBuffer.add(encode(DEFAULT_TIMECODE_SCALE, true));// this value MUST NOT exceed 4 bytes
|
||||
listBuffer.add(new byte[]{0x44, (byte) 0x89, (byte) 0x84,
|
||||
0x00, 0x00, 0x00, 0x00,// info.duration
|
||||
|
||||
/* MuxingApp */
|
||||
0x4d, (byte) 0x80, (byte) 0x87, 0x4E,
|
||||
0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string
|
||||
|
||||
/* WritingApp */
|
||||
0x57, 0x41, (byte) 0x87, 0x4E,
|
||||
0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
|
||||
0x00, 0x00, 0x00, 0x00,// info.duration
|
||||
|
||||
/* MuxingApp */
|
||||
0x4d, (byte) 0x80, (byte) 0x87, 0x4E,
|
||||
0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string
|
||||
|
||||
/* WritingApp */
|
||||
0x57, 0x41, (byte) 0x87, 0x4E,
|
||||
0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
|
||||
});
|
||||
|
||||
/* tracks */
|
||||
|
|
@ -200,7 +196,6 @@ public class WebMWriter {
|
|||
long nextCueTime = infoTracks[cuesForTrackId].trackType == 1 ? -1 : 0;
|
||||
ArrayList<KeyFrame> keyFrames = new ArrayList<>(32);
|
||||
|
||||
//ArrayList<Block> chunks = new ArrayList<>(readers.length);
|
||||
ArrayList<Long> clusterOffsets = new ArrayList<>(32);
|
||||
ArrayList<Integer> clusterSizes = new ArrayList<>(32);
|
||||
|
||||
|
|
@ -283,24 +278,21 @@ public class WebMWriter {
|
|||
|
||||
long segmentSize = written - offsetSegmentSizeSet - 7;
|
||||
|
||||
// final step write offsets and sizes
|
||||
out.rewind();
|
||||
written = 0;
|
||||
|
||||
skipTo(out, offsetSegmentSizeSet);
|
||||
/* ---- final step write offsets and sizes ---- */
|
||||
seekTo(out, offsetSegmentSizeSet);
|
||||
writeLong(out, segmentSize);
|
||||
|
||||
if (predefinedDurations[durationFromTrackId] > -1) {
|
||||
duration += predefinedDurations[durationFromTrackId];// this value is full-filled in makeTrackEntry() method
|
||||
}
|
||||
skipTo(out, offsetInfoDurationSet);
|
||||
seekTo(out, offsetInfoDurationSet);
|
||||
writeFloat(out, duration);
|
||||
|
||||
firstClusterOffset -= baseSegmentOffset;
|
||||
skipTo(out, offsetClusterSet);
|
||||
seekTo(out, offsetClusterSet);
|
||||
writeInt(out, firstClusterOffset);
|
||||
|
||||
skipTo(out, cueReservedOffset);
|
||||
seekTo(out, cueReservedOffset);
|
||||
|
||||
/* Cue */
|
||||
dump(new byte[]{0x1c, 0x53, (byte) 0xbb, 0x6b, 0x20, 0x00, 0x00}, out);
|
||||
|
|
@ -321,20 +313,16 @@ public class WebMWriter {
|
|||
voidBuffer.putShort((short) (firstClusterOffset - written - 4));
|
||||
dump(voidBuffer.array(), out);
|
||||
|
||||
out.rewind();
|
||||
written = 0;
|
||||
|
||||
skipTo(out, offsetCuesSet);
|
||||
seekTo(out, offsetCuesSet);
|
||||
writeInt(out, (int) (cueReservedOffset - baseSegmentOffset));
|
||||
|
||||
skipTo(out, cueReservedOffset + 5);
|
||||
seekTo(out, cueReservedOffset + 5);
|
||||
writeShort(out, cueSize);
|
||||
|
||||
for (int i = 0; i < clusterSizes.size(); i++) {
|
||||
skipTo(out, clusterOffsets.get(i));
|
||||
byte[] size = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x200000).array();
|
||||
out.write(size, 1, 3);
|
||||
written += 3;
|
||||
seekTo(out, clusterOffsets.get(i));
|
||||
byte[] buffer = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x10000000).array();
|
||||
dump(buffer, out);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -365,20 +353,29 @@ public class WebMWriter {
|
|||
bloq.dataSize = (int) res.dataSize;
|
||||
bloq.trackNumber = internalTrackId;
|
||||
bloq.flags = res.flags;
|
||||
bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale, DEFAULT_TIMECODE_SCALE);
|
||||
bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale);
|
||||
bloq.absoluteTimecode += readersCluter[internalTrackId].timecode;
|
||||
|
||||
return bloq;
|
||||
}
|
||||
|
||||
private short convertTimecode(int time, long oldTimeScale, int newTimeScale) {
|
||||
return (short) (time * (newTimeScale / oldTimeScale));
|
||||
private short convertTimecode(int time, long oldTimeScale) {
|
||||
return (short) (time * (DEFAULT_TIMECODE_SCALE / oldTimeScale));
|
||||
}
|
||||
|
||||
private void skipTo(SharpStream stream, long absoluteOffset) throws IOException {
|
||||
absoluteOffset -= written;
|
||||
written += absoluteOffset;
|
||||
stream.skip(absoluteOffset);
|
||||
private void seekTo(SharpStream stream, long offset) throws IOException {
|
||||
if (stream.canSeek()) {
|
||||
stream.seek(offset);
|
||||
} else {
|
||||
if (offset > written) {
|
||||
stream.skip(offset - written);
|
||||
} else {
|
||||
stream.rewind();
|
||||
stream.skip(offset);
|
||||
}
|
||||
}
|
||||
|
||||
written = offset;
|
||||
}
|
||||
|
||||
private void writeLong(SharpStream stream, long number) throws IOException {
|
||||
|
|
@ -453,7 +450,7 @@ public class WebMWriter {
|
|||
/* cluster */
|
||||
dump(new byte[]{0x1f, 0x43, (byte) 0xb6, 0x75}, stream);
|
||||
clusterOffsets.add(written);// warning: max cluster size is 256 MiB
|
||||
dump(new byte[]{0x20, 0x00, 0x00}, stream);
|
||||
dump(new byte[]{0x10, 0x00, 0x00, 0x00}, stream);
|
||||
|
||||
startOffset = written;// size for the this cluster
|
||||
|
||||
|
|
@ -468,12 +465,12 @@ public class WebMWriter {
|
|||
private void makeEBML(SharpStream stream) throws IOException {
|
||||
// deafult values
|
||||
dump(new byte[]{
|
||||
0x1A, 0x45, (byte) 0xDF, (byte) 0xA3, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x1F, 0x42, (byte) 0x86, (byte) 0x81, 0x01,
|
||||
0x42, (byte) 0xF7, (byte) 0x81, 0x01, 0x42, (byte) 0xF2, (byte) 0x81, 0x04,
|
||||
0x42, (byte) 0xF3, (byte) 0x81, 0x08, 0x42, (byte) 0x82, (byte) 0x84, 0x77,
|
||||
0x65, 0x62, 0x6D, 0x42, (byte) 0x87, (byte) 0x81, 0x02,
|
||||
0x42, (byte) 0x85, (byte) 0x81, 0x02
|
||||
0x1A, 0x45, (byte) 0xDF, (byte) 0xA3, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x1F, 0x42, (byte) 0x86, (byte) 0x81, 0x01,
|
||||
0x42, (byte) 0xF7, (byte) 0x81, 0x01, 0x42, (byte) 0xF2, (byte) 0x81, 0x04,
|
||||
0x42, (byte) 0xF3, (byte) 0x81, 0x08, 0x42, (byte) 0x82, (byte) 0x84, 0x77,
|
||||
0x65, 0x62, 0x6D, 0x42, (byte) 0x87, (byte) 0x81, 0x02,
|
||||
0x42, (byte) 0x85, (byte) 0x81, 0x02
|
||||
}, stream);
|
||||
}
|
||||
|
||||
|
|
@ -618,9 +615,10 @@ public class WebMWriter {
|
|||
|
||||
int offset = withLength ? 1 : 0;
|
||||
byte[] buffer = new byte[offset + length];
|
||||
long marker = (long) Math.floor((length - 1) / 8);
|
||||
long marker = (long) Math.floor((length - 1f) / 8f);
|
||||
|
||||
for (int i = length - 1, mul = 1; i >= 0; i--, mul *= 0x100) {
|
||||
float mul = 1;
|
||||
for (int i = length - 1; i >= 0; i--, mul *= 0x100) {
|
||||
long b = (long) Math.floor(number / mul);
|
||||
if (!withLength && i == marker) {
|
||||
b = b | (0x80 >> (length - 1));
|
||||
|
|
@ -637,11 +635,7 @@ public class WebMWriter {
|
|||
|
||||
private ArrayList<byte[]> encode(String value) {
|
||||
byte[] str;
|
||||
try {
|
||||
str = value.getBytes("utf-8");
|
||||
} catch (UnsupportedEncodingException err) {
|
||||
str = value.getBytes();
|
||||
}
|
||||
str = value.getBytes(StandardCharsets.UTF_8);// or use "utf-8"
|
||||
|
||||
ArrayList<byte[]> buffer = new ArrayList<>(2);
|
||||
buffer.add(encode(str.length, false));
|
||||
|
|
@ -720,9 +714,10 @@ public class WebMWriter {
|
|||
return (flags & 0x80) == 0x80;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, (flags & 0x80) == 0x80, absoluteTimecode);
|
||||
return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, isKeyframe(), absoluteTimecode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
package org.schabi.newpipe.streams.io;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* based c#
|
||||
* based on c#
|
||||
*/
|
||||
public abstract class SharpStream {
|
||||
public abstract class SharpStream implements Closeable {
|
||||
|
||||
public abstract int read() throws IOException;
|
||||
|
||||
|
|
@ -15,16 +16,14 @@ public abstract class SharpStream {
|
|||
|
||||
public abstract long skip(long amount) throws IOException;
|
||||
|
||||
|
||||
public abstract int available();
|
||||
public abstract long available();
|
||||
|
||||
public abstract void rewind() throws IOException;
|
||||
|
||||
public abstract boolean isClosed();
|
||||
|
||||
public abstract void dispose();
|
||||
|
||||
public abstract boolean isDisposed();
|
||||
|
||||
@Override
|
||||
public abstract void close();
|
||||
|
||||
public abstract boolean canRewind();
|
||||
|
||||
|
|
@ -32,6 +31,13 @@ public abstract class SharpStream {
|
|||
|
||||
public abstract boolean canWrite();
|
||||
|
||||
public boolean canSetLength() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean canSeek() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public abstract void write(byte value) throws IOException;
|
||||
|
||||
|
|
@ -39,9 +45,19 @@ public abstract class SharpStream {
|
|||
|
||||
public abstract void write(byte[] buffer, int offset, int count) throws IOException;
|
||||
|
||||
public abstract void flush() throws IOException;
|
||||
public void flush() throws IOException {
|
||||
// STUB
|
||||
}
|
||||
|
||||
public void setLength(long length) throws IOException {
|
||||
throw new IOException("Not implemented");
|
||||
}
|
||||
|
||||
public void seek(long offset) throws IOException {
|
||||
throw new IOException("Not implemented");
|
||||
}
|
||||
|
||||
public long length() throws IOException {
|
||||
throw new UnsupportedOperationException("Unsupported operation");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ public class CommentTextOnTouchListener implements View.OnTouchListener {
|
|||
|
||||
public static final CommentTextOnTouchListener INSTANCE = new CommentTextOnTouchListener();
|
||||
|
||||
private static final Pattern timestampPattern = Pattern.compile(".*&t=(\\d+)");
|
||||
private static final Pattern timestampPattern = Pattern.compile("(.*)#timestamp=(\\d+)");
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
|
|
@ -86,6 +86,12 @@ public class CommentTextOnTouchListener implements View.OnTouchListener {
|
|||
|
||||
private boolean handleUrl(Context context, URLSpan urlSpan) {
|
||||
String url = urlSpan.getURL();
|
||||
int seconds = -1;
|
||||
Matcher matcher = timestampPattern.matcher(url);
|
||||
if(matcher.matches()){
|
||||
url = matcher.group(1);
|
||||
seconds = Integer.parseInt(matcher.group(2));
|
||||
}
|
||||
StreamingService service;
|
||||
StreamingService.LinkType linkType;
|
||||
try {
|
||||
|
|
@ -97,9 +103,7 @@ public class CommentTextOnTouchListener implements View.OnTouchListener {
|
|||
if(linkType == StreamingService.LinkType.NONE){
|
||||
return false;
|
||||
}
|
||||
Matcher matcher = timestampPattern.matcher(url);
|
||||
if(linkType == StreamingService.LinkType.STREAM && matcher.matches()){
|
||||
int seconds = Integer.parseInt(matcher.group(1));
|
||||
if(linkType == StreamingService.LinkType.STREAM && seconds != -1){
|
||||
return playOnPopup(context, url, service, seconds);
|
||||
}else{
|
||||
NavigationHelper.openRouterActivity(context, url);
|
||||
|
|
@ -119,9 +123,8 @@ public class CommentTextOnTouchListener implements View.OnTouchListener {
|
|||
single.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(info -> {
|
||||
PlayQueue playQueue = new SinglePlayQueue((StreamInfo) info);
|
||||
((StreamInfo) info).setStartPosition(seconds);
|
||||
NavigationHelper.enqueueOnPopupPlayer(context, playQueue, true);
|
||||
PlayQueue playQueue = new SinglePlayQueue((StreamInfo) info, seconds*1000);
|
||||
NavigationHelper.playOnPopupPlayer(context, playQueue, false);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -243,8 +243,6 @@ public final class ExtractorHelper {
|
|||
context.startActivity(intent);
|
||||
} else if (exception instanceof IOException) {
|
||||
Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show();
|
||||
} else if (exception instanceof YoutubeStreamExtractor.GemaException) {
|
||||
Toast.makeText(context, R.string.blocked_by_gema, Toast.LENGTH_LONG).show();
|
||||
} else if (exception instanceof ContentNotAvailableException) {
|
||||
Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ import java.util.regex.Pattern;
|
|||
|
||||
public class FilenameUtils {
|
||||
|
||||
private static final String CHARSET_MOST_SPECIAL = "[\\n\\r|?*<\":\\\\>/']+";
|
||||
private static final String CHARSET_ONLY_LETTERS_AND_DIGITS = "[^\\w\\d]+";
|
||||
|
||||
/**
|
||||
* #143 #44 #42 #22: make sure that the filename does not contain illegal chars.
|
||||
* @param context the context to retrieve strings and preferences from
|
||||
|
|
@ -18,11 +21,28 @@ public class FilenameUtils {
|
|||
*/
|
||||
public static String createFilename(Context context, String title) {
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String key = context.getString(R.string.settings_file_charset_key);
|
||||
final String value = sharedPreferences.getString(key, context.getString(R.string.default_file_charset_value));
|
||||
Pattern pattern = Pattern.compile(value);
|
||||
|
||||
final String charset_ld = context.getString(R.string.charset_letters_and_digits_value);
|
||||
final String charset_ms = context.getString(R.string.charset_most_special_value);
|
||||
final String defaultCharset = context.getString(R.string.default_file_charset_value);
|
||||
|
||||
final String replacementChar = sharedPreferences.getString(context.getString(R.string.settings_file_replacement_character_key), "_");
|
||||
String selectedCharset = sharedPreferences.getString(context.getString(R.string.settings_file_charset_key), null);
|
||||
|
||||
final String charset;
|
||||
|
||||
if (selectedCharset == null || selectedCharset.isEmpty()) selectedCharset = defaultCharset;
|
||||
|
||||
if (selectedCharset.equals(charset_ld)) {
|
||||
charset = CHARSET_ONLY_LETTERS_AND_DIGITS;
|
||||
} else if (selectedCharset.equals(charset_ms)) {
|
||||
charset = CHARSET_MOST_SPECIAL;
|
||||
} else {
|
||||
charset = selectedCharset;// ¿is the user using a custom charset?
|
||||
}
|
||||
|
||||
Pattern pattern = Pattern.compile(charset);
|
||||
|
||||
return createFilename(title, pattern, replacementChar);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -430,24 +430,26 @@ public final class ListHelper {
|
|||
*/
|
||||
private static String getResolutionLimit(Context context) {
|
||||
String resolutionLimit = null;
|
||||
if (!isWifiActive(context)) {
|
||||
if (isMeteredNetwork(context)) {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String defValue = context.getString(R.string.limit_data_usage_none_key);
|
||||
String value = preferences.getString(
|
||||
context.getString(R.string.limit_mobile_data_usage_key), defValue);
|
||||
resolutionLimit = value.equals(defValue) ? null : value;
|
||||
resolutionLimit = defValue.equals(value) ? null : value;
|
||||
}
|
||||
return resolutionLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Are we connected to wifi?
|
||||
* The current network is metered (like mobile data)?
|
||||
* @param context App context
|
||||
* @return {@code true} if connected to wifi
|
||||
* @return {@code true} if connected to a metered network
|
||||
*/
|
||||
private static boolean isWifiActive(Context context)
|
||||
private static boolean isMeteredNetwork(Context context)
|
||||
{
|
||||
ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
return manager != null && manager.getActiveNetworkInfo() != null && manager.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_WIFI;
|
||||
if (manager == null || manager.getActiveNetworkInfo() == null) return false;
|
||||
|
||||
return manager.isActiveNetworkMetered();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,12 +69,14 @@ public class NavigationHelper {
|
|||
public static Intent getPlayerIntent(@NonNull final Context context,
|
||||
@NonNull final Class targetClazz,
|
||||
@NonNull final PlayQueue playQueue,
|
||||
@Nullable final String quality) {
|
||||
@Nullable final String quality,
|
||||
final boolean resumePlayback) {
|
||||
Intent intent = new Intent(context, targetClazz);
|
||||
|
||||
final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class);
|
||||
if (cacheKey != null) intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey);
|
||||
if (quality != null) intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality);
|
||||
intent.putExtra(VideoPlayer.RESUME_PLAYBACK, resumePlayback);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
|
@ -82,16 +84,18 @@ public class NavigationHelper {
|
|||
@NonNull
|
||||
public static Intent getPlayerIntent(@NonNull final Context context,
|
||||
@NonNull final Class targetClazz,
|
||||
@NonNull final PlayQueue playQueue) {
|
||||
return getPlayerIntent(context, targetClazz, playQueue, null);
|
||||
@NonNull final PlayQueue playQueue,
|
||||
final boolean resumePlayback) {
|
||||
return getPlayerIntent(context, targetClazz, playQueue, null, resumePlayback);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Intent getPlayerEnqueueIntent(@NonNull final Context context,
|
||||
@NonNull final Class targetClazz,
|
||||
@NonNull final PlayQueue playQueue,
|
||||
final boolean selectOnAppend) {
|
||||
return getPlayerIntent(context, targetClazz, playQueue)
|
||||
final boolean selectOnAppend,
|
||||
final boolean resumePlayback) {
|
||||
return getPlayerIntent(context, targetClazz, playQueue, resumePlayback)
|
||||
.putExtra(BasePlayer.APPEND_ONLY, true)
|
||||
.putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend);
|
||||
}
|
||||
|
|
@ -104,40 +108,41 @@ public class NavigationHelper {
|
|||
final float playbackSpeed,
|
||||
final float playbackPitch,
|
||||
final boolean playbackSkipSilence,
|
||||
@Nullable final String playbackQuality) {
|
||||
return getPlayerIntent(context, targetClazz, playQueue, playbackQuality)
|
||||
@Nullable final String playbackQuality,
|
||||
final boolean resumePlayback) {
|
||||
return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback)
|
||||
.putExtra(BasePlayer.REPEAT_MODE, repeatMode)
|
||||
.putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed)
|
||||
.putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch)
|
||||
.putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence);
|
||||
}
|
||||
|
||||
public static void playOnMainPlayer(final Context context, final PlayQueue queue) {
|
||||
final Intent playerIntent = getPlayerIntent(context, MainVideoPlayer.class, queue);
|
||||
public static void playOnMainPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
|
||||
final Intent playerIntent = getPlayerIntent(context, MainVideoPlayer.class, queue, resumePlayback);
|
||||
playerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(playerIntent);
|
||||
}
|
||||
|
||||
public static void playOnPopupPlayer(final Context context, final PlayQueue queue) {
|
||||
public static void playOnPopupPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
|
||||
if (!PermissionHelper.isPopupEnabled(context)) {
|
||||
PermissionHelper.showPopupEnablementToast(context);
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
|
||||
startService(context, getPlayerIntent(context, PopupVideoPlayer.class, queue));
|
||||
startService(context, getPlayerIntent(context, PopupVideoPlayer.class, queue, resumePlayback));
|
||||
}
|
||||
|
||||
public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue) {
|
||||
public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
|
||||
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show();
|
||||
startService(context, getPlayerIntent(context, BackgroundPlayer.class, queue));
|
||||
startService(context, getPlayerIntent(context, BackgroundPlayer.class, queue, resumePlayback));
|
||||
}
|
||||
|
||||
public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue) {
|
||||
enqueueOnPopupPlayer(context, queue, false);
|
||||
public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
|
||||
enqueueOnPopupPlayer(context, queue, false, resumePlayback);
|
||||
}
|
||||
|
||||
public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend) {
|
||||
public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend, final boolean resumePlayback) {
|
||||
if (!PermissionHelper.isPopupEnabled(context)) {
|
||||
PermissionHelper.showPopupEnablementToast(context);
|
||||
return;
|
||||
|
|
@ -145,17 +150,17 @@ public class NavigationHelper {
|
|||
|
||||
Toast.makeText(context, R.string.popup_playing_append, Toast.LENGTH_SHORT).show();
|
||||
startService(context,
|
||||
getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue, selectOnAppend));
|
||||
getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue, selectOnAppend, resumePlayback));
|
||||
}
|
||||
|
||||
public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue) {
|
||||
enqueueOnBackgroundPlayer(context, queue, false);
|
||||
public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
|
||||
enqueueOnBackgroundPlayer(context, queue, false, resumePlayback);
|
||||
}
|
||||
|
||||
public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend) {
|
||||
public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend, final boolean resumePlayback) {
|
||||
Toast.makeText(context, R.string.background_player_append, Toast.LENGTH_SHORT).show();
|
||||
startService(context,
|
||||
getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue, selectOnAppend));
|
||||
getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue, selectOnAppend, resumePlayback));
|
||||
}
|
||||
|
||||
public static void startService(@NonNull final Context context, @NonNull final Intent intent) {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ public class SecondaryStreamHelper<T extends Stream> {
|
|||
public static AudioStream getAudioStreamFor(@NonNull List<AudioStream> audioStreams, @NonNull VideoStream videoStream) {
|
||||
switch (videoStream.getFormat()) {
|
||||
case WEBM:
|
||||
case MPEG_4:
|
||||
case MPEG_4:// ¿is mpeg-4 DASH?
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
|
|
|
|||
22
app/src/main/java/org/schabi/newpipe/util/ShareUtils.java
Normal file
22
app/src/main/java/org/schabi/newpipe/util/ShareUtils.java
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class ShareUtils {
|
||||
public static void openUrlInBrowser(Context context, String url) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
context.startActivity(Intent.createChooser(intent, context.getString(R.string.share_dialog_title)));
|
||||
}
|
||||
|
||||
public static void shareUrl(Context context, String subject, String url) {
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, url);
|
||||
context.startActivity(Intent.createChooser(intent, context.getString(R.string.share_dialog_title)));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.util.SparseArray;
|
||||
|
||||
public abstract class SparseArrayUtils {
|
||||
|
||||
public static <T> void shiftItemsDown(SparseArray<T> sparseArray, int lower, int upper) {
|
||||
for (int i = lower + 1; i <= upper; i++) {
|
||||
final T o = sparseArray.get(i);
|
||||
sparseArray.put(i - 1, o);
|
||||
sparseArray.remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> void shiftItemsUp(SparseArray<T> sparseArray, int lower, int upper) {
|
||||
for (int i = upper - 1; i >= lower; i--) {
|
||||
final T o = sparseArray.get(i);
|
||||
sparseArray.put(i + 1, o);
|
||||
sparseArray.remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> int[] getKeys(SparseArray<T> sparseArray) {
|
||||
final int[] result = new int[sparseArray.size()];
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
result[i] = sparseArray.keyAt(i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package org.schabi.newpipe.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.Transformation;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
public final class AnimatedProgressBar extends ProgressBar {
|
||||
|
||||
@Nullable
|
||||
private ProgressBarAnimation animation = null;
|
||||
|
||||
public AnimatedProgressBar(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public AnimatedProgressBar(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public AnimatedProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public synchronized void setProgressAnimated(int progress) {
|
||||
cancelAnimation();
|
||||
animation = new ProgressBarAnimation(this, getProgress(), progress);
|
||||
startAnimation(animation);
|
||||
}
|
||||
|
||||
private void cancelAnimation() {
|
||||
if (animation != null) {
|
||||
animation.cancel();
|
||||
animation = null;
|
||||
}
|
||||
clearAnimation();
|
||||
}
|
||||
|
||||
private static class ProgressBarAnimation extends Animation {
|
||||
|
||||
private final AnimatedProgressBar progressBar;
|
||||
private final float from;
|
||||
private final float to;
|
||||
|
||||
ProgressBarAnimation(AnimatedProgressBar progressBar, float from, float to) {
|
||||
super();
|
||||
this.progressBar = progressBar;
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
setDuration(500);
|
||||
setInterpolator(new AccelerateDecelerateInterpolator());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyTransformation(float interpolatedTime, Transformation t) {
|
||||
super.applyTransformation(interpolatedTime, t);
|
||||
float value = from + (to - from) * interpolatedTime;
|
||||
progressBar.setProgress((int) value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue