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