1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2026-01-13 18:22:41 +00:00

Compare commits

..

166 Commits

Author SHA1 Message Date
Weblate
794ae4c5da Merge remote-tracking branch 'origin/master' 2016-03-13 22:38:50 +01:00
Mladen Pejaković
060e744924 Translated using Weblate (Serbian)
Currently translated at 98.1% (105 of 107 strings)
2016-03-13 22:38:50 +01:00
Christian Schabesberger
de62ed772f Merge branch 'master' of https://github.com/kamadi/NewPipe into kamadi-master 2016-03-13 22:36:33 +01:00
Christian Schabesberger
6a8c4a65c5 Merge pull request #237 from DevFactory/release/general-code-quality-fix-2
General code quality fix-2
2016-03-13 22:30:24 +01:00
Christian Schabesberger
03738aeb27 merged my changes with them of rrooij 2016-03-13 22:22:18 +01:00
Weblate
243cb8569e Merge remote-tracking branch 'origin/master' 2016-03-13 02:59:46 +01:00
Mladen Pejaković
1bd660fbbe Translated using Weblate (Serbian)
Currently translated at 98.1% (104 of 106 strings)
2016-03-13 02:59:46 +01:00
Christian Schabesberger
fec80b39f0 Merge pull request #238 from rrooij/yt_streampreviewextractor
Move YoutubeStreamPreviewInfoExtractor to separate class
2016-03-13 02:59:43 +01:00
rrooij
21768432c8 Move YoutubeStreamPreviewInfoExtractor to separate class
The class should be separate because channel support in the future would require video preview extraction too, which doesn't differ much from the extraction done from the search results.
2016-03-12 18:15:20 +01:00
Benedikt Freisen
e9b900ff28 Translated using Weblate (German)
Currently translated at 100.0% (106 of 106 strings)
2016-03-12 11:59:04 +01:00
naofum
c57cb8fec1 Translated using Weblate (Japanese)
Currently translated at 100.0% (106 of 106 strings)
2016-03-12 06:28:19 +01:00
Matej U
247681e3ef Translated using Weblate (Slovenian)
Currently translated at 100.0% (106 of 106 strings)
2016-03-11 23:46:28 +01:00
Faisal Hameed
2f9142419a Fixing squid:S1155, squid:S2293, squid:S1488 2016-03-12 02:01:47 +05:00
Christian Schabesberger
b84eb3df7f made stream type part of abstractVideoinfo 2016-03-11 16:20:02 +01:00
Christian Schabesberger
4058ec2ee9 fixed another livestream related issue 2016-03-11 15:43:50 +01:00
Christian Schabesberger
23a9061871 changed autoplay text 2016-03-11 15:10:13 +01:00
Weblate
9e95ca10b2 Merge remote-tracking branch 'origin/master' 2016-03-11 15:07:20 +01:00
Christian Schabesberger
4c0809d3d3 Translated using Weblate (German)
Currently translated at 100.0% (104 of 104 strings)
2016-03-11 15:07:20 +01:00
Christian Schabesberger
d43365f6e6 add custom error report function 2016-03-11 14:56:41 +01:00
Christian Schabesberger
c6ccc2b20d Merge pull request #232 from DevFactory/release/null-pointer-should-not-be-dereferenced-fix-1
Code quality fix - Null pointers should not be dereferenced.
2016-03-11 14:15:41 +01:00
Christian Schabesberger
31dd68be1d Merge pull request #233 from DevFactory/release/useless-parentheses-should-be-removed-fix-1
Code quality fix - Useless parentheses around expressions should be removed to prevent any misunderstanding.
2016-03-11 14:14:29 +01:00
Christian Schabesberger
1c94620c06 Merge pull request #234 from DevFactory/release/variable-should-comply-with-naming-conventions-fix-1
Code quality fix - Local variable and method parameter names should comply with a naming convention.
2016-03-11 14:13:51 +01:00
Weblate
82d214faa6 Merge remote-tracking branch 'origin/master' 2016-03-11 14:12:50 +01:00
Sérgio Marques
04b2e3689c Translated using Weblate (Portuguese)
Currently translated at 100.0% (104 of 104 strings)
2016-03-11 14:12:50 +01:00
Benedikt Volkmer
f00b8a3941 Translated using Weblate (German)
Currently translated at 100.0% (104 of 104 strings)
2016-03-11 14:12:49 +01:00
Henk de Vries
390cc9cfc9 Translated using Weblate (Dutch)
Currently translated at 95.1% (99 of 104 strings)
2016-03-11 14:12:49 +01:00
Christian Schabesberger
8259872e2d Merge pull request #235 from DevFactory/release/utility-classes-should-not-have-public-constructors-fix-1
Code quality fix - Utility classes should not have public constructors.
2016-03-11 14:12:45 +01:00
Faisal Hameed
1a3aaf86ee Fixing squid:S2259- Null pointers should not be dereferenced. 2016-03-10 15:07:23 +05:00
Faisal Hameed
cade4f932d Fixing squid:S00117- Local variable and method parameter names should comply with a naming convention. 2016-03-10 15:01:38 +05:00
Faisal Hameed
eb060602cd Fixing squid:S1118 - Utility classes should not have public constructors. 2016-03-10 14:50:42 +05:00
Faisal Hameed
805f046696 Fixing squid:UselessParenthesesCheck - Useless parentheses around expressions should be removed to prevent any misunderstanding. 2016-03-10 14:41:12 +05:00
Matej U
35e11e43de Translated using Weblate (Slovenian)
Currently translated at 100.0% (104 of 104 strings)
2016-03-08 21:22:44 +01:00
Mohamad Hasan Al Banna
ba15c6dc60 Translated using Weblate (Indonesian)
Currently translated at 100% (0 of 0 strings)

Created new translation.
2016-03-08 16:29:26 +01:00
Christian Schabesberger
569c227a99 try to give gradle more memory for CI build 2016-03-08 02:43:00 +01:00
Christian Schabesberger
8fd7599158 Merge branch 'master' of github.com:theScrabi/NewPipe 2016-03-08 02:22:35 +01:00
Christian Schabesberger
19418e5dfb fixed another parsing but related to livestreams 2016-03-08 02:22:23 +01:00
Christian Schabesberger
f90a163ede add --info to buildscript 2016-03-08 01:25:07 +01:00
Christian Schabesberger
8fb1fd3602 fixed image loading bug 2016-03-08 01:24:45 +01:00
Christian Schabesberger
0dbf2a347f moved on to version 0.7.7 2016-03-08 01:10:33 +01:00
Christian Schabesberger
38742fd5e8 fixed values-iw build failure 2016-03-08 01:09:39 +01:00
Weblate
67ce7e0979 Merge remote-tracking branch 'origin/master' 2016-03-08 01:06:49 +01:00
Benedikt Freisen
21a7e52f9a Translated using Weblate (German)
Currently translated at 99.0% (103 of 104 strings)
2016-03-08 01:06:49 +01:00
Christian Schabesberger
fcb445a381 removed --stacktrace form CI buildscript 2016-03-08 01:02:01 +01:00
Christian Schabesberger
f6450d4b4d removed not or almost not existing translations 2016-03-07 19:15:01 +01:00
Christian Schabesberger
a01d1b89b6 Merge branch 'master' of github.com:theScrabi/NewPipe 2016-03-07 18:51:51 +01:00
Christian Schabesberger
0c9b6582cc made CI script print stack trace 2016-03-07 18:51:43 +01:00
Christian Schabesberger
2d47daabf8 add xxxhdpi app icon 2016-03-07 15:03:26 +01:00
Weblate
ce20cbe435 Merge remote-tracking branch 'origin/master' 2016-03-07 15:00:43 +01:00
Matej U
99e5415bfc Translated using Weblate (Slovenian)
Currently translated at 87.5% (91 of 104 strings)
2016-03-07 15:00:42 +01:00
madiyar
b2d935dd6d Restoring search query after orientation change in two pane mode 2016-03-07 13:05:05 +06:00
madiyar
eb66cc5db8 Restoring search query after orientation change 2016-03-07 12:58:48 +06:00
naofum
6d6b8363a8 Translated using Weblate (Japanese)
Currently translated at 100.0% (104 of 104 strings)
2016-03-06 12:19:11 +01:00
Mladen Pejaković
bf79b02e15 Translated using Weblate (Serbian)
Currently translated at 100.0% (104 of 104 strings)
2016-03-05 18:57:47 +01:00
Christian Schabesberger
3ce7eda3eb made newpipe handle not available like/dislike count 2016-03-05 18:35:28 +01:00
Christian Schabesberger
378e6b6547 fixed fix portrait view 2016-03-05 17:49:11 +01:00
Christian Schabesberger
f63b35e2c0 fixed search error 2016-03-05 17:35:57 +01:00
Christian Schabesberger
9a480cbe3e fixed backarrow on player_activity 2016-03-05 17:16:28 +01:00
Christian Schabesberger
76ca937bb8 set tor into experimental state 2016-03-05 17:04:37 +01:00
Christian Schabesberger
8a29567572 merge addExoplayer 2016-03-05 16:54:24 +01:00
Christian Schabesberger
839cd7d1c7 some small fixes 2016-03-05 16:24:47 +01:00
Christian Schabesberger
0bfc7a9177 Merge branch 'master' of github.com:theScrabi/NewPipe 2016-03-05 16:15:00 +01:00
Christian Schabesberger
26e11f96e0 add streamtype field 2016-03-04 15:18:04 +01:00
Christian Schabesberger
86c36acedc removed android code from extractor side 2016-03-04 14:25:37 +01:00
Christian Schabesberger
2318ad2bde changed viewcount notation 2016-03-04 14:16:02 +01:00
Christian Schabesberger
9548dfabd6 made service list do propper error handling 2016-03-04 14:10:41 +01:00
Christian Schabesberger
0c716c12d7 fixed viewcount test again 2016-03-04 13:51:50 +01:00
Weblate
08dd176753 Merge remote-tracking branch 'origin/master' 2016-03-04 13:49:06 +01:00
Thejesh GN
07086bfb3e Translated using Weblate (Kannada)
Currently translated at 30.5% (26 of 85 strings)
2016-03-04 13:49:06 +01:00
Christian Schabesberger
b1d2e64450 ad service_id field, and pulled video/audio stream out of streaminfo 2016-03-04 13:45:41 +01:00
Christian Schabesberger
d9cd928100 fixed audio settings changed bug 2016-03-02 23:23:52 +01:00
naofum
37ec26c8fd Translated using Weblate (Japanese)
Currently translated at 100.0% (85 of 85 strings)
2016-03-02 13:05:54 +01:00
Christian Schabesberger
68b7d9cdff made duration an integer, and made youtubesearch engine throw more parsingexceptions 2016-03-02 02:36:45 +01:00
Christian Schabesberger
3aecd15916 Merge branch 'advancedErrorHandling' 2016-03-02 00:29:56 +01:00
Christian Schabesberger
97d76aee18 fixed failing unit test 2016-03-01 23:54:26 +01:00
Christian Schabesberger
781bf8e7ec fixed yet another bug 2016-03-01 23:28:22 +01:00
Christian Schabesberger
77b9457707 add error handling to suggestions 2016-03-01 18:43:36 +01:00
Christian Schabesberger
1210ab0de0 changed color of snackbar action text 2016-02-29 21:22:16 +01:00
Christian Schabesberger
3c93c4714e fixed crash during crash report 2016-02-29 21:13:02 +01:00
Christian Schabesberger
f90a1ede70 fixed some more bugs 2016-02-29 21:06:00 +01:00
Christian Schabesberger
028354b283 fixed some bugs 2016-02-29 20:33:28 +01:00
Christian Schabesberger
18493a578d made error message be displayed inside erroractivity 2016-02-29 19:23:01 +01:00
Christian Schabesberger
1829dc79c8 make erroractivity handle search engine exceptions 2016-02-29 19:02:40 +01:00
Weblate
7575e8fbe3 Merge remote-tracking branch 'origin/master' 2016-02-29 16:16:54 +01:00
Christian Schabesberger
eed9915c12 add missing gpl3 header to SuggestionListAdapter 2016-02-29 16:19:34 +01:00
Filip Sebastian
be71e45954 Translated using Weblate (Romanian)
Currently translated at 100.0% (72 of 72 strings)
2016-02-29 16:16:54 +01:00
Kamil Modzelewski
3a7978eca0 Translated using Weblate (Polish)
Currently translated at 54.1% (39 of 72 strings)
2016-02-29 16:16:53 +01:00
naofum
c37d2250d4 Translated using Weblate (Japanese)
Currently translated at 100.0% (72 of 72 strings)
2016-02-29 16:16:53 +01:00
Benedikt Freisen
45819d1cd4 Translated using Weblate (German)
Currently translated at 98.6% (71 of 72 strings)
2016-02-29 16:16:53 +01:00
Christian Schabesberger
4ac36af40c made extractor not throw exception upon missing dash mpd 2016-02-29 16:15:15 +01:00
Christian Schabesberger
1a2840b33f made ui handle missing information on search 2016-02-29 16:10:46 +01:00
Christian Schabesberger
d7e75e6011 restructured search engine 2016-02-29 15:59:06 +01:00
Gian Maria Viglianti
73316b87a3 Translated using Weblate (English)
Currently translated at 100.0% (72 of 72 strings)
2016-02-29 03:44:20 +01:00
Matej U
46402691b0 Translated using Weblate (Slovenian)
Currently translated at 100.0% (72 of 72 strings)
2016-02-27 23:05:13 +01:00
Filip Sebastian
e7cef4549f Translated using Weblate (Romanian)
Currently translated at 100.0% (72 of 72 strings)
2016-02-27 17:51:43 +01:00
Christian Schabesberger
737a41f45b display error message 2016-02-27 17:02:38 +01:00
Christian Schabesberger
045ca40a77 renamed video classes to stream classes 2016-02-27 16:37:15 +01:00
Christian Schabesberger
b1fe197c11 changed crashreport email address 2016-02-27 15:57:37 +01:00
Mladen Pejaković
8888530ae3 Translated using Weblate (Serbian)
Currently translated at 100.0% (72 of 72 strings)
2016-02-27 15:34:44 +01:00
Christian Schabesberger
2d51c7428e add ip and timestamp 2016-02-27 15:02:28 +01:00
Christian Schabesberger
863e2a80a2 set errors mail address 2016-02-27 02:26:06 +01:00
Weblate
41d17d2a47 Merge remote-tracking branch 'origin/master' 2016-02-27 02:12:18 +01:00
Christian Schabesberger
36934468d6 uppdate support framework 2016-02-27 02:14:55 +01:00
Thejesh GN
5029ce8728 Translated using Weblate (Kannada)
Currently translated at 1.3% (1 of 72 strings)
2016-02-27 02:12:17 +01:00
Gian Maria Viglianti
e6ab24bcb4 Translated using Weblate (Italian)
Currently translated at 100.0% (72 of 72 strings)
2016-02-27 02:12:17 +01:00
Christian Schabesberger
f3bd263ada new website for newpipe 2016-02-27 02:11:31 +01:00
Gian Maria Viglianti
db7ab3ffce Translated using Weblate (Italian)
Currently translated at 100.0% (72 of 72 strings)
2016-02-27 01:37:42 +01:00
Thejesh GN
85c3755b96 Translated using Weblate (Kannada)
Currently translated at 100% (0 of 0 strings)

Created new translation.
2016-02-26 10:28:11 +01:00
Christian Schabesberger
5decd55551 fixed another pattern problem 2016-02-26 01:42:38 +01:00
Christian Schabesberger
bc468b6f36 fixed stream exception handling 2016-02-26 01:32:44 +01:00
Christian Schabesberger
369d9204d9 add send via mail/share feature 2016-02-25 23:19:43 +01:00
Christian Schabesberger
7caf7be97e add info box 2016-02-25 22:02:42 +01:00
Christian Schabesberger
64c423902a made error handling work a bit 2016-02-25 03:43:13 +01:00
Christian Schabesberger
27a2dee3bd created error report activity 2016-02-24 23:12:02 +01:00
Christian Schabesberger
11d3aeb0dd fixed faling searchengine unittest 2016-02-24 22:36:16 +01:00
Christian Schabesberger
f54d8d318a remove parcelabel from VideoInfoItem 2016-02-24 22:25:51 +01:00
Christian Schabesberger
210f2ef452 Merge branch 'master' of github.com:theScrabi/NewPipe 2016-02-24 22:12:21 +01:00
Christian Schabesberger
d0bab6183a fixed related view 0 view count error 2016-02-24 22:12:13 +01:00
Christian Schabesberger
3441aceba3 fixed viewcount failure 2016-02-24 16:44:46 +01:00
Weblate
0908b9cd76 Merge remote-tracking branch 'origin/master' 2016-02-24 16:42:14 +01:00
vadin
67494ad4c4 Translated using Weblate (Spanish)
Currently translated at 98.6% (71 of 72 strings)
2016-02-24 16:42:14 +01:00
naofum
5d8f75beb4 Translated using Weblate (Japanese)
Currently translated at 100.0% (72 of 72 strings)
2016-02-24 15:06:59 +01:00
Christian Schabesberger
8e7fde99db fix bitcoin qrcode 2016-02-24 13:34:41 +01:00
Christian Schabesberger
bfdf165584 add bitcoin donations 2016-02-24 13:29:53 +01:00
Weblate
9427ebd489 Merge remote-tracking branch 'origin/master' 2016-02-23 23:33:55 +01:00
Christian Schabesberger
e4f753ae82 fixed emty search query on suggestions error 2016-02-23 23:36:02 +01:00
Benedikt Freisen
f2d9d3c2d7 Translated using Weblate (German)
Currently translated at 98.6% (71 of 72 strings)
2016-02-23 23:33:55 +01:00
Nathan Follens
04c5f31cc1 Translated using Weblate (Dutch)
Currently translated at 100.0% (72 of 72 strings)
2016-02-23 23:33:55 +01:00
Matej U
4ea86b714e Translated using Weblate (Slovenian)
Currently translated at 100.0% (72 of 72 strings)
2016-02-23 18:33:20 +01:00
Nathan Follens
cc0b96cba4 Translated using Weblate (Dutch)
Currently translated at 100.0% (72 of 72 strings)
2016-02-23 18:00:54 +01:00
Christian Schabesberger
9e176f8400 Merge branch 'kamadi-master' 2016-02-23 16:52:52 +01:00
Christian Schabesberger
7195ff349b fixed toast issue of the previous commit 2016-02-23 16:42:04 +01:00
Christian Schabesberger
8048ad343e add toast for suggestion error handling 2016-02-23 16:34:14 +01:00
Christian Schabesberger
799a27ec84 Merge branch 'master' of https://github.com/kamadi/NewPipe into kamadi-master 2016-02-23 16:21:16 +01:00
Christian Schabesberger
c7c77ab20c fixed another pattern problem 2016-02-23 15:25:34 +01:00
madiyar
8fc113cc52 Added country language param to the suggestionList method in search engine 2016-02-23 16:22:49 +06:00
madiyar
c7679bec87 Added suggestion to the searching 2016-02-23 15:01:59 +06:00
madiyar
3301d8b4fb Created suggestion adapter 2016-02-23 14:36:24 +06:00
Christian Schabesberger
f5892093a9 made exoplayer work with youtube again 2016-02-23 01:56:04 +01:00
Christian Schabesberger
b06238ba5d initial changes 2016-02-22 20:28:37 +01:00
Christian Schabesberger
dddcc80f30 merged faridk's code 2016-02-22 19:58:04 +01:00
Christian Schabesberger
8854c8c9d0 Merge branch 'master' of github.com:theScrabi/NewPipe 2016-02-22 19:19:22 +01:00
Christian Schabesberger
d02f441eb0 add age restricted material to description 2016-02-22 19:19:13 +01:00
Christian Schabesberger
ff89dd00b6 Merge branch 'rrooij-age_restriction' 2016-02-22 19:18:22 +01:00
Christian Schabesberger
7041e63268 merged age restricted video request 2016-02-22 19:17:05 +01:00
Weblate
8126fdcd15 Merge remote-tracking branch 'origin/master' 2016-02-22 19:16:57 +01:00
Selim
2d61a2f251 Translated using Weblate (Turkish)
Currently translated at 31.8% (22 of 69 strings)
2016-02-22 19:16:57 +01:00
Matej U
7594ee9dbe Translated using Weblate (Slovenian)
Currently translated at 100.0% (69 of 69 strings)
2016-02-22 19:16:56 +01:00
naofum
0862a38599 Translated using Weblate (Japanese)
Currently translated at 100.0% (69 of 69 strings)
2016-02-22 16:37:05 +01:00
Selim
bf7dc462e8 Translated using Weblate (Turkish)
Currently translated at 100% (0 of 0 strings)

Created new translation.
2016-02-22 13:46:14 +01:00
Christian Schabesberger
65b6aaec8e moved on to 0.7.6 2016-02-21 22:57:07 +01:00
Christian Schabesberger
e08aa14eab better detection for livestreams 2016-02-21 22:25:45 +01:00
Christian Schabesberger
851028997a fixed json error from the last commit 2016-02-21 21:15:39 +01:00
Christian Schabesberger
a1479d04df fixed some searchengine errors 2016-02-21 21:08:14 +01:00
rrooij
d12af16f46 VideoItemDetailFragment: check for next vid
Check if the next video is available before loading the thumbnails.
2016-02-21 19:07:24 +01:00
rrooij
2edfcb78fe YTStreamExtractor: implement getAgeRestriction 2016-02-21 18:45:28 +01:00
rrooij
3f0947fa5b Add unit test for restricted YouTube videos 2016-02-21 18:25:18 +01:00
rrooij
d2b808e540 YTStreamExtractor: fix audio tracks on restricted
Fix audio tracks on restriced videos.
2016-02-21 18:24:53 +01:00
Christian Schabesberger
2995a7dc8f actual fix for androi 6.0 notification problem
Notification buttons weren't respondig to clicks. Now they do.
2016-02-21 18:12:00 +01:00
Christian Schabesberger
819db0a30b antifix for android 6.0 notification z order problem 2016-02-21 17:55:00 +01:00
rrooij
4d727245e1 YTStreamExtractor: add support for age restricted
YouTube age restricted videos are now kind of supported. This is not a
final solution to this problem and this commit still crashes because of
some problem with the thumbnail.
2016-02-21 16:39:08 +01:00
Weblate
d8281aeb34 Merge remote-tracking branch 'origin/master' 2016-02-21 01:40:55 +01:00
LelixSuper
cd0f2061b5 Translated using Weblate (Italian)
Currently translated at 100.0% (68 of 68 strings)
2016-02-21 01:40:55 +01:00
Christian Schabesberger
96928d46ca Merge pull request #199 from szafir1100/develop
used regex to delete spaces inside parseLong() argument
2016-02-21 01:40:50 +01:00
LelixSuper
6d55ac2199 Translated using Weblate (Italian)
Currently translated at 100.0% (68 of 68 strings)
2016-02-20 15:16:59 +01:00
Tomasz Marzeion
80dae6ece7 used regex to delete spaces inside parseLong() argument 2016-02-20 13:49:47 +01:00
Farid
3a8611ebf8 Added ExoPlayer support 2016-02-15 18:49:58 -08:00
100 changed files with 5446 additions and 1136 deletions

View File

@@ -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

View File

@@ -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
[<img src="screenshots/screenshot_1.png" width=150>](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

View File

@@ -8,8 +8,8 @@ android {
applicationId "org.schabi.newpipe"
minSdkVersion 15
targetSdkVersion 23
versionCode 14
versionName "0.7.5"
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'
}

View File

@@ -1,16 +1,21 @@
package org.schabi.newpipe.services.youtube;
package org.schabi.newpipe.extractor.youtube;
import android.test.AndroidTestCase;
import org.schabi.newpipe.extractor.VideoPreviewInfo;
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 the-scrabi on 29.12.15.
* Created by Christian Schabesberger on 29.12.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* YoutubeSearchEngineTest.java is part of NewPipe.
@@ -30,21 +35,23 @@ import java.util.ArrayList;
*/
public class YoutubeSearchEngineTest extends AndroidTestCase {
private SearchEngine.Result result;
private SearchResult result;
private ArrayList<String> suggestionReply;
@Override
public void setUp() throws Exception{
super.setUp();
SearchEngine engine = new YoutubeSearchEngine();
SearchEngine engine = ServiceList.getService("Youtube")
.getSearchEngineInstance(new Downloader());
result = engine.search("https://www.youtube.com/results?search_query=bla",
0, "de", new Downloader());
suggestionReply = engine.suggestionList("hello", new Downloader());
result = engine.search("lefloid",
0, "de", new Downloader()).getSearchResult();
suggestionReply = engine.suggestionList("hello","de",new Downloader());
}
public void testIfNoErrorOccur() {
assertEquals(result.errorMessage, "");
assertTrue(result.errors.isEmpty() ? "" : ExceptionUtils.getStackTrace(result.errors.get(0))
,result.errors.isEmpty());
}
public void testIfListIsNotEmpty() {
@@ -52,37 +59,58 @@ public class YoutubeSearchEngineTest extends AndroidTestCase {
}
public void testItemsHaveTitle() {
for(VideoPreviewInfo i : result.resultList) {
for(StreamPreviewInfo i : result.resultList) {
assertEquals(i.title.isEmpty(), false);
}
}
public void testItemsHaveUploader() {
for(VideoPreviewInfo i : result.resultList) {
for(StreamPreviewInfo i : result.resultList) {
assertEquals(i.uploader.isEmpty(), false);
}
}
public void testItemsHaveRightDuration() {
for(VideoPreviewInfo i : result.resultList) {
assertTrue(i.duration, i.duration.contains(":"));
for(StreamPreviewInfo i : result.resultList) {
assertTrue(i.duration >= 0);
}
}
public void testItemsHaveRightThumbnail() {
for (VideoPreviewInfo i : result.resultList) {
for (StreamPreviewInfo i : result.resultList) {
assertTrue(i.thumbnail_url, i.thumbnail_url.contains("https://"));
}
}
public void testItemsHaveRightVideoUrl() {
for (VideoPreviewInfo i : result.resultList) {
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.size() > 0, true);
assertEquals(!suggestionReply.isEmpty(), true);
}
public void testIfSuggestionsAreValid() {

View File

@@ -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.extractor.ExctractionException;
import org.schabi.newpipe.extractor.AbstractVideoInfo;
import org.schabi.newpipe.extractor.ExtractionException;
import org.schabi.newpipe.extractor.ParsingException;
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.VideoInfo;
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 <chris.schabesberger@mailbox.org>
* 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, ExctractionException {
/* 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 ExctractionException, 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,6 +102,10 @@ 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() != null || !extractor.getDashMpdUrl().isEmpty());

View File

@@ -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.extractor.ExctractionException;
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 <chris.schabesberger@mailbox.org>
* 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, ExctractionException {
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);
}

View File

@@ -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);
}
}
}

View File

@@ -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 <chris.schabesberger@mailbox.org>
* 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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@@ -1,23 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.schabi.newpipe" >
<uses-permission android:name= "android.permission.INTERNET" />
<uses-permission android:name= "android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
package="org.schabi.newpipe">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:logo="@mipmap/ic_launcher"
android:label="@string/app_name"
android:logo="@mipmap/ic_launcher"
android:theme="@style/AppTheme"
tools:ignore="AllowBackup">
<activity
android:name=".VideoItemListActivity"
android:label="@string/app_name"
android:configChanges="orientation|screenSize">
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -27,9 +28,7 @@
<activity
android:name=".VideoItemDetailActivity"
android:label="@string/title_videoitem_detail"
android:theme="@style/AppTheme"
android:configChanges="orientation|screenSize"
android:screenOrientation="portrait">
android:theme="@style/AppTheme">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".VideoItemListActivity" />
@@ -76,20 +75,40 @@
<data android:scheme="vnd.youtube.launch" />
</intent-filter>
</activity>
<activity android:name=".PlayVideoActivity"
<activity
android:name=".player.PlayVideoActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:theme="@style/VideoPlayerTheme"
android:parentActivityName=".VideoItemDetailActivity"
tools:ignore="UnusedAttribute">
tools:ignore="UnusedAttribute"/>
<service
android:name=".player.BackgroundPlayer"
android:exported="false"
android:label="@string/background_player_name"/>
<activity
android:name=".player.ExoPlayerActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleInstance"
android:theme="@style/PlayerTheme">
<intent-filter>
<action android:name="org.schabi.newpipe.exoplayer.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:scheme="content" />
<data android:scheme="asset" />
<data android:scheme="file" />
</intent-filter>
</activity>
<service
android:name=".BackgroundPlayer"
android:name=".player.BackgroundPlayer"
android:label="@string/background_player_name"
android:exported="false" />
<activity
android:name=".SettingsActivity"
android:label="@string/settings_activity_title" >
</activity>
android:label="@string/settings_activity_title" />
<activity
android:name=".PanicResponderActivity"
android:launchMode="singleInstance"
@@ -97,11 +116,14 @@
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="info.guardianproject.panic.action.TRIGGER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ExitActivity"
android:label="@string/general_error"
android:theme="@android:style/Theme.NoDisplay" />
<activity android:name=".ErrorActivity"/>
</application>
</manifest>

View File

@@ -12,7 +12,8 @@ import android.view.MenuItem;
import android.widget.ArrayAdapter;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.VideoInfo;
import org.schabi.newpipe.extractor.StreamInfo;
import org.schabi.newpipe.extractor.VideoStream;
import java.util.List;
@@ -75,7 +76,7 @@ class ActionBarHandler {
}
}
public void setupStreamList(final List<VideoInfo.VideoStream> videoStreams) {
public void setupStreamList(final List<VideoStream> videoStreams) {
if (activity != null) {
selectedVideoStream = 0;
@@ -83,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);
@@ -108,13 +109,13 @@ class ActionBarHandler {
}
private int getDefaultResolution(final List<VideoInfo.VideoStream> videoStreams) {
private int getDefaultResolution(final List<VideoStream> 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;
}

View File

@@ -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<Exception> errorList;
public volatile Class returnActivity;
public volatile ErrorActivity.ErrorInfo errorInfo;
}

View File

@@ -130,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<String> forbiddenCharsPatterns = new ArrayList<String> ();
List<String> 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

View File

@@ -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.
* <p>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ErrorActivity.java is part of NewPipe.
* <p>
* 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.
* <p>
* 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.
* <p>
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class 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<Exception> 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<Exception> 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<Exception> 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<Exception> 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<Exception> 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<Exception> 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);
}
}
}
}

View File

@@ -34,6 +34,9 @@ import java.util.Locale;
public class Localization {
private Localization() {
}
public static Locale getPreferredLocale(Context context) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);

View File

@@ -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);

View File

@@ -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);
}
}
};

View File

@@ -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 <chris.schabesberger@mailbox.org>
* 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 <http://www.gnu.org/licenses/>.
*/
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<String> 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;
}
}

View File

@@ -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.extractor.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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@@ -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();

View File

@@ -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.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.VideoPreviewInfo;
import org.schabi.newpipe.extractor.StreamInfo;
import org.schabi.newpipe.extractor.StreamPreviewInfo;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.VideoInfo;
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;
/**
@@ -127,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();
@@ -147,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) {
@@ -157,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);
}
}
}
}
@@ -186,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();
}
@@ -198,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 =
@@ -227,7 +307,7 @@ public class VideoItemDetailFragment extends Fragment {
View nextVideoView = null;
if(info.next_video != null) {
nextVideoView = videoItemViewCreator
.getViewFromVideoInfoItem(null, nextVideoFrame, info.next_video, getContext());
.getViewFromVideoInfoItem(null, nextVideoFrame, info.next_video);
} else {
activity.findViewById(R.id.detailNextVidButtonAndContentLayout).setVisibility(View.GONE);
activity.findViewById(R.id.detailNextVideoTitle).setVisibility(View.GONE);
@@ -315,8 +395,8 @@ public class VideoItemDetailFragment extends Fragment {
descriptionView.setMovementMethod(LinkMovementMethod.getInstance());
// parse streams
Vector<VideoInfo.VideoStream> streamsToUse = new Vector<>();
for (VideoInfo.VideoStream i : info.video_streams) {
Vector<VideoStream> streamsToUse = new Vector<>();
for (VideoStream i : info.video_streams) {
if (useStream(i, streamsToUse)) {
streamsToUse.add(i);
}
@@ -372,7 +452,7 @@ public class VideoItemDetailFragment extends Fragment {
}
}
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);
@@ -409,13 +489,13 @@ public class VideoItemDetailFragment extends Fragment {
imageLoader.displayImage(info.uploader_thumbnail_url,
uploaderThumb, displayImageOptions, new ThumbnailLoadingListener());
}
if(info.thumbnail_url != null && !info.thumbnail_url.isEmpty()) {
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() {
@@ -482,7 +562,7 @@ public class VideoItemDetailFragment extends Fragment {
// website which was crawled. Then the ui has to understand this and act right.
if (info.audio_streams != null) {
VideoInfo.AudioStream audioStream =
AudioStream audioStream =
info.audio_streams.get(getPreferredAudioStreamId(info));
String audioSuffix = "." + MediaFormat.getSuffixById(audioStream.format);
@@ -491,7 +571,7 @@ public class VideoItemDetailFragment extends Fragment {
}
if (info.video_streams != null) {
VideoInfo.VideoStream selectedStreamItem = info.video_streams.get(selectedStreamId);
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);
@@ -518,7 +598,7 @@ public class VideoItemDetailFragment extends Fragment {
boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
Intent intent;
VideoInfo.AudioStream audioStream =
AudioStream audioStream =
info.audio_streams.get(getPreferredAudioStreamId(info));
if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 18) {
//internal music player: explicit intent
@@ -577,7 +657,7 @@ public class VideoItemDetailFragment extends Fragment {
}
}
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");
@@ -604,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<VideoPreviewInfo> similar = new ArrayList<>(info.related_videos);
for (final VideoPreviewInfo item : similar) {
ArrayList<StreamPreviewInfo> 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);
@@ -672,8 +752,17 @@ public class VideoItemDetailFragment extends Fragment {
.show();
}
private boolean useStream(VideoInfo.VideoStream stream, Vector<VideoInfo.VideoStream> 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<VideoStream> streams) {
for(VideoStream i : streams) {
if(i.resolution.equals(stream.resolution)) {
return false;
}
@@ -763,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)
@@ -774,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) {
@@ -789,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);
}
})
@@ -804,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 !!!
}
}
// --------------------------------------------

View File

@@ -2,7 +2,9 @@ package org.schabi.newpipe;
import android.content.Context;
import android.content.Intent;
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;
@@ -13,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 java.util.ArrayList;
import org.schabi.newpipe.extractor.VideoPreviewInfo;
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 java.util.Vector;
/**
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
@@ -61,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
@@ -78,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
@@ -94,11 +107,87 @@ public class VideoItemListActivity extends AppCompatActivity
@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 ArrayList<String>suggestions;
private SuggestionResultRunnable(ArrayList<String> 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));
ArrayList<String>suggestions = 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.
@@ -109,34 +198,23 @@ public class VideoItemListActivity extends AppCompatActivity
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_videoitem_list);
StreamingService streamingService = null;
//------ 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<VideoPreviewInfo> 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) {
@@ -167,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);
}
@@ -192,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);
@@ -218,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);
}
@@ -237,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 {
@@ -263,6 +353,14 @@ public class VideoItemListActivity extends AppCompatActivity
startActivity(intent);
return true;
}
case R.id.action_report_error: {
ErrorActivity.reportError(VideoItemListActivity.this, new Vector<Exception>(),
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);
@@ -281,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();
}
});
}
}

View File

@@ -1,5 +1,6 @@
package org.schabi.newpipe;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
@@ -16,8 +17,9 @@ import android.widget.Toast;
import java.io.IOException;
import java.util.List;
import org.schabi.newpipe.extractor.ExctractionException;
import org.schabi.newpipe.extractor.VideoPreviewInfo;
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;
@@ -67,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;
}
@@ -100,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(ExctractionException 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<String> thumbnailUrlList = new Vector<>();
private final Vector<Boolean> downloadedList;
final Handler h = new Handler();
private volatile boolean run = true;
private final int requestId;
public LoadThumbsRunnable(Vector<VideoPreviewInfo> videoList,
Vector<Boolean> 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<VideoPreviewInfo> videoList) {
public void present(List<StreamPreviewInfo> videoList) {
mode = PRESENT_VIDEOS_MODE;
setListShown(true);
getListView().smoothScrollToPosition(0);
@@ -215,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();
@@ -225,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<VideoPreviewInfo> list) {
private void updateList(List<StreamPreviewInfo> 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;
@@ -373,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();
}
});
}
}

View File

@@ -8,7 +8,7 @@ import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import org.schabi.newpipe.extractor.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<VideoPreviewInfo> videoList = new Vector<>();
private Vector<StreamPreviewInfo> 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<VideoPreviewInfo> videos) {
public void addVideoList(List<StreamPreviewInfo> videos) {
videoList.addAll(videos);
notifyDataSetChanged();
}
@@ -57,7 +57,7 @@ class VideoListAdapter extends BaseAdapter {
notifyDataSetChanged();
}
public Vector<VideoPreviewInfo> getVideoList() {
public Vector<StreamPreviewInfo> 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));

View File

@@ -20,8 +20,19 @@ import android.graphics.Bitmap;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/**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 = "";

View File

@@ -0,0 +1,46 @@
package org.schabi.newpipe.extractor;
/**
* Created by Christian Schabesberger on 04.03.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* 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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@@ -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<VideoInfo.AudioStream> getAudioStreams(String dashManifestUrl,
public static List<AudioStream> 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<VideoInfo.AudioStream> audioStreams = new Vector<>();
Vector<AudioStream> 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;

View File

@@ -4,7 +4,7 @@ package org.schabi.newpipe.extractor;
* Created by Christian Schabesberger on 30.01.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ExctractionException.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.extractor;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ExctractionException extends Exception {
public ExctractionException() {}
public ExctractionException(String message) {
public class ExtractionException extends Exception {
public ExtractionException(String message) {
super(message);
}
public ExctractionException(Throwable cause) {
public ExtractionException(Throwable cause) {
super(cause);
}
public ExctractionException(String message, Throwable cause) {
public ExtractionException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -1,5 +1,7 @@
package org.schabi.newpipe.extractor;
import android.util.Log;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.HashMap;
@@ -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<String, String> compatParseMap(final String input) throws UnsupportedEncodingException {
Map<String, String> 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;
}

View File

@@ -21,14 +21,10 @@ package org.schabi.newpipe.extractor;
*/
public class ParsingException extends ExctractionException {
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);
}

View File

@@ -26,17 +26,29 @@ import java.util.Vector;
*/
@SuppressWarnings("ALL")
public interface SearchEngine {
class Result {
public String errorMessage = "";
public String suggestion = "";
public final List<VideoPreviewInfo> resultList = new Vector<>();
public abstract class SearchEngine {
public static class NothingFoundException extends ExtractionException {
public NothingFoundException(String message) {
super(message);
}
}
ArrayList<String> suggestionList(String query, Downloader dl)
throws ExctractionException, IOException;
private StreamPreviewInfoCollector collector;
public SearchEngine(StreamUrlIdHandler urlIdHandler, int serviceId) {
collector = new StreamPreviewInfoCollector(urlIdHandler, serviceId);
}
public StreamPreviewInfoCollector getStreamPreviewInfoCollector() {
return collector;
}
public abstract ArrayList<String> 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 ExctractionException, IOException;
public abstract StreamPreviewInfoCollector search(
String query, int page, String contentCountry, Downloader dl)
throws ExtractionException, IOException;
}

View File

@@ -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 <chris.schabesberger@mailbox.org>
* 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 <http://www.gnu.org/licenses/>.
*/
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<StreamPreviewInfo> resultList = new Vector<>();
public List<Exception> errors = new Vector<>();
}

View File

@@ -29,26 +29,43 @@ import org.schabi.newpipe.extractor.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.");
}
}

View File

@@ -3,7 +3,7 @@ package org.schabi.newpipe.extractor;
/**
* Created by Christian Schabesberger on 10.08.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* 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 ExctractionException {
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<VideoInfo.AudioStream> getAudioStreams() throws ParsingException;
public abstract List<VideoInfo.VideoStream> getVideoStreams() throws ParsingException;
public abstract List<VideoInfo.VideoStream> getVideoOnlyStreams() throws ParsingException;
public abstract List<AudioStream> getAudioStreams() throws ParsingException;
public abstract List<VideoStream> getVideoStreams() throws ParsingException;
public abstract List<VideoStream> 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<VideoPreviewInfo> getRelatedVideos() throws ParsingException;
public abstract VideoUrlIdHandler getUrlIdConverter();
public abstract StreamPreviewInfo getNextVideo() throws ParsingException;
public abstract List<StreamPreviewInfo> getRelatedVideos() throws ParsingException;
public abstract StreamUrlIdHandler getUrlIdConverter();
public abstract String getPageUrl();
public abstract StreamInfo.StreamType getStreamType() throws ParsingException;
public int getServiceId() {
return serviceId;
}
}

View File

@@ -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 <chris.schabesberger@mailbox.org>
* 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 <http://www.gnu.org/licenses/>.
*/
/**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<VideoStream> video_streams = null;
public List<AudioStream> audio_streams = null;
public List<VideoStream> 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<StreamPreviewInfo> related_videos = null;
//in seconds. some metadata is not passed using a StreamInfo object!
public int start_position = 0;
public List<Exception> errors = new Vector<>();
}

View File

@@ -0,0 +1,26 @@
package org.schabi.newpipe.extractor;
/**
* Created by Christian Schabesberger on 26.08.15.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* 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 <http://www.gnu.org/licenses/>.
*/
/**Info object for previews of unopened videos, eg search results, related videos*/
public class StreamPreviewInfo extends AbstractVideoInfo {
public int duration = 0;
}

View File

@@ -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 <chris.schabesberger@mailbox.org>
* 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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@@ -0,0 +1,32 @@
package org.schabi.newpipe.extractor;
/**
* Created by Christian Schabesberger on 28.02.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* 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 <http://www.gnu.org/licenses/>.
*/
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;
}

View File

@@ -4,7 +4,7 @@ package org.schabi.newpipe.extractor;
* Created by Christian Schabesberger on 02.02.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* 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.extractor;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public interface VideoUrlIdHandler {
public interface StreamUrlIdHandler {
String getVideoUrl(String videoId);
String getVideoId(String siteUrl) throws ParsingException;
String cleanUrl(String siteUrl) throws ParsingException;

View File

@@ -5,7 +5,7 @@ import java.io.IOException;
/**
* Created by Christian Schabesberger on 23.08.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* 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 <http://www.gnu.org/licenses/>.
*/
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, ExctractionException;
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;
}
}

View File

@@ -1,302 +0,0 @@
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 2015 <chris.schabesberger@mailbox.org>
* 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 <http://www.gnu.org/licenses/>.
*/
/**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 ExctractionException, IOException {
VideoInfo videoInfo = new VideoInfo();
videoInfo = extractImportantData(videoInfo, extractor, downloader);
videoInfo = extractStreams(videoInfo, extractor, downloader);
videoInfo = extractOptionalData(videoInfo, extractor, downloader);
return videoInfo;
}
private static VideoInfo extractImportantData(
VideoInfo videoInfo, StreamExtractor extractor, Downloader downloader)
throws ExctractionException, 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.
VideoUrlIdHandler uiconv = extractor.getUrlIdConverter();
videoInfo.webpage_url = extractor.getPageUrl();
videoInfo.id = uiconv.getVideoId(extractor.getPageUrl());
videoInfo.title = extractor.getTitle();
if((videoInfo.webpage_url == null || videoInfo.webpage_url.isEmpty())
|| (videoInfo.id == null || videoInfo.id.isEmpty())
|| (videoInfo.title == null /* videoInfo.title can be empty of course */));
return videoInfo;
}
private static VideoInfo extractStreams(
VideoInfo videoInfo, StreamExtractor extractor, Downloader downloader)
throws ExctractionException, 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 {
videoInfo.dashMpdUrl = extractor.getDashMpdUrl();
} catch(Exception e) {
videoInfo.addException(new ExctractionException("Couldn't get Dash manifest", e));
}
/* Load and extract audio */
try {
videoInfo.audio_streams = extractor.getAudioStreams();
} catch(Exception e) {
videoInfo.addException(new ExctractionException("Couldn't get audio streams", e));
}
// also try to get streams from the dashMpd
if(videoInfo.dashMpdUrl != null && !videoInfo.dashMpdUrl.isEmpty()) {
if(videoInfo.audio_streams == null) {
videoInfo.audio_streams = new Vector<AudioStream>();
}
//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) {
videoInfo.addException(
new ExctractionException("Couldn't get audio streams from dash mpd", e));
}
}
/* Extract video stream url*/
try {
videoInfo.video_streams = extractor.getVideoStreams();
} catch (Exception e) {
videoInfo.addException(
new ExctractionException("Couldn't get video streams", e));
}
/* Extract video only stream url*/
try {
videoInfo.video_only_streams = extractor.getVideoOnlyStreams();
} catch(Exception e) {
videoInfo.addException(
new ExctractionException("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((videoInfo.video_streams == null || videoInfo.video_streams.isEmpty())
&& (videoInfo.audio_streams == null || videoInfo.audio_streams.isEmpty())
&& (videoInfo.dashMpdUrl == null || videoInfo.dashMpdUrl.isEmpty())) {
throw new ExctractionException("Could not get any stream. See error variable to get further details.");
}
return videoInfo;
}
private static VideoInfo extractOptionalData(
VideoInfo videoInfo, 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 {
videoInfo.thumbnail_url = extractor.getThumbnailUrl();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.duration = extractor.getLength();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.uploader = extractor.getUploader();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.description = extractor.getDescription();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.view_count = extractor.getViews();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.upload_date = extractor.getUploadDate();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.uploader_thumbnail_url = extractor.getUploaderThumbnailUrl();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.start_position = extractor.getTimeStamp();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.average_rating = extractor.getAverageRating();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.like_count = extractor.getLikeCount();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.dislike_count = extractor.getDislikeCount();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.next_video = extractor.getNextVideo();
} catch(Exception e) {
videoInfo.addException(e);
}
try {
videoInfo.related_videos = extractor.getRelatedVideos();
} catch(Exception e) {
videoInfo.addException(e);
}
return videoInfo;
}
public String uploader_thumbnail_url = "";
public String description = "";
public List<VideoStream> video_streams = null;
public List<AudioStream> audio_streams = null;
public List<VideoStream> 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 = 0;
public int like_count = -1;
public int dislike_count = -1;
public String average_rating = "";
public VideoPreviewInfo next_video = null;
public List<VideoPreviewInfo> 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 List<Exception> errors = new Vector<>();
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;
}
}
public void addException(Exception e) {
errors.add(e);
}
}

View File

@@ -1,77 +0,0 @@
package org.schabi.newpipe.extractor;
import android.graphics.Bitmap;
import android.os.Parcel;
import android.os.Parcelable;
/**
* Created by Christian Schabesberger on 26.08.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* 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 <http://www.gnu.org/licenses/>.
*/
/**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<VideoPreviewInfo> CREATOR = new Parcelable.Creator<VideoPreviewInfo>() {
@Override
public VideoPreviewInfo createFromParcel(Parcel in) {
return new VideoPreviewInfo(in);
}
@Override
public VideoPreviewInfo[] newArray(int size) {
return new VideoPreviewInfo[size];
}
};
}

View File

@@ -0,0 +1,44 @@
package org.schabi.newpipe.extractor;
/**
* Created by Christian Schabesberger on 04.03.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* 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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@@ -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 <chris.schabesberger@mailbox.org>
* 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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@@ -1,25 +1,26 @@
package org.schabi.newpipe.extractor.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.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.VideoPreviewInfo;
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 java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
@@ -45,14 +46,21 @@ import javax.xml.parsers.ParserConfigurationException;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class YoutubeSearchEngine implements SearchEngine {
public class YoutubeSearchEngine extends SearchEngine {
private static final String TAG = YoutubeSearchEngine.class.toString();
public YoutubeSearchEngine(StreamUrlIdHandler urlIdHandler, int serviceId) {
super(urlIdHandler, serviceId);
}
@Override
public Result search(String query, int page, String languageCode, Downloader downloader)
throws IOException, ParsingException {
Result result = new Result();
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")
@@ -60,9 +68,15 @@ public class YoutubeSearchEngine implements SearchEngine {
.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();
//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);
@@ -72,12 +86,11 @@ public class YoutubeSearchEngine implements SearchEngine {
site = downloader.download(url);
}
try {
Document doc = Jsoup.parse(site, url);
Element list = doc.select("ol[class=\"item-section\"]").first();
Document doc = Jsoup.parse(site, url);
Element list = doc.select("ol[class=\"item-section\"]").first();
for (Element item : list.children()) {
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
@@ -89,73 +102,39 @@ public class YoutubeSearchEngine implements SearchEngine {
playlists now.
*/
Element el;
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 + "\"");
// 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 + "\""));
}
} catch(Exception e) {
throw new ParsingException(e);
}
return result;
return collector;
}
@Override
public ArrayList<String> suggestionList(String query, Downloader dl)
public ArrayList<String> suggestionList(String query, String contentCountry, Downloader dl)
throws IOException, ParsingException {
ArrayList<String> 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")
@@ -164,46 +143,51 @@ public class YoutubeSearchEngine implements SearchEngine {
.appendQueryParameter("client", "")
.appendQueryParameter("output", "toolbar")
.appendQueryParameter("ds", "yt")
.appendQueryParameter("hl",contentCountry)
.appendQueryParameter("q", query);
String url = builder.build().toString();
*/
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.");
}
//TODO: Parse xml data using Jsoup not done
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder;
org.w3c.dom.Document doc = null;
try {
NodeList nList = doc.getElementsByTagName("CompleteSuggestion");
for (int temp = 0; temp < nList.getLength(); temp++) {
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"));
}
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);
throw new ParsingException("Could not get suggestions form document.", e);
}
}
private StreamPreviewInfoExtractor extractPreviewInfo(final Element item) {
return new YoutubeStreamPreviewInfoExtractor(item);
}
}

View File

@@ -1,10 +1,10 @@
package org.schabi.newpipe.extractor.services.youtube;
import org.schabi.newpipe.extractor.ExctractionException;
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.VideoUrlIdHandler;
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 <http://www.gnu.org/licenses/>.
*/
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 ExctractionException, 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();
}
}

View File

@@ -1,7 +1,5 @@
package org.schabi.newpipe.extractor.services.youtube;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import org.jsoup.Jsoup;
@@ -10,20 +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.extractor.ExctractionException;
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.VideoUrlIdHandler;
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.VideoInfo;
import org.schabi.newpipe.extractor.VideoPreviewInfo;
import org.schabi.newpipe.extractor.VideoStream;
import java.io.IOException;
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.
@@ -45,7 +47,31 @@ import java.util.Vector;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
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
@@ -138,29 +164,11 @@ public class YoutubeStreamExtractor implements StreamExtractor {
throw new ParsingException("itag=" + Integer.toString(itag) + " not supported");
}
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 Map<String, String> videoInfoPage;
private boolean isAgeRestricted;
private Map<String, String> videoInfoPage;
// static values
private static final String DECRYPTION_FUNC_NAME="decrypt";
@@ -168,84 +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 ExctractionException, 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("<meta property=\"og:restrictions:age")) {
String videoInfoUrl = GET_VIDEO_INFO_URL.replace("%%video_id%%",
urlidhandler.getVideoId(pageUrl)).replace("$$el_type$$", "&" + EL_INFO);
String videoInfoPageString = downloader.download(videoInfoUrl);
videoInfoPage = Parser.compatParseMap(videoInfoPageString);
playerUrl = getPlayerUrlFromRestrictedVideo(pageUrl);
isAgeRestricted = true;
} else {
ytPlayerConfig = getPlayerConfig(pageContent);
playerArgs = getPlayerArgs(ytPlayerConfig);
playerUrl = getPlayerUrl(ytPlayerConfig);
isAgeRestricted = false;
}
if(decryptionCode.isEmpty()) {
decryptionCode = loadDecryptionCode(playerUrl);
}
}
private JSONObject getPlayerConfig(String pageContent) throws ParsingException {
try {
ytPlayerConfigRaw =
String ytPlayerConfigRaw =
Parser.matchGroup1("ytplayer.config\\s*=\\s*(\\{.*?\\});", pageContent);
ytPlayerConfig = new JSONObject(ytPlayerConfigRaw);
playerArgs = ytPlayerConfig.getJSONObject("args");
return new JSONObject(ytPlayerConfigRaw);
} catch (Parser.RegexException e) {
String errorReason = findErrorReason(doc);
switch(errorReason) {
case "GEMA":
throw new GemaException(errorReason);
case "":
throw new ParsingException("player config empty", e);
throw new ContentNotAvailableException("Content not available: player config empty", e);
default:
throw new ContentNotAvailableException("Content not available", e);
}
} catch (JSONException e) {
throw new ParsingException("Could not parse yt player config");
throw new ParsingException("Could not parse yt player config", e);
}
}
private JSONObject getPlayerArgs(JSONObject playerConfig) throws ParsingException {
JSONObject playerArgs;
/* not yet nececeary
// get videoInfo page
//attempt to load the youtube js player JSON arguments
boolean isLiveStream = false; //used to determine if this is a livestream or not
try {
//Parser.unescapeEntities(url_data_str, true).split("&")
String getVideoInfoUrl = GET_VIDEO_INFO_URL.replace("%%video_id%%",
urlidhandler.getVideoId(pageUrl)).replace("$$el_type$$", "&" + EL_INFO);
videoInfoPage = Parser.compatParseMap(downloader.download(getVideoInfoUrl));
} catch(Exception e) {
throw new ParsingException("Could not load video info page.", e);
}
*/
playerArgs = playerConfig.getJSONObject("args");
//----------------------------------
// load and parse description code, if it isn't already initialised
//----------------------------------
if (decryptionCode.isEmpty()) {
try {
// The Youtube service needs to be initialized by downloading the
// js-Youtube-player. This is done in order to get the algorithm
// for decrypting cryptic signatures inside certain stream urls.
JSONObject ytAssets = ytPlayerConfig.getJSONObject("assets");
String playerUrl = ytAssets.getString("js");
if (playerUrl.startsWith("//")) {
playerUrl = "https:" + playerUrl;
}
decryptionCode = loadDecryptionCode(playerUrl);
} catch (JSONException e) {
throw new ParsingException(
"Could not load decryption code for the Youtube service.", e);
// check if we have a live stream. We need to filter it, since its not yet supported.
if((playerArgs.has("ps") && playerArgs.get("ps").toString().equals("live"))
|| (playerArgs.get("url_encoded_fmt_stream_map").toString().isEmpty())) {
isLiveStream = true;
}
} catch (JSONException e) {
throw new ParsingException("Could not parse yt player config", e);
}
if (isLiveStream) {
throw new LiveStreamException("This is a Life stream. Can't use those right now.");
}
return playerArgs;
}
private String getPlayerUrl(JSONObject playerConfig) throws ParsingException {
try {
// The Youtube service needs to be initialized by downloading the
// js-Youtube-player. This is done in order to get the algorithm
// for decrypting cryptic signatures inside certain stream urls.
String playerUrl = "";
JSONObject ytAssets = playerConfig.getJSONObject("assets");
playerUrl = ytAssets.getString("js");
if (playerUrl.startsWith("//")) {
playerUrl = "https:" + playerUrl;
}
return playerUrl;
} catch (JSONException e) {
throw new ParsingException(
"Could not load decryption code for the Youtube service.", e);
}
}
private String getPlayerUrlFromRestrictedVideo(String pageUrl) throws ParsingException {
try {
String playerUrl = "";
String videoId = urlidhandler.getVideoId(pageUrl);
String embedUrl = "https://www.youtube.com/embed/" + videoId;
String embedPageContent = downloader.download(embedUrl);
//todo: find out if this can be reapaced by Parser.matchGroup1()
Pattern assetsPattern = Pattern.compile("\"assets\":.+?\"js\":\\s*(\"[^\"]+\")");
Matcher patternMatcher = assetsPattern.matcher(embedPageContent);
while (patternMatcher.find()) {
playerUrl = patternMatcher.group(1);
}
playerUrl = playerUrl.replace("\\", "").replace("\"", "");
if (playerUrl.startsWith("//")) {
playerUrl = "https:" + playerUrl;
}
return playerUrl;
} catch (IOException e) {
throw new ParsingException(
"Could load decryption code form restricted video for the Youtube service.", e);
}
}
@Override
public String getTitle() throws ParsingException {
try {//json player args method
try {
if (playerArgs == null) {
return videoInfoPage.get("title");
}
//json player args method
return playerArgs.getString("title");
} catch(JSONException je) {//html <meta> 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) {
@@ -265,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();
@@ -281,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);
@@ -288,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);
}
}
@@ -314,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");
}
}
@@ -358,11 +432,17 @@ public class YoutubeStreamExtractor implements StreamExtractor {
@Override
public List<VideoInfo.AudioStream> getAudioStreams() throws ParsingException {
Vector<VideoInfo.AudioStream> audioStreams = new Vector<>();
public List<AudioStream> getAudioStreams() throws ParsingException {
Vector<AudioStream> 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<String, String> tags = Parser.compatParseMap(
@@ -380,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));
@@ -394,12 +474,18 @@ public class YoutubeStreamExtractor implements StreamExtractor {
}
@Override
public List<VideoInfo.VideoStream> getVideoStreams() throws ParsingException {
Vector<VideoInfo.VideoStream> videoStreams = new Vector<>();
public List<VideoStream> getVideoStreams() throws ParsingException {
Vector<VideoStream> 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.
@@ -417,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();
}
}
@@ -440,7 +527,7 @@ public class YoutubeStreamExtractor implements StreamExtractor {
}
@Override
public List<VideoInfo.VideoStream> getVideoOnlyStreams() throws ParsingException {
public List<VideoStream> getVideoOnlyStreams() throws ParsingException {
return null;
}
@@ -477,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);
@@ -495,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);
@@ -514,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(
@@ -529,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(
@@ -541,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());
@@ -551,9 +658,9 @@ public class YoutubeStreamExtractor implements StreamExtractor {
}
@Override
public Vector<VideoPreviewInfo> getRelatedVideos() throws ParsingException {
public Vector<StreamPreviewInfo> getRelatedVideos() throws ParsingException {
try {
Vector<VideoPreviewInfo> relatedVideos = new Vector<>();
Vector<StreamPreviewInfo> 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) {
@@ -567,8 +674,8 @@ public class YoutubeStreamExtractor implements StreamExtractor {
}
@Override
public VideoUrlIdHandler getUrlIdConverter() {
return new YoutubeVideoUrlIdHandler();
public StreamUrlIdHandler getUrlIdConverter() {
return new YoutubeStreamUrlIdHandler();
}
@Override
@@ -576,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()
@@ -602,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");
@@ -621,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;
}
@@ -675,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) {

View File

@@ -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 <chris.schabesberger@mailbox.org>
* 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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@@ -2,7 +2,7 @@ package org.schabi.newpipe.extractor.services.youtube;
import org.schabi.newpipe.extractor.Parser;
import org.schabi.newpipe.extractor.ParsingException;
import org.schabi.newpipe.extractor.VideoUrlIdHandler;
import org.schabi.newpipe.extractor.StreamUrlIdHandler;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
@@ -11,7 +11,7 @@ import java.net.URLDecoder;
* Created by Christian Schabesberger on 02.02.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* 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
@@ -27,7 +27,7 @@ import java.net.URLDecoder;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class YoutubeVideoUrlIdHandler implements VideoUrlIdHandler {
public class YoutubeStreamUrlIdHandler implements StreamUrlIdHandler {
@SuppressWarnings("WeakerAccess")
@Override
public String getVideoUrl(String videoId) {
@@ -37,7 +37,7 @@ public class YoutubeVideoUrlIdHandler implements VideoUrlIdHandler {
@SuppressWarnings("WeakerAccess")
@Override
public String getVideoId(String url) throws ParsingException {
String id = "";
String id;
if(url.contains("youtube")) {
if(url.contains("attribution_link")) {
@@ -49,11 +49,15 @@ public class YoutubeVideoUrlIdHandler implements VideoUrlIdHandler {
throw new ParsingException("Could not parse attribution_link", uee);
}
} else {
id = Parser.matchGroup1("youtube\\.com/watch\\?v=([\\-a-zA-Z0-9_]{11})", url);
id = Parser.matchGroup1("[?&]v=([\\-a-zA-Z0-9_]{11})", url);
}
}
else if(url.contains("youtu.be")) {
id = Parser.matchGroup1("youtu\\.be/([a-zA-Z0-9_-]{11})", url);
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);
@@ -61,7 +65,6 @@ public class YoutubeVideoUrlIdHandler implements VideoUrlIdHandler {
if(!id.isEmpty()){
//Log.i(TAG, "string \""+url+"\" matches!");
return id;
} else {
throw new ParsingException("Error could not parse url: " + url);

View File

@@ -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
@@ -294,7 +300,8 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
R.string.background_player_time_text), title))
.setContentIntent(PendingIntent.getActivity(getApplicationContext(),
noteID, openDetailViewIntent,
PendingIntent.FLAG_UPDATE_CURRENT));
PendingIntent.FLAG_UPDATE_CURRENT))
.setContentIntent(openDetailView);
RemoteViews view =
@@ -304,18 +311,19 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
view.setTextViewText(R.id.notificationArtist, channelName);
view.setOnClickPendingIntent(R.id.notificationStop, stopPI);
view.setOnClickPendingIntent(R.id.notificationPlayPause, playPI);
view.setOnClickPendingIntent(R.id.notificationBackgroundButton, openDetailView);
view.setOnClickPendingIntent(R.id.notificationContent, openDetailView);
//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.setImageViewBitmap(R.id.notificationCover, videoThumbnail);
expandedView.setTextViewText(R.id.notificationSongName, title);
expandedView.setTextViewText(R.id.notificationArtist, channelName);
expandedView.setTextViewText(R.id.notificationArtist, channelName);
expandedView.setOnClickPendingIntent(R.id.notificationStop, stopPI);
expandedView.setOnClickPendingIntent(R.id.notificationPlayPause, playPI);
expandedView.setOnClickPendingIntent(R.id.notificationBackgroundButton, openDetailView);
expandedView.setOnClickPendingIntent(R.id.notificationContent, openDetailView);
noteBuilder.setCategory(Notification.CATEGORY_TRANSPORT);

View File

@@ -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 <chris.schabesberger@mailbox.org>
* 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 <http://www.gnu.org/licenses/>.
*/
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<Cue> cues) {
subtitleLayout.setCues(cues);
}
};
NPExoPlayer.Id3MetadataListener id3MetadataListener = new NPExoPlayer.Id3MetadataListener() {
@Override
public void onId3Metadata(Map<String, Object> metadata) {
for (Map.Entry<String, Object> 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);
}
}
}

View File

@@ -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 <chris.schabesberger@mailbox.org>
* 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;
}

View File

@@ -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<MediaPresentationDescription>, UtcTimingCallback {
private final Context context;
private final String userAgent;
private final MediaDrmCallback drmCallback;
private final NPExoPlayer player;
private final ManifestFetcher<MediaPresentationDescription> 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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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.
}
}

View File

@@ -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<HlsPlaylist> {
private final Context context;
private final String userAgent;
private final String url;
private final NPExoPlayer player;
private final ManifestFetcher<HlsPlaylist> 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<Map<String, Object>> id3Renderer = new MetadataTrackRenderer<>(
sampleSource, new Id3Parser(), player, mainHandler.getLooper());
// Build the text renderer, preferring Webvtt where available.
boolean preferWebvtt = false;
if (manifest instanceof HlsMasterPlaylist) {
preferWebvtt = !((HlsMasterPlaylist) manifest).subtitles.isEmpty();
}
TrackRenderer textRenderer;
if (preferWebvtt) {
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
HlsChunkSource textChunkSource = new HlsChunkSource(false /* isMaster */, textDataSource,
url, manifest, DefaultHlsTrackSelector.newVttInstance(), bandwidthMeter,
timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE);
HlsSampleSource textSampleSource = new HlsSampleSource(textChunkSource, loadControl,
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, NPExoPlayer.TYPE_TEXT);
textRenderer = new TextTrackRenderer(textSampleSource, player, mainHandler.getLooper());
} else {
textRenderer = new Eia608TrackRenderer(sampleSource, player, mainHandler.getLooper());
}
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
renderers[NPExoPlayer.TYPE_METADATA] = id3Renderer;
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
player.onRenderers(renderers, bandwidthMeter);
}
}
}

View File

@@ -0,0 +1,599 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.schabi.newpipe.player.exoplayer;
import com.google.android.exoplayer.CodecCounters;
import com.google.android.exoplayer.DummyTrackRenderer;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecTrackRenderer;
import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.TimeRange;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.audio.AudioTrack;
import com.google.android.exoplayer.chunk.ChunkSampleSource;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.dash.DashChunkSource;
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer.hls.HlsSampleSource;
import com.google.android.exoplayer.metadata.MetadataTrackRenderer.MetadataRenderer;
import com.google.android.exoplayer.text.Cue;
import com.google.android.exoplayer.text.TextRenderer;
import com.google.android.exoplayer.upstream.BandwidthMeter;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.util.DebugTextViewHelper;
import com.google.android.exoplayer.util.PlayerControl;
import android.media.MediaCodec.CryptoException;
import android.os.Handler;
import android.os.Looper;
import android.view.Surface;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* A wrapper around {@link ExoPlayer} that provides a higher level interface. It can be prepared
* with one of a number of {@link RendererBuilder} classes to suit different use cases (e.g. DASH,
* SmoothStreaming and so on).
*/
public class NPExoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener,
HlsSampleSource.EventListener, DefaultBandwidthMeter.EventListener,
MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener,
StreamingDrmSessionManager.EventListener, DashChunkSource.EventListener, TextRenderer,
MetadataRenderer<Map<String, Object>>, DebugTextViewHelper.Provider {
/**
* Builds renderers for the player.
*/
public interface RendererBuilder {
/**
* Builds renderers for playback.
*
* @param player The player for which renderers are being built. {@link NPExoPlayer#onRenderers}
* should be invoked once the renderers have been built. If building fails,
* {@link NPExoPlayer#onRenderersError} should be invoked.
*/
void buildRenderers(NPExoPlayer player);
/**
* Cancels the current build operation, if there is one. Else does nothing.
* <p>
* A canceled build operation must not invoke {@link NPExoPlayer#onRenderers} or
* {@link NPExoPlayer#onRenderersError} on the player, which may have been released.
*/
void cancel();
}
/**
* A listener for core events.
*/
public interface Listener {
void onStateChanged(boolean playWhenReady, int playbackState);
void onError(Exception e);
void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
float pixelWidthHeightRatio);
}
/**
* A listener for internal errors.
* <p>
* These errors are not visible to the user, and hence this listener is provided for
* informational purposes only. Note however that an internal error may cause a fatal
* error if the player fails to recover. If this happens, {@link Listener#onError(Exception)}
* will be invoked.
*/
public interface InternalErrorListener {
void onRendererInitializationError(Exception e);
void onAudioTrackInitializationError(AudioTrack.InitializationException e);
void onAudioTrackWriteError(AudioTrack.WriteException e);
void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);
void onDecoderInitializationError(DecoderInitializationException e);
void onCryptoError(CryptoException e);
void onLoadError(int sourceId, IOException e);
void onDrmSessionManagerError(Exception e);
}
/**
* A listener for debugging information.
*/
public interface InfoListener {
void onVideoFormatEnabled(Format format, int trigger, long mediaTimeMs);
void onAudioFormatEnabled(Format format, int trigger, long mediaTimeMs);
void onDroppedFrames(int count, long elapsed);
void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate);
void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
long mediaStartTimeMs, long mediaEndTimeMs);
void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs);
void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs);
void onAvailableRangeChanged(int sourceId, TimeRange availableRange);
}
/**
* A listener for receiving notifications of timed text.
*/
public interface CaptionListener {
void onCues(List<Cue> cues);
}
/**
* A listener for receiving ID3 metadata parsed from the media stream.
*/
public interface Id3MetadataListener {
void onId3Metadata(Map<String, Object> metadata);
}
// Constants pulled into this class for convenience.
public static final int STATE_IDLE = ExoPlayer.STATE_IDLE;
public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING;
public static final int STATE_BUFFERING = ExoPlayer.STATE_BUFFERING;
public static final int STATE_READY = ExoPlayer.STATE_READY;
public static final int STATE_ENDED = ExoPlayer.STATE_ENDED;
public static final int TRACK_DISABLED = ExoPlayer.TRACK_DISABLED;
public static final int TRACK_DEFAULT = ExoPlayer.TRACK_DEFAULT;
public static final int RENDERER_COUNT = 4;
public static final int TYPE_VIDEO = 0;
public static final int TYPE_AUDIO = 1;
public static final int TYPE_TEXT = 2;
public static final int TYPE_METADATA = 3;
private static final int RENDERER_BUILDING_STATE_IDLE = 1;
private static final int RENDERER_BUILDING_STATE_BUILDING = 2;
private static final int RENDERER_BUILDING_STATE_BUILT = 3;
private final RendererBuilder rendererBuilder;
private final ExoPlayer player;
private final PlayerControl playerControl;
private final Handler mainHandler;
private final CopyOnWriteArrayList<Listener> listeners;
private int rendererBuildingState;
private int lastReportedPlaybackState;
private boolean lastReportedPlayWhenReady;
private Surface surface;
private TrackRenderer videoRenderer;
private CodecCounters codecCounters;
private Format videoFormat;
private int videoTrackToRestore;
private BandwidthMeter bandwidthMeter;
private boolean backgrounded;
private CaptionListener captionListener;
private Id3MetadataListener id3MetadataListener;
private InternalErrorListener internalErrorListener;
private InfoListener infoListener;
public NPExoPlayer(RendererBuilder rendererBuilder) {
this.rendererBuilder = rendererBuilder;
player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000);
player.addListener(this);
playerControl = new PlayerControl(player);
mainHandler = new Handler();
listeners = new CopyOnWriteArrayList<>();
lastReportedPlaybackState = STATE_IDLE;
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
// Disable text initially.
player.setSelectedTrack(TYPE_TEXT, TRACK_DISABLED);
}
public PlayerControl getPlayerControl() {
return playerControl;
}
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
public void setInternalErrorListener(InternalErrorListener listener) {
internalErrorListener = listener;
}
public void setInfoListener(InfoListener listener) {
infoListener = listener;
}
public void setCaptionListener(CaptionListener listener) {
captionListener = listener;
}
public void setMetadataListener(Id3MetadataListener listener) {
id3MetadataListener = listener;
}
public void setSurface(Surface surface) {
this.surface = surface;
pushSurface(false);
}
public Surface getSurface() {
return surface;
}
public void blockingClearSurface() {
surface = null;
pushSurface(true);
}
public int getTrackCount(int type) {
return player.getTrackCount(type);
}
public MediaFormat getTrackFormat(int type, int index) {
return player.getTrackFormat(type, index);
}
public int getSelectedTrack(int type) {
return player.getSelectedTrack(type);
}
public void setSelectedTrack(int type, int index) {
player.setSelectedTrack(type, index);
if (type == TYPE_TEXT && index < 0 && captionListener != null) {
captionListener.onCues(Collections.<Cue>emptyList());
}
}
public boolean getBackgrounded() {
return backgrounded;
}
public void setBackgrounded(boolean backgrounded) {
if (this.backgrounded == backgrounded) {
return;
}
this.backgrounded = backgrounded;
if (backgrounded) {
videoTrackToRestore = getSelectedTrack(TYPE_VIDEO);
setSelectedTrack(TYPE_VIDEO, TRACK_DISABLED);
blockingClearSurface();
} else {
setSelectedTrack(TYPE_VIDEO, videoTrackToRestore);
}
}
public void prepare() {
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT) {
player.stop();
}
rendererBuilder.cancel();
videoFormat = null;
videoRenderer = null;
rendererBuildingState = RENDERER_BUILDING_STATE_BUILDING;
maybeReportPlayerState();
rendererBuilder.buildRenderers(this);
}
/**
* Invoked with the results from a {@link RendererBuilder}.
*
* @param renderers Renderers indexed by {@link NPExoPlayer} TYPE_* constants. An individual
* element may be null if there do not exist tracks of the corresponding type.
* @param bandwidthMeter Provides an estimate of the currently available bandwidth. May be null.
*/
/* package */ void onRenderers(TrackRenderer[] renderers, BandwidthMeter bandwidthMeter) {
for (int i = 0; i < RENDERER_COUNT; i++) {
if (renderers[i] == null) {
// Convert a null renderer to a dummy renderer.
renderers[i] = new DummyTrackRenderer();
}
}
// Complete preparation.
this.videoRenderer = renderers[TYPE_VIDEO];
this.codecCounters = videoRenderer instanceof MediaCodecTrackRenderer
? ((MediaCodecTrackRenderer) videoRenderer).codecCounters
: renderers[TYPE_AUDIO] instanceof MediaCodecTrackRenderer
? ((MediaCodecTrackRenderer) renderers[TYPE_AUDIO]).codecCounters : null;
this.bandwidthMeter = bandwidthMeter;
pushSurface(false);
player.prepare(renderers);
rendererBuildingState = RENDERER_BUILDING_STATE_BUILT;
}
/**
* Invoked if a {@link RendererBuilder} encounters an error.
*
* @param e Describes the error.
*/
/* package */ void onRenderersError(Exception e) {
if (internalErrorListener != null) {
internalErrorListener.onRendererInitializationError(e);
}
for (Listener listener : listeners) {
listener.onError(e);
}
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
maybeReportPlayerState();
}
public void setPlayWhenReady(boolean playWhenReady) {
player.setPlayWhenReady(playWhenReady);
}
public void seekTo(long positionMs) {
player.seekTo(positionMs);
}
public void release() {
rendererBuilder.cancel();
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
surface = null;
player.release();
}
public int getPlaybackState() {
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) {
return STATE_PREPARING;
}
int playerState = player.getPlaybackState();
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT && playerState == STATE_IDLE) {
// This is an edge case where the renderers are built, but are still being passed to the
// player's playback thread.
return STATE_PREPARING;
}
return playerState;
}
@Override
public Format getFormat() {
return videoFormat;
}
@Override
public BandwidthMeter getBandwidthMeter() {
return bandwidthMeter;
}
@Override
public CodecCounters getCodecCounters() {
return codecCounters;
}
@Override
public long getCurrentPosition() {
return player.getCurrentPosition();
}
public long getDuration() {
return player.getDuration();
}
public int getBufferedPercentage() {
return player.getBufferedPercentage();
}
public boolean getPlayWhenReady() {
return player.getPlayWhenReady();
}
/* package */ Looper getPlaybackLooper() {
return player.getPlaybackLooper();
}
/* package */ Handler getMainHandler() {
return mainHandler;
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int state) {
maybeReportPlayerState();
}
@Override
public void onPlayerError(ExoPlaybackException exception) {
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
for (Listener listener : listeners) {
listener.onError(exception);
}
}
@Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
float pixelWidthHeightRatio) {
for (Listener listener : listeners) {
listener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
}
}
@Override
public void onDroppedFrames(int count, long elapsed) {
if (infoListener != null) {
infoListener.onDroppedFrames(count, elapsed);
}
}
@Override
public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) {
if (infoListener != null) {
infoListener.onBandwidthSample(elapsedMs, bytes, bitrateEstimate);
}
}
@Override
public void onDownstreamFormatChanged(int sourceId, Format format, int trigger,
long mediaTimeMs) {
if (infoListener == null) {
return;
}
if (sourceId == TYPE_VIDEO) {
videoFormat = format;
infoListener.onVideoFormatEnabled(format, trigger, mediaTimeMs);
} else if (sourceId == TYPE_AUDIO) {
infoListener.onAudioFormatEnabled(format, trigger, mediaTimeMs);
}
}
@Override
public void onDrmKeysLoaded() {
// Do nothing.
}
@Override
public void onDrmSessionManagerError(Exception e) {
if (internalErrorListener != null) {
internalErrorListener.onDrmSessionManagerError(e);
}
}
@Override
public void onDecoderInitializationError(DecoderInitializationException e) {
if (internalErrorListener != null) {
internalErrorListener.onDecoderInitializationError(e);
}
}
@Override
public void onAudioTrackInitializationError(AudioTrack.InitializationException e) {
if (internalErrorListener != null) {
internalErrorListener.onAudioTrackInitializationError(e);
}
}
@Override
public void onAudioTrackWriteError(AudioTrack.WriteException e) {
if (internalErrorListener != null) {
internalErrorListener.onAudioTrackWriteError(e);
}
}
@Override
public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
if (internalErrorListener != null) {
internalErrorListener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
}
}
@Override
public void onCryptoError(CryptoException e) {
if (internalErrorListener != null) {
internalErrorListener.onCryptoError(e);
}
}
@Override
public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs) {
if (infoListener != null) {
infoListener.onDecoderInitialized(decoderName, elapsedRealtimeMs, initializationDurationMs);
}
}
@Override
public void onLoadError(int sourceId, IOException e) {
if (internalErrorListener != null) {
internalErrorListener.onLoadError(sourceId, e);
}
}
@Override
public void onCues(List<Cue> cues) {
if (captionListener != null && getSelectedTrack(TYPE_TEXT) != TRACK_DISABLED) {
captionListener.onCues(cues);
}
}
@Override
public void onMetadata(Map<String, Object> metadata) {
if (id3MetadataListener != null && getSelectedTrack(TYPE_METADATA) != TRACK_DISABLED) {
id3MetadataListener.onId3Metadata(metadata);
}
}
@Override
public void onAvailableRangeChanged(int sourceId, TimeRange availableRange) {
if (infoListener != null) {
infoListener.onAvailableRangeChanged(sourceId, availableRange);
}
}
@Override
public void onPlayWhenReadyCommitted() {
// Do nothing.
}
@Override
public void onDrawnToSurface(Surface surface) {
// Do nothing.
}
@Override
public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
long mediaStartTimeMs, long mediaEndTimeMs) {
if (infoListener != null) {
infoListener.onLoadStarted(sourceId, length, type, trigger, format, mediaStartTimeMs,
mediaEndTimeMs);
}
}
@Override
public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) {
if (infoListener != null) {
infoListener.onLoadCompleted(sourceId, bytesLoaded, type, trigger, format, mediaStartTimeMs,
mediaEndTimeMs, elapsedRealtimeMs, loadDurationMs);
}
}
@Override
public void onLoadCanceled(int sourceId, long bytesLoaded) {
// Do nothing.
}
@Override
public void onUpstreamDiscarded(int sourceId, long mediaStartTimeMs, long mediaEndTimeMs) {
// Do nothing.
}
private void maybeReportPlayerState() {
boolean playWhenReady = player.getPlayWhenReady();
int playbackState = getPlaybackState();
if (lastReportedPlayWhenReady != playWhenReady || lastReportedPlaybackState != playbackState) {
for (Listener listener : listeners) {
listener.onStateChanged(playWhenReady, playbackState);
}
lastReportedPlayWhenReady = playWhenReady;
lastReportedPlaybackState = playbackState;
}
}
private void pushSurface(boolean blockForSurfacePush) {
if (videoRenderer == null) {
return;
}
if (blockForSurfacePush) {
player.blockingSendMessage(
videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
} else {
player.sendMessage(
videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
}
}
}

View File

@@ -0,0 +1,206 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.schabi.newpipe.player.exoplayer;
import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder;
import com.google.android.exoplayer.DefaultLoadControl;
import com.google.android.exoplayer.LoadControl;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecSelector;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.chunk.ChunkSampleSource;
import com.google.android.exoplayer.chunk.ChunkSource;
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
import com.google.android.exoplayer.drm.DrmSessionManager;
import com.google.android.exoplayer.drm.MediaDrmCallback;
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer.drm.UnsupportedDrmException;
import com.google.android.exoplayer.smoothstreaming.DefaultSmoothStreamingTrackSelector;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestParser;
import com.google.android.exoplayer.text.TextTrackRenderer;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultAllocator;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer.upstream.DefaultUriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.Util;
import android.content.Context;
import android.media.AudioManager;
import android.media.MediaCodec;
import android.os.Handler;
import java.io.IOException;
/**
* A {@link RendererBuilder} for SmoothStreaming.
*/
public class SmoothStreamingRendererBuilder implements RendererBuilder {
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int VIDEO_BUFFER_SEGMENTS = 200;
private static final int AUDIO_BUFFER_SEGMENTS = 54;
private static final int TEXT_BUFFER_SEGMENTS = 2;
private static final int LIVE_EDGE_LATENCY_MS = 30000;
private final Context context;
private final String userAgent;
private final String url;
private final MediaDrmCallback drmCallback;
private AsyncRendererBuilder currentAsyncBuilder;
public SmoothStreamingRendererBuilder(Context context, String userAgent, String url,
MediaDrmCallback drmCallback) {
this.context = context;
this.userAgent = userAgent;
this.url = Util.toLowerInvariant(url).endsWith("/manifest") ? url : url + "/Manifest";
this.drmCallback = drmCallback;
}
@Override
public void buildRenderers(NPExoPlayer player) {
currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, drmCallback, player);
currentAsyncBuilder.init();
}
@Override
public void cancel() {
if (currentAsyncBuilder != null) {
currentAsyncBuilder.cancel();
currentAsyncBuilder = null;
}
}
private static final class AsyncRendererBuilder
implements ManifestFetcher.ManifestCallback<SmoothStreamingManifest> {
private final Context context;
private final String userAgent;
private final MediaDrmCallback drmCallback;
private final NPExoPlayer player;
private final ManifestFetcher<SmoothStreamingManifest> manifestFetcher;
private boolean canceled;
public AsyncRendererBuilder(Context context, String userAgent, String url,
MediaDrmCallback drmCallback, NPExoPlayer player) {
this.context = context;
this.userAgent = userAgent;
this.drmCallback = drmCallback;
this.player = player;
SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser();
manifestFetcher = new ManifestFetcher<>(url, new DefaultHttpDataSource(userAgent, null),
parser);
}
public void init() {
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
}
public void cancel() {
canceled = true;
}
@Override
public void onSingleManifestError(IOException exception) {
if (canceled) {
return;
}
player.onRenderersError(exception);
}
@Override
public void onSingleManifest(SmoothStreamingManifest manifest) {
if (canceled) {
return;
}
Handler mainHandler = player.getMainHandler();
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player);
// Check drm support if necessary.
DrmSessionManager drmSessionManager = null;
if (manifest.protectionElement != null) {
if (Util.SDK_INT < 18) {
player.onRenderersError(
new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME));
return;
}
try {
drmSessionManager = new StreamingDrmSessionManager(manifest.protectionElement.uuid,
player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player);
} catch (UnsupportedDrmException e) {
player.onRenderersError(e);
return;
}
}
// Build the video renderer.
DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
DefaultSmoothStreamingTrackSelector.newVideoInstance(context, true, false),
videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
NPExoPlayer.TYPE_VIDEO);
TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, videoSampleSource,
MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
drmSessionManager, true, mainHandler, player, 50);
// Build the audio renderer.
DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
DefaultSmoothStreamingTrackSelector.newAudioInstance(),
audioDataSource, null, LIVE_EDGE_LATENCY_MS);
ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
NPExoPlayer.TYPE_AUDIO);
TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource,
MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, player,
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
// Build the text renderer.
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
ChunkSource textChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
DefaultSmoothStreamingTrackSelector.newTextInstance(),
textDataSource, null, LIVE_EDGE_LATENCY_MS);
ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
NPExoPlayer.TYPE_TEXT);
TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player,
mainHandler.getLooper());
// Invoke the callback.
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
player.onRenderers(renderers, bandwidthMeter);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

View File

@@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ErrorActivity">
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:focusable="true"
android:focusableInTouchMode="true">
<TextView
android:id="@+id/errorSorryView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:gravity="center"
android:text="@string/sorry_string"
android:textStyle="bold" />
<TextView
android:id="@+id/messageWhatHappenedView"
android:paddingTop="@dimen/activity_vertical_margin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/what_happened_headline"/>
<TextView
android:id="@+id/errorMessageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:text="@string/info_labels"/>
<TextView
android:id="@+id/errorDeviceHeadlineView"
android:paddingTop="@dimen/activity_vertical_margin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/what_device_headline"/>
<LinearLayout
android:id="@+id/errorInfoLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/errorInfoLabelsView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:text="@string/info_labels"/>
<HorizontalScrollView
android:paddingLeft="16dp"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/errorInfosView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</HorizontalScrollView>
</LinearLayout>
<TextView
android:id="@+id/errorDetailView"
android:paddingTop="@dimen/activity_vertical_margin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/error_details_headline"/>
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/horizontalScrollView"
android:layout_gravity="center" >
<TextView
android:id="@+id/errorView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:typeface="monospace"/>
</HorizontalScrollView>
<TextView
android:id="@+id/errorYourComment"
android:paddingTop="@dimen/activity_vertical_margin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/your_comment"/>
<EditText
android:id="@+id/errorCommentBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/errorReportButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/error_report_button_text" />
</LinearLayout>
</ScrollView>
</FrameLayout>

View File

@@ -2,7 +2,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="org.schabi.newpipe.PlayVideoActivity"
tools:context=".player.PlayVideoActivity"
android:gravity="center">
<VideoView android:id="@+id/video_view"

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root"
android:focusable="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<com.google.android.exoplayer.AspectRatioFrameLayout android:id="@+id/video_frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center">
<SurfaceView android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"/>
<View android:id="@+id/shutter"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"/>
<com.google.android.exoplayer.text.SubtitleLayout android:id="@+id/subtitles"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</com.google.android.exoplayer.AspectRatioFrameLayout>
</FrameLayout>

View File

@@ -1,20 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/content"
android:id="@+id/notificationContent"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:background="@color/background_notification_color"
tools:targetApi="jelly_bean">
<Button
android:id="@+id/notificationBackgroundButton"
android:layout_width="match_parent"
android:layout_height="64dp"
android:background="@android:color/transparent"/>
android:background="@color/background_notification_color">
<LinearLayout
android:layout_width="match_parent"
@@ -74,7 +66,6 @@
android:clickable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_close_white_24dp" />
</LinearLayout>

View File

@@ -1,18 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/content"
android:id="@+id/notificationContent"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:background="@color/background_notification_color"
tools:targetApi="jelly_bean" >
<Button
android:id="@+id/notificationBackgroundButton"
android:layout_width="match_parent"
android:layout_height="128dp"
android:background="@android:color/transparent"/>
android:background="@color/background_notification_color">
<ImageView
android:id="@+id/notificationCover"

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/menu_item_share_error"
android:title="@string/share"
app:showAsAction="ifRoom"
android:icon="@drawable/ic_share_black"/>
</menu>

View File

@@ -10,4 +10,8 @@
<item android:id="@+id/action_settings"
app:showAsAction="never"
android:title="@string/settings"/>
<item android:id="@+id/action_report_error"
app:showAsAction="never"
android:title="@string/report_error" />
</menu>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@@ -10,7 +10,7 @@
<string name="download">Download</string>
<string name="search">Suchen</string>
<string name="settings">Einstellungen</string>
<string name="did_you_mean">Meintest du: </string>
<string name="did_you_mean">Meintest du: %1$s ?</string>
<string name="search_page">Suchseite: </string>
<string name="share_dialog_title">Teilen mit:</string>
<string name="choose_browser">Browser:</string>
@@ -55,12 +55,12 @@
<string name="play_btn_text">Abspielen</string>
<string name="use_tor_title">Benutze TOR</string>
<string name="use_tor_summary">Erzwinge das Herunterladen durch TOR für verbesserte Privatsphäre (Videostream noch nicht unterstützt)</string>
<string name="use_tor_summary">(Experimentell) Erzwinge das Herunterladen durch TOR für verbesserte Privatsphäre (Videostream noch nicht unterstützt).</string>
<string name="background_player_name">NewPipe Hintergrundwiedergabe</string>
<string name="network_error">Netzwerkfehler</string>
<string name="download_path_audio_title">Downloadverzeichnis für Musik</string>
<string name="download_path_audio_summary">Verzeichnis zum Speichern heruntergeladener Audiodateien</string>
<string name="download_path_audio_summary">Verzeichnis zum Speichern heruntergeladener Audiodateien.</string>
<string name="download_path_audio_dialog_title">Pfad für heruntergeladene Audiodateien eingeben.</string>
<string name="theme_title">Aussehen</string>
@@ -78,4 +78,51 @@
<string name="content_not_available">Inhalt nicht verfügbar.</string>
<string name="blocked_by_gema">Durch die GEMA gesperrt.</string>
<string name="content">Inhalt</string>
<string name="show_age_restricted_content_title">Altersbeschränkte Inhalte anzeigen</string>
<string name="video_is_age_restricted">Video ist altersbeschränkt. Schalten Sie erst altersbeschränkte Videos in den Einstellungen ein.</string>
<string name="could_not_setup_download_menu">Konnte Downloadmenü nicht einrichten.</string>
<string name="live_streams_not_supported">Dies ist ein LIVESTREAM. Diese werden noch nicht unterstützt.</string>
<string name="light_parsing_error">Konnte Webseite nicht vollständig parsen.</string>
<string name="error_report_button_text">Fehler via Mail melden</string>
<string name="error_snackbar_action">MELDEN</string>
<string name="what_device_headline">Info:</string>
<string name="what_happened_headline">Dies ist passiert:</string>
<string name="info_labels">Was:\\nAnfrage:\\nSprache des Inhalts:\\nDienst:\\nZeit (GMT):\\nVersion:\\nOS-Version:\\nGlob. IP-Bereich:</string>
<string name="error_details_headline">Details:</string>
<string name="enable_background_audio">Im Hintergrund abspielen</string>
<string name="video">Video</string>
<string name="audio">Audio</string>
<string name="text">Text</string>
<string name="logging_normal">Normal</string>
<string name="logging_verbose">Ausführlich</string>
<string name="retry">Wiederholen</string>
<string name="off">[aus]</string>
<string name="error_drm_unsupported_scheme">Dieses Gerät unterstützt das erforderliche DRM-Schema nicht</string>
<string name="error_drm_unknown">Ein unbekannter DRM-Fehler ist aufgetreten</string>
<string name="error_querying_decoders">Konnte Dekodierer des Gerätes nicht abrufen</string>
<string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_instantiating_decoder">Konnte Dekodierer <xliff:g id="decoder_name">%1$s</xliff:g> nicht instantiieren</string>
<string name="storage_permission_denied">Zugriff auf den Massenspeicher wurde verweigert</string>
<string name="use_exoplayer_title">Benutze ExoPlayer</string>
<string name="use_exoplayer_summary">Experimentell</string>
<string name="sorry_string">Entschuldigung. Dies sollte nicht passieren.</string>
<string name="error_snackbar_message">Entschuldigung. Es sind einige Fehler aufgetreten.</string>
<string name="info_searched_lbl">Gesucht:</string>
<string name="info_requested_stream_lbl">Angeforderter Stream:</string>
<string name="your_comment">Dein Kommentar (auf englisch):</string>
<string name="logging">Protokollierung</string>
<string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_no_decoder">Dieses Gerät stellt keinen Dekodierer für <xliff:g id="mime_type">%1$s</xliff:g> bereit</string>
<string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_no_secure_decoder">Dieses Gerät stellt keinen abgesicherten Dekodierer für <xliff:g id="mime_type">%1$s</xliff:g> bereit</string>
<string name="could_not_get_stream">Konnte keinen Stream holen.</string>
<string name="error_drm_not_supported">Geschützte Inhalte werden von API-Ebenen unterhalb von 18 nicht unterstützt</string>
<string name="autoplay_by_calling_app_title">Bei Aufruf aus einer anderen App automatisch abspielen.</string>
<string name="autoplay_by_calling_app_summary">Spielt ein Video automatisch ab, wenn NewPipe von einer anderen App aufgerufen wurde.</string>
<string name="report_error">Einen Fehler melden</string>
<string name="user_report">Anwenderbericht</string>
</resources>

View File

@@ -74,4 +74,11 @@
<string name="detail_likes_img_view_description">Me gusta</string>
<string name="detail_dislikes_img_view_description">No me gusta</string>
<string name="detail_uploader_thumbnail_view_description">Foto miniatura del usuario</string>
<string name="live_streams_not_supported">Esta es una transmisión en Vivo. Todavía no es compatible.</string>
<string name="content">Contenido</string>
<string name="show_age_restricted_content_title">Mostrar contenido con restricción de edad</string>
<string name="video_is_age_restricted">El video tiene restricción de edad.Habilite los videos con restricción de edad en configuración.</string>
</resources>

View File

@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="view_count_text">%1$s نماها</string>
<string name="upload_date_text">بارگذاری‌شده در: %1$s</string>
<string name="no_player_found">هیچ پخش‌کننده‌ی جریانی یافت نشد. ممکن است بخواهید یکی نصب کنید.</string>
<string name="install">نصب کنید</string>
<string name="cancel">انصراف</string>
<string name="open_in_browser">بازکردن در مرورگر</string>
<string name="share">هم‌رسانی</string>
<string name="download">بارگیری</string>
<string name="search">جستجو</string>
<string name="settings">تنظیمات</string>
<string name="did_you_mean">منظورتان این است: </string>
<string name="search_page">صفحه‌ی جستجو: </string>
<string name="share_dialog_title">هم‌رسانی با:</string>
<string name="choose_browser">مرورگر را برگزینید:</string>
<string name="screen_rotation">چرخش</string>
<string name="settings_activity_title">تنظیمات</string>
<string name="useExternalPlayerTitle">استفاده از پخش‌کننده‌ی خارجی</string>
<string name="download_path_title">محل بارگیری</string>
<string name="download_path_summary">مسیری که ویدئوهای دریافت شده در آن ذخیره می‌شوند.</string>
<string name="download_path_dialog_title">مسیر دریافت را وارد کنید</string>
<string name="autoplay_through_intent_title">پخش خودکار از Intent</string>
<string name="autoplay_through_intent_summary">ویدئو هنگامی که از برنامه‌ی دیگری فراخوانده شد خودکار پخش می‌شود.</string>
<string name="default_resolution_title">وضوح پیش‌فرض</string>
<string name="play_with_kodi_title">پخش با Kodi</string>
<string name="kore_not_found">برنامه‌ی Kore نصب نیست. برای پخش کردن ویدئوها با مرکز رسانه‌ی Kodi، به Kore نیاز دارید.</string>
<string name="installeKore">نصب Kore</string>
<string name="show_play_with_kodi_title">نمایش گزینه‌ی «پخش با Kodi»</string>
<string name="show_play_with_kodi_summary">گزینه‌ای برای پخش کردن ویدئو با مرکز رسانه‌ی Kodi نشان می‌دهد.</string>
<string name="play_audio">صدا</string>
<string name="default_audio_format_title">قالب پیش‌فرض صدا</string>
<string name="webm_description">WebM &#8212; قالبی آزاد</string>
<string name="m4a_description">m4a &#8212; کیفیت بهتر</string>
<string name="download_dialog_title">دریافت</string>
<string-array name="downloadOptions">
<item>ویدئو</item>
<item>صدا</item>
</string-array>
<string name="next_video_title">ویدئوی بعدی</string>
<string name="url_not_supported_toast">پیوند پشتیبانی نمی‌شود.</string>
</resources>

View File

@@ -1,10 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<resources><string name="view_count_text">%1$s צפיות</string>
<string name="upload_date_text">הועלה בתאריך %1$s</string>
<string name="share">שתף</string>
<string name="search">חפש</string>
<string name="next_video_title">הבא</string>
<string name="download">הורדה</string>
<string name="settings">הגדרות</string>
<string name="settings_activity_title">הגדרות</string>
</resources>

View File

@@ -1 +0,0 @@
values-in

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -1,3 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
</resources>

View File

@@ -1,9 +1,9 @@
<?xml version='1.0' encoding='utf-8'?>
<?xml version='1.0' encoding='UTF-8'?>
<resources><string name="view_count_text">%1$s visite</string>
<string name="upload_date_text">Caricato in %1$s</string>
<string name="no_player_found">Nessun riproduttore trovato. Dovresti installarne uno.</string>
<string name="upload_date_text">Pubblicato il %1$s</string>
<string name="no_player_found">Nessun riproduttore trovato. Vuoi installare VLC?</string>
<string name="install">Installa</string>
<string name="cancel">Cancella</string>
<string name="cancel">Annulla</string>
<string name="open_in_browser">Apri nel browser</string>
<string name="share">Condividi</string>
<string name="download">Scarica</string>
@@ -16,34 +16,74 @@
<string name="screen_rotation">rotazione</string>
<string name="settings_activity_title">Impostazioni</string>
<string name="useExternalPlayerTitle">Usa un riproduttore video esterno</string>
<string name="download_path_title">Cartella di download</string>
<string name="download_path_title">Cartella dei download</string>
<string name="download_path_summary">Percorso dove memorizzare i video scaricati.</string>
<string name="download_path_dialog_title">Inserisci il percorso di download</string>
<string name="download_path_dialog_title">Inserisci il percorso per i download</string>
<string name="autoplay_through_intent_title">Auto riproduzione attraverso internet</string>
<string name="autoplay_through_intent_summary">Avvia automaticamente un video quando è stato chiamato da un\'altra applicazione.</string>
<string name="default_resolution_title">Risoluzione predefinita</string>
<string name="play_with_kodi_title">Riproduci con Kodi</string>
<string name="kore_not_found">Kore app non trovata. Kore è richiesto per riprodurre video con Kodi media center.</string>
<string name="kore_not_found">L\'applicazione Kore non è stata trovata. Kore è necessario per riprodurre video con Kodi media center. Vorresti installarlo?</string>
<string name="installeKore">Installa Kore</string>
<string name="show_play_with_kodi_title">Mostra l\'opzione \"Riproduci con Kodi\"</string>
<string name="show_play_with_kodi_summary">Mostra un opzione per riprodurre un video attraverso Kodi media center.</string>
<string name="play_audio">Audio</string>
<string name="default_audio_format_title">Formato audio predefinito</string>
<string name="webm_description">WedM &#8212; formato libero</string>
<string name="m4a_description">m4a &#8212; qualità migliore</string>
<string name="webm_description">WebM formato libero</string>
<string name="m4a_description">m4a qualità migliore</string>
<string name="download_dialog_title">Scarica</string>
<string name="next_video_title">Prossimo video</string>
<string name="show_next_and_similar_title">Mostra i video successivi e simili</string>
<string name="url_not_supported_toast">URL non supportato.</string>
<string name="show_next_and_similar_title">Mostra video a seguire e video simili</string>
<string name="url_not_supported_toast">URL non supportato</string>
<string name="similar_videos_btn_text">Video simili</string>
<string name="search_language_title">Lingua preferita dei contenuti</string>
<string name="settings_category_video_audio_title">VIDEO &amp; AUDIO</string>
<string name="search_language_title">Lingua preferita per i contenuti</string>
<string name="settings_category_video_audio_title">Video e Audio</string>
<string name="settingsCategoryVideoInfoTittle">INFO</string>
<string name="settingsCategoryEtcTitle">ETC</string>
<string name="list_thumbnail_view_description">Anteprima video</string>
<string name="detail_thumbnail_view_description">Anteprima video</string>
<string name="detail_uploader_thumbnail_view_description">Miniatura caricata</string>
<string name="detail_uploader_thumbnail_view_description">Miniatura dell\'immagine di profilo degli utenti</string>
<string name="detail_dislikes_img_view_description">Non mi piace</string>
<string name="detail_likes_img_view_description">Mi piace</string>
</resources>
<string name="err_dir_create">Impossibile creare la cartella di download \'%1$s\'</string>
<string name="info_dir_created">Creata la cartella per i download \'%1$s\'</string>
<string name="background_player_name">Player in background di NewPipe</string>
<string name="loading">Caricamento</string>
<string name="use_external_video_player_title">Usa un lettore video esterno</string>
<string name="use_external_audio_player_title">Usa un lettore audio esterno</string>
<string name="download_path_audio_title">Cartella dei download degli audio</string>
<string name="download_path_audio_summary">Cartella dove salvare gli audio scaricati.</string>
<string name="download_path_audio_dialog_title">Inserisci la cartella per i file audio.</string>
<string name="theme_title">Tema</string>
<string name="dark_theme_title">Scuro</string>
<string name="light_theme_title">Chiaro</string>
<string name="settings_category_appearance_title">Aspetto</string>
<string name="settings_category_other_title">Altro</string>
<string name="background_player_playing_toast">In riproduzione in background</string>
<string name="play_btn_text">Riproduci</string>
<string name="general_error">Errore</string>
<string name="network_error">Errore di rete</string>
<string name="could_not_load_thumbnails">Impossibile caricare tutte le miniature</string>
<string name="youtube_signature_decryption_error">Impossibile decriptare la firma dell\'URL del video.</string>
<string name="content_not_available">Contenuto non disponibile.</string>
<string name="blocked_by_gema">Bloccato dalla GEMA.</string>
<string name="use_tor_title">Usa Tor</string>
<string name="use_tor_summary">Forza il traffico in download tramite Tor per una maggiore privacy (lo streaming dei video non è ancora supportato).</string>
<string name="parsing_error">Impossibile analizzare il sito web.</string>
<string name="could_not_setup_download_menu">Impossibile impostare il menù di download.</string>
<string name="live_streams_not_supported">Questo è uno stream dal vivo. Gli stream dal vivo non sono ancora supportati.</string>
<string name="content">Contenuti</string>
<string name="show_age_restricted_content_title">Mostra contenuti vincolati all\'età</string>
<string name="video_is_age_restricted">Questo video è vincolato alla maggiore età. Per accedervi, abilita \"Mostra video vincolati all\'età\" nelle impostazioni.</string>
</resources>

View File

@@ -1 +0,0 @@
values-he

View File

@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="upload_date_text">"アップロード: "%1$s</string>
<string name="no_player_found">StreamPlayer が見つかりませんでした。インストールが必要になるかもしれません。</string>
<string name="no_player_found">ストリームのプレイヤーが見つかりません。VLC をインストールしますか?</string>
<string name="install">インストール</string>
<string name="cancel">取り消し</string>
<string name="open_in_browser">ブラウザーで開く</string>
@@ -9,7 +9,7 @@
<string name="download">ダウンロード</string>
<string name="search">検索</string>
<string name="settings">設定</string>
<string name="did_you_mean">"この意味ですか: "</string>
<string name="did_you_mean">この意味ですか: %1$s ?</string>
<string name="search_page">"検索ページ: "</string>
<string name="share_dialog_title">…共有:</string>
<string name="choose_browser">ブラウザーを選択:</string>
@@ -23,7 +23,7 @@
<string name="autoplay_through_intent_summary">他のアプリケーションから呼び出されたとき、自動的に動画再生を開始します。</string>
<string name="default_resolution_title">基本の解像度</string>
<string name="play_with_kodi_title">Kodi で再生</string>
<string name="kore_not_found">Kore アプリが見つかりません。 Kodi メディアセンターで動画を再生するには、 Kore が必要です。</string>
<string name="kore_not_found">Kore アプリが見つかりません。 Kore をインストールしますか?</string>
<string name="installeKore">Kore をインストール</string>
<string name="show_play_with_kodi_title">\"Kodi で再生\" 設定を表示</string>
<string name="show_play_with_kodi_summary">Kodi メディアセンター経由で動画を再生するための設定を表示します.</string>
@@ -53,7 +53,7 @@
<string name="play_btn_text">再生</string>
<string name="use_tor_title">Tor を使用する</string>
<string name="use_tor_summary">強制的に Tor を経由したプライバシーを高めたトラフィックでダウンロードします (ビデオのストリーミングはまだサポートされていません)</string>
<string name="use_tor_summary">(実験的) 強制的に Tor を経由したトラフィックでダウンロードして、プライバシーを高めます (ビデオのストリーミングはまだサポートされていません)</string>
<string name="theme_title">テーマ</string>
<string name="dark_theme_title">ダーク</string>
<string name="light_theme_title">ライト</string>
@@ -78,4 +78,50 @@
<string name="could_not_setup_download_menu">ダウンロード メニューを設定できませんでした。</string>
<string name="live_streams_not_supported">これはライブ ストリームです。これらはまだサポートされていません。</string>
<string name="content">コンテンツ</string>
<string name="show_age_restricted_content_title">年齢制限コンテンツを表示</string>
<string name="video_is_age_restricted">ビデオは年齢制限です。まず設定で年齢制限のビデオを有効にしてください。</string>
<string name="light_parsing_error">ウェブサイトを完全に解析できませんでした。</string>
<string name="could_not_get_stream">ストリームを取得できませんでした。</string>
<string name="sorry_string">申し訳ありません。発生すべきではありませんでした。</string>
<string name="error_report_button_text">メールでエラーを報告</string>
<string name="error_snackbar_message">申し訳ありません。いくつかのエラーが発生しました。</string>
<string name="error_snackbar_action">報告</string>
<string name="what_device_headline">情報:</string>
<string name="what_happened_headline">何が起こりましたか:</string>
<string name="info_labels">何:\\nリクエスト:\\nコンテンツの言語:\\nサービス:\\nGMT 時間:\\nバージョン:\\nOS バージョン:\\nグローバル IP の範囲:</string>
<string name="info_searched_lbl">検索結果:</string>
<string name="info_requested_stream_lbl">要求したストリーム:</string>
<string name="your_comment">あなたのコメント (英語で):</string>
<string name="error_details_headline">詳細:</string>
<string name="enable_background_audio">バックグラウンドで再生</string>
<string name="video">ビデオ</string>
<string name="audio">オーディオ</string>
<string name="text">テキスト</string>
<string name="logging">ログ</string>
<string name="logging_normal">標準</string>
<string name="logging_verbose">詳細</string>
<string name="retry">再試行</string>
<string name="off">[オフ]</string>
<string name="error_drm_not_supported">保護されたコンテンツは API レベル 18 未満でサポートされていません</string>
<string name="error_drm_unsupported_scheme">このデバイスは必要な DRM スキームをサポートしません</string>
<string name="error_drm_unknown">不明な DRM エラーが発生しました</string>
<string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_no_decoder">このデバイスは <xliff:g id="mime_type">%1$s</xliff:g> のデコーダーが提供されていません</string>
<string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_no_secure_decoder">このデバイスは <xliff:g id="mime_type">%1$s</xliff:g> のセキュア デコーダーが提供されていません</string>
<string name="error_querying_decoders">デバイスのデコーダーを問い合わせできません</string>
<string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_instantiating_decoder">デコーダー <xliff:g id="decoder_name">%1$s</xliff:g> をインスタンス化できません</string>
<string name="storage_permission_denied">ストレージにアクセスするアクセス許可が拒否されました</string>
<string name="use_exoplayer_title">ExoPlayer を使用する</string>
<string name="use_exoplayer_summary">実験的</string>
<string name="autoplay_by_calling_app_title">別のアプリから呼び出されたときに自動再生。</string>
<string name="autoplay_by_calling_app_summary">NewPipe が別のアプリから呼び出されたときに、自動的にビデオを再生します。</string>
<string name="report_error">エラーを報告</string>
<string name="user_report">ユーザー報告</string>
</resources>

View File

@@ -2,7 +2,7 @@
<resources>
<string name="view_count_text">%1$s keer bekeken</string>
<string name="upload_date_text">Geüpload op %1$s</string>
<string name="no_player_found">Geen speler met streamingondersteuning gevonden. Wil je VLC installeren?</string>
<string name="no_player_found">Geen speler met streamondersteuning gevonden. Wil je VLC installeren?</string>
<string name="install">Installeren</string>
<string name="cancel">Annuleren</string>
<string name="open_in_browser">In browser openen</string>
@@ -10,7 +10,7 @@
<string name="download">Downloaden</string>
<string name="search">Zoeken</string>
<string name="settings">Instellingen</string>
<string name="did_you_mean">Bedoelde je: </string>
<string name="did_you_mean">Bedoelde je: %1$s ?</string>
<string name="search_page">Zoekpagina: </string>
<string name="share_dialog_title">Delen met:</string>
<string name="choose_browser">Browser kiezen:</string>
@@ -23,31 +23,91 @@
<string name="autoplay_through_intent_title">Speel automatisch via Intent</string>
<string name="autoplay_through_intent_summary">Speel een video automatisch af indien geopend vanuit een andere app.</string>
<string name="default_resolution_title">Standaardresolutie</string>
<string name="play_with_kodi_title">Speel af met Kodi</string>
<string name="kore_not_found">Kore app niet gevonden. Kore is nodig om videos op Kodi af te spelen.</string>
<string name="play_with_kodi_title">Afspelen met Kodi</string>
<string name="kore_not_found">Kore-app niet gevonden. Kore installeren?</string>
<string name="installeKore">Installeer Kore</string>
<string name="show_play_with_kodi_title">Toon \"Speel af met Kodi\" optie</string>
<string name="show_play_with_kodi_title">Toon \"Afspelen met Kodi\" optie</string>
<string name="show_play_with_kodi_summary">Toont een optie om een video op een Kodi media center af te spelen.</string>
<string name="play_audio">Audio</string>
<string name="default_audio_format_title">Standaard audio formaat</string>
<string name="webm_description">WebM — open formaat</string>
<string name="default_audio_format_title">Standaard audioformaat</string>
<string name="webm_description">WebM — vrij formaat</string>
<string name="m4a_description">m4a — betere kwaliteit</string>
<string name="download_dialog_title">Download</string>
<string name="download_dialog_title">Downloaden</string>
<string name="next_video_title">Volgende video</string>
<string name="url_not_supported_toast">URL wordt niet ondersteund.</string>
<string name="similar_videos_btn_text">Vergelijkbare videos</string>
<string name="show_next_and_similar_title">Laat volgende en vergelijkbare videos zien</string>
<string name="search_language_title">Voorkeurs content taal</string>
<string name="url_not_supported_toast">URL wordt niet ondersteund</string>
<string name="similar_videos_btn_text">Vergelijkbare video\'s</string>
<string name="show_next_and_similar_title">Laat volgende en vergelijkbare video\'s zien</string>
<string name="search_language_title">Voorkeurstaal voor inhoud</string>
<string name="use_external_video_player_title">Externe videospeler gebruiken</string>
<string name="use_external_audio_player_title">Externe audiospeler gebruiken</string>
<string name="settings_category_video_audio_title">Video &amp; Geluid</string>
<string name="settings_category_video_audio_title">Video &amp; audio</string>
<string name="list_thumbnail_view_description">Videovoorbeeld thumbnail</string>
<string name="detail_thumbnail_view_description">Videovoorbeeld thumbnail</string>
<string name="detail_uploader_thumbnail_view_description">Uploader miniatuur</string>
<string name="detail_dislikes_img_view_description">Dislikes</string>
<string name="list_thumbnail_view_description">Videovoorbeeldminiatuur</string>
<string name="detail_thumbnail_view_description">Videovoorbeeldminiatuur</string>
<string name="detail_uploader_thumbnail_view_description">Uploaderminiatuur</string>
<string name="detail_dislikes_img_view_description">Duimpjes omlaag</string>
<string name="background_player_name">NewPipe-achtergrondspeler</string>
<string name="loading">Laden</string>
<string name="download_path_audio_title">Downloadlocatie voor audio</string>
<string name="download_path_audio_summary">Locatie om gedownloade muziek in op te slaan.</string>
<string name="download_path_audio_dialog_title">Voer downloadlocatie in voor audiobestanden.</string>
<string name="theme_title">Thema</string>
<string name="dark_theme_title">Donker</string>
<string name="light_theme_title">Licht</string>
<string name="settings_category_appearance_title">Uiterlijk</string>
<string name="settings_category_other_title">Overige</string>
<string name="background_player_playing_toast">Speelt af op achtergrond</string>
<string name="play_btn_text">Afspelen</string>
<string name="content">Inhoud</string>
<string name="show_age_restricted_content_title">Geef inhoud met leeftijdsbeperking weer</string>
<string name="video_is_age_restricted">Video heeft leeftijdsbeperking. Schakel leeftijdsbeperkte video\'s eerst in in de instellingen.</string>
<string name="general_error">Fout</string>
<string name="network_error">Netwerkfout</string>
<string name="could_not_load_thumbnails">Kon niet alle miniaturen laden</string>
<string name="youtube_signature_decryption_error">Kon video-URL-ondertekening niet ontsleutelen.</string>
<string name="parsing_error">Kon website niet verwerken.</string>
<string name="content_not_available">Inhoud niet beschikbaar.</string>
<string name="blocked_by_gema">Geblokkeerd door GEMA.</string>
<string name="could_not_setup_download_menu">Kon downloadmenu niet instellen.</string>
<string name="live_streams_not_supported">Dit is een LIVE STREAM. Deze worden nog niet ondersteund.</string>
<string name="detail_likes_img_view_description">Duimpjes omhoog</string>
<string name="use_tor_title">Gebruik Tor</string>
<string name="use_tor_summary">(Experimenteel) Dwing downloadverkeer door Tor voor verhoogde privacy (streamen van video\'s wordt nog niet ondersteund).</string>
<string name="err_dir_create">Kan downloadmap \'%1$s\' niet aanmaken</string>
<string name="info_dir_created">Downloadmap \'%1$s\' aangemaakt</string>
<string name="light_parsing_error">Kon de website niet volledig inlezen.</string>
<string name="could_not_get_stream">Kon geen streams vinden.</string>
<string name="sorry_string">Sorry, dit zou niet moeten gebeuren.</string>
<string name="error_report_button_text">Rapporteer fout via e-mail</string>
<string name="error_snackbar_message">Excuses, er waren wat fouten.</string>
<string name="error_snackbar_action">RAPPORTEER</string>
<string name="what_device_headline">Info:</string>
<string name="what_happened_headline">Wat gebeurde:</string>
<string name="info_searched_lbl">Gezocht voor:</string>
<string name="info_requested_stream_lbl">Aangevraagde stream:</string>
<string name="your_comment">Uw opmerking (in het Engels):</string>
<string name="error_details_headline">Details:</string>
<string name="use_exoplayer_summary">Experimenteel</string>
<string name="enable_background_audio">Speel op de achtergrond</string>
<string name="video">Video</string>
<string name="audio">Geluid</string>
<string name="text">Tekst</string>
<string name="logging_normal">Normaal</string>
<string name="logging_verbose">Uitgebreid</string>
<string name="retry">Opnieuw proberen</string>
<string name="off">[uit]</string>
<string name="error_drm_not_supported">Beschermde inhoud niet ondersteunt op API versies lager dan 18</string>
<string name="error_drm_unsupported_scheme">Dit apparaat ondersteunt niet het vereiste DRM-schema</string>
<string name="error_drm_unknown">Een onbekende DRM-fout heeft zich voorgedaan</string>
<string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_no_decoder">Dit apparaat heeft geen decoder voor <xliff:g id="mime_type"> 1%1$s</xliff:g> 2</string>
<string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_no_secure_decoder">Dit apparaat heeft geen veilige decoder voor<xliff:g id="mime_type"> 1%1$s</xliff:g></string>
<string name="error_querying_decoders">Kon apparaatdecoders niet ophalen</string>
</resources>

View File

@@ -1,50 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="no_player_found">Nie znaleziono odtwarzacza strumieniowego.</string>
<string name="install">Zainstaluj</string>
<string name="cancel">Anuluj</string>
<string name="open_in_browser">Otwórz w przeglądarce</string>
<string name="share">Udostępnij</string>
<string name="download">Pobierz</string>
<string name="search">Szukaj</string>
<string name="settings">Ustawienia</string>
<string name="did_you_mean">"Czy chodziło Ci o: "</string>
<string name="share_dialog_title">Udostępnij za pośrednictwem:</string>
<string name="choose_browser">Wybierz przeglądarkę:</string>
<string name="screen_rotation">obrót</string>
<string name="settings_activity_title">Ustawienia</string>
<string name="useExternalPlayerTitle">Użyj zewnętrznego odtwarzacza</string>
<string name="download_path_title">Miejsce zapisu pobieranych plików</string>
<string name="download_path_summary">Ścieżka folderu do zapisywania pobieranego wideo.</string>
<string name="download_path_dialog_title">Wprowadź ścieżkę folderu dla pobieranych plików</string>
<string name="autoplay_through_intent_title">Automatycznie odtwarzaj przez Intent</string>
<string name="autoplay_through_intent_summary">Automatycznie odtwarza wideo po wywołaniu z innej aplikacji.</string>
<string name="default_resolution_title">Domyślna rozdzielczość</string>
<string name="play_with_kodi_title">Odtwarzaj za pośrednictwem Kodi</string>
<string name="kore_not_found">Aplikacja Kore nie została znaleziona. Wymagana jest do odtwarzania w Kodi.</string>
<string name="installeKore">Zainstaluj Kore</string>
<string name="show_play_with_kodi_title">Wyświetlaj opcję \"Odtwarzaj za pośrednictwem Kodi\"</string>
<string name="show_play_with_kodi_summary">Wyświetla opcję do odtwarzania wideo przez aplikację Kodi.</string>
<string name="play_audio">Dźwięk</string>
<string name="default_audio_format_title">Domyślny format dźwięku</string>
<string name="webm_description">WebM &#8212; otwarty format</string>
<string name="m4a_description">m4a &#8212; lepsza jakość</string>
<string name="download_dialog_title">Pobierz</string>
<string name="next_video_title">Następne wideo</string>
<string name="show_next_and_similar_title">Wyświetl następne i podobne wideo</string>
<string name="url_not_supported_toast">Niewspierany URL.</string>
<string name="similar_videos_btn_text">Podobne wideo</string>
<string name="search_language_title">Preferowany język zawartości</string>
<string name="settings_category_video_audio_title">Wideo &amp; Dźwięk</string>
<string name="search_page">"Szukaj strony: "</string>
<string name="view_count_text">%1$s wyświetleń</string>
<string name="upload_date_text">Opublikowany %1$s</string>
<string name="list_thumbnail_view_description">Miniaturka podglądowa wideo</string>
<string name="detail_thumbnail_view_description">Miniaturka podglądowa wideo</string>
<string name="detail_uploader_thumbnail_view_description">Miniaturka wysyłającego</string>
<string name="detail_dislikes_img_view_description">łapek w dół</string>
<string name="detail_likes_img_view_description">Polubień</string>
<string name="use_external_video_player_title">Użyj zewnętrznego odtwarzacza wideo</string>
<string name="use_external_audio_player_title">Użyj zewnętrznego odtwarzacza audio</string>
</resources>

View File

@@ -9,7 +9,7 @@
<string name="download">Descarregar</string>
<string name="search">Pesquisar</string>
<string name="settings">Definições</string>
<string name="did_you_mean">"Será que queria dizer: "</string>
<string name="did_you_mean">Será que queria dizer: %1$s?</string>
<string name="search_page">"Página de pesquisa: "</string>
<string name="share_dialog_title">Partilhar com:</string>
<string name="choose_browser">Escolher navegador:</string>
@@ -60,7 +60,7 @@
<string name="network_error">Erro de rede</string>
<string name="use_tor_title">Utilizar Tor</string>
<string name="use_tor_summary">Utilizar a rede Tor para ter mais privacidade (ainda não é suportada a transmissão de vídeos)</string>
<string name="use_tor_summary">(Experimental) Utilizar a rede Tor para ter mais privacidade (ainda não é suportada a emissão de vídeos).</string>
<string name="download_path_audio_title">Descarga de áudio</string>
<string name="download_path_audio_summary">Local para guardar o áudio descarregado.</string>
<string name="download_path_audio_dialog_title">Digite o caminho para os ficheiros áudio.</string>
@@ -74,4 +74,44 @@
<string name="content_not_available">Conteúdo não disponível.</string>
<string name="blocked_by_gema">Bloqueado pela GEMA.</string>
</resources>
<string name="content">Conteúdo</string>
<string name="show_age_restricted_content_title">Restringir conteúdo por idade</string>
<string name="video_is_age_restricted">O vídeo está restringido por idade. Ative a restrição de vídeos por idade nas definições.</string>
<string name="light_parsing_error">Não foi possível processar o sítio web.</string>
<string name="could_not_setup_download_menu">Não foi possível configurar o menu de descargas.</string>
<string name="live_streams_not_supported">Esta é uma emissão em direto. Estas emissões ainda não são suportadas.</string>
<string name="could_not_get_stream">Não foi possível obter a emissão.</string>
<string name="sorry_string">Isto não deveria ter acontecido.</string>
<string name="error_report_button_text">Reportar erro por e-mail</string>
<string name="error_snackbar_message">Ocorreram alguns erros.</string>
<string name="error_snackbar_action">Relatório</string>
<string name="what_device_headline">Info:</string>
<string name="what_happened_headline">O que ocorreu:</string>
<string name="info_labels">O quê:\\nPedido:\\nIdioma do conteúdo:\\nServiço:\\nHora GMT:\\nVersão:\\nVersão do So:\\nGlob. IP:</string>
<string name="info_searched_lbl">Pesquisou por:</string>
<string name="info_requested_stream_lbl">Emissão solicitada:</string>
<string name="your_comment">Comentários (em Inglês):</string>
<string name="error_details_headline">Detalhes:</string>
<string name="enable_background_audio">Reproduzir em segundo plano</string>
<string name="video">Vídeo</string>
<string name="audio">Áudio</string>
<string name="text">Texto</string>
<string name="logging">Registo</string>
<string name="logging_normal">Normal</string>
<string name="logging_verbose">Detalhado</string>
<string name="retry">Tentar novamente</string>
<string name="off">[off]</string>
<string name="error_drm_not_supported">O conteúdo protegido não é permitido em API com nível inferior a 18</string>
<string name="error_drm_unsupported_scheme">Este dispositivo não tem suporte ao esquema DRM necessário</string>
<string name="error_drm_unknown">Ocorreu um erro de DRM desconhecido</string>
<string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_no_decoder">Este dispositivo não disponibiliza um descodificador para <xliff:g id="mime_type">%1$s</xliff:g></string>
<string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_no_secure_decoder">Este dispositivo não disponibiliza um descodificador seguro para <xliff:g id="mime_type">%1$s</xliff:g></string>
<string name="error_querying_decoders">Incapaz de processar os descodificadores</string>
<string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_instantiating_decoder">Incapaz de iniciar o descodificador <xliff:g id="decoder_name">%1$s</xliff:g></string>
<string name="storage_permission_denied">Não foi concedida permissão para aceder ao armazenamento</string>
<string name="use_exoplayer_title">Usar ExoPlayer</string>
<string name="use_exoplayer_summary">Experimental</string>
</resources>

View File

@@ -1,2 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<?xml version='1.0' encoding='UTF-8'?>
<resources><string name="background_player_name">Player NewPipe de fundal</string>
<string name="view_count_text">%1$s vizionări</string>
<string name="upload_date_text">Încărcat pe %1$s</string>
<string name="no_player_found">Niciun player pentru stream-uri găsit. Instalați VLC?</string>
<string name="install">Instalare</string>
<string name="cancel">Anulare</string>
<string name="open_in_browser">Deschidere în browser</string>
<string name="share">Partajează</string>
<string name="loading">Se încarcă</string>
<string name="download">Descărcare</string>
<string name="search">Căutare</string>
<string name="settings">Setări</string>
<string name="did_you_mean">"V-ați referit la: "</string>
<string name="search_page">"Pagina de căutare: "</string>
<string name="share_dialog_title">Partajează cu:</string>
<string name="choose_browser">Alegeți browser-ul:</string>
<string name="screen_rotation">rotație</string>
<string name="settings_activity_title">Setări</string>
<string name="use_external_video_player_title">Folosește un player video extern</string>
<string name="use_external_audio_player_title">Folosește un player audio extern</string>
<string name="download_path_title">Descărcați calea videoclipului</string>
<string name="download_path_summary">Locul în care se vor stoca videoclipurile descărcate.</string>
<string name="download_path_dialog_title">Introduceți locul în care se vor descărca videoclipurile</string>
<string name="download_path_audio_title">Descărcați calea audio-ului</string>
<string name="download_path_audio_summary">Locul în care se vor stoca fișierele audio descărcate.</string>
<string name="download_path_audio_dialog_title">Introduceți calea de descărcare pentru fișierele audio.</string>
<string name="autoplay_through_intent_title">Redare automată intenționată</string>
<string name="autoplay_through_intent_summary">Redați automat un videoclip atunci când este chemat din altă aplicație.</string>
<string name="default_resolution_title">Rezoluție de bază</string>
<string name="play_with_kodi_title">Redați folosind Kodi</string>
<string name="kore_not_found">Aplicația Kore nu a fost găsită. Instalați Kore?</string>
<string name="show_play_with_kodi_title">Arată opțiunea \"Redați folosind Kodi\"</string>
<string name="show_play_with_kodi_summary">Arătați o opțiune de redare a videoclipurilor via centrului media Kodi.</string>
<string name="play_audio">Audio</string>
<string name="default_audio_format_title">Format audio de bază</string>
<string name="info_dir_created">Directorul de descărcare \'%1$s\' a fost creat</string>
<string name="webm_description">WebM — format liber</string>
<string name="m4a_description">m4a — calitate superioară</string>
<string name="theme_title">Temă</string>
<string name="dark_theme_title">Întunecat</string>
<string name="light_theme_title">Luminos</string>
<string name="download_dialog_title">Descărcați</string>
<string name="next_video_title">Următorul videoclip</string>
<string name="show_next_and_similar_title">Arată videoclipurile care urmează și pe cele similare</string>
<string name="url_not_supported_toast">URL nesuportat</string>
<string name="similar_videos_btn_text">Videoclipuri similare</string>
<string name="search_language_title">Limba dorită a conținutului</string>
<string name="settings_category_video_audio_title">Video &amp; Audio</string>
<string name="settings_category_appearance_title">Aspect</string>
<string name="settings_category_other_title">Altele</string>
<string name="background_player_playing_toast">Redare în fundal</string>
<string name="play_btn_text">Redați</string>
<string name="content">Conținut</string>
<string name="show_age_restricted_content_title">Arată conținutul restricționat în funcție de vârstă</string>
<string name="video_is_age_restricted">Videoclipul este restricționat în funcție de vârstă. Activați din setări videoclipurile restricționate în funcție de vârstă.</string>
<string name="general_error">Eroare</string>
<string name="network_error">Eroare de rețea</string>
<string name="could_not_load_thumbnails">Nu s-au putut încărca toate Thumbnail-urile</string>
<string name="youtube_signature_decryption_error">Nu s-a putut decripta semnătura URL a videoclipului.</string>
<string name="parsing_error">Nu s-a putut analiza website-ul.</string>
<string name="content_not_available">Conținut indisponibil.</string>
<string name="blocked_by_gema">Blocat de către GEMA.</string>
<string name="could_not_setup_download_menu">Nu s-a putut seta meniul de descărcare.</string>
<string name="live_streams_not_supported">Acesta este un LIVE STREAM. Acestea nu sunt încă suportate.</string>
<string name="list_thumbnail_view_description">Thumbnail de previzualizare al videoclipului</string>
<string name="detail_thumbnail_view_description">Thumbnail de previzualizare al videoclipului</string>
<string name="detail_uploader_thumbnail_view_description">Thumbnail-ul pentru userpicul persoanei care a încărcat videoclipul</string>
<string name="detail_likes_img_view_description">Au apreciat</string>
<string name="detail_dislikes_img_view_description">Nu au apreciat</string>
<string name="use_tor_title">Folosiți Tor</string>
<string name="use_tor_summary">Forțați descărcarea traficului prin Tor pentru intimitate crescută (transmiterea în flux a videoclipurilor nu este deocamdată suportată).</string>
<string name="err_dir_create">Nu s-a putut crea directorul de descărcare \'%1$s\'</string>
</resources>

View File

@@ -9,7 +9,7 @@
<string name="download">Prejmi</string>
<string name="search">Poišči</string>
<string name="settings">Nastavitve</string>
<string name="did_you_mean">"Ste mislili vpisati: "</string>
<string name="did_you_mean">Ali ste mislili: %1$s?</string>
<string name="search_page">"Stran iskanja: "</string>
<string name="share_dialog_title">Omogoči souporabo z:</string>
<string name="choose_browser">Izbor brskalnika:</string>
@@ -60,7 +60,7 @@
<string name="network_error">Omrežna napaka</string>
<string name="use_tor_title">Uporabi Tor</string>
<string name="use_tor_summary">Vsili prenos prejema preko sistema Tor za povečanje zasebnosti (pretakanje videa ni še podprto)</string>
<string name="use_tor_summary">(Preizkusno) Vsili prenos prejema preko sistema Tor za povečanje zasebnosti (pretakanje videa ni še podprto).</string>
<string name="download_path_audio_title">Mapa za prejem zvočnih datotek</string>
<string name="download_path_audio_summary">Pot do mape, ki je določena za prejem zvočnih datotek.</string>
<string name="download_path_audio_dialog_title">Vpis poti za prejem zvočnih datotek</string>
@@ -77,4 +77,50 @@
<string name="could_not_setup_download_menu">Ni mogoče nastaviti menija za prejem datotek.</string>
<string name="live_streams_not_supported">To je pretok v živo. Ta vrsta še ni podprta.</string>
<string name="content">Vsebina</string>
<string name="show_age_restricted_content_title">Pokaži starostno omejeno vsebino</string>
<string name="video_is_age_restricted">Za posnetek je določena starostna omejitev ogleda. Pred ogledom je treba ustrezno nastaviti omejitve med nastavitvami.</string>
<string name="light_parsing_error">Ni mogoče dokončno razčleniti spletišča.</string>
<string name="could_not_get_stream">Ni mogoče prejeti pretoka.</string>
<string name="sorry_string">Do te napake naj ne bi prišlo.</string>
<string name="error_report_button_text">Pošlji poročilo preko elektronske pošte</string>
<string name="error_snackbar_message">Prišlo je do napak.</string>
<string name="error_snackbar_action">POROČILO</string>
<string name="what_device_headline">Podrobnosti:</string>
<string name="what_happened_headline">Kaj se je zgodilo:</string>
<string name="error_details_headline">Podrobnosi:</string>
<string name="enable_background_audio">Predvajaj v ozadju</string>
<string name="video">Video</string>
<string name="audio">Zvok</string>
<string name="text">Besedilo</string>
<string name="logging">Dnevniki</string>
<string name="logging_normal">Običajno</string>
<string name="logging_verbose">Podrobno</string>
<string name="retry">Poskusi znova</string>
<string name="off">[izklopljeno]</string>
<string name="use_exoplayer_title">Uporabi ExoPlayer</string>
<string name="use_exoplayer_summary">Preizkusne zmožnosti</string>
<string name="info_labels">Predmet:\\nZahteva:\\nJezik vsebine:\\nStoritev:\\nČas po GMT:\\nRazličica:\\nRazličica OS:\\nObseg IP:</string>
<string name="info_searched_lbl">Iskano:</string>
<string name="info_requested_stream_lbl">Zahtevan pretok:</string>
<string name="your_comment">Opomba (v angleščini):</string>
<string name="error_drm_not_supported">Zaščitena vsebina na ravneh API pod 18 ni podprta</string>
<string name="error_drm_unsupported_scheme">Ta naprava ne podpira zahtevane sheme DRM</string>
<string name="error_drm_unknown">Prišlo je do neznane napake DRM</string>
<string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_no_decoder">Na napravi ni nameščenega dekodirnika vrste <xliff:g id="mime_type">%1$s</xliff:g></string>
<string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_no_secure_decoder">Na napravi ni nameščenega varnega dekodirnika vrste <xliff:g id="mime_type">%1$s</xliff:g></string>
<string name="error_querying_decoders">Ni mogoče preiskati dekodirnikov na napravi</string>
<string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_instantiating_decoder">Ni mogoče začeti dekodirnika <xliff:g id="decoder_name">%1$s</xliff:g></string>
<string name="storage_permission_denied">Dovoljenje za dostop do shrambe je zavrnjeno</string>
<string name="autoplay_by_calling_app_title">Samodejno predvajaj vsebino, poslano iz drugega programa.</string>
<string name="autoplay_by_calling_app_summary">Samodejno predvajaj vsebino, če je NewPipe klican iz drugega programa.</string>
<string name="report_error">Pošlji poročilo o napaki</string>
<string name="user_report">Poročilo uporabnika</string>
</resources>

View File

@@ -10,7 +10,7 @@
<string name="download">Преузми</string>
<string name="search">Тражи</string>
<string name="settings">Поставке</string>
<string name="did_you_mean">Да ли сте мислили: </string>
<string name="did_you_mean">Да ли сте мислили: %1$s ?</string>
<string name="search_page">Страница претраге: </string>
<string name="share_dialog_title">Подели помоћу:</string>
<string name="choose_browser">Отвори помоћу:</string>
@@ -58,7 +58,7 @@
<string name="play_btn_text">Пусти</string>
<string name="use_tor_title">Користи Тор</string>
<string name="use_tor_summary">Принудно преусмерење саобраћаја кроз Тор за доданту приватност (токови још нису подржани)</string>
<string name="use_tor_summary">(Експериментално) Принудно преусмерење саобраћаја кроз Тор за доданту приватност (токови још нису подржани).</string>
<string name="theme_title">Тема</string>
<string name="dark_theme_title">Тамна</string>
<string name="light_theme_title">Светла</string>
@@ -79,4 +79,51 @@
<string name="content_not_available">Садржај није доступан.</string>
<string name="blocked_by_gema">Блокирала ГЕМА.</string>
<string name="content">Садржај</string>
<string name="show_age_restricted_content_title">Прикажи старосно-ограничени садржај</string>
<string name="video_is_age_restricted">Видео је старосно ограничен. Укључите премошћавање у поставкама.</string>
<string name="could_not_setup_download_menu">Не могох да поставим мени преузимања.</string>
<string name="live_streams_not_supported">Ово је ТОК. Токови још нису подржани.</string>
<string name="light_parsing_error">Не могох да рашчланим веб-сајт у целости.</string>
<string name="could_not_get_stream">Не могох да добавим ниједан ток.</string>
<string name="sorry_string">Нажалост, то не би требало да се деси.</string>
<string name="error_report_button_text">Пријавите грешку е-поштом</string>
<string name="error_snackbar_message">Нажалост, десиле су се неке грешке.</string>
<string name="error_snackbar_action">ПРИЈАВИ</string>
<string name="what_device_headline">Подаци:</string>
<string name="what_happened_headline">Шта се десило:</string>
<string name="info_labels">Шта:\\nЗахтев:\\nЈезик садржаја:\\nУслуга:\\nГМТ време:\\nИздање:\\nИздање система:\\nГлоб. ИП опсег:</string>
<string name="your_comment">Ваш коментар (на енглеском):</string>
<string name="error_details_headline">Детаљи:</string>
<string name="enable_background_audio">Пусти у позадини</string>
<string name="video">Видео</string>
<string name="audio">Аудио</string>
<string name="text">Текст</string>
<string name="logging">Записивање</string>
<string name="logging_normal">Нормално</string>
<string name="logging_verbose">Исцрпно</string>
<string name="retry">Покушај поново</string>
<string name="off">[искљ.]</string>
<string name="use_exoplayer_title">Користи ExoPlayer</string>
<string name="use_exoplayer_summary">Експериментално</string>
<string name="info_searched_lbl">Тражено:</string>
<string name="info_requested_stream_lbl">Захтевани ток:</string>
<string name="error_drm_not_supported">Заштићени садржај није подржани на издањима АПИ-ија испод 18</string>
<string name="error_drm_unsupported_scheme">Овај уређај не подржава захтевану ДРМ шему</string>
<string name="error_drm_unknown">Десила се непозната грешка ДРМ-а</string>
<string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_no_decoder">Овај уређај не обезбеђује декодер за <xliff:g id="mime_type">%1$s</xliff:g></string>
<string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_no_secure_decoder">Овај уређај не обезбеђује безбедни декодер за <xliff:g id="mime_type">%1$s</xliff:g></string>
<string name="error_querying_decoders">Не могох да потражим декодере уређаја</string>
<string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_instantiating_decoder">Не могох да покренем декодер <xliff:g id="decoder_name">%1$s</xliff:g></string>
<string name="storage_permission_denied">Дозвола за приступ складишту је одбијена</string>
<string name="report_error">Пријавите грешку</string>
<string name="user_report">Извештај корисника</string>
<string name="duration_live">уживо</string>
</resources>

View File

@@ -0,0 +1,6 @@
<resources>
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
(such as screen margins) for screens with more than 820dp of available width. This
would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
<dimen name="activity_horizontal_margin">64dp</dimen>
</resources>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The minimum subtitle font size. -->
<dimen name="subtitle_minimum_font_size">13sp</dimen>
</resources>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Video Item Search View Dimensions-->
<!-- Text Size -->
<dimen name="video_item_search_title_text_size">14sp</dimen>
@@ -35,5 +34,8 @@
<!-- Paddings & Margins -->
<dimen name="video_item_detail_like_margin">6sp</dimen>
<dimen name="video_item_detail_play_fab_margin">20dp</dimen>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
</resources>
</resources>

View File

@@ -3,6 +3,7 @@
<!-- Categories -->
<string name="settings_category_video_audio">settings_category_video_audio</string>
<string name="settings_category_appearance">settings_category_appearance</string>
<string name="settings_content_options">settings_content_options</string>
<string name="settings_category_other">settings_category_other</string>
<!-- Key values -->
<string name="download_path_key">download_path</string>
@@ -10,6 +11,7 @@
<string name="use_external_video_player_key">use_external_video_player</string>
<string name="use_external_audio_player_key">use_external_audio_player</string>
<string name="autoplay_through_intent_key">autoplay_through_intent</string>
<string name="use_exoplayer_key">use_exoplayer</string>
<string name="default_resolution_key">default_resolution_preference</string>
<string name="default_resolution_value">360p</string>
@@ -207,5 +209,6 @@
<item>日本語</item>
<item>한국어</item>
</string-array>
<string name="show_age_restricted_content">show_age_restricted_content</string>
<string name="use_tor_key">use_tor</string>
</resources>

View File

@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name" translatable="false">NewPipe</string>
<string name="background_player_name">NewPipe Background Player</string>
<string name="title_videoitem_detail" translatable="false">NewPipe</string>
@@ -15,7 +15,7 @@
<string name="download">Download</string>
<string name="search">Search</string>
<string name="settings">Settings</string>
<string name="did_you_mean">Did you mean: </string>
<string name="did_you_mean">Did you mean: %1$s ?</string>
<string name="search_page">Search page: </string>
<string name="share_dialog_title">Share with:</string>
<string name="choose_browser">Choose browser:</string>
@@ -32,8 +32,8 @@
<string name="download_path_audio_summary">Path to store downloaded audio in.</string>
<string name="download_path_audio_dialog_title">Enter download path for audio files.</string>
<string name="autoplay_through_intent_title">Autoplay through Intent</string>
<string name="autoplay_through_intent_summary">Automatically play a video when it\'s called from another app.</string>
<string name="autoplay_by_calling_app_title">Autoplay when called form another app.</string>
<string name="autoplay_by_calling_app_summary">Automatically play a video when NewPipe is called from another app.</string>
<string name="default_resolution_title">Default Resolution</string>
<string name="play_with_kodi_title">Play with Kodi</string>
<string name="kore_not_found">Kore app not found. Install Kore?</string>
@@ -74,6 +74,10 @@
<string name="background_player_playing_toast">Playing in background</string>
<string name="c3s_url" translatable="false">https://www.c3s.cc/</string>
<string name="play_btn_text">Play</string>
<string name="content">Content</string>
<string name="show_age_restricted_content_title">Show age restricted content</string>
<string name="video_is_age_restricted">Video is Age restricted. Enable age restricted videos in the settings first.</string>
<string name="duration_live">live</string>
<!-- error strings -->
<string name="general_error">Error</string>
@@ -81,9 +85,25 @@
<string name="could_not_load_thumbnails">Could not load all Thumbnails</string>
<string name="youtube_signature_decryption_error">Could not decrypt video url signature.</string>
<string name="parsing_error">Could not parse website.</string>
<string name="light_parsing_error">Could not parse website complete.</string>
<string name="content_not_available">Content not available.</string>
<string name="blocked_by_gema">Blocked by GEMA.</string>
<string name="could_not_setup_download_menu">Could not setup download menu.</string>
<string name="live_streams_not_supported">This is a LIVE STREAM. These are not yet supported.</string>
<string name="could_not_get_stream">Could not get any stream.</string>
<!-- error activity -->
<string name="sorry_string">Sorry that should not happen.</string>
<string name="guru_meditation" translatable="false">Guru Meditation.</string>
<string name="error_report_button_text">Report error via mail</string>
<string name="error_snackbar_message">Sorry some errors occurred.</string>
<string name="error_snackbar_action">REPORT</string>
<string name="what_device_headline">Info:</string>
<string name="what_happened_headline">What happened:</string>
<string name="info_labels">What:\\nRequest:\\nContent Lang:\\nService:\\nGMT Time:\\nVersion:\\nOS version:\\nGlob. IP range:</string>
<string name="info_searched_lbl">Searched for:</string>
<string name="info_requested_stream_lbl">Requested stream:</string>
<string name="your_comment">Your comment (in English):</string>
<string name="error_details_headline">Details:</string>
<!-- Content descriptions (for better accessibility) -->
@@ -93,8 +113,30 @@
<string name="detail_likes_img_view_description">Likes</string>
<string name="detail_dislikes_img_view_description">Dislikes</string>
<string name="use_tor_title">Use Tor</string>
<string name="use_tor_summary">Force download traffic through Tor for increased privacy (streaming videos not yet supported)</string>
<string name="use_tor_summary">(Experimental) Force download traffic through Tor for increased privacy (streaming videos not yet supported).</string>
<string name="report_error">Report an Error</string>
<string name="user_report">User report</string>
<string name="err_dir_create">Cannot create download directory \'%1$s\'</string>
<string name="info_dir_created">Created download directory \'%1$s\'</string>
</resources>
<string name="enable_background_audio">Play in background</string>
<string name="video">Video</string>
<string name="audio">Audio</string>
<string name="text">Text</string>
<string name="logging">Logging</string>
<string name="logging_normal">Normal</string>
<string name="logging_verbose">Verbose</string>
<string name="retry">Retry</string>
<string name="off">[off]</string>
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string>
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
<string name="error_drm_unknown">An unknown DRM error occurred</string>
<string name="error_no_decoder">This device does not provide a decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
<string name="error_no_secure_decoder">This device does not provide a secure decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
<string name="error_querying_decoders">Unable to query device decoders</string>
<string name="error_instantiating_decoder">Unable to instantiate decoder <xliff:g id="decoder_name">%1$s</xliff:g></string>
<string name="storage_permission_denied">Permission to access storage was denied</string>
<string name="use_exoplayer_title">Use ExoPlayer</string>
<string name="use_exoplayer_summary">Experimental</string>
</resources>

View File

@@ -1,4 +1,19 @@
<resources>
<style name="RootTheme" parent="android:Theme.Holo">
</style>
<style name="PlayerTheme" parent="@style/RootTheme">
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@android:color/black</item>
</style>
<style name="ExoPlayerButton">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:minWidth">40dp</item>
</style>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light">
<item name="android:actionBarStyle">@style/NewPipeActionbarTheme</item>
@@ -32,4 +47,5 @@
<item name="android:background">@color/video_overlay_color</item>
<item name="background">@color/video_overlay_color</item>
</style>
</resources>
</resources>

View File

@@ -33,6 +33,11 @@
android:entryValues="@array/audio_format_list"
android:defaultValue="@string/default_audio_format_value"/>
<CheckBoxPreference
android:key="@string/use_exoplayer_key"
android:title="@string/use_exoplayer_title"
android:summary="@string/use_exoplayer_summary"
android:defaultValue="false"/>
</PreferenceCategory>
<PreferenceCategory
android:key="@string/settings_category_appearance"
@@ -63,8 +68,8 @@
</PreferenceCategory>
<PreferenceCategory
android:key="@string/settings_category_other"
android:title="@string/settings_category_other_title"
android:key="@string/settings_content_options"
android:title="@string/content"
android:textAllCaps="true">
<ListPreference
@@ -75,6 +80,18 @@
android:entryValues="@array/language_codes"
android:defaultValue="@string/default_language_value" />
<CheckBoxPreference
android:key="@string/show_age_restricted_content"
android:title="@string/show_age_restricted_content_title"
android:defaultValue="false"/>
</PreferenceCategory>
<PreferenceCategory
android:key="@string/settings_category_other"
android:title="@string/settings_category_other_title"
android:textAllCaps="true">
<EditTextPreference
android:key="@string/download_path_key"
android:title="@string/download_path_title"
@@ -89,8 +106,8 @@
<CheckBoxPreference
android:key="@string/autoplay_through_intent_key"
android:title="@string/autoplay_through_intent_title"
android:summary="@string/autoplay_through_intent_summary"
android:title="@string/autoplay_by_calling_app_title"
android:summary="@string/autoplay_by_calling_app_summary"
android:defaultValue="false" />
<CheckBoxPreference

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B