mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-11-13 05:27:11 +00:00
Conflict resolution.
This commit is contained in:
@@ -89,7 +89,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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ import org.schabi.newpipe.extractor.stream.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
@@ -73,7 +74,6 @@ 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;
|
||||
@@ -371,14 +371,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:
|
||||
@@ -745,7 +745,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() {
|
||||
@@ -921,7 +921,7 @@ public class VideoDetailFragment
|
||||
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
|
||||
startOnExternalPlayer(activity, currentInfo, selectedVideoStream);
|
||||
} else {
|
||||
openNormalPlayer(selectedVideoStream);
|
||||
openNormalPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -934,24 +934,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);
|
||||
}
|
||||
|
||||
@@ -1276,6 +1265,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) {
|
||||
@@ -1333,4 +1323,4 @@ public class VideoDetailFragment
|
||||
relatedStreamRootLayout.setVisibility(visibility);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1134,6 +1134,7 @@ public abstract class BasePlayer implements
|
||||
}
|
||||
|
||||
public boolean isPlaying() {
|
||||
if (simpleExoPlayer == null) return false;
|
||||
final int state = simpleExoPlayer.getPlaybackState();
|
||||
return (state == Player.STATE_READY || state == Player.STATE_BUFFERING)
|
||||
&& simpleExoPlayer.getPlayWhenReady();
|
||||
|
||||
@@ -900,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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -87,7 +87,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 +97,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 +145,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 +158,7 @@ public class PlayerHelper {
|
||||
}
|
||||
}
|
||||
Collections.shuffle(autoQueueItems);
|
||||
return autoQueueItems.isEmpty() ? null : new SinglePlayQueue(autoQueueItems.get(0));
|
||||
return autoQueueItems.isEmpty() ? null : getAutoQueuedSinglePlayQueue(autoQueueItems.get(0));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -177,10 +177,6 @@ public class PlayerHelper {
|
||||
return isBrightnessGestureEnabled(context, true);
|
||||
}
|
||||
|
||||
public static boolean isUsingOldPlayer(@NonNull final Context context) {
|
||||
return isUsingOldPlayer(context, false);
|
||||
}
|
||||
|
||||
public static boolean isRememberingPopupDimensions(@NonNull final Context context) {
|
||||
return isRememberingPopupDimensions(context, true);
|
||||
}
|
||||
@@ -318,10 +314,6 @@ public class PlayerHelper {
|
||||
return getPreferences(context).getBoolean(context.getString(R.string.brightness_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 isRememberingPopupDimensions(@NonNull final Context context, final boolean b) {
|
||||
return getPreferences(context).getBoolean(context.getString(R.string.popup_remember_size_pos_key), b);
|
||||
}
|
||||
@@ -358,4 +350,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,7 +10,7 @@ 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.VideoStream;
|
||||
@@ -93,8 +93,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,
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,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;
|
||||
|
||||
@@ -117,26 +116,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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user