diff --git a/.travis.yml b/.travis.yml index 24ff6473c..ee704b548 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ android: env: global: - ADB_INSTALL_TIMEOUT=8 # minutes (2 by default) + - GRADLE_OPTS=-Xmx512m # give gradle more memory since it seem to fail otherwise matrix: - ANDROID_TARGET=android-19 ANDROID_ABI=armeabi-v7a @@ -28,3 +29,5 @@ before_script: - emulator -avd test -no-skin -no-audio -no-window & - android-wait-for-emulator - adb shell input keyevent 82 & + +script: ./gradlew --info build connectedCheck diff --git a/README.md b/README.md index 3fdcfecfa..f5b77b0e8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # NewPipe NewPipe: A free lightweight Youtube frontend for Android. -[![NewPipe](app/src/main/res/mipmap-xhdpi/ic_launcher.png)](http://dasochan.nl/newpipe/) +[![NewPipe](app/src/main/res/mipmap-xhdpi/ic_launcher.png)](https://newpipe.schabi.org) Project status: [![Translation Status](https://hosted.weblate.org/widgets/NewPipe/-/svg-badge.svg)](https://hosted.weblate.org/engage/NewPipe/) @@ -11,6 +11,12 @@ Project status: [![F-Droid](https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png)](https://f-droid.org/repository/browse/?fdfilter=newpipe&fdid=org.schabi.newpipe) +## Donate +![Bitcoin](https://bitcoin.org/img/icons/logotop.svg) +`16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh` + +![BitcoinQR](assets/16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh.png) + ## Screenshots [](screenshots/screenshot_1.png) @@ -36,6 +42,7 @@ NewPipe does not use any Google framework libraries, or the YouTube API. It only * Show Next/Related videos * Search YouTube in a specific language * Orbot/Tor support (no streaming yet, experimental) +* Watch age restricted material ### Coming Features diff --git a/app/build.gradle b/app/build.gradle index 84401da7e..8e08f39a2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "org.schabi.newpipe" minSdkVersion 15 targetSdkVersion 23 - versionCode 13 - versionName "0.7.4" + versionCode 16 + versionName "0.7.7" } buildTypes { release { @@ -32,14 +32,16 @@ android { dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') - compile 'com.android.support:appcompat-v7:23.1.1' - compile 'com.android.support:support-v4:23.1.1' - compile 'com.android.support:design:23.1.1' - compile 'com.android.support:recyclerview-v7:23.1.1' + compile 'com.android.support:appcompat-v7:23.2.0' + compile 'com.android.support:support-v4:23.2.0' + compile 'com.android.support:design:23.2.0' + compile 'com.android.support:recyclerview-v7:23.2.0' compile 'org.jsoup:jsoup:1.8.3' compile 'org.mozilla:rhino:1.7.7' compile 'info.guardianproject.netcipher:netcipher:1.2' compile 'de.hdodenhof:circleimageview:2.0.0' compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5' compile 'com.github.nirhart:parallaxscroll:1.0' + compile 'org.apache.directory.studio:org.apache.commons.lang:2.6' + compile 'com.google.android.exoplayer:exoplayer:r1.5.5' } diff --git a/app/src/androidTest/java/org/schabi/newpipe/extractor/youtube/YoutubeSearchEngineTest.java b/app/src/androidTest/java/org/schabi/newpipe/extractor/youtube/YoutubeSearchEngineTest.java new file mode 100644 index 000000000..0810a1351 --- /dev/null +++ b/app/src/androidTest/java/org/schabi/newpipe/extractor/youtube/YoutubeSearchEngineTest.java @@ -0,0 +1,121 @@ +package org.schabi.newpipe.extractor.youtube; + +import android.test.AndroidTestCase; + +import org.apache.commons.lang.exception.ExceptionUtils; +import org.schabi.newpipe.extractor.AbstractVideoInfo; +import org.schabi.newpipe.extractor.SearchResult; +import org.schabi.newpipe.extractor.ServiceList; +import org.schabi.newpipe.extractor.StreamPreviewInfo; +import org.schabi.newpipe.extractor.SearchEngine; +import org.schabi.newpipe.extractor.services.youtube.YoutubeSearchEngine; +import org.schabi.newpipe.Downloader; +import org.schabi.newpipe.extractor.services.youtube.YoutubeService; + +import java.util.ArrayList; + +/** + * Created by Christian Schabesberger on 29.12.15. + * + * Copyright (C) Christian Schabesberger 2015 + * YoutubeSearchEngineTest.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 . + */ + +public class YoutubeSearchEngineTest extends AndroidTestCase { + private SearchResult result; + private ArrayList suggestionReply; + + @Override + public void setUp() throws Exception{ + super.setUp(); + SearchEngine engine = ServiceList.getService("Youtube") + .getSearchEngineInstance(new Downloader()); + + result = engine.search("lefloid", + 0, "de", new Downloader()).getSearchResult(); + suggestionReply = engine.suggestionList("hello","de",new Downloader()); + } + + public void testIfNoErrorOccur() { + assertTrue(result.errors.isEmpty() ? "" : ExceptionUtils.getStackTrace(result.errors.get(0)) + ,result.errors.isEmpty()); + } + + public void testIfListIsNotEmpty() { + assertEquals(result.resultList.size() > 0, true); + } + + public void testItemsHaveTitle() { + for(StreamPreviewInfo i : result.resultList) { + assertEquals(i.title.isEmpty(), false); + } + } + + public void testItemsHaveUploader() { + for(StreamPreviewInfo i : result.resultList) { + assertEquals(i.uploader.isEmpty(), false); + } + } + + public void testItemsHaveRightDuration() { + for(StreamPreviewInfo i : result.resultList) { + assertTrue(i.duration >= 0); + } + } + + public void testItemsHaveRightThumbnail() { + for (StreamPreviewInfo i : result.resultList) { + assertTrue(i.thumbnail_url, i.thumbnail_url.contains("https://")); + } + } + + public void testItemsHaveRightVideoUrl() { + for (StreamPreviewInfo i : result.resultList) { + assertTrue(i.webpage_url, i.webpage_url.contains("https://")); + } + } + + public void testViewCount() { + /* + for(StreamPreviewInfo i : result.resultList) { + assertTrue(Long.toString(i.view_count), i.view_count != -1); + } + */ + // that specific link used for this test, there are no videos with less + // than 10.000 views, so we can test against that. + for(StreamPreviewInfo i : result.resultList) { + assertTrue(i.title + ": " + Long.toString(i.view_count), i.view_count >= 10000); + } + } + + public void testStreamType() { + for(StreamPreviewInfo i : result.resultList) { + assertTrue("not a livestream and not a video", + i.stream_type == AbstractVideoInfo.StreamType.VIDEO_STREAM || + i.stream_type == AbstractVideoInfo.StreamType.LIVE_STREAM); + } + } + + public void testIfSuggestionsAreReplied() { + assertEquals(!suggestionReply.isEmpty(), true); + } + + public void testIfSuggestionsAreValid() { + for(String s : suggestionReply) { + assertTrue(s, !s.isEmpty()); + } + } +} diff --git a/app/src/androidTest/java/org/schabi/newpipe/services/youtube/YoutubeStreamExtractorDefaultTest.java b/app/src/androidTest/java/org/schabi/newpipe/extractor/youtube/YoutubeStreamExtractorDefaultTest.java similarity index 66% rename from app/src/androidTest/java/org/schabi/newpipe/services/youtube/YoutubeStreamExtractorDefaultTest.java rename to app/src/androidTest/java/org/schabi/newpipe/extractor/youtube/YoutubeStreamExtractorDefaultTest.java index 2bf6dbf9d..41fc99c6b 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/services/youtube/YoutubeStreamExtractorDefaultTest.java +++ b/app/src/androidTest/java/org/schabi/newpipe/extractor/youtube/YoutubeStreamExtractorDefaultTest.java @@ -1,17 +1,19 @@ -package org.schabi.newpipe.services.youtube; +package org.schabi.newpipe.extractor.youtube; import android.test.AndroidTestCase; import org.schabi.newpipe.Downloader; -import org.schabi.newpipe.crawler.CrawlingException; -import org.schabi.newpipe.crawler.ParsingException; -import org.schabi.newpipe.crawler.services.youtube.YoutubeStreamExtractor; -import org.schabi.newpipe.crawler.VideoInfo; +import org.schabi.newpipe.extractor.AbstractVideoInfo; +import org.schabi.newpipe.extractor.ExtractionException; +import org.schabi.newpipe.extractor.ParsingException; +import org.schabi.newpipe.extractor.ServiceList; +import org.schabi.newpipe.extractor.StreamExtractor; +import org.schabi.newpipe.extractor.VideoStream; import java.io.IOException; /** - * Created by the-scrabi on 30.12.15. + * Created by Christian Schabesberger on 30.12.15. * * Copyright (C) Christian Schabesberger 2015 * YoutubeVideoExtractorDefault.java is part of NewPipe. @@ -31,15 +33,11 @@ import java.io.IOException; */ public class YoutubeStreamExtractorDefaultTest extends AndroidTestCase { - private YoutubeStreamExtractor extractor; + private StreamExtractor extractor; - public void setUp() throws IOException, CrawlingException { - /* some anonymus video test - extractor = new YoutubeStreamExtractor("https://www.youtube.com/watch?v=FmG385_uUys", - new Downloader()); */ - /* some vevo video (suggested to test against) */ - extractor = new YoutubeStreamExtractor("https://www.youtube.com/watch?v=YQHsXMglC9A", - new Downloader()); + public void setUp() throws IOException, ExtractionException { + extractor = ServiceList.getService("Youtube") + .getExtractorInstance("https://www.youtube.com/watch?v=YQHsXMglC9A", new Downloader()); } public void testGetInvalidTimeStamp() throws ParsingException { @@ -47,9 +45,10 @@ public class YoutubeStreamExtractorDefaultTest extends AndroidTestCase { extractor.getTimeStamp() <= 0); } - public void testGetValidTimeStamp() throws CrawlingException, IOException { - YoutubeStreamExtractor extractor = - new YoutubeStreamExtractor("https://youtu.be/FmG385_uUys?t=174", new Downloader()); + public void testGetValidTimeStamp() throws ExtractionException, IOException { + StreamExtractor extractor = + ServiceList.getService("Youtube") + .getExtractorInstance("https://youtu.be/FmG385_uUys?t=174", new Downloader()); assertTrue(Integer.toString(extractor.getTimeStamp()), extractor.getTimeStamp() == 174); } @@ -70,8 +69,9 @@ public class YoutubeStreamExtractorDefaultTest extends AndroidTestCase { assertTrue(extractor.getLength() > 0); } - public void testGetViews() throws ParsingException { - assertTrue(extractor.getLength() > 0); + public void testGetViewCount() throws ParsingException { + assertTrue(Long.toString(extractor.getViewCount()), + extractor.getViewCount() > /* specific to that video */ 1224000074); } public void testGetUploadDate() throws ParsingException { @@ -93,7 +93,7 @@ public class YoutubeStreamExtractorDefaultTest extends AndroidTestCase { } public void testGetVideoStreams() throws ParsingException { - for(VideoInfo.VideoStream s : extractor.getVideoStreams()) { + for(VideoStream s : extractor.getVideoStreams()) { assertTrue(s.url, s.url.contains("https://")); assertTrue(s.resolution.length() > 0); @@ -102,8 +102,12 @@ public class YoutubeStreamExtractorDefaultTest extends AndroidTestCase { } } + public void testStreamType() throws ParsingException { + assertTrue(extractor.getStreamType() == AbstractVideoInfo.StreamType.VIDEO_STREAM); + } + public void testGetDashMpd() throws ParsingException { assertTrue(extractor.getDashMpdUrl(), - !extractor.getDashMpdUrl().isEmpty()); + extractor.getDashMpdUrl() != null || !extractor.getDashMpdUrl().isEmpty()); } } diff --git a/app/src/androidTest/java/org/schabi/newpipe/services/youtube/YoutubeStreamExtractorGemaTest.java b/app/src/androidTest/java/org/schabi/newpipe/extractor/youtube/YoutubeStreamExtractorGemaTest.java similarity index 73% rename from app/src/androidTest/java/org/schabi/newpipe/services/youtube/YoutubeStreamExtractorGemaTest.java rename to app/src/androidTest/java/org/schabi/newpipe/extractor/youtube/YoutubeStreamExtractorGemaTest.java index 9d3bf376a..13ce65a31 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/services/youtube/YoutubeStreamExtractorGemaTest.java +++ b/app/src/androidTest/java/org/schabi/newpipe/extractor/youtube/YoutubeStreamExtractorGemaTest.java @@ -1,15 +1,16 @@ -package org.schabi.newpipe.services.youtube; +package org.schabi.newpipe.extractor.youtube; import android.test.AndroidTestCase; import org.schabi.newpipe.Downloader; -import org.schabi.newpipe.crawler.CrawlingException; -import org.schabi.newpipe.crawler.services.youtube.YoutubeStreamExtractor; +import org.schabi.newpipe.extractor.ExtractionException; +import org.schabi.newpipe.extractor.ServiceList; +import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; import java.io.IOException; /** - * Created by the-scrabi on 30.12.15. + * Created by Christian Schabesberger on 30.12.15. * * Copyright (C) Christian Schabesberger 2015 * YoutubeVideoExtractorGema.java is part of NewPipe. @@ -35,12 +36,12 @@ public class YoutubeStreamExtractorGemaTest extends AndroidTestCase { // Deaktivate this Test Case bevore uploading it githup, otherwise CI will fail. private static final boolean testActive = false; - public void testGemaError() throws IOException, CrawlingException { + public void testGemaError() throws IOException, ExtractionException { if(testActive) { try { - new YoutubeStreamExtractor("https://www.youtube.com/watch?v=3O1_3zBUKM8", + ServiceList.getService("Youtube") + .getExtractorInstance("https://www.youtube.com/watch?v=3O1_3zBUKM8", new Downloader()); - assertTrue("Gema exception not thrown", false); } catch(YoutubeStreamExtractor.GemaException ge) { assertTrue(true); } diff --git a/app/src/androidTest/java/org/schabi/newpipe/extractor/youtube/YoutubeStreamExtractorRestrictedTest.java b/app/src/androidTest/java/org/schabi/newpipe/extractor/youtube/YoutubeStreamExtractorRestrictedTest.java new file mode 100644 index 000000000..37be22b73 --- /dev/null +++ b/app/src/androidTest/java/org/schabi/newpipe/extractor/youtube/YoutubeStreamExtractorRestrictedTest.java @@ -0,0 +1,87 @@ +package org.schabi.newpipe.extractor.youtube; + +import android.test.AndroidTestCase; + +import org.schabi.newpipe.Downloader; +import org.schabi.newpipe.extractor.ExtractionException; +import org.schabi.newpipe.extractor.ParsingException; +import org.schabi.newpipe.extractor.ServiceList; +import org.schabi.newpipe.extractor.StreamExtractor; +import org.schabi.newpipe.extractor.VideoStream; + +import java.io.IOException; + +public class YoutubeStreamExtractorRestrictedTest extends AndroidTestCase { + private StreamExtractor extractor; + + public void setUp() throws IOException, ExtractionException { + extractor = ServiceList.getService("Youtube") + .getExtractorInstance("https://www.youtube.com/watch?v=i6JTvzrpBy0", + new Downloader()); + } + + public void testGetInvalidTimeStamp() throws ParsingException { + assertTrue(Integer.toString(extractor.getTimeStamp()), + extractor.getTimeStamp() <= 0); + } + + public void testGetValidTimeStamp() throws ExtractionException, IOException { + StreamExtractor extractor=ServiceList.getService("Youtube") + .getExtractorInstance("https://youtu.be/FmG385_uUys?t=174", + new Downloader()); + assertTrue(Integer.toString(extractor.getTimeStamp()), + extractor.getTimeStamp() == 174); + } + + public void testGetAgeLimit() throws ParsingException { + assertTrue(extractor.getAgeLimit() == 18); + } + + public void testGetTitle() throws ParsingException { + assertTrue(!extractor.getTitle().isEmpty()); + } + + public void testGetDescription() throws ParsingException { + assertTrue(extractor.getDescription() != null); + } + + public void testGetUploader() throws ParsingException { + assertTrue(!extractor.getUploader().isEmpty()); + } + + public void testGetLength() throws ParsingException { + assertTrue(extractor.getLength() > 0); + } + + public void testGetViews() throws ParsingException { + assertTrue(extractor.getLength() > 0); + } + + public void testGetUploadDate() throws ParsingException { + assertTrue(extractor.getUploadDate().length() > 0); + } + + public void testGetThumbnailUrl() throws ParsingException { + assertTrue(extractor.getThumbnailUrl(), + extractor.getThumbnailUrl().contains("https://")); + } + + public void testGetUploaderThumbnailUrl() throws ParsingException { + assertTrue(extractor.getUploaderThumbnailUrl(), + extractor.getUploaderThumbnailUrl().contains("https://")); + } + + public void testGetAudioStreams() throws ParsingException { + assertTrue(!extractor.getAudioStreams().isEmpty()); + } + + public void testGetVideoStreams() throws ParsingException { + for(VideoStream s : extractor.getVideoStreams()) { + assertTrue(s.url, + s.url.contains("https://")); + assertTrue(s.resolution.length() > 0); + assertTrue(Integer.toString(s.format), + 0 <= s.format && s.format <= 4); + } + } +} diff --git a/app/src/androidTest/java/org/schabi/newpipe/extractor/youtube/YoutubestreamExtractorLiveStreamTest.java b/app/src/androidTest/java/org/schabi/newpipe/extractor/youtube/YoutubestreamExtractorLiveStreamTest.java new file mode 100644 index 000000000..0394bbb27 --- /dev/null +++ b/app/src/androidTest/java/org/schabi/newpipe/extractor/youtube/YoutubestreamExtractorLiveStreamTest.java @@ -0,0 +1,52 @@ +package org.schabi.newpipe.extractor.youtube; + +import android.test.AndroidTestCase; + +import org.schabi.newpipe.Downloader; +import org.schabi.newpipe.extractor.AbstractVideoInfo; +import org.schabi.newpipe.extractor.ExtractionException; +import org.schabi.newpipe.extractor.ParsingException; +import org.schabi.newpipe.extractor.ServiceList; +import org.schabi.newpipe.extractor.StreamExtractor; + +import java.io.IOException; + +/** + * Created by Christian Schabesberger on 11.03.16. + * + * Copyright (C) Christian Schabesberger 2016 + * YoutubestreamExtractorLiveStreamTest.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 . + */ + + +public class YoutubestreamExtractorLiveStreamTest extends AndroidTestCase { + + private StreamExtractor extractor; + + public void setUp() throws IOException, ExtractionException { + //todo: make the extractor not throw over a livestream + /* + extractor = ServiceList.getService("Youtube") + .getExtractorInstance("https://www.youtube.com/watch?v=J0s6NjqdjLE", new Downloader()); + */ + } + + public void testStreamType() throws ParsingException { + assertTrue(true); + // assertTrue(extractor.getStreamType() == AbstractVideoInfo.StreamType.LIVE_STREAM); + } +} + diff --git a/app/src/androidTest/java/org/schabi/newpipe/services/youtube/YoutubeSearchEngineTest.java b/app/src/androidTest/java/org/schabi/newpipe/services/youtube/YoutubeSearchEngineTest.java deleted file mode 100644 index dfd9fef23..000000000 --- a/app/src/androidTest/java/org/schabi/newpipe/services/youtube/YoutubeSearchEngineTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package org.schabi.newpipe.services.youtube; - -import android.test.AndroidTestCase; - -import org.schabi.newpipe.crawler.VideoPreviewInfo; -import org.schabi.newpipe.crawler.SearchEngine; -import org.schabi.newpipe.crawler.services.youtube.YoutubeSearchEngine; -import org.schabi.newpipe.Downloader; - -import java.util.ArrayList; - -/** - * Created by the-scrabi on 29.12.15. - * - * Copyright (C) Christian Schabesberger 2015 - * YoutubeSearchEngineTest.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 . - */ - -public class YoutubeSearchEngineTest extends AndroidTestCase { - private SearchEngine.Result result; - private ArrayList suggestionReply; - - @Override - public void setUp() throws Exception{ - super.setUp(); - SearchEngine engine = new YoutubeSearchEngine(); - - result = engine.search("https://www.youtube.com/results?search_query=bla", - 0, "de", new Downloader()); - suggestionReply = engine.suggestionList("hello", new Downloader()); - } - - public void testIfNoErrorOccur() { - assertEquals(result.errorMessage, ""); - } - - public void testIfListIsNotEmpty() { - assertEquals(result.resultList.size() > 0, true); - } - - public void testItemsHaveTitle() { - for(VideoPreviewInfo i : result.resultList) { - assertEquals(i.title.isEmpty(), false); - } - } - - public void testItemsHaveUploader() { - for(VideoPreviewInfo i : result.resultList) { - assertEquals(i.uploader.isEmpty(), false); - } - } - - public void testItemsHaveRightDuration() { - for(VideoPreviewInfo i : result.resultList) { - assertTrue(i.duration, i.duration.contains(":")); - } - } - - public void testItemsHaveRightThumbnail() { - for (VideoPreviewInfo i : result.resultList) { - assertTrue(i.thumbnail_url, i.thumbnail_url.contains("https://")); - } - } - - public void testItemsHaveRightVideoUrl() { - for (VideoPreviewInfo i : result.resultList) { - assertTrue(i.webpage_url, i.webpage_url.contains("https://")); - } - } - - public void testIfSuggestionsAreReplied() { - assertEquals(suggestionReply.size() > 0, true); - } - - public void testIfSuggestionsAreValid() { - for(String s : suggestionReply) { - assertTrue(s, !s.isEmpty()); - } - } -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e30920404..a4ffff035 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,23 +1,24 @@ - - - - + package="org.schabi.newpipe"> + + + + + + + android:label="@string/app_name"> @@ -27,9 +28,7 @@ + android:theme="@style/AppTheme"> @@ -49,6 +48,7 @@ + @@ -75,20 +75,40 @@ - + tools:ignore="UnusedAttribute"/> + + + + + + + + + + + + + - + android:label="@string/settings_activity_title" /> + + diff --git a/app/src/main/java/org/schabi/newpipe/ActionBarHandler.java b/app/src/main/java/org/schabi/newpipe/ActionBarHandler.java index 3dee3dfe6..ab5074f3b 100644 --- a/app/src/main/java/org/schabi/newpipe/ActionBarHandler.java +++ b/app/src/main/java/org/schabi/newpipe/ActionBarHandler.java @@ -1,24 +1,19 @@ package org.schabi.newpipe; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.Bundle; import android.preference.PreferenceManager; import android.support.v7.app.ActionBar; -import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.widget.ArrayAdapter; -import android.widget.Toast; -import org.schabi.newpipe.crawler.MediaFormat; -import org.schabi.newpipe.crawler.VideoInfo; +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.StreamInfo; +import org.schabi.newpipe.extractor.VideoStream; import java.util.List; @@ -51,13 +46,16 @@ class ActionBarHandler { private SharedPreferences defaultPreferences = null; + private Menu menu; + // Only callbacks are listed here, there are more actions which don't need a callback. // those are edited directly. Typically VideoItemDetailFragment will implement those callbacks. - private OnActionListener onShareListener; - private OnActionListener onOpenInBrowserListener; - private OnActionListener onDownloadListener; - private OnActionListener onPlayWithKodiListener; - private OnActionListener onPlayAudioListener; + private OnActionListener onShareListener = null; + private OnActionListener onOpenInBrowserListener = null; + private OnActionListener onDownloadListener = null; + private OnActionListener onPlayWithKodiListener = null; + private OnActionListener onPlayAudioListener = null; + // Triggered when a stream related action is triggered. public interface OnActionListener { @@ -78,7 +76,7 @@ class ActionBarHandler { } } - public void setupStreamList(final List videoStreams) { + public void setupStreamList(final List videoStreams) { if (activity != null) { selectedVideoStream = 0; @@ -86,7 +84,7 @@ class ActionBarHandler { // this array will be shown in the dropdown menu for selecting the stream/resolution. String[] itemArray = new String[videoStreams.size()]; for (int i = 0; i < videoStreams.size(); i++) { - VideoInfo.VideoStream item = videoStreams.get(i); + VideoStream item = videoStreams.get(i); itemArray[i] = MediaFormat.getNameById(item.format) + " " + item.resolution; } int defaultResolution = getDefaultResolution(videoStreams); @@ -111,13 +109,13 @@ class ActionBarHandler { } - private int getDefaultResolution(final List videoStreams) { + private int getDefaultResolution(final List videoStreams) { String defaultResolution = defaultPreferences .getString(activity.getString(R.string.default_resolution_key), activity.getString(R.string.default_resolution_value)); for (int i = 0; i < videoStreams.size(); i++) { - VideoInfo.VideoStream item = videoStreams.get(i); + VideoStream item = videoStreams.get(i); if (defaultResolution.equals(item.resolution)) { return i; } @@ -128,15 +126,15 @@ class ActionBarHandler { } public void setupMenu(Menu menu, MenuInflater inflater) { + this.menu = menu; + // CAUTION set item properties programmatically otherwise it would not be accepted by // appcompat itemsinflater.inflate(R.menu.videoitem_detail, menu); defaultPreferences = PreferenceManager.getDefaultSharedPreferences(activity); - inflater.inflate(R.menu.videoitem_detail, menu); - MenuItem castItem = menu.findItem(R.id.action_play_with_kodi); - castItem.setVisible(defaultPreferences + showPlayWithKodiAction(defaultPreferences .getBoolean(activity.getString(R.string.show_play_with_kodi_key), false)); } @@ -151,15 +149,21 @@ class ActionBarHandler { intent.setType("text/plain"); activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title))); */ - onShareListener.onActionSelected(selectedVideoStream); + if(onShareListener != null) { + onShareListener.onActionSelected(selectedVideoStream); + } return true; } case R.id.menu_item_openInBrowser: { - onOpenInBrowserListener.onActionSelected(selectedVideoStream); + if(onOpenInBrowserListener != null) { + onOpenInBrowserListener.onActionSelected(selectedVideoStream); + } } return true; case R.id.menu_item_download: - onDownloadListener.onActionSelected(selectedVideoStream); + if(onDownloadListener != null) { + onDownloadListener.onActionSelected(selectedVideoStream); + } return true; case R.id.action_settings: { Intent intent = new Intent(activity, SettingsActivity.class); @@ -167,10 +171,14 @@ class ActionBarHandler { return true; } case R.id.action_play_with_kodi: - onPlayWithKodiListener.onActionSelected(selectedVideoStream); + if(onPlayWithKodiListener != null) { + onPlayWithKodiListener.onActionSelected(selectedVideoStream); + } return true; case R.id.menu_item_play_audio: - onPlayAudioListener.onActionSelected(selectedVideoStream); + if(onPlayAudioListener != null) { + onPlayAudioListener.onActionSelected(selectedVideoStream); + } return true; default: Log.e(TAG, "Menu Item not known"); @@ -201,4 +209,16 @@ class ActionBarHandler { public void setOnPlayAudioListener(OnActionListener listener) { onPlayAudioListener = listener; } + + public void showAudioAction(boolean visible) { + menu.findItem(R.id.menu_item_play_audio).setVisible(visible); + } + + public void showDownloadAction(boolean visible) { + menu.findItem(R.id.menu_item_download).setVisible(visible); + } + + public void showPlayWithKodiAction(boolean visible) { + menu.findItem(R.id.action_play_with_kodi).setVisible(visible); + } } diff --git a/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java b/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java index b09652a6c..4ea6ca657 100644 --- a/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java +++ b/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java @@ -22,10 +22,12 @@ package org.schabi.newpipe; import android.graphics.Bitmap; +import java.util.List; + /** * Singleton: * Used to send data between certain Activity/Services within the same process. - * This can be considered as hack inside the Android universe. **/ + * This can be considered as an ugly hack inside the Android universe. **/ public class ActivityCommunicator { private static ActivityCommunicator activityCommunicator = null; @@ -39,4 +41,9 @@ public class ActivityCommunicator { // Thumbnail send from VideoItemDetailFragment to BackgroundPlayer public volatile Bitmap backgroundPlayerThumbnail; + + // Sent from any activity to ErrorActivity. + public volatile List errorList; + public volatile Class returnActivity; + public volatile ErrorActivity.ErrorInfo errorInfo; } diff --git a/app/src/main/java/org/schabi/newpipe/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/DownloadDialog.java index 79d24823c..b528dfef3 100644 --- a/app/src/main/java/org/schabi/newpipe/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/DownloadDialog.java @@ -1,18 +1,13 @@ package org.schabi.newpipe; import android.Manifest; -import android.app.Activity; import android.app.Dialog; import android.app.DownloadManager; -import android.app.Notification; import android.content.Context; import android.content.DialogInterface; -import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; -import android.os.Environment; -import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.app.DialogFragment; @@ -63,74 +58,68 @@ public class DownloadDialog extends DialogFragment { if(ContextCompat.checkSelfPermission(this.getContext(),Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED) ActivityCompat.requestPermissions(getActivity(),new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},0); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(R.string.download_dialog_title) - .setItems(R.array.download_options, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Context context = getActivity(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - String suffix = ""; - String title = arguments.getString(TITLE); - String url = ""; - File downloadDir = NewPipeSettings.getDownloadFolder(); - switch(which) { - case 0: // Video - suffix = arguments.getString(FILE_SUFFIX_VIDEO); - url = arguments.getString(VIDEO_URL); - downloadDir = NewPipeSettings.getVideoDownloadFolder(context); - break; - case 1: - suffix = arguments.getString(FILE_SUFFIX_AUDIO); - url = arguments.getString(AUDIO_URL); - downloadDir = NewPipeSettings.getAudioDownloadFolder(context); - break; - default: - Log.d(TAG, "lolz"); - } - if(!downloadDir.exists()) { - //attempt to create directory - boolean mkdir = downloadDir.mkdirs(); - if(!mkdir && !downloadDir.isDirectory()) { - String message = context.getString(R.string.err_dir_create,downloadDir.toString()); - Log.e(TAG, message); - Toast.makeText(context,message , Toast.LENGTH_LONG).show(); + builder.setTitle(R.string.download_dialog_title); - return; - } - String message = context.getString(R.string.info_dir_created,downloadDir.toString()); - Log.e(TAG, message); - Toast.makeText(context,message , Toast.LENGTH_LONG).show(); - } - - File saveFilePath = new File(downloadDir,createFileName(title) + suffix); - - long id = 0; - if (App.isUsingTor()) { - // if using Tor, do not use DownloadManager because the proxy cannot be set - FileDownloader.downloadFile(getContext(), url, saveFilePath, title); - } else { - DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - DownloadManager.Request request = new DownloadManager.Request( - Uri.parse(url)); - request.setDestinationUri(Uri.fromFile(saveFilePath)); - request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); - - request.setTitle(title); - request.setDescription("'" + url + - "' => '" + saveFilePath + "'"); - request.allowScanningByMediaScanner(); - - try { - id = dm.enqueue(request); - } catch (Exception e) { - e.printStackTrace(); - } - } - - Log.i(TAG,"Started downloading '" + url + - "' => '" + saveFilePath + "' #" + id); + // If no audio stream available + if(arguments.getString(AUDIO_URL) == null) { + builder.setItems(R.array.download_options_no_audio, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Context context = getActivity(); + String title = arguments.getString(TITLE); + switch (which) { + case 0: // Video + download(arguments.getString(VIDEO_URL), + title, + arguments.getString(FILE_SUFFIX_VIDEO), context); + break; + default: + Log.d(TAG, "lolz"); } - }); + } + }); + // If no video stream available + } else if(arguments.getString(VIDEO_URL) == null) { + builder.setItems(R.array.download_options_no_video, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Context context = getActivity(); + String title = arguments.getString(TITLE); + switch (which) { + case 0: // Audio + download(arguments.getString(AUDIO_URL), + title, + arguments.getString(FILE_SUFFIX_AUDIO), context); + break; + default: + Log.d(TAG, "lolz"); + } + } + }); + //if both streams ar available + } else { + builder.setItems(R.array.download_options, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Context context = getActivity(); + String title = arguments.getString(TITLE); + switch (which) { + case 0: // Video + download(arguments.getString(VIDEO_URL), + title, + arguments.getString(FILE_SUFFIX_VIDEO), context); + break; + case 1: + download(arguments.getString(AUDIO_URL), + title, + arguments.getString(FILE_SUFFIX_AUDIO), context); + break; + default: + Log.d(TAG, "lolz"); + } + } + }); + } return builder.create(); } @@ -141,7 +130,7 @@ public class DownloadDialog extends DialogFragment { private String createFileName(String fName) { // from http://eng-przemelek.blogspot.de/2009/07/how-to-create-valid-file-name.html - List forbiddenCharsPatterns = new ArrayList (); + List forbiddenCharsPatterns = new ArrayList<> (); forbiddenCharsPatterns.add("[:]+"); // Mac OS, but it looks that also Windows XP forbiddenCharsPatterns.add("[\\*\"/\\\\\\[\\]\\:\\;\\|\\=\\,]+"); // Windows forbiddenCharsPatterns.add("[^\\w\\d\\.]+"); // last chance... only latin letters and digits @@ -151,4 +140,51 @@ public class DownloadDialog extends DialogFragment { } return nameToTest; } + + private void download(String url, String title, String fileSuffix, Context context) { + File downloadDir = NewPipeSettings.getDownloadFolder(); + + if(!downloadDir.exists()) { + //attempt to create directory + boolean mkdir = downloadDir.mkdirs(); + if(!mkdir && !downloadDir.isDirectory()) { + String message = context.getString(R.string.err_dir_create,downloadDir.toString()); + Log.e(TAG, message); + Toast.makeText(context,message , Toast.LENGTH_LONG).show(); + + return; + } + String message = context.getString(R.string.info_dir_created,downloadDir.toString()); + Log.e(TAG, message); + Toast.makeText(context,message , Toast.LENGTH_LONG).show(); + } + + File saveFilePath = new File(downloadDir,createFileName(title) + fileSuffix); + + long id = 0; + if (App.isUsingTor()) { + // if using Tor, do not use DownloadManager because the proxy cannot be set + FileDownloader.downloadFile(getContext(), url, saveFilePath, title); + } else { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + DownloadManager.Request request = new DownloadManager.Request( + Uri.parse(url)); + request.setDestinationUri(Uri.fromFile(saveFilePath)); + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + + request.setTitle(title); + request.setDescription("'" + url + + "' => '" + saveFilePath + "'"); + request.allowScanningByMediaScanner(); + + try { + id = dm.enqueue(request); + } catch (Exception e) { + e.printStackTrace(); + } + } + + Log.i(TAG,"Started downloading '" + url + + "' => '" + saveFilePath + "' #" + id); + } } diff --git a/app/src/main/java/org/schabi/newpipe/Downloader.java b/app/src/main/java/org/schabi/newpipe/Downloader.java index 80f1d0dd3..607eafb6e 100644 --- a/app/src/main/java/org/schabi/newpipe/Downloader.java +++ b/app/src/main/java/org/schabi/newpipe/Downloader.java @@ -30,7 +30,7 @@ import info.guardianproject.netcipher.NetCipher; * along with NewPipe. If not, see . */ -public class Downloader implements org.schabi.newpipe.crawler.Downloader { +public class Downloader implements org.schabi.newpipe.extractor.Downloader { private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0"; diff --git a/app/src/main/java/org/schabi/newpipe/ErrorActivity.java b/app/src/main/java/org/schabi/newpipe/ErrorActivity.java new file mode 100644 index 000000000..fbb76012d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ErrorActivity.java @@ -0,0 +1,404 @@ + + +package org.schabi.newpipe; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.preference.PreferenceManager; +import android.support.design.widget.Snackbar; +import android.support.v4.app.NavUtils; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import org.apache.commons.lang.exception.ExceptionUtils; +import org.json.JSONArray; +import org.json.JSONObject; +import org.schabi.newpipe.extractor.Parser; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; +import java.util.Vector; + +/** + * Created by Christian Schabesberger on 24.10.15. + *

+ * Copyright (C) Christian Schabesberger 2016 + * ErrorActivity.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 . + */ + +public class ErrorActivity extends AppCompatActivity { + public static class ErrorInfo { + public int userAction; + public String request; + public String serviceName; + public int message; + + public static ErrorInfo make(int userAction, String serviceName, String request, int message) { + ErrorInfo info = new ErrorInfo(); + info.userAction = userAction; + info.serviceName = serviceName; + info.request = request; + info.message = message; + return info; + } + } + + public static final String TAG = ErrorActivity.class.toString(); + public static final int SEARCHED = 0; + public static final int REQUESTED_STREAM = 1; + public static final int GET_SUGGESTIONS = 2; + public static final int SOMETHING_ELSE = 3; + public static final int USER_REPORT = 4; + public static final String SEARCHED_STRING = "searched"; + public static final String REQUESTED_STREAM_STRING = "requested stream"; + public static final String GET_SUGGESTIONS_STRING = "get suggestions"; + public static final String SOMETHING_ELSE_STRING = "something"; + public static final String USER_REPORT_STRING = "user report"; + + public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"; + public static final String ERROR_EMAIL_SUBJECT = "Exception in NewPipe " + BuildConfig.VERSION_NAME; + + private List errorList; + private ErrorInfo errorInfo; + private Class returnActivity; + private String currentTimeStamp; + private String globIpRange; + Thread globIpRangeThread = null; + + // views + private TextView errorView; + private EditText userCommentBox; + private Button reportButton; + private TextView infoView; + private TextView errorMessageView; + + public static void reportError(final Context context, final List el, + final Class returnAcitivty, View rootView, final ErrorInfo errorInfo) { + + if (rootView != null) { + Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG) + .setActionTextColor(Color.YELLOW) + .setAction(R.string.error_snackbar_action, new View.OnClickListener() { + @Override + public void onClick(View v) { + ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); + ac.errorList = el; + ac.returnActivity = returnAcitivty; + ac.errorInfo = errorInfo; + Intent intent = new Intent(context, ErrorActivity.class); + context.startActivity(intent); + } + }).show(); + } else { + ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); + ac.errorList = el; + ac.returnActivity = returnAcitivty; + ac.errorInfo = errorInfo; + Intent intent = new Intent(context, ErrorActivity.class); + context.startActivity(intent); + } + } + + public static void reportError(final Context context, final Exception e, + final Class returnAcitivty, View rootView, final ErrorInfo errorInfo) { + List el = null; + if(e != null) { + el = new Vector<>(); + el.add(e); + } + reportError(context, el, returnAcitivty, rootView, errorInfo); + } + + // async call + public static void reportError(Handler handler, final Context context, final Exception e, + final Class returnAcitivty, final View rootView, final ErrorInfo errorInfo) { + + List el = null; + if(e != null) { + el = new Vector<>(); + el.add(e); + } + reportError(handler, context, el, returnAcitivty, rootView, errorInfo); + } + + // async call + public static void reportError(Handler handler, final Context context, final List el, + final Class returnAcitivty, final View rootView, final ErrorInfo errorInfo) { + handler.post(new Runnable() { + @Override + public void run() { + reportError(context, el, returnAcitivty, rootView, errorInfo); + } + }); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_error); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); + errorList = ac.errorList; + returnActivity = ac.returnActivity; + errorInfo = ac.errorInfo; + + reportButton = (Button) findViewById(R.id.errorReportButton); + userCommentBox = (EditText) findViewById(R.id.errorCommentBox); + errorView = (TextView) findViewById(R.id.errorView); + infoView = (TextView) findViewById(R.id.errorInfosView); + errorMessageView = (TextView) findViewById(R.id.errorMessageView); + + errorView.setText(formErrorText(errorList)); + + //importand add gurumeditaion + addGuruMeditaion(); + currentTimeStamp = getCurrentTimeStamp(); + buildInfo(errorInfo); + + reportButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + Intent intent = new Intent(Intent.ACTION_SENDTO); + intent.setData(Uri.parse("mailto:" + ERROR_EMAIL_ADDRESS)) + .putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT) + .putExtra(Intent.EXTRA_TEXT, buildJson()); + + startActivity(Intent.createChooser(intent, "Send Email")); + } + }); + reportButton.setEnabled(false); + + globIpRangeThread = new Thread(new IpRagneRequester()); + globIpRangeThread.start(); + + if(errorInfo.message != 0) { + errorMessageView.setText(errorInfo.message); + } else { + errorMessageView.setVisibility(View.GONE); + findViewById(R.id.messageWhatHappenedView).setVisibility(View.GONE); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.error_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + switch (id) { + case android.R.id.home: + goToReturnActivity(); + break; + case R.id.menu_item_share_error: { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_TEXT, buildJson()); + intent.setType("text/plain"); + startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title))); + } + break; + } + return false; + } + + private String formErrorText(List el) { + String text = ""; + if(el != null) { + for (Exception e : el) { + text += "-------------------------------------\n" + + ExceptionUtils.getStackTrace(e); + } + } + text += "-------------------------------------"; + return text; + } + + private void goToReturnActivity() { + if (returnActivity == null) { + super.onBackPressed(); + } else { + Intent intent; + if (returnActivity != null && + returnActivity.isAssignableFrom(Activity.class)) { + intent = new Intent(this, returnActivity); + } else { + intent = new Intent(this, VideoItemListActivity.class); + } + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + NavUtils.navigateUpTo(this, intent); + } + } + + private void buildInfo(ErrorInfo info) { + TextView infoLabelView = (TextView) findViewById(R.id.errorInfoLabelsView); + TextView infoView = (TextView) findViewById(R.id.errorInfosView); + String text = ""; + + infoLabelView.setText(getString(R.string.info_labels).replace("\\n", "\n")); + + text += getUserActionString(info.userAction) + + "\n" + info.request + + "\n" + getContentLangString() + + "\n" + info.serviceName + + "\n" + currentTimeStamp + + "\n" + BuildConfig.VERSION_NAME + + "\n" + getOsString(); + + infoView.setText(text); + } + + private String buildJson() { + JSONObject errorObject = new JSONObject(); + + try { + errorObject.put("user_action", getUserActionString(errorInfo.userAction)) + .put("request", errorInfo.request) + .put("content_language", getContentLangString()) + .put("service", errorInfo.serviceName) + .put("version", BuildConfig.VERSION_NAME) + .put("os", getOsString()) + .put("time", currentTimeStamp) + .put("ip_range", globIpRange); + + JSONArray exceptionArray = new JSONArray(); + if(errorList != null) { + for (Exception e : errorList) { + exceptionArray.put(ExceptionUtils.getStackTrace(e)); + } + } + + errorObject.put("exceptions", exceptionArray); + errorObject.put("user_comment", userCommentBox.getText().toString()); + + return errorObject.toString(3); + } catch (Exception e) { + Log.e(TAG, "Error while erroring: Could not build json"); + e.printStackTrace(); + } + + return ""; + } + + private String getUserActionString(int userAction) { + switch (userAction) { + case REQUESTED_STREAM: + return REQUESTED_STREAM_STRING; + case SEARCHED: + return SEARCHED_STRING; + case GET_SUGGESTIONS: + return GET_SUGGESTIONS_STRING; + case SOMETHING_ELSE: + return SOMETHING_ELSE_STRING; + case USER_REPORT: + return USER_REPORT_STRING; + default: + return "Your description is in another castle."; + } + } + + private String getContentLangString() { + return PreferenceManager.getDefaultSharedPreferences(this) + .getString(this.getString(R.string.search_language_key), "none"); + } + + private String getOsString() { + String osBase = Build.VERSION.SDK_INT >= 23 ? Build.VERSION.BASE_OS : "Android"; + return System.getProperty("os.name") + + " " + (osBase.isEmpty() ? "Android" : osBase) + + " " + Build.VERSION.RELEASE + + " - " + Integer.toString(Build.VERSION.SDK_INT); + } + + private void addGuruMeditaion() { + //just an easter egg + TextView sorryView = (TextView) findViewById(R.id.errorSorryView); + String text = sorryView.getText().toString(); + text += "\n" + getString(R.string.guru_meditation); + sorryView.setText(text); + } + + @Override + public void onBackPressed() { + //super.onBackPressed(); + goToReturnActivity(); + } + + public String getCurrentTimeStamp() { + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + df.setTimeZone(TimeZone.getTimeZone("GMT")); + return df.format(new Date()); + } + + private class IpRagneRequester implements Runnable { + Handler h = new Handler(); + public void run() { + String ipRange = "none"; + try { + Downloader dl = new Downloader(); + String ip = dl.download("https://ifcfg.me/ip"); + + ipRange = Parser.matchGroup1("([0-9]*\\.[0-9]*\\.)[0-9]*\\.[0-9]*", ip) + + "0.0"; + } catch(Exception e) { + Log.d(TAG, "Error while error: could not get iprange"); + e.printStackTrace(); + } finally { + h.post(new IpRageReturnRunnable(ipRange)); + } + } + } + + + + private class IpRageReturnRunnable implements Runnable { + String ipRange; + public IpRageReturnRunnable(String ipRange) { + this.ipRange = ipRange; + } + public void run() { + globIpRange = ipRange; + if(infoView != null) { + String text = infoView.getText().toString(); + text += "\n" + globIpRange; + infoView.setText(text); + reportButton.setEnabled(true); + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/Localization.java b/app/src/main/java/org/schabi/newpipe/Localization.java index 0f448777e..2fa461129 100644 --- a/app/src/main/java/org/schabi/newpipe/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/Localization.java @@ -34,6 +34,9 @@ import java.util.Locale; public class Localization { + private Localization() { + } + public static Locale getPreferredLocale(Context context) { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/NewPipeSettings.java index 15fa4e899..1dea05f36 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/NewPipeSettings.java @@ -33,6 +33,10 @@ import java.io.File; * Helper for global settings */ public class NewPipeSettings { + + private NewPipeSettings() { + } + public static void initSettings(Context context) { PreferenceManager.setDefaultValues(context, R.xml.settings, false); getVideoDownloadFolder(context); diff --git a/app/src/main/java/org/schabi/newpipe/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/SettingsActivity.java index bad716bb3..04b084154 100644 --- a/app/src/main/java/org/schabi/newpipe/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/SettingsActivity.java @@ -119,18 +119,20 @@ public class SettingsActivity extends PreferenceActivity { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { Activity a = getActivity(); - updateSummary(); + if(a != null) { + updateSummary(); - if (defaultPreferences.getBoolean(USE_TOR_KEY, false)) { - if (OrbotHelper.isOrbotInstalled(a)) { - App.configureTor(true); - OrbotHelper.requestStartTor(a); + if (defaultPreferences.getBoolean(USE_TOR_KEY, false)) { + if (OrbotHelper.isOrbotInstalled(a)) { + App.configureTor(true); + OrbotHelper.requestStartTor(a); + } else { + Intent intent = OrbotHelper.getOrbotInstallIntent(a); + a.startActivityForResult(intent, REQUEST_INSTALL_ORBOT); + } } else { - Intent intent = OrbotHelper.getOrbotInstallIntent(a); - a.startActivityForResult(intent, REQUEST_INSTALL_ORBOT); + App.configureTor(false); } - } else { - App.configureTor(false); } } }; diff --git a/app/src/main/java/org/schabi/newpipe/SuggestionListAdapter.java b/app/src/main/java/org/schabi/newpipe/SuggestionListAdapter.java new file mode 100644 index 000000000..646d5ed18 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/SuggestionListAdapter.java @@ -0,0 +1,82 @@ +package org.schabi.newpipe; + +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.util.ArrayList; + +/** + * Created by Madiyar on 23.02.2016. + * + * Copyright (C) Christian Schabesberger 2015 + * SuggestionListAdapter.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 . + */ + +public class SuggestionListAdapter extends CursorAdapter { + + private String[] columns = new String[]{"_id", "title"}; + + public SuggestionListAdapter(Context context) { + super(context, null, false); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + ViewHolder viewHolder; + + View view = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent, false); + viewHolder = new ViewHolder(); + viewHolder.suggestionTitle = (TextView) view.findViewById(android.R.id.text1); + view.setTag(viewHolder); + + + return view; + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + ViewHolder viewHolder = (ViewHolder) view.getTag(); + viewHolder.suggestionTitle.setText(cursor.getString(1)); + } + + + public void updateAdapter(ArrayList suggestions) { + MatrixCursor cursor = new MatrixCursor(columns); + int i = 0; + for (String s : suggestions) { + String[] temp = new String[2]; + temp[0] = Integer.toString(i); + temp[1] = s; + i++; + cursor.addRow(temp); + } + changeCursor(cursor); + } + + public String getSuggestion(int position) { + return ((Cursor) getItem(position)).getString(1); + } + + private class ViewHolder { + public TextView suggestionTitle; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/VideoInfoItemViewCreator.java b/app/src/main/java/org/schabi/newpipe/VideoInfoItemViewCreator.java index 07f631146..baeaef813 100644 --- a/app/src/main/java/org/schabi/newpipe/VideoInfoItemViewCreator.java +++ b/app/src/main/java/org/schabi/newpipe/VideoInfoItemViewCreator.java @@ -1,13 +1,14 @@ package org.schabi.newpipe; -import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import org.schabi.newpipe.crawler.VideoPreviewInfo; +import org.schabi.newpipe.extractor.AbstractVideoInfo; +import org.schabi.newpipe.extractor.StreamPreviewInfo; + import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; @@ -31,7 +32,7 @@ import com.nostra13.universalimageloader.core.ImageLoader; * along with NewPipe. If not, see . */ -class VideoInfoItemViewCreator { +public class VideoInfoItemViewCreator { private final LayoutInflater inflater; private ImageLoader imageLoader = ImageLoader.getInstance(); private DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().cacheInMemory(true).build(); @@ -40,8 +41,10 @@ class VideoInfoItemViewCreator { this.inflater = inflater; } - public View getViewFromVideoInfoItem(View convertView, ViewGroup parent, VideoPreviewInfo info, Context context) { + public View getViewFromVideoInfoItem(View convertView, ViewGroup parent, StreamPreviewInfo info) { ViewHolder holder; + + // generate holder if(convertView == null) { convertView = inflater.inflate(R.layout.video_item, parent, false); holder = new ViewHolder(); @@ -56,20 +59,43 @@ class VideoInfoItemViewCreator { holder = (ViewHolder) convertView.getTag(); } + // fill with information + + /* if(info.thumbnail == null) { holder.itemThumbnailView.setImageResource(R.drawable.dummy_thumbnail); } else { holder.itemThumbnailView.setImageBitmap(info.thumbnail); } + */ holder.itemVideoTitleView.setText(info.title); - holder.itemUploaderView.setText(info.uploader); - holder.itemDurationView.setText(info.duration); - holder.itemViewCountView.setText(shortViewCount(info.view_count)); + if(info.uploader != null && !info.uploader.isEmpty()) { + holder.itemUploaderView.setText(info.uploader); + } else { + holder.itemDurationView.setVisibility(View.INVISIBLE); + } + if(info.duration > 0) { + holder.itemDurationView.setText(getDurationString(info.duration)); + } else { + if(info.stream_type == AbstractVideoInfo.StreamType.LIVE_STREAM) { + holder.itemDurationView.setText(R.string.duration_live); + } else { + holder.itemDurationView.setVisibility(View.GONE); + } + } + if(info.view_count >= 0) { + holder.itemViewCountView.setText(shortViewCount(info.view_count)); + } else { + holder.itemViewCountView.setVisibility(View.GONE); + } if(!info.upload_date.isEmpty()) { - holder.itemUploadDateView.setText(info.upload_date+" • "); + holder.itemUploadDateView.setText(info.upload_date + " • "); } - imageLoader.displayImage(info.thumbnail_url, holder.itemThumbnailView, displayImageOptions); + holder.itemThumbnailView.setImageResource(R.drawable.dummy_thumbnail); + if(info.thumbnail_url != null && !info.thumbnail_url.isEmpty()) { + imageLoader.displayImage(info.thumbnail_url, holder.itemThumbnailView, displayImageOptions); + } return convertView; } @@ -79,16 +105,69 @@ class VideoInfoItemViewCreator { public TextView itemVideoTitleView, itemUploaderView, itemDurationView, itemUploadDateView, itemViewCountView; } - private String shortViewCount(Long view_count){ - if(view_count >= 1000000000){ - return Long.toString(view_count/1000000000)+"B views"; - }else if(view_count>=1000000){ - return Long.toString(view_count/1000000)+"M views"; - }else if(view_count>=1000){ - return Long.toString(view_count/1000)+"K views"; + private String shortViewCount(Long viewCount){ + if(viewCount >= 1000000000){ + return Long.toString(viewCount/1000000000)+"B views"; + }else if(viewCount>=1000000){ + return Long.toString(viewCount/1000000)+"M views"; + }else if(viewCount>=1000){ + return Long.toString(viewCount/1000)+"K views"; }else { - return Long.toString(view_count)+" views"; + return Long.toString(viewCount)+" views"; } } + public static String getDurationString(int duration) { + String output = ""; + int days = duration / (24 * 60 * 60); /* greater than a day */ + duration %= (24 * 60 * 60); + int hours = duration / (60 * 60); /* greater than an hour */ + duration %= (60 * 60); + int minutes = duration / 60; + int seconds = duration % 60; + + //handle days + if(days > 0) { + output = Integer.toString(days) + ":"; + } + // handle hours + if(hours > 0 || !output.isEmpty()) { + if(hours > 0) { + if(hours >= 10 || output.isEmpty()) { + output += Integer.toString(minutes); + } else { + output += "0" + Integer.toString(minutes); + } + } else { + output += "00"; + } + output += ":"; + } + //handle minutes + if(minutes > 0 || !output.isEmpty()) { + if(minutes > 0) { + if(minutes >= 10 || output.isEmpty()) { + output += Integer.toString(minutes); + } else { + output += "0" + Integer.toString(minutes); + } + } else { + output += "00"; + } + output += ":"; + } + + //handle seconds + if(output.isEmpty()) { + output += "0:"; + } + + if(seconds >= 10) { + output += Integer.toString(seconds); + } else { + output += "0" + Integer.toString(seconds); + } + + return output; + } } diff --git a/app/src/main/java/org/schabi/newpipe/VideoItemDetailActivity.java b/app/src/main/java/org/schabi/newpipe/VideoItemDetailActivity.java index 21b5bf326..9ba18bdcc 100644 --- a/app/src/main/java/org/schabi/newpipe/VideoItemDetailActivity.java +++ b/app/src/main/java/org/schabi/newpipe/VideoItemDetailActivity.java @@ -11,8 +11,8 @@ import android.view.Menu; import android.view.MenuItem; import android.widget.Toast; -import org.schabi.newpipe.crawler.ServiceList; -import org.schabi.newpipe.crawler.StreamingService; +import org.schabi.newpipe.extractor.ServiceList; +import org.schabi.newpipe.extractor.StreamingService; /** @@ -73,7 +73,7 @@ public class VideoItemDetailActivity extends AppCompatActivity { StreamingService[] serviceList = ServiceList.getServices(); //StreamExtractor videoExtractor = null; for (int i = 0; i < serviceList.length; i++) { - if (serviceList[i].getUrlIdHandler().acceptUrl(videoUrl)) { + if (serviceList[i].getUrlIdHandlerInstance().acceptUrl(videoUrl)) { arguments.putInt(VideoItemDetailFragment.STREAMING_SERVICE, i); currentStreamingService = i; //videoExtractor = ServiceList.getService(i).getExtractorInstance(); diff --git a/app/src/main/java/org/schabi/newpipe/VideoItemDetailFragment.java b/app/src/main/java/org/schabi/newpipe/VideoItemDetailFragment.java index c1437376b..81b51da75 100644 --- a/app/src/main/java/org/schabi/newpipe/VideoItemDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/VideoItemDetailFragment.java @@ -38,6 +38,7 @@ import android.widget.Toast; import java.io.IOException; +import com.google.android.exoplayer.util.Util; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.assist.FailReason; @@ -46,14 +47,20 @@ import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; import java.util.ArrayList; import java.util.Vector; -import org.schabi.newpipe.crawler.MediaFormat; -import org.schabi.newpipe.crawler.ParsingException; -import org.schabi.newpipe.crawler.ServiceList; -import org.schabi.newpipe.crawler.StreamExtractor; -import org.schabi.newpipe.crawler.VideoPreviewInfo; -import org.schabi.newpipe.crawler.StreamingService; -import org.schabi.newpipe.crawler.VideoInfo; -import org.schabi.newpipe.crawler.services.youtube.YoutubeStreamExtractor; + +import org.schabi.newpipe.extractor.AudioStream; +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.ParsingException; +import org.schabi.newpipe.extractor.ServiceList; +import org.schabi.newpipe.extractor.StreamExtractor; +import org.schabi.newpipe.extractor.StreamInfo; +import org.schabi.newpipe.extractor.StreamPreviewInfo; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.VideoStream; +import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; +import org.schabi.newpipe.player.BackgroundPlayer; +import org.schabi.newpipe.player.PlayVideoActivity; +import org.schabi.newpipe.player.ExoPlayerActivity; /** @@ -98,6 +105,7 @@ public class VideoItemDetailFragment extends Fragment { private Bitmap videoThumbnail; private View thumbnailWindowLayout; + //this only remains due to downwards compatibility private FloatingActionButton playVideoButton; private final Point initialThumbnailPos = new Point(0, 0); @@ -126,11 +134,34 @@ public class VideoItemDetailFragment extends Fragment { @Override public void run() { + StreamInfo streamInfo = null; try { streamExtractor = service.getExtractorInstance(videoUrl, new Downloader()); - VideoInfo videoInfo = VideoInfo.getVideoInfo(streamExtractor, new Downloader()); + streamInfo = StreamInfo.getVideoInfo(streamExtractor, new Downloader()); - h.post(new VideoResultReturnedRunnable(videoInfo)); + h.post(new VideoResultReturnedRunnable(streamInfo)); + + // look for errors during extraction + // this if statement only covers extra information. + // if these are not available or caused an error, they are just not available + // but don't render the stream information unusalbe. + if(streamInfo != null && + !streamInfo.errors.isEmpty()) { + Log.e(TAG, "OCCURRED ERRORS DURING EXTRACTION:"); + for (Exception e : streamInfo.errors) { + e.printStackTrace(); + Log.e(TAG, "------"); + } + + Activity a = getActivity(); + View rootView = a != null ? a.findViewById(R.id.videoitem_detail) : null; + ErrorActivity.reportError(h, getActivity(), + streamInfo.errors, null, rootView, + ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, + service.getServiceInfo().name, videoUrl, 0 /* no message for the user */)); + } + + // These errors render the stream information unusable. } catch (IOException e) { postNewErrorToast(h, R.string.network_error); e.printStackTrace(); @@ -146,6 +177,13 @@ public class VideoItemDetailFragment extends Fragment { onErrorBlockedByGema(); } }); + } catch(YoutubeStreamExtractor.LiveStreamException e) { + h.post(new Runnable() { + @Override + public void run() { + onNotSpecifiedContentErrorWithMessage(R.string.live_streams_not_supported); + } + }); } // ---------------------------------------- catch(StreamExtractor.ContentNotAvailableException e) { @@ -156,26 +194,67 @@ public class VideoItemDetailFragment extends Fragment { } }); e.printStackTrace(); + } catch(StreamInfo.StreamExctractException e) { + if(!streamInfo.errors.isEmpty()) { + // !!! if this case ever kicks in someone gets kicked out !!! + ErrorActivity.reportError(h, getActivity(), e, VideoItemListActivity.class, null, + ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, + service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream)); + } else { + ErrorActivity.reportError(h, getActivity(), streamInfo.errors, VideoItemListActivity.class, null, + ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, + service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream)); + } + h.post(new Runnable() { + @Override + public void run() { + getActivity().finish(); + } + }); + e.printStackTrace(); } catch (ParsingException e) { - postNewErrorToast(h, e.getMessage()); + ErrorActivity.reportError(h, getActivity(), e, VideoItemListActivity.class, null, + ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, + service.getServiceInfo().name, videoUrl, R.string.parsing_error)); + h.post(new Runnable() { + @Override + public void run() { + getActivity().finish(); + } + }); e.printStackTrace(); } catch(Exception e) { - postNewErrorToast(h, R.string.general_error); + ErrorActivity.reportError(h, getActivity(), e, VideoItemListActivity.class, null, + ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, + service.getServiceInfo().name, videoUrl, R.string.general_error)); + h.post(new Runnable() { + @Override + public void run() { + getActivity().finish(); + } + }); e.printStackTrace(); } } } private class VideoResultReturnedRunnable implements Runnable { - private final VideoInfo videoInfo; - public VideoResultReturnedRunnable(VideoInfo videoInfo) { - this.videoInfo = videoInfo; + private final StreamInfo streamInfo; + public VideoResultReturnedRunnable(StreamInfo streamInfo) { + this.streamInfo = streamInfo; } @Override public void run() { - //todo: fix expired thread error: - // If the thread calling this runnable is expired, the following function will crash. - updateInfo(videoInfo); + Activity a = getActivity(); + if(a != null) { + boolean showAgeRestrictedContent = PreferenceManager.getDefaultSharedPreferences(a) + .getBoolean(activity.getString(R.string.show_age_restricted_content), false); + if (streamInfo.age_limit == 0 || showAgeRestrictedContent) { + updateInfo(streamInfo); + } else { + onNotSpecifiedContentErrorWithMessage(R.string.video_is_age_restricted); + } + } } } @@ -185,8 +264,10 @@ public class VideoItemDetailFragment extends Fragment { @Override public void onLoadingFailed(String imageUri, View view, FailReason failReason) { - Toast.makeText(VideoItemDetailFragment.this.getActivity(), - R.string.could_not_load_thumbnails, Toast.LENGTH_LONG).show(); + if(getContext() != null) { + Toast.makeText(VideoItemDetailFragment.this.getActivity(), + R.string.could_not_load_thumbnails, Toast.LENGTH_LONG).show(); + } failReason.getCause().printStackTrace(); } @@ -197,7 +278,7 @@ public class VideoItemDetailFragment extends Fragment { public void onLoadingCancelled(String imageUri, View view) {} } - private void updateInfo(final VideoInfo info) { + private void updateInfo(final StreamInfo info) { try { Context c = getContext(); VideoInfoItemViewCreator videoItemViewCreator = @@ -223,16 +304,31 @@ public class VideoItemDetailFragment extends Fragment { Button backgroundButton = (Button) activity.findViewById(R.id.detailVideoThumbnailWindowBackgroundButton); View topView = activity.findViewById(R.id.detailTopView); - View nextVideoView = videoItemViewCreator - .getViewFromVideoInfoItem(null, nextVideoFrame, info.next_video, getContext()); + View nextVideoView = null; + if(info.next_video != null) { + nextVideoView = videoItemViewCreator + .getViewFromVideoInfoItem(null, nextVideoFrame, info.next_video); + } else { + activity.findViewById(R.id.detailNextVidButtonAndContentLayout).setVisibility(View.GONE); + activity.findViewById(R.id.detailNextVideoTitle).setVisibility(View.GONE); + activity.findViewById(R.id.detailNextVideoButton).setVisibility(View.GONE); + } progressBar.setVisibility(View.GONE); - nextVideoFrame.addView(nextVideoView); + if(nextVideoView != null) { + nextVideoFrame.addView(nextVideoView); + } initThumbnailViews(info, nextVideoFrame); textContentLayout.setVisibility(View.VISIBLE); - playVideoButton.setVisibility(View.VISIBLE); + if (android.os.Build.VERSION.SDK_INT < 18) { + playVideoButton.setVisibility(View.VISIBLE); + } else { + ImageView playArrowView = (ImageView) activity.findViewById(R.id.playArrowView); + playArrowView.setVisibility(View.VISIBLE); + } + if (!showNextVideoItem) { nextVideoRootFrame.setVisibility(View.GONE); similarTitle.setVisibility(View.GONE); @@ -258,20 +354,49 @@ public class VideoItemDetailFragment extends Fragment { } }); - uploaderView.setText(info.uploader); + // Since newpipe is designed to work even if certain information is not available, + // the UI has to react on missing information. videoTitleView.setText(info.title); - uploaderView.setText(info.uploader); - viewCountView.setText(Localization.localizeViewCount(info.view_count, c)); - thumbsUpView.setText(Localization.localizeNumber(info.like_count, c)); - thumbsDownView.setText(Localization.localizeNumber(info.dislike_count, c)); - uploadDateView.setText(Localization.localizeDate(info.upload_date, c)); - descriptionView.setText(Html.fromHtml(info.description)); + if(!info.uploader.isEmpty()) { + uploaderView.setText(info.uploader); + } else { + activity.findViewById(R.id.detailUploaderWrapView).setVisibility(View.GONE); + } + if(info.view_count >= 0) { + viewCountView.setText(Localization.localizeViewCount(info.view_count, c)); + } else { + viewCountView.setVisibility(View.GONE); + } + if(info.dislike_count >= 0) { + thumbsDownView.setText(Localization.localizeNumber(info.dislike_count, c)); + } else { + thumbsDownView.setVisibility(View.INVISIBLE); + activity.findViewById(R.id.detailThumbsDownImgView).setVisibility(View.GONE); + } + if(info.like_count >= 0) { + thumbsUpView.setText(Localization.localizeNumber(info.like_count, c)); + } else { + thumbsUpView.setVisibility(View.GONE); + activity.findViewById(R.id.detailThumbsUpImgView).setVisibility(View.GONE); + thumbsDownView.setVisibility(View.GONE); + activity.findViewById(R.id.detailThumbsDownImgView).setVisibility(View.GONE); + } + if(!info.upload_date.isEmpty()) { + uploadDateView.setText(Localization.localizeDate(info.upload_date, c)); + } else { + uploadDateView.setVisibility(View.GONE); + } + if(!info.description.isEmpty()) { + descriptionView.setText(Html.fromHtml(info.description)); + } else { + descriptionView.setVisibility(View.GONE); + } descriptionView.setMovementMethod(LinkMovementMethod.getInstance()); // parse streams - Vector streamsToUse = new Vector<>(); - for (VideoInfo.VideoStream i : info.video_streams) { + Vector streamsToUse = new Vector<>(); + for (VideoStream i : info.video_streams) { if (useStream(i, streamsToUse)) { streamsToUse.add(i); } @@ -292,18 +417,27 @@ public class VideoItemDetailFragment extends Fragment { }); textContentLayout.setVisibility(View.VISIBLE); - initSimilarVideos(info, videoItemViewCreator); + if(info.related_videos != null && !info.related_videos.isEmpty()) { + initSimilarVideos(info, videoItemViewCreator); + } else { + activity.findViewById(R.id.detailSimilarTitle).setVisibility(View.GONE); + activity.findViewById(R.id.similarVideosView).setVisibility(View.GONE); + } + + setupActionBarHandler(info); if(autoPlayEnabled) { playVideo(info); } - playVideoButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - playVideo(info); - } - }); + if (android.os.Build.VERSION.SDK_INT < 18) { + playVideoButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + playVideo(info); + } + }); + } backgroundButton.setOnClickListener(new View.OnClickListener() { @Override @@ -312,49 +446,56 @@ public class VideoItemDetailFragment extends Fragment { } }); - setupActionBarHandler(info); } catch (java.lang.NullPointerException e) { Log.w(TAG, "updateInfo(): Fragment closed before thread ended work... or else"); e.printStackTrace(); } } - private void initThumbnailViews(VideoInfo info, View nextVideoFrame) { + private void initThumbnailViews(StreamInfo info, View nextVideoFrame) { ImageView videoThumbnailView = (ImageView) activity.findViewById(R.id.detailThumbnailView); ImageView uploaderThumb = (ImageView) activity.findViewById(R.id.detailUploaderThumbnailView); ImageView nextVideoThumb = (ImageView) nextVideoFrame.findViewById(R.id.itemThumbnailView); - imageLoader.displayImage(info.thumbnail_url, videoThumbnailView, - displayImageOptions, new ImageLoadingListener() { - @Override - public void onLoadingStarted(String imageUri, View view) { - } + if(info.thumbnail_url != null && !info.thumbnail_url.isEmpty()) { + imageLoader.displayImage(info.thumbnail_url, videoThumbnailView, + displayImageOptions, new ImageLoadingListener() { + @Override + public void onLoadingStarted(String imageUri, View view) { + } - @Override - public void onLoadingFailed(String imageUri, View view, FailReason failReason) { - Toast.makeText(VideoItemDetailFragment.this.getActivity(), - R.string.could_not_load_thumbnails, Toast.LENGTH_LONG).show(); - failReason.getCause().printStackTrace(); - } + @Override + public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + Toast.makeText(VideoItemDetailFragment.this.getActivity(), + R.string.could_not_load_thumbnails, Toast.LENGTH_LONG).show(); + failReason.getCause().printStackTrace(); + } - @Override - public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { - videoThumbnail = loadedImage; - } + @Override + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + videoThumbnail = loadedImage; + } - @Override - public void onLoadingCancelled(String imageUri, View view) { - } - }); - imageLoader.displayImage(info.uploader_thumbnail_url, - uploaderThumb, displayImageOptions, new ThumbnailLoadingListener()); - imageLoader.displayImage(info.next_video.thumbnail_url, - nextVideoThumb, displayImageOptions, new ThumbnailLoadingListener()); + @Override + public void onLoadingCancelled(String imageUri, View view) { + } + }); + } else { + videoThumbnailView.setImageResource(R.drawable.dummy_thumbnail_dark); + } + if(info.uploader_thumbnail_url != null && !info.uploader_thumbnail_url.isEmpty()) { + imageLoader.displayImage(info.uploader_thumbnail_url, + uploaderThumb, displayImageOptions, new ThumbnailLoadingListener()); + } + if(info.thumbnail_url != null && !info.thumbnail_url.isEmpty() && info.next_video != null) { + imageLoader.displayImage(info.next_video.thumbnail_url, + nextVideoThumb, displayImageOptions, new ThumbnailLoadingListener()); + } } - private void setupActionBarHandler(final VideoInfo info) { + private void setupActionBarHandler(final StreamInfo info) { actionBarHandler.setupStreamList(info.video_streams); actionBarHandler.setOnShareListener(new ActionBarHandler.OnActionListener() { @@ -414,89 +555,109 @@ public class VideoItemDetailFragment extends Fragment { actionBarHandler.setOnDownloadListener(new ActionBarHandler.OnActionListener() { @Override public void onActionSelected(int selectedStreamId) { - //VideoInfo.VideoStream selectedStreamItem = videoStreams.get(selectedStream); - VideoInfo.AudioStream audioStream = - info.audio_streams.get(getPreferredAudioStreamId(info)); - VideoInfo.VideoStream selectedStreamItem = info.video_streams.get(selectedStreamId); - String videoSuffix = "." + MediaFormat.getSuffixById(selectedStreamItem.format); - String audioSuffix = "." + MediaFormat.getSuffixById(audioStream.format); - Bundle args = new Bundle(); - args.putString(DownloadDialog.FILE_SUFFIX_VIDEO, videoSuffix); - args.putString(DownloadDialog.FILE_SUFFIX_AUDIO, audioSuffix); - args.putString(DownloadDialog.TITLE, info.title); - args.putString(DownloadDialog.VIDEO_URL, selectedStreamItem.url); - args.putString(DownloadDialog.AUDIO_URL, audioStream.url); - DownloadDialog downloadDialog = new DownloadDialog(); - downloadDialog.setArguments(args); - downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); - } - }); + try { + Bundle args = new Bundle(); - actionBarHandler.setOnPlayAudioListener(new ActionBarHandler.OnActionListener() { - @Override - public void onActionSelected(int selectedStreamId) { - boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity) - .getBoolean(activity.getString(R.string.use_external_audio_player_key), false); - Intent intent; - VideoInfo.AudioStream audioStream = - info.audio_streams.get(getPreferredAudioStreamId(info)); - if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 18) { - //internal music player: explicit intent - if (!BackgroundPlayer.isRunning && videoThumbnail != null) { - ActivityCommunicator.getCommunicator() - .backgroundPlayerThumbnail = videoThumbnail; - intent = new Intent(activity, BackgroundPlayer.class); + // Sometimes it may be that some information is not available due to changes fo the + // website which was crawled. Then the ui has to understand this and act right. - intent.setAction(Intent.ACTION_VIEW); - Log.i(TAG, "audioStream is null:" + (audioStream == null)); - Log.i(TAG, "audioStream.url is null:" + (audioStream.url == null)); - intent.setDataAndType(Uri.parse(audioStream.url), - MediaFormat.getMimeById(audioStream.format)); - intent.putExtra(BackgroundPlayer.TITLE, info.title); - intent.putExtra(BackgroundPlayer.WEB_URL, info.webpage_url); - intent.putExtra(BackgroundPlayer.SERVICE_ID, streamingServiceId); - intent.putExtra(BackgroundPlayer.CHANNEL_NAME, info.uploader); - activity.startService(intent); + if (info.audio_streams != null) { + AudioStream audioStream = + info.audio_streams.get(getPreferredAudioStreamId(info)); + + String audioSuffix = "." + MediaFormat.getSuffixById(audioStream.format); + args.putString(DownloadDialog.AUDIO_URL, audioStream.url); + args.putString(DownloadDialog.FILE_SUFFIX_AUDIO, audioSuffix); } - } else { - intent = new Intent(); - try { - intent.setAction(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.parse(audioStream.url), - MediaFormat.getMimeById(audioStream.format)); - intent.putExtra(Intent.EXTRA_TITLE, info.title); - intent.putExtra("title", info.title); - // HERE !!! - activity.startActivity(intent); - } catch (Exception e) { - e.printStackTrace(); - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setMessage(R.string.no_player_found) - .setPositiveButton(R.string.install, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url))); - activity.startActivity(intent); - } - }) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Log.i(TAG, "You unlocked a secret unicorn."); - } - }); - builder.create().show(); - Log.e(TAG, "Either no Streaming player for audio was installed, or something important crashed:"); - e.printStackTrace(); + + if (info.video_streams != null) { + VideoStream selectedStreamItem = info.video_streams.get(selectedStreamId); + String videoSuffix = "." + MediaFormat.getSuffixById(selectedStreamItem.format); + args.putString(DownloadDialog.FILE_SUFFIX_VIDEO, videoSuffix); + args.putString(DownloadDialog.VIDEO_URL, selectedStreamItem.url); } + + args.putString(DownloadDialog.TITLE, info.title); + DownloadDialog downloadDialog = new DownloadDialog(); + downloadDialog.setArguments(args); + downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); + } catch (Exception e) { + Toast.makeText(VideoItemDetailFragment.this.getActivity(), + R.string.could_not_setup_download_menu, Toast.LENGTH_LONG).show(); + e.printStackTrace(); } } }); + + if(info.audio_streams == null) { + actionBarHandler.showAudioAction(false); + } else { + actionBarHandler.setOnPlayAudioListener(new ActionBarHandler.OnActionListener() { + @Override + public void onActionSelected(int selectedStreamId) { + boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity) + .getBoolean(activity.getString(R.string.use_external_audio_player_key), false); + Intent intent; + AudioStream audioStream = + info.audio_streams.get(getPreferredAudioStreamId(info)); + if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 18) { + //internal music player: explicit intent + if (!BackgroundPlayer.isRunning && videoThumbnail != null) { + ActivityCommunicator.getCommunicator() + .backgroundPlayerThumbnail = videoThumbnail; + intent = new Intent(activity, BackgroundPlayer.class); + + intent.setAction(Intent.ACTION_VIEW); + Log.i(TAG, "audioStream is null:" + (audioStream == null)); + Log.i(TAG, "audioStream.url is null:" + (audioStream.url == null)); + intent.setDataAndType(Uri.parse(audioStream.url), + MediaFormat.getMimeById(audioStream.format)); + intent.putExtra(BackgroundPlayer.TITLE, info.title); + intent.putExtra(BackgroundPlayer.WEB_URL, info.webpage_url); + intent.putExtra(BackgroundPlayer.SERVICE_ID, streamingServiceId); + intent.putExtra(BackgroundPlayer.CHANNEL_NAME, info.uploader); + activity.startService(intent); + } + } else { + intent = new Intent(); + try { + intent.setAction(Intent.ACTION_VIEW); + intent.setDataAndType(Uri.parse(audioStream.url), + MediaFormat.getMimeById(audioStream.format)); + intent.putExtra(Intent.EXTRA_TITLE, info.title); + intent.putExtra("title", info.title); + // HERE !!! + activity.startActivity(intent); + } catch (Exception e) { + e.printStackTrace(); + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setMessage(R.string.no_player_found) + .setPositiveButton(R.string.install, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url))); + activity.startActivity(intent); + } + }) + .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Log.i(TAG, "You unlocked a secret unicorn."); + } + }); + builder.create().show(); + Log.e(TAG, "Either no Streaming player for audio was installed, or something important crashed:"); + e.printStackTrace(); + } + } + } + }); + } } - private int getPreferredAudioStreamId(final VideoInfo info) { + private int getPreferredAudioStreamId(final StreamInfo info) { String preferredFormatString = PreferenceManager.getDefaultSharedPreferences(getActivity()) .getString(activity.getString(R.string.default_audio_format_key), "webm"); @@ -523,12 +684,12 @@ public class VideoItemDetailFragment extends Fragment { return 0; } - private void initSimilarVideos(final VideoInfo info, VideoInfoItemViewCreator videoItemViewCreator) { + private void initSimilarVideos(final StreamInfo info, VideoInfoItemViewCreator videoItemViewCreator) { LinearLayout similarLayout = (LinearLayout) activity.findViewById(R.id.similarVideosView); - ArrayList similar = new ArrayList<>(info.related_videos); - for (final VideoPreviewInfo item : similar) { + ArrayList similar = new ArrayList<>(info.related_videos); + for (final StreamPreviewInfo item : similar) { View similarView = videoItemViewCreator - .getViewFromVideoInfoItem(null, similarLayout, item, getContext()); + .getViewFromVideoInfoItem(null, similarLayout, item); similarView.setClickable(true); similarView.setFocusable(true); @@ -591,8 +752,17 @@ public class VideoItemDetailFragment extends Fragment { .show(); } - private boolean useStream(VideoInfo.VideoStream stream, Vector streams) { - for(VideoInfo.VideoStream i : streams) { + private void onNotSpecifiedContentErrorWithMessage(int resourceId) { + ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detailThumbnailView); + progressBar.setVisibility(View.GONE); + thumbnailView.setImageBitmap(BitmapFactory.decodeResource( + getResources(), R.drawable.not_available_monkey)); + Toast.makeText(activity, resourceId, Toast.LENGTH_LONG) + .show(); + } + + private boolean useStream(VideoStream stream, Vector streams) { + for(VideoStream i : streams) { if(i.resolution.equals(stream.resolution)) { return false; } @@ -633,7 +803,9 @@ public class VideoItemDetailFragment extends Fragment { public void onActivityCreated(Bundle savedInstanceBundle) { super.onActivityCreated(savedInstanceBundle); Activity a = getActivity(); - playVideoButton = (FloatingActionButton) a.findViewById(R.id.playVideoButton); + if (android.os.Build.VERSION.SDK_INT < 18) { + playVideoButton = (FloatingActionButton) a.findViewById(R.id.playVideoButton); + } thumbnailWindowLayout = a.findViewById(R.id.detailVideoThumbnailWindowLayout); Button backgroundButton = (Button) a.findViewById(R.id.detailVideoThumbnailWindowBackgroundButton); @@ -641,7 +813,7 @@ public class VideoItemDetailFragment extends Fragment { // Sometimes when this fragment is not visible it still gets initiated // then we must not try to access objects of this fragment. // Otherwise the applications would crash. - if(playVideoButton != null) { + if(backgroundButton != null) { try { streamingServiceId = getArguments().getInt(STREAMING_SERVICE); StreamingService streamingService = ServiceList.getService(streamingServiceId); @@ -654,13 +826,15 @@ public class VideoItemDetailFragment extends Fragment { e.printStackTrace(); } - // todo: Fix this workaround (probably with a better design), so that older android - // versions don't have problems rendering the thumbnail right. if(Build.VERSION.SDK_INT >= 18) { ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detailThumbnailView); thumbnailView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { // This is used to synchronize the thumbnailWindowButton and the playVideoButton // inside the ScrollView with the actual size of the thumbnail. + //todo: onLayoutChage sometimes not triggered + // background buttons area seem to overlap the thumbnail view + // So although you just clicked slightly beneath the thumbnail the action still + // gets triggered. @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { @@ -678,9 +852,9 @@ public class VideoItemDetailFragment extends Fragment { } } - public void playVideo(final VideoInfo info) { + public void playVideo(final StreamInfo info) { // ----------- THE MAGIC MOMENT --------------- - VideoInfo.VideoStream selectedVideoStream = + VideoStream selectedVideoStream = info.video_streams.get(actionBarHandler.getSelectedVideoStream()); if (PreferenceManager.getDefaultSharedPreferences(activity) @@ -689,12 +863,11 @@ public class VideoItemDetailFragment extends Fragment { // External Player Intent intent = new Intent(); try { - intent.setAction(Intent.ACTION_VIEW); - - intent.setDataAndType(Uri.parse(selectedVideoStream.url), - MediaFormat.getMimeById(selectedVideoStream.format)); - intent.putExtra(Intent.EXTRA_TITLE, info.title); - intent.putExtra("title", info.title); + intent.setAction(Intent.ACTION_VIEW) + .setDataAndType(Uri.parse(selectedVideoStream.url), + MediaFormat.getMimeById(selectedVideoStream.format)) + .putExtra(Intent.EXTRA_TITLE, info.title) + .putExtra("title", info.title); activity.startActivity(intent); // HERE !!! } catch (Exception e) { @@ -704,9 +877,9 @@ public class VideoItemDetailFragment extends Fragment { .setPositiveButton(R.string.install, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url))); + Intent intent = new Intent() + .setAction(Intent.ACTION_VIEW) + .setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url))); activity.startActivity(intent); } }) @@ -719,13 +892,41 @@ public class VideoItemDetailFragment extends Fragment { builder.create().show(); } } else { - // Internal Player - Intent intent = new Intent(activity, PlayVideoActivity.class); - intent.putExtra(PlayVideoActivity.VIDEO_TITLE, info.title); - intent.putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.url); - intent.putExtra(PlayVideoActivity.VIDEO_URL, info.webpage_url); - intent.putExtra(PlayVideoActivity.START_POSITION, info.start_position); - activity.startActivity(intent); //also HERE !!! + if (PreferenceManager.getDefaultSharedPreferences(activity) + .getBoolean(activity.getString(R.string.use_exoplayer_key), false)) { + + // exo player + + if(info.dashMpdUrl != null && !info.dashMpdUrl.isEmpty()) { + // try dash + Intent intent = new Intent(activity, ExoPlayerActivity.class) + .setData(Uri.parse(info.dashMpdUrl)) + .putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_DASH); + startActivity(intent); + } else if((info.audio_streams != null && !info.audio_streams.isEmpty()) && + (info.video_only_streams != null && !info.video_only_streams.isEmpty())) { + // try smooth streaming + + } else { + //default streaming + Intent intent = new Intent(activity, ExoPlayerActivity.class) + .setDataAndType(Uri.parse(selectedVideoStream.url), + MediaFormat.getMimeById(selectedVideoStream.format)) + .putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_OTHER); + + activity.startActivity(intent); // HERE !!! + } + //------------- + + } else { + // Internal Player + Intent intent = new Intent(activity, PlayVideoActivity.class) + .putExtra(PlayVideoActivity.VIDEO_TITLE, info.title) + .putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.url) + .putExtra(PlayVideoActivity.VIDEO_URL, info.webpage_url) + .putExtra(PlayVideoActivity.START_POSITION, info.start_position); + activity.startActivity(intent); //also HERE !!! + } } // -------------------------------------------- diff --git a/app/src/main/java/org/schabi/newpipe/VideoItemListActivity.java b/app/src/main/java/org/schabi/newpipe/VideoItemListActivity.java index 4c7717331..48220582d 100644 --- a/app/src/main/java/org/schabi/newpipe/VideoItemListActivity.java +++ b/app/src/main/java/org/schabi/newpipe/VideoItemListActivity.java @@ -2,8 +2,9 @@ package org.schabi.newpipe; import android.content.Context; import android.content.Intent; -import android.media.AudioManager; +import android.content.SharedPreferences; import android.os.Bundle; +import android.os.Handler; import android.preference.PreferenceManager; import android.support.v4.app.NavUtils; import android.support.v7.app.AppCompatActivity; @@ -14,11 +15,16 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.inputmethod.InputMethodManager; +import android.widget.Toast; +import org.schabi.newpipe.extractor.ExtractionException; +import org.schabi.newpipe.extractor.SearchEngine; +import org.schabi.newpipe.extractor.ServiceList; +import org.schabi.newpipe.extractor.StreamingService; + +import java.io.IOException; import java.util.ArrayList; - -import org.schabi.newpipe.crawler.VideoPreviewInfo; -import org.schabi.newpipe.crawler.ServiceList; +import java.util.Vector; /** * Copyright (C) Christian Schabesberger 2015 @@ -62,6 +68,10 @@ public class VideoItemListActivity extends AppCompatActivity private VideoItemDetailFragment videoFragment = null; private Menu menu = null; + private SuggestionListAdapter suggestionListAdapter; + private SuggestionSearchRunnable suggestionSearchRunnable; + private Thread searchThread; + private class SearchVideoQueryListener implements SearchView.OnQueryTextListener { @Override @@ -79,6 +89,8 @@ public class VideoItemListActivity extends AppCompatActivity getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); } catch(NullPointerException e) { Log.e(TAG, "Could not get widget with focus"); + Toast.makeText(VideoItemListActivity.this, "Could not get widget with focus", + Toast.LENGTH_SHORT).show(); e.printStackTrace(); } // clear focus @@ -90,18 +102,92 @@ public class VideoItemListActivity extends AppCompatActivity } catch(Exception e) { e.printStackTrace(); } - View bg = findViewById(R.id.mainBG); - bg.setVisibility(View.GONE); return true; } @Override public boolean onQueryTextChange(String newText) { + if(!newText.isEmpty()) { + searchSuggestions(newText); + } return true; } } + private class SearchSuggestionListener implements SearchView.OnSuggestionListener{ + private SearchView searchView; + + private SearchSuggestionListener(SearchView searchView) { + this.searchView = searchView; + } + + @Override + public boolean onSuggestionSelect(int position) { + String suggestion = suggestionListAdapter.getSuggestion(position); + searchView.setQuery(suggestion,true); + return false; + } + + @Override + public boolean onSuggestionClick(int position) { + String suggestion = suggestionListAdapter.getSuggestion(position); + searchView.setQuery(suggestion,true); + return false; + } + } + + private class SuggestionResultRunnable implements Runnable{ + + private ArrayListsuggestions; + + private SuggestionResultRunnable(ArrayList suggestions) { + this.suggestions = suggestions; + } + + @Override + public void run() { + suggestionListAdapter.updateAdapter(suggestions); + } + } + + private class SuggestionSearchRunnable implements Runnable{ + private final int serviceId; + private final String query; + final Handler h = new Handler(); + private Context context; + private SuggestionSearchRunnable(int serviceId, String query) { + this.serviceId = serviceId; + this.query = query; + context = VideoItemListActivity.this; + } + + @Override + public void run() { + try { + SearchEngine engine = + ServiceList.getService(serviceId).getSearchEngineInstance(new Downloader()); + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); + String searchLanguageKey = context.getString(R.string.search_language_key); + String searchLanguage = sp.getString(searchLanguageKey, + getString(R.string.default_language_value)); + ArrayListsuggestions = engine.suggestionList(query,searchLanguage,new Downloader()); + h.post(new SuggestionResultRunnable(suggestions)); + } catch (ExtractionException e) { + ErrorActivity.reportError(h, VideoItemListActivity.this, e, null, findViewById(R.id.videoitem_list), + ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, + ServiceList.getNameOfService(serviceId), query, R.string.parsing_error)); + e.printStackTrace(); + } catch (IOException e) { + postNewErrorToast(h, R.string.network_error); + e.printStackTrace(); + } catch (Exception e) { + ErrorActivity.reportError(h, VideoItemListActivity.this, e, null, findViewById(R.id.videoitem_list), + ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, + ServiceList.getNameOfService(serviceId), query, R.string.general_error)); + } + } + } /** * Whether or not the activity is in two-pane mode, i.e. running on a tablet * device. @@ -112,37 +198,23 @@ public class VideoItemListActivity extends AppCompatActivity protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_videoitem_list); + StreamingService streamingService = null; - View bg = findViewById(R.id.mainBG); - bg.setVisibility(View.VISIBLE); - - //------ todo: remove this line when multiservice support is implemented ------ - currentStreamingServiceId = ServiceList.getIdOfService("Youtube"); + try { + //------ todo: remove this line when multiservice support is implemented ------ + currentStreamingServiceId = ServiceList.getIdOfService("Youtube"); + streamingService = ServiceList.getService(currentStreamingServiceId); + } catch (Exception e) { + e.printStackTrace(); + ErrorActivity.reportError(VideoItemListActivity.this, e, null, findViewById(R.id.videoitem_list), + ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, + ServiceList.getNameOfService(currentStreamingServiceId), "", R.string.general_error)); + } //----------------------------------------------------------------------------- //to solve issue 38 listFragment = (VideoItemListFragment) getSupportFragmentManager() .findFragmentById(R.id.videoitem_list); - listFragment.setStreamingService(ServiceList.getService(currentStreamingServiceId)); - - Bundle arguments = getIntent().getExtras(); - - if(arguments != null) { - //Parcelable[] p = arguments.getParcelableArray(VIDEO_INFO_ITEMS); - ArrayList p = arguments.getParcelableArrayList(VIDEO_INFO_ITEMS); - if(p != null) { - mode = PRESENT_VIDEOS_MODE; - try { - //noinspection ConstantConditions - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } catch (NullPointerException e) { - Log.e(TAG, "Could not get SupportActionBar"); - e.printStackTrace(); - } - - listFragment.present(p); - } - } - + listFragment.setStreamingService(streamingService); if(savedInstanceState != null && mode != PRESENT_VIDEOS_MODE) { @@ -173,7 +245,13 @@ public class VideoItemListActivity extends AppCompatActivity // the support version on SearchView, so it needs to be set programmatically. searchView.setIconifiedByDefault(false); searchView.setIconified(false); + if(!searchQuery.isEmpty()) { + searchView.setQuery(searchQuery,false); + } searchView.setOnQueryTextListener(new SearchVideoQueryListener()); + suggestionListAdapter = new SuggestionListAdapter(this); + searchView.setSuggestionsAdapter(suggestionListAdapter); + searchView.setOnSuggestionListener(new SearchSuggestionListener(searchView)); } else { searchView.setVisibility(View.GONE); } @@ -198,14 +276,14 @@ public class VideoItemListActivity extends AppCompatActivity getSupportFragmentManager() .findFragmentById(R.id.videoitem_list)) .getListAdapter(); - String webpage_url = listAdapter.getVideoList().get((int) Long.parseLong(id)).webpage_url; + String webpageUrl = listAdapter.getVideoList().get((int) Long.parseLong(id)).webpage_url; if (mTwoPane) { // In two-pane mode, show the detail view in this activity by // adding or replacing the detail fragment using a // fragment transaction. Bundle arguments = new Bundle(); //arguments.putString(VideoItemDetailFragment.ARG_ITEM_ID, id); - arguments.putString(VideoItemDetailFragment.VIDEO_URL, webpage_url); + arguments.putString(VideoItemDetailFragment.VIDEO_URL, webpageUrl); arguments.putInt(VideoItemDetailFragment.STREAMING_SERVICE, currentStreamingServiceId); videoFragment = new VideoItemDetailFragment(); videoFragment.setArguments(arguments); @@ -224,7 +302,7 @@ public class VideoItemListActivity extends AppCompatActivity // for the selected item ID. Intent detailIntent = new Intent(this, VideoItemDetailActivity.class); //detailIntent.putExtra(VideoItemDetailFragment.ARG_ITEM_ID, id); - detailIntent.putExtra(VideoItemDetailFragment.VIDEO_URL, webpage_url); + detailIntent.putExtra(VideoItemDetailFragment.VIDEO_URL, webpageUrl); detailIntent.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, currentStreamingServiceId); startActivity(detailIntent); } @@ -243,7 +321,13 @@ public class VideoItemListActivity extends AppCompatActivity searchView.setFocusable(false); searchView.setOnQueryTextListener( new SearchVideoQueryListener()); - + suggestionListAdapter = new SuggestionListAdapter(this); + searchView.setSuggestionsAdapter(suggestionListAdapter); + searchView.setOnSuggestionListener(new SearchSuggestionListener(searchView)); + if(!searchQuery.isEmpty()) { + searchView.setQuery(searchQuery,false); + searchView.setIconifiedByDefault(false); + } } else if (videoFragment != null){ videoFragment.onCreateOptionsMenu(menu, inflater); } else { @@ -269,6 +353,14 @@ public class VideoItemListActivity extends AppCompatActivity startActivity(intent); return true; } + case R.id.action_report_error: { + ErrorActivity.reportError(VideoItemListActivity.this, new Vector(), + null, null, + ErrorActivity.ErrorInfo.make(ErrorActivity.USER_REPORT, + ServiceList.getNameOfService(currentStreamingServiceId), + "user_report", R.string.user_report)); + return true; + } default: return videoFragment.onOptionsItemSelected(item) || super.onOptionsItemSelected(item); @@ -287,4 +379,22 @@ public class VideoItemListActivity extends AppCompatActivity outState.putString(QUERY, searchQuery); outState.putInt(STREAMING_SERVICE, currentStreamingServiceId); } + + private void searchSuggestions(String query) { + suggestionSearchRunnable = + new SuggestionSearchRunnable(currentStreamingServiceId, query); + searchThread = new Thread(suggestionSearchRunnable); + searchThread.start(); + + } + + private void postNewErrorToast(Handler h, final int stringResource) { + h.post(new Runnable() { + @Override + public void run() { + Toast.makeText(VideoItemListActivity.this, getString(stringResource), + Toast.LENGTH_SHORT).show(); + } + }); + } } diff --git a/app/src/main/java/org/schabi/newpipe/VideoItemListFragment.java b/app/src/main/java/org/schabi/newpipe/VideoItemListFragment.java index cf8a5a08c..10d4997f4 100644 --- a/app/src/main/java/org/schabi/newpipe/VideoItemListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/VideoItemListFragment.java @@ -1,9 +1,8 @@ package org.schabi.newpipe; +import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; @@ -16,14 +15,13 @@ import android.widget.ListView; import android.widget.Toast; import java.io.IOException; -import java.net.URL; import java.util.List; -import java.util.Vector; -import org.schabi.newpipe.crawler.CrawlingException; -import org.schabi.newpipe.crawler.VideoPreviewInfo; -import org.schabi.newpipe.crawler.SearchEngine; -import org.schabi.newpipe.crawler.StreamingService; +import org.schabi.newpipe.extractor.ExtractionException; +import org.schabi.newpipe.extractor.SearchResult; +import org.schabi.newpipe.extractor.StreamPreviewInfo; +import org.schabi.newpipe.extractor.SearchEngine; +import org.schabi.newpipe.extractor.StreamingService; /** @@ -71,9 +69,9 @@ public class VideoItemListFragment extends ListFragment { private boolean loadingNextPage = true; private class ResultRunnable implements Runnable { - private final SearchEngine.Result result; + private final SearchResult result; private final int requestId; - public ResultRunnable(SearchEngine.Result result, int requestId) { + public ResultRunnable(SearchResult result, int requestId) { this.result = result; this.requestId = requestId; } @@ -104,92 +102,60 @@ public class VideoItemListFragment extends ListFragment { } @Override public void run() { + SearchResult result = null; try { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); String searchLanguageKey = getContext().getString(R.string.search_language_key); String searchLanguage = sp.getString(searchLanguageKey, getString(R.string.default_language_value)); - SearchEngine.Result result = engine.search(query, page, searchLanguage, - new Downloader()); + result = SearchResult + .getSearchResult(engine, query, page, searchLanguage, new Downloader()); - Log.i(TAG, "language code passed:\""+searchLanguage+"\""); if(runs) { h.post(new ResultRunnable(result, requestId)); } - } catch(IOException e) { - postNewErrorToast(h, R.string.network_error); - e.printStackTrace(); - } catch(CrawlingException ce) { - postNewErrorToast(h, R.string.parsing_error); - ce.printStackTrace(); - } catch(Exception e) { - postNewErrorToast(h, R.string.general_error); - e.printStackTrace(); - } - } - } -/* -<<< - private class LoadThumbsRunnable implements Runnable { - private final Vector thumbnailUrlList = new Vector<>(); - private final Vector downloadedList; - final Handler h = new Handler(); - private volatile boolean run = true; - private final int requestId; - public LoadThumbsRunnable(Vector videoList, - Vector downloadedList, int requestId) { - for(VideoPreviewInfo item : videoList) { - thumbnailUrlList.add(item.thumbnail_url); - } - this.downloadedList = downloadedList; - this.requestId = requestId; - } - public void terminate() { - run = false; - } - public boolean isRunning() { - return run; - } - @Override - public void run() { - for(int i = 0; i < thumbnailUrlList.size() && run; i++) { - if(!downloadedList.get(i)) { - Bitmap thumbnail; - try { - //todo: make bitmaps not bypass tor - thumbnail = BitmapFactory.decodeStream( - new URL(thumbnailUrlList.get(i)).openConnection().getInputStream()); - h.post(new SetThumbnailRunnable(i, thumbnail, requestId)); - } catch (Exception e) { + + // look for errors during extraction + // soft errors: + if(result != null && + !result.errors.isEmpty()) { + Log.e(TAG, "OCCURRED ERRORS DURING SEARCH EXTRACTION:"); + for(Exception e : result.errors) { e.printStackTrace(); + Log.e(TAG, "------"); } + + Activity a = getActivity(); + View rootView = a.findViewById(R.id.videoitem_list); + ErrorActivity.reportError(h, getActivity(), result.errors, null, rootView, + ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, + /* todo: this shoudl not be assigned static */ "Youtube", query, R.string.light_parsing_error)); + } + // hard errors: + } catch(IOException e) { + postNewNothingFoundToast(h, R.string.network_error); + e.printStackTrace(); + } catch(SearchEngine.NothingFoundException e) { + postNewErrorToast(h, e.getMessage()); + } catch(ExtractionException e) { + ErrorActivity.reportError(h, getActivity(), e, null, null, + ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, + /* todo: this shoudl not be assigned static */ "Youtube", query, R.string.parsing_error)); + //postNewErrorToast(h, R.string.parsing_error); + e.printStackTrace(); + + } catch(Exception e) { + ErrorActivity.reportError(h, getActivity(), e, null, null, + ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, + /* todo: this shoudl not be assigned static */ "Youtube", query, R.string.general_error)); + + e.printStackTrace(); } } } - private class SetThumbnailRunnable implements Runnable { - private final int index; - private final Bitmap thumbnail; - private final int requestId; - public SetThumbnailRunnable(int index, Bitmap thumbnail, int requestId) { - this.index = index; - this.thumbnail = thumbnail; - this.requestId = requestId; - } - @Override - public void run() { - if(requestId == currentRequestId) { - videoListAdapter.updateDownloadedThumbnailList(index); - videoListAdapter.setThumbnail(index, thumbnail); - } - } - } - -======= ->>>>>>> 6d1b4652fc98e5c2d5e19b0f98ba38a731137a70 -*/ - public void present(List videoList) { + public void present(List videoList) { mode = PRESENT_VIDEOS_MODE; setListShown(true); getListView().smoothScrollToPosition(0); @@ -219,7 +185,7 @@ public class VideoItemListFragment extends ListFragment { private void startSearch(String query, int page) { currentRequestId++; terminateThreads(); - searchRunnable = new SearchRunnable(streamingService.getSearchEngineInstance(), + searchRunnable = new SearchRunnable(streamingService.getSearchEngineInstance(new Downloader()), query, page, currentRequestId); searchThread = new Thread(searchRunnable); searchThread.start(); @@ -229,28 +195,29 @@ public class VideoItemListFragment extends ListFragment { this.streamingService = streamingService; } - private void updateListOnResult(SearchEngine.Result result, int requestId) { + private void updateListOnResult(SearchResult result, int requestId) { if(requestId == currentRequestId) { setListShown(true); - if (result.resultList.isEmpty()) { - Toast.makeText(getActivity(), result.errorMessage, Toast.LENGTH_LONG).show(); - } else { - if (!result.suggestion.isEmpty()) { - Toast.makeText(getActivity(), getString(R.string.did_you_mean) + result.suggestion + " ?", - Toast.LENGTH_LONG).show(); - } - updateList(result.resultList); + updateList(result.resultList); + if(!result.suggestion.isEmpty()) { + Toast.makeText(getActivity(), + String.format(getString(R.string.did_you_mean), result.suggestion), + Toast.LENGTH_LONG).show(); } } } - private void updateList(List list) { + private void updateList(List list) { try { videoListAdapter.addVideoList(list); terminateThreads(); } catch(java.lang.IllegalStateException e) { + Toast.makeText(getActivity(), "Trying to set value while activity doesn't exist anymore.", + Toast.LENGTH_SHORT).show(); Log.w(TAG, "Trying to set value while activity doesn't exist anymore."); } catch(Exception e) { + Toast.makeText(getActivity(), getString(R.string.general_error), + Toast.LENGTH_SHORT).show(); e.printStackTrace(); } finally { loadingNextPage = false; @@ -377,14 +344,25 @@ public class VideoItemListFragment extends ListFragment { mActivatedPosition = position; } - private void postNewErrorToast(Handler h, final int stringResource) { + private void postNewErrorToast(Handler h, final String message) { h.post(new Runnable() { @Override public void run() { setListShown(true); - Toast.makeText(getActivity(), getString(R.string.network_error), + Toast.makeText(getActivity(), message, Toast.LENGTH_SHORT).show(); } }); } + + private void postNewNothingFoundToast(Handler h, final int stringResource) { + h.post(new Runnable() { + @Override + public void run() { + setListShown(true); + Toast.makeText(getActivity(), getString(stringResource), + Toast.LENGTH_LONG).show(); + } + }); + } } diff --git a/app/src/main/java/org/schabi/newpipe/VideoListAdapter.java b/app/src/main/java/org/schabi/newpipe/VideoListAdapter.java index 7077810ad..d69b842cf 100644 --- a/app/src/main/java/org/schabi/newpipe/VideoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/VideoListAdapter.java @@ -8,7 +8,7 @@ import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ListView; -import org.schabi.newpipe.crawler.VideoPreviewInfo; +import org.schabi.newpipe.extractor.StreamPreviewInfo; import java.util.List; import java.util.Vector; @@ -36,7 +36,7 @@ import java.util.Vector; class VideoListAdapter extends BaseAdapter { private final Context context; private final VideoInfoItemViewCreator viewCreator; - private Vector videoList = new Vector<>(); + private Vector videoList = new Vector<>(); private final ListView listView; public VideoListAdapter(Context context, VideoItemListFragment videoListFragment) { @@ -47,7 +47,7 @@ class VideoListAdapter extends BaseAdapter { this.context = context; } - public void addVideoList(List videos) { + public void addVideoList(List videos) { videoList.addAll(videos); notifyDataSetChanged(); } @@ -57,7 +57,7 @@ class VideoListAdapter extends BaseAdapter { notifyDataSetChanged(); } - public Vector getVideoList() { + public Vector getVideoList() { return videoList; } @@ -78,7 +78,7 @@ class VideoListAdapter extends BaseAdapter { @Override public View getView(int position, View convertView, ViewGroup parent) { - convertView = viewCreator.getViewFromVideoInfoItem(convertView, parent, videoList.get(position), context); + convertView = viewCreator.getViewFromVideoInfoItem(convertView, parent, videoList.get(position)); if(listView.isItemChecked(position)) { convertView.setBackgroundColor(ContextCompat.getColor(context,R.color.light_youtube_primary_color)); diff --git a/app/src/main/java/org/schabi/newpipe/crawler/VideoInfo.java b/app/src/main/java/org/schabi/newpipe/crawler/VideoInfo.java deleted file mode 100644 index a6aa4a43e..000000000 --- a/app/src/main/java/org/schabi/newpipe/crawler/VideoInfo.java +++ /dev/null @@ -1,191 +0,0 @@ -package org.schabi.newpipe.crawler; - -import java.io.IOException; -import java.util.List; -import java.util.Vector; - -/** - * Created by Christian Schabesberger on 26.08.15. - * - * Copyright (C) Christian Schabesberger 2015 - * VideoInfo.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 . - */ - -/**Info object for opened videos, ie the video ready to play.*/ -@SuppressWarnings("ALL") -public class VideoInfo extends AbstractVideoInfo { - - /**Fills out the video info fields which are common to all services. - * Probably needs to be overridden by subclasses*/ - public static VideoInfo getVideoInfo(StreamExtractor extractor, Downloader downloader) - throws CrawlingException, IOException { - VideoInfo videoInfo = new VideoInfo(); - - VideoUrlIdHandler uiconv = extractor.getUrlIdConverter(); - - - videoInfo.webpage_url = extractor.getPageUrl(); - videoInfo.title = extractor.getTitle(); - videoInfo.duration = extractor.getLength(); - videoInfo.uploader = extractor.getUploader(); - videoInfo.description = extractor.getDescription(); - videoInfo.view_count = extractor.getViews(); - videoInfo.upload_date = extractor.getUploadDate(); - videoInfo.thumbnail_url = extractor.getThumbnailUrl(); - videoInfo.id = uiconv.getVideoId(extractor.getPageUrl()); - //todo: make this quick and dirty solution a real fallback - // The front end should be notified that the dash mpd could not be downloaded - // although not getting the dash mpd is not the end of the world, therfore - // we continue. - try { - videoInfo.dashMpdUrl = extractor.getDashMpdUrl(); - } catch(Exception e) { - e.printStackTrace(); - } - /** Load and extract audio*/ - videoInfo.audio_streams = extractor.getAudioStreams(); - if(videoInfo.dashMpdUrl != null && !videoInfo.dashMpdUrl.isEmpty()) { - if(videoInfo.audio_streams == null) { - videoInfo.audio_streams = new Vector(); - } - //todo: make this quick and dirty solution a real fallback - // same as the quick and dirty aboth - try { - videoInfo.audio_streams.addAll( - DashMpdParser.getAudioStreams(videoInfo.dashMpdUrl, downloader)); - } catch(Exception e) { - e.printStackTrace(); - } - } - /** Extract video stream url*/ - videoInfo.video_streams = extractor.getVideoStreams(); - /** Extract video only stream url*/ - videoInfo.video_only_streams = extractor.getVideoOnlyStreams(); - videoInfo.uploader_thumbnail_url = extractor.getUploaderThumbnailUrl(); - videoInfo.start_position = extractor.getTimeStamp(); - videoInfo.average_rating = extractor.getAverageRating(); - videoInfo.like_count = extractor.getLikeCount(); - videoInfo.dislike_count = extractor.getDislikeCount(); - videoInfo.next_video = extractor.getNextVideo(); - videoInfo.related_videos = extractor.getRelatedVideos(); - - //Bitmap thumbnail = null; - //Bitmap uploader_thumbnail = null; - //int videoAvailableStatus = VIDEO_AVAILABLE; - return videoInfo; - } - - - public String uploader_thumbnail_url = ""; - public String description = ""; - /*todo: make this lists over vectors*/ - public List video_streams = null; - public List audio_streams = null; - public List video_only_streams = null; - // video streams provided by the dash mpd do not need to be provided as VideoStream. - // Later on this will also aplly to audio streams. Since dash mpd is standarized, - // crawling such a file is not service dependent. Therefore getting audio only streams by yust - // providing the dash mpd fille will be possible in the future. - public String dashMpdUrl = ""; - public int duration = -1; - - /*YouTube-specific fields - todo: move these to a subclass*/ - public int age_limit = 0; - public int like_count = -1; - public int dislike_count = -1; - public String average_rating = ""; - public VideoPreviewInfo next_video = null; - public List related_videos = null; - //in seconds. some metadata is not passed using a VideoInfo object! - public int start_position = 0; - //todo: public int service_id = -1; - - public VideoInfo() {} - - /**Creates a new VideoInfo object from an existing AbstractVideoInfo. - * All the shared properties are copied to the new VideoInfo.*/ - @SuppressWarnings("WeakerAccess") - public VideoInfo(AbstractVideoInfo avi) { - this.id = avi.id; - this.title = avi.title; - this.uploader = avi.uploader; - this.thumbnail_url = avi.thumbnail_url; - this.thumbnail = avi.thumbnail; - this.webpage_url = avi.webpage_url; - this.upload_date = avi.upload_date; - this.upload_date = avi.upload_date; - this.view_count = avi.view_count; - - //todo: better than this - if(avi instanceof VideoPreviewInfo) { - //shitty String to convert code - String dur = ((VideoPreviewInfo)avi).duration; - int minutes = Integer.parseInt(dur.substring(0, dur.indexOf(":"))); - int seconds = Integer.parseInt(dur.substring(dur.indexOf(":")+1, dur.length())); - this.duration = (minutes*60)+seconds; - } - } - - public static class VideoStream { - //url of the stream - public String url = ""; - public int format = -1; - public String resolution = ""; - - public VideoStream(String url, int format, String res) { - this.url = url; this.format = format; resolution = res; - } - - // reveals wether two streams are the same, but have diferent urls - public boolean equalStats(VideoStream cmp) { - return format == cmp.format - && resolution == cmp.resolution; - } - - // revelas wether two streams are equal - public boolean equals(VideoStream cmp) { - return equalStats(cmp) - && url == cmp.url; - } - } - - @SuppressWarnings("unused") - public static class AudioStream { - public String url = ""; - public int format = -1; - public int bandwidth = -1; - public int sampling_rate = -1; - - public AudioStream(String url, int format, int bandwidth, int samplingRate) { - this.url = url; this.format = format; - this.bandwidth = bandwidth; this.sampling_rate = samplingRate; - } - - // reveals wether two streams are the same, but have diferent urls - public boolean equalStats(AudioStream cmp) { - return format == cmp.format - && bandwidth == cmp.bandwidth - && sampling_rate == cmp.sampling_rate; - } - - // revelas wether two streams are equal - public boolean equals(AudioStream cmp) { - return equalStats(cmp) - && url == cmp.url; - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/crawler/VideoPreviewInfo.java b/app/src/main/java/org/schabi/newpipe/crawler/VideoPreviewInfo.java deleted file mode 100644 index bca13a208..000000000 --- a/app/src/main/java/org/schabi/newpipe/crawler/VideoPreviewInfo.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.schabi.newpipe.crawler; - -import android.graphics.Bitmap; -import android.os.Parcel; -import android.os.Parcelable; - -import org.schabi.newpipe.crawler.AbstractVideoInfo; - -/** - * Created by Christian Schabesberger on 26.08.15. - * - * Copyright (C) Christian Schabesberger 2015 - * VideoPreviewInfo.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 . - */ - -/**Info object for previews of unopened videos, eg search results, related videos*/ -public class VideoPreviewInfo extends AbstractVideoInfo implements Parcelable { - public String duration = ""; - @SuppressWarnings("WeakerAccess") - protected VideoPreviewInfo(Parcel in) { - id = in.readString(); - title = in.readString(); - uploader = in.readString(); - duration = in.readString(); - thumbnail_url = in.readString(); - thumbnail = (Bitmap) in.readValue(Bitmap.class.getClassLoader()); - webpage_url = in.readString(); - upload_date = in.readString(); - view_count = in.readLong(); - } - - public VideoPreviewInfo() { - - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(id); - dest.writeString(title); - dest.writeString(uploader); - dest.writeString(duration); - dest.writeString(thumbnail_url); - dest.writeValue(thumbnail); - dest.writeString(webpage_url); - dest.writeString(upload_date); - dest.writeLong(view_count); - } - - @SuppressWarnings("unused") - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public VideoPreviewInfo createFromParcel(Parcel in) { - return new VideoPreviewInfo(in); - } - - @Override - public VideoPreviewInfo[] newArray(int size) { - return new VideoPreviewInfo[size]; - } - }; -} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/crawler/services/youtube/YoutubeSearchEngine.java b/app/src/main/java/org/schabi/newpipe/crawler/services/youtube/YoutubeSearchEngine.java deleted file mode 100644 index a5a547706..000000000 --- a/app/src/main/java/org/schabi/newpipe/crawler/services/youtube/YoutubeSearchEngine.java +++ /dev/null @@ -1,209 +0,0 @@ -package org.schabi.newpipe.crawler.services.youtube; - -import android.net.Uri; -import android.util.Log; - -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.schabi.newpipe.crawler.Downloader; -import org.schabi.newpipe.crawler.ParsingException; -import org.schabi.newpipe.crawler.SearchEngine; -import org.schabi.newpipe.crawler.VideoPreviewInfo; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; - -/** - * Created by Christian Schabesberger on 09.08.15. - * - * Copyright (C) Christian Schabesberger 2015 - * YoutubeSearchEngine.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 . - */ - -public class YoutubeSearchEngine implements SearchEngine { - - private static final String TAG = YoutubeSearchEngine.class.toString(); - - @Override - public Result search(String query, int page, String languageCode, Downloader downloader) - throws IOException, ParsingException { - Result result = new Result(); - Uri.Builder builder = new Uri.Builder(); - builder.scheme("https") - .authority("www.youtube.com") - .appendPath("results") - .appendQueryParameter("search_query", query) - .appendQueryParameter("page", Integer.toString(page)) - .appendQueryParameter("filters", "video"); - - String site; - String url = builder.build().toString(); - //if we've been passed a valid language code, append it to the URL - if(!languageCode.isEmpty()) { - //assert Pattern.matches("[a-z]{2}(-([A-Z]{2}|[0-9]{1,3}))?", languageCode); - site = downloader.download(url, languageCode); - } - else { - site = downloader.download(url); - } - - try { - - Document doc = Jsoup.parse(site, url); - Element list = doc.select("ol[class=\"item-section\"]").first(); - - for (Element item : list.children()) { - /* First we need to determine which kind of item we are working with. - Youtube depicts five different kinds of items on its search result page. These are - regular videos, playlists, channels, two types of video suggestions, and a "no video - found" item. Since we only want videos, we need to filter out all the others. - An example for this can be seen here: - https://www.youtube.com/results?search_query=asdf&page=1 - - We already applied a filter to the url, so we don't need to care about channels and - playlists now. - */ - - Element el; - - // both types of spell correction item - if (!((el = item.select("div[class*=\"spell-correction\"]").first()) == null)) { - result.suggestion = el.select("a").first().text(); - // search message item - } else if (!((el = item.select("div[class*=\"search-message\"]").first()) == null)) { - result.errorMessage = el.text(); - - // video item type - } else if (!((el = item.select("div[class*=\"yt-lockup-video\"").first()) == null)) { - VideoPreviewInfo resultItem = new VideoPreviewInfo(); - Element dl = el.select("h3").first().select("a").first(); - resultItem.webpage_url = dl.attr("abs:href"); - try { - Pattern p = Pattern.compile("v=([0-9a-zA-Z-]*)"); - Matcher m = p.matcher(resultItem.webpage_url); - resultItem.id = m.group(1); - } catch (Exception e) { - //e.printStackTrace(); - } - resultItem.title = dl.text(); - - resultItem.duration = item.select("span[class=\"video-time\"]").first().text(); - - resultItem.uploader = item.select("div[class=\"yt-lockup-byline\"]").first() - .select("a").first() - .text(); - resultItem.upload_date = item.select("div[class=\"yt-lockup-meta\"]").first() - .select("li").first() - .text(); - - //todo: test against view_count - String viewCountInfo = item.select("div[class=\"yt-lockup-meta\"]").first() - .select("li").get(1) - .text(); - viewCountInfo = viewCountInfo.substring(0, viewCountInfo.indexOf(' ')); - viewCountInfo = viewCountInfo.replaceAll("[,.]", ""); - resultItem.view_count = Long.parseLong(viewCountInfo); - - Element te = item.select("div[class=\"yt-thumb video-thumb\"]").first() - .select("img").first(); - resultItem.thumbnail_url = te.attr("abs:src"); - // Sometimes youtube sends links to gif files which somehow seem to not exist - // anymore. Items with such gif also offer a secondary image source. So we are going - // to use that if we've caught such an item. - if (resultItem.thumbnail_url.contains(".gif")) { - resultItem.thumbnail_url = te.attr("abs:data-thumb"); - } - result.resultList.add(resultItem); - } else { - //noinspection ConstantConditions - Log.e(TAG, "unexpected element found:\"" + el + "\""); - } - } - } catch(Exception e) { - throw new ParsingException(e); - } - return result; - } - - @Override - public ArrayList suggestionList(String query, Downloader dl) - throws IOException, ParsingException { - - ArrayList suggestions = new ArrayList<>(); - - Uri.Builder builder = new Uri.Builder(); - builder.scheme("https") - .authority("suggestqueries.google.com") - .appendPath("complete") - .appendPath("search") - .appendQueryParameter("client", "") - .appendQueryParameter("output", "toolbar") - .appendQueryParameter("ds", "yt") - .appendQueryParameter("q", query); - String url = builder.build().toString(); - - - String response = dl.download(url); - - try { - - //TODO: Parse xml data using Jsoup not done - DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder dBuilder; - org.w3c.dom.Document doc = null; - - try { - dBuilder = dbFactory.newDocumentBuilder(); - doc = dBuilder.parse(new InputSource( - new ByteArrayInputStream(response.getBytes("utf-8")))); - doc.getDocumentElement().normalize(); - } catch (ParserConfigurationException | SAXException | IOException e) { - e.printStackTrace(); - } - - if (doc != null) { - NodeList nList = doc.getElementsByTagName("CompleteSuggestion"); - for (int temp = 0; temp < nList.getLength(); temp++) { - - NodeList nList1 = doc.getElementsByTagName("suggestion"); - Node nNode1 = nList1.item(temp); - if (nNode1.getNodeType() == Node.ELEMENT_NODE) { - org.w3c.dom.Element eElement = (org.w3c.dom.Element) nNode1; - suggestions.add(eElement.getAttribute("data")); - } - } - } else { - Log.e(TAG, "GREAT FUCKING ERROR"); - } - return suggestions; - } catch(Exception e) { - throw new ParsingException(e); - } - } - -} diff --git a/app/src/main/java/org/schabi/newpipe/crawler/AbstractVideoInfo.java b/app/src/main/java/org/schabi/newpipe/extractor/AbstractVideoInfo.java similarity index 74% rename from app/src/main/java/org/schabi/newpipe/crawler/AbstractVideoInfo.java rename to app/src/main/java/org/schabi/newpipe/extractor/AbstractVideoInfo.java index 7a15a8af2..9ea107f22 100644 --- a/app/src/main/java/org/schabi/newpipe/crawler/AbstractVideoInfo.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/AbstractVideoInfo.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.crawler; +package org.schabi.newpipe.extractor; import android.graphics.Bitmap; @@ -20,8 +20,19 @@ import android.graphics.Bitmap; * along with NewPipe. If not, see . */ -/**Common properties between VideoInfo and VideoPreviewInfo.*/ +/**Common properties between StreamInfo and StreamPreviewInfo.*/ public abstract class AbstractVideoInfo { + public static enum StreamType { + NONE, // placeholder to check if stream type was checked or not + VIDEO_STREAM, + AUDIO_STREAM, + LIVE_STREAM, + AUDIO_LIVE_STREAM, + FILE + } + + public StreamType stream_type; + public int service_id = -1; public String id = ""; public String title = ""; public String uploader = ""; diff --git a/app/src/main/java/org/schabi/newpipe/extractor/AudioStream.java b/app/src/main/java/org/schabi/newpipe/extractor/AudioStream.java new file mode 100644 index 000000000..807ae666e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/extractor/AudioStream.java @@ -0,0 +1,46 @@ +package org.schabi.newpipe.extractor; + +/** + * Created by Christian Schabesberger on 04.03.16. + * + * Copyright (C) Christian Schabesberger 2016 + * AudioStream.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 . + */ + +public class AudioStream { + public String url = ""; + public int format = -1; + public int bandwidth = -1; + public int sampling_rate = -1; + + public AudioStream(String url, int format, int bandwidth, int samplingRate) { + this.url = url; this.format = format; + this.bandwidth = bandwidth; this.sampling_rate = samplingRate; + } + + // reveals wether two streams are the same, but have diferent urls + public boolean equalStats(AudioStream cmp) { + return format == cmp.format + && bandwidth == cmp.bandwidth + && sampling_rate == cmp.sampling_rate; + } + + // revelas wether two streams are equal + public boolean equals(AudioStream cmp) { + return cmp != null && equalStats(cmp) + && url == cmp.url; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/crawler/DashMpdParser.java b/app/src/main/java/org/schabi/newpipe/extractor/DashMpdParser.java similarity index 93% rename from app/src/main/java/org/schabi/newpipe/crawler/DashMpdParser.java rename to app/src/main/java/org/schabi/newpipe/extractor/DashMpdParser.java index 7758a24ee..a834d0b9b 100644 --- a/app/src/main/java/org/schabi/newpipe/crawler/DashMpdParser.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/DashMpdParser.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.crawler; +package org.schabi.newpipe.extractor; import android.util.Xml; @@ -31,13 +31,16 @@ import java.util.Vector; public class DashMpdParser { + private DashMpdParser() { + } + static class DashMpdParsingException extends ParsingException { DashMpdParsingException(String message, Exception e) { super(message, e); } } - public static List getAudioStreams(String dashManifestUrl, + public static List getAudioStreams(String dashManifestUrl, Downloader downloader) throws DashMpdParsingException { String dashDoc; @@ -46,7 +49,7 @@ public class DashMpdParser { } catch(IOException ioe) { throw new DashMpdParsingException("Could not get dash mpd: " + dashManifestUrl, ioe); } - Vector audioStreams = new Vector<>(); + Vector audioStreams = new Vector<>(); try { XmlPullParser parser = Xml.newPullParser(); parser.setInput(new StringReader(dashDoc)); @@ -83,7 +86,7 @@ public class DashMpdParser { } else if(currentMimeType.equals(MediaFormat.M4A.mimeType)) { format = MediaFormat.M4A.id; } - audioStreams.add(new VideoInfo.AudioStream(parser.getText(), + audioStreams.add(new AudioStream(parser.getText(), format, currentBandwidth, currentSamplingRate)); } break; diff --git a/app/src/main/java/org/schabi/newpipe/crawler/Downloader.java b/app/src/main/java/org/schabi/newpipe/extractor/Downloader.java similarity index 97% rename from app/src/main/java/org/schabi/newpipe/crawler/Downloader.java rename to app/src/main/java/org/schabi/newpipe/extractor/Downloader.java index 8732c0372..a4cde400d 100644 --- a/app/src/main/java/org/schabi/newpipe/crawler/Downloader.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/Downloader.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.crawler; +package org.schabi.newpipe.extractor; import java.io.IOException; diff --git a/app/src/main/java/org/schabi/newpipe/crawler/CrawlingException.java b/app/src/main/java/org/schabi/newpipe/extractor/ExtractionException.java similarity index 72% rename from app/src/main/java/org/schabi/newpipe/crawler/CrawlingException.java rename to app/src/main/java/org/schabi/newpipe/extractor/ExtractionException.java index 291670953..fbbca89c1 100644 --- a/app/src/main/java/org/schabi/newpipe/crawler/CrawlingException.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/ExtractionException.java @@ -1,10 +1,10 @@ -package org.schabi.newpipe.crawler; +package org.schabi.newpipe.extractor; /** * Created by Christian Schabesberger on 30.01.16. * * Copyright (C) Christian Schabesberger 2016 - * CrawlingException.java is part of NewPipe. + * ExtractionException.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 @@ -20,18 +20,16 @@ package org.schabi.newpipe.crawler; * along with NewPipe. If not, see . */ -public class CrawlingException extends Exception { - public CrawlingException() {} - - public CrawlingException(String message) { +public class ExtractionException extends Exception { + public ExtractionException(String message) { super(message); } - public CrawlingException(Throwable cause) { + public ExtractionException(Throwable cause) { super(cause); } - public CrawlingException(String message, Throwable cause) { + public ExtractionException(String message, Throwable cause) { super(message, cause); } } \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/crawler/MediaFormat.java b/app/src/main/java/org/schabi/newpipe/extractor/MediaFormat.java similarity index 98% rename from app/src/main/java/org/schabi/newpipe/crawler/MediaFormat.java rename to app/src/main/java/org/schabi/newpipe/extractor/MediaFormat.java index 938c6310b..1455fe396 100644 --- a/app/src/main/java/org/schabi/newpipe/crawler/MediaFormat.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/MediaFormat.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.crawler; +package org.schabi.newpipe.extractor; /** * Created by Adam Howard on 08/11/15. diff --git a/app/src/main/java/org/schabi/newpipe/crawler/Parser.java b/app/src/main/java/org/schabi/newpipe/extractor/Parser.java similarity index 85% rename from app/src/main/java/org/schabi/newpipe/crawler/Parser.java rename to app/src/main/java/org/schabi/newpipe/extractor/Parser.java index 56eec5c62..211c1ed47 100644 --- a/app/src/main/java/org/schabi/newpipe/crawler/Parser.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/Parser.java @@ -1,4 +1,6 @@ -package org.schabi.newpipe.crawler; +package org.schabi.newpipe.extractor; + +import android.util.Log; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; @@ -30,6 +32,9 @@ import java.util.regex.Pattern; /** avoid using regex !!! */ public class Parser { + private Parser() { + } + public static class RegexException extends ParsingException { public RegexException(String message) { super(message); @@ -52,8 +57,12 @@ public class Parser { public static Map compatParseMap(final String input) throws UnsupportedEncodingException { Map map = new HashMap<>(); for(String arg : input.split("&")) { - String[] split_arg = arg.split("="); - map.put(split_arg[0], URLDecoder.decode(split_arg[1], "UTF-8")); + String[] splitArg = arg.split("="); + if(splitArg.length > 1) { + map.put(splitArg[0], URLDecoder.decode(splitArg[1], "UTF-8")); + } else { + map.put(splitArg[0], ""); + } } return map; } diff --git a/app/src/main/java/org/schabi/newpipe/crawler/ParsingException.java b/app/src/main/java/org/schabi/newpipe/extractor/ParsingException.java similarity index 83% rename from app/src/main/java/org/schabi/newpipe/crawler/ParsingException.java rename to app/src/main/java/org/schabi/newpipe/extractor/ParsingException.java index 25d46b119..5803537bb 100644 --- a/app/src/main/java/org/schabi/newpipe/crawler/ParsingException.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/ParsingException.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.crawler; +package org.schabi.newpipe.extractor; /** * Created by Christian Schabesberger on 31.01.16. @@ -21,14 +21,10 @@ package org.schabi.newpipe.crawler; */ -public class ParsingException extends CrawlingException { - public ParsingException() {} +public class ParsingException extends ExtractionException { public ParsingException(String message) { super(message); } - public ParsingException(Throwable cause) { - super(cause); - } public ParsingException(String message, Throwable cause) { super(message, cause); } diff --git a/app/src/main/java/org/schabi/newpipe/crawler/SearchEngine.java b/app/src/main/java/org/schabi/newpipe/extractor/SearchEngine.java similarity index 52% rename from app/src/main/java/org/schabi/newpipe/crawler/SearchEngine.java rename to app/src/main/java/org/schabi/newpipe/extractor/SearchEngine.java index 845c78926..851025c28 100644 --- a/app/src/main/java/org/schabi/newpipe/crawler/SearchEngine.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/SearchEngine.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.crawler; +package org.schabi.newpipe.extractor; import java.io.IOException; import java.util.ArrayList; @@ -26,17 +26,29 @@ import java.util.Vector; */ @SuppressWarnings("ALL") -public interface SearchEngine { - class Result { - public String errorMessage = ""; - public String suggestion = ""; - public final List resultList = new Vector<>(); +public abstract class SearchEngine { + public static class NothingFoundException extends ExtractionException { + public NothingFoundException(String message) { + super(message); + } } - ArrayList suggestionList(String query, Downloader dl) - throws CrawlingException, IOException; + private StreamPreviewInfoCollector collector; + + public SearchEngine(StreamUrlIdHandler urlIdHandler, int serviceId) { + collector = new StreamPreviewInfoCollector(urlIdHandler, serviceId); + } + + public StreamPreviewInfoCollector getStreamPreviewInfoCollector() { + return collector; + } + + public abstract ArrayList suggestionList( + String query,String contentCountry, Downloader dl) + throws ExtractionException, IOException; //Result search(String query, int page); - Result search(String query, int page, String contentCountry, Downloader dl) - throws CrawlingException, IOException; + public abstract StreamPreviewInfoCollector search( + String query, int page, String contentCountry, Downloader dl) + throws ExtractionException, IOException; } diff --git a/app/src/main/java/org/schabi/newpipe/extractor/SearchResult.java b/app/src/main/java/org/schabi/newpipe/extractor/SearchResult.java new file mode 100644 index 000000000..d21e2ba62 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/extractor/SearchResult.java @@ -0,0 +1,47 @@ +package org.schabi.newpipe.extractor; + +import java.io.IOException; +import java.util.List; +import java.util.Vector; + +/** + * Created by Christian Schabesberger on 29.02.16. + * + * Copyright (C) Christian Schabesberger 2016 + * SearchResult.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 . + */ + +public class SearchResult { + public static SearchResult getSearchResult(SearchEngine engine, String query, + int page, String languageCode, Downloader dl) + throws ExtractionException, IOException { + + SearchResult result = engine.search(query, page, languageCode, dl).getSearchResult(); + if(result.resultList.isEmpty()) { + if(result.suggestion.isEmpty()) { + throw new ExtractionException("Empty result despite no error"); + } else { + // This is used as a fallback. Do not relay on it !!! + throw new SearchEngine.NothingFoundException(result.suggestion); + } + } + return result; + } + + public String suggestion = ""; + public final List resultList = new Vector<>(); + public List errors = new Vector<>(); +} diff --git a/app/src/main/java/org/schabi/newpipe/crawler/ServiceList.java b/app/src/main/java/org/schabi/newpipe/extractor/ServiceList.java similarity index 61% rename from app/src/main/java/org/schabi/newpipe/crawler/ServiceList.java rename to app/src/main/java/org/schabi/newpipe/extractor/ServiceList.java index b1d98a73f..63ccc5cb2 100644 --- a/app/src/main/java/org/schabi/newpipe/crawler/ServiceList.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/ServiceList.java @@ -1,8 +1,8 @@ -package org.schabi.newpipe.crawler; +package org.schabi.newpipe.extractor; import android.util.Log; -import org.schabi.newpipe.crawler.services.youtube.YoutubeService; +import org.schabi.newpipe.extractor.services.youtube.YoutubeService; /** * Created by Christian Schabesberger on 23.08.15. @@ -29,26 +29,43 @@ import org.schabi.newpipe.crawler.services.youtube.YoutubeService; @SuppressWarnings("ALL") public class ServiceList { + + private ServiceList() { + } + private static final String TAG = ServiceList.class.toString(); private static final StreamingService[] services = { - new YoutubeService() + new YoutubeService(0) }; public static StreamingService[] getServices() { return services; } - public static StreamingService getService(int serviceId) { - return services[serviceId]; + public static StreamingService getService(int serviceId) throws ExtractionException { + for(StreamingService s : services) { + if(s.getServiceId() == serviceId) { + return s; + } + } + throw new ExtractionException("Service not known: " + Integer.toString(serviceId)); } - public static StreamingService getService(String serviceName) { + public static StreamingService getService(String serviceName) throws ExtractionException { return services[getIdOfService(serviceName)]; } - public static int getIdOfService(String serviceName) { + public static String getNameOfService(int id) { + try { + return getService(id).getServiceInfo().name; + } catch (Exception e) { + System.err.println("Service id not known"); + e.printStackTrace(); + return ""; + } + } + public static int getIdOfService(String serviceName) throws ExtractionException { for(int i = 0; i < services.length; i++) { if(services[i].getServiceInfo().name.equals(serviceName)) { return i; } } - Log.e(TAG, "Error: Service " + serviceName + " not known."); - return -1; + throw new ExtractionException("Error: Service " + serviceName + " not known."); } } diff --git a/app/src/main/java/org/schabi/newpipe/crawler/StreamExtractor.java b/app/src/main/java/org/schabi/newpipe/extractor/StreamExtractor.java similarity index 69% rename from app/src/main/java/org/schabi/newpipe/crawler/StreamExtractor.java rename to app/src/main/java/org/schabi/newpipe/extractor/StreamExtractor.java index a363269cb..a3aed0363 100644 --- a/app/src/main/java/org/schabi/newpipe/crawler/StreamExtractor.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/StreamExtractor.java @@ -1,9 +1,9 @@ -package org.schabi.newpipe.crawler; +package org.schabi.newpipe.extractor; /** * Created by Christian Schabesberger on 10.08.15. * - * Copyright (C) Christian Schabesberger 2015 + * Copyright (C) Christian Schabesberger 2016 * StreamExtractor.java is part of NewPipe. * * NewPipe is free software: you can redistribute it and/or modify @@ -26,10 +26,11 @@ import java.util.List; @SuppressWarnings("ALL") -public interface StreamExtractor { +public abstract class StreamExtractor { - public class ExctractorInitException extends CrawlingException { - public ExctractorInitException() {} + private int serviceId; + + public class ExctractorInitException extends ExtractionException { public ExctractorInitException(String message) { super(message); } @@ -42,37 +43,41 @@ public interface StreamExtractor { } public class ContentNotAvailableException extends ParsingException { - public ContentNotAvailableException() {} public ContentNotAvailableException(String message) { super(message); } - public ContentNotAvailableException(Throwable cause) { - super(cause); - } public ContentNotAvailableException(String message, Throwable cause) { super(message, cause); } } + public StreamExtractor(String url, Downloader dl, int serviceId) { + this.serviceId = serviceId; + } + public abstract int getTimeStamp() throws ParsingException; public abstract String getTitle() throws ParsingException; public abstract String getDescription() throws ParsingException; public abstract String getUploader() throws ParsingException; public abstract int getLength() throws ParsingException; - public abstract long getViews() throws ParsingException; + public abstract long getViewCount() throws ParsingException; public abstract String getUploadDate() throws ParsingException; public abstract String getThumbnailUrl() throws ParsingException; public abstract String getUploaderThumbnailUrl() throws ParsingException; - public abstract List getAudioStreams() throws ParsingException; - public abstract List getVideoStreams() throws ParsingException; - public abstract List getVideoOnlyStreams() throws ParsingException; + public abstract List getAudioStreams() throws ParsingException; + public abstract List getVideoStreams() throws ParsingException; + public abstract List getVideoOnlyStreams() throws ParsingException; public abstract String getDashMpdUrl() throws ParsingException; public abstract int getAgeLimit() throws ParsingException; public abstract String getAverageRating() throws ParsingException; public abstract int getLikeCount() throws ParsingException; public abstract int getDislikeCount() throws ParsingException; - public abstract VideoPreviewInfo getNextVideo() throws ParsingException; - public abstract List getRelatedVideos() throws ParsingException; - public abstract VideoUrlIdHandler getUrlIdConverter(); + public abstract StreamPreviewInfo getNextVideo() throws ParsingException; + public abstract List getRelatedVideos() throws ParsingException; + public abstract StreamUrlIdHandler getUrlIdConverter(); public abstract String getPageUrl(); + public abstract StreamInfo.StreamType getStreamType() throws ParsingException; + public int getServiceId() { + return serviceId; + } } diff --git a/app/src/main/java/org/schabi/newpipe/extractor/StreamInfo.java b/app/src/main/java/org/schabi/newpipe/extractor/StreamInfo.java new file mode 100644 index 000000000..2090ef538 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/extractor/StreamInfo.java @@ -0,0 +1,268 @@ +package org.schabi.newpipe.extractor; + +import java.io.IOException; +import java.util.List; +import java.util.Vector; + +/** + * Created by Christian Schabesberger on 26.08.15. + * + * Copyright (C) Christian Schabesberger 2016 + * StreamInfo.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 . + */ + +/**Info object for opened videos, ie the video ready to play.*/ +@SuppressWarnings("ALL") +public class StreamInfo extends AbstractVideoInfo { + + public static class StreamExctractException extends ExtractionException { + StreamExctractException(String message) { + super(message); + } + } + + public StreamInfo() {} + + /**Creates a new StreamInfo object from an existing AbstractVideoInfo. + * All the shared properties are copied to the new StreamInfo.*/ + @SuppressWarnings("WeakerAccess") + public StreamInfo(AbstractVideoInfo avi) { + this.id = avi.id; + this.title = avi.title; + this.uploader = avi.uploader; + this.thumbnail_url = avi.thumbnail_url; + this.thumbnail = avi.thumbnail; + this.webpage_url = avi.webpage_url; + this.upload_date = avi.upload_date; + this.upload_date = avi.upload_date; + this.view_count = avi.view_count; + + //todo: better than this + if(avi instanceof StreamPreviewInfo) { + //shitty String to convert code + /* + String dur = ((StreamPreviewInfo)avi).duration; + int minutes = Integer.parseInt(dur.substring(0, dur.indexOf(":"))); + int seconds = Integer.parseInt(dur.substring(dur.indexOf(":")+1, dur.length())); + */ + this.duration = ((StreamPreviewInfo)avi).duration; + } + } + + public void addException(Exception e) { + errors.add(e); + } + + /**Fills out the video info fields which are common to all services. + * Probably needs to be overridden by subclasses*/ + public static StreamInfo getVideoInfo(StreamExtractor extractor, Downloader downloader) + throws ExtractionException, IOException { + StreamInfo streamInfo = new StreamInfo(); + + streamInfo = extractImportantData(streamInfo, extractor, downloader); + streamInfo = extractStreams(streamInfo, extractor, downloader); + streamInfo = extractOptionalData(streamInfo, extractor, downloader); + + return streamInfo; + } + + private static StreamInfo extractImportantData( + StreamInfo streamInfo, StreamExtractor extractor, Downloader downloader) + throws ExtractionException, IOException { + /* ---- importand data, withoug the video can't be displayed goes here: ---- */ + // if one of these is not available an exception is ment to be thrown directly into the frontend. + + StreamUrlIdHandler uiconv = extractor.getUrlIdConverter(); + + streamInfo.service_id = extractor.getServiceId(); + streamInfo.webpage_url = extractor.getPageUrl(); + streamInfo.stream_type = extractor.getStreamType(); + streamInfo.id = uiconv.getVideoId(extractor.getPageUrl()); + streamInfo.title = extractor.getTitle(); + streamInfo.age_limit = extractor.getAgeLimit(); + + if((streamInfo.stream_type == StreamType.NONE) + || (streamInfo.webpage_url == null || streamInfo.webpage_url.isEmpty()) + || (streamInfo.id == null || streamInfo.id.isEmpty()) + || (streamInfo.title == null /* streamInfo.title can be empty of course */) + || (streamInfo.age_limit == -1)) { + throw new ExtractionException("Some importand stream information was not given."); + } + + return streamInfo; + } + + private static StreamInfo extractStreams( + StreamInfo streamInfo, StreamExtractor extractor, Downloader downloader) + throws ExtractionException, IOException { + /* ---- stream extraction goes here ---- */ + // At least one type of stream has to be available, + // otherwise an exception will be thrown directly into the frontend. + + try { + streamInfo.dashMpdUrl = extractor.getDashMpdUrl(); + } catch(Exception e) { + streamInfo.addException(new ExtractionException("Couldn't get Dash manifest", e)); + } + + /* Load and extract audio */ + try { + streamInfo.audio_streams = extractor.getAudioStreams(); + } catch(Exception e) { + streamInfo.addException(new ExtractionException("Couldn't get audio streams", e)); + } + // also try to get streams from the dashMpd + if(streamInfo.dashMpdUrl != null && !streamInfo.dashMpdUrl.isEmpty()) { + if(streamInfo.audio_streams == null) { + streamInfo.audio_streams = new Vector<>(); + } + //todo: make this quick and dirty solution a real fallback + // same as the quick and dirty aboth + try { + streamInfo.audio_streams.addAll( + DashMpdParser.getAudioStreams(streamInfo.dashMpdUrl, downloader)); + } catch(Exception e) { + streamInfo.addException( + new ExtractionException("Couldn't get audio streams from dash mpd", e)); + } + } + /* Extract video stream url*/ + try { + streamInfo.video_streams = extractor.getVideoStreams(); + } catch (Exception e) { + streamInfo.addException( + new ExtractionException("Couldn't get video streams", e)); + } + /* Extract video only stream url*/ + try { + streamInfo.video_only_streams = extractor.getVideoOnlyStreams(); + } catch(Exception e) { + streamInfo.addException( + new ExtractionException("Couldn't get video only streams", e)); + } + + // either dash_mpd audio_only or video has to be available, otherwise we didn't get a stream, + // and therefore failed. (Since video_only_streams are just optional they don't caunt). + if((streamInfo.video_streams == null || streamInfo.video_streams.isEmpty()) + && (streamInfo.audio_streams == null || streamInfo.audio_streams.isEmpty()) + && (streamInfo.dashMpdUrl == null || streamInfo.dashMpdUrl.isEmpty())) { + throw new StreamExctractException( + "Could not get any stream. See error variable to get further details."); + } + + return streamInfo; + } + + private static StreamInfo extractOptionalData( + StreamInfo streamInfo, StreamExtractor extractor, Downloader downloader) { + /* ---- optional data goes here: ---- */ + // If one of these failes, the frontend neets to handle that they are not available. + // Exceptions are therfore not thrown into the frontend, but stored into the error List, + // so the frontend can afterwads check where errors happend. + + try { + streamInfo.thumbnail_url = extractor.getThumbnailUrl(); + } catch(Exception e) { + streamInfo.addException(e); + } + try { + streamInfo.duration = extractor.getLength(); + } catch(Exception e) { + streamInfo.addException(e); + } + try { + streamInfo.uploader = extractor.getUploader(); + } catch(Exception e) { + streamInfo.addException(e); + } + try { + streamInfo.description = extractor.getDescription(); + } catch(Exception e) { + streamInfo.addException(e); + } + try { + streamInfo.view_count = extractor.getViewCount(); + } catch(Exception e) { + streamInfo.addException(e); + } + try { + streamInfo.upload_date = extractor.getUploadDate(); + } catch(Exception e) { + streamInfo.addException(e); + } + try { + streamInfo.uploader_thumbnail_url = extractor.getUploaderThumbnailUrl(); + } catch(Exception e) { + streamInfo.addException(e); + } + try { + streamInfo.start_position = extractor.getTimeStamp(); + } catch(Exception e) { + streamInfo.addException(e); + } + try { + streamInfo.average_rating = extractor.getAverageRating(); + } catch(Exception e) { + streamInfo.addException(e); + } + try { + streamInfo.like_count = extractor.getLikeCount(); + } catch(Exception e) { + streamInfo.addException(e); + } + try { + streamInfo.dislike_count = extractor.getDislikeCount(); + } catch(Exception e) { + streamInfo.addException(e); + } + try { + streamInfo.next_video = extractor.getNextVideo(); + } catch(Exception e) { + streamInfo.addException(e); + } + try { + streamInfo.related_videos = extractor.getRelatedVideos(); + } catch(Exception e) { + streamInfo.addException(e); + } + + return streamInfo; + } + + public String uploader_thumbnail_url = ""; + public String description = ""; + + public List video_streams = null; + public List audio_streams = null; + public List video_only_streams = null; + // video streams provided by the dash mpd do not need to be provided as VideoStream. + // Later on this will also aplly to audio streams. Since dash mpd is standarized, + // crawling such a file is not service dependent. Therefore getting audio only streams by yust + // providing the dash mpd fille will be possible in the future. + public String dashMpdUrl = ""; + public int duration = -1; + + public int age_limit = -1; + public int like_count = -1; + public int dislike_count = -1; + public String average_rating = ""; + public StreamPreviewInfo next_video = null; + public List related_videos = null; + //in seconds. some metadata is not passed using a StreamInfo object! + public int start_position = 0; + + public List errors = new Vector<>(); +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/extractor/StreamPreviewInfo.java b/app/src/main/java/org/schabi/newpipe/extractor/StreamPreviewInfo.java new file mode 100644 index 000000000..5b0ff7c57 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/extractor/StreamPreviewInfo.java @@ -0,0 +1,26 @@ +package org.schabi.newpipe.extractor; + +/** + * Created by Christian Schabesberger on 26.08.15. + * + * Copyright (C) Christian Schabesberger 2016 + * StreamPreviewInfo.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 . + */ + +/**Info object for previews of unopened videos, eg search results, related videos*/ +public class StreamPreviewInfo extends AbstractVideoInfo { + public int duration = 0; +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/extractor/StreamPreviewInfoCollector.java b/app/src/main/java/org/schabi/newpipe/extractor/StreamPreviewInfoCollector.java new file mode 100644 index 000000000..70c2feb77 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/extractor/StreamPreviewInfoCollector.java @@ -0,0 +1,94 @@ +package org.schabi.newpipe.extractor; + +import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamUrlIdHandler; + +/** + * Created by Christian Schabesberger on 28.02.16. + * + * Copyright (C) Christian Schabesberger 2016 + * StreamPreviewInfoCollector.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 . + */ + +public class StreamPreviewInfoCollector { + private SearchResult result = new SearchResult(); + private StreamUrlIdHandler urlIdHandler = null; + private int serviceId = -1; + + public StreamPreviewInfoCollector(StreamUrlIdHandler handler, int serviceId) { + urlIdHandler = handler; + this.serviceId = serviceId; + } + + public void setSuggestion(String suggestion) { + result.suggestion = suggestion; + } + + public void addError(Exception e) { + result.errors.add(e); + } + + public SearchResult getSearchResult() { + return result; + } + + public void commit(StreamPreviewInfoExtractor extractor) throws ParsingException { + try { + StreamPreviewInfo resultItem = new StreamPreviewInfo(); + // importand information + resultItem.service_id = serviceId; + resultItem.webpage_url = extractor.getWebPageUrl(); + if (urlIdHandler == null) { + throw new ParsingException("Error: UrlIdHandler not set"); + } else { + resultItem.id = (new YoutubeStreamUrlIdHandler()).getVideoId(resultItem.webpage_url); + } + resultItem.title = extractor.getTitle(); + resultItem.stream_type = extractor.getStreamType(); + + // optional iformation + try { + resultItem.duration = extractor.getDuration(); + } catch (Exception e) { + addError(e); + } + try { + resultItem.uploader = extractor.getUploader(); + } catch (Exception e) { + addError(e); + } + try { + resultItem.upload_date = extractor.getUploadDate(); + } catch (Exception e) { + addError(e); + } + try { + resultItem.view_count = extractor.getViewCount(); + } catch (Exception e) { + addError(e); + } + try { + resultItem.thumbnail_url = extractor.getThumbnailUrl(); + } catch (Exception e) { + addError(e); + } + + result.resultList.add(resultItem); + } catch (Exception e) { + addError(e); + } + + } +} diff --git a/app/src/main/java/org/schabi/newpipe/extractor/StreamPreviewInfoExtractor.java b/app/src/main/java/org/schabi/newpipe/extractor/StreamPreviewInfoExtractor.java new file mode 100644 index 000000000..bea8eb4e6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/extractor/StreamPreviewInfoExtractor.java @@ -0,0 +1,32 @@ +package org.schabi.newpipe.extractor; + +/** + * Created by Christian Schabesberger on 28.02.16. + * + * Copyright (C) Christian Schabesberger 2016 + * StreamPreviewInfoExtractor.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 . + */ + +public interface StreamPreviewInfoExtractor { + AbstractVideoInfo.StreamType getStreamType() throws ParsingException; + String getWebPageUrl() throws ParsingException; + String getTitle() throws ParsingException; + int getDuration() throws ParsingException; + String getUploader() throws ParsingException; + String getUploadDate() throws ParsingException; + long getViewCount() throws ParsingException; + String getThumbnailUrl() throws ParsingException; +} diff --git a/app/src/main/java/org/schabi/newpipe/crawler/VideoUrlIdHandler.java b/app/src/main/java/org/schabi/newpipe/extractor/StreamUrlIdHandler.java similarity index 90% rename from app/src/main/java/org/schabi/newpipe/crawler/VideoUrlIdHandler.java rename to app/src/main/java/org/schabi/newpipe/extractor/StreamUrlIdHandler.java index 66d8c3cd8..4f91ac80e 100644 --- a/app/src/main/java/org/schabi/newpipe/crawler/VideoUrlIdHandler.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/StreamUrlIdHandler.java @@ -1,10 +1,10 @@ -package org.schabi.newpipe.crawler; +package org.schabi.newpipe.extractor; /** * Created by Christian Schabesberger on 02.02.16. * * Copyright (C) Christian Schabesberger 2016 - * VideoUrlIdHandler.java is part of NewPipe. + * StreamUrlIdHandler.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 @@ -20,7 +20,7 @@ package org.schabi.newpipe.crawler; * along with NewPipe. If not, see . */ -public interface VideoUrlIdHandler { +public interface StreamUrlIdHandler { String getVideoUrl(String videoId); String getVideoId(String siteUrl) throws ParsingException; String cleanUrl(String siteUrl) throws ParsingException; diff --git a/app/src/main/java/org/schabi/newpipe/crawler/StreamingService.java b/app/src/main/java/org/schabi/newpipe/extractor/StreamingService.java similarity index 54% rename from app/src/main/java/org/schabi/newpipe/crawler/StreamingService.java rename to app/src/main/java/org/schabi/newpipe/extractor/StreamingService.java index 6c87a5c7b..de9a4ebfe 100644 --- a/app/src/main/java/org/schabi/newpipe/crawler/StreamingService.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/StreamingService.java @@ -1,11 +1,11 @@ -package org.schabi.newpipe.crawler; +package org.schabi.newpipe.extractor; import java.io.IOException; /** * Created by Christian Schabesberger on 23.08.15. * - * Copyright (C) Christian Schabesberger 2015 + * Copyright (C) Christian Schabesberger 2016 * StreamingService.java is part of NewPipe. * * NewPipe is free software: you can redistribute it and/or modify @@ -22,16 +22,25 @@ import java.io.IOException; * along with NewPipe. If not, see . */ -public interface StreamingService { - class ServiceInfo { +public abstract class StreamingService { + public class ServiceInfo { public String name = ""; } - ServiceInfo getServiceInfo(); - StreamExtractor getExtractorInstance(String url, Downloader downloader) - throws IOException, CrawlingException; - SearchEngine getSearchEngineInstance(); - VideoUrlIdHandler getUrlIdHandler(); + private int serviceId; + public StreamingService(int id) { + serviceId = id; + } + public abstract ServiceInfo getServiceInfo(); + + public abstract StreamExtractor getExtractorInstance(String url, Downloader downloader) + throws IOException, ExtractionException; + public abstract SearchEngine getSearchEngineInstance(Downloader downloader); + public abstract StreamUrlIdHandler getUrlIdHandlerInstance(); + + public final int getServiceId() { + return serviceId; + } } diff --git a/app/src/main/java/org/schabi/newpipe/extractor/VideoStream.java b/app/src/main/java/org/schabi/newpipe/extractor/VideoStream.java new file mode 100644 index 000000000..b1642f2c6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/extractor/VideoStream.java @@ -0,0 +1,44 @@ +package org.schabi.newpipe.extractor; + +/** + * Created by Christian Schabesberger on 04.03.16. + * + * Copyright (C) Christian Schabesberger 2016 + * VideoStream.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 . + */ + +public class VideoStream { + //url of the stream + public String url = ""; + public int format = -1; + public String resolution = ""; + + public VideoStream(String url, int format, String res) { + this.url = url; this.format = format; resolution = res; + } + + // reveals wether two streams are the same, but have diferent urls + public boolean equalStats(VideoStream cmp) { + return format == cmp.format + && resolution == cmp.resolution; + } + + // revelas wether two streams are equal + public boolean equals(VideoStream cmp) { + return cmp != null && equalStats(cmp) + && url == cmp.url; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java new file mode 100644 index 000000000..11e2d564d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java @@ -0,0 +1,65 @@ +package org.schabi.newpipe.extractor.services.youtube; + +import org.schabi.newpipe.extractor.ParsingException; + +/** + * Created by Christian Schabesberger on 02.03.16. + * + * Copyright (C) Christian Schabesberger 2016 + * YoutubeParsingHelper.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 . + */ + +public class YoutubeParsingHelper { + + private YoutubeParsingHelper() { + } + + public static int parseDurationString(String input) + throws ParsingException, NumberFormatException { + String[] splitInput = input.split(":"); + String days = "0"; + String hours = "0"; + String minutes = "0"; + String seconds; + + switch(splitInput.length) { + case 4: + days = splitInput[0]; + hours = splitInput[1]; + minutes = splitInput[2]; + seconds = splitInput[3]; + break; + case 3: + hours = splitInput[0]; + minutes = splitInput[1]; + seconds = splitInput[2]; + break; + case 2: + minutes = splitInput[0]; + seconds = splitInput[1]; + break; + case 1: + seconds = splitInput[0]; + break; + default: + throw new ParsingException("Error duration string with unknown format: " + input); + } + return ((((Integer.parseInt(days) * 24) + + Integer.parseInt(hours) * 60) + + Integer.parseInt(minutes)) * 60) + + Integer.parseInt(seconds); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeSearchEngine.java b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeSearchEngine.java new file mode 100644 index 000000000..bd1f5107c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeSearchEngine.java @@ -0,0 +1,193 @@ +package org.schabi.newpipe.extractor.services.youtube; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.schabi.newpipe.extractor.AbstractVideoInfo; +import org.schabi.newpipe.extractor.Downloader; +import org.schabi.newpipe.extractor.ExtractionException; +import org.schabi.newpipe.extractor.Parser; +import org.schabi.newpipe.extractor.ParsingException; +import org.schabi.newpipe.extractor.SearchEngine; +import org.schabi.newpipe.extractor.StreamPreviewInfoCollector; +import org.schabi.newpipe.extractor.StreamPreviewInfoExtractor; +import org.schabi.newpipe.extractor.StreamUrlIdHandler; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import java.net.URLEncoder; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +/** + * Created by Christian Schabesberger on 09.08.15. + * + * Copyright (C) Christian Schabesberger 2015 + * YoutubeSearchEngine.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 . + */ + +public class YoutubeSearchEngine extends SearchEngine { + + private static final String TAG = YoutubeSearchEngine.class.toString(); + + public YoutubeSearchEngine(StreamUrlIdHandler urlIdHandler, int serviceId) { + super(urlIdHandler, serviceId); + } + + @Override + public StreamPreviewInfoCollector search(String query, int page, String languageCode, Downloader downloader) + throws IOException, ExtractionException { + StreamPreviewInfoCollector collector = getStreamPreviewInfoCollector(); + + /* Cant use Uri.Bilder since it's android code. + // Android code is baned from the extractor side. + Uri.Builder builder = new Uri.Builder(); + builder.scheme("https") + .authority("www.youtube.com") + .appendPath("results") + .appendQueryParameter("search_query", query) + .appendQueryParameter("page", Integer.toString(page)) + .appendQueryParameter("filters", "video"); + */ + + String url = "https://www.youtube.com/results" + + "?search_query=" + URLEncoder.encode(query, "UTF-8") + + "&page=" + Integer.toString(page) + + "&filters=" + "video"; + + String site; + //String url = builder.build().toString(); + //if we've been passed a valid language code, append it to the URL + if(!languageCode.isEmpty()) { + //assert Pattern.matches("[a-z]{2}(-([A-Z]{2}|[0-9]{1,3}))?", languageCode); + site = downloader.download(url, languageCode); + } + else { + site = downloader.download(url); + } + + + Document doc = Jsoup.parse(site, url); + Element list = doc.select("ol[class=\"item-section\"]").first(); + + for (Element item : list.children()) { + /* First we need to determine which kind of item we are working with. + Youtube depicts five different kinds of items on its search result page. These are + regular videos, playlists, channels, two types of video suggestions, and a "no video + found" item. Since we only want videos, we need to filter out all the others. + An example for this can be seen here: + https://www.youtube.com/results?search_query=asdf&page=1 + + We already applied a filter to the url, so we don't need to care about channels and + playlists now. + */ + + Element el; + + // both types of spell correction item + if (!((el = item.select("div[class*=\"spell-correction\"]").first()) == null)) { + collector.setSuggestion(el.select("a").first().text()); + if(list.children().size() == 1) { + throw new NothingFoundException("Did you mean: " + el.select("a").first().text()); + } + // search message item + } else if (!((el = item.select("div[class*=\"search-message\"]").first()) == null)) { + //result.errorMessage = el.text(); + throw new NothingFoundException(el.text()); + + // video item type + } else if (!((el = item.select("div[class*=\"yt-lockup-video\"").first()) == null)) { + collector.commit(extractPreviewInfo(el)); + } else { + //noinspection ConstantConditions + collector.addError(new Exception("unexpected element found:\"" + el + "\"")); + } + } + + return collector; + } + + @Override + public ArrayList suggestionList(String query, String contentCountry, Downloader dl) + throws IOException, ParsingException { + + ArrayList suggestions = new ArrayList<>(); + + /* Cant use Uri.Bilder since it's android code. + // Android code is baned from the extractor side. + Uri.Builder builder = new Uri.Builder(); + builder.scheme("https") + .authority("suggestqueries.google.com") + .appendPath("complete") + .appendPath("search") + .appendQueryParameter("client", "") + .appendQueryParameter("output", "toolbar") + .appendQueryParameter("ds", "yt") + .appendQueryParameter("hl",contentCountry) + .appendQueryParameter("q", query); + */ + String url = "https://suggestqueries.google.com/complete/search" + + "?client=" + "" + + "&output=" + "toolbar" + + "&ds=" + "yt" + + "&hl=" + URLEncoder.encode(contentCountry, "UTF-8") + + "&q=" + URLEncoder.encode(query, "UTF-8"); + + + String response = dl.download(url); + + //TODO: Parse xml data using Jsoup not done + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder; + org.w3c.dom.Document doc = null; + + try { + dBuilder = dbFactory.newDocumentBuilder(); + doc = dBuilder.parse(new InputSource( + new ByteArrayInputStream(response.getBytes("utf-8")))); + doc.getDocumentElement().normalize(); + } catch (ParserConfigurationException | SAXException | IOException e) { + throw new ParsingException("Could not parse document."); + } + + try { + NodeList nList = doc.getElementsByTagName("CompleteSuggestion"); + for (int temp = 0; temp < nList.getLength(); temp++) { + + NodeList nList1 = doc.getElementsByTagName("suggestion"); + Node nNode1 = nList1.item(temp); + if (nNode1.getNodeType() == Node.ELEMENT_NODE) { + org.w3c.dom.Element eElement = (org.w3c.dom.Element) nNode1; + suggestions.add(eElement.getAttribute("data")); + } + } + return suggestions; + } catch(Exception e) { + throw new ParsingException("Could not get suggestions form document.", e); + } + } + + private StreamPreviewInfoExtractor extractPreviewInfo(final Element item) { + return new YoutubeStreamPreviewInfoExtractor(item); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/crawler/services/youtube/YoutubeService.java b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeService.java similarity index 58% rename from app/src/main/java/org/schabi/newpipe/crawler/services/youtube/YoutubeService.java rename to app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeService.java index 43f673c63..94f9fb28e 100644 --- a/app/src/main/java/org/schabi/newpipe/crawler/services/youtube/YoutubeService.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeService.java @@ -1,11 +1,11 @@ -package org.schabi.newpipe.crawler.services.youtube; +package org.schabi.newpipe.extractor.services.youtube; -import org.schabi.newpipe.crawler.CrawlingException; -import org.schabi.newpipe.crawler.Downloader; -import org.schabi.newpipe.crawler.StreamExtractor; -import org.schabi.newpipe.crawler.StreamingService; -import org.schabi.newpipe.crawler.VideoUrlIdHandler; -import org.schabi.newpipe.crawler.SearchEngine; +import org.schabi.newpipe.extractor.ExtractionException; +import org.schabi.newpipe.extractor.Downloader; +import org.schabi.newpipe.extractor.StreamExtractor; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.StreamUrlIdHandler; +import org.schabi.newpipe.extractor.SearchEngine; import java.io.IOException; @@ -30,7 +30,12 @@ import java.io.IOException; * along with NewPipe. If not, see . */ -public class YoutubeService implements StreamingService { +public class YoutubeService extends StreamingService { + + public YoutubeService(int id) { + super(id); + } + @Override public ServiceInfo getServiceInfo() { ServiceInfo serviceInfo = new ServiceInfo(); @@ -39,22 +44,22 @@ public class YoutubeService implements StreamingService { } @Override public StreamExtractor getExtractorInstance(String url, Downloader downloader) - throws CrawlingException, IOException { - VideoUrlIdHandler urlIdHandler = new YoutubeVideoUrlIdHandler(); + throws ExtractionException, IOException { + StreamUrlIdHandler urlIdHandler = new YoutubeStreamUrlIdHandler(); if(urlIdHandler.acceptUrl(url)) { - return new YoutubeStreamExtractor(url, downloader) ; + return new YoutubeStreamExtractor(url, downloader, getServiceId()); } else { throw new IllegalArgumentException("supplied String is not a valid Youtube URL"); } } @Override - public SearchEngine getSearchEngineInstance() { - return new YoutubeSearchEngine(); + public SearchEngine getSearchEngineInstance(Downloader downloader) { + return new YoutubeSearchEngine(getUrlIdHandlerInstance(), getServiceId()); } @Override - public VideoUrlIdHandler getUrlIdHandler() { - return new YoutubeVideoUrlIdHandler(); + public StreamUrlIdHandler getUrlIdHandlerInstance() { + return new YoutubeStreamUrlIdHandler(); } } diff --git a/app/src/main/java/org/schabi/newpipe/crawler/services/youtube/YoutubeStreamExtractor.java b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamExtractor.java similarity index 69% rename from app/src/main/java/org/schabi/newpipe/crawler/services/youtube/YoutubeStreamExtractor.java rename to app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamExtractor.java index 78047398d..2f1b45785 100644 --- a/app/src/main/java/org/schabi/newpipe/crawler/services/youtube/YoutubeStreamExtractor.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamExtractor.java @@ -1,7 +1,4 @@ -package org.schabi.newpipe.crawler.services.youtube; - -import android.provider.MediaStore; -import android.util.Log; +package org.schabi.newpipe.extractor.services.youtube; import org.json.JSONException; import org.json.JSONObject; @@ -11,22 +8,24 @@ import org.jsoup.nodes.Element; import org.mozilla.javascript.Context; import org.mozilla.javascript.Function; import org.mozilla.javascript.ScriptableObject; -import org.schabi.newpipe.crawler.CrawlingException; -import org.schabi.newpipe.crawler.Downloader; -import org.schabi.newpipe.crawler.Parser; -import org.schabi.newpipe.crawler.ParsingException; -import org.schabi.newpipe.crawler.VideoUrlIdHandler; -import org.schabi.newpipe.crawler.StreamExtractor; -import org.schabi.newpipe.crawler.MediaFormat; -import org.schabi.newpipe.crawler.VideoInfo; -import org.schabi.newpipe.crawler.VideoPreviewInfo; +import org.schabi.newpipe.extractor.AudioStream; +import org.schabi.newpipe.extractor.ExtractionException; +import org.schabi.newpipe.extractor.Downloader; +import org.schabi.newpipe.extractor.Parser; +import org.schabi.newpipe.extractor.ParsingException; +import org.schabi.newpipe.extractor.StreamInfo; +import org.schabi.newpipe.extractor.StreamPreviewInfo; +import org.schabi.newpipe.extractor.StreamUrlIdHandler; +import org.schabi.newpipe.extractor.StreamExtractor; +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.VideoStream; import java.io.IOException; -import java.net.URLDecoder; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Vector; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Created by Christian Schabesberger on 06.08.15. @@ -48,7 +47,41 @@ import java.util.Vector; * along with NewPipe. If not, see . */ -public class YoutubeStreamExtractor implements StreamExtractor { +public class YoutubeStreamExtractor extends StreamExtractor { + + // exceptions + + public class DecryptException extends ParsingException { + DecryptException(String message, Throwable cause) { + super(message, cause); + } + } + + // special content not available exceptions + + public class GemaException extends ContentNotAvailableException { + GemaException(String message) { + super(message); + } + } + + public class LiveStreamException extends ContentNotAvailableException { + LiveStreamException(String message) { + super(message); + } + } + + // ---------------- + + // Sometimes if the html page of youtube is already downloaded, youtube web page will internally + // download the /get_video_info page. Since a certain date dashmpd url is only available over + // this /get_video_info page, so we always need to download this one to. + // %%video_id%% will be replaced by the actual video id + // $$el_type$$ will be replaced by the actual el_type (se the declarations below) + private static final String GET_VIDEO_INFO_URL = + "https://www.youtube.com/get_video_info?video_id=%%video_id%%$$el_type$$&ps=default&eurl=&gl=US&hl=en"; + // eltype is nececeary for the url aboth + private static final String EL_INFO = "el=info"; public enum ItagType { AUDIO, @@ -131,38 +164,10 @@ public class YoutubeStreamExtractor implements StreamExtractor { throw new ParsingException("itag=" + Integer.toString(itag) + " not supported"); } - // Sometimes if the html page of youtube is already downloaded, youtube web page will internally - // download the /get_video_info page. Since a certain date dashmpd url is only available over - // this /get_video_info page, so we always need to download this one to. - // %%video_id%% will be replaced by the actual video id - // $$el_type$$ will be replaced by the actual el_type (se the declarations below) - private static final String GET_VIDEO_INFO_URL = - "https://www.youtube.com/get_video_info?video_id=%%video_id%%$$el_type$$&ps=default&eurl=&gl=US&hl=en"; - // eltype is nececeary for the url aboth - private static final String EL_INFO = "el=info"; - - public class DecryptException extends ParsingException { - DecryptException(Throwable cause) { - super(cause); - } - DecryptException(String message, Throwable cause) { - super(message, cause); - } - } - - // special content not available exceptions - - public class GemaException extends ContentNotAvailableException { - GemaException(String message) { - super(message); - } - } - - // ---------------- - private static final String TAG = YoutubeStreamExtractor.class.toString(); private final Document doc; private JSONObject playerArgs; + private boolean isAgeRestricted; private Map videoInfoPage; // static values @@ -171,80 +176,140 @@ public class YoutubeStreamExtractor implements StreamExtractor { // cached values private static volatile String decryptionCode = ""; - VideoUrlIdHandler urlidhandler = new YoutubeVideoUrlIdHandler(); + StreamUrlIdHandler urlidhandler = new YoutubeStreamUrlIdHandler(); String pageUrl = ""; private Downloader downloader; - public YoutubeStreamExtractor(String pageUrl, Downloader dl) throws CrawlingException, IOException { + public YoutubeStreamExtractor(String pageUrl, Downloader dl, int serviceId) + throws ExtractionException, IOException { + super(pageUrl, dl, serviceId); //most common videoInfo fields are now set in our superclass, for all services downloader = dl; this.pageUrl = pageUrl; String pageContent = downloader.download(urlidhandler.cleanUrl(pageUrl)); doc = Jsoup.parse(pageContent, pageUrl); - String ytPlayerConfigRaw; JSONObject ytPlayerConfig; + String playerUrl; - //attempt to load the youtube js player JSON arguments + // Check if the video is age restricted + if (pageContent.contains(" method je.printStackTrace(); - Log.w(TAG, "failed to load title from JSON args; trying to extract it from HTML"); + System.err.println("failed to load title from JSON args; trying to extract it from HTML"); try { // fall through to fall-back return doc.select("meta[name=title]").attr("content"); } catch (Exception e) { @@ -264,11 +329,15 @@ public class YoutubeStreamExtractor implements StreamExtractor { @Override public String getUploader() throws ParsingException { - try {//json player args method + try { + if (playerArgs == null) { + return videoInfoPage.get("author"); + } + //json player args method return playerArgs.getString("author"); } catch(JSONException je) { je.printStackTrace(); - Log.w(TAG, + System.err.println( "failed to load uploader name from JSON args; trying to extract it from HTML"); } try {//fall through to fallback HTML method return doc.select("div.yt-user-info").first().text(); @@ -280,6 +349,9 @@ public class YoutubeStreamExtractor implements StreamExtractor { @Override public int getLength() throws ParsingException { try { + if (playerArgs == null) { + return Integer.valueOf(videoInfoPage.get("length_seconds")); + } return playerArgs.getInt("length_seconds"); } catch (JSONException e) {//todo: find fallback method throw new ParsingException("failed to load video duration from JSON args", e); @@ -287,12 +359,12 @@ public class YoutubeStreamExtractor implements StreamExtractor { } @Override - public long getViews() throws ParsingException { + public long getViewCount() throws ParsingException { try { String viewCountString = doc.select("meta[itemprop=interactionCount]").attr("content"); return Long.parseLong(viewCountString); } catch (Exception e) {//todo: find fallback method - throw new ParsingException("failed to number of views", e); + throw new ParsingException("failed to get number of views", e); } } @@ -313,13 +385,16 @@ public class YoutubeStreamExtractor implements StreamExtractor { try { return doc.select("link[itemprop=\"thumbnailUrl\"]").first().attr("abs:href"); } catch(Exception e) { - Log.w(TAG, "Could not find high res Thumbnail. Using low res instead"); + System.err.println("Could not find high res Thumbnail. Using low res instead"); } try { //fall through to fallback return playerArgs.getString("thumbnail_url"); } catch (JSONException je) { throw new ParsingException( "failed to extract thumbnail URL from JSON args; trying to extract it from HTML", je); + } catch (NullPointerException ne) { + // Get from the video info page instead + return videoInfoPage.get("thumbnail_url"); } } @@ -337,24 +412,6 @@ public class YoutubeStreamExtractor implements StreamExtractor { @Override public String getDashMpdUrl() throws ParsingException { /* - try { - String dashManifest = playerArgs.getString("dashmpd"); - if(!dashManifest.contains("/signature/")) { - String encryptedSig = Parser.matchGroup1("/s/([a-fA-F0-9\\.]+)", dashManifest); - String decryptedSig; - - decryptedSig = decryptSignature(encryptedSig, decryptionCode); - dashManifest = dashManifest.replace("/s/" + encryptedSig, "/signature/" + decryptedSig); - } - - return dashManifest; - } catch(JSONException je) { - throw new ParsingException( - "Could not find \"dashmpd\" upon the player args (maybe no dash manifest available).", je); - } catch (Exception e) { - throw new ParsingException(e); - } - */ try { String dashManifestUrl = videoInfoPage.get("dashmpd"); if(!dashManifestUrl.contains("/signature/")) { @@ -369,15 +426,23 @@ public class YoutubeStreamExtractor implements StreamExtractor { throw new ParsingException( "Could not get \"dashmpd\" maybe VideoInfoPage is broken.", e); } + */ + return ""; } @Override - public List getAudioStreams() throws ParsingException { - Vector audioStreams = new Vector<>(); + public List getAudioStreams() throws ParsingException { + Vector audioStreams = new Vector<>(); try{ - String encoded_url_map = playerArgs.getString("adaptive_fmts"); - for(String url_data_str : encoded_url_map.split(",")) { + String encodedUrlMap; + // playerArgs could be null if the video is age restricted + if (playerArgs == null) { + encodedUrlMap = videoInfoPage.get("adaptive_fmts"); + } else { + encodedUrlMap = playerArgs.getString("adaptive_fmts"); + } + for(String url_data_str : encodedUrlMap.split(",")) { // This loop iterates through multiple streams, therefor tags // is related to one and the same stream at a time. Map tags = Parser.compatParseMap( @@ -395,7 +460,7 @@ public class YoutubeStreamExtractor implements StreamExtractor { + decryptSignature(tags.get("s"), decryptionCode); } - audioStreams.add(new VideoInfo.AudioStream(streamUrl, + audioStreams.add(new AudioStream(streamUrl, itagItem.mediaFormatId, itagItem.bandWidth, itagItem.samplingRate)); @@ -409,12 +474,18 @@ public class YoutubeStreamExtractor implements StreamExtractor { } @Override - public List getVideoStreams() throws ParsingException { - Vector videoStreams = new Vector<>(); + public List getVideoStreams() throws ParsingException { + Vector videoStreams = new Vector<>(); try{ - String encoded_url_map = playerArgs.getString("url_encoded_fmt_stream_map"); - for(String url_data_str : encoded_url_map.split(",")) { + String encodedUrlMap; + // playerArgs could be null if the video is age restricted + if (playerArgs == null) { + encodedUrlMap = videoInfoPage.get("url_encoded_fmt_stream_map"); + } else { + encodedUrlMap = playerArgs.getString("url_encoded_fmt_stream_map"); + } + for(String url_data_str : encodedUrlMap.split(",")) { try { // This loop iterates through multiple streams, therefor tags // is related to one and the same stream at a time. @@ -432,14 +503,15 @@ public class YoutubeStreamExtractor implements StreamExtractor { streamUrl = streamUrl + "&signature=" + decryptSignature(tags.get("s"), decryptionCode); } - videoStreams.add(new VideoInfo.VideoStream( + videoStreams.add(new VideoStream( streamUrl, itagItem.mediaFormatId, itagItem.resolutionString)); } } } catch (Exception e) { - Log.w(TAG, "Could not get Video stream."); + //todo: dont log throw an error + System.err.println("Could not get Video stream."); e.printStackTrace(); } } @@ -455,7 +527,7 @@ public class YoutubeStreamExtractor implements StreamExtractor { } @Override - public List getVideoOnlyStreams() throws ParsingException { + public List getVideoOnlyStreams() throws ParsingException { return null; } @@ -492,13 +564,13 @@ public class YoutubeStreamExtractor implements StreamExtractor { } } - int seconds = (secondsString.isEmpty() ? 0 : Integer.parseInt(secondsString)); - int minutes = (minutesString.isEmpty() ? 0 : Integer.parseInt(minutesString)); - int hours = (hoursString.isEmpty() ? 0 : Integer.parseInt(hoursString)); + int seconds = secondsString.isEmpty() ? 0 : Integer.parseInt(secondsString); + int minutes = minutesString.isEmpty() ? 0 : Integer.parseInt(minutesString); + int hours = hoursString.isEmpty() ? 0 : Integer.parseInt(hoursString); - int ret = seconds + (60 * minutes) + (3600 * hours);//don't trust BODMAS! + //don't trust BODMAS! + return seconds + (60 * minutes) + (3600 * hours); //Log.d(TAG, "derived timestamp value:"+ret); - return ret; //the ordering varies internationally } catch (ParsingException e) { throw new ParsingException("Could not get timestamp.", e); @@ -510,15 +582,24 @@ public class YoutubeStreamExtractor implements StreamExtractor { @Override public int getAgeLimit() throws ParsingException { - // Not yet implemented. - // Also you need to be logged in to see age restricted videos on youtube, - // therefore NP is not able to receive such videos. - return 0; + if (!isAgeRestricted) { + return 0; + } + try { + return Integer.valueOf(doc.head() + .getElementsByAttributeValue("property", "og:restrictions:age") + .attr("content").replace("+", "")); + } catch (Exception e) { + throw new ParsingException("Could not get age restriction"); + } } @Override public String getAverageRating() throws ParsingException { try { + if (playerArgs == null) { + return videoInfoPage.get("avg_rating"); + } return playerArgs.getString("avg_rating"); } catch (JSONException e) { throw new ParsingException("Could not get Average rating", e); @@ -529,8 +610,14 @@ public class YoutubeStreamExtractor implements StreamExtractor { public int getLikeCount() throws ParsingException { String likesString = ""; try { - likesString = doc.select("button.like-button-renderer-like-button").first() - .select("span.yt-uix-button-content").first().text(); + + Element button = doc.select("button.like-button-renderer-like-button").first(); + try { + likesString = button.select("span.yt-uix-button-content").first().text(); + } catch (NullPointerException e) { + //if this ckicks in our button has no content and thefore likes/dislikes are disabled + return -1; + } return Integer.parseInt(likesString.replaceAll("[^\\d]", "")); } catch (NumberFormatException nfe) { throw new ParsingException( @@ -544,8 +631,13 @@ public class YoutubeStreamExtractor implements StreamExtractor { public int getDislikeCount() throws ParsingException { String dislikesString = ""; try { - dislikesString = doc.select("button.like-button-renderer-dislike-button").first() - .select("span.yt-uix-button-content").first().text(); + Element button = doc.select("button.like-button-renderer-dislike-button").first(); + try { + dislikesString = button.select("span.yt-uix-button-content").first().text(); + } catch (NullPointerException e) { + //if this kicks in our button has no content and therefore likes/dislikes are disabled + return -1; + } return Integer.parseInt(dislikesString.replaceAll("[^\\d]", "")); } catch(NumberFormatException nfe) { throw new ParsingException( @@ -556,7 +648,7 @@ public class YoutubeStreamExtractor implements StreamExtractor { } @Override - public VideoPreviewInfo getNextVideo() throws ParsingException { + public StreamPreviewInfo getNextVideo() throws ParsingException { try { return extractVideoPreviewInfo(doc.select("div[class=\"watch-sidebar-section\"]").first() .select("li").first()); @@ -566,9 +658,9 @@ public class YoutubeStreamExtractor implements StreamExtractor { } @Override - public Vector getRelatedVideos() throws ParsingException { + public Vector getRelatedVideos() throws ParsingException { try { - Vector relatedVideos = new Vector<>(); + Vector relatedVideos = new Vector<>(); for (Element li : doc.select("ul[id=\"watch-related\"]").first().children()) { // first check if we have a playlist. If so leave them out if (li.select("a[class*=\"content-link\"]").first() != null) { @@ -582,8 +674,8 @@ public class YoutubeStreamExtractor implements StreamExtractor { } @Override - public VideoUrlIdHandler getUrlIdConverter() { - return new YoutubeVideoUrlIdHandler(); + public StreamUrlIdHandler getUrlIdConverter() { + return new YoutubeStreamUrlIdHandler(); } @Override @@ -591,11 +683,17 @@ public class YoutubeStreamExtractor implements StreamExtractor { return pageUrl; } + @Override + public StreamInfo.StreamType getStreamType() throws ParsingException { + //todo: if implementing livestream support this value should be generated dynamically + return StreamInfo.StreamType.VIDEO_STREAM; + } + /**Provides information about links to other videos on the video page, such as related videos. - * This is encapsulated in a VideoPreviewInfo object, - * which is a subset of the fields in a full VideoInfo.*/ - private VideoPreviewInfo extractVideoPreviewInfo(Element li) throws ParsingException { - VideoPreviewInfo info = new VideoPreviewInfo(); + * This is encapsulated in a StreamPreviewInfo object, + * which is a subset of the fields in a full StreamInfo.*/ + private StreamPreviewInfo extractVideoPreviewInfo(Element li) throws ParsingException { + StreamPreviewInfo info = new StreamPreviewInfo(); try { info.webpage_url = li.select("a.content-link").first() @@ -617,12 +715,13 @@ public class YoutubeStreamExtractor implements StreamExtractor { try { info.view_count = Long.parseLong(li.select("span.view-count") .first().text().replaceAll("[^\\d]", "")); - } catch (NullPointerException e) {//related videos sometimes have no view count + } catch (Exception e) {//related videos sometimes have no view count info.view_count = 0; } info.uploader = li.select("span.g-hovercard").first().text(); - info.duration = li.select("span.video-time").first().text(); + info.duration = YoutubeParsingHelper.parseDurationString( + li.select("span.video-time").first().text()); Element img = li.select("img").first(); info.thumbnail_url = img.attr("abs:src"); @@ -636,7 +735,7 @@ public class YoutubeStreamExtractor implements StreamExtractor { info.thumbnail_url = "https:" + info.thumbnail_url; } } catch (Exception e) { - throw new ParsingException(e); + throw new ParsingException("Could not get video preview info", e); } return info; } @@ -690,11 +789,11 @@ public class YoutubeStreamExtractor implements StreamExtractor { Function decryptionFunc = (Function) scope.get("decrypt", scope); result = decryptionFunc.call(context, scope, scope, new Object[]{encryptedSig}); } catch (Exception e) { - throw new DecryptException(e); + throw new DecryptException("could not get decrypt signature", e); } finally { Context.exit(); } - return (result == null ? "" : result.toString()); + return result == null ? "" : result.toString(); } private String findErrorReason(Document doc) { diff --git a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamPreviewInfoExtractor.java b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamPreviewInfoExtractor.java new file mode 100644 index 000000000..a23bf904c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamPreviewInfoExtractor.java @@ -0,0 +1,171 @@ +package org.schabi.newpipe.extractor.services.youtube; + +import org.jsoup.nodes.Element; +import org.schabi.newpipe.extractor.AbstractVideoInfo; +import org.schabi.newpipe.extractor.Parser; +import org.schabi.newpipe.extractor.ParsingException; +import org.schabi.newpipe.extractor.StreamPreviewInfoExtractor; + +/** + * Copyright (C) Christian Schabesberger 2016 + * YoutubeStreamPreviewInfoExtractor.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 . + */ + +public class YoutubeStreamPreviewInfoExtractor implements StreamPreviewInfoExtractor { + + private final Element item; + + public YoutubeStreamPreviewInfoExtractor(Element item) { + this.item = item; + } + + @Override + public String getWebPageUrl() throws ParsingException { + try { + Element el = item.select("div[class*=\"yt-lockup-video\"").first(); + Element dl = el.select("h3").first().select("a").first(); + return dl.attr("abs:href"); + } catch (Exception e) { + throw new ParsingException("Could not get web page url for the video", e); + } + } + + @Override + public String getTitle() throws ParsingException { + try { + Element el = item.select("div[class*=\"yt-lockup-video\"").first(); + Element dl = el.select("h3").first().select("a").first(); + return dl.text(); + } catch (Exception e) { + throw new ParsingException("Could not get title", e); + } + } + + @Override + public int getDuration() throws ParsingException { + try { + return YoutubeParsingHelper.parseDurationString( + item.select("span[class=\"video-time\"]").first().text()); + } catch(Exception e) { + if(isLiveStream(item)) { + // -1 for no duration + return -1; + } else { + throw new ParsingException("Could not get Duration: " + getTitle(), e); + } + + + } + } + + @Override + public String getUploader() throws ParsingException { + try { + return item.select("div[class=\"yt-lockup-byline\"]").first() + .select("a").first() + .text(); + } catch (Exception e) { + throw new ParsingException("Could not get uploader", e); + } + } + + @Override + public String getUploadDate() throws ParsingException { + try { + return item.select("div[class=\"yt-lockup-meta\"]").first() + .select("li").first() + .text(); + } catch(Exception e) { + throw new ParsingException("Could not get uplaod date", e); + } + } + + @Override + public long getViewCount() throws ParsingException { + String output; + String input; + try { + input = item.select("div[class=\"yt-lockup-meta\"]").first() + .select("li").get(1) + .text(); + } catch (IndexOutOfBoundsException e) { + if(isLiveStream(item)) { + // -1 for no view count + return -1; + } else { + throw new ParsingException( + "Could not parse yt-lockup-meta although available: " + getTitle(), e); + } + } + + output = Parser.matchGroup1("([0-9,\\. ]*)", input) + .replace(" ", "") + .replace(".", "") + .replace(",", ""); + + try { + return Long.parseLong(output); + } catch (NumberFormatException e) { + // if this happens the video probably has no views + if(!input.isEmpty()) { + return 0; + } else { + throw new ParsingException("Could not handle input: " + input, e); + } + } + } + + @Override + public String getThumbnailUrl() throws ParsingException { + try { + String url; + Element te = item.select("div[class=\"yt-thumb video-thumb\"]").first() + .select("img").first(); + url = te.attr("abs:src"); + // Sometimes youtube sends links to gif files which somehow seem to not exist + // anymore. Items with such gif also offer a secondary image source. So we are going + // to use that if we've caught such an item. + if (url.contains(".gif")) { + url = te.attr("abs:data-thumb"); + } + return url; + } catch (Exception e) { + throw new ParsingException("Could not get thumbnail url", e); + } + } + + @Override + public AbstractVideoInfo.StreamType getStreamType() { + if(isLiveStream(item)) { + return AbstractVideoInfo.StreamType.LIVE_STREAM; + } else { + return AbstractVideoInfo.StreamType.VIDEO_STREAM; + } + } + + private boolean isLiveStream(Element item) { + Element bla = item.select("span[class*=\"yt-badge-live\"]").first(); + + if(bla == null) { + // sometimes livestreams dont have badges but sill are live streams + // if video time is not available we most likly have an offline livestream + if(item.select("span[class*=\"video-time\"]").first() == null) { + return true; + } + } + return bla != null; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/crawler/services/youtube/YoutubeVideoUrlIdHandler.java b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamUrlIdHandler.java similarity index 58% rename from app/src/main/java/org/schabi/newpipe/crawler/services/youtube/YoutubeVideoUrlIdHandler.java rename to app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamUrlIdHandler.java index 7e1786a5b..998e93a51 100644 --- a/app/src/main/java/org/schabi/newpipe/crawler/services/youtube/YoutubeVideoUrlIdHandler.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamUrlIdHandler.java @@ -1,14 +1,17 @@ -package org.schabi.newpipe.crawler.services.youtube; +package org.schabi.newpipe.extractor.services.youtube; -import org.schabi.newpipe.crawler.Parser; -import org.schabi.newpipe.crawler.ParsingException; -import org.schabi.newpipe.crawler.VideoUrlIdHandler; +import org.schabi.newpipe.extractor.Parser; +import org.schabi.newpipe.extractor.ParsingException; +import org.schabi.newpipe.extractor.StreamUrlIdHandler; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; /** * Created by Christian Schabesberger on 02.02.16. * * Copyright (C) Christian Schabesberger 2016 - * YoutubeVideoUrlIdHandler.java is part of NewPipe. + * YoutubeStreamUrlIdHandler.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 @@ -24,7 +27,7 @@ import org.schabi.newpipe.crawler.VideoUrlIdHandler; * along with NewPipe. If not, see . */ -public class YoutubeVideoUrlIdHandler implements VideoUrlIdHandler { +public class YoutubeStreamUrlIdHandler implements StreamUrlIdHandler { @SuppressWarnings("WeakerAccess") @Override public String getVideoUrl(String videoId) { @@ -35,21 +38,33 @@ public class YoutubeVideoUrlIdHandler implements VideoUrlIdHandler { @Override public String getVideoId(String url) throws ParsingException { String id; - String pat; if(url.contains("youtube")) { - pat = "youtube\\.com/watch\\?v=([\\-a-zA-Z0-9_]{11})"; + if(url.contains("attribution_link")) { + try { + String escapedQuery = Parser.matchGroup1("u=(.[^&|$]*)", url); + String query = URLDecoder.decode(escapedQuery, "UTF-8"); + id = Parser.matchGroup1("v=([\\-a-zA-Z0-9_]{11})", query); + } catch(UnsupportedEncodingException uee) { + throw new ParsingException("Could not parse attribution_link", uee); + } + } else { + id = Parser.matchGroup1("[?&]v=([\\-a-zA-Z0-9_]{11})", url); + } } else if(url.contains("youtu.be")) { - pat = "youtu\\.be/([a-zA-Z0-9_-]{11})"; + if(url.contains("v=")) { + id = Parser.matchGroup1("v=([\\-a-zA-Z0-9_]{11})", url); + } else { + id = Parser.matchGroup1("youtu\\.be/([a-zA-Z0-9_-]{11})", url); + } } else { throw new ParsingException("Error no suitable url: " + url); } - id = Parser.matchGroup1(pat, url); + if(!id.isEmpty()){ - //Log.i(TAG, "string \""+url+"\" matches!"); return id; } else { throw new ParsingException("Error could not parse url: " + url); diff --git a/app/src/main/java/org/schabi/newpipe/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java similarity index 72% rename from app/src/main/java/org/schabi/newpipe/BackgroundPlayer.java rename to app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index 7223928fe..792a35704 100644 --- a/app/src/main/java/org/schabi/newpipe/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe; +package org.schabi.newpipe.player; import android.app.Notification; import android.app.NotificationManager; @@ -20,6 +20,12 @@ import android.util.Log; import android.widget.RemoteViews; import android.widget.Toast; +import org.schabi.newpipe.ActivityCommunicator; +import org.schabi.newpipe.BuildConfig; +import org.schabi.newpipe.R; +import org.schabi.newpipe.VideoItemDetailActivity; +import org.schabi.newpipe.VideoItemDetailFragment; + import java.io.IOException; /** @@ -151,7 +157,7 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare e.printStackTrace(); } - WifiManager wifiMgr = ((WifiManager)getSystemService(Context.WIFI_SERVICE)); + WifiManager wifiMgr = (WifiManager)getSystemService(Context.WIFI_SERVICE); wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL, TAG); //listen for end of video @@ -205,9 +211,9 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare if(action.equals(ACTION_PLAYPAUSE)) { if(mediaPlayer.isPlaying()) { mediaPlayer.pause(); - note.contentView.setImageViewResource(R.id.backgroundPlayPause, R.drawable.ic_play_circle_filled_white_24dp); + note.contentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_play_circle_filled_white_24dp); if(android.os.Build.VERSION.SDK_INT >=16){ - note.bigContentView.setImageViewResource(R.id.backgroundPlayPause, R.drawable.ic_play_circle_filled_white_24dp); + note.bigContentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_play_circle_filled_white_24dp); } noteMgr.notify(noteID, note); } @@ -215,9 +221,9 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare //reacquire CPU lock after auto-releasing it on pause mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK); mediaPlayer.start(); - note.contentView.setImageViewResource(R.id.backgroundPlayPause, R.drawable.ic_pause_white_24dp); + note.contentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_pause_white_24dp); if(android.os.Build.VERSION.SDK_INT >=16){ - note.bigContentView.setImageViewResource(R.id.backgroundPlayPause, R.drawable.ic_pause_white_24dp); + note.bigContentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_pause_white_24dp); } noteMgr.notify(noteID, note); } @@ -275,11 +281,13 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare */ //build intent to return to video, on tapping notification - Intent openDetailView = new Intent(getApplicationContext(), + Intent openDetailViewIntent = new Intent(getApplicationContext(), VideoItemDetailActivity.class); - openDetailView.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, serviceId); - openDetailView.putExtra(VideoItemDetailFragment.VIDEO_URL, webUrl); - openDetailView.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + openDetailViewIntent.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, serviceId); + openDetailViewIntent.putExtra(VideoItemDetailFragment.VIDEO_URL, webUrl); + openDetailViewIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + PendingIntent openDetailView = PendingIntent.getActivity(owner, noteID, + openDetailViewIntent, PendingIntent.FLAG_UPDATE_CURRENT); noteBuilder .setOngoing(true) @@ -291,74 +299,41 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare String.format(res.getString( R.string.background_player_time_text), title)) .setContentIntent(PendingIntent.getActivity(getApplicationContext(), - noteID, openDetailView, - PendingIntent.FLAG_UPDATE_CURRENT)); + noteID, openDetailViewIntent, + PendingIntent.FLAG_UPDATE_CURRENT)) + .setContentIntent(openDetailView); - if (android.os.Build.VERSION.SDK_INT < 21) { + RemoteViews view = + new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification); + view.setImageViewBitmap(R.id.notificationCover, videoThumbnail); + view.setTextViewText(R.id.notificationSongName, title); + view.setTextViewText(R.id.notificationArtist, channelName); + view.setOnClickPendingIntent(R.id.notificationStop, stopPI); + view.setOnClickPendingIntent(R.id.notificationPlayPause, playPI); + view.setOnClickPendingIntent(R.id.notificationContent, openDetailView); - NotificationCompat.Action playButton = new NotificationCompat.Action.Builder - (R.drawable.ic_play_arrow_white_48dp, - res.getString(R.string.play_btn_text), playPI).build(); + //possibly found the expandedView problem, + //but can't test it as I don't have a 5.0 device. -medavox + RemoteViews expandedView = + new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification_expanded); + expandedView.setImageViewBitmap(R.id.notificationCover, videoThumbnail); + expandedView.setTextViewText(R.id.notificationSongName, title); + expandedView.setTextViewText(R.id.notificationArtist, channelName); + expandedView.setOnClickPendingIntent(R.id.notificationStop, stopPI); + expandedView.setOnClickPendingIntent(R.id.notificationPlayPause, playPI); + expandedView.setOnClickPendingIntent(R.id.notificationContent, openDetailView); - noteBuilder - .setContentTitle(title) - //really? Id like to put something more helpful here. - //was more of a placeholder than anything else. -medavox - //.setContentText("NewPipe is playing in the background") - .setContentText(channelName) - //.setAutoCancel(!mediaPlayer.isPlaying()) - .setDeleteIntent(stopPI) - //doesn't fit with Notification.MediaStyle - //.setProgress(vidLength, 0, false) - .setLargeIcon(videoThumbnail) - .addAction(playButton); - //.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - //.setLargeIcon(cover) - //is wrapping this in an SDK version check really necessary, - // if we're using NotificationCompat? - // the compat libraries should handle this, right? -medavox - if (android.os.Build.VERSION.SDK_INT >= 16) - noteBuilder.setPriority(Notification.PRIORITY_LOW); + noteBuilder.setCategory(Notification.CATEGORY_TRANSPORT); - noteBuilder.setStyle(new NotificationCompat.MediaStyle() - //.setMediaSession(mMediaSession.getSessionToken()) - .setShowActionsInCompactView(new int[]{0}) - .setShowCancelButton(true) - .setCancelButtonIntent(stopPI)); - if (videoThumbnail != null) { - noteBuilder.setLargeIcon(videoThumbnail); - } - note = noteBuilder.build(); - } else { - RemoteViews view = - new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification); - view.setImageViewBitmap(R.id.backgroundCover, videoThumbnail); - view.setTextViewText(R.id.backgroundSongName, title); - view.setTextViewText(R.id.backgroundArtist, channelName); - view.setOnClickPendingIntent(R.id.backgroundStop, stopPI); - view.setOnClickPendingIntent(R.id.backgroundPlayPause, playPI); + //Make notification appear on lockscreen + noteBuilder.setVisibility(Notification.VISIBILITY_PUBLIC); - //possibly found the expandedView problem, - //but can't test it as I don't have a 5.0 device. -medavox - RemoteViews expandedView = - new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification_expanded); - expandedView.setImageViewBitmap(R.id.backgroundCover, videoThumbnail); - expandedView.setTextViewText(R.id.backgroundSongName, title); - expandedView.setTextViewText(R.id.backgroundArtist, channelName); - expandedView.setOnClickPendingIntent(R.id.backgroundStop, stopPI); - expandedView.setOnClickPendingIntent(R.id.backgroundPlayPause, playPI); + note = noteBuilder.build(); + note.contentView = view; - noteBuilder.setCategory(Notification.CATEGORY_TRANSPORT); - - //Make notification appear on lockscreen - noteBuilder.setVisibility(Notification.VISIBILITY_PUBLIC); - - note = noteBuilder.build(); - note.contentView = view; - - //todo: This never shows up. I was not able to figure out why: + if (android.os.Build.VERSION.SDK_INT > 16) { note.bigContentView = expandedView; } diff --git a/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java new file mode 100644 index 000000000..c38e63545 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java @@ -0,0 +1,564 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Extended by Christian Schabesberger on 24.12.15. + * + * Copyright (C) Christian Schabesberger 2015 + * ExoPlayerActivity.java is part of NewPipe. all changes are under GPL3 + * + * 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 . + */ + +package org.schabi.newpipe.player; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.player.exoplayer.DashRendererBuilder; +import org.schabi.newpipe.player.exoplayer.EventLogger; +import org.schabi.newpipe.player.exoplayer.ExtractorRendererBuilder; +import org.schabi.newpipe.player.exoplayer.HlsRendererBuilder; +import org.schabi.newpipe.player.exoplayer.NPExoPlayer; +import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder; +import org.schabi.newpipe.player.exoplayer.SmoothStreamingRendererBuilder; + +import com.google.android.exoplayer.AspectRatioFrameLayout; +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.ExoPlayer; +import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; +import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.audio.AudioCapabilities; +import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver; +import com.google.android.exoplayer.drm.UnsupportedDrmException; +import com.google.android.exoplayer.metadata.GeobMetadata; +import com.google.android.exoplayer.metadata.PrivMetadata; +import com.google.android.exoplayer.metadata.TxxxMetadata; +import com.google.android.exoplayer.text.CaptionStyleCompat; +import com.google.android.exoplayer.text.Cue; +import com.google.android.exoplayer.text.SubtitleLayout; +import com.google.android.exoplayer.util.DebugTextViewHelper; +import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.Util; +import com.google.android.exoplayer.util.VerboseLogUtil; + +import android.Manifest.permission; +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.View.OnKeyListener; +import android.view.View.OnTouchListener; +import android.view.accessibility.CaptioningManager; +import android.widget.MediaController; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; +import android.widget.Toast; + +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * An activity that plays media using {@link NPExoPlayer}. + */ +public class ExoPlayerActivity extends Activity { + + // For use within demo app code. + public static final String CONTENT_ID_EXTRA = "content_id"; + public static final String CONTENT_TYPE_EXTRA = "content_type"; + public static final String PROVIDER_EXTRA = "provider"; + + // For use when launching the demo app using adb. + private static final String CONTENT_EXT_EXTRA = "type"; + + private static final String TAG = "PlayerActivity"; + private static final int MENU_GROUP_TRACKS = 1; + private static final int ID_OFFSET = 2; + + private static final CookieManager defaultCookieManager; + static { + defaultCookieManager = new CookieManager(); + defaultCookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); + } + + private EventLogger eventLogger; + private MediaController mediaController; + private View shutterView; + private AspectRatioFrameLayout videoFrame; + private SurfaceView surfaceView; + private SubtitleLayout subtitleLayout; + + private NPExoPlayer player; + private boolean playerNeedsPrepare; + + private long playerPosition; + private boolean enableBackgroundAudio = true; + + private Uri contentUri; + private int contentType; + private String contentId; + private String provider; + + private AudioCapabilitiesReceiver audioCapabilitiesReceiver; + + + NPExoPlayer.Listener exoPlayerListener = new NPExoPlayer.Listener() { + @Override + public void onStateChanged(boolean playWhenReady, int playbackState) { + if (playbackState == ExoPlayer.STATE_ENDED) { + showControls(); + } + String text = "playWhenReady=" + playWhenReady + ", playbackState="; + switch(playbackState) { + case ExoPlayer.STATE_BUFFERING: + text += "buffering"; + break; + case ExoPlayer.STATE_ENDED: + text += "ended"; + break; + case ExoPlayer.STATE_IDLE: + text += "idle"; + break; + case ExoPlayer.STATE_PREPARING: + text += "preparing"; + break; + case ExoPlayer.STATE_READY: + text += "ready"; + break; + default: + text += "unknown"; + break; + } + //todo: put text in some log + } + + @Override + public void onError(Exception e) { + String errorString = null; + if (e instanceof UnsupportedDrmException) { + // Special case DRM failures. + UnsupportedDrmException unsupportedDrmException = (UnsupportedDrmException) e; + errorString = getString(Util.SDK_INT < 18 ? R.string.error_drm_not_supported + : unsupportedDrmException.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME + ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); + } else if (e instanceof ExoPlaybackException + && e.getCause() instanceof DecoderInitializationException) { + // Special case for decoder initialization failures. + DecoderInitializationException decoderInitializationException = + (DecoderInitializationException) e.getCause(); + if (decoderInitializationException.decoderName == null) { + if (decoderInitializationException.getCause() instanceof DecoderQueryException) { + errorString = getString(R.string.error_querying_decoders); + } else if (decoderInitializationException.secureDecoderRequired) { + errorString = getString(R.string.error_no_secure_decoder, + decoderInitializationException.mimeType); + } else { + errorString = getString(R.string.error_no_decoder, + decoderInitializationException.mimeType); + } + } else { + errorString = getString(R.string.error_instantiating_decoder, + decoderInitializationException.decoderName); + } + } + if (errorString != null) { + Toast.makeText(getApplicationContext(), errorString, Toast.LENGTH_LONG).show(); + } + playerNeedsPrepare = true; + showControls(); + } + + @Override + public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthAspectRatio) { + shutterView.setVisibility(View.GONE); + videoFrame.setAspectRatio( + height == 0 ? 1 : (width * pixelWidthAspectRatio) / height); + } + }; + + SurfaceHolder.Callback surfaceHolderCallback = new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(SurfaceHolder holder) { + if (player != null) { + player.setSurface(holder.getSurface()); + } + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + // Do nothing. + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + if (player != null) { + player.blockingClearSurface(); + } + } + }; + + NPExoPlayer.CaptionListener captionListener = new NPExoPlayer.CaptionListener() { + @Override + public void onCues(List cues) { + subtitleLayout.setCues(cues); + } + }; + + NPExoPlayer.Id3MetadataListener id3MetadataListener = new NPExoPlayer.Id3MetadataListener() { + @Override + public void onId3Metadata(Map metadata) { + for (Map.Entry entry : metadata.entrySet()) { + if (TxxxMetadata.TYPE.equals(entry.getKey())) { + TxxxMetadata txxxMetadata = (TxxxMetadata) entry.getValue(); + Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s, value=%s", + TxxxMetadata.TYPE, txxxMetadata.description, txxxMetadata.value)); + } else if (PrivMetadata.TYPE.equals(entry.getKey())) { + PrivMetadata privMetadata = (PrivMetadata) entry.getValue(); + Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s", + PrivMetadata.TYPE, privMetadata.owner)); + } else if (GeobMetadata.TYPE.equals(entry.getKey())) { + GeobMetadata geobMetadata = (GeobMetadata) entry.getValue(); + Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, filename=%s, description=%s", + GeobMetadata.TYPE, geobMetadata.mimeType, geobMetadata.filename, + geobMetadata.description)); + } else { + Log.i(TAG, String.format("ID3 TimedMetadata %s", entry.getKey())); + } + } + } + }; + + AudioCapabilitiesReceiver.Listener audioCapabilitiesListener = new AudioCapabilitiesReceiver.Listener() { + @Override + public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { + if (player == null) { + return; + } + boolean backgrounded = player.getBackgrounded(); + boolean playWhenReady = player.getPlayWhenReady(); + releasePlayer(); + preparePlayer(playWhenReady); + player.setBackgrounded(backgrounded); + } + }; + + // Activity lifecycle + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.exo_player_activity); + View root = findViewById(R.id.root); + root.setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { + toggleControlsVisibility(); + } else if (motionEvent.getAction() == MotionEvent.ACTION_UP) { + view.performClick(); + } + return true; + } + }); + root.setOnKeyListener(new OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE + || keyCode == KeyEvent.KEYCODE_MENU) { + return false; + } + return mediaController.dispatchKeyEvent(event); + } + }); + + shutterView = findViewById(R.id.shutter); + + videoFrame = (AspectRatioFrameLayout) findViewById(R.id.video_frame); + surfaceView = (SurfaceView) findViewById(R.id.surface_view); + surfaceView.getHolder().addCallback(surfaceHolderCallback); + subtitleLayout = (SubtitleLayout) findViewById(R.id.subtitles); + + //todo: replace that creapy mediaController + mediaController = new KeyCompatibleMediaController(this); + mediaController.setAnchorView(root); + + //todo: check what cookie handler does, and if we even need it + CookieHandler currentHandler = CookieHandler.getDefault(); + if (currentHandler != defaultCookieManager) { + CookieHandler.setDefault(defaultCookieManager); + } + + audioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, audioCapabilitiesListener); + audioCapabilitiesReceiver.register(); + } + + @Override + public void onNewIntent(Intent intent) { + releasePlayer(); + playerPosition = 0; + setIntent(intent); + } + + @Override + public void onResume() { + super.onResume(); + Intent intent = getIntent(); + contentUri = intent.getData(); + contentType = intent.getIntExtra(CONTENT_TYPE_EXTRA, + inferContentType(contentUri, intent.getStringExtra(CONTENT_EXT_EXTRA))); + contentId = intent.getStringExtra(CONTENT_ID_EXTRA); + provider = intent.getStringExtra(PROVIDER_EXTRA); + configureSubtitleView(); + if (player == null) { + if (!maybeRequestPermission()) { + preparePlayer(true); + } + } else { + player.setBackgrounded(false); + } + } + + @Override + public void onPause() { + super.onPause(); + if (!enableBackgroundAudio) { + releasePlayer(); + } else { + player.setBackgrounded(true); + } + shutterView.setVisibility(View.VISIBLE); + } + + @Override + public void onDestroy() { + super.onDestroy(); + audioCapabilitiesReceiver.unregister(); + releasePlayer(); + } + + + // Permission request listener method + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, + int[] grantResults) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + preparePlayer(true); + } else { + Toast.makeText(getApplicationContext(), R.string.storage_permission_denied, + Toast.LENGTH_LONG).show(); + finish(); + } + } + + // Permission management methods + + /** + * Checks whether it is necessary to ask for permission to read storage. If necessary, it also + * requests permission. + * + * @return true if a permission request is made. False if it is not necessary. + */ + @TargetApi(23) + private boolean maybeRequestPermission() { + if (requiresPermission(contentUri)) { + requestPermissions(new String[] {permission.READ_EXTERNAL_STORAGE}, 0); + return true; + } else { + return false; + } + } + + @TargetApi(23) + private boolean requiresPermission(Uri uri) { + return Util.SDK_INT >= 23 + && Util.isLocalFileUri(uri) + && checkSelfPermission(permission.READ_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED; + } + + // Internal methods + + private RendererBuilder getRendererBuilder() { + String userAgent = Util.getUserAgent(this, "NewPipeExoPlayer"); + switch (contentType) { + case Util.TYPE_SS: + // default + //return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString()); + case Util.TYPE_DASH: + // if a dash manifest is available + //return new DashRendererBuilder(this, userAgent, contentUri.toString()); + case Util.TYPE_HLS: + // for livestreams + return new HlsRendererBuilder(this, userAgent, contentUri.toString()); + case Util.TYPE_OTHER: + // video only streaming + return new ExtractorRendererBuilder(this, userAgent, contentUri); + default: + throw new IllegalStateException("Unsupported type: " + contentType); + } + } + + private void preparePlayer(boolean playWhenReady) { + if (player == null) { + player = new NPExoPlayer(getRendererBuilder()); + player.addListener(exoPlayerListener); + player.setCaptionListener(captionListener); + player.setMetadataListener(id3MetadataListener); + player.seekTo(playerPosition); + playerNeedsPrepare = true; + mediaController.setMediaPlayer(player.getPlayerControl()); + mediaController.setEnabled(true); + eventLogger = new EventLogger(); + eventLogger.startSession(); + player.addListener(eventLogger); + player.setInfoListener(eventLogger); + player.setInternalErrorListener(eventLogger); + } + if (playerNeedsPrepare) { + player.prepare(); + playerNeedsPrepare = false; + } + player.setSurface(surfaceView.getHolder().getSurface()); + player.setPlayWhenReady(playWhenReady); + } + + private void releasePlayer() { + if (player != null) { + playerPosition = player.getCurrentPosition(); + player.release(); + player = null; + eventLogger.endSession(); + eventLogger = null; + } + } + + private void toggleControlsVisibility() { + if (mediaController.isShowing()) { + mediaController.hide(); + } else { + showControls(); + } + } + + private void showControls() { + mediaController.show(0); + } + + private void configureSubtitleView() { + CaptionStyleCompat style; + float fontScale; + if (Util.SDK_INT >= 19) { + style = getUserCaptionStyleV19(); + fontScale = getUserCaptionFontScaleV19(); + } else { + style = CaptionStyleCompat.DEFAULT; + fontScale = 1.0f; + } + subtitleLayout.setStyle(style); + subtitleLayout.setFractionalTextSize(SubtitleLayout.DEFAULT_TEXT_SIZE_FRACTION * fontScale); + } + + @TargetApi(19) + private float getUserCaptionFontScaleV19() { + CaptioningManager captioningManager = + (CaptioningManager) getSystemService(Context.CAPTIONING_SERVICE); + return captioningManager.getFontScale(); + } + + @TargetApi(19) + private CaptionStyleCompat getUserCaptionStyleV19() { + CaptioningManager captioningManager = + (CaptioningManager) getSystemService(Context.CAPTIONING_SERVICE); + return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle()); + } + + /** + * Makes a best guess to infer the type from a media {@link Uri} and an optional overriding file + * extension. + * + * @param uri The {@link Uri} of the media. + * @param fileExtension An overriding file extension. + * @return The inferred type. + */ + private static int inferContentType(Uri uri, String fileExtension) { + String lastPathSegment = !TextUtils.isEmpty(fileExtension) ? "." + fileExtension + : uri.getLastPathSegment(); + return Util.inferContentType(lastPathSegment); + } + + private static final class KeyCompatibleMediaController extends MediaController { + + private MediaController.MediaPlayerControl playerControl; + + public KeyCompatibleMediaController(Context context) { + super(context); + } + + @Override + public void setMediaPlayer(MediaController.MediaPlayerControl playerControl) { + super.setMediaPlayer(playerControl); + this.playerControl = playerControl; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + int keyCode = event.getKeyCode(); + if (playerControl.canSeekForward() && keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + playerControl.seekTo(playerControl.getCurrentPosition() + 15000); // milliseconds + show(); + } + return true; + } else if (playerControl.canSeekBackward() && keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + playerControl.seekTo(playerControl.getCurrentPosition() - 5000); // milliseconds + show(); + } + return true; + } + return super.dispatchKeyEvent(event); + } + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/PlayVideoActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayVideoActivity.java similarity index 97% rename from app/src/main/java/org/schabi/newpipe/PlayVideoActivity.java rename to app/src/main/java/org/schabi/newpipe/player/PlayVideoActivity.java index 823fb762c..c00381d32 100644 --- a/app/src/main/java/org/schabi/newpipe/PlayVideoActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayVideoActivity.java @@ -1,10 +1,11 @@ -package org.schabi.newpipe; +package org.schabi.newpipe.player; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.res.Configuration; +import android.graphics.drawable.Drawable; import android.media.MediaPlayer; import android.media.AudioManager; import android.net.Uri; @@ -27,6 +28,9 @@ import android.widget.MediaController; import android.widget.ProgressBar; import android.widget.VideoView; +import org.schabi.newpipe.App; +import org.schabi.newpipe.R; + /** * Copyright (C) Christian Schabesberger 2015 * PlayVideoActivity.java is part of NewPipe. @@ -80,6 +84,10 @@ public class PlayVideoActivity extends AppCompatActivity { 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(); @@ -191,7 +199,6 @@ public class PlayVideoActivity extends AppCompatActivity { @Override public void onResume() { super.onResume(); - App.checkStartTor(this); } @Override @@ -320,7 +327,7 @@ public class PlayVideoActivity extends AppCompatActivity { int realHeight = realDisplayMetrics.heightPixels; int displayHeight = displayMetrics.heightPixels; - return (realHeight - displayHeight); + return realHeight - displayHeight; } else { return 50; } @@ -337,7 +344,7 @@ public class PlayVideoActivity extends AppCompatActivity { int realWidth = realDisplayMetrics.widthPixels; int displayWidth = displayMetrics.widthPixels; - return (realWidth - displayWidth); + return realWidth - displayWidth; } else { return 50; } diff --git a/app/src/main/java/org/schabi/newpipe/player/exoplayer/DashRendererBuilder.java b/app/src/main/java/org/schabi/newpipe/player/exoplayer/DashRendererBuilder.java new file mode 100644 index 000000000..f12dc8975 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/exoplayer/DashRendererBuilder.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.schabi.newpipe.player.exoplayer; + +import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder; + +import com.google.android.exoplayer.DefaultLoadControl; +import com.google.android.exoplayer.LoadControl; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecSelector; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.audio.AudioCapabilities; +import com.google.android.exoplayer.chunk.ChunkSampleSource; +import com.google.android.exoplayer.chunk.ChunkSource; +import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; +import com.google.android.exoplayer.dash.DashChunkSource; +import com.google.android.exoplayer.dash.DefaultDashTrackSelector; +import com.google.android.exoplayer.dash.mpd.AdaptationSet; +import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; +import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser; +import com.google.android.exoplayer.dash.mpd.Period; +import com.google.android.exoplayer.dash.mpd.UtcTimingElement; +import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver; +import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver.UtcTimingCallback; +import com.google.android.exoplayer.drm.MediaDrmCallback; +import com.google.android.exoplayer.drm.StreamingDrmSessionManager; +import com.google.android.exoplayer.drm.UnsupportedDrmException; +import com.google.android.exoplayer.text.TextTrackRenderer; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DefaultAllocator; +import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer.upstream.DefaultUriDataSource; +import com.google.android.exoplayer.upstream.UriDataSource; +import com.google.android.exoplayer.util.ManifestFetcher; +import com.google.android.exoplayer.util.Util; + +import android.content.Context; +import android.media.AudioManager; +import android.media.MediaCodec; +import android.os.Handler; +import android.util.Log; + +import java.io.IOException; + +/** + * A {@link RendererBuilder} for DASH. + */ +public class DashRendererBuilder implements RendererBuilder { + + private static final String TAG = "DashRendererBuilder"; + + private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; + private static final int VIDEO_BUFFER_SEGMENTS = 200; + private static final int AUDIO_BUFFER_SEGMENTS = 54; + private static final int TEXT_BUFFER_SEGMENTS = 2; + private static final int LIVE_EDGE_LATENCY_MS = 30000; + + private static final int SECURITY_LEVEL_UNKNOWN = -1; + private static final int SECURITY_LEVEL_1 = 1; + private static final int SECURITY_LEVEL_3 = 3; + + private final Context context; + private final String userAgent; + private final String url; + private final MediaDrmCallback drmCallback; + + private AsyncRendererBuilder currentAsyncBuilder; + + public DashRendererBuilder(Context context, String userAgent, String url, + MediaDrmCallback drmCallback) { + this.context = context; + this.userAgent = userAgent; + this.url = url; + this.drmCallback = drmCallback; + } + + @Override + public void buildRenderers(NPExoPlayer player) { + currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, drmCallback, player); + currentAsyncBuilder.init(); + } + + @Override + public void cancel() { + if (currentAsyncBuilder != null) { + currentAsyncBuilder.cancel(); + currentAsyncBuilder = null; + } + } + + private static final class AsyncRendererBuilder + implements ManifestFetcher.ManifestCallback, UtcTimingCallback { + + private final Context context; + private final String userAgent; + private final MediaDrmCallback drmCallback; + private final NPExoPlayer player; + private final ManifestFetcher manifestFetcher; + private final UriDataSource manifestDataSource; + + private boolean canceled; + private MediaPresentationDescription manifest; + private long elapsedRealtimeOffset; + + public AsyncRendererBuilder(Context context, String userAgent, String url, + MediaDrmCallback drmCallback, NPExoPlayer player) { + this.context = context; + this.userAgent = userAgent; + this.drmCallback = drmCallback; + this.player = player; + MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); + manifestDataSource = new DefaultUriDataSource(context, userAgent); + manifestFetcher = new ManifestFetcher<>(url, manifestDataSource, parser); + } + + public void init() { + manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); + } + + public void cancel() { + canceled = true; + } + + @Override + public void onSingleManifest(MediaPresentationDescription manifest) { + if (canceled) { + return; + } + + this.manifest = manifest; + if (manifest.dynamic && manifest.utcTiming != null) { + UtcTimingElementResolver.resolveTimingElement(manifestDataSource, manifest.utcTiming, + manifestFetcher.getManifestLoadCompleteTimestamp(), this); + } else { + buildRenderers(); + } + } + + @Override + public void onSingleManifestError(IOException e) { + if (canceled) { + return; + } + + player.onRenderersError(e); + } + + @Override + public void onTimestampResolved(UtcTimingElement utcTiming, long elapsedRealtimeOffset) { + if (canceled) { + return; + } + + this.elapsedRealtimeOffset = elapsedRealtimeOffset; + buildRenderers(); + } + + @Override + public void onTimestampError(UtcTimingElement utcTiming, IOException e) { + if (canceled) { + return; + } + + Log.e(TAG, "Failed to resolve UtcTiming element [" + utcTiming + "]", e); + // Be optimistic and continue in the hope that the device clock is correct. + buildRenderers(); + } + + private void buildRenderers() { + Period period = manifest.getPeriod(0); + Handler mainHandler = player.getMainHandler(); + LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); + DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player); + + boolean hasContentProtection = false; + for (int i = 0; i < period.adaptationSets.size(); i++) { + AdaptationSet adaptationSet = period.adaptationSets.get(i); + if (adaptationSet.type != AdaptationSet.TYPE_UNKNOWN) { + hasContentProtection |= adaptationSet.hasContentProtection(); + } + } + + // Check drm support if necessary. + boolean filterHdContent = false; + StreamingDrmSessionManager drmSessionManager = null; + if (hasContentProtection) { + if (Util.SDK_INT < 18) { + player.onRenderersError( + new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME)); + return; + } + try { + drmSessionManager = StreamingDrmSessionManager.newWidevineInstance( + player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player); + filterHdContent = getWidevineSecurityLevel(drmSessionManager) != SECURITY_LEVEL_1; + } catch (UnsupportedDrmException e) { + player.onRenderersError(e); + return; + } + } + + // Build the video renderer. + DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); + ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, + DefaultDashTrackSelector.newVideoInstance(context, true, filterHdContent), + videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS, + elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_VIDEO); + ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, + VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, + NPExoPlayer.TYPE_VIDEO); + TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, videoSampleSource, + MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, + drmSessionManager, true, mainHandler, player, 50); + + // Build the audio renderer. + DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); + ChunkSource audioChunkSource = new DashChunkSource(manifestFetcher, + DefaultDashTrackSelector.newAudioInstance(), audioDataSource, null, LIVE_EDGE_LATENCY_MS, + elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_AUDIO); + ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, + AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, + NPExoPlayer.TYPE_AUDIO); + TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, + MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, player, + AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC); + + // Build the text renderer. + DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); + ChunkSource textChunkSource = new DashChunkSource(manifestFetcher, + DefaultDashTrackSelector.newTextInstance(), textDataSource, null, LIVE_EDGE_LATENCY_MS, + elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_TEXT); + ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl, + TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, + NPExoPlayer.TYPE_TEXT); + TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player, + mainHandler.getLooper()); + + // Invoke the callback. + TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT]; + renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer; + renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer; + renderers[NPExoPlayer.TYPE_TEXT] = textRenderer; + player.onRenderers(renderers, bandwidthMeter); + } + + private static int getWidevineSecurityLevel(StreamingDrmSessionManager sessionManager) { + String securityLevelProperty = sessionManager.getPropertyString("securityLevel"); + return securityLevelProperty.equals("L1") ? SECURITY_LEVEL_1 : securityLevelProperty + .equals("L3") ? SECURITY_LEVEL_3 : SECURITY_LEVEL_UNKNOWN; + } + + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/player/exoplayer/EventLogger.java b/app/src/main/java/org/schabi/newpipe/player/exoplayer/EventLogger.java new file mode 100644 index 000000000..62553ab3b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/exoplayer/EventLogger.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.schabi.newpipe.player.exoplayer; + +import com.google.android.exoplayer.ExoPlayer; +import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; +import com.google.android.exoplayer.TimeRange; +import com.google.android.exoplayer.audio.AudioTrack; +import com.google.android.exoplayer.chunk.Format; +import com.google.android.exoplayer.util.VerboseLogUtil; + +import android.media.MediaCodec.CryptoException; +import android.os.SystemClock; +import android.util.Log; + +import java.io.IOException; +import java.text.NumberFormat; +import java.util.Locale; + +/** + * Logs player events using {@link Log}. + */ +public class EventLogger implements NPExoPlayer.Listener, NPExoPlayer.InfoListener, + NPExoPlayer.InternalErrorListener { + + private static final String TAG = "EventLogger"; + private static final NumberFormat TIME_FORMAT; + static { + TIME_FORMAT = NumberFormat.getInstance(Locale.US); + TIME_FORMAT.setMinimumFractionDigits(2); + TIME_FORMAT.setMaximumFractionDigits(2); + } + + private long sessionStartTimeMs; + private long[] loadStartTimeMs; + private long[] availableRangeValuesUs; + + public EventLogger() { + loadStartTimeMs = new long[NPExoPlayer.RENDERER_COUNT]; + } + + public void startSession() { + sessionStartTimeMs = SystemClock.elapsedRealtime(); + Log.d(TAG, "start [0]"); + } + + public void endSession() { + Log.d(TAG, "end [" + getSessionTimeString() + "]"); + } + + // NPExoPlayer.Listener + + @Override + public void onStateChanged(boolean playWhenReady, int state) { + Log.d(TAG, "state [" + getSessionTimeString() + ", " + playWhenReady + ", " + + getStateString(state) + "]"); + } + + @Override + public void onError(Exception e) { + Log.e(TAG, "playerFailed [" + getSessionTimeString() + "]", e); + } + + @Override + public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, + float pixelWidthHeightRatio) { + Log.d(TAG, "videoSizeChanged [" + width + ", " + height + ", " + unappliedRotationDegrees + + ", " + pixelWidthHeightRatio + "]"); + } + + // NPExoPlayer.InfoListener + + @Override + public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) { + Log.d(TAG, "bandwidth [" + getSessionTimeString() + ", " + bytes + ", " + + getTimeString(elapsedMs) + ", " + bitrateEstimate + "]"); + } + + @Override + public void onDroppedFrames(int count, long elapsed) { + Log.d(TAG, "droppedFrames [" + getSessionTimeString() + ", " + count + "]"); + } + + @Override + public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format, + long mediaStartTimeMs, long mediaEndTimeMs) { + loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime(); + if (VerboseLogUtil.isTagEnabled(TAG)) { + Log.v(TAG, "loadStart [" + getSessionTimeString() + ", " + sourceId + ", " + type + + ", " + mediaStartTimeMs + ", " + mediaEndTimeMs + "]"); + } + } + + @Override + public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format, + long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) { + if (VerboseLogUtil.isTagEnabled(TAG)) { + long downloadTime = SystemClock.elapsedRealtime() - loadStartTimeMs[sourceId]; + Log.v(TAG, "loadEnd [" + getSessionTimeString() + ", " + sourceId + ", " + downloadTime + + "]"); + } + } + + @Override + public void onVideoFormatEnabled(Format format, int trigger, long mediaTimeMs) { + Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + format.id + ", " + + Integer.toString(trigger) + "]"); + } + + @Override + public void onAudioFormatEnabled(Format format, int trigger, long mediaTimeMs) { + Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + format.id + ", " + + Integer.toString(trigger) + "]"); + } + + // NPExoPlayer.InternalErrorListener + + @Override + public void onLoadError(int sourceId, IOException e) { + printInternalError("loadError", e); + } + + @Override + public void onRendererInitializationError(Exception e) { + printInternalError("rendererInitError", e); + } + + @Override + public void onDrmSessionManagerError(Exception e) { + printInternalError("drmSessionManagerError", e); + } + + @Override + public void onDecoderInitializationError(DecoderInitializationException e) { + printInternalError("decoderInitializationError", e); + } + + @Override + public void onAudioTrackInitializationError(AudioTrack.InitializationException e) { + printInternalError("audioTrackInitializationError", e); + } + + @Override + public void onAudioTrackWriteError(AudioTrack.WriteException e) { + printInternalError("audioTrackWriteError", e); + } + + @Override + public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + printInternalError("audioTrackUnderrun [" + bufferSize + ", " + bufferSizeMs + ", " + + elapsedSinceLastFeedMs + "]", null); + } + + @Override + public void onCryptoError(CryptoException e) { + printInternalError("cryptoError", e); + } + + @Override + public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, + long initializationDurationMs) { + Log.d(TAG, "decoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]"); + } + + @Override + public void onAvailableRangeChanged(int sourceId, TimeRange availableRange) { + availableRangeValuesUs = availableRange.getCurrentBoundsUs(availableRangeValuesUs); + Log.d(TAG, "availableRange [" + availableRange.isStatic() + ", " + availableRangeValuesUs[0] + + ", " + availableRangeValuesUs[1] + "]"); + } + + private void printInternalError(String type, Exception e) { + Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e); + } + + private String getStateString(int state) { + switch (state) { + case ExoPlayer.STATE_BUFFERING: + return "B"; + case ExoPlayer.STATE_ENDED: + return "E"; + case ExoPlayer.STATE_IDLE: + return "I"; + case ExoPlayer.STATE_PREPARING: + return "P"; + case ExoPlayer.STATE_READY: + return "R"; + default: + return "?"; + } + } + + private String getSessionTimeString() { + return getTimeString(SystemClock.elapsedRealtime() - sessionStartTimeMs); + } + + private String getTimeString(long timeMs) { + return TIME_FORMAT.format((timeMs) / 1000f); + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/player/exoplayer/ExtractorRendererBuilder.java b/app/src/main/java/org/schabi/newpipe/player/exoplayer/ExtractorRendererBuilder.java new file mode 100644 index 000000000..a74c33bf8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/exoplayer/ExtractorRendererBuilder.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.schabi.newpipe.player.exoplayer; + +import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder; + +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecSelector; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.audio.AudioCapabilities; +import com.google.android.exoplayer.extractor.Extractor; +import com.google.android.exoplayer.extractor.ExtractorSampleSource; +import com.google.android.exoplayer.text.TextTrackRenderer; +import com.google.android.exoplayer.upstream.Allocator; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DefaultAllocator; +import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer.upstream.DefaultUriDataSource; + +import android.content.Context; +import android.media.AudioManager; +import android.media.MediaCodec; +import android.net.Uri; + +/** + * A {@link RendererBuilder} for streams that can be read using an {@link Extractor}. + */ +public class ExtractorRendererBuilder implements RendererBuilder { + + private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; + private static final int BUFFER_SEGMENT_COUNT = 256; + + private final Context context; + private final String userAgent; + private final Uri uri; + + public ExtractorRendererBuilder(Context context, String userAgent, Uri uri) { + this.context = context; + this.userAgent = userAgent; + this.uri = uri; + } + + @Override + public void buildRenderers(NPExoPlayer player) { + Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE); + + // Build the video and audio renderers. + DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(player.getMainHandler(), + null); + DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); + ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, allocator, + BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE); + MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, + sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, + player.getMainHandler(), player, 50); + MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource, + MediaCodecSelector.DEFAULT, null, true, player.getMainHandler(), player, + AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC); + TrackRenderer textRenderer = new TextTrackRenderer(sampleSource, player, + player.getMainHandler().getLooper()); + + // Invoke the callback. + TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT]; + renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer; + renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer; + renderers[NPExoPlayer.TYPE_TEXT] = textRenderer; + player.onRenderers(renderers, bandwidthMeter); + } + + @Override + public void cancel() { + // Do nothing. + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/player/exoplayer/HlsRendererBuilder.java b/app/src/main/java/org/schabi/newpipe/player/exoplayer/HlsRendererBuilder.java new file mode 100644 index 000000000..8e6c2d9f5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/exoplayer/HlsRendererBuilder.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.schabi.newpipe.player.exoplayer; + +import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder; + +import com.google.android.exoplayer.DefaultLoadControl; +import com.google.android.exoplayer.LoadControl; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecSelector; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.audio.AudioCapabilities; +import com.google.android.exoplayer.hls.DefaultHlsTrackSelector; +import com.google.android.exoplayer.hls.HlsChunkSource; +import com.google.android.exoplayer.hls.HlsMasterPlaylist; +import com.google.android.exoplayer.hls.HlsPlaylist; +import com.google.android.exoplayer.hls.HlsPlaylistParser; +import com.google.android.exoplayer.hls.HlsSampleSource; +import com.google.android.exoplayer.hls.PtsTimestampAdjusterProvider; +import com.google.android.exoplayer.metadata.Id3Parser; +import com.google.android.exoplayer.metadata.MetadataTrackRenderer; +import com.google.android.exoplayer.text.TextTrackRenderer; +import com.google.android.exoplayer.text.eia608.Eia608TrackRenderer; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DefaultAllocator; +import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer.upstream.DefaultUriDataSource; +import com.google.android.exoplayer.util.ManifestFetcher; +import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; + +import android.content.Context; +import android.media.AudioManager; +import android.media.MediaCodec; +import android.os.Handler; + +import java.io.IOException; +import java.util.Map; + +/** + * A {@link RendererBuilder} for HLS. + */ +public class HlsRendererBuilder implements RendererBuilder { + + private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; + private static final int MAIN_BUFFER_SEGMENTS = 256; + private static final int TEXT_BUFFER_SEGMENTS = 2; + + private final Context context; + private final String userAgent; + private final String url; + + private AsyncRendererBuilder currentAsyncBuilder; + + public HlsRendererBuilder(Context context, String userAgent, String url) { + this.context = context; + this.userAgent = userAgent; + this.url = url; + } + + @Override + public void buildRenderers(NPExoPlayer player) { + currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, player); + currentAsyncBuilder.init(); + } + + @Override + public void cancel() { + if (currentAsyncBuilder != null) { + currentAsyncBuilder.cancel(); + currentAsyncBuilder = null; + } + } + + private static final class AsyncRendererBuilder implements ManifestCallback { + + private final Context context; + private final String userAgent; + private final String url; + private final NPExoPlayer player; + private final ManifestFetcher playlistFetcher; + + private boolean canceled; + + public AsyncRendererBuilder(Context context, String userAgent, String url, NPExoPlayer player) { + this.context = context; + this.userAgent = userAgent; + this.url = url; + this.player = player; + HlsPlaylistParser parser = new HlsPlaylistParser(); + playlistFetcher = new ManifestFetcher<>(url, new DefaultUriDataSource(context, userAgent), + parser); + } + + public void init() { + playlistFetcher.singleLoad(player.getMainHandler().getLooper(), this); + } + + public void cancel() { + canceled = true; + } + + @Override + public void onSingleManifestError(IOException e) { + if (canceled) { + return; + } + + player.onRenderersError(e); + } + + @Override + public void onSingleManifest(HlsPlaylist manifest) { + if (canceled) { + return; + } + + Handler mainHandler = player.getMainHandler(); + LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); + DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + PtsTimestampAdjusterProvider timestampAdjusterProvider = new PtsTimestampAdjusterProvider(); + + // Build the video/audio/metadata renderers. + DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); + HlsChunkSource chunkSource = new HlsChunkSource(true /* isMaster */, dataSource, url, + manifest, DefaultHlsTrackSelector.newDefaultInstance(context), bandwidthMeter, + timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE); + HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl, + MAIN_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, NPExoPlayer.TYPE_VIDEO); + MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, + sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, + 5000, mainHandler, player, 50); + MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource, + MediaCodecSelector.DEFAULT, null, true, player.getMainHandler(), player, + AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC); + MetadataTrackRenderer> id3Renderer = new MetadataTrackRenderer<>( + sampleSource, new Id3Parser(), player, mainHandler.getLooper()); + + // Build the text renderer, preferring Webvtt where available. + boolean preferWebvtt = false; + if (manifest instanceof HlsMasterPlaylist) { + preferWebvtt = !((HlsMasterPlaylist) manifest).subtitles.isEmpty(); + } + TrackRenderer textRenderer; + if (preferWebvtt) { + DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); + HlsChunkSource textChunkSource = new HlsChunkSource(false /* isMaster */, textDataSource, + url, manifest, DefaultHlsTrackSelector.newVttInstance(), bandwidthMeter, + timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE); + HlsSampleSource textSampleSource = new HlsSampleSource(textChunkSource, loadControl, + TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, NPExoPlayer.TYPE_TEXT); + textRenderer = new TextTrackRenderer(textSampleSource, player, mainHandler.getLooper()); + } else { + textRenderer = new Eia608TrackRenderer(sampleSource, player, mainHandler.getLooper()); + } + + TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT]; + renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer; + renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer; + renderers[NPExoPlayer.TYPE_METADATA] = id3Renderer; + renderers[NPExoPlayer.TYPE_TEXT] = textRenderer; + player.onRenderers(renderers, bandwidthMeter); + } + + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/player/exoplayer/NPExoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/exoplayer/NPExoPlayer.java new file mode 100644 index 000000000..63a6a9261 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/exoplayer/NPExoPlayer.java @@ -0,0 +1,599 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.schabi.newpipe.player.exoplayer; + +import com.google.android.exoplayer.CodecCounters; +import com.google.android.exoplayer.DummyTrackRenderer; +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.ExoPlayer; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecTrackRenderer; +import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.TimeRange; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.audio.AudioTrack; +import com.google.android.exoplayer.chunk.ChunkSampleSource; +import com.google.android.exoplayer.chunk.Format; +import com.google.android.exoplayer.dash.DashChunkSource; +import com.google.android.exoplayer.drm.StreamingDrmSessionManager; +import com.google.android.exoplayer.hls.HlsSampleSource; +import com.google.android.exoplayer.metadata.MetadataTrackRenderer.MetadataRenderer; +import com.google.android.exoplayer.text.Cue; +import com.google.android.exoplayer.text.TextRenderer; +import com.google.android.exoplayer.upstream.BandwidthMeter; +import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer.util.DebugTextViewHelper; +import com.google.android.exoplayer.util.PlayerControl; + +import android.media.MediaCodec.CryptoException; +import android.os.Handler; +import android.os.Looper; +import android.view.Surface; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * A wrapper around {@link ExoPlayer} that provides a higher level interface. It can be prepared + * with one of a number of {@link RendererBuilder} classes to suit different use cases (e.g. DASH, + * SmoothStreaming and so on). + */ +public class NPExoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener, + HlsSampleSource.EventListener, DefaultBandwidthMeter.EventListener, + MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener, + StreamingDrmSessionManager.EventListener, DashChunkSource.EventListener, TextRenderer, + MetadataRenderer>, DebugTextViewHelper.Provider { + + /** + * Builds renderers for the player. + */ + public interface RendererBuilder { + /** + * Builds renderers for playback. + * + * @param player The player for which renderers are being built. {@link NPExoPlayer#onRenderers} + * should be invoked once the renderers have been built. If building fails, + * {@link NPExoPlayer#onRenderersError} should be invoked. + */ + void buildRenderers(NPExoPlayer player); + /** + * Cancels the current build operation, if there is one. Else does nothing. + *

+ * A canceled build operation must not invoke {@link NPExoPlayer#onRenderers} or + * {@link NPExoPlayer#onRenderersError} on the player, which may have been released. + */ + void cancel(); + } + + /** + * A listener for core events. + */ + public interface Listener { + void onStateChanged(boolean playWhenReady, int playbackState); + void onError(Exception e); + void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, + float pixelWidthHeightRatio); + } + + /** + * A listener for internal errors. + *

+ * These errors are not visible to the user, and hence this listener is provided for + * informational purposes only. Note however that an internal error may cause a fatal + * error if the player fails to recover. If this happens, {@link Listener#onError(Exception)} + * will be invoked. + */ + public interface InternalErrorListener { + void onRendererInitializationError(Exception e); + void onAudioTrackInitializationError(AudioTrack.InitializationException e); + void onAudioTrackWriteError(AudioTrack.WriteException e); + void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs); + void onDecoderInitializationError(DecoderInitializationException e); + void onCryptoError(CryptoException e); + void onLoadError(int sourceId, IOException e); + void onDrmSessionManagerError(Exception e); + } + + /** + * A listener for debugging information. + */ + public interface InfoListener { + void onVideoFormatEnabled(Format format, int trigger, long mediaTimeMs); + void onAudioFormatEnabled(Format format, int trigger, long mediaTimeMs); + void onDroppedFrames(int count, long elapsed); + void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate); + void onLoadStarted(int sourceId, long length, int type, int trigger, Format format, + long mediaStartTimeMs, long mediaEndTimeMs); + void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format, + long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs); + void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, + long initializationDurationMs); + void onAvailableRangeChanged(int sourceId, TimeRange availableRange); + } + + /** + * A listener for receiving notifications of timed text. + */ + public interface CaptionListener { + void onCues(List cues); + } + + /** + * A listener for receiving ID3 metadata parsed from the media stream. + */ + public interface Id3MetadataListener { + void onId3Metadata(Map metadata); + } + + // Constants pulled into this class for convenience. + public static final int STATE_IDLE = ExoPlayer.STATE_IDLE; + public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING; + public static final int STATE_BUFFERING = ExoPlayer.STATE_BUFFERING; + public static final int STATE_READY = ExoPlayer.STATE_READY; + public static final int STATE_ENDED = ExoPlayer.STATE_ENDED; + public static final int TRACK_DISABLED = ExoPlayer.TRACK_DISABLED; + public static final int TRACK_DEFAULT = ExoPlayer.TRACK_DEFAULT; + + public static final int RENDERER_COUNT = 4; + public static final int TYPE_VIDEO = 0; + public static final int TYPE_AUDIO = 1; + public static final int TYPE_TEXT = 2; + public static final int TYPE_METADATA = 3; + + private static final int RENDERER_BUILDING_STATE_IDLE = 1; + private static final int RENDERER_BUILDING_STATE_BUILDING = 2; + private static final int RENDERER_BUILDING_STATE_BUILT = 3; + + private final RendererBuilder rendererBuilder; + private final ExoPlayer player; + private final PlayerControl playerControl; + private final Handler mainHandler; + private final CopyOnWriteArrayList listeners; + + private int rendererBuildingState; + private int lastReportedPlaybackState; + private boolean lastReportedPlayWhenReady; + + private Surface surface; + private TrackRenderer videoRenderer; + private CodecCounters codecCounters; + private Format videoFormat; + private int videoTrackToRestore; + + private BandwidthMeter bandwidthMeter; + private boolean backgrounded; + + private CaptionListener captionListener; + private Id3MetadataListener id3MetadataListener; + private InternalErrorListener internalErrorListener; + private InfoListener infoListener; + + public NPExoPlayer(RendererBuilder rendererBuilder) { + this.rendererBuilder = rendererBuilder; + player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000); + player.addListener(this); + playerControl = new PlayerControl(player); + mainHandler = new Handler(); + listeners = new CopyOnWriteArrayList<>(); + lastReportedPlaybackState = STATE_IDLE; + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + // Disable text initially. + player.setSelectedTrack(TYPE_TEXT, TRACK_DISABLED); + } + + public PlayerControl getPlayerControl() { + return playerControl; + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + public void setInternalErrorListener(InternalErrorListener listener) { + internalErrorListener = listener; + } + + public void setInfoListener(InfoListener listener) { + infoListener = listener; + } + + public void setCaptionListener(CaptionListener listener) { + captionListener = listener; + } + + public void setMetadataListener(Id3MetadataListener listener) { + id3MetadataListener = listener; + } + + public void setSurface(Surface surface) { + this.surface = surface; + pushSurface(false); + } + + public Surface getSurface() { + return surface; + } + + public void blockingClearSurface() { + surface = null; + pushSurface(true); + } + + public int getTrackCount(int type) { + return player.getTrackCount(type); + } + + public MediaFormat getTrackFormat(int type, int index) { + return player.getTrackFormat(type, index); + } + + public int getSelectedTrack(int type) { + return player.getSelectedTrack(type); + } + + public void setSelectedTrack(int type, int index) { + player.setSelectedTrack(type, index); + if (type == TYPE_TEXT && index < 0 && captionListener != null) { + captionListener.onCues(Collections.emptyList()); + } + } + + public boolean getBackgrounded() { + return backgrounded; + } + + public void setBackgrounded(boolean backgrounded) { + if (this.backgrounded == backgrounded) { + return; + } + this.backgrounded = backgrounded; + if (backgrounded) { + videoTrackToRestore = getSelectedTrack(TYPE_VIDEO); + setSelectedTrack(TYPE_VIDEO, TRACK_DISABLED); + blockingClearSurface(); + } else { + setSelectedTrack(TYPE_VIDEO, videoTrackToRestore); + } + } + + public void prepare() { + if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT) { + player.stop(); + } + rendererBuilder.cancel(); + videoFormat = null; + videoRenderer = null; + rendererBuildingState = RENDERER_BUILDING_STATE_BUILDING; + maybeReportPlayerState(); + rendererBuilder.buildRenderers(this); + } + + /** + * Invoked with the results from a {@link RendererBuilder}. + * + * @param renderers Renderers indexed by {@link NPExoPlayer} TYPE_* constants. An individual + * element may be null if there do not exist tracks of the corresponding type. + * @param bandwidthMeter Provides an estimate of the currently available bandwidth. May be null. + */ + /* package */ void onRenderers(TrackRenderer[] renderers, BandwidthMeter bandwidthMeter) { + for (int i = 0; i < RENDERER_COUNT; i++) { + if (renderers[i] == null) { + // Convert a null renderer to a dummy renderer. + renderers[i] = new DummyTrackRenderer(); + } + } + // Complete preparation. + this.videoRenderer = renderers[TYPE_VIDEO]; + this.codecCounters = videoRenderer instanceof MediaCodecTrackRenderer + ? ((MediaCodecTrackRenderer) videoRenderer).codecCounters + : renderers[TYPE_AUDIO] instanceof MediaCodecTrackRenderer + ? ((MediaCodecTrackRenderer) renderers[TYPE_AUDIO]).codecCounters : null; + this.bandwidthMeter = bandwidthMeter; + pushSurface(false); + player.prepare(renderers); + rendererBuildingState = RENDERER_BUILDING_STATE_BUILT; + } + + /** + * Invoked if a {@link RendererBuilder} encounters an error. + * + * @param e Describes the error. + */ + /* package */ void onRenderersError(Exception e) { + if (internalErrorListener != null) { + internalErrorListener.onRendererInitializationError(e); + } + for (Listener listener : listeners) { + listener.onError(e); + } + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + maybeReportPlayerState(); + } + + public void setPlayWhenReady(boolean playWhenReady) { + player.setPlayWhenReady(playWhenReady); + } + + public void seekTo(long positionMs) { + player.seekTo(positionMs); + } + + public void release() { + rendererBuilder.cancel(); + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + surface = null; + player.release(); + } + + public int getPlaybackState() { + if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) { + return STATE_PREPARING; + } + int playerState = player.getPlaybackState(); + if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT && playerState == STATE_IDLE) { + // This is an edge case where the renderers are built, but are still being passed to the + // player's playback thread. + return STATE_PREPARING; + } + return playerState; + } + + @Override + public Format getFormat() { + return videoFormat; + } + + @Override + public BandwidthMeter getBandwidthMeter() { + return bandwidthMeter; + } + + @Override + public CodecCounters getCodecCounters() { + return codecCounters; + } + + @Override + public long getCurrentPosition() { + return player.getCurrentPosition(); + } + + public long getDuration() { + return player.getDuration(); + } + + public int getBufferedPercentage() { + return player.getBufferedPercentage(); + } + + public boolean getPlayWhenReady() { + return player.getPlayWhenReady(); + } + + /* package */ Looper getPlaybackLooper() { + return player.getPlaybackLooper(); + } + + /* package */ Handler getMainHandler() { + return mainHandler; + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int state) { + maybeReportPlayerState(); + } + + @Override + public void onPlayerError(ExoPlaybackException exception) { + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + for (Listener listener : listeners) { + listener.onError(exception); + } + } + + @Override + public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, + float pixelWidthHeightRatio) { + for (Listener listener : listeners) { + listener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); + } + } + + @Override + public void onDroppedFrames(int count, long elapsed) { + if (infoListener != null) { + infoListener.onDroppedFrames(count, elapsed); + } + } + + @Override + public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) { + if (infoListener != null) { + infoListener.onBandwidthSample(elapsedMs, bytes, bitrateEstimate); + } + } + + @Override + public void onDownstreamFormatChanged(int sourceId, Format format, int trigger, + long mediaTimeMs) { + if (infoListener == null) { + return; + } + if (sourceId == TYPE_VIDEO) { + videoFormat = format; + infoListener.onVideoFormatEnabled(format, trigger, mediaTimeMs); + } else if (sourceId == TYPE_AUDIO) { + infoListener.onAudioFormatEnabled(format, trigger, mediaTimeMs); + } + } + + @Override + public void onDrmKeysLoaded() { + // Do nothing. + } + + @Override + public void onDrmSessionManagerError(Exception e) { + if (internalErrorListener != null) { + internalErrorListener.onDrmSessionManagerError(e); + } + } + + @Override + public void onDecoderInitializationError(DecoderInitializationException e) { + if (internalErrorListener != null) { + internalErrorListener.onDecoderInitializationError(e); + } + } + + @Override + public void onAudioTrackInitializationError(AudioTrack.InitializationException e) { + if (internalErrorListener != null) { + internalErrorListener.onAudioTrackInitializationError(e); + } + } + + @Override + public void onAudioTrackWriteError(AudioTrack.WriteException e) { + if (internalErrorListener != null) { + internalErrorListener.onAudioTrackWriteError(e); + } + } + + @Override + public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + if (internalErrorListener != null) { + internalErrorListener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); + } + } + + @Override + public void onCryptoError(CryptoException e) { + if (internalErrorListener != null) { + internalErrorListener.onCryptoError(e); + } + } + + @Override + public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, + long initializationDurationMs) { + if (infoListener != null) { + infoListener.onDecoderInitialized(decoderName, elapsedRealtimeMs, initializationDurationMs); + } + } + + @Override + public void onLoadError(int sourceId, IOException e) { + if (internalErrorListener != null) { + internalErrorListener.onLoadError(sourceId, e); + } + } + + @Override + public void onCues(List cues) { + if (captionListener != null && getSelectedTrack(TYPE_TEXT) != TRACK_DISABLED) { + captionListener.onCues(cues); + } + } + + @Override + public void onMetadata(Map metadata) { + if (id3MetadataListener != null && getSelectedTrack(TYPE_METADATA) != TRACK_DISABLED) { + id3MetadataListener.onId3Metadata(metadata); + } + } + + @Override + public void onAvailableRangeChanged(int sourceId, TimeRange availableRange) { + if (infoListener != null) { + infoListener.onAvailableRangeChanged(sourceId, availableRange); + } + } + + @Override + public void onPlayWhenReadyCommitted() { + // Do nothing. + } + + @Override + public void onDrawnToSurface(Surface surface) { + // Do nothing. + } + + @Override + public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format, + long mediaStartTimeMs, long mediaEndTimeMs) { + if (infoListener != null) { + infoListener.onLoadStarted(sourceId, length, type, trigger, format, mediaStartTimeMs, + mediaEndTimeMs); + } + } + + @Override + public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format, + long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) { + if (infoListener != null) { + infoListener.onLoadCompleted(sourceId, bytesLoaded, type, trigger, format, mediaStartTimeMs, + mediaEndTimeMs, elapsedRealtimeMs, loadDurationMs); + } + } + + @Override + public void onLoadCanceled(int sourceId, long bytesLoaded) { + // Do nothing. + } + + @Override + public void onUpstreamDiscarded(int sourceId, long mediaStartTimeMs, long mediaEndTimeMs) { + // Do nothing. + } + + private void maybeReportPlayerState() { + boolean playWhenReady = player.getPlayWhenReady(); + int playbackState = getPlaybackState(); + if (lastReportedPlayWhenReady != playWhenReady || lastReportedPlaybackState != playbackState) { + for (Listener listener : listeners) { + listener.onStateChanged(playWhenReady, playbackState); + } + lastReportedPlayWhenReady = playWhenReady; + lastReportedPlaybackState = playbackState; + } + } + + private void pushSurface(boolean blockForSurfacePush) { + if (videoRenderer == null) { + return; + } + + if (blockForSurfacePush) { + player.blockingSendMessage( + videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); + } else { + player.sendMessage( + videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); + } + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/player/exoplayer/SmoothStreamingRendererBuilder.java b/app/src/main/java/org/schabi/newpipe/player/exoplayer/SmoothStreamingRendererBuilder.java new file mode 100644 index 000000000..55b59c276 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/exoplayer/SmoothStreamingRendererBuilder.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.schabi.newpipe.player.exoplayer; + + +import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder; + +import com.google.android.exoplayer.DefaultLoadControl; +import com.google.android.exoplayer.LoadControl; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecSelector; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.audio.AudioCapabilities; +import com.google.android.exoplayer.chunk.ChunkSampleSource; +import com.google.android.exoplayer.chunk.ChunkSource; +import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; +import com.google.android.exoplayer.drm.DrmSessionManager; +import com.google.android.exoplayer.drm.MediaDrmCallback; +import com.google.android.exoplayer.drm.StreamingDrmSessionManager; +import com.google.android.exoplayer.drm.UnsupportedDrmException; +import com.google.android.exoplayer.smoothstreaming.DefaultSmoothStreamingTrackSelector; +import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource; +import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest; +import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestParser; +import com.google.android.exoplayer.text.TextTrackRenderer; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DefaultAllocator; +import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer.upstream.DefaultHttpDataSource; +import com.google.android.exoplayer.upstream.DefaultUriDataSource; +import com.google.android.exoplayer.util.ManifestFetcher; +import com.google.android.exoplayer.util.Util; + +import android.content.Context; +import android.media.AudioManager; +import android.media.MediaCodec; +import android.os.Handler; + +import java.io.IOException; + +/** + * A {@link RendererBuilder} for SmoothStreaming. + */ +public class SmoothStreamingRendererBuilder implements RendererBuilder { + + private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; + private static final int VIDEO_BUFFER_SEGMENTS = 200; + private static final int AUDIO_BUFFER_SEGMENTS = 54; + private static final int TEXT_BUFFER_SEGMENTS = 2; + private static final int LIVE_EDGE_LATENCY_MS = 30000; + + private final Context context; + private final String userAgent; + private final String url; + private final MediaDrmCallback drmCallback; + + private AsyncRendererBuilder currentAsyncBuilder; + + public SmoothStreamingRendererBuilder(Context context, String userAgent, String url, + MediaDrmCallback drmCallback) { + this.context = context; + this.userAgent = userAgent; + this.url = Util.toLowerInvariant(url).endsWith("/manifest") ? url : url + "/Manifest"; + this.drmCallback = drmCallback; + } + + @Override + public void buildRenderers(NPExoPlayer player) { + currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, drmCallback, player); + currentAsyncBuilder.init(); + } + + @Override + public void cancel() { + if (currentAsyncBuilder != null) { + currentAsyncBuilder.cancel(); + currentAsyncBuilder = null; + } + } + + private static final class AsyncRendererBuilder + implements ManifestFetcher.ManifestCallback { + + private final Context context; + private final String userAgent; + private final MediaDrmCallback drmCallback; + private final NPExoPlayer player; + private final ManifestFetcher manifestFetcher; + + private boolean canceled; + + public AsyncRendererBuilder(Context context, String userAgent, String url, + MediaDrmCallback drmCallback, NPExoPlayer player) { + this.context = context; + this.userAgent = userAgent; + this.drmCallback = drmCallback; + this.player = player; + SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser(); + manifestFetcher = new ManifestFetcher<>(url, new DefaultHttpDataSource(userAgent, null), + parser); + } + + public void init() { + manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); + } + + public void cancel() { + canceled = true; + } + + @Override + public void onSingleManifestError(IOException exception) { + if (canceled) { + return; + } + + player.onRenderersError(exception); + } + + @Override + public void onSingleManifest(SmoothStreamingManifest manifest) { + if (canceled) { + return; + } + + Handler mainHandler = player.getMainHandler(); + LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); + DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player); + + // Check drm support if necessary. + DrmSessionManager drmSessionManager = null; + if (manifest.protectionElement != null) { + if (Util.SDK_INT < 18) { + player.onRenderersError( + new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME)); + return; + } + try { + drmSessionManager = new StreamingDrmSessionManager(manifest.protectionElement.uuid, + player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player); + } catch (UnsupportedDrmException e) { + player.onRenderersError(e); + return; + } + } + + // Build the video renderer. + DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); + ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher, + DefaultSmoothStreamingTrackSelector.newVideoInstance(context, true, false), + videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS); + ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, + VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, + NPExoPlayer.TYPE_VIDEO); + TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, videoSampleSource, + MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, + drmSessionManager, true, mainHandler, player, 50); + + // Build the audio renderer. + DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); + ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifestFetcher, + DefaultSmoothStreamingTrackSelector.newAudioInstance(), + audioDataSource, null, LIVE_EDGE_LATENCY_MS); + ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, + AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, + NPExoPlayer.TYPE_AUDIO); + TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, + MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, player, + AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC); + + // Build the text renderer. + DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); + ChunkSource textChunkSource = new SmoothStreamingChunkSource(manifestFetcher, + DefaultSmoothStreamingTrackSelector.newTextInstance(), + textDataSource, null, LIVE_EDGE_LATENCY_MS); + ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl, + TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, + NPExoPlayer.TYPE_TEXT); + TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player, + mainHandler.getLooper()); + + // Invoke the callback. + TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT]; + renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer; + renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer; + renderers[NPExoPlayer.TYPE_TEXT] = textRenderer; + player.onRenderers(renderers, bandwidthMeter); + } + + } + +} diff --git a/app/src/main/res/drawable-hdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_arrow_back_white_24dp.png new file mode 100644 index 000000000..cd1972677 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_arrow_back_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_play_circle_transparent.png b/app/src/main/res/drawable-hdpi/ic_play_circle_transparent.png new file mode 100644 index 000000000..4290e2346 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_play_circle_transparent.png differ diff --git a/app/src/main/res/drawable-ldrtl-hdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-ldrtl-hdpi/ic_arrow_back_white_24dp.png new file mode 100644 index 000000000..f51755762 Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-hdpi/ic_arrow_back_white_24dp.png differ diff --git a/app/src/main/res/drawable-ldrtl-mdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-ldrtl-mdpi/ic_arrow_back_white_24dp.png new file mode 100644 index 000000000..22a1140ae Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-mdpi/ic_arrow_back_white_24dp.png differ diff --git a/app/src/main/res/drawable-ldrtl-xhdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-ldrtl-xhdpi/ic_arrow_back_white_24dp.png new file mode 100644 index 000000000..d858f18e6 Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-xhdpi/ic_arrow_back_white_24dp.png differ diff --git a/app/src/main/res/drawable-ldrtl-xxhdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-ldrtl-xxhdpi/ic_arrow_back_white_24dp.png new file mode 100644 index 000000000..614ad49a3 Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-xxhdpi/ic_arrow_back_white_24dp.png differ diff --git a/app/src/main/res/drawable-ldrtl-xxxhdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-ldrtl-xxxhdpi/ic_arrow_back_white_24dp.png new file mode 100644 index 000000000..d409b544b Binary files /dev/null and b/app/src/main/res/drawable-ldrtl-xxxhdpi/ic_arrow_back_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_arrow_back_white_24dp.png new file mode 100644 index 000000000..4ef72eec9 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_arrow_back_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_play_circle_transparent.png b/app/src/main/res/drawable-mdpi/ic_play_circle_transparent.png new file mode 100644 index 000000000..743e4e810 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_play_circle_transparent.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_arrow_back_white_24dp.png new file mode 100644 index 000000000..832f5a361 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_arrow_back_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_play_circle_transparent.png b/app/src/main/res/drawable-xhdpi/ic_play_circle_transparent.png new file mode 100644 index 000000000..afb9a7bf6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_play_circle_transparent.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white_24dp.png new file mode 100644 index 000000000..32a6d91ce Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_play_circle_transparent.png b/app/src/main/res/drawable-xxhdpi/ic_play_circle_transparent.png new file mode 100644 index 000000000..5d7afaef4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_play_circle_transparent.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white_24dp.png new file mode 100644 index 000000000..e27034d67 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_play_circle_transparent.png b/app/src/main/res/drawable-xxxhdpi/ic_play_circle_transparent.png new file mode 100644 index 000000000..5bc515bf2 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_play_circle_transparent.png differ diff --git a/app/src/main/res/layout-v18/fragment_videoitem_detail.xml b/app/src/main/res/layout-v18/fragment_videoitem_detail.xml new file mode 100644 index 000000000..8473c098c --- /dev/null +++ b/app/src/main/res/layout-v18/fragment_videoitem_detail.xml @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + +