merged dev
This commit is contained in:
commit
d61797fa3a
193 changed files with 13000 additions and 3424 deletions
|
|
@ -5,6 +5,7 @@ import android.app.NotificationChannel;
|
|||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
|
|
@ -66,7 +67,8 @@ public class App extends Application {
|
|||
private RefWatcher refWatcher;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static final Class<? extends ReportSenderFactory>[] reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class};
|
||||
private static final Class<? extends ReportSenderFactory>[]
|
||||
reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class};
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
|
|
@ -89,7 +91,8 @@ public class App extends Application {
|
|||
// Initialize settings first because others inits can use its values
|
||||
SettingsActivity.initSettings(this);
|
||||
|
||||
NewPipe.init(getDownloader(), new Localization("GB", "en"));
|
||||
NewPipe.init(getDownloader(),
|
||||
org.schabi.newpipe.util.Localization.getPreferredExtractorLocal(this));
|
||||
StateSaver.init(this);
|
||||
initNotificationChannel();
|
||||
|
||||
|
|
@ -181,7 +184,11 @@ public class App extends Application {
|
|||
ACRA.init(this, acraConfig);
|
||||
} catch (ACRAConfigurationException ace) {
|
||||
ace.printStackTrace();
|
||||
ErrorActivity.reportError(this, ace, null, null, ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
ErrorActivity.reportError(this,
|
||||
ace,
|
||||
null,
|
||||
null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not initialize ACRA crash report", R.string.app_ui_crash));
|
||||
}
|
||||
}
|
||||
|
|
@ -201,7 +208,8 @@ public class App extends Application {
|
|||
NotificationChannel mChannel = new NotificationChannel(id, name, importance);
|
||||
mChannel.setDescription(description);
|
||||
|
||||
NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
NotificationManager mNotificationManager =
|
||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
mNotificationManager.createNotificationChannel(mChannel);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import org.schabi.newpipe.extractor.utils.Localization;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
|
@ -95,7 +94,8 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
|||
.build();
|
||||
response = client.newCall(request).execute();
|
||||
|
||||
return Long.parseLong(response.header("Content-Length"));
|
||||
String contentLength = response.header("Content-Length");
|
||||
return contentLength == null ? -1 : Long.parseLong(contentLength);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException("Invalid content length", e);
|
||||
} finally {
|
||||
|
|
@ -110,7 +110,7 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
|||
* but set the HTTP header field "Accept-Language" to the supplied string.
|
||||
*
|
||||
* @param siteUrl the URL of the text file to return the contents of
|
||||
* @param localization the language and country (usually a 2-character code for both values)
|
||||
* @param localization the language and country (usually a 2-character code) to set
|
||||
* @return the contents of the specified text file
|
||||
*/
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -542,8 +542,7 @@ public class RouterActivity extends AppCompatActivity {
|
|||
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
|
||||
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
|
||||
boolean useOldVideoPlayer = PlayerHelper.isUsingOldPlayer(this);
|
||||
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);;
|
||||
|
||||
PlayQueue playQueue;
|
||||
String playerChoice = choice.playerChoice;
|
||||
|
|
@ -555,9 +554,6 @@ public class RouterActivity extends AppCompatActivity {
|
|||
} else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) {
|
||||
NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info);
|
||||
|
||||
} else if (playerChoice.equals(videoPlayerKey) && useOldVideoPlayer) {
|
||||
NavigationHelper.playOnOldVideoPlayer(this, (StreamInfo) info);
|
||||
|
||||
} else {
|
||||
playQueue = new SinglePlayQueue((StreamInfo) info);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,158 +0,0 @@
|
|||
package org.schabi.newpipe.download;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.BaseTransientBottomBar;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
import us.shandian.giga.get.DownloadManager;
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
|
||||
public class DeleteDownloadManager {
|
||||
|
||||
private static final String KEY_STATE = "delete_manager_state";
|
||||
|
||||
private final View mView;
|
||||
private final HashSet<String> mPendingMap;
|
||||
private final List<Disposable> mDisposableList;
|
||||
private DownloadManager mDownloadManager;
|
||||
private final PublishSubject<DownloadMission> publishSubject = PublishSubject.create();
|
||||
|
||||
DeleteDownloadManager(Activity activity) {
|
||||
mPendingMap = new HashSet<>();
|
||||
mDisposableList = new ArrayList<>();
|
||||
mView = activity.findViewById(android.R.id.content);
|
||||
}
|
||||
|
||||
public Observable<DownloadMission> getUndoObservable() {
|
||||
return publishSubject;
|
||||
}
|
||||
|
||||
public boolean contains(@NonNull DownloadMission mission) {
|
||||
return mPendingMap.contains(mission.url);
|
||||
}
|
||||
|
||||
public void add(@NonNull DownloadMission mission) {
|
||||
mPendingMap.add(mission.url);
|
||||
|
||||
if (mPendingMap.size() == 1) {
|
||||
showUndoDeleteSnackbar(mission);
|
||||
}
|
||||
}
|
||||
|
||||
public void setDownloadManager(@NonNull DownloadManager downloadManager) {
|
||||
mDownloadManager = downloadManager;
|
||||
|
||||
if (mPendingMap.size() < 1) return;
|
||||
|
||||
showUndoDeleteSnackbar();
|
||||
}
|
||||
|
||||
public void restoreState(@Nullable Bundle savedInstanceState) {
|
||||
if (savedInstanceState == null) return;
|
||||
|
||||
List<String> list = savedInstanceState.getStringArrayList(KEY_STATE);
|
||||
if (list != null) {
|
||||
mPendingMap.addAll(list);
|
||||
}
|
||||
}
|
||||
|
||||
public void saveState(@Nullable Bundle outState) {
|
||||
if (outState == null) return;
|
||||
|
||||
for (Disposable disposable : mDisposableList) {
|
||||
disposable.dispose();
|
||||
}
|
||||
|
||||
outState.putStringArrayList(KEY_STATE, new ArrayList<>(mPendingMap));
|
||||
}
|
||||
|
||||
private void showUndoDeleteSnackbar() {
|
||||
if (mPendingMap.size() < 1) return;
|
||||
|
||||
String url = mPendingMap.iterator().next();
|
||||
|
||||
for (int i = 0; i < mDownloadManager.getCount(); i++) {
|
||||
DownloadMission mission = mDownloadManager.getMission(i);
|
||||
if (url.equals(mission.url)) {
|
||||
showUndoDeleteSnackbar(mission);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showUndoDeleteSnackbar(@NonNull DownloadMission mission) {
|
||||
final Snackbar snackbar = Snackbar.make(mView, mission.name, Snackbar.LENGTH_INDEFINITE);
|
||||
final Disposable disposable = Observable.timer(3, TimeUnit.SECONDS)
|
||||
.subscribeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(l -> snackbar.dismiss());
|
||||
|
||||
mDisposableList.add(disposable);
|
||||
|
||||
snackbar.setAction(R.string.undo, v -> {
|
||||
mPendingMap.remove(mission.url);
|
||||
publishSubject.onNext(mission);
|
||||
disposable.dispose();
|
||||
snackbar.dismiss();
|
||||
});
|
||||
|
||||
snackbar.addCallback(new BaseTransientBottomBar.BaseCallback<Snackbar>() {
|
||||
@Override
|
||||
public void onDismissed(Snackbar transientBottomBar, int event) {
|
||||
if (!disposable.isDisposed()) {
|
||||
Completable.fromAction(() -> deletePending(mission))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe();
|
||||
}
|
||||
mPendingMap.remove(mission.url);
|
||||
snackbar.removeCallback(this);
|
||||
mDisposableList.remove(disposable);
|
||||
showUndoDeleteSnackbar();
|
||||
}
|
||||
});
|
||||
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
public void deletePending() {
|
||||
if (mPendingMap.size() < 1) return;
|
||||
|
||||
HashSet<Integer> idSet = new HashSet<>();
|
||||
for (int i = 0; i < mDownloadManager.getCount(); i++) {
|
||||
if (contains(mDownloadManager.getMission(i))) {
|
||||
idSet.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
for (Integer id : idSet) {
|
||||
mDownloadManager.deleteMission(id);
|
||||
}
|
||||
|
||||
mPendingMap.clear();
|
||||
}
|
||||
|
||||
private void deletePending(@NonNull DownloadMission mission) {
|
||||
for (int i = 0; i < mDownloadManager.getCount(); i++) {
|
||||
if (mission.url.equals(mDownloadManager.getMission(i).url)) {
|
||||
mDownloadManager.deleteMission(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,16 +15,12 @@ import org.schabi.newpipe.R;
|
|||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.ui.fragment.AllMissionsFragment;
|
||||
import us.shandian.giga.ui.fragment.MissionsFragment;
|
||||
|
||||
public class DownloadActivity extends AppCompatActivity {
|
||||
|
||||
private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag";
|
||||
private DeleteDownloadManager mDeleteDownloadManager;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
|
@ -47,32 +43,17 @@ public class DownloadActivity extends AppCompatActivity {
|
|||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
}
|
||||
|
||||
mDeleteDownloadManager = new DeleteDownloadManager(this);
|
||||
mDeleteDownloadManager.restoreState(savedInstanceState);
|
||||
|
||||
MissionsFragment fragment = (MissionsFragment) getFragmentManager().findFragmentByTag(MISSIONS_FRAGMENT_TAG);
|
||||
if (fragment != null) {
|
||||
fragment.setDeleteManager(mDeleteDownloadManager);
|
||||
} else {
|
||||
getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
updateFragments();
|
||||
getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
mDeleteDownloadManager.saveState(outState);
|
||||
super.onSaveInstanceState(outState);
|
||||
getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
updateFragments();
|
||||
getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateFragments() {
|
||||
MissionsFragment fragment = new AllMissionsFragment();
|
||||
fragment.setDeleteManager(mDeleteDownloadManager);
|
||||
MissionsFragment fragment = new MissionsFragment();
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG)
|
||||
|
|
@ -99,7 +80,6 @@ public class DownloadActivity extends AppCompatActivity {
|
|||
case R.id.action_settings: {
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
deletePending();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
|
|
@ -108,14 +88,7 @@ public class DownloadActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
deletePending();
|
||||
}
|
||||
|
||||
private void deletePending() {
|
||||
Completable.fromAction(mDeleteDownloadManager::deletePending)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe();
|
||||
public void onRestoreInstanceState(Bundle inState){
|
||||
super.onRestoreInstanceState(inState);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
package org.schabi.newpipe.download;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.IdRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
|
@ -22,38 +26,55 @@ import android.widget.Toast;
|
|||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.Stream;
|
||||
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.util.FilenameUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.SecondaryStreamHelper;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
|
||||
public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
||||
private static final String TAG = "DialogFragment";
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
|
||||
@State protected StreamInfo currentInfo;
|
||||
@State protected StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
|
||||
@State protected StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
|
||||
@State protected int selectedVideoIndex = 0;
|
||||
@State protected int selectedAudioIndex = 0;
|
||||
@State
|
||||
protected StreamInfo currentInfo;
|
||||
@State
|
||||
protected StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
|
||||
@State
|
||||
protected StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
|
||||
@State
|
||||
protected StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams = StreamSizeWrapper.empty();
|
||||
@State
|
||||
protected int selectedVideoIndex = 0;
|
||||
@State
|
||||
protected int selectedAudioIndex = 0;
|
||||
@State
|
||||
protected int selectedSubtitleIndex = 0;
|
||||
|
||||
private StreamItemAdapter<AudioStream> audioStreamsAdapter;
|
||||
private StreamItemAdapter<VideoStream> videoStreamsAdapter;
|
||||
private StreamItemAdapter<AudioStream, Stream> audioStreamsAdapter;
|
||||
private StreamItemAdapter<VideoStream, AudioStream> videoStreamsAdapter;
|
||||
private StreamItemAdapter<SubtitlesStream, Stream> subtitleStreamsAdapter;
|
||||
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
|
|
@ -63,6 +84,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
private TextView threadsCountTextView;
|
||||
private SeekBar threadsSeekBar;
|
||||
|
||||
private SharedPreferences prefs;
|
||||
|
||||
public static DownloadDialog newInstance(StreamInfo info) {
|
||||
DownloadDialog dialog = new DownloadDialog();
|
||||
dialog.setInfo(info);
|
||||
|
|
@ -78,6 +101,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
instance.setVideoStreams(streamsList);
|
||||
instance.setSelectedVideoStream(selectedStreamIndex);
|
||||
instance.setAudioStreams(info.getAudioStreams());
|
||||
instance.setSubtitleStreams(info.getSubtitles());
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +111,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
}
|
||||
|
||||
public void setAudioStreams(List<AudioStream> audioStreams) {
|
||||
setAudioStreams(new StreamSizeWrapper<>(audioStreams));
|
||||
setAudioStreams(new StreamSizeWrapper<>(audioStreams, getContext()));
|
||||
}
|
||||
|
||||
public void setAudioStreams(StreamSizeWrapper<AudioStream> wrappedAudioStreams) {
|
||||
|
|
@ -94,13 +119,21 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
}
|
||||
|
||||
public void setVideoStreams(List<VideoStream> videoStreams) {
|
||||
setVideoStreams(new StreamSizeWrapper<>(videoStreams));
|
||||
setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext()));
|
||||
}
|
||||
|
||||
public void setVideoStreams(StreamSizeWrapper<VideoStream> wrappedVideoStreams) {
|
||||
this.wrappedVideoStreams = wrappedVideoStreams;
|
||||
}
|
||||
|
||||
public void setSubtitleStreams(List<SubtitlesStream> subtitleStreams) {
|
||||
setSubtitleStreams(new StreamSizeWrapper<>(subtitleStreams, getContext()));
|
||||
}
|
||||
|
||||
public void setSubtitleStreams(StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams) {
|
||||
this.wrappedSubtitleStreams = wrappedSubtitleStreams;
|
||||
}
|
||||
|
||||
public void setSelectedVideoStream(int selectedVideoIndex) {
|
||||
this.selectedVideoIndex = selectedVideoIndex;
|
||||
}
|
||||
|
|
@ -109,6 +142,10 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
this.selectedAudioIndex = selectedAudioIndex;
|
||||
}
|
||||
|
||||
public void setSelectedSubtitleStream(int selectedSubtitleIndex) {
|
||||
this.selectedSubtitleIndex = selectedSubtitleIndex;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
|
@ -116,7 +153,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
if (!PermissionHelper.checkStoragePermissions(getActivity(), PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||
getDialog().dismiss();
|
||||
return;
|
||||
|
|
@ -125,13 +163,29 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(getContext()));
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, true);
|
||||
SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams = new SparseArray<>(4);
|
||||
List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
|
||||
|
||||
for (int i = 0; i < videoStreams.size(); i++) {
|
||||
if (!videoStreams.get(i).isVideoOnly()) continue;
|
||||
AudioStream audioStream = SecondaryStreamHelper.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
|
||||
|
||||
if (audioStream != null) {
|
||||
secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream));
|
||||
} else if (DEBUG) {
|
||||
Log.w(TAG, "No audio stream candidates for video format " + videoStreams.get(i).getFormat().name());
|
||||
}
|
||||
}
|
||||
|
||||
this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, secondaryStreams);
|
||||
this.audioStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedAudioStreams);
|
||||
this.subtitleStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedSubtitleStreams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
return inflater.inflate(R.layout.download_dialog, container);
|
||||
}
|
||||
|
||||
|
|
@ -142,6 +196,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName()));
|
||||
selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams());
|
||||
|
||||
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
|
||||
|
||||
streamsSpinner = view.findViewById(R.id.quality_spinner);
|
||||
streamsSpinner.setOnItemSelectedListener(this);
|
||||
|
||||
|
|
@ -154,14 +210,18 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
initToolbar(view.findViewById(R.id.toolbar));
|
||||
setupDownloadOptions();
|
||||
|
||||
int def = 3;
|
||||
threadsCountTextView.setText(String.valueOf(def));
|
||||
threadsSeekBar.setProgress(def - 1);
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
|
||||
int threads = prefs.getInt(getString(R.string.default_download_threads), 3);
|
||||
threadsCountTextView.setText(String.valueOf(threads));
|
||||
threadsSeekBar.setProgress(threads - 1);
|
||||
threadsSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekbar, int progress, boolean fromUser) {
|
||||
threadsCountTextView.setText(String.valueOf(progress + 1));
|
||||
progress++;
|
||||
prefs.edit().putInt(getString(R.string.default_download_threads), progress).apply();
|
||||
threadsCountTextView.setText(String.valueOf(progress));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -189,6 +249,11 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
setupAudioSpinner();
|
||||
}
|
||||
}));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> {
|
||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
||||
setupSubtitleSpinner();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -216,7 +281,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
|
||||
toolbar.setOnMenuItemClickListener(item -> {
|
||||
if (item.getItemId() == R.id.okay) {
|
||||
downloadSelected();
|
||||
prepareSelectedDownload();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -239,13 +304,24 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
setRadioButtonsState(true);
|
||||
}
|
||||
|
||||
private void setupSubtitleSpinner() {
|
||||
if (getContext() == null) return;
|
||||
|
||||
streamsSpinner.setAdapter(subtitleStreamsAdapter);
|
||||
streamsSpinner.setSelection(selectedSubtitleIndex);
|
||||
setRadioButtonsState(true);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Radio group Video&Audio options - Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(RadioGroup group, @IdRes int checkedId) {
|
||||
if (DEBUG) Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]");
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]");
|
||||
boolean flag = true;
|
||||
|
||||
switch (checkedId) {
|
||||
case R.id.audio_button:
|
||||
setupAudioSpinner();
|
||||
|
|
@ -253,7 +329,13 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
case R.id.video_button:
|
||||
setupVideoSpinner();
|
||||
break;
|
||||
case R.id.subtitle_button:
|
||||
setupSubtitleSpinner();
|
||||
flag = false;
|
||||
break;
|
||||
}
|
||||
|
||||
threadsSeekBar.setEnabled(flag);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -262,7 +344,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
|
||||
@Override
|
||||
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 + "]");
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
|
||||
switch (radioVideoAudioGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
selectedAudioIndex = position;
|
||||
|
|
@ -270,6 +353,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
case R.id.video_button:
|
||||
selectedVideoIndex = position;
|
||||
break;
|
||||
case R.id.subtitle_button:
|
||||
selectedSubtitleIndex = position;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -286,11 +372,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
|
||||
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 boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
|
||||
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
|
||||
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
|
||||
|
||||
audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
subtitleButton.setVisibility(isSubtitleStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (isVideoStreamsAvailable) {
|
||||
videoButton.setChecked(true);
|
||||
|
|
@ -298,6 +387,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
} else if (isAudioStreamsAvailable) {
|
||||
audioButton.setChecked(true);
|
||||
setupAudioSpinner();
|
||||
} else if (isSubtitleStreamsAvailable) {
|
||||
subtitleButton.setChecked(true);
|
||||
setupSubtitleSpinner();
|
||||
} else {
|
||||
Toast.makeText(getContext(), R.string.no_streams_available_download, Toast.LENGTH_SHORT).show();
|
||||
getDialog().dismiss();
|
||||
|
|
@ -307,28 +399,144 @@ 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);
|
||||
}
|
||||
|
||||
private void downloadSelected() {
|
||||
Stream stream;
|
||||
String location;
|
||||
private int getSubtitleIndexBy(List<SubtitlesStream> streams) {
|
||||
Localization loc = NewPipe.getPreferredLocalization();
|
||||
|
||||
String fileName = nameEditText.getText().toString().trim();
|
||||
if (fileName.isEmpty()) fileName = FilenameUtils.createFilename(getContext(), currentInfo.getName());
|
||||
|
||||
boolean isAudio = radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button;
|
||||
if (isAudio) {
|
||||
stream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
||||
location = NewPipeSettings.getAudioDownloadPath(getContext());
|
||||
} else {
|
||||
stream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||
location = NewPipeSettings.getVideoDownloadPath(getContext());
|
||||
for (int i = 0; i < streams.size(); i++) {
|
||||
Locale streamLocale = streams.get(i).getLocale();
|
||||
String tag = streamLocale.getLanguage().concat("-").concat(streamLocale.getCountry());
|
||||
if (tag.equalsIgnoreCase(loc.getLanguage())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
String url = stream.getUrl();
|
||||
fileName += "." + stream.getFormat().getSuffix();
|
||||
// fallback
|
||||
// 1st loop match country & language
|
||||
// 2nd loop match language only
|
||||
int index = loc.getLanguage().indexOf("-");
|
||||
String lang = index > 0 ? loc.getLanguage().substring(0, index) : loc.getLanguage();
|
||||
|
||||
for (int j = 0; j < 2; j++) {
|
||||
for (int i = 0; i < streams.size(); i++) {
|
||||
Locale streamLocale = streams.get(i).getLocale();
|
||||
|
||||
if (streamLocale.getLanguage().equalsIgnoreCase(lang)) {
|
||||
if (j > 0 || streamLocale.getCountry().equalsIgnoreCase(loc.getCountry())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void prepareSelectedDownload() {
|
||||
final Context context = getContext();
|
||||
Stream stream;
|
||||
String location;
|
||||
char kind;
|
||||
|
||||
String fileName = nameEditText.getText().toString().trim();
|
||||
if (fileName.isEmpty())
|
||||
fileName = FilenameUtils.createFilename(context, currentInfo.getName());
|
||||
|
||||
switch (radioVideoAudioGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
stream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
||||
location = NewPipeSettings.getAudioDownloadPath(context);
|
||||
kind = 'a';
|
||||
break;
|
||||
case R.id.video_button:
|
||||
stream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||
location = NewPipeSettings.getVideoDownloadPath(context);
|
||||
kind = 'v';
|
||||
break;
|
||||
case R.id.subtitle_button:
|
||||
stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
||||
location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video go together
|
||||
kind = 's';
|
||||
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();
|
||||
}
|
||||
|
||||
final String finalFileName = fileName;
|
||||
|
||||
DownloadManagerService.checkForRunningMission(context, location, fileName, (listed, finished) -> {
|
||||
// should be safe run the following code without "getActivity().runOnUiThread()"
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void downloadSelected(Context context, Stream selectedStream, String location, String fileName, char kind, int threads) {
|
||||
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));
|
||||
|
||||
if (secondaryStream != null) {
|
||||
secondaryStreamUrl = secondaryStream.getStream().getUrl();
|
||||
psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_DASH_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 weak internet connections
|
||||
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
|
||||
nearLength = secondaryStream.getSizeInBytes() + videoSize;
|
||||
}
|
||||
}
|
||||
} 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
|
||||
};
|
||||
}
|
||||
|
||||
if (secondaryStreamUrl == null) {
|
||||
urls = new String[]{selectedStream.getUrl()};
|
||||
} else {
|
||||
urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl};
|
||||
}
|
||||
|
||||
DownloadManagerService.startMission(context, urls, location, fileName, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);
|
||||
|
||||
DownloadManagerService.startMission(getContext(), url, location, fileName, isAudio, threadsSeekBar.getProgress() + 1);
|
||||
getDialog().dismiss();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
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;
|
||||
import android.support.v4.view.PagerAdapter;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
|
@ -13,10 +12,11 @@ public class TabAdaptor extends FragmentPagerAdapter {
|
|||
|
||||
private final List<Fragment> mFragmentList = new ArrayList<>();
|
||||
private final List<String> mFragmentTitleList = new ArrayList<>();
|
||||
int baseId = 0;
|
||||
private final FragmentManager fragmentManager;
|
||||
|
||||
public TabAdaptor(FragmentManager fm) {
|
||||
super(fm);
|
||||
this.fragmentManager = fm;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -29,18 +29,6 @@ public class TabAdaptor extends FragmentPagerAdapter {
|
|||
return mFragmentList.size();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return mFragmentTitleList.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
// give an ID different from position when position has been changed
|
||||
return baseId + position;
|
||||
}
|
||||
|
||||
public void addFragment(Fragment fragment, String title) {
|
||||
mFragmentList.add(fragment);
|
||||
mFragmentTitleList.add(title);
|
||||
|
|
@ -73,19 +61,13 @@ public class TabAdaptor extends FragmentPagerAdapter {
|
|||
else return POSITION_NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify that the position of a fragment has been changed.
|
||||
* Create a new ID for each position to force recreation of the fragment
|
||||
* @param n number of items which have been changed
|
||||
*/
|
||||
public void notifyChangeInPosition(int n) {
|
||||
// shift the ID returned by getItemId outside the range of all previous fragments
|
||||
// https://stackoverflow.com/questions/10396321/remove-fragment-page-from-viewpager-in-android
|
||||
baseId += getCount() + n;
|
||||
}
|
||||
|
||||
public void notifyDataSetUpdate(){
|
||||
notifyChangeInPosition(1);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(ViewGroup container, int position, Object object) {
|
||||
fragmentManager.beginTransaction().remove((Fragment) object).commitNowAllowingStateLoss();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import android.os.Build;
|
|||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.FloatRange;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.AppBarLayout;
|
||||
|
|
@ -18,7 +17,6 @@ import android.support.design.widget.TabLayout;
|
|||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v4.view.animation.FastOutSlowInInterpolator;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
|
|
@ -29,7 +27,6 @@ import android.text.method.LinkMovementMethod;
|
|||
import android.text.util.Linkify;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
|
|
@ -39,7 +36,6 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
|
|
@ -56,8 +52,6 @@ import org.schabi.newpipe.ReCaptchaActivity;
|
|||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
|
|
@ -72,18 +66,16 @@ import org.schabi.newpipe.fragments.BackPressable;
|
|||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
|
||||
import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.MainVideoPlayer;
|
||||
import org.schabi.newpipe.player.PopupVideoPlayer;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.old.PlayVideoActivity;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
|
|
@ -91,11 +83,9 @@ import org.schabi.newpipe.util.InfoCache;
|
|||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
|
|
@ -192,6 +182,7 @@ public class VideoDetailFragment
|
|||
private ViewPager viewPager;
|
||||
private TabAdaptor pageAdapter;
|
||||
private TabLayout tabLayout;
|
||||
private FrameLayout relatedStreamsLayout;
|
||||
|
||||
|
||||
/*////////////////////////////////////////////////////////////////////////*/
|
||||
|
|
@ -383,14 +374,14 @@ public class VideoDetailFragment
|
|||
Log.w(TAG, "Can't open channel because we got no channel URL");
|
||||
} else {
|
||||
try {
|
||||
NavigationHelper.openChannelFragment(
|
||||
getFragmentManager(),
|
||||
currentInfo.getServiceId(),
|
||||
currentInfo.getUploaderUrl(),
|
||||
currentInfo.getUploaderName());
|
||||
NavigationHelper.openChannelFragment(
|
||||
getFragmentManager(),
|
||||
currentInfo.getServiceId(),
|
||||
currentInfo.getUploaderUrl(),
|
||||
currentInfo.getUploaderName());
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case R.id.detail_thumbnail_root_layout:
|
||||
|
|
@ -489,6 +480,8 @@ public class VideoDetailFragment
|
|||
tabLayout = rootView.findViewById(R.id.tablayout);
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
|
||||
relatedStreamsLayout = rootView.findViewById(R.id.relatedStreamsLayout);
|
||||
|
||||
setHeightThumbnail();
|
||||
|
||||
|
||||
|
|
@ -588,6 +581,7 @@ public class VideoDetailFragment
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
|
@ -676,7 +670,7 @@ public class VideoDetailFragment
|
|||
sortedVideoStreams = ListHelper.getSortedStreamVideosList(activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false);
|
||||
selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(activity, sortedVideoStreams);
|
||||
|
||||
final StreamItemAdapter<VideoStream> streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams), isExternalPlayerEnabled);
|
||||
final StreamItemAdapter<VideoStream, Stream> streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams, activity), isExternalPlayerEnabled);
|
||||
spinnerToolbar.setAdapter(streamsAdapter);
|
||||
spinnerToolbar.setSelection(selectedVideoStreamIndex);
|
||||
spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
|
|
@ -771,11 +765,9 @@ public class VideoDetailFragment
|
|||
initTabs();
|
||||
|
||||
if (scrollToTop) appBarLayout.setExpanded(true, true);
|
||||
animateView(contentRootLayoutHiding,
|
||||
false, 0, 0, () -> {
|
||||
handleResult(info);
|
||||
showContentWithAnimation(120, 0, .01f);
|
||||
});
|
||||
handleResult(info);
|
||||
showContent();
|
||||
|
||||
}
|
||||
|
||||
protected void prepareAndLoadInfo() {
|
||||
|
|
@ -798,8 +790,8 @@ public class VideoDetailFragment
|
|||
.subscribe((@NonNull StreamInfo result) -> {
|
||||
isLoading.set(false);
|
||||
currentInfo = result;
|
||||
showContentWithAnimation(120, 0, 0);
|
||||
handleResult(result);
|
||||
showContent();
|
||||
}, (@NonNull Throwable throwable) -> {
|
||||
isLoading.set(false);
|
||||
onError(throwable);
|
||||
|
|
@ -814,7 +806,7 @@ public class VideoDetailFragment
|
|||
pageAdapter.addFragment(CommentsFragment.getInstance(serviceId, url, name), COMMENTS_TAB_TAG);
|
||||
}
|
||||
|
||||
if(showRelatedStreams){
|
||||
if(showRelatedStreams && null == relatedStreamsLayout){
|
||||
//temp empty fragment. will be updated in handleResult
|
||||
pageAdapter.addFragment(new Fragment(), RELATED_TAB_TAG);
|
||||
}
|
||||
|
|
@ -879,7 +871,7 @@ public class VideoDetailFragment
|
|||
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
|
||||
startOnExternalPlayer(activity, currentInfo, selectedVideoStream);
|
||||
} else {
|
||||
openNormalPlayer(selectedVideoStream);
|
||||
openNormalPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -892,24 +884,13 @@ public class VideoDetailFragment
|
|||
}
|
||||
}
|
||||
|
||||
private void openNormalPlayer(VideoStream selectedVideoStream) {
|
||||
private void openNormalPlayer() {
|
||||
Intent mIntent;
|
||||
boolean useOldPlayer = PlayerHelper.isUsingOldPlayer(activity) || (Build.VERSION.SDK_INT < 16);
|
||||
if (!useOldPlayer) {
|
||||
// ExoPlayer
|
||||
final PlayQueue playQueue = new SinglePlayQueue(currentInfo);
|
||||
mIntent = NavigationHelper.getPlayerIntent(activity,
|
||||
MainVideoPlayer.class,
|
||||
playQueue,
|
||||
getSelectedVideoStream().getResolution());
|
||||
} else {
|
||||
// Internal Player
|
||||
mIntent = new Intent(activity, PlayVideoActivity.class)
|
||||
.putExtra(PlayVideoActivity.VIDEO_TITLE, currentInfo.getName())
|
||||
.putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.getUrl())
|
||||
.putExtra(PlayVideoActivity.VIDEO_URL, currentInfo.getUrl())
|
||||
.putExtra(PlayVideoActivity.START_POSITION, currentInfo.getStartPosition());
|
||||
}
|
||||
final PlayQueue playQueue = new SinglePlayQueue(currentInfo);
|
||||
mIntent = NavigationHelper.getPlayerIntent(activity,
|
||||
MainVideoPlayer.class,
|
||||
playQueue,
|
||||
getSelectedVideoStream().getResolution());
|
||||
startActivity(mIntent);
|
||||
}
|
||||
|
||||
|
|
@ -964,24 +945,6 @@ public class VideoDetailFragment
|
|||
}));
|
||||
}
|
||||
|
||||
private View getSeparatorView() {
|
||||
View separator = new View(activity);
|
||||
LinearLayout.LayoutParams params =
|
||||
new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1);
|
||||
int m8 = (int) TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics());
|
||||
int m5 = (int) TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics());
|
||||
params.setMargins(m8, m5, m8, m5);
|
||||
separator.setLayoutParams(params);
|
||||
|
||||
TypedValue typedValue = new TypedValue();
|
||||
activity.getTheme().resolveAttribute(R.attr.separator_color, typedValue, true);
|
||||
separator.setBackgroundColor(typedValue.data);
|
||||
|
||||
return separator;
|
||||
}
|
||||
|
||||
private void setHeightThumbnail() {
|
||||
final DisplayMetrics metrics = getResources().getDisplayMetrics();
|
||||
boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
|
||||
|
|
@ -993,36 +956,8 @@ public class VideoDetailFragment
|
|||
thumbnailImageView.setMinimumHeight(height);
|
||||
}
|
||||
|
||||
private void showContentWithAnimation(long duration,
|
||||
long delay,
|
||||
@FloatRange(from = 0.0f, to = 1.0f) float translationPercent) {
|
||||
int translationY = (int) (getResources().getDisplayMetrics().heightPixels *
|
||||
(translationPercent > 0.0f ? translationPercent : .06f));
|
||||
|
||||
contentRootLayoutHiding.animate().setListener(null).cancel();
|
||||
contentRootLayoutHiding.setAlpha(0f);
|
||||
contentRootLayoutHiding.setTranslationY(translationY);
|
||||
contentRootLayoutHiding.setVisibility(View.VISIBLE);
|
||||
contentRootLayoutHiding.animate()
|
||||
.alpha(1f)
|
||||
.translationY(0)
|
||||
.setStartDelay(delay)
|
||||
.setDuration(duration)
|
||||
.setInterpolator(new FastOutSlowInInterpolator())
|
||||
.start();
|
||||
|
||||
uploaderRootLayout.animate().setListener(null).cancel();
|
||||
uploaderRootLayout.setAlpha(0f);
|
||||
uploaderRootLayout.setTranslationY(translationY);
|
||||
uploaderRootLayout.setVisibility(View.VISIBLE);
|
||||
uploaderRootLayout.animate()
|
||||
.alpha(1f)
|
||||
.translationY(0)
|
||||
.setStartDelay((long) (duration * .5f) + delay)
|
||||
.setDuration(duration)
|
||||
.setInterpolator(new FastOutSlowInInterpolator())
|
||||
.start();
|
||||
|
||||
private void showContent() {
|
||||
AnimationUtils.slideUp(contentRootLayoutHiding,120, 96, 0.06f);
|
||||
}
|
||||
|
||||
protected void setInitialData(int serviceId, String url, String name) {
|
||||
|
|
@ -1057,7 +992,7 @@ public class VideoDetailFragment
|
|||
public void showLoading() {
|
||||
super.showLoading();
|
||||
|
||||
animateView(contentRootLayoutHiding, false, 200);
|
||||
contentRootLayoutHiding.setVisibility(View.INVISIBLE);
|
||||
animateView(spinnerToolbar, false, 200);
|
||||
animateView(thumbnailPlayButton, false, 50);
|
||||
animateView(detailDurationView, false, 100);
|
||||
|
|
@ -1071,6 +1006,14 @@ public class VideoDetailFragment
|
|||
videoTitleToggleArrow.setVisibility(View.GONE);
|
||||
videoTitleRoot.setClickable(false);
|
||||
|
||||
if(relatedStreamsLayout != null){
|
||||
if(showRelatedStreams){
|
||||
relatedStreamsLayout.setVisibility(View.INVISIBLE);
|
||||
}else{
|
||||
relatedStreamsLayout.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
imageLoader.cancelDisplayTask(thumbnailImageView);
|
||||
imageLoader.cancelDisplayTask(uploaderThumb);
|
||||
thumbnailImageView.setImageBitmap(null);
|
||||
|
|
@ -1084,8 +1027,15 @@ public class VideoDetailFragment
|
|||
setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName());
|
||||
|
||||
if(showRelatedStreams){
|
||||
pageAdapter.updateItem(RELATED_TAB_TAG, RelatedVideosFragment.getInstance(currentInfo));
|
||||
pageAdapter.notifyDataSetUpdate();
|
||||
if(null == relatedStreamsLayout){ //phone
|
||||
pageAdapter.updateItem(RELATED_TAB_TAG, RelatedVideosFragment.getInstance(currentInfo));
|
||||
pageAdapter.notifyDataSetUpdate();
|
||||
}else{ //tablet
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.replace(R.id.relatedStreamsLayout, RelatedVideosFragment.getInstance(currentInfo))
|
||||
.commitNow();
|
||||
relatedStreamsLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
pushToStack(serviceId, url, name);
|
||||
|
|
@ -1149,10 +1099,10 @@ public class VideoDetailFragment
|
|||
detailDurationView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
videoDescriptionView.setVisibility(View.GONE);
|
||||
videoTitleRoot.setClickable(true);
|
||||
videoTitleToggleArrow.setVisibility(View.VISIBLE);
|
||||
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
|
||||
videoDescriptionView.setVisibility(View.GONE);
|
||||
videoDescriptionRootLayout.setVisibility(View.GONE);
|
||||
if (!TextUtils.isEmpty(info.getUploadDate())) {
|
||||
videoUploadDateView.setText(Localization.localizeDate(activity, info.getUploadDate()));
|
||||
|
|
@ -1205,6 +1155,7 @@ public class VideoDetailFragment
|
|||
downloadDialog.setVideoStreams(sortedVideoStreams);
|
||||
downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
|
||||
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
|
||||
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
|
||||
|
||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||
} catch (Exception e) {
|
||||
|
|
@ -1253,4 +1204,4 @@ public class VideoDetailFragment
|
|||
|
||||
showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,15 @@ package org.schabi.newpipe.fragments.list;
|
|||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
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;
|
||||
import android.util.Log;
|
||||
|
|
@ -22,9 +27,9 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
|||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
|
@ -37,7 +42,7 @@ import java.util.Queue;
|
|||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implements ListViewContract<I, N>, StateSaver.WriteRead {
|
||||
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implements ListViewContract<I, N>, StateSaver.WriteRead, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
|
|
@ -45,6 +50,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||
|
||||
protected InfoListAdapter infoListAdapter;
|
||||
protected RecyclerView itemsList;
|
||||
private int updateFlags = 0;
|
||||
|
||||
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
|
|
@ -60,12 +68,31 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
StateSaver.onDestroy(savedState);
|
||||
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.unregisterOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (updateFlags != 0) {
|
||||
if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) {
|
||||
final boolean useGrid = isGridLayout();
|
||||
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
|
||||
infoListAdapter.setGridItemVariants(useGrid);
|
||||
infoListAdapter.notifyDataSetChanged();
|
||||
}
|
||||
updateFlags = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -120,13 +147,25 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||
return new LinearLayoutManager(activity);
|
||||
}
|
||||
|
||||
protected RecyclerView.LayoutManager getGridLayoutManager() {
|
||||
final Resources resources = activity.getResources();
|
||||
int width = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width);
|
||||
width += (24 * resources.getDisplayMetrics().density);
|
||||
final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels / (double)width);
|
||||
final GridLayoutManager lm = new GridLayoutManager(activity, spanCount);
|
||||
lm.setSpanSizeLookup(infoListAdapter.getSpanSizeLookup(spanCount));
|
||||
return lm;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
final boolean useGrid = isGridLayout();
|
||||
itemsList = rootView.findViewById(R.id.items_list);
|
||||
itemsList.setLayoutManager(getListLayoutManager());
|
||||
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
|
||||
|
||||
infoListAdapter.setGridItemVariants(useGrid);
|
||||
infoListAdapter.setFooter(getListFooter());
|
||||
infoListAdapter.setHeader(getListHeader());
|
||||
|
||||
|
|
@ -185,7 +224,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<CommentsInfoItem>() {
|
||||
@Override
|
||||
public void selected(CommentsInfoItem selectedItem) {
|
||||
//Log.d("comments" , "this comment was clicked" + selectedItem.getCommentText());
|
||||
onItemSelected(selectedItem);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -316,4 +355,22 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||
public void handleNextItems(N result) {
|
||||
isLoading.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (key.equals(getString(R.string.list_view_mode_key))) {
|
||||
updateFlags |= LIST_MODE_UPDATE_FLAG;
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isGridLayout() {
|
||||
final String list_mode = PreferenceManager.getDefaultSharedPreferences(activity).getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value));
|
||||
if ("auto".equals(list_mode)) {
|
||||
final Configuration configuration = getResources().getConfiguration();
|
||||
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
|
||||
} else {
|
||||
return "grid".equals(list_mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,72 +1,26 @@
|
|||
package org.schabi.newpipe.fragments.list.comments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionService;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
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 java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Action;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.functions.Function;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateBackgroundColor;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateTextColor;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||
|
||||
|
|
@ -139,6 +93,8 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
|||
public void handleResult(@NonNull CommentsInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
AnimationUtils.slideUp(getView(),120, 96, 0.06f);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
}
|
||||
|
|
@ -167,6 +123,7 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
|||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
hideLoading();
|
||||
showSnackBarError(exception, UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(serviceId), url, R.string.error_unable_to_load_comments);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -184,4 +141,9 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
|||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isGridLayout() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,26 +128,16 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
|||
|
||||
@Override
|
||||
public Single<KioskInfo> loadResult(boolean forceReload) {
|
||||
String contentCountry = PreferenceManager
|
||||
.getDefaultSharedPreferences(activity)
|
||||
.getString(getString(R.string.content_country_key),
|
||||
getString(R.string.default_country_value));
|
||||
return ExtractorHelper.getKioskInfo(serviceId,
|
||||
url,
|
||||
contentCountry,
|
||||
forceReload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||
String contentCountry = PreferenceManager
|
||||
.getDefaultSharedPreferences(activity)
|
||||
.getString(getString(R.string.content_country_key),
|
||||
getString(R.string.default_country_value));
|
||||
return ExtractorHelper.getMoreKioskItems(serviceId,
|
||||
url,
|
||||
currentNextPageUrl,
|
||||
contentCountry);
|
||||
currentNextPageUrl);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -626,7 +626,7 @@ public class SearchFragment
|
|||
}
|
||||
|
||||
final Observable<List<SuggestionItem>> network = ExtractorHelper
|
||||
.suggestionsFor(serviceId, query, contentCountry)
|
||||
.suggestionsFor(serviceId, query)
|
||||
.toObservable()
|
||||
.map(strings -> {
|
||||
List<SuggestionItem> result = new ArrayList<>();
|
||||
|
|
@ -726,8 +726,7 @@ public class SearchFragment
|
|||
searchDisposable = ExtractorHelper.searchFor(serviceId,
|
||||
searchString,
|
||||
Arrays.asList(contentFilter),
|
||||
sortFilter,
|
||||
contentCountry)
|
||||
sortFilter)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnEvent((searchResult, throwable) -> isLoading.set(false))
|
||||
|
|
@ -745,8 +744,7 @@ public class SearchFragment
|
|||
searchString,
|
||||
asList(contentFilter),
|
||||
sortFilter,
|
||||
nextPageUrl,
|
||||
contentCountry)
|
||||
nextPageUrl)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnEvent((nextItemsResult, throwable) -> isLoading.set(false))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package org.schabi.newpipe.fragments.list.videos;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
|
|
@ -9,36 +11,32 @@ import android.view.Menu;
|
|||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.Switch;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.RelatedStreamInfo;
|
||||
|
||||
import java.util.List;
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
|
||||
public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInfo> {
|
||||
public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInfo> implements SharedPreferences.OnSharedPreferenceChangeListener{
|
||||
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
private RelatedStreamInfo relatedStreamInfo;
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
|
||||
private View headerRootLayout;
|
||||
private Switch aSwitch;
|
||||
|
||||
private boolean mIsVisibleToUser = false;
|
||||
|
||||
|
|
@ -74,6 +72,28 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
|
|||
if (disposables != null) disposables.clear();
|
||||
}
|
||||
|
||||
protected View getListHeader(){
|
||||
if(relatedStreamInfo != null && relatedStreamInfo.getNextStream() != null){
|
||||
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.related_streams_header, itemsList, false);
|
||||
aSwitch = headerRootLayout.findViewById(R.id.autoplay_switch);
|
||||
|
||||
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
Boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
||||
aSwitch.setChecked(autoplay);
|
||||
aSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
|
||||
SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(getContext()).edit();
|
||||
prefEdit.putBoolean(getString(R.string.auto_queue_key), b);
|
||||
prefEdit.apply();
|
||||
}
|
||||
});
|
||||
return headerRootLayout;
|
||||
}else{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||
return Single.fromCallable(() -> ListExtractor.InfoItemsPage.emptyPage());
|
||||
|
|
@ -91,12 +111,17 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
|
|||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
headerRootLayout.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull RelatedStreamInfo result) {
|
||||
|
||||
super.handleResult(result);
|
||||
|
||||
headerRootLayout.setVisibility(View.VISIBLE);
|
||||
AnimationUtils.slideUp(getView(),120, 96, 0.06f);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
}
|
||||
|
|
@ -125,6 +150,7 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
|
|||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
hideLoading();
|
||||
showSnackBarError(exception, UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, R.string.general_error);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -145,6 +171,38 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
|
|||
|
||||
private void setInitialData(StreamInfo info) {
|
||||
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
||||
this.relatedStreamInfo = RelatedStreamInfo.getInfo(info);
|
||||
if(this.relatedStreamInfo == null) this.relatedStreamInfo = RelatedStreamInfo.getInfo(info);
|
||||
}
|
||||
|
||||
|
||||
private static final String INFO_KEY = "related_info_key";
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putSerializable(INFO_KEY, relatedStreamInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(@NonNull Bundle savedState) {
|
||||
super.onRestoreInstanceState(savedState);
|
||||
if (savedState != null) {
|
||||
Serializable serializable = savedState.getSerializable(INFO_KEY);
|
||||
if(serializable instanceof RelatedStreamInfo){
|
||||
this.relatedStreamInfo = (RelatedStreamInfo) serializable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
|
||||
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
Boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
||||
aSwitch.setChecked(autoplay);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isGridLayout() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
|
@ -15,9 +16,12 @@ 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;
|
||||
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.StreamGridInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.util.FallbackViewHolder;
|
||||
|
|
@ -55,16 +59,20 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||
|
||||
private static final int MINI_STREAM_HOLDER_TYPE = 0x100;
|
||||
private static final int STREAM_HOLDER_TYPE = 0x101;
|
||||
private static final int GRID_STREAM_HOLDER_TYPE = 0x102;
|
||||
private static final int MINI_CHANNEL_HOLDER_TYPE = 0x200;
|
||||
private static final int CHANNEL_HOLDER_TYPE = 0x201;
|
||||
private static final int GRID_CHANNEL_HOLDER_TYPE = 0x202;
|
||||
private static final int MINI_PLAYLIST_HOLDER_TYPE = 0x300;
|
||||
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
|
||||
private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302;
|
||||
private static final int MINI_COMMENT_HOLDER_TYPE = 0x400;
|
||||
private static final int COMMENT_HOLDER_TYPE = 0x401;
|
||||
|
||||
private final InfoItemBuilder infoItemBuilder;
|
||||
private final ArrayList<InfoItem> infoItemList;
|
||||
private boolean useMiniVariant = false;
|
||||
private boolean useGridVariant = false;
|
||||
private boolean showFooter = false;
|
||||
private View header = null;
|
||||
private View footer = null;
|
||||
|
|
@ -103,6 +111,10 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||
this.useMiniVariant = useMiniVariant;
|
||||
}
|
||||
|
||||
public void setGridItemVariants(boolean useGridVariant) {
|
||||
this.useGridVariant = useGridVariant;
|
||||
}
|
||||
|
||||
public void addInfoItemList(List<InfoItem> data) {
|
||||
if (data != null) {
|
||||
if (DEBUG) {
|
||||
|
|
@ -215,11 +227,11 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||
final InfoItem item = infoItemList.get(position);
|
||||
switch (item.getInfoType()) {
|
||||
case STREAM:
|
||||
return useMiniVariant ? MINI_STREAM_HOLDER_TYPE : STREAM_HOLDER_TYPE;
|
||||
return useGridVariant ? GRID_STREAM_HOLDER_TYPE : useMiniVariant ? MINI_STREAM_HOLDER_TYPE : STREAM_HOLDER_TYPE;
|
||||
case CHANNEL:
|
||||
return useMiniVariant ? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE;
|
||||
return useGridVariant ? GRID_CHANNEL_HOLDER_TYPE : useMiniVariant ? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE;
|
||||
case PLAYLIST:
|
||||
return useMiniVariant ? MINI_PLAYLIST_HOLDER_TYPE : PLAYLIST_HOLDER_TYPE;
|
||||
return useGridVariant ? GRID_PLAYLIST_HOLDER_TYPE : useMiniVariant ? MINI_PLAYLIST_HOLDER_TYPE : PLAYLIST_HOLDER_TYPE;
|
||||
case COMMENT:
|
||||
return useMiniVariant ? MINI_COMMENT_HOLDER_TYPE : COMMENT_HOLDER_TYPE;
|
||||
default:
|
||||
|
|
@ -241,14 +253,20 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||
return new StreamMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case STREAM_HOLDER_TYPE:
|
||||
return new StreamInfoItemHolder(infoItemBuilder, parent);
|
||||
case GRID_STREAM_HOLDER_TYPE:
|
||||
return new StreamGridInfoItemHolder(infoItemBuilder, parent);
|
||||
case MINI_CHANNEL_HOLDER_TYPE:
|
||||
return new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case CHANNEL_HOLDER_TYPE:
|
||||
return new ChannelInfoItemHolder(infoItemBuilder, parent);
|
||||
case GRID_CHANNEL_HOLDER_TYPE:
|
||||
return new ChannelGridInfoItemHolder(infoItemBuilder, parent);
|
||||
case MINI_PLAYLIST_HOLDER_TYPE:
|
||||
return new PlaylistMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case PLAYLIST_HOLDER_TYPE:
|
||||
return new PlaylistInfoItemHolder(infoItemBuilder, parent);
|
||||
case GRID_PLAYLIST_HOLDER_TYPE:
|
||||
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
||||
case MINI_COMMENT_HOLDER_TYPE:
|
||||
return new CommentsMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case COMMENT_HOLDER_TYPE:
|
||||
|
|
@ -273,4 +291,14 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||
((HFHolder) holder).view = footer;
|
||||
}
|
||||
}
|
||||
|
||||
public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) {
|
||||
return new GridLayoutManager.SpanSizeLookup() {
|
||||
@Override
|
||||
public int getSpanSize(int position) {
|
||||
final int type = getItemViewType(position);
|
||||
return type == HEADER_TYPE || type == FOOTER_TYPE ? spanCount : 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
|
||||
public class ChannelGridInfoItemHolder extends ChannelMiniInfoItemHolder {
|
||||
|
||||
public ChannelGridInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_channel_grid_item, parent);
|
||||
}
|
||||
}
|
||||
|
|
@ -47,6 +47,13 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
|||
itemBuilder.getOnChannelSelectedListener().selected(item);
|
||||
}
|
||||
});
|
||||
|
||||
itemView.setOnLongClickListener(view -> {
|
||||
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
||||
itemBuilder.getOnChannelSelectedListener().held(item);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
protected String getDetailLine(final ChannelInfoItem item) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package org.schabi.newpipe.info_list.holder;
|
|||
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
|
@ -13,7 +12,6 @@ import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
|
|
@ -23,6 +21,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
|||
private final TextView itemContentView;
|
||||
private final TextView itemLikesCountView;
|
||||
private final TextView itemDislikesCountView;
|
||||
private final TextView itemPublishedTime;
|
||||
|
||||
private static final int commentDefaultLines = 2;
|
||||
private static final int commentExpandedLines = 1000;
|
||||
|
|
@ -31,9 +30,10 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
|||
super(infoItemBuilder, layoutId, parent);
|
||||
|
||||
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
||||
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
|
||||
itemDislikesCountView = itemView.findViewById(R.id.detail_thumbs_down_count_view);
|
||||
itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime);
|
||||
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
||||
}
|
||||
|
||||
public CommentsMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
|
|
@ -66,10 +66,17 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
|||
}
|
||||
});
|
||||
|
||||
// ellipsize if not already ellipsized
|
||||
if (null == itemContentView.getEllipsize()) {
|
||||
itemContentView.setEllipsize(TextUtils.TruncateAt.END);
|
||||
itemContentView.setMaxLines(commentDefaultLines);
|
||||
}
|
||||
|
||||
itemContentView.setText(item.getCommentText());
|
||||
if (null != item.getLikeCount()) {
|
||||
itemLikesCountView.setText(String.valueOf(item.getLikeCount()));
|
||||
}
|
||||
itemPublishedTime.setText(item.getPublishedTime());
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
toggleEllipsize(item.getCommentText());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
|
||||
public class PlaylistGridInfoItemHolder extends PlaylistMiniInfoItemHolder {
|
||||
|
||||
public PlaylistGridInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_playlist_grid_item, parent);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
|
||||
public class StreamGridInfoItemHolder extends StreamMiniInfoItemHolder {
|
||||
|
||||
public StreamGridInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_stream_grid_item, parent);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,13 @@
|
|||
package org.schabi.newpipe.local;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
|
|
@ -25,7 +30,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
|||
* called and is memory efficient when in backstack.
|
||||
* */
|
||||
public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
||||
implements ListViewContract<I, N> {
|
||||
implements ListViewContract<I, N>, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
|
|
@ -36,6 +41,9 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
|||
|
||||
protected LocalItemListAdapter itemListAdapter;
|
||||
protected RecyclerView itemsList;
|
||||
private int updateFlags = 0;
|
||||
|
||||
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Lifecycle - Creation
|
||||
|
|
@ -45,6 +53,29 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
|||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.unregisterOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (updateFlags != 0) {
|
||||
if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) {
|
||||
final boolean useGrid = isGridLayout();
|
||||
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
|
||||
itemListAdapter.setGridItemVariants(useGrid);
|
||||
itemListAdapter.notifyDataSetChanged();
|
||||
}
|
||||
updateFlags = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -59,6 +90,16 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
|||
return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false);
|
||||
}
|
||||
|
||||
protected RecyclerView.LayoutManager getGridLayoutManager() {
|
||||
final Resources resources = activity.getResources();
|
||||
int width = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width);
|
||||
width += (24 * resources.getDisplayMetrics().density);
|
||||
final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels / (double)width);
|
||||
final GridLayoutManager lm = new GridLayoutManager(activity, spanCount);
|
||||
lm.setSpanSizeLookup(itemListAdapter.getSpanSizeLookup(spanCount));
|
||||
return lm;
|
||||
}
|
||||
|
||||
protected RecyclerView.LayoutManager getListLayoutManager() {
|
||||
return new LinearLayoutManager(activity);
|
||||
}
|
||||
|
|
@ -67,10 +108,13 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
|||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
itemsList = rootView.findViewById(R.id.items_list);
|
||||
itemsList.setLayoutManager(getListLayoutManager());
|
||||
|
||||
itemListAdapter = new LocalItemListAdapter(activity);
|
||||
|
||||
final boolean useGrid = isGridLayout();
|
||||
itemsList = rootView.findViewById(R.id.items_list);
|
||||
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
|
||||
|
||||
itemListAdapter.setGridItemVariants(useGrid);
|
||||
itemListAdapter.setHeader(headerRootView = getListHeader());
|
||||
itemListAdapter.setFooter(footerRootView = getListFooter());
|
||||
|
||||
|
|
@ -174,4 +218,22 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
|||
resetFragment();
|
||||
return super.onError(exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (key.equals(getString(R.string.list_view_mode_key))) {
|
||||
updateFlags |= LIST_MODE_UPDATE_FLAG;
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isGridLayout() {
|
||||
final String list_mode = PreferenceManager.getDefaultSharedPreferences(activity).getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value));
|
||||
if ("auto".equals(list_mode)) {
|
||||
final Configuration configuration = getResources().getConfiguration();
|
||||
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
|
||||
} else {
|
||||
return "grid".equals(list_mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
package org.schabi.newpipe.local;
|
||||
|
||||
import android.app.Activity;
|
||||
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.LocalItem;
|
||||
import org.schabi.newpipe.local.HeaderFooterHolder;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.holder.LocalItemHolder;
|
||||
import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder;
|
||||
import org.schabi.newpipe.local.holder.LocalPlaylistItemHolder;
|
||||
import org.schabi.newpipe.local.holder.LocalPlaylistStreamGridItemHolder;
|
||||
import org.schabi.newpipe.local.holder.LocalPlaylistStreamItemHolder;
|
||||
import org.schabi.newpipe.local.holder.LocalStatisticStreamGridItemHolder;
|
||||
import org.schabi.newpipe.local.holder.LocalStatisticStreamItemHolder;
|
||||
import org.schabi.newpipe.local.holder.RemotePlaylistGridItemHolder;
|
||||
import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder;
|
||||
import org.schabi.newpipe.util.FallbackViewHolder;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
|
@ -52,14 +55,19 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||
|
||||
private static final int STREAM_STATISTICS_HOLDER_TYPE = 0x1000;
|
||||
private static final int STREAM_PLAYLIST_HOLDER_TYPE = 0x1001;
|
||||
private static final int STREAM_STATISTICS_GRID_HOLDER_TYPE = 0x1002;
|
||||
private static final int STREAM_PLAYLIST_GRID_HOLDER_TYPE = 0x1004;
|
||||
private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x2000;
|
||||
private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x2001;
|
||||
private static final int LOCAL_PLAYLIST_GRID_HOLDER_TYPE = 0x2002;
|
||||
private static final int REMOTE_PLAYLIST_GRID_HOLDER_TYPE = 0x2004;
|
||||
|
||||
private final LocalItemBuilder localItemBuilder;
|
||||
private final ArrayList<LocalItem> localItems;
|
||||
private final DateFormat dateFormat;
|
||||
|
||||
private boolean showFooter = false;
|
||||
private boolean useGridVariant = false;
|
||||
private View header = null;
|
||||
private View footer = null;
|
||||
|
||||
|
|
@ -134,6 +142,10 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setGridItemVariants(boolean useGridVariant) {
|
||||
this.useGridVariant = useGridVariant;
|
||||
}
|
||||
|
||||
public void setHeader(View header) {
|
||||
boolean changed = header != this.header;
|
||||
this.header = header;
|
||||
|
|
@ -195,11 +207,11 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||
final LocalItem item = localItems.get(position);
|
||||
|
||||
switch (item.getLocalItemType()) {
|
||||
case PLAYLIST_LOCAL_ITEM: return LOCAL_PLAYLIST_HOLDER_TYPE;
|
||||
case PLAYLIST_REMOTE_ITEM: return REMOTE_PLAYLIST_HOLDER_TYPE;
|
||||
case PLAYLIST_LOCAL_ITEM: return useGridVariant ? LOCAL_PLAYLIST_GRID_HOLDER_TYPE : LOCAL_PLAYLIST_HOLDER_TYPE;
|
||||
case PLAYLIST_REMOTE_ITEM: return useGridVariant ? REMOTE_PLAYLIST_GRID_HOLDER_TYPE : REMOTE_PLAYLIST_HOLDER_TYPE;
|
||||
|
||||
case PLAYLIST_STREAM_ITEM: return STREAM_PLAYLIST_HOLDER_TYPE;
|
||||
case STATISTIC_STREAM_ITEM: return STREAM_STATISTICS_HOLDER_TYPE;
|
||||
case PLAYLIST_STREAM_ITEM: return useGridVariant ? STREAM_PLAYLIST_GRID_HOLDER_TYPE : STREAM_PLAYLIST_HOLDER_TYPE;
|
||||
case STATISTIC_STREAM_ITEM: return useGridVariant ? STREAM_STATISTICS_GRID_HOLDER_TYPE : STREAM_STATISTICS_HOLDER_TYPE;
|
||||
default:
|
||||
Log.e(TAG, "No holder type has been considered for item: [" +
|
||||
item.getLocalItemType() + "]");
|
||||
|
|
@ -218,12 +230,20 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||
return new HeaderFooterHolder(footer);
|
||||
case LOCAL_PLAYLIST_HOLDER_TYPE:
|
||||
return new LocalPlaylistItemHolder(localItemBuilder, parent);
|
||||
case LOCAL_PLAYLIST_GRID_HOLDER_TYPE:
|
||||
return new LocalPlaylistGridItemHolder(localItemBuilder, parent);
|
||||
case REMOTE_PLAYLIST_HOLDER_TYPE:
|
||||
return new RemotePlaylistItemHolder(localItemBuilder, parent);
|
||||
case REMOTE_PLAYLIST_GRID_HOLDER_TYPE:
|
||||
return new RemotePlaylistGridItemHolder(localItemBuilder, parent);
|
||||
case STREAM_PLAYLIST_HOLDER_TYPE:
|
||||
return new LocalPlaylistStreamItemHolder(localItemBuilder, parent);
|
||||
case STREAM_PLAYLIST_GRID_HOLDER_TYPE:
|
||||
return new LocalPlaylistStreamGridItemHolder(localItemBuilder, parent);
|
||||
case STREAM_STATISTICS_HOLDER_TYPE:
|
||||
return new LocalStatisticStreamItemHolder(localItemBuilder, parent);
|
||||
case STREAM_STATISTICS_GRID_HOLDER_TYPE:
|
||||
return new LocalStatisticStreamGridItemHolder(localItemBuilder, parent);
|
||||
default:
|
||||
Log.e(TAG, "No view type has been considered for holder: [" + type + "]");
|
||||
return new FallbackViewHolder(new View(parent.getContext()));
|
||||
|
|
@ -247,4 +267,14 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||
((HeaderFooterHolder) holder).view = footer;
|
||||
}
|
||||
}
|
||||
|
||||
public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) {
|
||||
return new GridLayoutManager.SpanSizeLookup() {
|
||||
@Override
|
||||
public int getSpanSize(int position) {
|
||||
final int type = getItemViewType(position);
|
||||
return type == HEADER_TYPE || type == FOOTER_TYPE ? spanCount : 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package org.schabi.newpipe.local.dialog;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.view.Window;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
|
@ -41,6 +43,18 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
|
|||
StateSaver.onDestroy(savedState);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Dialog dialog = super.onCreateDialog(savedInstanceState);
|
||||
//remove title
|
||||
final Window window = dialog.getWindow();
|
||||
if (window != null) {
|
||||
window.requestFeature(Window.FEATURE_NO_TITLE);
|
||||
}
|
||||
return dialog;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// State Saving
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package org.schabi.newpipe.local.holder;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
|
||||
public class LocalPlaylistGridItemHolder extends LocalPlaylistItemHolder {
|
||||
|
||||
public LocalPlaylistGridItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_playlist_grid_item, parent);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,10 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
|
|||
super(infoItemBuilder, parent);
|
||||
}
|
||||
|
||||
LocalPlaylistItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
|
||||
super(infoItemBuilder, layoutId, parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
||||
if (!(localItem instanceof PlaylistMetadataEntry)) return;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package org.schabi.newpipe.local.holder;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
|
||||
public class LocalPlaylistStreamGridItemHolder extends LocalPlaylistStreamItemHolder {
|
||||
|
||||
public LocalPlaylistStreamGridItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_stream_playlist_grid_item, parent); //TODO
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package org.schabi.newpipe.local.holder;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
|
||||
public class LocalStatisticStreamGridItemHolder extends LocalStatisticStreamItemHolder {
|
||||
|
||||
public LocalStatisticStreamGridItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_stream_grid_item, parent);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package org.schabi.newpipe.local.holder;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
|
@ -42,10 +43,15 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
|||
public final TextView itemVideoTitleView;
|
||||
public final TextView itemUploaderView;
|
||||
public final TextView itemDurationView;
|
||||
@Nullable
|
||||
public final TextView itemAdditionalDetails;
|
||||
|
||||
public LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_stream_item, parent);
|
||||
public LocalStatisticStreamItemHolder(LocalItemBuilder itemBuilder, ViewGroup parent) {
|
||||
this(itemBuilder, R.layout.list_stream_item, parent);
|
||||
}
|
||||
|
||||
LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
|
||||
super(infoItemBuilder, layoutId, parent);
|
||||
|
||||
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||
itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView);
|
||||
|
|
@ -80,7 +86,9 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
|||
itemDurationView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat));
|
||||
if (itemAdditionalDetails != null) {
|
||||
itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat));
|
||||
}
|
||||
|
||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package org.schabi.newpipe.local.holder;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
|
||||
public class RemotePlaylistGridItemHolder extends RemotePlaylistItemHolder {
|
||||
|
||||
public RemotePlaylistGridItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_playlist_grid_item, parent);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,10 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
|||
super(infoItemBuilder, parent);
|
||||
}
|
||||
|
||||
RemotePlaylistItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
|
||||
super(infoItemBuilder, layoutId, parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
||||
if (!(localItem instanceof PlaylistRemoteEntity)) return;
|
||||
|
|
|
|||
|
|
@ -459,7 +459,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
|
||||
|
||||
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
||||
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
|
||||
int directions = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
|
||||
if (isGridLayout()) {
|
||||
directions |= ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
|
||||
}
|
||||
return new ItemTouchHelper.SimpleCallback(directions,
|
||||
ItemTouchHelper.ACTION_STATE_IDLE) {
|
||||
@Override
|
||||
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
|
||||
|
|
|
|||
|
|
@ -1,21 +1,30 @@
|
|||
package org.schabi.newpipe.local.subscription;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.Parcelable;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.NonNull;
|
||||
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;
|
||||
import android.view.LayoutInflater;
|
||||
|
|
@ -42,6 +51,8 @@ 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;
|
||||
|
|
@ -70,7 +81,7 @@ import static org.schabi.newpipe.local.subscription.services.SubscriptionsImport
|
|||
import static org.schabi.newpipe.util.AnimationUtils.animateRotation;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEntity>> {
|
||||
public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEntity>> implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final int REQUEST_EXPORT_CODE = 666;
|
||||
private static final int REQUEST_IMPORT_CODE = 667;
|
||||
|
||||
|
|
@ -78,6 +89,9 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
|||
@State
|
||||
protected Parcelable itemsListState;
|
||||
private InfoListAdapter infoListAdapter;
|
||||
private int updateFlags = 0;
|
||||
|
||||
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
|
||||
|
||||
private View whatsNewItemListHeader;
|
||||
private View importExportListHeader;
|
||||
|
|
@ -97,6 +111,8 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
|||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -124,6 +140,15 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
|||
public void onResume() {
|
||||
super.onResume();
|
||||
setupBroadcastReceiver();
|
||||
if (updateFlags != 0) {
|
||||
if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) {
|
||||
final boolean useGrid = isGridLayout();
|
||||
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
|
||||
infoListAdapter.setGridItemVariants(useGrid);
|
||||
infoListAdapter.notifyDataSetChanged();
|
||||
}
|
||||
updateFlags = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -150,9 +175,25 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
|||
disposables = null;
|
||||
subscriptionService = null;
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.unregisterOnSharedPreferenceChangeListener(this);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
protected RecyclerView.LayoutManager getListLayoutManager() {
|
||||
return new LinearLayoutManager(activity);
|
||||
}
|
||||
|
||||
protected RecyclerView.LayoutManager getGridLayoutManager() {
|
||||
final Resources resources = activity.getResources();
|
||||
int width = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width);
|
||||
width += (24 * resources.getDisplayMetrics().density);
|
||||
final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels / (double)width);
|
||||
final GridLayoutManager lm = new GridLayoutManager(activity, spanCount);
|
||||
lm.setSpanSizeLookup(infoListAdapter.getSpanSizeLookup(spanCount));
|
||||
return lm;
|
||||
}
|
||||
|
||||
/*/////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
/////////////////////////////////////////////////////////////////////////*/
|
||||
|
|
@ -284,9 +325,10 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
|||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
final boolean useGrid = isGridLayout();
|
||||
infoListAdapter = new InfoListAdapter(getActivity());
|
||||
itemsList = rootView.findViewById(R.id.items_list);
|
||||
itemsList.setLayoutManager(new LinearLayoutManager(activity));
|
||||
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
|
||||
|
||||
View headerRootLayout;
|
||||
infoListAdapter.setHeader(headerRootLayout = activity.getLayoutInflater().inflate(R.layout.subscription_header, itemsList, false));
|
||||
|
|
@ -295,6 +337,7 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
|||
importExportOptions = headerRootLayout.findViewById(R.id.import_export_options);
|
||||
|
||||
infoListAdapter.useMiniItemVariants(true);
|
||||
infoListAdapter.setGridItemVariants(useGrid);
|
||||
itemsList.setAdapter(infoListAdapter);
|
||||
|
||||
setupImportFromItems(headerRootLayout.findViewById(R.id.import_from_options));
|
||||
|
|
@ -318,7 +361,7 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
|||
super.initListeners();
|
||||
|
||||
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() {
|
||||
@Override
|
||||
|
||||
public void selected(ChannelInfoItem selectedItem) {
|
||||
final FragmentManager fragmentManager = getFM();
|
||||
NavigationHelper.openChannelFragment(fragmentManager,
|
||||
|
|
@ -326,6 +369,11 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
|||
selectedItem.getUrl(),
|
||||
selectedItem.getName());
|
||||
}
|
||||
|
||||
public void held(ChannelInfoItem selectedItem) {
|
||||
showLongTapDialog(selectedItem);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
//noinspection ConstantConditions
|
||||
|
|
@ -336,6 +384,85 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
|||
importExportListHeader.setOnClickListener(v -> importExportOptions.switchState());
|
||||
}
|
||||
|
||||
private void showLongTapDialog(ChannelInfoItem selectedItem) {
|
||||
final Context context = getContext();
|
||||
final Activity activity = getActivity();
|
||||
if (context == null || context.getResources() == null || getActivity() == null) return;
|
||||
|
||||
final String[] commands = new String[]{
|
||||
context.getResources().getString(R.string.share),
|
||||
context.getResources().getString(R.string.unsubscribe)
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||
switch (i) {
|
||||
case 0:
|
||||
shareChannel(selectedItem);
|
||||
break;
|
||||
case 1:
|
||||
deleteChannel(selectedItem);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
final View bannerView = View.inflate(activity, R.layout.dialog_title, null);
|
||||
bannerView.setSelected(true);
|
||||
|
||||
TextView titleView = bannerView.findViewById(R.id.itemTitleView);
|
||||
titleView.setText(selectedItem.getName());
|
||||
|
||||
TextView detailsView = bannerView.findViewById(R.id.itemAdditionalDetails);
|
||||
detailsView.setVisibility(View.GONE);
|
||||
|
||||
new AlertDialog.Builder(activity)
|
||||
.setCustomTitle(bannerView)
|
||||
.setItems(commands, actions)
|
||||
.create()
|
||||
.show();
|
||||
|
||||
}
|
||||
|
||||
private void shareChannel (ChannelInfoItem selectedItem) {
|
||||
shareUrl(selectedItem.getName(), selectedItem.getUrl());
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private void deleteChannel (ChannelInfoItem selectedItem) {
|
||||
subscriptionService.subscriptionTable()
|
||||
.getSubscription(selectedItem.getServiceId(), selectedItem.getUrl())
|
||||
.toObservable()
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe(getDeleteObserver());
|
||||
|
||||
Toast.makeText(activity, getString(R.string.channel_unsubscribed), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
|
||||
|
||||
private Observer<List<SubscriptionEntity>> getDeleteObserver(){
|
||||
return new Observer<List<SubscriptionEntity>>() {
|
||||
@Override
|
||||
public void onSubscribe(Disposable d) {
|
||||
disposables.add(d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(List<SubscriptionEntity> subscriptionEntities) {
|
||||
subscriptionService.subscriptionTable().delete(subscriptionEntities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable exception) {
|
||||
SubscriptionFragment.this.onError(exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() { }
|
||||
};
|
||||
}
|
||||
|
||||
private void resetFragment() {
|
||||
if (disposables != null) disposables.clear();
|
||||
if (infoListAdapter != null) infoListAdapter.clearStreamItemList();
|
||||
|
|
@ -445,4 +572,22 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
|||
R.string.general_error);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (key.equals(getString(R.string.list_view_mode_key))) {
|
||||
updateFlags |= LIST_MODE_UPDATE_FLAG;
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isGridLayout() {
|
||||
final String list_mode = PreferenceManager.getDefaultSharedPreferences(activity).getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value));
|
||||
if ("auto".equals(list_mode)) {
|
||||
final Configuration configuration = getResources().getConfiguration();
|
||||
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
|
||||
} else {
|
||||
return "grid".equals(list_mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,35 +98,54 @@ public abstract class BasePlayer implements
|
|||
Player.EventListener, PlaybackListener, ImageLoadingListener {
|
||||
|
||||
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
||||
@NonNull public static final String TAG = "BasePlayer";
|
||||
@NonNull
|
||||
public static final String TAG = "BasePlayer";
|
||||
|
||||
@NonNull final protected Context context;
|
||||
@NonNull
|
||||
final protected Context context;
|
||||
|
||||
@NonNull final protected BroadcastReceiver broadcastReceiver;
|
||||
@NonNull final protected IntentFilter intentFilter;
|
||||
@NonNull
|
||||
final protected BroadcastReceiver broadcastReceiver;
|
||||
@NonNull
|
||||
final protected IntentFilter intentFilter;
|
||||
|
||||
@NonNull final protected HistoryRecordManager recordManager;
|
||||
@NonNull
|
||||
final protected HistoryRecordManager recordManager;
|
||||
|
||||
@NonNull final protected CustomTrackSelector trackSelector;
|
||||
@NonNull final protected PlayerDataSource dataSource;
|
||||
@NonNull
|
||||
final protected CustomTrackSelector trackSelector;
|
||||
@NonNull
|
||||
final protected PlayerDataSource dataSource;
|
||||
|
||||
@NonNull final private LoadControl loadControl;
|
||||
@NonNull final private RenderersFactory renderFactory;
|
||||
@NonNull
|
||||
final private LoadControl loadControl;
|
||||
@NonNull
|
||||
final private RenderersFactory renderFactory;
|
||||
|
||||
@NonNull final private SerialDisposable progressUpdateReactor;
|
||||
@NonNull final private CompositeDisposable databaseUpdateReactor;
|
||||
@NonNull
|
||||
final private SerialDisposable progressUpdateReactor;
|
||||
@NonNull
|
||||
final private CompositeDisposable databaseUpdateReactor;
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Intent
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@NonNull public static final String REPEAT_MODE = "repeat_mode";
|
||||
@NonNull public static final String PLAYBACK_PITCH = "playback_pitch";
|
||||
@NonNull public static final String PLAYBACK_SPEED = "playback_speed";
|
||||
@NonNull public static final String PLAYBACK_SKIP_SILENCE = "playback_skip_silence";
|
||||
@NonNull public static final String PLAYBACK_QUALITY = "playback_quality";
|
||||
@NonNull public static final String PLAY_QUEUE_KEY = "play_queue_key";
|
||||
@NonNull public static final String APPEND_ONLY = "append_only";
|
||||
@NonNull public static final String SELECT_ON_APPEND = "select_on_append";
|
||||
@NonNull
|
||||
public static final String REPEAT_MODE = "repeat_mode";
|
||||
@NonNull
|
||||
public static final String PLAYBACK_PITCH = "playback_pitch";
|
||||
@NonNull
|
||||
public static final String PLAYBACK_SPEED = "playback_speed";
|
||||
@NonNull
|
||||
public static final String PLAYBACK_SKIP_SILENCE = "playback_skip_silence";
|
||||
@NonNull
|
||||
public static final String PLAYBACK_QUALITY = "playback_quality";
|
||||
@NonNull
|
||||
public static final String PLAY_QUEUE_KEY = "play_queue_key";
|
||||
@NonNull
|
||||
public static final String APPEND_ONLY = "append_only";
|
||||
@NonNull
|
||||
public static final String SELECT_ON_APPEND = "select_on_append";
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback
|
||||
|
|
@ -137,13 +156,18 @@ public abstract class BasePlayer implements
|
|||
protected PlayQueue playQueue;
|
||||
protected PlayQueueAdapter playQueueAdapter;
|
||||
|
||||
@Nullable protected MediaSourceManager playbackManager;
|
||||
@Nullable
|
||||
protected MediaSourceManager playbackManager;
|
||||
|
||||
@Nullable private PlayQueueItem currentItem;
|
||||
@Nullable private MediaSourceTag currentMetadata;
|
||||
@Nullable private Bitmap currentThumbnail;
|
||||
@Nullable
|
||||
private PlayQueueItem currentItem;
|
||||
@Nullable
|
||||
private MediaSourceTag currentMetadata;
|
||||
@Nullable
|
||||
private Bitmap currentThumbnail;
|
||||
|
||||
@Nullable protected Toast errorToast;
|
||||
@Nullable
|
||||
protected Toast errorToast;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Player
|
||||
|
|
@ -213,7 +237,8 @@ public abstract class BasePlayer implements
|
|||
registerBroadcastReceiver();
|
||||
}
|
||||
|
||||
public void initListeners() {}
|
||||
public void initListeners() {
|
||||
}
|
||||
|
||||
public void handleIntent(Intent intent) {
|
||||
if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
|
||||
|
|
@ -230,7 +255,8 @@ public abstract class BasePlayer implements
|
|||
int sizeBeforeAppend = playQueue.size();
|
||||
playQueue.append(queue.getStreams());
|
||||
|
||||
if (intent.getBooleanExtra(SELECT_ON_APPEND, false) &&
|
||||
if ((intent.getBooleanExtra(SELECT_ON_APPEND, false) ||
|
||||
getCurrentState() == STATE_COMPLETED) &&
|
||||
queue.getStreams().size() > 0) {
|
||||
playQueue.setIndex(sizeBeforeAppend);
|
||||
}
|
||||
|
|
@ -296,7 +322,6 @@ public abstract class BasePlayer implements
|
|||
databaseUpdateReactor.clear();
|
||||
progressUpdateReactor.set(null);
|
||||
|
||||
simpleExoPlayer = null;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -424,13 +449,15 @@ public abstract class BasePlayer implements
|
|||
if (!isProgressLoopRunning()) startProgressLoop();
|
||||
}
|
||||
|
||||
public void onBuffering() {}
|
||||
public void onBuffering() {
|
||||
}
|
||||
|
||||
public void onPaused() {
|
||||
if (isProgressLoopRunning()) stopProgressLoop();
|
||||
}
|
||||
|
||||
public void onPausedSeek() {}
|
||||
public void onPausedSeek() {
|
||||
}
|
||||
|
||||
public void onCompleted() {
|
||||
if (DEBUG) Log.d(TAG, "onCompleted() called");
|
||||
|
|
@ -601,19 +628,19 @@ public abstract class BasePlayer implements
|
|||
/**
|
||||
* Processes the exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}.
|
||||
* There are multiple types of errors: <br><br>
|
||||
*
|
||||
* <p>
|
||||
* {@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}: <br><br>
|
||||
*
|
||||
* <p>
|
||||
* {@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}: <br><br>
|
||||
* If a runtime error occurred, then we can try to recover it by restarting the playback
|
||||
* after setting the timestamp recovery. <br><br>
|
||||
*
|
||||
* <p>
|
||||
* {@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER}: <br><br>
|
||||
* If the renderer failed, treat the error as unrecoverable.
|
||||
*
|
||||
* @see #processSourceError(IOException)
|
||||
* @see Player.EventListener#onPlayerError(ExoPlaybackException)
|
||||
* */
|
||||
*/
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
if (DEBUG) Log.d(TAG, "ExoPlayer - onPlayerError() called with: " +
|
||||
|
|
@ -899,8 +926,8 @@ public abstract class BasePlayer implements
|
|||
if (DEBUG) Log.d(TAG, "onPlayPrevious() called");
|
||||
|
||||
/* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT_MILLIS milliseconds,
|
||||
* restart current track. Also restart the track if the current track
|
||||
* is the first in a queue.*/
|
||||
* restart current track. Also restart the track if the current track
|
||||
* is the first in a queue.*/
|
||||
if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT_MILLIS ||
|
||||
playQueue.getIndex() == 0) {
|
||||
seekToDefault();
|
||||
|
|
@ -1009,8 +1036,8 @@ public abstract class BasePlayer implements
|
|||
try {
|
||||
metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag();
|
||||
} catch (IndexOutOfBoundsException | ClassCastException error) {
|
||||
if(DEBUG) Log.d(TAG, "Could not update metadata: " + error.getMessage());
|
||||
if(DEBUG) error.printStackTrace();
|
||||
if (DEBUG) Log.d(TAG, "Could not update metadata: " + error.getMessage());
|
||||
if (DEBUG) error.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1074,7 +1101,9 @@ public abstract class BasePlayer implements
|
|||
currentThumbnail;
|
||||
}
|
||||
|
||||
/** Checks if the current playback is a livestream AND is playing at or beyond the live edge */
|
||||
/**
|
||||
* Checks if the current playback is a livestream AND is playing at or beyond the live edge
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean isLiveEdge() {
|
||||
if (simpleExoPlayer == null || !isLive()) return false;
|
||||
|
|
@ -1098,13 +1127,14 @@ public abstract class BasePlayer implements
|
|||
} catch (@NonNull IndexOutOfBoundsException ignored) {
|
||||
// Why would this even happen =(
|
||||
// But lets log it anyway. Save is save
|
||||
if(DEBUG) Log.d(TAG, "Could not update metadata: " + ignored.getMessage());
|
||||
if(DEBUG) ignored.printStackTrace();
|
||||
if (DEBUG) Log.d(TAG, "Could not update metadata: " + ignored.getMessage());
|
||||
if (DEBUG) ignored.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isPlaying() {
|
||||
if (simpleExoPlayer == null) return false;
|
||||
final int state = simpleExoPlayer.getPlaybackState();
|
||||
return (state == Player.STATE_READY || state == Player.STATE_BUFFERING)
|
||||
&& simpleExoPlayer.getPlayWhenReady();
|
||||
|
|
@ -1112,7 +1142,9 @@ public abstract class BasePlayer implements
|
|||
|
||||
@Player.RepeatMode
|
||||
public int getRepeatMode() {
|
||||
return simpleExoPlayer == null ? Player.REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode();
|
||||
return simpleExoPlayer == null
|
||||
? Player.REPEAT_MODE_OFF
|
||||
: simpleExoPlayer.getRepeatMode();
|
||||
}
|
||||
|
||||
public void setRepeatMode(@Player.RepeatMode final int repeatMode) {
|
||||
|
|
@ -1178,4 +1210,8 @@ public abstract class BasePlayer implements
|
|||
if (DEBUG) Log.d(TAG, "Setting recovery, queue: " + queuePos + ", pos: " + windowPos);
|
||||
playQueue.setRecovery(queuePos, windowPos);
|
||||
}
|
||||
|
||||
public boolean gotDestroyed() {
|
||||
return simpleExoPlayer == null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,6 +175,10 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
setLandscape(lastOrientationWasLandscape);
|
||||
}
|
||||
|
||||
final int lastResizeMode = defaultPreferences.getInt(
|
||||
getString(R.string.last_resize_mode), AspectRatioFrameLayout.RESIZE_MODE_FIT);
|
||||
playerImpl.setResizeMode(lastResizeMode);
|
||||
|
||||
// Upon going in or out of multiwindow mode, isInMultiWindow will always be false,
|
||||
// since the first onResume needs to restore the player.
|
||||
// Subsequent onResume calls while multiwindow mode remains the same and the player is
|
||||
|
|
@ -213,10 +217,9 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
if (playerImpl == null) return;
|
||||
|
||||
playerImpl.setRecovery();
|
||||
playerState = new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(),
|
||||
playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(),
|
||||
playerImpl.getPlaybackQuality(), playerImpl.getPlaybackSkipSilence(),
|
||||
playerImpl.isPlaying());
|
||||
if(!playerImpl.gotDestroyed()) {
|
||||
playerState = createPlayerState();
|
||||
}
|
||||
StateSaver.tryToSave(isChangingConfigurations(), null, outState, this);
|
||||
}
|
||||
|
||||
|
|
@ -231,6 +234,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
if (!isBackPressed) {
|
||||
playerImpl.minimize();
|
||||
}
|
||||
playerState = createPlayerState();
|
||||
playerImpl.destroy();
|
||||
|
||||
isInMultiWindow = false;
|
||||
|
|
@ -241,6 +245,13 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
// State Saving
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private PlayerState createPlayerState() {
|
||||
return new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(),
|
||||
playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(),
|
||||
playerImpl.getPlaybackQuality(), playerImpl.getPlaybackSkipSilence(),
|
||||
playerImpl.isPlaying());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateSuffix() {
|
||||
return "." + UUID.randomUUID().toString() + ".player";
|
||||
|
|
@ -705,14 +716,27 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
|
||||
@Override
|
||||
protected int nextResizeMode(int currentResizeMode) {
|
||||
final int newResizeMode;
|
||||
switch (currentResizeMode) {
|
||||
case AspectRatioFrameLayout.RESIZE_MODE_FIT:
|
||||
return AspectRatioFrameLayout.RESIZE_MODE_FILL;
|
||||
newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL;
|
||||
break;
|
||||
case AspectRatioFrameLayout.RESIZE_MODE_FILL:
|
||||
return AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
||||
newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
||||
break;
|
||||
default:
|
||||
return AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
||||
newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
||||
break;
|
||||
}
|
||||
|
||||
storeResizeMode(newResizeMode);
|
||||
return newResizeMode;
|
||||
}
|
||||
|
||||
private void storeResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode) {
|
||||
defaultPreferences.edit()
|
||||
.putInt(getString(R.string.last_resize_mode), resizeMode)
|
||||
.apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -876,6 +900,11 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
public void onMove(int sourceIndex, int targetIndex) {
|
||||
if (playQueue != null) playQueue.move(sourceIndex, targetIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(int index) {
|
||||
if(index != -1) playQueue.remove(index);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -989,12 +1018,14 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
|
||||
private static final int MOVEMENT_THRESHOLD = 40;
|
||||
|
||||
private final boolean isPlayerGestureEnabled = PlayerHelper.isPlayerGestureEnabled(getApplicationContext());
|
||||
private final boolean isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(getApplicationContext());
|
||||
private final boolean isBrightnessGestureEnabled = PlayerHelper.isBrightnessGestureEnabled(getApplicationContext());
|
||||
|
||||
private final int maxVolume = playerImpl.getAudioReactor().getMaxVolume();
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent initialEvent, MotionEvent movingEvent, float distanceX, float distanceY) {
|
||||
if (!isPlayerGestureEnabled) return false;
|
||||
if (!isVolumeGestureEnabled && !isBrightnessGestureEnabled) return false;
|
||||
|
||||
//noinspection PointlessBooleanExpression
|
||||
if (DEBUG && false) Log.d(TAG, "MainVideoPlayer.onScroll = " +
|
||||
|
|
@ -1010,7 +1041,11 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
|
||||
isMoving = true;
|
||||
|
||||
if (initialEvent.getX() > playerImpl.getRootView().getWidth() / 2) {
|
||||
boolean acceptAnyArea = isVolumeGestureEnabled != isBrightnessGestureEnabled;
|
||||
boolean acceptVolumeArea = acceptAnyArea || initialEvent.getX() > playerImpl.getRootView().getWidth() / 2;
|
||||
boolean acceptBrightnessArea = acceptAnyArea || !acceptVolumeArea;
|
||||
|
||||
if (isVolumeGestureEnabled && acceptVolumeArea) {
|
||||
playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY);
|
||||
float currentProgressPercent =
|
||||
(float) playerImpl.getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength();
|
||||
|
|
@ -1035,7 +1070,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||
if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
|
||||
playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
} else if (isBrightnessGestureEnabled && acceptBrightnessArea) {
|
||||
playerImpl.getBrightnessProgressBar().incrementProgressBy((int) distanceY);
|
||||
float currentProgressPercent =
|
||||
(float) playerImpl.getBrightnessProgressBar().getProgress() / playerImpl.getMaxGestureLength();
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
|
|||
import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||
import org.schabi.newpipe.player.helper.LockManager;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.old.PlayVideoActivity;
|
||||
import org.schabi.newpipe.player.resolver.MediaSourceTag;
|
||||
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
|
|
@ -80,7 +79,6 @@ import java.util.List;
|
|||
import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING;
|
||||
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION;
|
||||
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.isUsingOldPlayer;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
/**
|
||||
|
|
@ -554,27 +552,17 @@ public final class PopupVideoPlayer extends Service {
|
|||
if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called");
|
||||
|
||||
setRecovery();
|
||||
Intent intent;
|
||||
if (!isUsingOldPlayer(getApplicationContext())) {
|
||||
intent = NavigationHelper.getPlayerIntent(
|
||||
context,
|
||||
MainVideoPlayer.class,
|
||||
this.getPlayQueue(),
|
||||
this.getRepeatMode(),
|
||||
this.getPlaybackSpeed(),
|
||||
this.getPlaybackPitch(),
|
||||
this.getPlaybackSkipSilence(),
|
||||
this.getPlaybackQuality()
|
||||
);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
} else {
|
||||
intent = new Intent(PopupVideoPlayer.this, PlayVideoActivity.class)
|
||||
.putExtra(PlayVideoActivity.VIDEO_TITLE, getVideoTitle())
|
||||
.putExtra(PlayVideoActivity.STREAM_URL, getSelectedVideoStream().getUrl())
|
||||
.putExtra(PlayVideoActivity.VIDEO_URL, getVideoUrl())
|
||||
.putExtra(PlayVideoActivity.START_POSITION, Math.round(getPlayer().getCurrentPosition() / 1000f));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
}
|
||||
final Intent intent = NavigationHelper.getPlayerIntent(
|
||||
context,
|
||||
MainVideoPlayer.class,
|
||||
this.getPlayQueue(),
|
||||
this.getRepeatMode(),
|
||||
this.getPlaybackSpeed(),
|
||||
this.getPlaybackPitch(),
|
||||
this.getPlaybackSkipSilence(),
|
||||
this.getPlaybackQuality()
|
||||
);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
closePopup();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -375,6 +375,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
|||
public void onMove(int sourceIndex, int targetIndex) {
|
||||
if (player != null) player.getPlayQueue().move(sourceIndex, targetIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(int index) {
|
||||
if (index != -1) player.getPlayQueue().remove(index);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -683,12 +683,17 @@ public abstract class VideoPlayer extends BasePlayer
|
|||
if (getAspectRatioFrameLayout() != null) {
|
||||
final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode();
|
||||
final int newResizeMode = nextResizeMode(currentResizeMode);
|
||||
getAspectRatioFrameLayout().setResizeMode(newResizeMode);
|
||||
getResizeView().setText(PlayerHelper.resizeTypeOf(context, newResizeMode));
|
||||
setResizeMode(newResizeMode);
|
||||
}
|
||||
}
|
||||
|
||||
protected void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) {
|
||||
getAspectRatioFrameLayout().setResizeMode(resizeMode);
|
||||
getResizeView().setText(PlayerHelper.resizeTypeOf(context, resizeMode));
|
||||
}
|
||||
|
||||
protected abstract int nextResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode);
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// SeekBar Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@ import com.google.android.exoplayer2.util.MimeTypes;
|
|||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.Subtitles;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesFormat;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
|
|
@ -45,7 +45,9 @@ import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MOD
|
|||
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
||||
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.*;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
|
||||
|
||||
public class PlayerHelper {
|
||||
private PlayerHelper() {}
|
||||
|
|
@ -87,7 +89,7 @@ public class PlayerHelper {
|
|||
return pitchFormatter.format(pitch);
|
||||
}
|
||||
|
||||
public static String mimeTypesOf(final SubtitlesFormat format) {
|
||||
public static String subtitleMimeTypesOf(final MediaFormat format) {
|
||||
switch (format) {
|
||||
case VTT: return MimeTypes.TEXT_VTT;
|
||||
case TTML: return MimeTypes.APPLICATION_TTML;
|
||||
|
|
@ -97,8 +99,8 @@ public class PlayerHelper {
|
|||
|
||||
@NonNull
|
||||
public static String captionLanguageOf(@NonNull final Context context,
|
||||
@NonNull final Subtitles subtitles) {
|
||||
final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale());
|
||||
@NonNull final SubtitlesStream subtitles) {
|
||||
final String displayName = subtitles.getDisplayLanguageName();
|
||||
return displayName + (subtitles.isAutoGenerated() ? " (" + context.getString(R.string.caption_auto_generated)+ ")" : "");
|
||||
}
|
||||
|
||||
|
|
@ -145,7 +147,7 @@ public class PlayerHelper {
|
|||
|
||||
final StreamInfoItem nextVideo = info.getNextVideo();
|
||||
if (nextVideo != null && !urls.contains(nextVideo.getUrl())) {
|
||||
return new SinglePlayQueue(nextVideo);
|
||||
return getAutoQueuedSinglePlayQueue(nextVideo);
|
||||
}
|
||||
|
||||
final List<InfoItem> relatedItems = info.getRelatedStreams();
|
||||
|
|
@ -158,7 +160,7 @@ public class PlayerHelper {
|
|||
}
|
||||
}
|
||||
Collections.shuffle(autoQueueItems);
|
||||
return autoQueueItems.isEmpty() ? null : new SinglePlayQueue(autoQueueItems.get(0));
|
||||
return autoQueueItems.isEmpty() ? null : getAutoQueuedSinglePlayQueue(autoQueueItems.get(0));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -169,12 +171,12 @@ public class PlayerHelper {
|
|||
return isResumeAfterAudioFocusGain(context, false);
|
||||
}
|
||||
|
||||
public static boolean isPlayerGestureEnabled(@NonNull final Context context) {
|
||||
return isPlayerGestureEnabled(context, true);
|
||||
public static boolean isVolumeGestureEnabled(@NonNull final Context context) {
|
||||
return isVolumeGestureEnabled(context, true);
|
||||
}
|
||||
|
||||
public static boolean isUsingOldPlayer(@NonNull final Context context) {
|
||||
return isUsingOldPlayer(context, false);
|
||||
public static boolean isBrightnessGestureEnabled(@NonNull final Context context) {
|
||||
return isBrightnessGestureEnabled(context, true);
|
||||
}
|
||||
|
||||
public static boolean isRememberingPopupDimensions(@NonNull final Context context) {
|
||||
|
|
@ -306,12 +308,12 @@ public class PlayerHelper {
|
|||
return getPreferences(context).getBoolean(context.getString(R.string.resume_on_audio_focus_gain_key), b);
|
||||
}
|
||||
|
||||
private static boolean isPlayerGestureEnabled(@NonNull final Context context, final boolean b) {
|
||||
return getPreferences(context).getBoolean(context.getString(R.string.player_gesture_controls_key), b);
|
||||
private static boolean isVolumeGestureEnabled(@NonNull final Context context, final boolean b) {
|
||||
return getPreferences(context).getBoolean(context.getString(R.string.volume_gesture_control_key), b);
|
||||
}
|
||||
|
||||
private static boolean isUsingOldPlayer(@NonNull final Context context, final boolean b) {
|
||||
return getPreferences(context).getBoolean(context.getString(R.string.use_old_player_key), b);
|
||||
private static boolean isBrightnessGestureEnabled(@NonNull final Context context, final boolean b) {
|
||||
return getPreferences(context).getBoolean(context.getString(R.string.brightness_gesture_control_key), b);
|
||||
}
|
||||
|
||||
private static boolean isRememberingPopupDimensions(@NonNull final Context context, final boolean b) {
|
||||
|
|
@ -350,4 +352,10 @@ public class PlayerHelper {
|
|||
return getPreferences(context).getString(context.getString(R.string.minimize_on_exit_key),
|
||||
key);
|
||||
}
|
||||
|
||||
private static SinglePlayQueue getAutoQueuedSinglePlayQueue(StreamInfoItem streamInfoItem) {
|
||||
SinglePlayQueue singlePlayQueue = new SinglePlayQueue(streamInfoItem);
|
||||
singlePlayQueue.getItem().setAutoQueued(true);
|
||||
return singlePlayQueue;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,369 +0,0 @@
|
|||
package org.schabi.newpipe.player.old;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.MediaController;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.VideoView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
/*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* PlayVideoActivity.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class PlayVideoActivity extends AppCompatActivity {
|
||||
|
||||
//// TODO: 11.09.15 add "choose stream" menu
|
||||
|
||||
private static final String TAG = PlayVideoActivity.class.toString();
|
||||
public static final String VIDEO_URL = "video_url";
|
||||
public static final String STREAM_URL = "stream_url";
|
||||
public static final String VIDEO_TITLE = "video_title";
|
||||
private static final String POSITION = "position";
|
||||
public static final String START_POSITION = "start_position";
|
||||
|
||||
private static final long HIDING_DELAY = 3000;
|
||||
|
||||
private String videoUrl = "";
|
||||
|
||||
private ActionBar actionBar;
|
||||
private VideoView videoView;
|
||||
private int position;
|
||||
private MediaController mediaController;
|
||||
private ProgressBar progressBar;
|
||||
private View decorView;
|
||||
private boolean uiIsHidden;
|
||||
private static long lastUiShowTime;
|
||||
private boolean isLandscape = true;
|
||||
private boolean hasSoftKeys;
|
||||
|
||||
private SharedPreferences prefs;
|
||||
private static final String PREF_IS_LANDSCAPE = "is_landscape";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_play_video);
|
||||
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||
|
||||
//set background arrow style
|
||||
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_arrow_back_white_24dp);
|
||||
|
||||
isLandscape = checkIfLandscape();
|
||||
hasSoftKeys = checkIfHasSoftKeys();
|
||||
|
||||
actionBar = getSupportActionBar();
|
||||
assert actionBar != null;
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
Intent intent = getIntent();
|
||||
if(mediaController == null) {
|
||||
//prevents back button hiding media controller controls (after showing them)
|
||||
//instead of exiting video
|
||||
//see http://stackoverflow.com/questions/6051825
|
||||
//also solves https://github.com/theScrabi/NewPipe/issues/99
|
||||
mediaController = new MediaController(this) {
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
int keyCode = event.getKeyCode();
|
||||
final boolean uniqueDown = event.getRepeatCount() == 0
|
||||
&& event.getAction() == KeyEvent.ACTION_DOWN;
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
if (uniqueDown)
|
||||
{
|
||||
if (isShowing()) {
|
||||
finish();
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
position = intent.getIntExtra(START_POSITION, 0)*1000;//convert from seconds to milliseconds
|
||||
|
||||
videoView = findViewById(R.id.video_view);
|
||||
progressBar = findViewById(R.id.play_video_progress_bar);
|
||||
try {
|
||||
videoView.setMediaController(mediaController);
|
||||
videoView.setVideoURI(Uri.parse(intent.getStringExtra(STREAM_URL)));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
videoView.requestFocus();
|
||||
videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
|
||||
@Override
|
||||
public void onPrepared(MediaPlayer mp) {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
videoView.seekTo(position);
|
||||
if (position <= 0) {
|
||||
videoView.start();
|
||||
showUi();
|
||||
} else {
|
||||
videoView.pause();
|
||||
}
|
||||
}
|
||||
});
|
||||
videoUrl = intent.getStringExtra(VIDEO_URL);
|
||||
|
||||
Button button = findViewById(R.id.content_button);
|
||||
button.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if(uiIsHidden) {
|
||||
showUi();
|
||||
} else {
|
||||
hideUi();
|
||||
}
|
||||
}
|
||||
});
|
||||
decorView = getWindow().getDecorView();
|
||||
decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
|
||||
@Override
|
||||
public void onSystemUiVisibilityChange(int visibility) {
|
||||
if (visibility == View.VISIBLE && uiIsHidden) {
|
||||
showUi();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= 17) {
|
||||
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
}
|
||||
|
||||
prefs = getPreferences(Context.MODE_PRIVATE);
|
||||
if(prefs.getBoolean(PREF_IS_LANDSCAPE, false) && !isLandscape) {
|
||||
toggleOrientation();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreatePanelMenu(int featured, Menu menu) {
|
||||
super.onCreatePanelMenu(featured, menu);
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.video_player, menu);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
videoView.pause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
prefs = getPreferences(Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
switch(id) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, videoUrl);
|
||||
intent.setType("text/plain");
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
|
||||
break;
|
||||
case R.id.menu_item_screen_rotation:
|
||||
toggleOrientation();
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Error: MenuItem not known");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration config) {
|
||||
super.onConfigurationChanged(config);
|
||||
|
||||
if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
isLandscape = true;
|
||||
adjustMediaControlMetrics();
|
||||
} else if (config.orientation == Configuration.ORIENTATION_PORTRAIT){
|
||||
isLandscape = false;
|
||||
adjustMediaControlMetrics();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle savedInstanceState) {
|
||||
super.onSaveInstanceState(savedInstanceState);
|
||||
//savedInstanceState.putInt(POSITION, videoView.getCurrentPosition());
|
||||
//videoView.pause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
position = savedInstanceState.getInt(POSITION);
|
||||
//videoView.seekTo(position);
|
||||
}
|
||||
|
||||
private void showUi() {
|
||||
try {
|
||||
uiIsHidden = false;
|
||||
mediaController.show(100000);
|
||||
actionBar.show();
|
||||
adjustMediaControlMetrics();
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
Handler handler = new Handler();
|
||||
handler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if ((System.currentTimeMillis() - lastUiShowTime) >= HIDING_DELAY) {
|
||||
hideUi();
|
||||
}
|
||||
}
|
||||
}, HIDING_DELAY);
|
||||
lastUiShowTime = System.currentTimeMillis();
|
||||
}catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void hideUi() {
|
||||
uiIsHidden = true;
|
||||
actionBar.hide();
|
||||
mediaController.hide();
|
||||
if (android.os.Build.VERSION.SDK_INT >= 17) {
|
||||
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
}
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
}
|
||||
|
||||
private void adjustMediaControlMetrics() {
|
||||
MediaController.LayoutParams mediaControllerLayout
|
||||
= new MediaController.LayoutParams(MediaController.LayoutParams.MATCH_PARENT,
|
||||
MediaController.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
if(!hasSoftKeys) {
|
||||
mediaControllerLayout.setMargins(20, 0, 20, 20);
|
||||
} else {
|
||||
int width = getNavigationBarWidth();
|
||||
int height = getNavigationBarHeight();
|
||||
mediaControllerLayout.setMargins(width + 20, 0, width + 20, height + 20);
|
||||
}
|
||||
mediaController.setLayoutParams(mediaControllerLayout);
|
||||
}
|
||||
|
||||
private boolean checkIfHasSoftKeys(){
|
||||
return Build.VERSION.SDK_INT >= 17 ||
|
||||
getNavigationBarHeight() != 0 ||
|
||||
getNavigationBarWidth() != 0;
|
||||
}
|
||||
|
||||
private int getNavigationBarHeight() {
|
||||
if(Build.VERSION.SDK_INT >= 17) {
|
||||
Display d = getWindowManager().getDefaultDisplay();
|
||||
|
||||
DisplayMetrics realDisplayMetrics = new DisplayMetrics();
|
||||
d.getRealMetrics(realDisplayMetrics);
|
||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
d.getMetrics(displayMetrics);
|
||||
|
||||
int realHeight = realDisplayMetrics.heightPixels;
|
||||
int displayHeight = displayMetrics.heightPixels;
|
||||
return realHeight - displayHeight;
|
||||
} else {
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
|
||||
private int getNavigationBarWidth() {
|
||||
if(Build.VERSION.SDK_INT >= 17) {
|
||||
Display d = getWindowManager().getDefaultDisplay();
|
||||
|
||||
DisplayMetrics realDisplayMetrics = new DisplayMetrics();
|
||||
d.getRealMetrics(realDisplayMetrics);
|
||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
d.getMetrics(displayMetrics);
|
||||
|
||||
int realWidth = realDisplayMetrics.widthPixels;
|
||||
int displayWidth = displayMetrics.widthPixels;
|
||||
return realWidth - displayWidth;
|
||||
} else {
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkIfLandscape() {
|
||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
|
||||
return displayMetrics.heightPixels < displayMetrics.widthPixels;
|
||||
}
|
||||
|
||||
private void toggleOrientation() {
|
||||
if(isLandscape) {
|
||||
isLandscape = false;
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
} else {
|
||||
isLandscape = true;
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
|
||||
}
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putBoolean(PREF_IS_LANDSCAPE, isLandscape);
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
|
|
@ -233,6 +233,9 @@ public abstract class PlayQueue implements Serializable {
|
|||
backup.addAll(itemList);
|
||||
Collections.shuffle(itemList);
|
||||
}
|
||||
if (!streams.isEmpty() && streams.get(streams.size() - 1).isAutoQueued() && !itemList.get(0).isAutoQueued()) {
|
||||
streams.remove(streams.size() - 1);
|
||||
}
|
||||
streams.addAll(itemList);
|
||||
|
||||
broadcast(new AppendEvent(itemList.size()));
|
||||
|
|
@ -314,7 +317,9 @@ public abstract class PlayQueue implements Serializable {
|
|||
queueIndex.incrementAndGet();
|
||||
}
|
||||
|
||||
streams.add(target, streams.remove(source));
|
||||
PlayQueueItem playQueueItem = streams.remove(source);
|
||||
playQueueItem.setAutoQueued(false);
|
||||
streams.add(target, playQueueItem);
|
||||
broadcast(new MoveEvent(source, target));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,9 +25,10 @@ public class PlayQueueItem implements Serializable {
|
|||
@NonNull final private String uploader;
|
||||
@NonNull final private StreamType streamType;
|
||||
|
||||
private boolean isAutoQueued;
|
||||
|
||||
private long recoveryPosition;
|
||||
private Throwable error;
|
||||
|
||||
PlayQueueItem(@NonNull final StreamInfo info) {
|
||||
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
|
||||
info.getThumbnailUrl(), info.getUploaderName(), info.getStreamType());
|
||||
|
|
@ -105,6 +106,14 @@ public class PlayQueueItem implements Serializable {
|
|||
.doOnError(throwable -> error = throwable);
|
||||
}
|
||||
|
||||
public boolean isAutoQueued() {
|
||||
return isAutoQueued;
|
||||
}
|
||||
|
||||
public void setAutoQueued(boolean autoQueued) {
|
||||
isAutoQueued = autoQueued;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Item States, keep external access out
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleC
|
|||
private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25;
|
||||
|
||||
public PlayQueueItemTouchCallback() {
|
||||
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
|
||||
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.RIGHT);
|
||||
}
|
||||
|
||||
public abstract void onMove(final int sourceIndex, final int targetIndex);
|
||||
|
||||
public abstract void onSwiped(int index);
|
||||
|
||||
@Override
|
||||
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
|
||||
int viewSizeOutOfBounds, int totalSize,
|
||||
|
|
@ -44,9 +46,11 @@ public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleC
|
|||
|
||||
@Override
|
||||
public boolean isItemViewSwipeEnabled() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
|
||||
onSwiped(viewHolder.getAdapterPosition());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ import com.google.android.exoplayer2.source.MediaSource;
|
|||
import com.google.android.exoplayer2.source.MergingMediaSource;
|
||||
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.Subtitles;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
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.player.helper.PlayerDataSource;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
|
|
@ -93,8 +94,8 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
|||
// Below are auxiliary media sources
|
||||
|
||||
// Create subtitle sources
|
||||
for (final Subtitles subtitle : info.getSubtitles()) {
|
||||
final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType());
|
||||
for (final SubtitlesStream subtitle : info.getSubtitles()) {
|
||||
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
|
||||
if (mimeType == null) continue;
|
||||
|
||||
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import com.nononsenseapps.filepicker.Utils;
|
|||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.utils.Localization;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
|
|
@ -106,6 +108,20 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
|||
startActivityForResult(i, REQUEST_EXPORT_PATH);
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference setPreferredLanguage = findPreference(getString(R.string.content_language_key));
|
||||
setPreferredLanguage.setOnPreferenceChangeListener((Preference p, Object newLanguage) -> {
|
||||
Localization oldLocal = org.schabi.newpipe.util.Localization.getPreferredExtractorLocal(getActivity());
|
||||
NewPipe.setLocalization(new Localization(oldLocal.getCountry(), (String) newLanguage));
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference setPreferredCountry = findPreference(getString(R.string.content_country_key));
|
||||
setPreferredCountry.setOnPreferenceChangeListener((Preference p, Object newCountry) -> {
|
||||
Localization oldLocal = org.schabi.newpipe.util.Localization.getPreferredExtractorLocal(getActivity());
|
||||
NewPipe.setLocalization(new Localization((String) newCountry, oldLocal.getLanguage()));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
103
app/src/main/java/org/schabi/newpipe/streams/DataReader.java
Normal file
103
app/src/main/java/org/schabi/newpipe/streams/DataReader.java
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
package org.schabi.newpipe.streams;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class DataReader {
|
||||
|
||||
public final static int SHORT_SIZE = 2;
|
||||
public final static int LONG_SIZE = 8;
|
||||
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;
|
||||
|
||||
public DataReader(SharpStream stream) {
|
||||
this.rewind = stream.canRewind();
|
||||
this.stream = stream;
|
||||
this.pos = 0L;
|
||||
}
|
||||
|
||||
public long position() {
|
||||
return pos;
|
||||
}
|
||||
|
||||
public final 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 final long skipBytes(long amount) throws IOException {
|
||||
amount = stream.skip(amount);
|
||||
pos += amount;
|
||||
return amount;
|
||||
}
|
||||
|
||||
public final 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 {
|
||||
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;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public final boolean available() {
|
||||
return stream.available() > 0;
|
||||
}
|
||||
|
||||
public void rewind() throws IOException {
|
||||
stream.rewind();
|
||||
pos = 0;
|
||||
}
|
||||
|
||||
public boolean canRewind() {
|
||||
return rewind;
|
||||
}
|
||||
|
||||
private 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;
|
||||
if (read != amount) {
|
||||
throw new EOFException("Truncated data, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
817
app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java
Normal file
817
app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java
Normal file
|
|
@ -0,0 +1,817 @@
|
|||
package org.schabi.newpipe.streams;
|
||||
|
||||
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
|
||||
*/
|
||||
public class Mp4DashReader {
|
||||
|
||||
// <editor-fold defaultState="collapsed" desc="Constants">
|
||||
private static final int ATOM_MOOF = 0x6D6F6F66;
|
||||
private static final int ATOM_MFHD = 0x6D666864;
|
||||
private static final int ATOM_TRAF = 0x74726166;
|
||||
private static final int ATOM_TFHD = 0x74666864;
|
||||
private static final int ATOM_TFDT = 0x74666474;
|
||||
private static final int ATOM_TRUN = 0x7472756E;
|
||||
private static final int ATOM_MDIA = 0x6D646961;
|
||||
private static final int ATOM_FTYP = 0x66747970;
|
||||
private static final int ATOM_SIDX = 0x73696478;
|
||||
private static final int ATOM_MOOV = 0x6D6F6F76;
|
||||
private static final int ATOM_MDAT = 0x6D646174;
|
||||
private static final int ATOM_MVHD = 0x6D766864;
|
||||
private static final int ATOM_TRAK = 0x7472616B;
|
||||
private static final int ATOM_MVEX = 0x6D766578;
|
||||
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 BRAND_DASH = 0x64617368;
|
||||
// </editor-fold>
|
||||
|
||||
private final DataReader stream;
|
||||
|
||||
private Mp4Track[] tracks = null;
|
||||
|
||||
private Box box;
|
||||
private Moof moof;
|
||||
|
||||
private boolean chunkZero = false;
|
||||
|
||||
private int selectedTrack = -1;
|
||||
|
||||
public enum TrackKind {
|
||||
Audio, Video, Other
|
||||
}
|
||||
|
||||
public Mp4DashReader(SharpStream source) {
|
||||
this.stream = new DataReader(source);
|
||||
}
|
||||
|
||||
public void parse() throws IOException, NoSuchElementException {
|
||||
if (selectedTrack > -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
box = readBox(ATOM_FTYP);
|
||||
if (parse_ftyp() != BRAND_DASH) {
|
||||
throw new NoSuchElementException("Main Brand is not dash");
|
||||
}
|
||||
|
||||
Moov moov = null;
|
||||
int i;
|
||||
|
||||
while (box.type != ATOM_MOOF) {
|
||||
ensure(box);
|
||||
box = readBox();
|
||||
|
||||
switch (box.type) {
|
||||
case ATOM_MOOV:
|
||||
moov = parse_moov(box);
|
||||
break;
|
||||
case ATOM_SIDX:
|
||||
break;
|
||||
case ATOM_MFRA:
|
||||
break;
|
||||
case ATOM_MDAT:
|
||||
throw new IOException("Expected moof, found mdat");
|
||||
}
|
||||
}
|
||||
|
||||
if (moov == null) {
|
||||
throw new IOException("The provided Mp4 doesn't have the 'moov' box");
|
||||
}
|
||||
|
||||
tracks = new Mp4Track[moov.trak.length];
|
||||
|
||||
for (i = 0; i < tracks.length; i++) {
|
||||
tracks[i] = new Mp4Track();
|
||||
tracks[i].trak = moov.trak[i];
|
||||
|
||||
if (moov.mvex_trex != null) {
|
||||
for (Trex mvex_trex : moov.mvex_trex) {
|
||||
if (tracks[i].trak.tkhd.trackId == mvex_trex.trackId) {
|
||||
tracks[i].trex = mvex_trex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Mp4Track selectTrack(int index) {
|
||||
selectedTrack = index;
|
||||
return tracks[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Count all fragments present. This operation requires a seekable stream
|
||||
*
|
||||
* @return list with a basic info
|
||||
* @throws IOException if the source stream is not seekeable
|
||||
*/
|
||||
public int getFragmentsCount() throws IOException {
|
||||
if (selectedTrack < 0) {
|
||||
throw new IllegalStateException("track no selected");
|
||||
}
|
||||
if (!stream.canRewind()) {
|
||||
throw new IOException("The provided stream doesn't allow seek");
|
||||
}
|
||||
|
||||
Box tmp;
|
||||
int count = 0;
|
||||
long orig_offset = stream.position();
|
||||
|
||||
if (box.type == ATOM_MOOF) {
|
||||
tmp = box;
|
||||
} else {
|
||||
ensure(box);
|
||||
tmp = readBox();
|
||||
}
|
||||
|
||||
do {
|
||||
if (tmp.type == ATOM_MOOF) {
|
||||
ensure(readBox(ATOM_MFHD));
|
||||
Box traf;
|
||||
while ((traf = untilBox(tmp, ATOM_TRAF)) != null) {
|
||||
Box tfhd = readBox(ATOM_TFHD);
|
||||
if (parse_tfhd(tracks[selectedTrack].trak.tkhd.trackId) != null) {
|
||||
count++;
|
||||
break;
|
||||
}
|
||||
ensure(tfhd);
|
||||
ensure(traf);
|
||||
}
|
||||
}
|
||||
ensure(tmp);
|
||||
} while (stream.available() && (tmp = readBox()) != null);
|
||||
|
||||
stream.rewind();
|
||||
stream.skipBytes((int) orig_offset);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public Mp4Track[] getAvailableTracks() {
|
||||
return tracks;
|
||||
}
|
||||
|
||||
public Mp4TrackChunk getNextChunk() throws IOException {
|
||||
Mp4Track track = tracks[selectedTrack];
|
||||
|
||||
while (stream.available()) {
|
||||
|
||||
if (chunkZero) {
|
||||
ensure(box);
|
||||
if (!stream.available()) {
|
||||
break;
|
||||
}
|
||||
box = readBox();
|
||||
} else {
|
||||
chunkZero = true;
|
||||
}
|
||||
|
||||
switch (box.type) {
|
||||
case ATOM_MOOF:
|
||||
if (moof != null) {
|
||||
throw new IOException("moof found without mdat");
|
||||
}
|
||||
|
||||
moof = parse_moof(box, track.trak.tkhd.trackId);
|
||||
|
||||
if (moof.traf != null) {
|
||||
|
||||
if (hasFlag(moof.traf.trun.bFlags, 0x0001)) {
|
||||
moof.traf.trun.dataOffset -= box.size + 8;
|
||||
if (moof.traf.trun.dataOffset < 0) {
|
||||
throw new IOException("trun box has wrong data offset, points outside of concurrent mdat box");
|
||||
}
|
||||
}
|
||||
|
||||
if (moof.traf.trun.chunkSize < 1) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (!hasFlag(moof.traf.trun.bFlags, 0x900) && moof.traf.trun.chunkDuration == 0) {
|
||||
if (hasFlag(moof.traf.tfhd.bFlags, 0x20)) {
|
||||
moof.traf.trun.chunkDuration = moof.traf.tfhd.defaultSampleDuration * moof.traf.trun.entryCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ATOM_MDAT:
|
||||
if (moof == null) {
|
||||
throw new IOException("mdat found without moof");
|
||||
}
|
||||
|
||||
if (moof.traf == null) {
|
||||
moof = null;
|
||||
continue;// find another chunk
|
||||
}
|
||||
|
||||
Mp4TrackChunk chunk = new Mp4TrackChunk();
|
||||
chunk.moof = moof;
|
||||
chunk.data = new TrackDataChunk(stream, moof.traf.trun.chunkSize);
|
||||
moof = null;
|
||||
|
||||
stream.skipBytes(chunk.moof.traf.trun.dataOffset);
|
||||
return chunk;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// <editor-fold defaultState="collapsed" desc="Utils">
|
||||
private long readUint() throws IOException {
|
||||
return stream.readInt() & 0xffffffffL;
|
||||
}
|
||||
|
||||
public static boolean hasFlag(int flags, int mask) {
|
||||
return (flags & mask) == mask;
|
||||
}
|
||||
|
||||
private String boxName(Box ref) {
|
||||
return boxName(ref.type);
|
||||
}
|
||||
|
||||
private String boxName(int type) {
|
||||
try {
|
||||
return new String(ByteBuffer.allocate(4).putInt(type).array(), "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
return "0x" + Integer.toHexString(type);
|
||||
}
|
||||
}
|
||||
|
||||
private Box readBox() throws IOException {
|
||||
Box b = new Box();
|
||||
b.offset = stream.position();
|
||||
b.size = stream.readInt();
|
||||
b.type = stream.readInt();
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
private Box readBox(int expected) throws IOException {
|
||||
Box b = readBox();
|
||||
if (b.type != expected) {
|
||||
throw new NoSuchElementException("expected " + boxName(expected) + " found " + boxName(b));
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
private void ensure(Box ref) throws IOException {
|
||||
long skip = ref.offset + ref.size - stream.position();
|
||||
|
||||
if (skip == 0) {
|
||||
return;
|
||||
} else if (skip < 0) {
|
||||
throw new EOFException(String.format(
|
||||
"parser go beyond limits of the box. type=%s offset=%s size=%s position=%s",
|
||||
boxName(ref), ref.offset, ref.size, stream.position()
|
||||
));
|
||||
}
|
||||
|
||||
stream.skipBytes((int) skip);
|
||||
}
|
||||
|
||||
private Box untilBox(Box ref, int... expected) throws IOException {
|
||||
Box b;
|
||||
while (stream.position() < (ref.offset + ref.size)) {
|
||||
b = readBox();
|
||||
for (int type : expected) {
|
||||
if (b.type == type) {
|
||||
return b;
|
||||
}
|
||||
}
|
||||
ensure(b);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultState="collapsed" desc="Box readers">
|
||||
|
||||
private Moof parse_moof(Box ref, int trackId) throws IOException {
|
||||
Moof obj = new Moof();
|
||||
|
||||
Box b = readBox(ATOM_MFHD);
|
||||
obj.mfhd_SequenceNumber = parse_mfhd();
|
||||
ensure(b);
|
||||
|
||||
while ((b = untilBox(ref, ATOM_TRAF)) != null) {
|
||||
obj.traf = parse_traf(b, trackId);
|
||||
ensure(b);
|
||||
|
||||
if (obj.traf != null) {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private int parse_mfhd() throws IOException {
|
||||
// version
|
||||
// flags
|
||||
stream.skipBytes(4);
|
||||
|
||||
return stream.readInt();
|
||||
}
|
||||
|
||||
private Traf parse_traf(Box ref, int trackId) throws IOException {
|
||||
Traf traf = new Traf();
|
||||
|
||||
Box b = readBox(ATOM_TFHD);
|
||||
traf.tfhd = parse_tfhd(trackId);
|
||||
ensure(b);
|
||||
|
||||
if (traf.tfhd == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
b = untilBox(ref, ATOM_TRUN, ATOM_TFDT);
|
||||
|
||||
if (b.type == ATOM_TFDT) {
|
||||
traf.tfdt = parse_tfdt();
|
||||
ensure(b);
|
||||
b = readBox(ATOM_TRUN);
|
||||
}
|
||||
|
||||
traf.trun = parse_trun();
|
||||
ensure(b);
|
||||
|
||||
return traf;
|
||||
}
|
||||
|
||||
private Tfhd parse_tfhd(int trackId) throws IOException {
|
||||
Tfhd obj = new Tfhd();
|
||||
|
||||
obj.bFlags = stream.readInt();
|
||||
obj.trackId = stream.readInt();
|
||||
|
||||
if (trackId != -1 && obj.trackId != trackId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hasFlag(obj.bFlags, 0x01)) {
|
||||
stream.skipBytes(8);
|
||||
}
|
||||
if (hasFlag(obj.bFlags, 0x02)) {
|
||||
stream.skipBytes(4);
|
||||
}
|
||||
if (hasFlag(obj.bFlags, 0x08)) {
|
||||
obj.defaultSampleDuration = stream.readInt();
|
||||
}
|
||||
if (hasFlag(obj.bFlags, 0x10)) {
|
||||
obj.defaultSampleSize = stream.readInt();
|
||||
}
|
||||
if (hasFlag(obj.bFlags, 0x20)) {
|
||||
obj.defaultSampleFlags = stream.readInt();
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private long parse_tfdt() throws IOException {
|
||||
int version = stream.read();
|
||||
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.entries_rowSize = 0;
|
||||
if (hasFlag(obj.bFlags, 0x0100)) {
|
||||
obj.entries_rowSize += 4;
|
||||
}
|
||||
if (hasFlag(obj.bFlags, 0x0200)) {
|
||||
obj.entries_rowSize += 4;
|
||||
}
|
||||
if (hasFlag(obj.bFlags, 0x0400)) {
|
||||
obj.entries_rowSize += 4;
|
||||
}
|
||||
if (hasFlag(obj.bFlags, 0x0800)) {
|
||||
obj.entries_rowSize += 4;
|
||||
}
|
||||
obj.bEntries = new byte[obj.entries_rowSize * obj.entryCount];
|
||||
|
||||
if (hasFlag(obj.bFlags, 0x0001)) {
|
||||
obj.dataOffset = stream.readInt();
|
||||
}
|
||||
if (hasFlag(obj.bFlags, 0x0004)) {
|
||||
obj.bFirstSampleFlags = stream.readInt();
|
||||
}
|
||||
|
||||
stream.read(obj.bEntries);
|
||||
|
||||
for (int i = 0; i < obj.entryCount; i++) {
|
||||
TrunEntry entry = obj.getEntry(i);
|
||||
if (hasFlag(obj.bFlags, 0x0100)) {
|
||||
obj.chunkDuration += entry.sampleDuration;
|
||||
}
|
||||
if (hasFlag(obj.bFlags, 0x0200)) {
|
||||
obj.chunkSize += entry.sampleSize;
|
||||
}
|
||||
if (hasFlag(obj.bFlags, 0x0800)) {
|
||||
if (!hasFlag(obj.bFlags, 0x0100)) {
|
||||
obj.chunkDuration += entry.sampleCompositionTimeOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private int parse_ftyp() throws IOException {
|
||||
int brand = stream.readInt();
|
||||
stream.skipBytes(4);// minor version
|
||||
|
||||
return brand;
|
||||
}
|
||||
|
||||
private Mvhd parse_mvhd() throws IOException {
|
||||
int version = stream.read();
|
||||
stream.skipBytes(3);// flags
|
||||
|
||||
// creation entries_time
|
||||
// modification entries_time
|
||||
stream.skipBytes(2 * (version == 0 ? 4 : 8));
|
||||
|
||||
Mvhd obj = new Mvhd();
|
||||
obj.timeScale = readUint();
|
||||
|
||||
// chunkDuration
|
||||
stream.skipBytes(version == 0 ? 4 : 8);
|
||||
|
||||
// rate
|
||||
// volume
|
||||
// reserved
|
||||
// matrix array
|
||||
// predefined
|
||||
stream.skipBytes(76);
|
||||
|
||||
obj.nextTrackId = readUint();
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private Tkhd parse_tkhd() throws IOException {
|
||||
int version = stream.read();
|
||||
|
||||
Tkhd obj = new Tkhd();
|
||||
|
||||
// flags
|
||||
// creation entries_time
|
||||
// modification entries_time
|
||||
stream.skipBytes(3 + (2 * (version == 0 ? 4 : 8)));
|
||||
|
||||
obj.trackId = stream.readInt();
|
||||
|
||||
stream.skipBytes(4);// reserved
|
||||
|
||||
obj.duration = version == 0 ? readUint() : stream.readLong();
|
||||
|
||||
stream.skipBytes(2 * 4);// reserved
|
||||
|
||||
obj.bLayer = stream.readShort();
|
||||
obj.bAlternateGroup = stream.readShort();
|
||||
obj.bVolume = stream.readShort();
|
||||
|
||||
stream.skipBytes(2);// reserved
|
||||
|
||||
obj.matrix = new byte[9 * 4];
|
||||
stream.read(obj.matrix);
|
||||
|
||||
obj.bWidth = stream.readInt();
|
||||
obj.bHeight = stream.readInt();
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private Trak parse_trak(Box ref) throws IOException {
|
||||
Trak trak = new Trak();
|
||||
|
||||
Box b = readBox(ATOM_TKHD);
|
||||
trak.tkhd = parse_tkhd();
|
||||
ensure(b);
|
||||
|
||||
b = untilBox(ref, ATOM_MDIA);
|
||||
trak.mdia = new byte[b.size];
|
||||
|
||||
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);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
data.position(end);
|
||||
}
|
||||
|
||||
return 0;// this NEVER should happen
|
||||
}
|
||||
|
||||
private Moov parse_moov(Box ref) throws IOException {
|
||||
Box b = readBox(ATOM_MVHD);
|
||||
Moov moov = new Moov();
|
||||
moov.mvhd = parse_mvhd();
|
||||
ensure(b);
|
||||
|
||||
ArrayList<Trak> tmp = new ArrayList<>((int) moov.mvhd.nextTrackId);
|
||||
while ((b = untilBox(ref, ATOM_TRAK, ATOM_MVEX)) != null) {
|
||||
|
||||
switch (b.type) {
|
||||
case ATOM_TRAK:
|
||||
tmp.add(parse_trak(b));
|
||||
break;
|
||||
case ATOM_MVEX:
|
||||
moov.mvex_trex = parse_mvex(b, (int) moov.mvhd.nextTrackId);
|
||||
break;
|
||||
}
|
||||
|
||||
ensure(b);
|
||||
}
|
||||
|
||||
moov.trak = tmp.toArray(new Trak[tmp.size()]);
|
||||
|
||||
return moov;
|
||||
}
|
||||
|
||||
private Trex[] parse_mvex(Box ref, int possibleTrackCount) throws IOException {
|
||||
ArrayList<Trex> tmp = new ArrayList<>(possibleTrackCount);
|
||||
|
||||
Box b;
|
||||
while ((b = untilBox(ref, ATOM_TREX)) != null) {
|
||||
tmp.add(parse_trex());
|
||||
ensure(b);
|
||||
}
|
||||
|
||||
return tmp.toArray(new Trex[tmp.size()]);
|
||||
}
|
||||
|
||||
private Trex parse_trex() throws IOException {
|
||||
// version
|
||||
// flags
|
||||
stream.skipBytes(4);
|
||||
|
||||
Trex obj = new Trex();
|
||||
obj.trackId = stream.readInt();
|
||||
obj.defaultSampleDescriptionIndex = stream.readInt();
|
||||
obj.defaultSampleDuration = stream.readInt();
|
||||
obj.defaultSampleSize = stream.readInt();
|
||||
obj.defaultSampleFlags = stream.readInt();
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
return tfra;
|
||||
}
|
||||
|
||||
private Sidx parse_sidx() throws IOException {
|
||||
int version = stream.read();
|
||||
|
||||
stream.skipBytes(3);// flags
|
||||
|
||||
Sidx obj = new Sidx();
|
||||
obj.referenceId = stream.readInt();
|
||||
obj.timescale = stream.readInt();
|
||||
|
||||
// earliest presentation entries_time
|
||||
// first offset
|
||||
// reserved
|
||||
stream.skipBytes((2 * (version == 0 ? 4 : 8)) + 2);
|
||||
|
||||
obj.entries_subsegmentDuration = new int[stream.readShort()];
|
||||
|
||||
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
|
||||
|
||||
// starts with SAP
|
||||
// SAP type
|
||||
// SAP delta entries_time
|
||||
stream.skipBytes(4);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private Tfra[] parse_mfra(Box ref, int trackCount) throws IOException {
|
||||
ArrayList<Tfra> tmp = new ArrayList<>(trackCount);
|
||||
long limit = ref.offset + ref.size;
|
||||
|
||||
while (stream.position() < limit) {
|
||||
box = readBox();
|
||||
|
||||
if (box.type == ATOM_TFRA) {
|
||||
tmp.add(parse_tfra());
|
||||
}
|
||||
|
||||
ensure(box);
|
||||
}
|
||||
|
||||
return tmp.toArray(new Tfra[tmp.size()]);
|
||||
}
|
||||
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultState="collapsed" desc="Helper classes">
|
||||
class Box {
|
||||
|
||||
int type;
|
||||
long offset;
|
||||
int size;
|
||||
}
|
||||
|
||||
class Sidx {
|
||||
|
||||
int timescale;
|
||||
int referenceId;
|
||||
int[] entries_subsegmentDuration;
|
||||
}
|
||||
|
||||
public class Moof {
|
||||
|
||||
int mfhd_SequenceNumber;
|
||||
public Traf traf;
|
||||
}
|
||||
|
||||
public class Traf {
|
||||
|
||||
public Tfhd tfhd;
|
||||
long tfdt;
|
||||
public Trun trun;
|
||||
}
|
||||
|
||||
public class Tfhd {
|
||||
|
||||
int bFlags;
|
||||
public int trackId;
|
||||
int defaultSampleDuration;
|
||||
int defaultSampleSize;
|
||||
int defaultSampleFlags;
|
||||
}
|
||||
|
||||
public class TrunEntry {
|
||||
|
||||
public int sampleDuration;
|
||||
public int sampleSize;
|
||||
public int sampleFlags;
|
||||
public int sampleCompositionTimeOffset;
|
||||
}
|
||||
|
||||
public class Trun {
|
||||
|
||||
public int chunkDuration;
|
||||
public int chunkSize;
|
||||
|
||||
public int bFlags;
|
||||
int bFirstSampleFlags;
|
||||
int dataOffset;
|
||||
|
||||
public int entryCount;
|
||||
byte[] bEntries;
|
||||
int entries_rowSize;
|
||||
|
||||
public TrunEntry getEntry(int i) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(bEntries, i * entries_rowSize, entries_rowSize);
|
||||
TrunEntry entry = new TrunEntry();
|
||||
|
||||
if (hasFlag(bFlags, 0x0100)) {
|
||||
entry.sampleDuration = buffer.getInt();
|
||||
}
|
||||
if (hasFlag(bFlags, 0x0200)) {
|
||||
entry.sampleSize = buffer.getInt();
|
||||
}
|
||||
if (hasFlag(bFlags, 0x0400)) {
|
||||
entry.sampleFlags = buffer.getInt();
|
||||
}
|
||||
if (hasFlag(bFlags, 0x0800)) {
|
||||
entry.sampleCompositionTimeOffset = buffer.getInt();
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
public class Tkhd {
|
||||
|
||||
int trackId;
|
||||
long duration;
|
||||
short bVolume;
|
||||
int bWidth;
|
||||
int bHeight;
|
||||
byte[] matrix;
|
||||
short bLayer;
|
||||
short bAlternateGroup;
|
||||
}
|
||||
|
||||
public class Trak {
|
||||
|
||||
public Tkhd tkhd;
|
||||
public int mdia_mdhd_timeScale;
|
||||
|
||||
byte[] mdia;
|
||||
}
|
||||
|
||||
class Mvhd {
|
||||
|
||||
long timeScale;
|
||||
long nextTrackId;
|
||||
}
|
||||
|
||||
class Moov {
|
||||
|
||||
Mvhd mvhd;
|
||||
Trak[] trak;
|
||||
Trex[] mvex_trex;
|
||||
}
|
||||
|
||||
class Tfra {
|
||||
|
||||
int trackId;
|
||||
int[] entries_time;
|
||||
}
|
||||
|
||||
public class Trex {
|
||||
|
||||
private int trackId;
|
||||
int defaultSampleDescriptionIndex;
|
||||
int defaultSampleDuration;
|
||||
int defaultSampleSize;
|
||||
int defaultSampleFlags;
|
||||
}
|
||||
|
||||
public class Mp4Track {
|
||||
|
||||
public TrackKind kind;
|
||||
public Trak trak;
|
||||
public Trex trex;
|
||||
}
|
||||
|
||||
public class Mp4TrackChunk {
|
||||
|
||||
public InputStream data;
|
||||
public Moof moof;
|
||||
}
|
||||
//</editor-fold>
|
||||
}
|
||||
623
app/src/main/java/org/schabi/newpipe/streams/Mp4DashWriter.java
Normal file
623
app/src/main/java/org/schabi/newpipe/streams/Mp4DashWriter.java
Normal file
|
|
@ -0,0 +1,623 @@
|
|||
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,370 @@
|
|||
package org.schabi.newpipe.streams;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
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;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class SubtitleConverter {
|
||||
private static final String NEW_LINE = "\r\n";
|
||||
|
||||
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()) {
|
||||
return;
|
||||
}
|
||||
out.write(String.valueOf(frameIndex++).getBytes(charset));
|
||||
out.write(NEW_LINE.getBytes(charset));
|
||||
out.write(getTime(frame.start, true).getBytes(charset));
|
||||
out.write(" --> ".getBytes(charset));
|
||||
out.write(getTime(frame.end, true).getBytes(charset));
|
||||
out.write(NEW_LINE.getBytes(charset));
|
||||
out.write(frame.text.getBytes(charset));
|
||||
out.write(NEW_LINE.getBytes(charset));
|
||||
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,
|
||||
String root, String formatAttr, String formatVersion, String[] cuePath, String[] framePath,
|
||||
String timeAttr, String durationAttr, boolean hasTimestamp
|
||||
) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException {
|
||||
/*
|
||||
* XML based subtitles parser with BASIC support
|
||||
* multiple CUE is not supported
|
||||
* styling is not supported
|
||||
* tag timestamps (in auto-generated subtitles) are not supported, maybe in the future
|
||||
* also TimestampTagOption enum is not applicable
|
||||
* Language parsing is not supported
|
||||
*/
|
||||
|
||||
byte[] buffer = new byte[source.available()];
|
||||
source.read(buffer);
|
||||
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
factory.setNamespaceAware(true);
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
Document xml = builder.parse(new ByteArrayInputStream(buffer));
|
||||
|
||||
String attr;
|
||||
|
||||
// get the format version or namespace
|
||||
Element node = xml.getDocumentElement();
|
||||
|
||||
if (node == null) {
|
||||
throw new ParseException("Can't get the format version. ¿wrong namespace?", -1);
|
||||
} else if (!node.getNodeName().equals(root)) {
|
||||
throw new ParseException("Invalid root", -1);
|
||||
}
|
||||
|
||||
if (formatAttr.equals("xmlns")) {
|
||||
if (!node.getNamespaceURI().equals(formatVersion)) {
|
||||
throw new UnsupportedOperationException("Expected xml namespace: " + formatVersion);
|
||||
}
|
||||
} else {
|
||||
attr = node.getAttributeNS(formatVersion, formatAttr);
|
||||
if (attr == null) {
|
||||
throw new ParseException("Can't get the format attribute", -1);
|
||||
}
|
||||
if (!attr.equals(formatVersion)) {
|
||||
throw new ParseException("Invalid format version : " + attr, -1);
|
||||
}
|
||||
}
|
||||
|
||||
NodeList node_list;
|
||||
|
||||
int line_break = 0;// Maximum characters per line if present (valid for TranScript v3)
|
||||
|
||||
if (!hasTimestamp) {
|
||||
node_list = selectNodes(xml, cuePath, formatVersion);
|
||||
|
||||
if (node_list != null) {
|
||||
// if the subtitle has multiple CUEs, use the highest value
|
||||
for (int i = 0; i < node_list.getLength(); i++) {
|
||||
try {
|
||||
int tmp = Integer.parseInt(((Element) node_list.item(i)).getAttributeNS(formatVersion, "ah"));
|
||||
if (tmp > line_break) {
|
||||
line_break = tmp;
|
||||
}
|
||||
} catch (Exception err) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse every frame
|
||||
node_list = selectNodes(xml, framePath, formatVersion);
|
||||
|
||||
if (node_list == null) {
|
||||
return;// no frames detected
|
||||
}
|
||||
|
||||
int fs_ff = -1;// first timestamp of first frame
|
||||
boolean limit_lines = false;
|
||||
|
||||
for (int i = 0; i < node_list.getLength(); i++) {
|
||||
Element elem = (Element) node_list.item(i);
|
||||
SubtitleFrame obj = new SubtitleFrame();
|
||||
obj.text = elem.getTextContent();
|
||||
|
||||
attr = elem.getAttribute(timeAttr);// ¡this cant be null!
|
||||
obj.start = hasTimestamp ? parseTimestamp(attr) : Integer.parseInt(attr);
|
||||
|
||||
attr = elem.getAttribute(durationAttr);
|
||||
if (obj.text == null || attr == null) {
|
||||
continue;// normally is a blank line (on auto-generated subtitles) ignore
|
||||
}
|
||||
|
||||
if (hasTimestamp) {
|
||||
obj.end = parseTimestamp(attr);
|
||||
|
||||
if (detectYoutubeDuplicateLines) {
|
||||
if (limit_lines) {
|
||||
int swap = obj.end;
|
||||
obj.end = fs_ff;
|
||||
fs_ff = swap;
|
||||
} else {
|
||||
if (fs_ff < 0) {
|
||||
fs_ff = obj.end;
|
||||
} else {
|
||||
if (fs_ff < obj.start) {
|
||||
limit_lines = true;// the subtitles has duplicated lines
|
||||
} else {
|
||||
detectYoutubeDuplicateLines = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
obj.end = obj.start + Integer.parseInt(attr);
|
||||
}
|
||||
|
||||
if (/*node.getAttribute("w").equals("1") &&*/line_break > 1 && obj.text.length() > line_break) {
|
||||
|
||||
// implement auto line breaking (once)
|
||||
StringBuilder text = new StringBuilder(obj.text);
|
||||
obj.text = null;
|
||||
|
||||
switch (text.charAt(line_break)) {
|
||||
case ' ':
|
||||
case '\t':
|
||||
putBreakAt(line_break, text);
|
||||
break;
|
||||
default:// find the word start position
|
||||
for (int j = line_break - 1; j > 0; j--) {
|
||||
switch (text.charAt(j)) {
|
||||
case ' ':
|
||||
case '\t':
|
||||
putBreakAt(j, text);
|
||||
j = -1;
|
||||
break;
|
||||
case '\r':
|
||||
case '\n':
|
||||
j = -1;// long word, just ignore
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
obj.text = text.toString();// set the processed text
|
||||
}
|
||||
|
||||
callback.yield(obj);
|
||||
}
|
||||
}
|
||||
|
||||
private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) throws XPathExpressionException {
|
||||
Element ref = xml.getDocumentElement();
|
||||
|
||||
for (int i = 0; i < path.length - 1; i++) {
|
||||
NodeList nodes = ref.getChildNodes();
|
||||
if (nodes.getLength() < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Element elem;
|
||||
for (int j = 0; j < nodes.getLength(); j++) {
|
||||
if (nodes.item(j).getNodeType() == Node.ELEMENT_NODE) {
|
||||
elem = (Element) nodes.item(j);
|
||||
if (elem.getNodeName().equals(path[i]) && elem.getNamespaceURI().equals(namespaceUri)) {
|
||||
ref = elem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ref.getElementsByTagNameNS(namespaceUri, path[path.length - 1]);
|
||||
}
|
||||
|
||||
private static int parseTimestamp(String multiImpl) throws NumberFormatException, ParseException {
|
||||
if (multiImpl.length() < 1) {
|
||||
return 0;
|
||||
} else if (multiImpl.length() == 1) {
|
||||
return Integer.parseInt(multiImpl) * 1000;// ¡this must be a number in seconds!
|
||||
}
|
||||
|
||||
// detect wallclock-time
|
||||
if (multiImpl.startsWith("wallclock(")) {
|
||||
throw new UnsupportedOperationException("Parsing wallclock timestamp is not implemented");
|
||||
}
|
||||
|
||||
// detect offset-time
|
||||
if (multiImpl.indexOf(':') < 0) {
|
||||
int multiplier = 1000;
|
||||
char metric = multiImpl.charAt(multiImpl.length() - 1);
|
||||
switch (metric) {
|
||||
case 'h':
|
||||
multiplier *= 3600000;
|
||||
break;
|
||||
case 'm':
|
||||
multiplier *= 60000;
|
||||
break;
|
||||
case 's':
|
||||
if (multiImpl.charAt(multiImpl.length() - 2) == 'm') {
|
||||
multiplier = 1;// ms
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!Character.isDigit(metric)) {
|
||||
throw new NumberFormatException("Invalid metric suffix found on : " + multiImpl);
|
||||
}
|
||||
metric = '\0';
|
||||
break;
|
||||
}
|
||||
try {
|
||||
String offset_time = multiImpl;
|
||||
|
||||
if (multiplier == 1) {
|
||||
offset_time = offset_time.substring(0, offset_time.length() - 2);
|
||||
} else if (metric != '\0') {
|
||||
offset_time = offset_time.substring(0, offset_time.length() - 1);
|
||||
}
|
||||
|
||||
double time_metric_based = Double.parseDouble(offset_time);
|
||||
if (Math.abs(time_metric_based) <= Double.MAX_VALUE) {
|
||||
return (int) (time_metric_based * multiplier);
|
||||
}
|
||||
} catch (Exception err) {
|
||||
throw new UnsupportedOperationException("Invalid or not implemented timestamp on: " + multiImpl);
|
||||
}
|
||||
}
|
||||
|
||||
// detect clock-time
|
||||
int time = 0;
|
||||
String[] units = multiImpl.split(":");
|
||||
|
||||
if (units.length < 3) {
|
||||
throw new ParseException("Invalid clock-time timestamp", -1);
|
||||
}
|
||||
|
||||
time += Integer.parseInt(units[0]) * 3600000;// hours
|
||||
time += Integer.parseInt(units[1]) * 60000;//minutes
|
||||
time += Float.parseFloat(units[2]) * 1000f;// seconds and milliseconds (if present)
|
||||
|
||||
// frames and sub-frames are ignored (not implemented)
|
||||
// time += units[3] * fps;
|
||||
return time;
|
||||
}
|
||||
|
||||
private static void putBreakAt(int idx, StringBuilder str) {
|
||||
// this should be optimized at compile time
|
||||
|
||||
if (NEW_LINE.length() > 1) {
|
||||
str.delete(idx, idx + 1);// remove after replace
|
||||
str.insert(idx, NEW_LINE);
|
||||
} else {
|
||||
str.setCharAt(idx, NEW_LINE.charAt(0));
|
||||
}
|
||||
}
|
||||
|
||||
private static String getTime(int time, boolean comma) {
|
||||
// cast every value to integer to avoid auto-round in ToString("00").
|
||||
StringBuilder str = new StringBuilder(12);
|
||||
str.append(numberToString(time / 1000 / 3600, 2));// hours
|
||||
str.append(':');
|
||||
str.append(numberToString(time / 1000 / 60 % 60, 2));// minutes
|
||||
str.append(':');
|
||||
str.append(numberToString(time / 1000 % 60, 2));// seconds
|
||||
str.append(comma ? ',' : '.');
|
||||
str.append(numberToString(time % 1000, 3));// miliseconds
|
||||
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
private static String numberToString(int nro, int pad) {
|
||||
return String.format(Locale.ENGLISH, "%0".concat(String.valueOf(pad)).concat("d"), nro);
|
||||
}
|
||||
|
||||
|
||||
/******************
|
||||
* helper classes *
|
||||
******************/
|
||||
|
||||
private interface FrameWriter {
|
||||
|
||||
void yield(SubtitleFrame frame) throws IOException;
|
||||
}
|
||||
|
||||
private static class SubtitleFrame {
|
||||
//Java no support unsigned int
|
||||
|
||||
public int end;
|
||||
public int start;
|
||||
public String text = "";
|
||||
|
||||
private boolean isEmptyText() {
|
||||
if (text == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
switch (text.charAt(i)) {
|
||||
case ' ':
|
||||
case '\t':
|
||||
case '\r':
|
||||
case '\n':
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
507
app/src/main/java/org/schabi/newpipe/streams/WebMReader.java
Normal file
507
app/src/main/java/org/schabi/newpipe/streams/WebMReader.java
Normal file
|
|
@ -0,0 +1,507 @@
|
|||
package org.schabi.newpipe.streams;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class WebMReader {
|
||||
|
||||
//<editor-fold defaultState="collapsed" desc="constants">
|
||||
private final static int ID_EMBL = 0x0A45DFA3;
|
||||
private final static int ID_EMBLReadVersion = 0x02F7;
|
||||
private final static int ID_EMBLDocType = 0x0282;
|
||||
private final static int ID_EMBLDocTypeReadVersion = 0x0285;
|
||||
|
||||
private final static int ID_Segment = 0x08538067;
|
||||
|
||||
private final static int ID_Info = 0x0549A966;
|
||||
private final static int ID_TimecodeScale = 0x0AD7B1;
|
||||
private final static int ID_Duration = 0x489;
|
||||
|
||||
private final static int ID_Tracks = 0x0654AE6B;
|
||||
private final static int ID_TrackEntry = 0x2E;
|
||||
private final static int ID_TrackNumber = 0x57;
|
||||
private final static int ID_TrackType = 0x03;
|
||||
private final static int ID_CodecID = 0x06;
|
||||
private final static int ID_CodecPrivate = 0x23A2;
|
||||
private final static int ID_Video = 0x60;
|
||||
private final static int ID_Audio = 0x61;
|
||||
private final static int ID_DefaultDuration = 0x3E383;
|
||||
private final static int ID_FlagLacing = 0x1C;
|
||||
|
||||
private final static int ID_Cluster = 0x0F43B675;
|
||||
private final static int ID_Timecode = 0x67;
|
||||
private final static int ID_SimpleBlock = 0x23;
|
||||
//</editor-fold>
|
||||
|
||||
public enum TrackKind {
|
||||
Audio/*2*/, Video/*1*/, Other
|
||||
}
|
||||
|
||||
private DataReader stream;
|
||||
private Segment segment;
|
||||
private WebMTrack[] tracks;
|
||||
private int selectedTrack;
|
||||
private boolean done;
|
||||
private boolean firstSegment;
|
||||
|
||||
public WebMReader(SharpStream source) {
|
||||
this.stream = new DataReader(source);
|
||||
}
|
||||
|
||||
public void parse() throws IOException {
|
||||
Element elem = readElement(ID_EMBL);
|
||||
if (!readEbml(elem, 1, 2)) {
|
||||
throw new UnsupportedOperationException("Unsupported EBML data (WebM)");
|
||||
}
|
||||
ensure(elem);
|
||||
|
||||
elem = untilElement(null, ID_Segment);
|
||||
if (elem == null) {
|
||||
throw new IOException("Fragment element not found");
|
||||
}
|
||||
segment = readSegment(elem, 0, true);
|
||||
tracks = segment.tracks;
|
||||
selectedTrack = -1;
|
||||
done = false;
|
||||
firstSegment = true;
|
||||
}
|
||||
|
||||
public WebMTrack[] getAvailableTracks() {
|
||||
return tracks;
|
||||
}
|
||||
|
||||
public WebMTrack selectTrack(int index) {
|
||||
selectedTrack = index;
|
||||
return tracks[index];
|
||||
}
|
||||
|
||||
public Segment getNextSegment() throws IOException {
|
||||
if (done) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (firstSegment && segment != null) {
|
||||
firstSegment = false;
|
||||
return segment;
|
||||
}
|
||||
|
||||
ensure(segment.ref);
|
||||
|
||||
Element elem = untilElement(null, ID_Segment);
|
||||
if (elem == null) {
|
||||
done = true;
|
||||
return null;
|
||||
}
|
||||
segment = readSegment(elem, 0, false);
|
||||
|
||||
return segment;
|
||||
}
|
||||
|
||||
//<editor-fold defaultstate="collapsed" desc="utils">
|
||||
private long readNumber(Element parent) throws IOException {
|
||||
int length = (int) parent.contentSize;
|
||||
long value = 0;
|
||||
while (length-- > 0) {
|
||||
int read = stream.read();
|
||||
if (read == -1) {
|
||||
throw new EOFException();
|
||||
}
|
||||
value = (value << 8) | read;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String readString(Element parent) throws IOException {
|
||||
return new String(readBlob(parent), "utf-8");
|
||||
}
|
||||
|
||||
private byte[] readBlob(Element parent) throws IOException {
|
||||
long length = parent.contentSize;
|
||||
byte[] buffer = new byte[(int) length];
|
||||
int read = stream.read(buffer);
|
||||
if (read < length) {
|
||||
throw new EOFException();
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private long readEncodedNumber() throws IOException {
|
||||
int value = stream.read();
|
||||
|
||||
if (value > 0) {
|
||||
byte size = 1;
|
||||
int mask = 0x80;
|
||||
|
||||
while (size < 9) {
|
||||
if ((value & mask) == mask) {
|
||||
mask = 0xFF;
|
||||
mask >>= size;
|
||||
|
||||
long number = value & mask;
|
||||
|
||||
for (int i = 1; i < size; i++) {
|
||||
value = stream.read();
|
||||
number <<= 8;
|
||||
number |= value;
|
||||
}
|
||||
|
||||
return number;
|
||||
}
|
||||
|
||||
mask >>= 1;
|
||||
size++;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IOException("Invalid encoded length");
|
||||
}
|
||||
|
||||
private Element readElement() throws IOException {
|
||||
Element elem = new Element();
|
||||
elem.offset = stream.position();
|
||||
elem.type = (int) readEncodedNumber();
|
||||
elem.contentSize = readEncodedNumber();
|
||||
elem.size = elem.contentSize + stream.position() - elem.offset;
|
||||
|
||||
return elem;
|
||||
}
|
||||
|
||||
private Element readElement(int expected) throws IOException {
|
||||
Element elem = readElement();
|
||||
if (expected != 0 && elem.type != expected) {
|
||||
throw new NoSuchElementException("expected " + elementID(expected) + " found " + elementID(elem.type));
|
||||
}
|
||||
|
||||
return elem;
|
||||
}
|
||||
|
||||
private Element untilElement(Element ref, int... expected) throws IOException {
|
||||
Element elem;
|
||||
while (ref == null ? stream.available() : (stream.position() < (ref.offset + ref.size))) {
|
||||
elem = readElement();
|
||||
for (int type : expected) {
|
||||
if (elem.type == type) {
|
||||
return elem;
|
||||
}
|
||||
}
|
||||
ensure(elem);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String elementID(long type) {
|
||||
return "0x".concat(Long.toHexString(type));
|
||||
}
|
||||
|
||||
private void ensure(Element ref) throws IOException {
|
||||
long skip = (ref.offset + ref.size) - stream.position();
|
||||
|
||||
if (skip == 0) {
|
||||
return;
|
||||
} else if (skip < 0) {
|
||||
throw new EOFException(String.format(
|
||||
"parser go beyond limits of the Element. type=%s offset=%s size=%s position=%s",
|
||||
elementID(ref.type), ref.offset, ref.size, stream.position()
|
||||
));
|
||||
}
|
||||
|
||||
stream.skipBytes(skip);
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
//<editor-fold defaultState="collapsed" desc="elements readers">
|
||||
private boolean readEbml(Element ref, int minReadVersion, int minDocTypeVersion) throws IOException {
|
||||
Element elem = untilElement(ref, ID_EMBLReadVersion);
|
||||
if (elem == null) {
|
||||
return false;
|
||||
}
|
||||
if (readNumber(elem) > minReadVersion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
elem = untilElement(ref, ID_EMBLDocType);
|
||||
if (elem == null) {
|
||||
return false;
|
||||
}
|
||||
if (!readString(elem).equals("webm")) {
|
||||
return false;
|
||||
}
|
||||
elem = untilElement(ref, ID_EMBLDocTypeReadVersion);
|
||||
|
||||
return elem != null && readNumber(elem) <= minDocTypeVersion;
|
||||
}
|
||||
|
||||
private Info readInfo(Element ref) throws IOException {
|
||||
Element elem;
|
||||
Info info = new Info();
|
||||
|
||||
while ((elem = untilElement(ref, ID_TimecodeScale, ID_Duration)) != null) {
|
||||
switch (elem.type) {
|
||||
case ID_TimecodeScale:
|
||||
info.timecodeScale = readNumber(elem);
|
||||
break;
|
||||
case ID_Duration:
|
||||
info.duration = readNumber(elem);
|
||||
break;
|
||||
}
|
||||
ensure(elem);
|
||||
}
|
||||
|
||||
if (info.timecodeScale == 0) {
|
||||
throw new NoSuchElementException("Element Timecode not found");
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private Segment readSegment(Element ref, int trackLacingExpected, boolean metadataExpected) throws IOException {
|
||||
Segment obj = new Segment(ref);
|
||||
Element elem;
|
||||
while ((elem = untilElement(ref, ID_Info, ID_Tracks, ID_Cluster)) != null) {
|
||||
if (elem.type == ID_Cluster) {
|
||||
obj.currentCluster = elem;
|
||||
break;
|
||||
}
|
||||
switch (elem.type) {
|
||||
case ID_Info:
|
||||
obj.info = readInfo(elem);
|
||||
break;
|
||||
case ID_Tracks:
|
||||
obj.tracks = readTracks(elem, trackLacingExpected);
|
||||
break;
|
||||
}
|
||||
ensure(elem);
|
||||
}
|
||||
|
||||
if (metadataExpected && (obj.info == null || obj.tracks == null)) {
|
||||
throw new RuntimeException("Cluster element found without Info and/or Tracks element at position " + String.valueOf(ref.offset));
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private WebMTrack[] readTracks(Element ref, int lacingExpected) throws IOException {
|
||||
ArrayList<WebMTrack> trackEntries = new ArrayList<>(2);
|
||||
Element elem_trackEntry;
|
||||
|
||||
while ((elem_trackEntry = untilElement(ref, ID_TrackEntry)) != null) {
|
||||
WebMTrack entry = new WebMTrack();
|
||||
boolean drop = false;
|
||||
Element elem;
|
||||
while ((elem = untilElement(elem_trackEntry,
|
||||
ID_TrackNumber, ID_TrackType, ID_CodecID, ID_CodecPrivate, ID_FlagLacing, ID_DefaultDuration, ID_Audio, ID_Video
|
||||
)) != null) {
|
||||
switch (elem.type) {
|
||||
case ID_TrackNumber:
|
||||
entry.trackNumber = readNumber(elem);
|
||||
break;
|
||||
case ID_TrackType:
|
||||
entry.trackType = (int)readNumber(elem);
|
||||
break;
|
||||
case ID_CodecID:
|
||||
entry.codecId = readString(elem);
|
||||
break;
|
||||
case ID_CodecPrivate:
|
||||
entry.codecPrivate = readBlob(elem);
|
||||
break;
|
||||
case ID_Audio:
|
||||
case ID_Video:
|
||||
entry.bMetadata = readBlob(elem);
|
||||
break;
|
||||
case ID_DefaultDuration:
|
||||
entry.defaultDuration = readNumber(elem);
|
||||
break;
|
||||
case ID_FlagLacing:
|
||||
drop = readNumber(elem) != lacingExpected;
|
||||
break;
|
||||
default:
|
||||
System.out.println();
|
||||
break;
|
||||
}
|
||||
ensure(elem);
|
||||
}
|
||||
if (!drop) {
|
||||
trackEntries.add(entry);
|
||||
}
|
||||
ensure(elem_trackEntry);
|
||||
}
|
||||
|
||||
WebMTrack[] entries = new WebMTrack[trackEntries.size()];
|
||||
trackEntries.toArray(entries);
|
||||
|
||||
for (WebMTrack entry : entries) {
|
||||
switch (entry.trackType) {
|
||||
case 1:
|
||||
entry.kind = TrackKind.Video;
|
||||
break;
|
||||
case 2:
|
||||
entry.kind = TrackKind.Audio;
|
||||
break;
|
||||
default:
|
||||
entry.kind = TrackKind.Other;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private SimpleBlock readSimpleBlock(Element ref) throws IOException {
|
||||
SimpleBlock obj = new SimpleBlock(ref);
|
||||
obj.dataSize = stream.position();
|
||||
obj.trackNumber = readEncodedNumber();
|
||||
obj.relativeTimeCode = stream.readShort();
|
||||
obj.flags = (byte) stream.read();
|
||||
obj.dataSize = (ref.offset + ref.size) - stream.position();
|
||||
|
||||
if (obj.dataSize < 0) {
|
||||
throw new IOException(String.format("Unexpected SimpleBlock element size, missing %s bytes", -obj.dataSize));
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
private Cluster readCluster(Element ref) throws IOException {
|
||||
Cluster obj = new Cluster(ref);
|
||||
|
||||
Element elem = untilElement(ref, ID_Timecode);
|
||||
if (elem == null) {
|
||||
throw new NoSuchElementException("Cluster at " + String.valueOf(ref.offset) + " without Timecode element");
|
||||
}
|
||||
obj.timecode = readNumber(elem);
|
||||
|
||||
return obj;
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
//<editor-fold defaultstate="collapsed" desc="class helpers">
|
||||
class Element {
|
||||
|
||||
int type;
|
||||
long offset;
|
||||
long contentSize;
|
||||
long size;
|
||||
}
|
||||
|
||||
public class Info {
|
||||
|
||||
public long timecodeScale;
|
||||
public long duration;
|
||||
}
|
||||
|
||||
public class WebMTrack {
|
||||
|
||||
public long trackNumber;
|
||||
protected int trackType;
|
||||
public String codecId;
|
||||
public byte[] codecPrivate;
|
||||
public byte[] bMetadata;
|
||||
public TrackKind kind;
|
||||
public long defaultDuration;
|
||||
}
|
||||
|
||||
public class Segment {
|
||||
|
||||
Segment(Element ref) {
|
||||
this.ref = ref;
|
||||
this.firstClusterInSegment = true;
|
||||
}
|
||||
|
||||
public Info info;
|
||||
WebMTrack[] tracks;
|
||||
private Element currentCluster;
|
||||
private final Element ref;
|
||||
boolean firstClusterInSegment;
|
||||
|
||||
public Cluster getNextCluster() throws IOException {
|
||||
if (done) {
|
||||
return null;
|
||||
}
|
||||
if (firstClusterInSegment && segment.currentCluster != null) {
|
||||
firstClusterInSegment = false;
|
||||
return readCluster(segment.currentCluster);
|
||||
}
|
||||
ensure(segment.currentCluster);
|
||||
|
||||
Element elem = untilElement(segment.ref, ID_Cluster);
|
||||
if (elem == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
segment.currentCluster = elem;
|
||||
|
||||
return readCluster(segment.currentCluster);
|
||||
}
|
||||
}
|
||||
|
||||
public class SimpleBlock {
|
||||
|
||||
public TrackDataChunk data;
|
||||
|
||||
SimpleBlock(Element ref) {
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
public long trackNumber;
|
||||
public short relativeTimeCode;
|
||||
public byte flags;
|
||||
public long dataSize;
|
||||
private final Element ref;
|
||||
|
||||
public boolean isKeyframe() {
|
||||
return (flags & 0x80) == 0x80;
|
||||
}
|
||||
}
|
||||
|
||||
public class Cluster {
|
||||
|
||||
Element ref;
|
||||
SimpleBlock currentSimpleBlock = null;
|
||||
public long timecode;
|
||||
|
||||
Cluster(Element ref) {
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
boolean check() {
|
||||
return stream.position() >= (ref.offset + ref.size);
|
||||
}
|
||||
|
||||
public SimpleBlock getNextSimpleBlock() throws IOException {
|
||||
if (check()) {
|
||||
return null;
|
||||
}
|
||||
if (currentSimpleBlock != null) {
|
||||
ensure(currentSimpleBlock.ref);
|
||||
}
|
||||
|
||||
while (!check()) {
|
||||
Element elem = untilElement(ref, ID_SimpleBlock);
|
||||
if (elem == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
currentSimpleBlock = readSimpleBlock(elem);
|
||||
if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) {
|
||||
currentSimpleBlock.data = new TrackDataChunk(stream, (int) currentSimpleBlock.dataSize);
|
||||
return currentSimpleBlock;
|
||||
}
|
||||
|
||||
ensure(elem);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
//</editor-fold>
|
||||
}
|
||||
728
app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java
Normal file
728
app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java
Normal file
|
|
@ -0,0 +1,728 @@
|
|||
package org.schabi.newpipe.streams;
|
||||
|
||||
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 java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class WebMWriter {
|
||||
|
||||
private final static int BUFFER_SIZE = 8 * 1024;
|
||||
private final static int DEFAULT_TIMECODE_SCALE = 1000000;
|
||||
private final static int INTERV = 100;// 100ms on 1000000us timecode scale
|
||||
private final static int DEFAULT_CUES_EACH_MS = 5000;// 100ms on 1000000us timecode scale
|
||||
|
||||
private WebMReader.WebMTrack[] infoTracks;
|
||||
private SharpStream[] sourceTracks;
|
||||
|
||||
private WebMReader[] readers;
|
||||
|
||||
private boolean done = false;
|
||||
private boolean parsed = false;
|
||||
|
||||
private long written = 0;
|
||||
|
||||
private Segment[] readersSegment;
|
||||
private Cluster[] readersCluter;
|
||||
|
||||
private int[] predefinedDurations;
|
||||
|
||||
private byte[] outBuffer;
|
||||
|
||||
public WebMWriter(SharpStream... source) {
|
||||
sourceTracks = source;
|
||||
readers = new WebMReader[sourceTracks.length];
|
||||
infoTracks = new WebMTrack[sourceTracks.length];
|
||||
outBuffer = new byte[BUFFER_SIZE];
|
||||
}
|
||||
|
||||
public WebMTrack[] getTracksFromSource(int sourceIndex) throws IllegalStateException {
|
||||
if (done) {
|
||||
throw new IllegalStateException("already done");
|
||||
}
|
||||
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 WebMReader(sourceTracks[i]);
|
||||
readers[i].parse();
|
||||
}
|
||||
|
||||
} finally {
|
||||
parsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void selectTracks(int... trackIndex) throws IOException {
|
||||
try {
|
||||
readersSegment = new Segment[readers.length];
|
||||
readersCluter = new Cluster[readers.length];
|
||||
predefinedDurations = new int[readers.length];
|
||||
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
infoTracks[i] = readers[i].selectTrack(trackIndex[i]);
|
||||
predefinedDurations[i] = -1;
|
||||
readersSegment[i] = readers[i].getNextSegment();
|
||||
}
|
||||
} finally {
|
||||
parsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public long getBytesWritten() {
|
||||
return written;
|
||||
}
|
||||
|
||||
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;
|
||||
readersSegment = null;
|
||||
readersCluter = null;
|
||||
outBuffer = null;
|
||||
}
|
||||
|
||||
public void build(SharpStream out) throws IOException, RuntimeException {
|
||||
if (!out.canRewind()) {
|
||||
throw new IOException("The output stream must be allow seek");
|
||||
}
|
||||
|
||||
makeEBML(out);
|
||||
|
||||
long offsetSegmentSizeSet = written + 5;
|
||||
long offsetInfoDurationSet = written + 94;
|
||||
long offsetClusterSet = written + 58;
|
||||
long offsetCuesSet = written + 75;
|
||||
|
||||
ArrayList<byte[]> listBuffer = new ArrayList<>(4);
|
||||
|
||||
/* segment */
|
||||
listBuffer.add(new byte[]{
|
||||
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
|
||||
});
|
||||
|
||||
/* info */
|
||||
listBuffer.add(new byte[]{
|
||||
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
|
||||
});
|
||||
|
||||
/* tracks */
|
||||
listBuffer.addAll(makeTracks());
|
||||
|
||||
for (byte[] buff : listBuffer) {
|
||||
dump(buff, out);
|
||||
}
|
||||
|
||||
// reserve space for Cues element, but is a waste of space (actually is 64 KiB)
|
||||
// TODO: better Cue maker
|
||||
long cueReservedOffset = written;
|
||||
dump(new byte[]{(byte) 0xec, 0x20, (byte) 0xff, (byte) 0xfb}, out);
|
||||
int reserved = (1024 * 63) - 4;
|
||||
while (reserved > 0) {
|
||||
int write = Math.min(reserved, outBuffer.length);
|
||||
out.write(outBuffer, 0, write);
|
||||
reserved -= write;
|
||||
written += write;
|
||||
}
|
||||
|
||||
// Select a track for the cue
|
||||
int cuesForTrackId = selectTrackForCue();
|
||||
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);
|
||||
|
||||
long duration = 0;
|
||||
int durationFromTrackId = 0;
|
||||
|
||||
byte[] bTimecode = makeTimecode(0);
|
||||
|
||||
int firstClusterOffset = (int) written;
|
||||
long currentClusterOffset = makeCluster(out, bTimecode, 0, clusterOffsets, clusterSizes);
|
||||
|
||||
long baseTimecode = 0;
|
||||
long limitTimecode = -1;
|
||||
int limitTimecodeByTrackId = cuesForTrackId;
|
||||
|
||||
int blockWritten = Integer.MAX_VALUE;
|
||||
|
||||
int newClusterByTrackId = -1;
|
||||
|
||||
while (blockWritten > 0) {
|
||||
blockWritten = 0;
|
||||
int i = 0;
|
||||
while (i < readers.length) {
|
||||
Block bloq = getNextBlockFrom(i);
|
||||
if (bloq == null) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bloq.data == null) {
|
||||
blockWritten = 1;// fake block
|
||||
newClusterByTrackId = i;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (newClusterByTrackId == i) {
|
||||
limitTimecodeByTrackId = i;
|
||||
newClusterByTrackId = -1;
|
||||
baseTimecode = bloq.absoluteTimecode;
|
||||
limitTimecode = baseTimecode + INTERV;
|
||||
bTimecode = makeTimecode(baseTimecode);
|
||||
currentClusterOffset = makeCluster(out, bTimecode, currentClusterOffset, clusterOffsets, clusterSizes);
|
||||
}
|
||||
|
||||
if (cuesForTrackId == i) {
|
||||
if ((nextCueTime > -1 && bloq.absoluteTimecode >= nextCueTime) || (nextCueTime < 0 && bloq.isKeyframe())) {
|
||||
if (nextCueTime > -1) {
|
||||
nextCueTime += DEFAULT_CUES_EACH_MS;
|
||||
}
|
||||
keyFrames.add(
|
||||
new KeyFrame(baseSegmentOffset, currentClusterOffset - 7, written, bTimecode.length, bloq.absoluteTimecode)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
writeBlock(out, bloq, baseTimecode);
|
||||
blockWritten++;
|
||||
|
||||
if (bloq.absoluteTimecode > duration) {
|
||||
duration = bloq.absoluteTimecode;
|
||||
durationFromTrackId = bloq.trackNumber;
|
||||
}
|
||||
|
||||
if (limitTimecode < 0) {
|
||||
limitTimecode = bloq.absoluteTimecode + INTERV;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bloq.absoluteTimecode >= limitTimecode) {
|
||||
if (limitTimecodeByTrackId != i) {
|
||||
limitTimecode += INTERV - (bloq.absoluteTimecode - limitTimecode);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
makeCluster(out, null, currentClusterOffset, null, clusterSizes);
|
||||
|
||||
long segmentSize = written - offsetSegmentSizeSet - 7;
|
||||
|
||||
// final step write offsets and sizes
|
||||
out.rewind();
|
||||
written = 0;
|
||||
|
||||
skipTo(out, offsetSegmentSizeSet);
|
||||
writeLong(out, segmentSize);
|
||||
|
||||
if (predefinedDurations[durationFromTrackId] > -1) {
|
||||
duration += predefinedDurations[durationFromTrackId];// this value is full-filled in makeTrackEntry() method
|
||||
}
|
||||
skipTo(out, offsetInfoDurationSet);
|
||||
writeFloat(out, duration);
|
||||
|
||||
firstClusterOffset -= baseSegmentOffset;
|
||||
skipTo(out, offsetClusterSet);
|
||||
writeInt(out, firstClusterOffset);
|
||||
|
||||
skipTo(out, cueReservedOffset);
|
||||
|
||||
/* Cue */
|
||||
dump(new byte[]{0x1c, 0x53, (byte) 0xbb, 0x6b, 0x20, 0x00, 0x00}, out);
|
||||
|
||||
for (KeyFrame keyFrame : keyFrames) {
|
||||
for (byte[] buffer : makeCuePoint(cuesForTrackId, keyFrame)) {
|
||||
dump(buffer, out);
|
||||
if (written >= (cueReservedOffset + 65535 - 16)) {
|
||||
throw new IOException("Too many Cues");
|
||||
}
|
||||
}
|
||||
}
|
||||
short cueSize = (short) (written - cueReservedOffset - 7);
|
||||
|
||||
/* EBML Void */
|
||||
ByteBuffer voidBuffer = ByteBuffer.allocate(4);
|
||||
voidBuffer.putShort((short) 0xec20);
|
||||
voidBuffer.putShort((short) (firstClusterOffset - written - 4));
|
||||
dump(voidBuffer.array(), out);
|
||||
|
||||
out.rewind();
|
||||
written = 0;
|
||||
|
||||
skipTo(out, offsetCuesSet);
|
||||
writeInt(out, (int) (cueReservedOffset - baseSegmentOffset));
|
||||
|
||||
skipTo(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;
|
||||
}
|
||||
}
|
||||
|
||||
private Block getNextBlockFrom(int internalTrackId) throws IOException {
|
||||
if (readersSegment[internalTrackId] == null) {
|
||||
readersSegment[internalTrackId] = readers[internalTrackId].getNextSegment();
|
||||
if (readersSegment[internalTrackId] == null) {
|
||||
return null;// no more blocks in the selected track
|
||||
}
|
||||
}
|
||||
|
||||
if (readersCluter[internalTrackId] == null) {
|
||||
readersCluter[internalTrackId] = readersSegment[internalTrackId].getNextCluster();
|
||||
if (readersCluter[internalTrackId] == null) {
|
||||
readersSegment[internalTrackId] = null;
|
||||
return getNextBlockFrom(internalTrackId);
|
||||
}
|
||||
}
|
||||
|
||||
SimpleBlock res = readersCluter[internalTrackId].getNextSimpleBlock();
|
||||
if (res == null) {
|
||||
readersCluter[internalTrackId] = null;
|
||||
return new Block();// fake block to indicate the end of the cluster
|
||||
}
|
||||
|
||||
Block bloq = new Block();
|
||||
bloq.data = res.data;
|
||||
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 += readersCluter[internalTrackId].timecode;
|
||||
|
||||
return bloq;
|
||||
}
|
||||
|
||||
private short convertTimecode(int time, long oldTimeScale, int newTimeScale) {
|
||||
return (short) (time * (newTimeScale / oldTimeScale));
|
||||
}
|
||||
|
||||
private void skipTo(SharpStream stream, long absoluteOffset) throws IOException {
|
||||
absoluteOffset -= written;
|
||||
written += absoluteOffset;
|
||||
stream.skip(absoluteOffset);
|
||||
}
|
||||
|
||||
private void writeLong(SharpStream stream, long number) throws IOException {
|
||||
byte[] buffer = ByteBuffer.allocate(DataReader.LONG_SIZE).putLong(number).array();
|
||||
stream.write(buffer, 1, buffer.length - 1);
|
||||
written += buffer.length - 1;
|
||||
}
|
||||
|
||||
private void writeFloat(SharpStream stream, float number) throws IOException {
|
||||
byte[] buffer = ByteBuffer.allocate(DataReader.FLOAT_SIZE).putFloat(number).array();
|
||||
dump(buffer, stream);
|
||||
}
|
||||
|
||||
private void writeShort(SharpStream stream, short number) throws IOException {
|
||||
byte[] buffer = ByteBuffer.allocate(DataReader.SHORT_SIZE).putShort(number).array();
|
||||
dump(buffer, stream);
|
||||
}
|
||||
|
||||
private void writeInt(SharpStream stream, int number) throws IOException {
|
||||
byte[] buffer = ByteBuffer.allocate(DataReader.INTEGER_SIZE).putInt(number).array();
|
||||
dump(buffer, stream);
|
||||
}
|
||||
|
||||
private void writeBlock(SharpStream stream, Block bloq, long clusterTimecode) throws IOException {
|
||||
long relativeTimeCode = bloq.absoluteTimecode - clusterTimecode;
|
||||
|
||||
if (relativeTimeCode < Short.MIN_VALUE || relativeTimeCode > Short.MAX_VALUE) {
|
||||
throw new IndexOutOfBoundsException("SimpleBlock timecode overflow.");
|
||||
}
|
||||
|
||||
ArrayList<byte[]> listBuffer = new ArrayList<>(5);
|
||||
listBuffer.add(new byte[]{(byte) 0xa3});
|
||||
listBuffer.add(null);// block size
|
||||
listBuffer.add(encode(bloq.trackNumber + 1, false));
|
||||
listBuffer.add(ByteBuffer.allocate(DataReader.SHORT_SIZE).putShort((short) relativeTimeCode).array());
|
||||
listBuffer.add(new byte[]{bloq.flags});
|
||||
|
||||
int blockSize = bloq.dataSize;
|
||||
for (int i = 2; i < listBuffer.size(); i++) {
|
||||
blockSize += listBuffer.get(i).length;
|
||||
}
|
||||
listBuffer.set(1, encode(blockSize, false));
|
||||
|
||||
for (byte[] buff : listBuffer) {
|
||||
dump(buff, stream);
|
||||
}
|
||||
|
||||
int read;
|
||||
while ((read = bloq.data.read(outBuffer)) > 0) {
|
||||
stream.write(outBuffer, 0, read);
|
||||
written += read;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] makeTimecode(long timecode) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(9);
|
||||
buffer.put((byte) 0xe7);
|
||||
buffer.put(encode(timecode, true));
|
||||
|
||||
byte[] res = new byte[buffer.position()];
|
||||
System.arraycopy(buffer.array(), 0, res, 0, res.length);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private long makeCluster(SharpStream stream, byte[] bTimecode, long startOffset, ArrayList<Long> clusterOffsets, ArrayList<Integer> clusterSizes) throws IOException {
|
||||
if (startOffset > 0) {
|
||||
clusterSizes.add((int) (written - startOffset));// size for last offset
|
||||
}
|
||||
|
||||
if (clusterOffsets != null) {
|
||||
/* 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);
|
||||
|
||||
startOffset = written;// size for the this cluster
|
||||
|
||||
dump(bTimecode, stream);
|
||||
|
||||
return startOffset;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
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
|
||||
}, stream);
|
||||
}
|
||||
|
||||
private ArrayList<byte[]> makeTracks() {
|
||||
ArrayList<byte[]> buffer = new ArrayList<>(1);
|
||||
buffer.add(new byte[]{0x16, 0x54, (byte) 0xae, 0x6b});
|
||||
buffer.add(null);
|
||||
|
||||
for (int i = 0; i < infoTracks.length; i++) {
|
||||
buffer.addAll(makeTrackEntry(i, infoTracks[i]));
|
||||
}
|
||||
|
||||
return lengthFor(buffer);
|
||||
}
|
||||
|
||||
private ArrayList<byte[]> makeTrackEntry(int internalTrackId, WebMTrack track) {
|
||||
byte[] id = encode(internalTrackId + 1, true);
|
||||
ArrayList<byte[]> buffer = new ArrayList<>(12);
|
||||
|
||||
/* track */
|
||||
buffer.add(new byte[]{(byte) 0xae});
|
||||
buffer.add(null);
|
||||
|
||||
/* track number */
|
||||
buffer.add(new byte[]{(byte) 0xd7});
|
||||
buffer.add(id);
|
||||
|
||||
/* track uid */
|
||||
buffer.add(new byte[]{0x73, (byte) 0xc5});
|
||||
buffer.add(id);
|
||||
|
||||
/* flag lacing */
|
||||
buffer.add(new byte[]{(byte) 0x9c, (byte) 0x81, 0x00});
|
||||
|
||||
/* lang */
|
||||
buffer.add(new byte[]{0x22, (byte) 0xb5, (byte) 0x9c, (byte) 0x83, 0x75, 0x6e, 0x64});
|
||||
|
||||
/* codec id */
|
||||
buffer.add(new byte[]{(byte) 0x86});
|
||||
buffer.addAll(encode(track.codecId));
|
||||
|
||||
/* type */
|
||||
buffer.add(new byte[]{(byte) 0x83});
|
||||
buffer.add(encode(track.trackType, true));
|
||||
|
||||
/* default duration */
|
||||
if (track.defaultDuration != 0) {
|
||||
predefinedDurations[internalTrackId] = (int) Math.ceil(track.defaultDuration / (float) DEFAULT_TIMECODE_SCALE);
|
||||
buffer.add(new byte[]{0x23, (byte) 0xe3, (byte) 0x83});
|
||||
buffer.add(encode(track.defaultDuration, true));
|
||||
}
|
||||
|
||||
/* audio/video */
|
||||
if ((track.trackType == 1 || track.trackType == 2) && valid(track.bMetadata)) {
|
||||
buffer.add(new byte[]{(byte) (track.trackType == 1 ? 0xe0 : 0xe1)});
|
||||
buffer.add(encode(track.bMetadata.length, false));
|
||||
buffer.add(track.bMetadata);
|
||||
}
|
||||
|
||||
/* codec private*/
|
||||
if (valid(track.codecPrivate)) {
|
||||
buffer.add(new byte[]{0x63, (byte) 0xa2});
|
||||
buffer.add(encode(track.codecPrivate.length, false));
|
||||
buffer.add(track.codecPrivate);
|
||||
}
|
||||
|
||||
return lengthFor(buffer);
|
||||
|
||||
}
|
||||
|
||||
private ArrayList<byte[]> makeCuePoint(int internalTrackId, KeyFrame keyFrame) {
|
||||
ArrayList<byte[]> buffer = new ArrayList<>(5);
|
||||
|
||||
/* CuePoint */
|
||||
buffer.add(new byte[]{(byte) 0xbb});
|
||||
buffer.add(null);
|
||||
|
||||
/* CueTime */
|
||||
buffer.add(new byte[]{(byte) 0xb3});
|
||||
buffer.add(encode(keyFrame.atTimecode, true));
|
||||
|
||||
/* CueTrackPosition */
|
||||
buffer.addAll(makeCueTrackPosition(internalTrackId, keyFrame));
|
||||
|
||||
return lengthFor(buffer);
|
||||
}
|
||||
|
||||
private ArrayList<byte[]> makeCueTrackPosition(int internalTrackId, KeyFrame keyFrame) {
|
||||
ArrayList<byte[]> buffer = new ArrayList<>(8);
|
||||
|
||||
/* CueTrackPositions */
|
||||
buffer.add(new byte[]{(byte) 0xb7});
|
||||
buffer.add(null);
|
||||
|
||||
/* CueTrack */
|
||||
buffer.add(new byte[]{(byte) 0xf7});
|
||||
buffer.add(encode(internalTrackId + 1, true));
|
||||
|
||||
/* CueClusterPosition */
|
||||
buffer.add(new byte[]{(byte) 0xf1});
|
||||
buffer.add(encode(keyFrame.atCluster, true));
|
||||
|
||||
/* CueRelativePosition */
|
||||
if (keyFrame.atBlock > 0) {
|
||||
buffer.add(new byte[]{(byte) 0xf0});
|
||||
buffer.add(encode(keyFrame.atBlock, true));
|
||||
}
|
||||
|
||||
return lengthFor(buffer);
|
||||
}
|
||||
|
||||
private void dump(byte[] buffer, SharpStream stream) throws IOException {
|
||||
stream.write(buffer);
|
||||
written += buffer.length;
|
||||
}
|
||||
|
||||
private ArrayList<byte[]> lengthFor(ArrayList<byte[]> buffer) {
|
||||
long size = 0;
|
||||
for (int i = 2; i < buffer.size(); i++) {
|
||||
size += buffer.get(i).length;
|
||||
}
|
||||
buffer.set(1, encode(size, false));
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private byte[] encode(long number, boolean withLength) {
|
||||
int length = -1;
|
||||
for (int i = 1; i <= 7; i++) {
|
||||
if (number < Math.pow(2, 7 * i)) {
|
||||
length = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (length < 1) {
|
||||
throw new ArithmeticException("Can't encode a number of bigger than 7 bytes");
|
||||
}
|
||||
|
||||
if (number == (Math.pow(2, 7 * length)) - 1) {
|
||||
length++;
|
||||
}
|
||||
|
||||
int offset = withLength ? 1 : 0;
|
||||
byte[] buffer = new byte[offset + length];
|
||||
long marker = (long) Math.floor((length - 1) / 8);
|
||||
|
||||
for (int i = length - 1, mul = 1; i >= 0; i--, mul *= 0x100) {
|
||||
long b = (long) Math.floor(number / mul);
|
||||
if (!withLength && i == marker) {
|
||||
b = b | (0x80 >> (length - 1));
|
||||
}
|
||||
buffer[offset + i] = (byte) b;
|
||||
}
|
||||
|
||||
if (withLength) {
|
||||
buffer[0] = (byte) (0x80 | length);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private ArrayList<byte[]> encode(String value) {
|
||||
byte[] str;
|
||||
try {
|
||||
str = value.getBytes("utf-8");
|
||||
} catch (UnsupportedEncodingException err) {
|
||||
str = value.getBytes();
|
||||
}
|
||||
|
||||
ArrayList<byte[]> buffer = new ArrayList<>(2);
|
||||
buffer.add(encode(str.length, false));
|
||||
buffer.add(str);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private boolean valid(byte[] buffer) {
|
||||
return buffer != null && buffer.length > 0;
|
||||
}
|
||||
|
||||
private int selectTrackForCue() {
|
||||
int i = 0;
|
||||
int videoTracks = 0;
|
||||
int audioTracks = 0;
|
||||
|
||||
for (; i < infoTracks.length; i++) {
|
||||
switch (infoTracks[i].trackType) {
|
||||
case 1:
|
||||
videoTracks++;
|
||||
break;
|
||||
case 2:
|
||||
audioTracks++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int kind;
|
||||
if (audioTracks == infoTracks.length) {
|
||||
kind = 2;
|
||||
} else if (videoTracks == infoTracks.length) {
|
||||
kind = 1;
|
||||
} else if (videoTracks > 0) {
|
||||
kind = 1;
|
||||
} else if (audioTracks > 0) {
|
||||
kind = 2;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// TODO: in the adove code, find and select the shortest track for the desired kind
|
||||
for (i = 0; i < infoTracks.length; i++) {
|
||||
if (kind == infoTracks[i].trackType) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
class KeyFrame {
|
||||
|
||||
KeyFrame(long segment, long cluster, long block, int bTimecodeLength, long timecode) {
|
||||
atCluster = cluster - segment;
|
||||
if ((block - bTimecodeLength) > cluster) {
|
||||
atBlock = (int) (block - cluster);
|
||||
}
|
||||
atTimecode = timecode;
|
||||
}
|
||||
|
||||
long atCluster;
|
||||
int atBlock;
|
||||
long atTimecode;
|
||||
}
|
||||
|
||||
class Block {
|
||||
|
||||
InputStream data;
|
||||
int trackNumber;
|
||||
byte flags;
|
||||
int dataSize;
|
||||
long absoluteTimecode;
|
||||
|
||||
boolean isKeyframe() {
|
||||
return (flags & 0x80) == 0x80;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, (flags & 0x80) == 0x80, absoluteTimecode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package org.schabi.newpipe.streams.io;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* based c#
|
||||
*/
|
||||
public abstract class SharpStream {
|
||||
|
||||
public abstract int read() throws IOException;
|
||||
|
||||
public abstract int read(byte buffer[]) throws IOException;
|
||||
|
||||
public abstract int read(byte buffer[], int offset, int count) throws IOException;
|
||||
|
||||
public abstract long skip(long amount) throws IOException;
|
||||
|
||||
|
||||
public abstract int available();
|
||||
|
||||
public abstract void rewind() throws IOException;
|
||||
|
||||
|
||||
public abstract void dispose();
|
||||
|
||||
public abstract boolean isDisposed();
|
||||
|
||||
|
||||
public abstract boolean canRewind();
|
||||
|
||||
public abstract boolean canRead();
|
||||
|
||||
public abstract boolean canWrite();
|
||||
|
||||
|
||||
public abstract void write(byte value) throws IOException;
|
||||
|
||||
public abstract void write(byte[] buffer) throws IOException;
|
||||
|
||||
public abstract void write(byte[] buffer, int offset, int count) throws IOException;
|
||||
|
||||
public abstract void flush() throws IOException;
|
||||
|
||||
public void setLength(long length) throws IOException {
|
||||
throw new IOException("Not implemented");
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ import android.animation.ArgbEvaluator;
|
|||
import android.animation.ValueAnimator;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.FloatRange;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.animation.FastOutSlowInInterpolator;
|
||||
import android.util.Log;
|
||||
|
|
@ -363,4 +364,24 @@ public class AnimationUtils {
|
|||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
public static void slideUp(final View view,
|
||||
long duration,
|
||||
long delay,
|
||||
@FloatRange(from = 0.0f, to = 1.0f) float translationPercent) {
|
||||
int translationY = (int) (view.getResources().getDisplayMetrics().heightPixels *
|
||||
(translationPercent));
|
||||
|
||||
view.animate().setListener(null).cancel();
|
||||
view.setAlpha(0f);
|
||||
view.setTranslationY(translationY);
|
||||
view.setVisibility(View.VISIBLE);
|
||||
view.animate()
|
||||
.alpha(1f)
|
||||
.translationY(0)
|
||||
.setStartDelay(delay)
|
||||
.setDuration(duration)
|
||||
.setInterpolator(new FastOutSlowInInterpolator())
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,8 +69,7 @@ public final class ExtractorHelper {
|
|||
public static Single<SearchInfo> searchFor(final int serviceId,
|
||||
final String searchString,
|
||||
final List<String> contentFilter,
|
||||
final String sortFilter,
|
||||
final String contentCountry) {
|
||||
final String sortFilter) {
|
||||
checkServiceId(serviceId);
|
||||
return Single.fromCallable(() ->
|
||||
SearchInfo.getInfo(NewPipe.getService(serviceId),
|
||||
|
|
@ -83,8 +82,7 @@ public final class ExtractorHelper {
|
|||
final String searchString,
|
||||
final List<String> contentFilter,
|
||||
final String sortFilter,
|
||||
final String pageUrl,
|
||||
final String contentCountry) {
|
||||
final String pageUrl) {
|
||||
checkServiceId(serviceId);
|
||||
return Single.fromCallable(() ->
|
||||
SearchInfo.getMoreItems(NewPipe.getService(serviceId),
|
||||
|
|
@ -96,8 +94,7 @@ public final class ExtractorHelper {
|
|||
}
|
||||
|
||||
public static Single<List<String>> suggestionsFor(final int serviceId,
|
||||
final String query,
|
||||
final String contentCountry) {
|
||||
final String query) {
|
||||
checkServiceId(serviceId);
|
||||
return Single.fromCallable(() ->
|
||||
NewPipe.getService(serviceId)
|
||||
|
|
@ -126,7 +123,7 @@ public final class ExtractorHelper {
|
|||
final String nextStreamsUrl) {
|
||||
checkServiceId(serviceId);
|
||||
return Single.fromCallable(() ->
|
||||
ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl, NewPipe.getLocalization()));
|
||||
ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl));
|
||||
}
|
||||
|
||||
public static Single<CommentsInfo> getCommentsInfo(final int serviceId,
|
||||
|
|
@ -163,19 +160,17 @@ public final class ExtractorHelper {
|
|||
|
||||
public static Single<KioskInfo> getKioskInfo(final int serviceId,
|
||||
final String url,
|
||||
final String contentCountry,
|
||||
boolean forceLoad) {
|
||||
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST, Single.fromCallable(() ->
|
||||
KioskInfo.getInfo(NewPipe.getService(serviceId), url, contentCountry)));
|
||||
KioskInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||
}
|
||||
|
||||
public static Single<InfoItemsPage> getMoreKioskItems(final int serviceId,
|
||||
final String url,
|
||||
final String nextStreamsUrl,
|
||||
final String contentCountry) {
|
||||
final String url,
|
||||
final String nextStreamsUrl) {
|
||||
return Single.fromCallable(() ->
|
||||
KioskInfo.getMoreItems(NewPipe.getService(serviceId),
|
||||
url, nextStreamsUrl, contentCountry));
|
||||
url, nextStreamsUrl));
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -110,7 +110,8 @@ public final class ListHelper {
|
|||
: context.getString(R.string.best_resolution_key);
|
||||
|
||||
String maxResolution = getResolutionLimit(context);
|
||||
if (maxResolution != null && compareVideoStreamResolution(maxResolution, resolution) < 1){
|
||||
if (maxResolution != null && (resolution.equals(context.getString(R.string.best_resolution_key))
|
||||
|| compareVideoStreamResolution(maxResolution, resolution) < 1)) {
|
||||
resolution = maxResolution;
|
||||
}
|
||||
return resolution;
|
||||
|
|
|
|||
|
|
@ -69,10 +69,23 @@ public class Localization {
|
|||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
public static org.schabi.newpipe.extractor.utils.Localization getPreferredExtractorLocal(Context context) {
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
String languageCode = sp.getString(context.getString(R.string.content_language_key),
|
||||
context.getString(R.string.default_language_value));
|
||||
|
||||
String countryCode = sp.getString(context.getString(R.string.content_country_key),
|
||||
context.getString(R.string.default_country_value));
|
||||
|
||||
return new org.schabi.newpipe.extractor.utils.Localization(countryCode, languageCode);
|
||||
}
|
||||
|
||||
public static Locale getPreferredLocale(Context context) {
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
String languageCode = sp.getString(context.getString(R.string.search_language_key), context.getString(R.string.default_language_value));
|
||||
String languageCode = sp.getString(context.getString(R.string.content_language_key),
|
||||
context.getString(R.string.default_language_value));
|
||||
|
||||
try {
|
||||
if (languageCode.length() == 2) {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ import org.schabi.newpipe.player.MainVideoPlayer;
|
|||
import org.schabi.newpipe.player.PopupVideoPlayer;
|
||||
import org.schabi.newpipe.player.PopupVideoPlayerActivity;
|
||||
import org.schabi.newpipe.player.VideoPlayer;
|
||||
import org.schabi.newpipe.player.old.PlayVideoActivity;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
|
||||
|
|
@ -118,26 +117,6 @@ public class NavigationHelper {
|
|||
context.startActivity(playerIntent);
|
||||
}
|
||||
|
||||
public static void playOnOldVideoPlayer(Context context, StreamInfo info) {
|
||||
ArrayList<VideoStream> videoStreamsList = new ArrayList<>(ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false));
|
||||
int index = ListHelper.getDefaultResolutionIndex(context, videoStreamsList);
|
||||
|
||||
if (index == -1) {
|
||||
Toast.makeText(context, R.string.video_streams_empty, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
VideoStream videoStream = videoStreamsList.get(index);
|
||||
Intent intent = new Intent(context, PlayVideoActivity.class)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(PlayVideoActivity.VIDEO_TITLE, info.getName())
|
||||
.putExtra(PlayVideoActivity.STREAM_URL, videoStream.getUrl())
|
||||
.putExtra(PlayVideoActivity.VIDEO_URL, info.getUrl())
|
||||
.putExtra(PlayVideoActivity.START_POSITION, info.getStartPosition());
|
||||
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
public static void playOnPopupPlayer(final Context context, final PlayQueue queue) {
|
||||
if (!PermissionHelper.isPopupEnabled(context)) {
|
||||
PermissionHelper.showPopupEnablementToast(context);
|
||||
|
|
|
|||
|
|
@ -4,11 +4,15 @@ import org.schabi.newpipe.extractor.InfoItem;
|
|||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class RelatedStreamInfo extends ListInfo<InfoItem> {
|
||||
|
||||
private StreamInfoItem nextStream;
|
||||
|
||||
public RelatedStreamInfo(int serviceId, ListLinkHandler listUrlIdHandler, String name) {
|
||||
super(serviceId, listUrlIdHandler, name);
|
||||
|
|
@ -17,7 +21,21 @@ public class RelatedStreamInfo extends ListInfo<InfoItem> {
|
|||
public static RelatedStreamInfo getInfo(StreamInfo info) {
|
||||
ListLinkHandler handler = new ListLinkHandler(info.getOriginalUrl(), info.getUrl(), info.getId(), Collections.emptyList(), null);
|
||||
RelatedStreamInfo relatedStreamInfo = new RelatedStreamInfo(info.getServiceId(), handler, info.getName());
|
||||
relatedStreamInfo.setRelatedItems(info.getRelatedStreams());
|
||||
return relatedStreamInfo;
|
||||
List<InfoItem> streams = new ArrayList<>();
|
||||
if(info.getNextVideo() != null){
|
||||
streams.add(info.getNextVideo());
|
||||
}
|
||||
streams.addAll(info.getRelatedStreams());
|
||||
relatedStreamInfo.setRelatedItems(streams);
|
||||
relatedStreamInfo.setNextStream(info.getNextVideo());
|
||||
return relatedStreamInfo;
|
||||
}
|
||||
|
||||
public StreamInfoItem getNextStream() {
|
||||
return nextStream;
|
||||
}
|
||||
|
||||
public void setNextStream(StreamInfoItem nextStream) {
|
||||
this.nextStream = nextStream;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SecondaryStreamHelper<T extends Stream> {
|
||||
private final int position;
|
||||
private final StreamSizeWrapper<T> streams;
|
||||
|
||||
public SecondaryStreamHelper(StreamSizeWrapper<T> streams, T selectedStream) {
|
||||
this.streams = streams;
|
||||
this.position = streams.getStreamsList().indexOf(selectedStream);
|
||||
if (this.position < 0) throw new RuntimeException("selected stream not found");
|
||||
}
|
||||
|
||||
public T getStream() {
|
||||
return streams.getStreamsList().get(position);
|
||||
}
|
||||
|
||||
public long getSizeInBytes() {
|
||||
return streams.getSizeInBytes(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* find the correct audio stream for the desired video stream
|
||||
*
|
||||
* @param audioStreams list of audio streams
|
||||
* @param videoStream desired video ONLY stream
|
||||
* @return selected audio stream or null if a candidate was not found
|
||||
*/
|
||||
public static AudioStream getAudioStreamFor(@NonNull List<AudioStream> audioStreams, @NonNull VideoStream videoStream) {
|
||||
// TODO: check if m4v and m4a selected streams are DASH compliant
|
||||
switch (videoStream.getFormat()) {
|
||||
case WEBM:
|
||||
case MPEG_4:
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean m4v = videoStream.getFormat() == MediaFormat.MPEG_4;
|
||||
|
||||
for (AudioStream audio : audioStreams) {
|
||||
if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) {
|
||||
return audio;
|
||||
}
|
||||
}
|
||||
|
||||
// retry, but this time in reverse order
|
||||
for (int i = audioStreams.size() - 1; i >= 0; i--) {
|
||||
AudioStream audio = audioStreams.get(i);
|
||||
if (audio.getFormat() == (m4v ? MediaFormat.MP3 : MediaFormat.OPUS)) {
|
||||
return audio;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
|
@ -13,6 +14,7 @@ import org.schabi.newpipe.Downloader;
|
|||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
|
@ -28,26 +30,34 @@ import us.shandian.giga.util.Utility;
|
|||
/**
|
||||
* A list adapter for a list of {@link Stream streams}, currently supporting {@link VideoStream} and {@link AudioStream}.
|
||||
*/
|
||||
public class StreamItemAdapter<T extends Stream> extends BaseAdapter {
|
||||
public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseAdapter {
|
||||
private final Context context;
|
||||
|
||||
private final StreamSizeWrapper<T> streamsWrapper;
|
||||
private final boolean showIconNoAudio;
|
||||
private final SparseArray<SecondaryStreamHelper<U>> secondaryStreams;
|
||||
|
||||
public StreamItemAdapter(Context context, StreamSizeWrapper<T> streamsWrapper, boolean showIconNoAudio) {
|
||||
public StreamItemAdapter(Context context, StreamSizeWrapper<T> streamsWrapper, SparseArray<SecondaryStreamHelper<U>> secondaryStreams) {
|
||||
this.context = context;
|
||||
this.streamsWrapper = streamsWrapper;
|
||||
this.showIconNoAudio = showIconNoAudio;
|
||||
this.secondaryStreams = secondaryStreams;
|
||||
}
|
||||
|
||||
public StreamItemAdapter(Context context, StreamSizeWrapper<T> streamsWrapper, boolean showIconNoAudio) {
|
||||
this(context, streamsWrapper, showIconNoAudio ? new SparseArray<>() : null);
|
||||
}
|
||||
|
||||
public StreamItemAdapter(Context context, StreamSizeWrapper<T> streamsWrapper) {
|
||||
this(context, streamsWrapper, false);
|
||||
this(context, streamsWrapper, null);
|
||||
}
|
||||
|
||||
public List<T> getAll() {
|
||||
return streamsWrapper.getStreamsList();
|
||||
}
|
||||
|
||||
public SparseArray<SecondaryStreamHelper<U>> getAllSecondary() {
|
||||
return secondaryStreams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return streamsWrapper.getStreamsList().size();
|
||||
|
|
@ -89,29 +99,46 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter {
|
|||
String qualityString;
|
||||
|
||||
if (stream instanceof VideoStream) {
|
||||
qualityString = ((VideoStream) stream).getResolution();
|
||||
VideoStream videoStream = ((VideoStream) stream);
|
||||
qualityString = videoStream.getResolution();
|
||||
|
||||
if (!showIconNoAudio) {
|
||||
woSoundIconVisibility = View.GONE;
|
||||
} else if (((VideoStream) stream).isVideoOnly()) {
|
||||
woSoundIconVisibility = View.VISIBLE;
|
||||
} else if (isDropdownItem) {
|
||||
woSoundIconVisibility = View.INVISIBLE;
|
||||
if (secondaryStreams != null) {
|
||||
if (videoStream.isVideoOnly()) {
|
||||
woSoundIconVisibility = secondaryStreams.get(position) == null ? View.VISIBLE : View.INVISIBLE;
|
||||
} else if (isDropdownItem) {
|
||||
woSoundIconVisibility = View.INVISIBLE;
|
||||
}
|
||||
}
|
||||
} else if (stream instanceof AudioStream) {
|
||||
qualityString = ((AudioStream) stream).getAverageBitrate() + "kbps";
|
||||
} else if (stream instanceof SubtitlesStream) {
|
||||
qualityString = ((SubtitlesStream) stream).getDisplayLanguageName();
|
||||
if (((SubtitlesStream) stream).isAutoGenerated()) {
|
||||
qualityString += " (" + context.getString(R.string.caption_auto_generated) + ")";
|
||||
}
|
||||
} else {
|
||||
qualityString = stream.getFormat().getSuffix();
|
||||
}
|
||||
|
||||
if (streamsWrapper.getSizeInBytes(position) > 0) {
|
||||
sizeView.setText(streamsWrapper.getFormattedSize(position));
|
||||
SecondaryStreamHelper secondary = secondaryStreams == null ? null : secondaryStreams.get(position);
|
||||
if (secondary != null) {
|
||||
long size = secondary.getSizeInBytes() + streamsWrapper.getSizeInBytes(position);
|
||||
sizeView.setText(Utility.formatBytes(size));
|
||||
} else {
|
||||
sizeView.setText(streamsWrapper.getFormattedSize(position));
|
||||
}
|
||||
sizeView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
sizeView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
formatNameView.setText(stream.getFormat().getName());
|
||||
if (stream instanceof SubtitlesStream) {
|
||||
formatNameView.setText(((SubtitlesStream) stream).getLanguageTag());
|
||||
} else {
|
||||
formatNameView.setText(stream.getFormat().getName());
|
||||
}
|
||||
|
||||
qualityView.setText(qualityString);
|
||||
woSoundIconView.setVisibility(woSoundIconVisibility);
|
||||
|
||||
|
|
@ -122,15 +149,17 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter {
|
|||
* A wrapper class that includes a way of storing the stream sizes.
|
||||
*/
|
||||
public static class StreamSizeWrapper<T extends Stream> implements Serializable {
|
||||
private static final StreamSizeWrapper<Stream> EMPTY = new StreamSizeWrapper<>(Collections.emptyList());
|
||||
private static final StreamSizeWrapper<Stream> EMPTY = new StreamSizeWrapper<>(Collections.emptyList(), null);
|
||||
private final List<T> streamsList;
|
||||
private final long[] streamSizes;
|
||||
private final String unknownSize;
|
||||
|
||||
public StreamSizeWrapper(List<T> streamsList) {
|
||||
public StreamSizeWrapper(List<T> streamsList, Context context) {
|
||||
this.streamsList = streamsList;
|
||||
this.streamSizes = new long[streamsList.size()];
|
||||
this.unknownSize = context == null ? "--.-" : context.getString(R.string.unknown_content);
|
||||
|
||||
for (int i = 0; i < streamSizes.length; i++) streamSizes[i] = -1;
|
||||
for (int i = 0; i < streamSizes.length; i++) streamSizes[i] = -2;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -143,7 +172,7 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter {
|
|||
final Callable<Boolean> fetchAndSet = () -> {
|
||||
boolean hasChanged = false;
|
||||
for (X stream : streamsWrapper.getStreamsList()) {
|
||||
if (streamsWrapper.getSizeInBytes(stream) > 0) {
|
||||
if (streamsWrapper.getSizeInBytes(stream) > -2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -173,11 +202,18 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter {
|
|||
}
|
||||
|
||||
public String getFormattedSize(int streamIndex) {
|
||||
return Utility.formatBytes(getSizeInBytes(streamIndex));
|
||||
return formatSize(getSizeInBytes(streamIndex));
|
||||
}
|
||||
|
||||
public String getFormattedSize(T stream) {
|
||||
return Utility.formatBytes(getSizeInBytes(stream));
|
||||
return formatSize(getSizeInBytes(stream));
|
||||
}
|
||||
|
||||
private String formatSize(long size) {
|
||||
if (size > -1) {
|
||||
return Utility.formatBytes(size);
|
||||
}
|
||||
return unknownSize;
|
||||
}
|
||||
|
||||
public void setSize(int streamIndex, long sizeInBytes) {
|
||||
|
|
@ -193,4 +229,4 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter {
|
|||
return (StreamSizeWrapper<X>) EMPTY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue