diff --git a/app/build.gradle b/app/build.gradle index f8fc1565f..5da8c9ff0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -53,6 +53,7 @@ ext { okHttpLibVersion = '3.12.6' icepickLibVersion = '3.2.0' stethoLibVersion = '1.5.0' + markwonVersion = '4.2.1' } dependencies { @@ -62,7 +63,7 @@ dependencies { exclude module: 'support-annotations' }) - implementation 'com.github.TeamNewPipe:NewPipeExtractor:ff61e284' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:9112a10' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.0' @@ -108,4 +109,7 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:${okHttpLibVersion}" debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoLibVersion}" + + implementation "io.noties.markwon:core:${markwonVersion}" + implementation "io.noties.markwon:linkify:${markwonVersion}" } diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java index 9e23d9d3d..edfc54375 100644 --- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java +++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java @@ -32,18 +32,20 @@ public class AboutActivity extends AppCompatActivity { * List of all software components */ private static final SoftwareComponent[] SOFTWARE_COMPONENTS = new SoftwareComponent[]{ - new SoftwareComponent("Giga Get", "2014", "Peter Cai", "https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL2), - new SoftwareComponent("NewPipe Extractor", "2017", "Christian Schabesberger", "https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3), + new SoftwareComponent("Giga Get", "2014 - 2015", "Peter Cai", "https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL2), + new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger", "https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3), new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley", "https://github.com/jhy/jsoup", StandardLicenses.MIT), new SoftwareComponent("Rhino", "2015", "Mozilla", "https://www.mozilla.org/rhino/", StandardLicenses.MPL2), new SoftwareComponent("ACRA", "2013", "Kevin Gaudin", "http://www.acra.ch", StandardLicenses.APACHE2), new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich", "https://github.com/nostra13/Android-Universal-Image-Loader", StandardLicenses.APACHE2), - new SoftwareComponent("CircleImageView", "2014 - 2017", "Henning Dodenhof", "https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2), + new SoftwareComponent("CircleImageView", "2014 - 2020", "Henning Dodenhof", "https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2), new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam", "https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2), - new SoftwareComponent("ExoPlayer", "2014-2017", "Google Inc", "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2), - new SoftwareComponent("RxAndroid", "2015", "The RxAndroid authors", "https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2), - new SoftwareComponent("RxJava", "2016-present", "RxJava Contributors", "https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2), - new SoftwareComponent("RxBinding", "2015", "Jake Wharton", "https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2) + new SoftwareComponent("ExoPlayer", "2014 - 2020", "Google Inc", "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2), + new SoftwareComponent("RxAndroid", "2015 - 2018", "The RxAndroid authors", "https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2), + new SoftwareComponent("RxJava", "2016 - 2020", "RxJava Contributors", "https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2), + new SoftwareComponent("RxBinding", "2015 - 2018", "Jake Wharton", "https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2), + new SoftwareComponent("PrettyTime", "2012 - 2020", "Lincoln Baxter, III", "https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2), + new SoftwareComponent("Markwon", "2017 - 2020", "Noties", "https://github.com/noties/Markwon", StandardLicenses.APACHE2) }; /** diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 1fb44a813..424b74070 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -2,7 +2,6 @@ package org.schabi.newpipe.fragments.detail; import android.app.Activity; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; @@ -18,7 +17,6 @@ import androidx.fragment.app.Fragment; import androidx.core.content.ContextCompat; import androidx.viewpager.widget.ViewPager; import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import android.text.Html; import android.text.Spanned; @@ -58,6 +56,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamType; @@ -97,6 +96,8 @@ import java.util.List; import java.util.concurrent.TimeUnit; import icepick.State; +import io.noties.markwon.Markwon; +import io.noties.markwon.linkify.LinkifyPlugin; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; @@ -486,7 +487,6 @@ public class VideoDetailFragment videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout); videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view); videoDescriptionView = rootView.findViewById(R.id.detail_description_view); - videoDescriptionView.setAutoLinkMask(Linkify.WEB_URLS); thumbsUpTextView = rootView.findViewById(R.id.detail_thumbs_up_count_view); thumbsUpImageView = rootView.findViewById(R.id.detail_thumbs_up_img_view); @@ -922,28 +922,41 @@ public class VideoDetailFragment return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null; } - private void prepareDescription(final String descriptionHtml) { - if (TextUtils.isEmpty(descriptionHtml)) { + private void prepareDescription(Description description) { + if (TextUtils.isEmpty(description.getContent()) || description == Description.emptyDescription) { return; } - disposables.add(Single.just(descriptionHtml) - .map((@io.reactivex.annotations.NonNull String description) -> { - Spanned parsedDescription; - if (Build.VERSION.SDK_INT >= 24) { - parsedDescription = Html.fromHtml(description, 0); - } else { - //noinspection deprecation - parsedDescription = Html.fromHtml(description); - } - return parsedDescription; - }) - .subscribeOn(Schedulers.computation()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe((@io.reactivex.annotations.NonNull Spanned spanned) -> { - videoDescriptionView.setText(spanned); - videoDescriptionView.setVisibility(View.VISIBLE); - })); + if (description.getType() == Description.HTML) { + disposables.add(Single.just(description.getContent()) + .map((@io.reactivex.annotations.NonNull String descriptionText) -> { + Spanned parsedDescription; + if (Build.VERSION.SDK_INT >= 24) { + parsedDescription = Html.fromHtml(descriptionText, 0); + } else { + //noinspection deprecation + parsedDescription = Html.fromHtml(descriptionText); + } + return parsedDescription; + }) + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe((@io.reactivex.annotations.NonNull Spanned spanned) -> { + videoDescriptionView.setText(spanned); + videoDescriptionView.setVisibility(View.VISIBLE); + })); + } else if (description.getType() == Description.MARKDOWN) { + final Markwon markwon = Markwon.builder(getContext()) + .usePlugin(LinkifyPlugin.create()) + .build(); + markwon.setMarkdown(videoDescriptionView, description.getContent()); + videoDescriptionView.setVisibility(View.VISIBLE); + } else { + //== Description.PLAIN_TEXT + videoDescriptionView.setAutoLinkMask(Linkify.WEB_URLS); + videoDescriptionView.setText(description.getContent(), TextView.BufferType.SPANNABLE); + videoDescriptionView.setVisibility(View.VISIBLE); + } } private void setHeightThumbnail() { diff --git a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java index 0e62810c5..75b55cd73 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java @@ -137,6 +137,7 @@ public class DataReader { position = 0; readOffset = readBuffer.length; + readCount = 0; } public boolean canRewind() { diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java index 818f6148e..57a7aaa9c 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java @@ -11,6 +11,7 @@ import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; /** * @author kapodamy @@ -23,7 +24,6 @@ public class Mp4FromDashWriter { private final static byte SAMPLES_PER_CHUNK = 6;// ffmpeg uses 2, basic uses 1 (with 60fps uses 21 or 22). NewPipe will use 6 private final static long THRESHOLD_FOR_CO64 = 0xFFFEFFFFL;// near 3.999 GiB private final static int THRESHOLD_MOOV_LENGTH = (256 * 1024) + (2048 * 1024); // 2.2 MiB enough for: 1080p 60fps 00h35m00s - private final static short SINGLE_CHUNK_SAMPLE_BUFFER = 256; private final long time; @@ -46,6 +46,8 @@ public class Mp4FromDashWriter { private int overrideMainBrand = 0x00; + private ArrayList compatibleBrands = new ArrayList<>(5); + public Mp4FromDashWriter(SharpStream... sources) throws IOException { for (SharpStream src : sources) { if (!src.canRewind() && !src.canRead()) { @@ -57,6 +59,10 @@ public class Mp4FromDashWriter { readers = new Mp4DashReader[sourceTracks.length]; readersChunks = new Mp4DashChunk[readers.length]; time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET; + + compatibleBrands.add(0x6D703431);// mp41 + compatibleBrands.add(0x69736F6D);// isom + compatibleBrands.add(0x69736F32);// iso2 } public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException { @@ -104,8 +110,8 @@ public class Mp4FromDashWriter { } } - public void setMainBrand(int brandId) { - overrideMainBrand = brandId; + public void setMainBrand(int brand) { + overrideMainBrand = brand; } public boolean isDone() { @@ -159,7 +165,13 @@ public class Mp4FromDashWriter { tablesInfo[i] = new TablesInfo(); } - boolean singleChunk = tracks.length == 1 && tracks[0].kind == TrackKind.Audio; + int single_sample_buffer; + if (tracks.length == 1 && tracks[0].kind == TrackKind.Audio) { + // near 1 second of audio data per chunk, avoid split the audio stream in large chunks + single_sample_buffer = tracks[0].trak.mdia.mdhd_timeScale / 1000; + } else { + single_sample_buffer = -1; + } for (int i = 0; i < readers.length; i++) { @@ -210,31 +222,10 @@ public class Mp4FromDashWriter { readers[i].rewind(); - int tmp = tablesInfo[i].stsz - SAMPLES_PER_CHUNK_INIT; - tablesInfo[i].stco = (tmp / SAMPLES_PER_CHUNK) + 1;// +1 for samples in first chunk - - tmp = tmp % SAMPLES_PER_CHUNK; - if (singleChunk) { - // avoid split audio streams in chunks - tablesInfo[i].stsc = 1; - tablesInfo[i].stsc_bEntries = new int[]{ - 1, tablesInfo[i].stsz, 1 - }; - tablesInfo[i].stco = 1; - } else if (tmp == 0) { - tablesInfo[i].stsc = 2;// first chunk (init) and succesive chunks - tablesInfo[i].stsc_bEntries = new int[]{ - 1, SAMPLES_PER_CHUNK_INIT, 1, - 2, SAMPLES_PER_CHUNK, 1 - }; + if (single_sample_buffer > 0) { + initChunkTables(tablesInfo[i], single_sample_buffer, single_sample_buffer); } else { - tablesInfo[i].stsc = 3;// first chunk (init) and successive chunks and remain chunk - tablesInfo[i].stsc_bEntries = new int[]{ - 1, SAMPLES_PER_CHUNK_INIT, 1, - 2, SAMPLES_PER_CHUNK, 1, - tablesInfo[i].stco + 1, tmp, 1 - }; - tablesInfo[i].stco++; + initChunkTables(tablesInfo[i], SAMPLES_PER_CHUNK_INIT, SAMPLES_PER_CHUNK); } sampleCount[i] = tablesInfo[i].stsz; @@ -259,7 +250,7 @@ public class Mp4FromDashWriter { boolean is64 = read > THRESHOLD_FOR_CO64; - // calculate the moov size; + // calculate the moov size int auxSize = make_moov(defaultMediaTime, tablesInfo, is64); if (auxSize < THRESHOLD_MOOV_LENGTH) { @@ -272,11 +263,6 @@ public class Mp4FromDashWriter { final int ftyp_size = make_ftyp(); // reserve moov space in the output stream - /*if (outStream.canSetLength()) { - long length = writeOffset + auxSize; - outStream.setLength(length); - outSeek(length); - } else {*/ if (auxSize > 0) { int length = auxSize; byte[] buffer = new byte[64 * 1024];// 64 KiB @@ -292,10 +278,10 @@ public class Mp4FromDashWriter { } // tablesInfo contains row counts - // and after returning from make_moov() will contain table offsets + // and after returning from make_moov() will contain those table offsets make_moov(defaultMediaTime, tablesInfo, is64); - // write tables: stts stsc + // write tables: stts stsc sbgp // reset for ctts table: sampleCount sampleExtra for (int i = 0; i < readers.length; i++) { writeEntryArray(tablesInfo[i].stts, 2, sampleCount[i], defaultSampleDuration[i]); @@ -305,6 +291,7 @@ public class Mp4FromDashWriter { sampleCount[i] = 1;// the index is not base zero sampleExtra[i] = -1; } + writeEntryArray(tablesInfo[i].sbgp, 1, sampleCount[i]); } if (auxBuffer == null) { @@ -314,8 +301,8 @@ public class Mp4FromDashWriter { outWrite(make_mdat(totalSampleSize, is64)); int[] sampleIndex = new int[readers.length]; - int[] sizes = new int[singleChunk ? SINGLE_CHUNK_SAMPLE_BUFFER : SAMPLES_PER_CHUNK]; - int[] sync = new int[singleChunk ? SINGLE_CHUNK_SAMPLE_BUFFER : SAMPLES_PER_CHUNK]; + int[] sizes = new int[single_sample_buffer > 0 ? single_sample_buffer : SAMPLES_PER_CHUNK]; + int[] sync = new int[single_sample_buffer > 0 ? single_sample_buffer : SAMPLES_PER_CHUNK]; int written = readers.length; while (written > 0) { @@ -329,8 +316,8 @@ public class Mp4FromDashWriter { long chunkOffset = writeOffset; int syncCount = 0; int limit; - if (singleChunk) { - limit = SINGLE_CHUNK_SAMPLE_BUFFER; + if (single_sample_buffer > 0) { + limit = single_sample_buffer; } else { limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK; } @@ -342,6 +329,7 @@ public class Mp4FromDashWriter { if (sample == null) { if (tablesInfo[i].ctts > 0 && sampleExtra[i] >= 0) { writeEntryArray(tablesInfo[i].ctts, 1, sampleCount[i], sampleExtra[i]);// flush last entries + outRestore(); } sampleIndex[i] = -1; break; @@ -390,10 +378,6 @@ public class Mp4FromDashWriter { } else { tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, (int) chunkOffset); } - - if (singleChunk) { - tablesInfo[i].stco = -1; - } } outRestore(); @@ -470,7 +454,42 @@ public class Mp4FromDashWriter { } } + private void initChunkTables(TablesInfo tables, int firstCount, int succesiveCount) { + // tables.stsz holds amount of samples of the track (total) + int totalSamples = (tables.stsz - firstCount); + float chunkAmount = totalSamples / (float) succesiveCount; + int remainChunkOffset = (int) Math.ceil(chunkAmount); + boolean remain = remainChunkOffset != (int) chunkAmount; + int index = 0; + tables.stsc = 1; + if (firstCount != succesiveCount) { + tables.stsc++; + } + if (remain) { + tables.stsc++; + } + + // stsc_table_entry = [first_chunk, samples_per_chunk, sample_description_index] + tables.stsc_bEntries = new int[tables.stsc * 3]; + tables.stco = remainChunkOffset + 1;// total entrys in chunk offset box + + tables.stsc_bEntries[index++] = 1; + tables.stsc_bEntries[index++] = firstCount; + tables.stsc_bEntries[index++] = 1; + + if (firstCount != succesiveCount) { + tables.stsc_bEntries[index++] = 2; + tables.stsc_bEntries[index++] = succesiveCount; + tables.stsc_bEntries[index++] = 1; + } + + if (remain) { + tables.stsc_bEntries[index++] = remainChunkOffset + 1; + tables.stsc_bEntries[index++] = totalSamples % succesiveCount; + tables.stsc_bEntries[index] = 1; + } + } private void outWrite(byte[] buffer) throws IOException { outWrite(buffer, buffer.length); @@ -585,19 +604,29 @@ public class Mp4FromDashWriter { private int make_ftyp() throws IOException { - byte[] buffer = new byte[]{ - 0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70,// ftyp - 0x6D, 0x70, 0x34, 0x32,// mayor brand (mp42) - 0x00, 0x00, 0x02, 0x00,// default minor version (512) - 0x6D, 0x70, 0x34, 0x31, 0x69, 0x73, 0x6F, 0x6D, 0x69, 0x73, 0x6F, 0x32// compatible brands: mp41 isom iso2 - }; + int size = 16 + (compatibleBrands.size() * 4); + if (overrideMainBrand != 0) size += 4; - if (overrideMainBrand != 0) - ByteBuffer.wrap(buffer).putInt(8, overrideMainBrand); + ByteBuffer buffer = ByteBuffer.allocate(size); + buffer.putInt(size); + buffer.putInt(0x66747970);// "ftyp" - outWrite(buffer); + if (overrideMainBrand == 0) { + buffer.putInt(0x6D703432);// mayor brand "mp42" + buffer.putInt(512);// default minor version + } else { + buffer.putInt(overrideMainBrand); + buffer.putInt(0); + buffer.putInt(0x6D703432);// "mp42" compatible brand + } - return buffer.length; + for (Integer brand : compatibleBrands) { + buffer.putInt(brand);// compatible brand + } + + outWrite(buffer.array()); + + return size; } private byte[] make_mdat(long refSize, boolean is64) { @@ -740,13 +769,12 @@ public class Mp4FromDashWriter { .array() ); - make_mdia(tracks[index].trak.mdia, tables, is64); + make_mdia(tracks[index].trak.mdia, tables, is64, tracks[index].kind == TrackKind.Audio); lengthFor(start); } - private void make_mdia(Mdia mdia, TablesInfo tablesInfo, boolean is64) throws IOException { - + private void make_mdia(Mdia mdia, TablesInfo tablesInfo, boolean is64, boolean isAudio) throws IOException { int start_mdia = auxOffset(); auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x61});// mdia auxWrite(mdia.mdhd); @@ -766,7 +794,7 @@ public class Mp4FromDashWriter { // And stsz can be empty if has a default sample size // if (moovSimulation) { - make(0x73747473, -1, 2, 1); + make(0x73747473, -1, 2, 1);// stts if (tablesInfo.stss > 0) { make(0x73747373, -1, 1, tablesInfo.stss); } @@ -789,6 +817,11 @@ public class Mp4FromDashWriter { tablesInfo.stco = make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco); } + if (isAudio) { + auxWrite(make_sgpd()); + tablesInfo.sbgp = make_sbgp();// during simulation the returned offset is ignored + } + lengthFor(start_stbl); lengthFor(start_minf); lengthFor(start_mdia); @@ -816,6 +849,48 @@ public class Mp4FromDashWriter { return buffer.array(); } + private int make_sbgp() throws IOException { + int offset = auxOffset(); + + auxWrite(new byte[] { + 0x00, 0x00, 0x00, 0x1C,// box size + 0x73, 0x62, 0x67, 0x70,// "sbpg" + 0x00, 0x00, 0x00, 0x00,// default box flags + 0x72, 0x6F, 0x6C, 0x6C,// group type "roll" + 0x00, 0x00, 0x00, 0x01,// group table size + 0x00, 0x00, 0x00, 0x00,// group[0] total samples (to be set later) + 0x00, 0x00, 0x00, 0x01// group[0] description index + }); + + return offset + 0x14; + } + + private byte[] make_sgpd() { + /* + * Sample Group Description Box + * + * ¿whats does? + * the table inside of this box gives information about the + * characteristics of sample groups. The descriptive information is any other + * information needed to define or characterize the sample group. + * + * ¿is replicabled this box? + * NO due lacks of documentation about this box but... + * most of m4a encoders and ffmpeg uses this box with dummy values (same values) + */ + + ByteBuffer buffer = ByteBuffer.wrap(new byte[] { + 0x00, 0x00, 0x00, 0x1A,// box size + 0x73, 0x67, 0x70, 0x64,// "sgpd" + 0x01, 0x00, 0x00, 0x00,// box flags (unknown flag sets) + 0x72, 0x6F, 0x6C, 0x6C, // ¿¿group type?? + 0x00, 0x00, 0x00, 0x02,// ¿¿?? + 0x00, 0x00, 0x00, 0x01,// ¿¿?? + (byte)0xFF, (byte)0xFF// ¿¿?? + }); + + return buffer.array(); + } class TablesInfo { @@ -827,5 +902,6 @@ public class Mp4FromDashWriter { int stsz_default; int stss; int stco; + int sbgp; } }