mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-01-14 02:32:40 +00:00
Compare commits
609 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c29ea4693d | ||
|
|
7d0df7aa80 | ||
|
|
7ba71e3b37 | ||
|
|
670a95a01d | ||
|
|
acea26717c | ||
|
|
e6bcb4628a | ||
|
|
c4f08d541d | ||
|
|
58546751dd | ||
|
|
5470c9a002 | ||
|
|
8885b45259 | ||
|
|
85632b24fc | ||
|
|
96802c7b5c | ||
|
|
e5207f8b42 | ||
|
|
9d573e1b1d | ||
|
|
dd276aabc1 | ||
|
|
e4d0635ae1 | ||
|
|
60f534d7a1 | ||
|
|
223ddaa9bf | ||
|
|
1d6c722c28 | ||
|
|
9c9dd6c7bf | ||
|
|
7ff48a6d84 | ||
|
|
645e16fd90 | ||
|
|
1bb58a10e2 | ||
|
|
118788436e | ||
|
|
0b0f7919a2 | ||
|
|
c8e23fb6ce | ||
|
|
bc10717f61 | ||
|
|
e621dd3b28 | ||
|
|
20208be556 | ||
|
|
9074733aab | ||
|
|
ae0ee61e7d | ||
|
|
ac797196f5 | ||
|
|
30aa64e9c6 | ||
|
|
b992add77b | ||
|
|
88ebd963f7 | ||
|
|
96baa2978d | ||
|
|
c01609b858 | ||
|
|
c89f0e5547 | ||
|
|
10dfcbf0b9 | ||
|
|
43446d56c5 | ||
|
|
e66f2ab36b | ||
|
|
63def07a0e | ||
|
|
1ba7710af8 | ||
|
|
8f13a7ec97 | ||
|
|
93dff5cf7a | ||
|
|
6a0450b9f6 | ||
|
|
fffeadd8ea | ||
|
|
b697e058d9 | ||
|
|
14db8b1283 | ||
|
|
45ad8621cf | ||
|
|
dee3a18ea8 | ||
|
|
950cf714d9 | ||
|
|
652184506b | ||
|
|
6457cac797 | ||
|
|
f66c2ba171 | ||
|
|
6133c97f45 | ||
|
|
0dc71ce37a | ||
|
|
c96a05a8f9 | ||
|
|
c190dc4792 | ||
|
|
ebf91d27c7 | ||
|
|
63301ee771 | ||
|
|
7da827a06a | ||
|
|
00fc5217f5 | ||
|
|
04e725bb50 | ||
|
|
e6617ff8e8 | ||
|
|
1b0a958436 | ||
|
|
5053d470f6 | ||
|
|
8de5c53485 | ||
|
|
ec3ae7c7b8 | ||
|
|
c46af7d194 | ||
|
|
5254e85840 | ||
|
|
5883f6e763 | ||
|
|
c02383d7d9 | ||
|
|
f98e5cc22d | ||
|
|
9fbb61a744 | ||
|
|
5191907af0 | ||
|
|
35a69b4b1d | ||
|
|
a64f520644 | ||
|
|
5aced46345 | ||
|
|
3cd485069d | ||
|
|
fabb07bb28 | ||
|
|
2328ea6d07 | ||
|
|
0375194e7d | ||
|
|
5a6a6bcc78 | ||
|
|
c8f475bba1 | ||
|
|
31f3757880 | ||
|
|
a60a9bb144 | ||
|
|
21a90bb7ee | ||
|
|
2f66913813 | ||
|
|
d9b042d9e3 | ||
|
|
ef9044d933 | ||
|
|
12c9dbf1bf | ||
|
|
8cc8aa8693 | ||
|
|
e529b16956 | ||
|
|
ffe8d4b689 | ||
|
|
4e5a20ec45 | ||
|
|
7c9ef58acd | ||
|
|
d076fe72cd | ||
|
|
25fbbfaf94 | ||
|
|
9df27f43de | ||
|
|
759e9846ad | ||
|
|
3aeba7ca8a | ||
|
|
2a44a091c8 | ||
|
|
4c92aebc3c | ||
|
|
d4ecd0dfab | ||
|
|
3f790d01fa | ||
|
|
e7b068ed8e | ||
|
|
bd485937c4 | ||
|
|
8edc332a4e | ||
|
|
bb5028364b | ||
|
|
ef070a4e0e | ||
|
|
6787d0224c | ||
|
|
8a43e24095 | ||
|
|
f879f549e4 | ||
|
|
db55484163 | ||
|
|
4d8f66f28e | ||
|
|
7a44061fa3 | ||
|
|
5d4bb42e39 | ||
|
|
3a6c22da5c | ||
|
|
064f0e414a | ||
|
|
77db3cb6fa | ||
|
|
b83a1fd102 | ||
|
|
99c519c065 | ||
|
|
632e52b38d | ||
|
|
1b66ffac6c | ||
|
|
ee9052ad3d | ||
|
|
550c74da77 | ||
|
|
ccdd450283 | ||
|
|
224a607bc3 | ||
|
|
8fcd23663c | ||
|
|
ad79a71fbd | ||
|
|
d862a59349 | ||
|
|
2d6362dddb | ||
|
|
ee3ec3a4ea | ||
|
|
daecfd97c2 | ||
|
|
200a81d536 | ||
|
|
8059ac89d3 | ||
|
|
60f5f07dd6 | ||
|
|
372d5ce413 | ||
|
|
6f97819ca7 | ||
|
|
2b2ee56712 | ||
|
|
3715326034 | ||
|
|
6cbb8b1753 | ||
|
|
806896ea05 | ||
|
|
fc8746e077 | ||
|
|
e11df5bb49 | ||
|
|
37a9e98ebc | ||
|
|
80b4975188 | ||
|
|
c4ef40f4dc | ||
|
|
6a4bb6e3e1 | ||
|
|
05ef926a7f | ||
|
|
0007451735 | ||
|
|
e599de038a | ||
|
|
61472a995f | ||
|
|
2a41802f36 | ||
|
|
1d1cee17c3 | ||
|
|
e99266f9d8 | ||
|
|
1f2cd064f7 | ||
|
|
9c2cf9eef7 | ||
|
|
38b0b79644 | ||
|
|
5252834075 | ||
|
|
162df5eb6c | ||
|
|
ac5e2e0532 | ||
|
|
f0ba6afbdf | ||
|
|
04a5f43472 | ||
|
|
a15ef4b7ce | ||
|
|
f1f9147433 | ||
|
|
3c0d7de377 | ||
|
|
2a57d74f1a | ||
|
|
79717859b3 | ||
|
|
be423939ed | ||
|
|
086cceb271 | ||
|
|
a14033afb7 | ||
|
|
cc89a342ed | ||
|
|
25c3669564 | ||
|
|
21eff0b2ec | ||
|
|
472fd72c82 | ||
|
|
429a9a42d3 | ||
|
|
90c525e99a | ||
|
|
196117998a | ||
|
|
630cbc77a8 | ||
|
|
b6e4afe321 | ||
|
|
59085ff8c8 | ||
|
|
a4274c6301 | ||
|
|
b4ef44b343 | ||
|
|
08bc97582b | ||
|
|
3952c88510 | ||
|
|
b1f27b9da7 | ||
|
|
171b258d5c | ||
|
|
af971b6a19 | ||
|
|
7ca026393b | ||
|
|
f8784ae3c7 | ||
|
|
3ddc3acf4c | ||
|
|
ff430f5e33 | ||
|
|
daf2890161 | ||
|
|
4ca639323d | ||
|
|
a92bf155a3 | ||
|
|
d153772eb2 | ||
|
|
cdc8fe86ce | ||
|
|
4844037ce9 | ||
|
|
691c1e1a37 | ||
|
|
bd55b91a86 | ||
|
|
8842f53696 | ||
|
|
ffed9f6116 | ||
|
|
50e8f45601 | ||
|
|
8cbfe9e6cf | ||
|
|
99ad3dc292 | ||
|
|
6908355d38 | ||
|
|
7b948f83c3 | ||
|
|
34b2b96158 | ||
|
|
d1573a0a6e | ||
|
|
16d6bda85d | ||
|
|
4b3eb2ece5 | ||
|
|
1089de6321 | ||
|
|
d00dc798f4 | ||
|
|
f6b32823ba | ||
|
|
9e34fee58c | ||
|
|
1684a2110c | ||
|
|
5e00e34552 | ||
|
|
ce204eba62 | ||
|
|
c7cb652322 | ||
|
|
f8ccc3128e | ||
|
|
4a8baaef45 | ||
|
|
a9f3939c83 | ||
|
|
d8cb950248 | ||
|
|
aefc51db4b | ||
|
|
fb18ea7ff8 | ||
|
|
407c61e212 | ||
|
|
d8e6ad48ca | ||
|
|
bd42f4188f | ||
|
|
f766f383ea | ||
|
|
1a9922d790 | ||
|
|
6213623431 | ||
|
|
2809ee7a3e | ||
|
|
281cae7a18 | ||
|
|
1670751c94 | ||
|
|
e1ead9d2ef | ||
|
|
359a9a96d6 | ||
|
|
b6cfb8a3dc | ||
|
|
6f028ecb19 | ||
|
|
8695466690 | ||
|
|
bdb1be9967 | ||
|
|
9395df4cc3 | ||
|
|
93edb333d4 | ||
|
|
30eeef46c2 | ||
|
|
8b584f3922 | ||
|
|
0c354c4fdb | ||
|
|
bd7413119a | ||
|
|
f26915aab6 | ||
|
|
f6068dc69d | ||
|
|
bd2c65cd94 | ||
|
|
4777910644 | ||
|
|
66b26e52ce | ||
|
|
a758267d72 | ||
|
|
72eae64698 | ||
|
|
d63c18f7f0 | ||
|
|
b05e3ca8d8 | ||
|
|
0a88100b67 | ||
|
|
43b859f778 | ||
|
|
d1bd7f695f | ||
|
|
312e1378d3 | ||
|
|
55d6825f63 | ||
|
|
e4f9af7076 | ||
|
|
ff52fe4884 | ||
|
|
0ab29b7c1f | ||
|
|
d742ed7b65 | ||
|
|
a684e380b7 | ||
|
|
c90feaf3db | ||
|
|
460610f672 | ||
|
|
46511533aa | ||
|
|
40efed6580 | ||
|
|
78b1c1eb4a | ||
|
|
5a59a3dd50 | ||
|
|
0763280196 | ||
|
|
93f2518159 | ||
|
|
273f731dd5 | ||
|
|
c7cd9e86ac | ||
|
|
41fb6f5464 | ||
|
|
03b1a8bd41 | ||
|
|
e7d0685ebc | ||
|
|
aadbfe1eed | ||
|
|
4a54fbb872 | ||
|
|
8b1836d699 | ||
|
|
c47d4fd35a | ||
|
|
a58af1275c | ||
|
|
1d63b39553 | ||
|
|
c300d52b29 | ||
|
|
5314e275bc | ||
|
|
08f8b9770a | ||
|
|
a9c64b2fec | ||
|
|
1edfa78a05 | ||
|
|
1df8af35d4 | ||
|
|
f47c5e53b1 | ||
|
|
a48cbc6971 | ||
|
|
73be8cf074 | ||
|
|
002a1412cb | ||
|
|
049cd2d236 | ||
|
|
e99714eba6 | ||
|
|
4e1423d224 | ||
|
|
4751075e87 | ||
|
|
416e0fb609 | ||
|
|
f96a371464 | ||
|
|
e423192265 | ||
|
|
e9444e058c | ||
|
|
037632fbf0 | ||
|
|
6cdea85a49 | ||
|
|
f86d755890 | ||
|
|
98cc97251a | ||
|
|
562f414e3e | ||
|
|
1afc301432 | ||
|
|
115b44585b | ||
|
|
3ff47623d5 | ||
|
|
943e03f9d8 | ||
|
|
f7a534a0d0 | ||
|
|
72e30d8e40 | ||
|
|
eb265300fc | ||
|
|
704b8f61dd | ||
|
|
40957c445f | ||
|
|
8eead9fda2 | ||
|
|
aadc8168be | ||
|
|
cb33f04bfc | ||
|
|
82cb71bf3f | ||
|
|
c37b88a239 | ||
|
|
de59bf695d | ||
|
|
592627b013 | ||
|
|
9ed1fb2588 | ||
|
|
8232a92653 | ||
|
|
09eeaa92d1 | ||
|
|
1e4b1a2c70 | ||
|
|
0a1e7a7c86 | ||
|
|
e5f3b2bf6e | ||
|
|
29183c10ff | ||
|
|
46b8bdace7 | ||
|
|
9b6924ec9f | ||
|
|
c69de107e5 | ||
|
|
c4d451e420 | ||
|
|
be6bce5771 | ||
|
|
9bcccc87e6 | ||
|
|
bf845be727 | ||
|
|
e92a5414d1 | ||
|
|
657125f43c | ||
|
|
e98f68e93b | ||
|
|
bef84e9eec | ||
|
|
6a5f2402c7 | ||
|
|
d6cc6ba144 | ||
|
|
07f8dcb3ca | ||
|
|
a026143a84 | ||
|
|
73a5b6738d | ||
|
|
3e2b12ae4a | ||
|
|
8073364b7a | ||
|
|
b141d96e7c | ||
|
|
a86e8b98fe | ||
|
|
a7d77716f3 | ||
|
|
b017e439b1 | ||
|
|
a7156f665a | ||
|
|
44e34d084e | ||
|
|
065820ffa4 | ||
|
|
26991928ae | ||
|
|
f4fa68c390 | ||
|
|
8b86f9ea6d | ||
|
|
48f966e7db | ||
|
|
d8746dc592 | ||
|
|
6421d3017e | ||
|
|
f9e771f8f7 | ||
|
|
09456ce421 | ||
|
|
716f7e722b | ||
|
|
ffa4b1483f | ||
|
|
76f7165462 | ||
|
|
fdf0d8e9c8 | ||
|
|
6651aa924f | ||
|
|
369fd95e2b | ||
|
|
e242adec66 | ||
|
|
58e562f7d4 | ||
|
|
70238fd773 | ||
|
|
fc4e007cc4 | ||
|
|
b7667ce97a | ||
|
|
1315da0da7 | ||
|
|
c4d8eae547 | ||
|
|
b01fc1be62 | ||
|
|
c46e3cf5cb | ||
|
|
1a6b915112 | ||
|
|
2f38943488 | ||
|
|
06711dc6c3 | ||
|
|
cc7e342ab7 | ||
|
|
5b64743987 | ||
|
|
a84ad031d9 | ||
|
|
c4c2fe2a9c | ||
|
|
8ccaef454c | ||
|
|
27579dff37 | ||
|
|
389d08c233 | ||
|
|
5412a087fe | ||
|
|
dd0f3ac651 | ||
|
|
4d4d776e4d | ||
|
|
7877b107c1 | ||
|
|
a2aa0aa9a8 | ||
|
|
b3475d30c0 | ||
|
|
6484c8d636 | ||
|
|
587cf554f2 | ||
|
|
1d4e4eb6b3 | ||
|
|
31c4ed7d0e | ||
|
|
7f246b2d3d | ||
|
|
7d68cff700 | ||
|
|
4d80bdcc9f | ||
|
|
8b4a94e5aa | ||
|
|
111ad14ad3 | ||
|
|
aec3f19d23 | ||
|
|
d8b80f961a | ||
|
|
891bb7fa40 | ||
|
|
0e3af45466 | ||
|
|
6aebbc3109 | ||
|
|
15eb7f3186 | ||
|
|
fb4cd98014 | ||
|
|
eb692dea59 | ||
|
|
d8039fb542 | ||
|
|
5e06d19d77 | ||
|
|
963f390336 | ||
|
|
2309e15261 | ||
|
|
8491035b2f | ||
|
|
4d4107aefc | ||
|
|
67d2b9131e | ||
|
|
da8644168c | ||
|
|
c0004e988a | ||
|
|
b9187445e0 | ||
|
|
554ebf7ab9 | ||
|
|
3e54cd7284 | ||
|
|
a7afc23a9a | ||
|
|
fa3a047519 | ||
|
|
f24fab0fa2 | ||
|
|
261620fd13 | ||
|
|
9af6effad9 | ||
|
|
92602916dd | ||
|
|
f4d215664e | ||
|
|
84894a557a | ||
|
|
ffad6e4c61 | ||
|
|
93b266e6be | ||
|
|
f92ea28581 | ||
|
|
85213e4b6c | ||
|
|
4e46119e18 | ||
|
|
fdc6e9f1c3 | ||
|
|
cb20f1e212 | ||
|
|
e3fccd7125 | ||
|
|
15142c1ec3 | ||
|
|
6c7b90e1c3 | ||
|
|
d8e57144f7 | ||
|
|
49fe8a427a | ||
|
|
4587428d13 | ||
|
|
5318e77035 | ||
|
|
e69c45a246 | ||
|
|
3f87a6d714 | ||
|
|
841124b1f3 | ||
|
|
257a878ef4 | ||
|
|
07d3f82912 | ||
|
|
7d3eb4f5a6 | ||
|
|
0efcc55373 | ||
|
|
eafceb8a6c | ||
|
|
5ac8318e71 | ||
|
|
7981f9dc91 | ||
|
|
a9bf04cbc6 | ||
|
|
4b5591d884 | ||
|
|
c08197f025 | ||
|
|
5d22eabfa3 | ||
|
|
c3a38e384a | ||
|
|
0d3dd94559 | ||
|
|
9cdaa37519 | ||
|
|
202a319d69 | ||
|
|
198384b2ed | ||
|
|
9d8b070f1e | ||
|
|
28ba2d5008 | ||
|
|
37ddd63d27 | ||
|
|
d7f8b8c1e0 | ||
|
|
9e70c5bbea | ||
|
|
4dd572063e | ||
|
|
db9cf95648 | ||
|
|
df6bae4712 | ||
|
|
56cb8209b8 | ||
|
|
aba9c2e113 | ||
|
|
3d25008739 | ||
|
|
9437f057d0 | ||
|
|
917d6089a7 | ||
|
|
f19e99e3ad | ||
|
|
2d0fb05fa6 | ||
|
|
c617f2dfad | ||
|
|
747c5fc89a | ||
|
|
4f662ef203 | ||
|
|
eb274ad88f | ||
|
|
bd7b41be9b | ||
|
|
076720bfff | ||
|
|
2aadae407e | ||
|
|
b16bb07774 | ||
|
|
81dd083388 | ||
|
|
92d4cef1e2 | ||
|
|
0cb5197ccf | ||
|
|
d2cd79dcf5 | ||
|
|
0ef8b391e4 | ||
|
|
0fbd105977 | ||
|
|
5c23af541e | ||
|
|
db021ff78b | ||
|
|
31294c6f54 | ||
|
|
03d3495759 | ||
|
|
4644e105a8 | ||
|
|
af5002508d | ||
|
|
9416d2b9c7 | ||
|
|
f60f5d5a6c | ||
|
|
26d00f87a8 | ||
|
|
6c33ea423c | ||
|
|
ad354aca4e | ||
|
|
ce6a5ebf0a | ||
|
|
d66ef38e8d | ||
|
|
1b6c49f621 | ||
|
|
0262c83815 | ||
|
|
c212934130 | ||
|
|
ac30e47e59 | ||
|
|
d640057453 | ||
|
|
0cea136d4d | ||
|
|
9b3d93e734 | ||
|
|
ca3782ac62 | ||
|
|
dc9ffb7fd5 | ||
|
|
b21b3a55fe | ||
|
|
e62bd85a7a | ||
|
|
fed221b008 | ||
|
|
935298d159 | ||
|
|
909919250c | ||
|
|
d24f278f7c | ||
|
|
ca408a495c | ||
|
|
ee99719137 | ||
|
|
5fb0729eae | ||
|
|
a1db3187cd | ||
|
|
488a3cf1ff | ||
|
|
1253a620a4 | ||
|
|
64547fc4a7 | ||
|
|
d643d140cf | ||
|
|
222e2e3242 | ||
|
|
ea7b6daf26 | ||
|
|
14cfab495d | ||
|
|
ed4b4a1a3c | ||
|
|
511c552188 | ||
|
|
8c650219eb | ||
|
|
189b779abe | ||
|
|
c26074b184 | ||
|
|
6c70779ff1 | ||
|
|
73897481ac | ||
|
|
a62c523ed5 | ||
|
|
0f46f57b52 | ||
|
|
bc4d65a20c | ||
|
|
f1e9a44ad9 | ||
|
|
61972c4719 | ||
|
|
9183c9c220 | ||
|
|
1ce59f13ff | ||
|
|
1bac5db6d5 | ||
|
|
58f2c4d8e6 | ||
|
|
2c2c61b2fc | ||
|
|
98b04a4a83 | ||
|
|
3ac64c44f4 | ||
|
|
3375847302 | ||
|
|
14043c86f5 | ||
|
|
030117780a | ||
|
|
971c9fe5a1 | ||
|
|
77c6d3d576 | ||
|
|
6edbfe2a6f | ||
|
|
d0a3125df4 | ||
|
|
d8c76d4c21 | ||
|
|
a1cc0897df | ||
|
|
e88a90f242 | ||
|
|
1ae54f6f8c | ||
|
|
8a4cb484fa | ||
|
|
93defbf341 | ||
|
|
ca1089da9a | ||
|
|
dd07c7db0e | ||
|
|
dfe932e37a | ||
|
|
3ac4899b96 | ||
|
|
86d619be30 | ||
|
|
fec7672598 | ||
|
|
c0aca723da | ||
|
|
28e5ee51ec | ||
|
|
7ab10aeb80 | ||
|
|
9316962e47 | ||
|
|
505c528194 | ||
|
|
559c397b2f | ||
|
|
646698f1ed | ||
|
|
c9b938ae55 | ||
|
|
e4409e8ea4 | ||
|
|
990c220fa0 | ||
|
|
f19cfb75e6 | ||
|
|
ceaacc771d | ||
|
|
48067e3285 | ||
|
|
222c8fdb62 | ||
|
|
1a62b9a161 | ||
|
|
ccb9bceecc | ||
|
|
c1a67ff1f8 | ||
|
|
ff90f257cc | ||
|
|
9b84046865 | ||
|
|
5ac17ee6b8 | ||
|
|
fa5896ee5b | ||
|
|
9fc38b5bb8 | ||
|
|
cf3e53eb71 | ||
|
|
b8865e925d | ||
|
|
2e9a860aaa | ||
|
|
e4bef056e6 | ||
|
|
803b8eab28 | ||
|
|
08bbfc50ee | ||
|
|
4e6722f201 | ||
|
|
a29e2116a7 | ||
|
|
515be677a9 | ||
|
|
d694c5f511 | ||
|
|
66c753f3a3 | ||
|
|
7047b62442 | ||
|
|
5cc73555cd | ||
|
|
219922cd82 | ||
|
|
08127e5806 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
liberapay: TeamNewPipe
|
||||
15
README.md
15
README.md
@@ -16,7 +16,9 @@
|
||||
<p align="center"><a href="https://newpipe.schabi.org">Website</a> • <a href="https://newpipe.schabi.org/blog/">Blog</a> • <a href="https://newpipe.schabi.org/press/">Press</a></p>
|
||||
<hr>
|
||||
|
||||
<b>WARNING: PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS.</b>
|
||||
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
|
||||
|
||||
<b>PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS.</b>
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -64,15 +66,22 @@ NewPipe does not use any Google framework libraries, nor the YouTube API. Websit
|
||||
* Enqueue videos
|
||||
* Local playlists
|
||||
* Subtitles
|
||||
* Multi-service support (e.g. SoundCloud \[beta\])
|
||||
* Livestream support
|
||||
* Show comments
|
||||
|
||||
### Coming Features
|
||||
|
||||
* Cast to UPnP and Cast
|
||||
* Show comments
|
||||
* … and many more
|
||||
|
||||
### Supported Services
|
||||
|
||||
NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/documentation/) provide more info on how a new service can be added to the app and the extractor. Please get in touch with us if you intend to add a new one. Currently supported services are:
|
||||
|
||||
* YouTube
|
||||
* SoundCloud \[beta\]
|
||||
* media.ccc.de \[beta\]
|
||||
|
||||
## Updates
|
||||
When a change to the NewPipe code occurs (due to either adding features or bug fixing), eventually a release will occur. These are in the format x.xx.x . In order to get this new version, you can:
|
||||
* Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
|
||||
|
||||
@@ -8,8 +8,8 @@ android {
|
||||
applicationId "org.schabi.newpipe"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
versionCode 71
|
||||
versionName "0.15.1"
|
||||
versionCode 770
|
||||
versionName "0.17.2"
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
@@ -44,10 +44,10 @@ android {
|
||||
|
||||
ext {
|
||||
supportLibVersion = '28.0.0'
|
||||
exoPlayerLibVersion = '2.8.4' //2.9.0
|
||||
exoPlayerLibVersion = '2.9.6'
|
||||
roomDbLibVersion = '1.1.1'
|
||||
leakCanaryLibVersion = '1.5.4' //1.6.1
|
||||
okHttpLibVersion = '3.11.0'
|
||||
okHttpLibVersion = '3.12.1'
|
||||
icepickLibVersion = '3.2.0'
|
||||
stethoLibVersion = '1.5.0'
|
||||
}
|
||||
@@ -57,7 +57,7 @@ dependencies {
|
||||
exclude module: 'support-annotations'
|
||||
})
|
||||
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:f7c7b9df1a'
|
||||
implementation 'com.github.teamnewpipe:NewPipeExtractor:ec3554a2ea6aa20'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.mockito:mockito-core:2.23.0'
|
||||
|
||||
@@ -35,12 +35,6 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name=".player.old.PlayVideoActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:theme="@style/OldVideoPlayerTheme"
|
||||
tools:ignore="UnusedAttribute"/>
|
||||
|
||||
<service
|
||||
android:name=".player.BackgroundPlayer"
|
||||
android:exported="false">
|
||||
@@ -183,6 +177,19 @@
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="www.youtube-nocookie.com"/>
|
||||
<data android:pathPrefix="/embed/"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="vnd.youtube"/>
|
||||
<data android:scheme="vnd.youtube.launch"/>
|
||||
</intent-filter>
|
||||
@@ -209,6 +216,29 @@
|
||||
<data android:pathPrefix="/user/"/>
|
||||
</intent-filter>
|
||||
|
||||
<!-- Invidious filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="invidio.us"/>
|
||||
<data android:host="www.invidio.us"/>
|
||||
<!-- video prefix -->
|
||||
<data android:pathPrefix="/embed/"/>
|
||||
<data android:pathPrefix="/watch"/>
|
||||
<!-- channel prefix -->
|
||||
<data android:pathPrefix="/channel/"/>
|
||||
<data android:pathPrefix="/user/"/>
|
||||
<!-- playlist prefix -->
|
||||
<data android:pathPrefix="/playlist"/>
|
||||
</intent-filter>
|
||||
|
||||
<!-- Soundcloud filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package android.support.design.widget;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.design.animation.AnimationUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
// check this https://github.com/ToDou/appbarlayout-spring-behavior/blob/master/appbarspring/src/main/java/android/support/design/widget/AppBarFlingFixBehavior.java
|
||||
public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||
|
||||
private ValueAnimator mOffsetAnimator;
|
||||
private static final int MAX_OFFSET_ANIMATION_DURATION = 600; // ms
|
||||
|
||||
public FlingBehavior() {
|
||||
}
|
||||
|
||||
public FlingBehavior(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) {
|
||||
if (dy != 0) {
|
||||
int val = child.getBottom();
|
||||
if (val != 0) {
|
||||
int min, max;
|
||||
if (dy < 0) {
|
||||
// We're scrolling down
|
||||
} else {
|
||||
// We're scrolling up
|
||||
if (mOffsetAnimator != null && mOffsetAnimator.isRunning()) {
|
||||
mOffsetAnimator.cancel();
|
||||
}
|
||||
min = -child.getUpNestedPreScrollRange();
|
||||
max = 0;
|
||||
consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull AppBarLayout child, @NonNull View target, float velocityX, float velocityY) {
|
||||
|
||||
if (velocityY != 0) {
|
||||
if (velocityY < 0) {
|
||||
// We're flinging down
|
||||
int val = child.getBottom();
|
||||
if (val != 0) {
|
||||
final int targetScroll =
|
||||
+child.getDownNestedPreScrollRange();
|
||||
animateOffsetTo(coordinatorLayout, child, targetScroll, velocityY);
|
||||
}
|
||||
|
||||
} else {
|
||||
// We're flinging up
|
||||
int val = child.getBottom();
|
||||
if (val != 0) {
|
||||
final int targetScroll = -child.getUpNestedPreScrollRange();
|
||||
if (getTopBottomOffsetForScrollingSibling() > targetScroll) {
|
||||
animateOffsetTo(coordinatorLayout, child, targetScroll, velocityY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
|
||||
}
|
||||
|
||||
private void animateOffsetTo(final CoordinatorLayout coordinatorLayout,
|
||||
final AppBarLayout child, final int offset, float velocity) {
|
||||
final int distance = Math.abs(getTopBottomOffsetForScrollingSibling() - offset);
|
||||
|
||||
final int duration;
|
||||
velocity = Math.abs(velocity);
|
||||
if (velocity > 0) {
|
||||
duration = 3 * Math.round(1000 * (distance / velocity));
|
||||
} else {
|
||||
final float distanceRatio = (float) distance / child.getHeight();
|
||||
duration = (int) ((distanceRatio + 1) * 150);
|
||||
}
|
||||
|
||||
animateOffsetWithDuration(coordinatorLayout, child, offset, duration);
|
||||
}
|
||||
|
||||
private void animateOffsetWithDuration(final CoordinatorLayout coordinatorLayout,
|
||||
final AppBarLayout child, final int offset, final int duration) {
|
||||
final int currentOffset = getTopBottomOffsetForScrollingSibling();
|
||||
if (currentOffset == offset) {
|
||||
if (mOffsetAnimator != null && mOffsetAnimator.isRunning()) {
|
||||
mOffsetAnimator.cancel();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mOffsetAnimator == null) {
|
||||
mOffsetAnimator = new ValueAnimator();
|
||||
mOffsetAnimator.setInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR);
|
||||
mOffsetAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animator) {
|
||||
setHeaderTopBottomOffset(coordinatorLayout, child,
|
||||
(Integer) animator.getAnimatedValue());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mOffsetAnimator.cancel();
|
||||
}
|
||||
|
||||
mOffsetAnimator.setDuration(Math.min(duration, MAX_OFFSET_ANIMATION_DURATION));
|
||||
mOffsetAnimator.setIntValues(currentOffset, offset);
|
||||
mOffsetAnimator.start();
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,19 @@ package org.schabi.newpipe;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -45,6 +48,8 @@ import okhttp3.Response;
|
||||
*/
|
||||
public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
|
||||
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final String TAG = CheckForNewAppVersionTask.class.getSimpleName();
|
||||
private static final Application app = App.getApp();
|
||||
private static final String GITHUB_APK_SHA1 = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
|
||||
private static final String newPipeApiUrl = "https://newpipe.schabi.org/api/data.json";
|
||||
@@ -68,6 +73,8 @@ public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
|
||||
|
||||
@Override
|
||||
protected String doInBackground(Void... voids) {
|
||||
|
||||
if(isCancelled() || !isConnected()) return null;
|
||||
|
||||
// Make a network request to get latest NewPipe data.
|
||||
if (client == null) {
|
||||
@@ -86,9 +93,8 @@ public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
|
||||
Response response = client.newCall(request).execute();
|
||||
return response.body().string();
|
||||
} catch (IOException ex) {
|
||||
ErrorActivity.reportError(app, ex, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"app update API fail", R.string.app_ui_crash));
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex));
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -113,9 +119,8 @@ public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
|
||||
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode);
|
||||
|
||||
} catch (JSONException ex) {
|
||||
ErrorActivity.reportError(app, ex, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"could not parse app update JSON data", R.string.app_ui_crash));
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,4 +232,12 @@ public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
|
||||
|
||||
return getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1);
|
||||
}
|
||||
|
||||
private boolean isConnected() {
|
||||
|
||||
ConnectivityManager cm =
|
||||
(ConnectivityManager) app.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
return cm.getActiveNetworkInfo() != null
|
||||
&& cm.getActiveNetworkInfo().isConnected();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,24 @@ package org.schabi.newpipe;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.schabi.newpipe.extractor.DownloadRequest;
|
||||
import org.schabi.newpipe.extractor.DownloadResponse;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.utils.Localization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
@@ -139,13 +145,16 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||
|
||||
private ResponseBody getBody(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
|
||||
final Request.Builder requestBuilder = new Request.Builder()
|
||||
.method("GET", null).url(siteUrl)
|
||||
.addHeader("User-Agent", USER_AGENT);
|
||||
.method("GET", null).url(siteUrl);
|
||||
|
||||
for (Map.Entry<String, String> header : customProperties.entrySet()) {
|
||||
requestBuilder.addHeader(header.getKey(), header.getValue());
|
||||
}
|
||||
|
||||
if (!customProperties.containsKey("User-Agent")) {
|
||||
requestBuilder.header("User-Agent", USER_AGENT);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(mCookies)) {
|
||||
requestBuilder.addHeader("Cookie", mCookies);
|
||||
}
|
||||
@@ -155,7 +164,7 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||
final ResponseBody body = response.body();
|
||||
|
||||
if (response.code() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested");
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
@@ -177,4 +186,96 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||
public String download(String siteUrl) throws IOException, ReCaptchaException {
|
||||
return download(siteUrl, Collections.emptyMap());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public DownloadResponse get(String siteUrl, DownloadRequest request) throws IOException, ReCaptchaException {
|
||||
final Request.Builder requestBuilder = new Request.Builder()
|
||||
.method("GET", null).url(siteUrl);
|
||||
|
||||
Map<String, List<String>> requestHeaders = request.getRequestHeaders();
|
||||
// set custom headers in request
|
||||
for (Map.Entry<String, List<String>> pair : requestHeaders.entrySet()) {
|
||||
for(String value : pair.getValue()){
|
||||
requestBuilder.addHeader(pair.getKey(), value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!requestHeaders.containsKey("User-Agent")) {
|
||||
requestBuilder.header("User-Agent", USER_AGENT);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(mCookies)) {
|
||||
requestBuilder.addHeader("Cookie", mCookies);
|
||||
}
|
||||
|
||||
final Request okRequest = requestBuilder.build();
|
||||
final Response response = client.newCall(okRequest).execute();
|
||||
final ResponseBody body = response.body();
|
||||
|
||||
if (response.code() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
response.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DownloadResponse(body.string(), response.headers().toMultimap());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadResponse get(String siteUrl) throws IOException, ReCaptchaException {
|
||||
return get(siteUrl, DownloadRequest.emptyRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadResponse post(String siteUrl, DownloadRequest request) throws IOException, ReCaptchaException {
|
||||
|
||||
Map<String, List<String>> requestHeaders = request.getRequestHeaders();
|
||||
if(null == requestHeaders.get("Content-Type") || requestHeaders.get("Content-Type").isEmpty()){
|
||||
// content type header is required. maybe throw an exception here
|
||||
return null;
|
||||
}
|
||||
|
||||
String contentType = requestHeaders.get("Content-Type").get(0);
|
||||
|
||||
RequestBody okRequestBody = null;
|
||||
if(null != request.getRequestBody()){
|
||||
okRequestBody = RequestBody.create(MediaType.parse(contentType), request.getRequestBody());
|
||||
}
|
||||
final Request.Builder requestBuilder = new Request.Builder()
|
||||
.method("POST", okRequestBody).url(siteUrl);
|
||||
|
||||
// set custom headers in request
|
||||
for (Map.Entry<String, List<String>> pair : requestHeaders.entrySet()) {
|
||||
for(String value : pair.getValue()){
|
||||
requestBuilder.addHeader(pair.getKey(), value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!requestHeaders.containsKey("User-Agent")) {
|
||||
requestBuilder.header("User-Agent", USER_AGENT);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(mCookies)) {
|
||||
requestBuilder.addHeader("Cookie", mCookies);
|
||||
}
|
||||
|
||||
final Request okRequest = requestBuilder.build();
|
||||
final Response response = client.newCall(okRequest).execute();
|
||||
final ResponseBody body = response.body();
|
||||
|
||||
if (response.code() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
response.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DownloadResponse(body.string(), response.headers().toMultimap());
|
||||
}
|
||||
}
|
||||
@@ -37,15 +37,24 @@ import android.webkit.WebViewClient;
|
||||
*/
|
||||
public class ReCaptchaActivity extends AppCompatActivity {
|
||||
public static final int RECAPTCHA_REQUEST = 10;
|
||||
public static final String RECAPTCHA_URL_EXTRA = "recaptcha_url_extra";
|
||||
|
||||
public static final String TAG = ReCaptchaActivity.class.toString();
|
||||
public static final String YT_URL = "https://www.youtube.com";
|
||||
|
||||
private String url;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_recaptcha);
|
||||
|
||||
url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA);
|
||||
if (url == null || url.isEmpty()) {
|
||||
url = YT_URL;
|
||||
}
|
||||
|
||||
|
||||
// Set return to Cancel by default
|
||||
setResult(RESULT_CANCELED);
|
||||
|
||||
@@ -73,15 +82,12 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
myWebView.clearHistory();
|
||||
android.webkit.CookieManager cookieManager = CookieManager.getInstance();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
cookieManager.removeAllCookies(new ValueCallback<Boolean>() {
|
||||
@Override
|
||||
public void onReceiveValue(Boolean aBoolean) {}
|
||||
});
|
||||
cookieManager.removeAllCookies(aBoolean -> {});
|
||||
} else {
|
||||
cookieManager.removeAllCookie();
|
||||
}
|
||||
|
||||
myWebView.loadUrl(YT_URL);
|
||||
myWebView.loadUrl(url);
|
||||
}
|
||||
|
||||
private class ReCaptchaWebViewClient extends WebViewClient {
|
||||
|
||||
@@ -36,12 +36,12 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
@@ -74,17 +74,23 @@ import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
|
||||
*/
|
||||
public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
@State protected int currentServiceId = -1;
|
||||
@State
|
||||
protected int currentServiceId = -1;
|
||||
private StreamingService currentService;
|
||||
@State protected LinkType currentLinkType;
|
||||
@State protected int selectedRadioPosition = -1;
|
||||
@State
|
||||
protected LinkType currentLinkType;
|
||||
@State
|
||||
protected int selectedRadioPosition = -1;
|
||||
protected int selectedPreviously = -1;
|
||||
|
||||
protected String currentUrl;
|
||||
protected boolean internalRoute = false;
|
||||
protected final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
private boolean selectionIsDownload = false;
|
||||
|
||||
public static final String internalRouteKey = "internalRoute";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -94,11 +100,13 @@ public class RouterActivity extends AppCompatActivity {
|
||||
currentUrl = getUrl(getIntent());
|
||||
|
||||
if (TextUtils.isEmpty(currentUrl)) {
|
||||
Toast.makeText(this, R.string.invalid_url_toast, Toast.LENGTH_LONG).show();
|
||||
handleText();
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
internalRoute = getIntent().getBooleanExtra(internalRouteKey, false);
|
||||
|
||||
setTheme(ThemeHelper.isLightThemeSelected(this)
|
||||
? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark);
|
||||
}
|
||||
@@ -112,7 +120,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
|
||||
handleUrl(currentUrl);
|
||||
}
|
||||
|
||||
@@ -252,7 +260,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
.setNegativeButton(R.string.just_once, dialogButtonsClickListener)
|
||||
.setPositiveButton(R.string.always, dialogButtonsClickListener)
|
||||
.setOnDismissListener((dialog) -> {
|
||||
if(!selectionIsDownload) finish();
|
||||
if (!selectionIsDownload) finish();
|
||||
})
|
||||
.create();
|
||||
|
||||
@@ -353,6 +361,15 @@ public class RouterActivity extends AppCompatActivity {
|
||||
positiveButton.setEnabled(state);
|
||||
}
|
||||
|
||||
private void handleText() {
|
||||
String searchString = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||
int serviceId = getIntent().getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
Intent intent = new Intent(getThemeWrapperContext(), MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
NavigationHelper.openSearch(getThemeWrapperContext(), serviceId, searchString);
|
||||
}
|
||||
|
||||
private void handleChoice(final String selectedChoiceKey) {
|
||||
final List<String> validChoicesList = Arrays.asList(getResources().getStringArray(R.array.preferred_open_action_values_list));
|
||||
if (validChoicesList.contains(selectedChoiceKey)) {
|
||||
@@ -383,8 +400,10 @@ public class RouterActivity extends AppCompatActivity {
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(intent -> {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
if (!internalRoute) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
}
|
||||
startActivity(intent);
|
||||
|
||||
finish();
|
||||
@@ -431,8 +450,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
for (int i: grantResults){
|
||||
if (i == PackageManager.PERMISSION_DENIED){
|
||||
for (int i : grantResults) {
|
||||
if (i == PackageManager.PERMISSION_DENIED) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
@@ -444,7 +463,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
private static class AdapterChoiceItem {
|
||||
final String description, key;
|
||||
@DrawableRes final int icon;
|
||||
@DrawableRes
|
||||
final int icon;
|
||||
|
||||
AdapterChoiceItem(String key, String description, int icon) {
|
||||
this.description = description;
|
||||
@@ -542,7 +562,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
|
||||
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);;
|
||||
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
|
||||
;
|
||||
|
||||
PlayQueue playQueue;
|
||||
String playerChoice = choice.playerChoice;
|
||||
@@ -558,7 +579,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
playQueue = new SinglePlayQueue((StreamInfo) info);
|
||||
|
||||
if (playerChoice.equals(videoPlayerKey)) {
|
||||
NavigationHelper.playOnMainPlayer(this, playQueue);
|
||||
NavigationHelper.playOnMainPlayer(this, playQueue, true);
|
||||
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true);
|
||||
} else if (playerChoice.equals(popupPlayerKey)) {
|
||||
@@ -571,11 +592,11 @@ public class RouterActivity extends AppCompatActivity {
|
||||
playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info);
|
||||
|
||||
if (playerChoice.equals(videoPlayerKey)) {
|
||||
NavigationHelper.playOnMainPlayer(this, playQueue);
|
||||
NavigationHelper.playOnMainPlayer(this, playQueue, true);
|
||||
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
||||
NavigationHelper.playOnBackgroundPlayer(this, playQueue);
|
||||
NavigationHelper.playOnBackgroundPlayer(this, playQueue, true);
|
||||
} else if (playerChoice.equals(popupPlayerKey)) {
|
||||
NavigationHelper.playOnPopupPlayer(this, playQueue);
|
||||
NavigationHelper.playOnPopupPlayer(this, playQueue, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,15 +3,24 @@ package org.schabi.newpipe.database;
|
||||
import android.arch.persistence.db.SupportSQLiteDatabase;
|
||||
import android.arch.persistence.room.migration.Migration;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
|
||||
public class Migrations {
|
||||
|
||||
public static final int DB_VER_11_0 = 1;
|
||||
public static final int DB_VER_12_0 = 2;
|
||||
|
||||
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
||||
private static final String TAG = Migrations.class.getName();
|
||||
|
||||
public static final Migration MIGRATION_11_12 = new Migration(DB_VER_11_0, DB_VER_12_0) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
if(DEBUG) {
|
||||
Log.d(TAG, "Start migrating database");
|
||||
}
|
||||
/*
|
||||
* Unfortunately these queries must be hardcoded due to the possibility of
|
||||
* schema and names changing at a later date, thus invalidating the older migration
|
||||
@@ -56,6 +65,10 @@ public class Migrations {
|
||||
"ORDER BY creation_date DESC");
|
||||
|
||||
database.execSQL("DROP TABLE IF EXISTS watch_history");
|
||||
|
||||
if(DEBUG) {
|
||||
Log.d(TAG, "Stop migrating database");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,6 +50,11 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
|
||||
" ORDER BY " + STREAM_ACCESS_DATE + " DESC")
|
||||
public abstract Flowable<List<StreamHistoryEntry>> getHistory();
|
||||
|
||||
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID +
|
||||
" = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1")
|
||||
@Nullable
|
||||
public abstract StreamHistoryEntity getLatestEntry(final long streamId);
|
||||
|
||||
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||
public abstract int deleteStreamHistory(final long streamId);
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ package org.schabi.newpipe.database.stream.model;
|
||||
import android.arch.persistence.room.ColumnInfo;
|
||||
import android.arch.persistence.room.Entity;
|
||||
import android.arch.persistence.room.ForeignKey;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static android.arch.persistence.room.ForeignKey.CASCADE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||
@@ -22,6 +25,12 @@ public class StreamStateEntity {
|
||||
final public static String JOIN_STREAM_ID = "stream_id";
|
||||
final public static String STREAM_PROGRESS_TIME = "progress_time";
|
||||
|
||||
|
||||
/** Playback state will not be saved, if playback time less than this threshold */
|
||||
private static final int PLAYBACK_SAVE_THRESHOLD_START_SECONDS = 5;
|
||||
/** Playback state will not be saved, if time left less than this threshold */
|
||||
private static final int PLAYBACK_SAVE_THRESHOLD_END_SECONDS = 10;
|
||||
|
||||
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||
private long streamUid;
|
||||
|
||||
@@ -48,4 +57,18 @@ public class StreamStateEntity {
|
||||
public void setProgressTime(long progressTime) {
|
||||
this.progressTime = progressTime;
|
||||
}
|
||||
|
||||
public boolean isValid(int durationInSeconds) {
|
||||
final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(progressTime);
|
||||
return seconds > PLAYBACK_SAVE_THRESHOLD_START_SECONDS
|
||||
&& seconds < durationInSeconds - PLAYBACK_SAVE_THRESHOLD_END_SECONDS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (obj instanceof StreamStateEntity) {
|
||||
return ((StreamStateEntity) obj).streamUid == streamUid
|
||||
&& ((StreamStateEntity) obj).progressTime == progressTime;
|
||||
} else return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
updateFragments();
|
||||
getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||
getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -55,12 +55,13 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
private void updateFragments() {
|
||||
MissionsFragment fragment = new MissionsFragment();
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG)
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
@@ -86,9 +87,4 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(Bundle inState){
|
||||
super.onRestoreInstanceState(inState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
package org.schabi.newpipe.download;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.IdRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.provider.DocumentFile;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.view.menu.ActionMenuItemView;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
@@ -24,6 +34,8 @@ import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
@@ -34,7 +46,10 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.extractor.utils.Localization;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
import org.schabi.newpipe.util.FilenameUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
@@ -43,19 +58,28 @@ import org.schabi.newpipe.util.StreamItemAdapter;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
import us.shandian.giga.service.DownloadManager;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
|
||||
import us.shandian.giga.service.MissionState;
|
||||
|
||||
public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
||||
private static final String TAG = "DialogFragment";
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final int REQUEST_DOWNLOAD_SAVE_AS = 0x1230;
|
||||
|
||||
@State
|
||||
protected StreamInfo currentInfo;
|
||||
@@ -80,7 +104,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
|
||||
private EditText nameEditText;
|
||||
private Spinner streamsSpinner;
|
||||
private RadioGroup radioVideoAudioGroup;
|
||||
private RadioGroup radioStreamsGroup;
|
||||
private TextView threadsCountTextView;
|
||||
private SeekBar threadsSeekBar;
|
||||
|
||||
@@ -155,12 +179,15 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
super.onCreate(savedInstanceState);
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
|
||||
if (!PermissionHelper.checkStoragePermissions(getActivity(), PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||
getDialog().dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(getContext()));
|
||||
context = getContext();
|
||||
|
||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams = new SparseArray<>(4);
|
||||
@@ -177,9 +204,33 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
}
|
||||
}
|
||||
|
||||
this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, secondaryStreams);
|
||||
this.audioStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedAudioStreams);
|
||||
this.subtitleStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedSubtitleStreams);
|
||||
this.videoStreamsAdapter = new StreamItemAdapter<>(context, wrappedVideoStreams, secondaryStreams);
|
||||
this.audioStreamsAdapter = new StreamItemAdapter<>(context, wrappedAudioStreams);
|
||||
this.subtitleStreamsAdapter = new StreamItemAdapter<>(context, wrappedSubtitleStreams);
|
||||
|
||||
Intent intent = new Intent(context, DownloadManagerService.class);
|
||||
context.startService(intent);
|
||||
|
||||
context.bindService(intent, new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName cname, IBinder service) {
|
||||
DownloadManagerBinder mgr = (DownloadManagerBinder) service;
|
||||
|
||||
mainStorageAudio = mgr.getMainStorageAudio();
|
||||
mainStorageVideo = mgr.getMainStorageVideo();
|
||||
downloadManager = mgr.getDownloadManager();
|
||||
askForSavePath = mgr.askForSavePath();
|
||||
|
||||
okButton.setEnabled(true);
|
||||
|
||||
context.unbindService(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
// nothing to do
|
||||
}
|
||||
}, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -204,8 +255,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
threadsCountTextView = view.findViewById(R.id.threads_count);
|
||||
threadsSeekBar = view.findViewById(R.id.threads);
|
||||
|
||||
radioVideoAudioGroup = view.findViewById(R.id.video_audio_group);
|
||||
radioVideoAudioGroup.setOnCheckedChangeListener(this);
|
||||
radioStreamsGroup = view.findViewById(R.id.video_audio_group);
|
||||
radioStreamsGroup.setOnCheckedChangeListener(this);
|
||||
|
||||
initToolbar(view.findViewById(R.id.toolbar));
|
||||
setupDownloadOptions();
|
||||
@@ -240,17 +291,17 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
disposables.clear();
|
||||
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams).subscribe(result -> {
|
||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) {
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.video_button) {
|
||||
setupVideoSpinner();
|
||||
}
|
||||
}));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams).subscribe(result -> {
|
||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
setupAudioSpinner();
|
||||
}
|
||||
}));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> {
|
||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
||||
setupSubtitleSpinner();
|
||||
}
|
||||
}));
|
||||
@@ -263,22 +314,55 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == REQUEST_DOWNLOAD_SAVE_AS && resultCode == Activity.RESULT_OK) {
|
||||
if (data.getData() == null) {
|
||||
showFailedDialog(R.string.general_error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (FilePickerActivityHelper.isOwnFileUri(context, data.getData())) {
|
||||
File file = Utils.getFileForUri(data.getData());
|
||||
checkSelectedDownload(null, Uri.fromFile(file), file.getName(), StoredFileHelper.DEFAULT_MIME);
|
||||
return;
|
||||
}
|
||||
|
||||
DocumentFile docFile = DocumentFile.fromSingleUri(context, data.getData());
|
||||
if (docFile == null) {
|
||||
showFailedDialog(R.string.general_error);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the selected file was previously used
|
||||
checkSelectedDownload(null, data.getData(), docFile.getName(), docFile.getType());
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Inits
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void initToolbar(Toolbar toolbar) {
|
||||
if (DEBUG) Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
|
||||
|
||||
boolean isLight = ThemeHelper.isLightThemeSelected(getActivity());
|
||||
|
||||
toolbar.setTitle(R.string.download_dialog_title);
|
||||
toolbar.setNavigationIcon(ThemeHelper.isLightThemeSelected(getActivity()) ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
|
||||
toolbar.setNavigationIcon(isLight ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
|
||||
toolbar.inflateMenu(R.menu.dialog_url);
|
||||
toolbar.setNavigationOnClickListener(v -> getDialog().dismiss());
|
||||
|
||||
okButton = toolbar.findViewById(R.id.okay);
|
||||
okButton.setEnabled(false);// disable until the download service connection is done
|
||||
|
||||
toolbar.setOnMenuItemClickListener(item -> {
|
||||
if (item.getItemId() == R.id.okay) {
|
||||
prepareSelectedDownload();
|
||||
@@ -346,7 +430,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
|
||||
switch (radioVideoAudioGroup.getCheckedRadioButtonId()) {
|
||||
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
selectedAudioIndex = position;
|
||||
break;
|
||||
@@ -370,9 +454,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
protected void setupDownloadOptions() {
|
||||
setRadioButtonsState(false);
|
||||
|
||||
final RadioButton audioButton = radioVideoAudioGroup.findViewById(R.id.audio_button);
|
||||
final RadioButton videoButton = radioVideoAudioGroup.findViewById(R.id.video_button);
|
||||
final RadioButton subtitleButton = radioVideoAudioGroup.findViewById(R.id.subtitle_button);
|
||||
final RadioButton audioButton = radioStreamsGroup.findViewById(R.id.audio_button);
|
||||
final RadioButton videoButton = radioStreamsGroup.findViewById(R.id.video_button);
|
||||
final RadioButton subtitleButton = radioStreamsGroup.findViewById(R.id.subtitle_button);
|
||||
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
|
||||
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
|
||||
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
|
||||
@@ -397,9 +481,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
}
|
||||
|
||||
private void setRadioButtonsState(boolean enabled) {
|
||||
radioVideoAudioGroup.findViewById(R.id.audio_button).setEnabled(enabled);
|
||||
radioVideoAudioGroup.findViewById(R.id.video_button).setEnabled(enabled);
|
||||
radioVideoAudioGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
|
||||
radioStreamsGroup.findViewById(R.id.audio_button).setEnabled(enabled);
|
||||
radioStreamsGroup.findViewById(R.id.video_button).setEnabled(enabled);
|
||||
radioStreamsGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
|
||||
}
|
||||
|
||||
private int getSubtitleIndexBy(List<SubtitlesStream> streams) {
|
||||
@@ -434,98 +518,320 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
return 0;
|
||||
}
|
||||
|
||||
StoredDirectoryHelper mainStorageAudio = null;
|
||||
StoredDirectoryHelper mainStorageVideo = null;
|
||||
DownloadManager downloadManager = null;
|
||||
ActionMenuItemView okButton = null;
|
||||
Context context;
|
||||
boolean askForSavePath;
|
||||
|
||||
private String getNameEditText() {
|
||||
String str = nameEditText.getText().toString().trim();
|
||||
|
||||
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
|
||||
}
|
||||
|
||||
private void showFailedDialog(@StringRes int msg) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.general_error)
|
||||
.setMessage(msg)
|
||||
.setNegativeButton(android.R.string.ok, null)
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showErrorActivity(Exception e) {
|
||||
ErrorActivity.reportError(
|
||||
context,
|
||||
Collections.singletonList(e),
|
||||
null,
|
||||
null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error)
|
||||
);
|
||||
}
|
||||
|
||||
private void prepareSelectedDownload() {
|
||||
final Context context = getContext();
|
||||
Stream stream;
|
||||
String location;
|
||||
char kind;
|
||||
StoredDirectoryHelper mainStorage;
|
||||
MediaFormat format;
|
||||
String mime;
|
||||
|
||||
String fileName = nameEditText.getText().toString().trim();
|
||||
if (fileName.isEmpty())
|
||||
fileName = FilenameUtils.createFilename(context, currentInfo.getName());
|
||||
// first, build the filename and get the output folder (if possible)
|
||||
// later, run a very very very large file checking logic
|
||||
|
||||
switch (radioVideoAudioGroup.getCheckedRadioButtonId()) {
|
||||
String filename = getNameEditText().concat(".");
|
||||
|
||||
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
stream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
||||
location = NewPipeSettings.getAudioDownloadPath(context);
|
||||
kind = 'a';
|
||||
mainStorage = mainStorageAudio;
|
||||
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
||||
mime = format.mimeType;
|
||||
filename += format.suffix;
|
||||
break;
|
||||
case R.id.video_button:
|
||||
stream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||
location = NewPipeSettings.getVideoDownloadPath(context);
|
||||
kind = 'v';
|
||||
mainStorage = mainStorageVideo;
|
||||
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
||||
mime = format.mimeType;
|
||||
filename += format.suffix;
|
||||
break;
|
||||
case R.id.subtitle_button:
|
||||
stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
||||
location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video files go together
|
||||
kind = 's';
|
||||
mainStorage = mainStorageVideo;// subtitle & video files go together
|
||||
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
||||
mime = format.mimeType;
|
||||
filename += format == MediaFormat.TTML ? MediaFormat.SRT.suffix : format.suffix;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("No stream selected");
|
||||
}
|
||||
|
||||
if (mainStorage == null || askForSavePath) {
|
||||
// This part is called if with SAF preferred:
|
||||
// * older android version running
|
||||
// * save path not defined (via download settings)
|
||||
// * the user checked the "ask where to download" option
|
||||
|
||||
if (!askForSavePath)
|
||||
Toast.makeText(context, getString(R.string.no_available_dir), Toast.LENGTH_LONG).show();
|
||||
|
||||
if (NewPipeSettings.useStorageAccessFramework(context)) {
|
||||
StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_SAVE_AS, filename, mime);
|
||||
} else {
|
||||
File initialSavePath;
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button)
|
||||
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
|
||||
else
|
||||
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
|
||||
|
||||
initialSavePath = new File(initialSavePath, filename);
|
||||
startActivityForResult(
|
||||
FilePickerActivityHelper.chooseFileToSave(context, initialSavePath.getAbsolutePath()),
|
||||
REQUEST_DOWNLOAD_SAVE_AS
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// check for existing file with the same name
|
||||
checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime);
|
||||
}
|
||||
|
||||
private void checkSelectedDownload(StoredDirectoryHelper mainStorage, Uri targetFile, String filename, String mime) {
|
||||
StoredFileHelper storage;
|
||||
|
||||
try {
|
||||
if (mainStorage == null) {
|
||||
// using SAF on older android version
|
||||
storage = new StoredFileHelper(context, null, targetFile, "");
|
||||
} else if (targetFile == null) {
|
||||
// the file does not exist, but it is probably used in a pending download
|
||||
storage = new StoredFileHelper(mainStorage.getUri(), filename, mime, mainStorage.getTag());
|
||||
} else {
|
||||
// the target filename is already use, attempt to use it
|
||||
storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
showErrorActivity(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if is our file
|
||||
MissionState state = downloadManager.checkForExistingMission(storage);
|
||||
@StringRes int msgBtn;
|
||||
@StringRes int msgBody;
|
||||
|
||||
switch (state) {
|
||||
case Finished:
|
||||
msgBtn = R.string.overwrite;
|
||||
msgBody = R.string.overwrite_finished_warning;
|
||||
break;
|
||||
case Pending:
|
||||
msgBtn = R.string.overwrite;
|
||||
msgBody = R.string.download_already_pending;
|
||||
break;
|
||||
case PendingRunning:
|
||||
msgBtn = R.string.generate_unique_name;
|
||||
msgBody = R.string.download_already_running;
|
||||
break;
|
||||
case None:
|
||||
if (mainStorage == null) {
|
||||
// This part is called if:
|
||||
// * using SAF on older android version
|
||||
// * save path not defined
|
||||
// * if the file exists overwrite it, is not necessary ask
|
||||
if (!storage.existsAsFile() && !storage.create()) {
|
||||
showFailedDialog(R.string.error_file_creation);
|
||||
return;
|
||||
}
|
||||
continueSelectedDownload(storage);
|
||||
return;
|
||||
} else if (targetFile == null) {
|
||||
// This part is called if:
|
||||
// * the filename is not used in a pending/finished download
|
||||
// * the file does not exists, create
|
||||
|
||||
if (!mainStorage.mkdirs()) {
|
||||
showFailedDialog(R.string.error_path_creation);
|
||||
return;
|
||||
}
|
||||
|
||||
storage = mainStorage.createFile(filename, mime);
|
||||
if (storage == null || !storage.canWrite()) {
|
||||
showFailedDialog(R.string.error_file_creation);
|
||||
return;
|
||||
}
|
||||
|
||||
continueSelectedDownload(storage);
|
||||
return;
|
||||
}
|
||||
msgBtn = R.string.overwrite;
|
||||
msgBody = R.string.overwrite_unrelated_warning;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
int threads;
|
||||
|
||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
||||
threads = 1;// use unique thread for subtitles due small file size
|
||||
fileName += ".srt";// final subtitle format
|
||||
} else {
|
||||
threads = threadsSeekBar.getProgress() + 1;
|
||||
fileName += "." + stream.getFormat().getSuffix();
|
||||
AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.download_dialog_title)
|
||||
.setMessage(msgBody)
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
final StoredFileHelper finalStorage = storage;
|
||||
|
||||
|
||||
if (mainStorage == null) {
|
||||
// This part is called if:
|
||||
// * using SAF on older android version
|
||||
// * save path not defined
|
||||
switch (state) {
|
||||
case Pending:
|
||||
case Finished:
|
||||
askDialog.setPositiveButton(msgBtn, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
downloadManager.forgetMission(finalStorage);
|
||||
continueSelectedDownload(finalStorage);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
askDialog.create().show();
|
||||
return;
|
||||
}
|
||||
|
||||
final String finalFileName = fileName;
|
||||
askDialog.setPositiveButton(msgBtn, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
|
||||
DownloadManagerService.checkForRunningMission(context, location, fileName, (listed, finished) -> {
|
||||
if (listed) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.download_dialog_title)
|
||||
.setMessage(finished ? R.string.overwrite_warning : R.string.download_already_running)
|
||||
.setPositiveButton(
|
||||
finished ? R.string.overwrite : R.string.generate_unique_name,
|
||||
(dialog, which) -> downloadSelected(context, stream, location, finalFileName, kind, threads)
|
||||
)
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||
dialog.cancel();
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
} else {
|
||||
downloadSelected(context, stream, location, finalFileName, kind, threads);
|
||||
StoredFileHelper storageNew;
|
||||
switch (state) {
|
||||
case Finished:
|
||||
case Pending:
|
||||
downloadManager.forgetMission(finalStorage);
|
||||
case None:
|
||||
if (targetFile == null) {
|
||||
storageNew = mainStorage.createFile(filename, mime);
|
||||
} else {
|
||||
try {
|
||||
// try take (or steal) the file
|
||||
storageNew = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag());
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to take (or steal) the file in " + targetFile.toString());
|
||||
storageNew = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (storageNew != null && storageNew.canWrite())
|
||||
continueSelectedDownload(storageNew);
|
||||
else
|
||||
showFailedDialog(R.string.error_file_creation);
|
||||
break;
|
||||
case PendingRunning:
|
||||
storageNew = mainStorage.createUniqueFile(filename, mime);
|
||||
if (storageNew == null)
|
||||
showFailedDialog(R.string.error_file_creation);
|
||||
else
|
||||
continueSelectedDownload(storageNew);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
askDialog.create().show();
|
||||
}
|
||||
|
||||
private void downloadSelected(Context context, Stream selectedStream, String location, String fileName, char kind, int threads) {
|
||||
private void continueSelectedDownload(@NonNull StoredFileHelper storage) {
|
||||
if (!storage.canWrite()) {
|
||||
showFailedDialog(R.string.permission_denied);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the selected file has to be overwritten, by simply checking its length
|
||||
try {
|
||||
if (storage.length() > 0) storage.truncate();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "failed to truncate the file: " + storage.getUri().toString(), e);
|
||||
showFailedDialog(R.string.overwrite_failed);
|
||||
return;
|
||||
}
|
||||
|
||||
Stream selectedStream;
|
||||
char kind;
|
||||
int threads = threadsSeekBar.getProgress() + 1;
|
||||
String[] urls;
|
||||
String psName = null;
|
||||
String[] psArgs = null;
|
||||
String secondaryStreamUrl = null;
|
||||
long nearLength = 0;
|
||||
|
||||
if (selectedStream instanceof VideoStream) {
|
||||
SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter
|
||||
.getAllSecondary()
|
||||
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
|
||||
// more download logic: select muxer, subtitle converter, etc.
|
||||
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
kind = 'a';
|
||||
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
||||
|
||||
if (secondaryStream != null) {
|
||||
secondaryStreamUrl = secondaryStream.getStream().getUrl();
|
||||
psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER;
|
||||
psArgs = null;
|
||||
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
|
||||
|
||||
// set nearLength, only, if both sizes are fetched or known. this probably does not work on slow networks
|
||||
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
|
||||
nearLength = secondaryStream.getSizeInBytes() + videoSize;
|
||||
if (selectedStream.getFormat() == MediaFormat.M4A) {
|
||||
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
|
||||
}
|
||||
}
|
||||
} else if ((selectedStream instanceof SubtitlesStream) && selectedStream.getFormat() == MediaFormat.TTML) {
|
||||
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
|
||||
psArgs = new String[]{
|
||||
selectedStream.getFormat().getSuffix(),
|
||||
"false",// ignore empty frames
|
||||
"false",// detect youtube duplicate lines
|
||||
};
|
||||
break;
|
||||
case R.id.video_button:
|
||||
kind = 'v';
|
||||
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||
|
||||
SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter
|
||||
.getAllSecondary()
|
||||
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
|
||||
|
||||
if (secondaryStream != null) {
|
||||
secondaryStreamUrl = secondaryStream.getStream().getUrl();
|
||||
|
||||
if (selectedStream.getFormat() == MediaFormat.MPEG_4)
|
||||
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
|
||||
else
|
||||
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
|
||||
|
||||
psArgs = null;
|
||||
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
|
||||
|
||||
// set nearLength, only, if both sizes are fetched or known. This probably
|
||||
// does not work on slow networks but is later updated in the downloader
|
||||
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
|
||||
nearLength = secondaryStream.getSizeInBytes() + videoSize;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case R.id.subtitle_button:
|
||||
threads = 1;// use unique thread for subtitles due small file size
|
||||
kind = 's';
|
||||
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
||||
|
||||
if (selectedStream.getFormat() == MediaFormat.TTML) {
|
||||
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
|
||||
psArgs = new String[]{
|
||||
selectedStream.getFormat().getSuffix(),
|
||||
"false",// ignore empty frames
|
||||
"false",// detect youtube duplicate lines
|
||||
};
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (secondaryStreamUrl == null) {
|
||||
@@ -534,8 +840,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl};
|
||||
}
|
||||
|
||||
DownloadManagerService.startMission(context, urls, location, fileName, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);
|
||||
DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);
|
||||
|
||||
getDialog().dismiss();
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
}
|
||||
|
||||
if (exception instanceof ReCaptchaException) {
|
||||
onReCaptchaException();
|
||||
onReCaptchaException((ReCaptchaException) exception);
|
||||
return true;
|
||||
} else if (exception instanceof IOException) {
|
||||
showError(getString(R.string.network_error), true);
|
||||
@@ -190,11 +190,13 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onReCaptchaException() {
|
||||
public void onReCaptchaException(ReCaptchaException exception) {
|
||||
if (DEBUG) Log.d(TAG, "onReCaptchaException() called");
|
||||
Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
startActivityForResult(new Intent(activity, ReCaptchaActivity.class), ReCaptchaActivity.RECAPTCHA_REQUEST);
|
||||
Intent intent = new Intent(activity, ReCaptchaActivity.class);
|
||||
intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, exception.getUrl());
|
||||
startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST);
|
||||
|
||||
showError(getString(R.string.recaptcha_request_toast), false);
|
||||
}
|
||||
@@ -230,21 +232,4 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView,
|
||||
ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId));
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void openUrlInBrowser(String url) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title)));
|
||||
}
|
||||
|
||||
protected void shareUrl(String subject, String url) {
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, url);
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class EmptyFragment extends BaseFragment {
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_empty, container, false);
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
destroyOldFragments();
|
||||
|
||||
tabsManager = TabsManager.getManager(activity);
|
||||
tabsManager.setSavedTabsListener(() -> {
|
||||
if (DEBUG) {
|
||||
@@ -63,6 +65,17 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
});
|
||||
}
|
||||
|
||||
private void destroyOldFragments() {
|
||||
for (Fragment fragment : getChildFragmentManager().getFragments()) {
|
||||
if (fragment != null) {
|
||||
getChildFragmentManager()
|
||||
.beginTransaction()
|
||||
.remove(fragment)
|
||||
.commitNowAllowingStateLoss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_main, container, false);
|
||||
@@ -98,6 +111,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
tabsManager.unsetSavedTabsListener();
|
||||
pagerAdapter = null;
|
||||
viewPager.setAdapter(pagerAdapter);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -177,6 +192,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
}
|
||||
|
||||
private class SelectedTabsPagerAdapter extends FragmentPagerAdapter {
|
||||
|
||||
private SelectedTabsPagerAdapter(FragmentManager fragmentManager) {
|
||||
super(fragmentManager);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentPagerAdapter;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TabAdaptor extends FragmentPagerAdapter {
|
||||
|
||||
private final List<Fragment> mFragmentList = new ArrayList<>();
|
||||
private final List<String> mFragmentTitleList = new ArrayList<>();
|
||||
private final FragmentManager fragmentManager;
|
||||
|
||||
public TabAdaptor(FragmentManager fm) {
|
||||
super(fm);
|
||||
this.fragmentManager = fm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
return mFragmentList.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mFragmentList.size();
|
||||
}
|
||||
|
||||
public void addFragment(Fragment fragment, String title) {
|
||||
mFragmentList.add(fragment);
|
||||
mFragmentTitleList.add(title);
|
||||
}
|
||||
|
||||
public void clearAllItems() {
|
||||
mFragmentList.clear();
|
||||
mFragmentTitleList.clear();
|
||||
}
|
||||
|
||||
public void removeItem(int position){
|
||||
mFragmentList.remove(position == 0 ? 0 : position - 1);
|
||||
mFragmentTitleList.remove(position == 0 ? 0 : position - 1);
|
||||
}
|
||||
|
||||
public void updateItem(int position, Fragment fragment){
|
||||
mFragmentList.set(position, fragment);
|
||||
}
|
||||
|
||||
public void updateItem(String title, Fragment fragment){
|
||||
int index = mFragmentTitleList.indexOf(title);
|
||||
if(index != -1){
|
||||
updateItem(index, fragment);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemPosition(Object object) {
|
||||
if (mFragmentList.contains(object)) return mFragmentList.indexOf(object);
|
||||
else return POSITION_NONE;
|
||||
}
|
||||
|
||||
public int getItemPositionByTitle(String title) {
|
||||
return mFragmentTitleList.indexOf(title);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getItemTitle(int position) {
|
||||
if (position < 0 || position >= mFragmentTitleList.size()) {
|
||||
return null;
|
||||
}
|
||||
return mFragmentTitleList.get(position);
|
||||
}
|
||||
|
||||
public void notifyDataSetUpdate(){
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(ViewGroup container, int position, Object object) {
|
||||
fragmentManager.beginTransaction().remove((Fragment) object).commitNowAllowingStateLoss();
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,6 @@ package org.schabi.newpipe.fragments.list;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
@@ -22,20 +21,20 @@ import android.view.View;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
@@ -63,6 +62,11 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
infoListAdapter = new InfoListAdapter(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -220,6 +224,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
}
|
||||
});
|
||||
|
||||
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<CommentsInfoItem>() {
|
||||
@Override
|
||||
public void selected(CommentsInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
}
|
||||
});
|
||||
|
||||
itemsList.clearOnScrollListeners();
|
||||
itemsList.addOnScrollListener(new OnScrollBelowItemsListener() {
|
||||
@Override
|
||||
@@ -241,41 +252,32 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
protected void showStreamDialog(final StreamInfoItem item) {
|
||||
final Context context = getContext();
|
||||
final Activity activity = getActivity();
|
||||
if (context == null || context.getResources() == null || getActivity() == null) return;
|
||||
if (context == null || context.getResources() == null || activity == null) return;
|
||||
|
||||
final String[] commands = new String[]{
|
||||
context.getResources().getString(R.string.enqueue_on_background),
|
||||
context.getResources().getString(R.string.enqueue_on_popup),
|
||||
context.getResources().getString(R.string.append_playlist),
|
||||
context.getResources().getString(R.string.share)
|
||||
};
|
||||
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
|
||||
StreamDialogEntry.setEnabledEntries(
|
||||
StreamDialogEntry.enqueue_on_background,
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
StreamDialogEntry.append_playlist,
|
||||
StreamDialogEntry.share);
|
||||
} else {
|
||||
StreamDialogEntry.setEnabledEntries(
|
||||
StreamDialogEntry.enqueue_on_background,
|
||||
StreamDialogEntry.enqueue_on_popup,
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
StreamDialogEntry.start_here_on_popup,
|
||||
StreamDialogEntry.append_playlist,
|
||||
StreamDialogEntry.share);
|
||||
}
|
||||
|
||||
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||
switch (i) {
|
||||
case 0:
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
|
||||
break;
|
||||
case 1:
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
|
||||
break;
|
||||
case 2:
|
||||
if (getFragmentManager() != null) {
|
||||
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
|
||||
.show(getFragmentManager(), TAG);
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
shareUrl(item.getName(), item.getUrl());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
new InfoItemDialog(getActivity(), item, commands, actions).show();
|
||||
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), (dialog, which) ->
|
||||
StreamDialogEntry.clickOn(which, this, item)).show();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.schabi.newpipe.fragments.list.channel;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
@@ -34,21 +32,18 @@ import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionService;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -149,56 +144,6 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
return headerRootLayout;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void showStreamDialog(final StreamInfoItem item) {
|
||||
final Activity activity = getActivity();
|
||||
final Context context = getContext();
|
||||
if (context == null || context.getResources() == null || getActivity() == null) return;
|
||||
|
||||
final String[] commands = new String[]{
|
||||
context.getResources().getString(R.string.enqueue_on_background),
|
||||
context.getResources().getString(R.string.enqueue_on_popup),
|
||||
context.getResources().getString(R.string.start_here_on_main),
|
||||
context.getResources().getString(R.string.start_here_on_background),
|
||||
context.getResources().getString(R.string.start_here_on_popup),
|
||||
context.getResources().getString(R.string.append_playlist),
|
||||
context.getResources().getString(R.string.share)
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> {
|
||||
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
|
||||
switch (i) {
|
||||
case 0:
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
|
||||
break;
|
||||
case 1:
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
|
||||
break;
|
||||
case 2:
|
||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
|
||||
break;
|
||||
case 3:
|
||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
|
||||
break;
|
||||
case 4:
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||
break;
|
||||
case 5:
|
||||
if (getFragmentManager() != null) {
|
||||
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
|
||||
.show(getFragmentManager(), TAG);
|
||||
}
|
||||
break;
|
||||
case 6:
|
||||
shareUrl(item.getName(), item.getUrl());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
new InfoItemDialog(getActivity(), item, commands, actions).show();
|
||||
}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -233,10 +178,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
openRssFeed();
|
||||
break;
|
||||
case R.id.menu_item_openInBrowser:
|
||||
openUrlInBrowser(url);
|
||||
ShareUtils.openUrlInBrowser(this.getContext(), currentInfo.getOriginalUrl());
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
shareUrl(name, url);
|
||||
ShareUtils.shareUrl(this.getContext(), name, currentInfo.getOriginalUrl());
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
@@ -439,11 +384,11 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
monitorSubscription(result);
|
||||
|
||||
headerPlayAllButton.setOnClickListener(
|
||||
view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||
view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
|
||||
headerPopupButton.setOnClickListener(
|
||||
view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
||||
view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||
headerBackgroundButton.setOnClickListener(
|
||||
view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
||||
view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue() {
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package org.schabi.newpipe.fragments.list.comments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
|
||||
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
|
||||
|
||||
private boolean mIsVisibleToUser = false;
|
||||
|
||||
public static CommentsFragment getInstance(int serviceId, String url, String name) {
|
||||
CommentsFragment instance = new CommentsFragment();
|
||||
instance.setInitialData(serviceId, url, name);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
mIsVisibleToUser = isVisibleToUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_comments, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (disposables != null) disposables.clear();
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Load and handle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPageUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<CommentsInfo> loadResult(boolean forceLoad) {
|
||||
return ExtractorHelper.getCommentsInfo(serviceId, url, forceLoad);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull CommentsInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
AnimationUtils.slideUp(getView(),120, 96, 0.06f);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
}
|
||||
|
||||
if (disposables != null) disposables.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(),
|
||||
UserAction.REQUESTED_COMMENTS,
|
||||
NewPipe.getNameOfService(serviceId),
|
||||
"Get next page of: " + url,
|
||||
R.string.general_error);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OnError
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
hideLoading();
|
||||
showSnackBarError(exception, UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(serviceId), url, R.string.error_unable_to_load_comments);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void setTitle(String title) {
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isGridLayout() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.schabi.newpipe.fragments.list.kiosk;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.ActionBar;
|
||||
@@ -155,9 +154,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
super.handleResult(result);
|
||||
|
||||
name = kioskTranslatedName;
|
||||
if(!useAsFrontPage) {
|
||||
setTitle(kioskTranslatedName);
|
||||
}
|
||||
setTitle(kioskTranslatedName);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(),
|
||||
|
||||
@@ -2,10 +2,10 @@ package org.schabi.newpipe.fragments.list.playlist;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
@@ -29,17 +29,19 @@ import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -134,48 +136,40 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
infoListAdapter.useMiniItemVariants(true);
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueueStartingAt(StreamInfoItem infoItem) {
|
||||
return getPlayQueue(Math.max(infoListAdapter.getItemsList().indexOf(infoItem), 0));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void showStreamDialog(final StreamInfoItem item) {
|
||||
protected void showStreamDialog(StreamInfoItem item) {
|
||||
final Context context = getContext();
|
||||
final Activity activity = getActivity();
|
||||
if (context == null || context.getResources() == null || getActivity() == null) return;
|
||||
if (context == null || context.getResources() == null || activity == null) return;
|
||||
|
||||
final String[] commands = new String[]{
|
||||
context.getResources().getString(R.string.enqueue_on_background),
|
||||
context.getResources().getString(R.string.enqueue_on_popup),
|
||||
context.getResources().getString(R.string.start_here_on_main),
|
||||
context.getResources().getString(R.string.start_here_on_background),
|
||||
context.getResources().getString(R.string.start_here_on_popup),
|
||||
context.getResources().getString(R.string.share)
|
||||
};
|
||||
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
|
||||
StreamDialogEntry.setEnabledEntries(
|
||||
StreamDialogEntry.enqueue_on_background,
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
StreamDialogEntry.append_playlist,
|
||||
StreamDialogEntry.share);
|
||||
} else {
|
||||
StreamDialogEntry.setEnabledEntries(
|
||||
StreamDialogEntry.enqueue_on_background,
|
||||
StreamDialogEntry.enqueue_on_popup,
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
StreamDialogEntry.start_here_on_popup,
|
||||
StreamDialogEntry.append_playlist,
|
||||
StreamDialogEntry.share);
|
||||
|
||||
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
|
||||
switch (i) {
|
||||
case 0:
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
|
||||
break;
|
||||
case 1:
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
|
||||
break;
|
||||
case 2:
|
||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
|
||||
break;
|
||||
case 3:
|
||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
|
||||
break;
|
||||
case 4:
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||
break;
|
||||
case 5:
|
||||
shareUrl(item.getName(), item.getUrl());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
StreamDialogEntry.start_here_on_popup.setCustomAction(
|
||||
(fragment, infoItem) -> NavigationHelper.playOnPopupPlayer(context, getPlayQueueStartingAt(infoItem), true));
|
||||
}
|
||||
|
||||
new InfoItemDialog(getActivity(), item, commands, actions).show();
|
||||
StreamDialogEntry.start_here_on_background.setCustomAction(
|
||||
(fragment, infoItem) -> NavigationHelper.playOnBackgroundPlayer(context, getPlayQueueStartingAt(infoItem), true));
|
||||
|
||||
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), (dialog, which) ->
|
||||
StreamDialogEntry.clickOn(which, this, item)).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -230,10 +224,10 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_item_openInBrowser:
|
||||
openUrlInBrowser(url);
|
||||
ShareUtils.openUrlInBrowser(this.getContext(), url);
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
shareUrl(name, url);
|
||||
ShareUtils.shareUrl(this.getContext(), name, url);
|
||||
break;
|
||||
case R.id.menu_item_bookmark:
|
||||
onBookmarkClicked();
|
||||
@@ -300,11 +294,21 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
.subscribe(getPlaylistBookmarkSubscriber());
|
||||
|
||||
headerPlayAllButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
|
||||
headerPopupButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||
headerBackgroundButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
|
||||
headerPopupButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
|
||||
return true;
|
||||
});
|
||||
|
||||
headerBackgroundButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue() {
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.TooltipCompat;
|
||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
@@ -39,15 +40,15 @@ import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.search.SearchExtractor;
|
||||
import org.schabi.newpipe.extractor.search.SearchInfo;
|
||||
import org.schabi.newpipe.util.FireTvUtils;
|
||||
import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.LayoutManagerSmoothScroller;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
|
||||
@@ -72,8 +73,8 @@ import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
|
||||
import static android.support.v7.widget.helper.ItemTouchHelper.Callback.makeMovementFlags;
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class SearchFragment
|
||||
@@ -104,8 +105,13 @@ public class SearchFragment
|
||||
// this three represet the current search query
|
||||
@State
|
||||
protected String searchString;
|
||||
|
||||
/**
|
||||
* No content filter should add like contentfilter = all
|
||||
* be aware of this when implementing an extractor.
|
||||
*/
|
||||
@State
|
||||
protected String[] contentFilter;
|
||||
protected String[] contentFilter = new String[0];
|
||||
@State
|
||||
protected String sortFilter;
|
||||
|
||||
@@ -292,7 +298,23 @@ public class SearchFragment
|
||||
suggestionsPanel = rootView.findViewById(R.id.suggestions_panel);
|
||||
suggestionsRecyclerView = rootView.findViewById(R.id.suggestions_list);
|
||||
suggestionsRecyclerView.setAdapter(suggestionListAdapter);
|
||||
suggestionsRecyclerView.setLayoutManager(new LayoutManagerSmoothScroller(activity));
|
||||
new ItemTouchHelper(new ItemTouchHelper.Callback() {
|
||||
@Override
|
||||
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
return getSuggestionMovementFlags(recyclerView, viewHolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
|
||||
onSuggestionItemSwiped(viewHolder, i);
|
||||
}
|
||||
}).attachToRecyclerView(suggestionsRecyclerView);
|
||||
|
||||
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
|
||||
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
|
||||
@@ -335,7 +357,7 @@ public class SearchFragment
|
||||
|| (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
|
||||
search(!TextUtils.isEmpty(searchString)
|
||||
? searchString
|
||||
: searchEditText.getText().toString(), new String[0], "");
|
||||
: searchEditText.getText().toString(), this.contentFilter, "");
|
||||
} else {
|
||||
if (searchEditText != null) {
|
||||
searchEditText.setText("");
|
||||
@@ -449,6 +471,9 @@ public class SearchFragment
|
||||
if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) {
|
||||
showSuggestionsPanel();
|
||||
}
|
||||
if(FireTvUtils.isFireTv()){
|
||||
showKeyboardSearch();
|
||||
}
|
||||
});
|
||||
|
||||
searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
|
||||
@@ -499,7 +524,9 @@ public class SearchFragment
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]");
|
||||
}
|
||||
if (event != null
|
||||
if(actionId == EditorInfo.IME_ACTION_PREVIOUS){
|
||||
hideKeyboardSearch();
|
||||
} else if (event != null
|
||||
&& (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|
||||
|| event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
||||
search(searchEditText.getText().toString(), new String[0], "");
|
||||
@@ -541,7 +568,7 @@ public class SearchFragment
|
||||
if (searchEditText.requestFocus()) {
|
||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(
|
||||
Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT);
|
||||
imm.showSoftInput(searchEditText, InputMethodManager.SHOW_FORCED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,8 +578,7 @@ public class SearchFragment
|
||||
|
||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(
|
||||
Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(),
|
||||
InputMethodManager.HIDE_NOT_ALWAYS);
|
||||
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), InputMethodManager.RESULT_UNCHANGED_SHOWN);
|
||||
|
||||
searchEditText.clearFocus();
|
||||
}
|
||||
@@ -736,6 +762,7 @@ public class SearchFragment
|
||||
|
||||
@Override
|
||||
protected void loadMoreItems() {
|
||||
if(nextPageUrl == null || nextPageUrl.isEmpty()) return;
|
||||
isLoading.set(true);
|
||||
showListFooter(true);
|
||||
if (searchDisposable != null) searchDisposable.dispose();
|
||||
@@ -890,4 +917,28 @@ public class SearchFragment
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Suggestion item touch helper
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public int getSuggestionMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
final int position = viewHolder.getAdapterPosition();
|
||||
final SuggestionItem item = suggestionListAdapter.getItem(position);
|
||||
return item.fromHistory ? makeMovementFlags(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0;
|
||||
}
|
||||
|
||||
public void onSuggestionItemSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
|
||||
final int position = viewHolder.getAdapterPosition();
|
||||
final String query = suggestionListAdapter.getItem(position).query;
|
||||
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
howManyDeleted -> suggestionPublisher
|
||||
.onNext(searchEditText.getText().toString()),
|
||||
throwable -> showSnackBarError(throwable,
|
||||
UserAction.DELETE_FROM_HISTORY, "none",
|
||||
"Deleting item failed", R.string.general_error));
|
||||
disposables.add(onDelete);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
|
||||
});
|
||||
}
|
||||
|
||||
private SuggestionItem getItem(int position) {
|
||||
SuggestionItem getItem(int position) {
|
||||
return items.get(position);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
package org.schabi.newpipe.fragments.list.videos;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.Switch;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.RelatedStreamInfo;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
|
||||
public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInfo> implements SharedPreferences.OnSharedPreferenceChangeListener{
|
||||
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
private RelatedStreamInfo relatedStreamInfo;
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
private View headerRootLayout;
|
||||
private Switch aSwitch;
|
||||
|
||||
private boolean mIsVisibleToUser = false;
|
||||
|
||||
public static RelatedVideosFragment getInstance(StreamInfo info) {
|
||||
RelatedVideosFragment instance = new RelatedVideosFragment();
|
||||
instance.setInitialData(info);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
mIsVisibleToUser = isVisibleToUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_related_streams, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (disposables != null) disposables.clear();
|
||||
}
|
||||
|
||||
protected View getListHeader(){
|
||||
if(relatedStreamInfo != null && relatedStreamInfo.getNextStream() != null){
|
||||
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.related_streams_header, itemsList, false);
|
||||
aSwitch = headerRootLayout.findViewById(R.id.autoplay_switch);
|
||||
|
||||
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
Boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
||||
aSwitch.setChecked(autoplay);
|
||||
aSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
|
||||
SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(getContext()).edit();
|
||||
prefEdit.putBoolean(getString(R.string.auto_queue_key), b);
|
||||
prefEdit.apply();
|
||||
}
|
||||
});
|
||||
return headerRootLayout;
|
||||
}else{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||
return Single.fromCallable(() -> ListExtractor.InfoItemsPage.emptyPage());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<RelatedStreamInfo> loadResult(boolean forceLoad) {
|
||||
return Single.fromCallable(() -> relatedStreamInfo);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
if(null != headerRootLayout) headerRootLayout.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull RelatedStreamInfo result) {
|
||||
|
||||
super.handleResult(result);
|
||||
|
||||
if(null != headerRootLayout) headerRootLayout.setVisibility(View.VISIBLE);
|
||||
AnimationUtils.slideUp(getView(),120, 96, 0.06f);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
}
|
||||
|
||||
if (disposables != null) disposables.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(),
|
||||
UserAction.REQUESTED_STREAM,
|
||||
NewPipe.getNameOfService(serviceId),
|
||||
"Get next page of: " + url,
|
||||
R.string.general_error);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OnError
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
hideLoading();
|
||||
showSnackBarError(exception, UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, R.string.general_error);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void setTitle(String title) {
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
return;
|
||||
}
|
||||
|
||||
private void setInitialData(StreamInfo info) {
|
||||
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
||||
if(this.relatedStreamInfo == null) this.relatedStreamInfo = RelatedStreamInfo.getInfo(info);
|
||||
}
|
||||
|
||||
|
||||
private static final String INFO_KEY = "related_info_key";
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putSerializable(INFO_KEY, relatedStreamInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(@NonNull Bundle savedState) {
|
||||
super.onRestoreInstanceState(savedState);
|
||||
if (savedState != null) {
|
||||
Serializable serializable = savedState.getSerializable(INFO_KEY);
|
||||
if(serializable instanceof RelatedStreamInfo){
|
||||
this.relatedStreamInfo = (RelatedStreamInfo) serializable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
|
||||
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
Boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
||||
if(null != aSwitch) aSwitch.setChecked(autoplay);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isGridLayout() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
@@ -10,15 +9,19 @@ import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
/*
|
||||
@@ -50,18 +53,20 @@ public class InfoItemBuilder {
|
||||
private OnClickGesture<StreamInfoItem> onStreamSelectedListener;
|
||||
private OnClickGesture<ChannelInfoItem> onChannelSelectedListener;
|
||||
private OnClickGesture<PlaylistInfoItem> onPlaylistSelectedListener;
|
||||
private OnClickGesture<CommentsInfoItem> onCommentsSelectedListener;
|
||||
|
||||
public InfoItemBuilder(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem) {
|
||||
return buildView(parent, infoItem, false);
|
||||
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
|
||||
return buildView(parent, infoItem, historyRecordManager, false);
|
||||
}
|
||||
|
||||
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, boolean useMiniVariant) {
|
||||
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem,
|
||||
final HistoryRecordManager historyRecordManager, boolean useMiniVariant) {
|
||||
InfoItemHolder holder = holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
|
||||
holder.updateFromItem(infoItem);
|
||||
holder.updateFromItem(infoItem, historyRecordManager);
|
||||
return holder.itemView;
|
||||
}
|
||||
|
||||
@@ -73,8 +78,9 @@ public class InfoItemBuilder {
|
||||
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) : new ChannelInfoItemHolder(this, parent);
|
||||
case PLAYLIST:
|
||||
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) : new PlaylistInfoItemHolder(this, parent);
|
||||
case COMMENT:
|
||||
return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent) : new CommentsInfoItemHolder(this, parent);
|
||||
default:
|
||||
Log.e(TAG, "Trollolo");
|
||||
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
||||
}
|
||||
}
|
||||
@@ -111,4 +117,12 @@ public class InfoItemBuilder {
|
||||
this.onPlaylistSelectedListener = listener;
|
||||
}
|
||||
|
||||
public OnClickGesture<CommentsInfoItem> getOnCommentsSelectedListener() {
|
||||
return onCommentsSelectedListener;
|
||||
}
|
||||
|
||||
public void setOnCommentsSelectedListener(OnClickGesture<CommentsInfoItem> onCommentsSelectedListener) {
|
||||
this.onCommentsSelectedListener = onCommentsSelectedListener;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
||||
@@ -21,6 +27,7 @@ import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.StreamGridInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.FallbackViewHolder;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
@@ -63,9 +70,13 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
private static final int MINI_PLAYLIST_HOLDER_TYPE = 0x300;
|
||||
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
|
||||
private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302;
|
||||
private static final int MINI_COMMENT_HOLDER_TYPE = 0x400;
|
||||
private static final int COMMENT_HOLDER_TYPE = 0x401;
|
||||
|
||||
private final InfoItemBuilder infoItemBuilder;
|
||||
private final ArrayList<InfoItem> infoItemList;
|
||||
private final HistoryRecordManager recordManager;
|
||||
|
||||
private boolean useMiniVariant = false;
|
||||
private boolean useGridVariant = false;
|
||||
private boolean showFooter = false;
|
||||
@@ -81,8 +92,9 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
}
|
||||
}
|
||||
|
||||
public InfoListAdapter(Activity a) {
|
||||
infoItemBuilder = new InfoItemBuilder(a);
|
||||
public InfoListAdapter(Context context) {
|
||||
this.recordManager = new HistoryRecordManager(context);
|
||||
infoItemBuilder = new InfoItemBuilder(context);
|
||||
infoItemList = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -98,6 +110,10 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
infoItemBuilder.setOnPlaylistSelectedListener(listener);
|
||||
}
|
||||
|
||||
public void setOnCommentsSelectedListener(OnClickGesture<CommentsInfoItem> listener) {
|
||||
infoItemBuilder.setOnCommentsSelectedListener(listener);
|
||||
}
|
||||
|
||||
public void useMiniItemVariants(boolean useMiniVariant) {
|
||||
this.useMiniVariant = useMiniVariant;
|
||||
}
|
||||
@@ -106,50 +122,53 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
this.useGridVariant = useGridVariant;
|
||||
}
|
||||
|
||||
public void addInfoItemList(List<InfoItem> data) {
|
||||
if (data != null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + infoItemList.size() + ", data.size() = " + data.size());
|
||||
}
|
||||
public void addInfoItemList(@Nullable final List<InfoItem> data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
if (DEBUG) Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " +
|
||||
infoItemList.size() + ", data.size() = " + data.size());
|
||||
|
||||
int offsetStart = sizeConsideringHeaderOffset();
|
||||
infoItemList.addAll(data);
|
||||
int offsetStart = sizeConsideringHeaderOffset();
|
||||
infoItemList.addAll(data);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
|
||||
}
|
||||
if (DEBUG) Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart +
|
||||
", infoItemList.size() = " + infoItemList.size() +
|
||||
", header = " + header + ", footer = " + footer +
|
||||
", showFooter = " + showFooter);
|
||||
notifyItemRangeInserted(offsetStart, data.size());
|
||||
|
||||
notifyItemRangeInserted(offsetStart, data.size());
|
||||
if (footer != null && showFooter) {
|
||||
int footerNow = sizeConsideringHeaderOffset();
|
||||
notifyItemMoved(offsetStart, footerNow);
|
||||
|
||||
if (footer != null && showFooter) {
|
||||
int footerNow = sizeConsideringHeaderOffset();
|
||||
notifyItemMoved(offsetStart, footerNow);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart + " to " + footerNow);
|
||||
}
|
||||
if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart +
|
||||
" to " + footerNow);
|
||||
}
|
||||
}
|
||||
|
||||
public void addInfoItem(InfoItem data) {
|
||||
if (data != null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + infoItemList.size() + ", thread = " + Thread.currentThread());
|
||||
}
|
||||
public void addInfoItem(@Nullable InfoItem data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
if (DEBUG) Log.d(TAG, "addInfoItem() before > infoItemList.size() = " +
|
||||
infoItemList.size() + ", thread = " + Thread.currentThread());
|
||||
|
||||
int positionInserted = sizeConsideringHeaderOffset();
|
||||
infoItemList.add(data);
|
||||
int positionInserted = sizeConsideringHeaderOffset();
|
||||
infoItemList.add(data);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
|
||||
}
|
||||
notifyItemInserted(positionInserted);
|
||||
if (DEBUG) Log.d(TAG, "addInfoItem() after > position = " + positionInserted +
|
||||
", infoItemList.size() = " + infoItemList.size() +
|
||||
", header = " + header + ", footer = " + footer +
|
||||
", showFooter = " + showFooter);
|
||||
notifyItemInserted(positionInserted);
|
||||
|
||||
if (footer != null && showFooter) {
|
||||
int footerNow = sizeConsideringHeaderOffset();
|
||||
notifyItemMoved(positionInserted, footerNow);
|
||||
if (footer != null && showFooter) {
|
||||
int footerNow = sizeConsideringHeaderOffset();
|
||||
notifyItemMoved(positionInserted, footerNow);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted + " to " + footerNow);
|
||||
}
|
||||
if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted +
|
||||
" to " + footerNow);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,15 +242,18 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
return useGridVariant ? GRID_CHANNEL_HOLDER_TYPE : useMiniVariant ? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE;
|
||||
case PLAYLIST:
|
||||
return useGridVariant ? GRID_PLAYLIST_HOLDER_TYPE : useMiniVariant ? MINI_PLAYLIST_HOLDER_TYPE : PLAYLIST_HOLDER_TYPE;
|
||||
case COMMENT:
|
||||
return useMiniVariant ? MINI_COMMENT_HOLDER_TYPE : COMMENT_HOLDER_TYPE;
|
||||
default:
|
||||
Log.e(TAG, "Trollolo");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateViewHolder() called with: parent = [" + parent + "], type = [" + type + "]");
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int type) {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onCreateViewHolder() called with: parent = [" + parent + "], type = [" + type + "]");
|
||||
switch (type) {
|
||||
case HEADER_TYPE:
|
||||
return new HFHolder(header);
|
||||
@@ -255,20 +277,23 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
return new PlaylistInfoItemHolder(infoItemBuilder, parent);
|
||||
case GRID_PLAYLIST_HOLDER_TYPE:
|
||||
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
||||
case MINI_COMMENT_HOLDER_TYPE:
|
||||
return new CommentsMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case COMMENT_HOLDER_TYPE:
|
||||
return new CommentsInfoItemHolder(infoItemBuilder, parent);
|
||||
default:
|
||||
Log.e(TAG, "Trollolo");
|
||||
return new FallbackViewHolder(new View(parent.getContext()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
if (DEBUG) Log.d(TAG, "onBindViewHolder() called with: holder = [" + holder.getClass().getSimpleName() + "], position = [" + position + "]");
|
||||
if (holder instanceof InfoItemHolder) {
|
||||
// If header isn't null, offset the items by -1
|
||||
if (header != null) position--;
|
||||
|
||||
((InfoItemHolder) holder).updateFromItem(infoItemList.get(position));
|
||||
((InfoItemHolder) holder).updateFromItem(infoItemList.get(position), recordManager);
|
||||
} else if (holder instanceof HFHolder && position == 0 && header != null) {
|
||||
((HFHolder) holder).view = header;
|
||||
} else if (holder instanceof HFHolder && position == sizeConsideringHeaderOffset() && footer != null && showFooter) {
|
||||
@@ -276,6 +301,21 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
if (!payloads.isEmpty() && holder instanceof InfoItemHolder) {
|
||||
for (Object payload : payloads) {
|
||||
if (payload instanceof StreamStateEntity) {
|
||||
((InfoItemHolder) holder).updateState(infoItemList.get(header == null ? position : position - 1), recordManager);
|
||||
} else if (payload instanceof Boolean) {
|
||||
((InfoItemHolder) holder).updateState(infoItemList.get(header == null ? position : position - 1), recordManager);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onBindViewHolder(holder, position);
|
||||
}
|
||||
}
|
||||
|
||||
public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) {
|
||||
return new GridLayoutManager.SpanSizeLookup() {
|
||||
@Override
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
/*
|
||||
@@ -38,8 +39,8 @@ public class ChannelInfoItemHolder extends ChannelMiniInfoItemHolder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem) {
|
||||
super.updateFromItem(infoItem);
|
||||
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
|
||||
super.updateFromItem(infoItem, historyRecordManager);
|
||||
|
||||
if (!(infoItem instanceof ChannelInfoItem)) return;
|
||||
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
@@ -30,7 +31,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem) {
|
||||
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
|
||||
if (!(infoItem instanceof ChannelInfoItem)) return;
|
||||
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 12.02.17.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* ChannelInfoItemHolder .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 CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
|
||||
|
||||
public final TextView itemTitleView;
|
||||
|
||||
public CommentsInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_comments_item, parent);
|
||||
|
||||
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
|
||||
super.updateFromItem(infoItem, historyRecordManager);
|
||||
|
||||
if (!(infoItem instanceof CommentsInfoItem)) return;
|
||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||
|
||||
itemTitleView.setText(item.getAuthorName());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.text.util.Linkify;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.jsoup.helper.StringUtil;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.CommentTextOnTouchListener;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
|
||||
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
public final CircleImageView itemThumbnailView;
|
||||
private final TextView itemContentView;
|
||||
private final TextView itemLikesCountView;
|
||||
private final TextView itemDislikesCountView;
|
||||
private final TextView itemPublishedTime;
|
||||
|
||||
private static final int commentDefaultLines = 2;
|
||||
private static final int commentExpandedLines = 1000;
|
||||
|
||||
private String commentText;
|
||||
private String streamUrl;
|
||||
|
||||
private static final Pattern pattern = Pattern.compile("(\\d+:)?(\\d+)?:(\\d+)");
|
||||
|
||||
private final Linkify.TransformFilter timestampLink = new Linkify.TransformFilter() {
|
||||
@Override
|
||||
public String transformUrl(Matcher match, String url) {
|
||||
int timestamp = 0;
|
||||
String hours = match.group(1);
|
||||
String minutes = match.group(2);
|
||||
String seconds = match.group(3);
|
||||
if(hours != null) timestamp += (Integer.parseInt(hours.replace(":", ""))*3600);
|
||||
if(minutes != null) timestamp += (Integer.parseInt(minutes.replace(":", ""))*60);
|
||||
if(seconds != null) timestamp += (Integer.parseInt(seconds));
|
||||
return streamUrl + url.replace(match.group(0), "#timestamp=" + timestamp);
|
||||
}
|
||||
};
|
||||
|
||||
CommentsMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
|
||||
super(infoItemBuilder, layoutId, parent);
|
||||
|
||||
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
|
||||
itemDislikesCountView = itemView.findViewById(R.id.detail_thumbs_down_count_view);
|
||||
itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime);
|
||||
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
||||
}
|
||||
|
||||
public CommentsMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
this(infoItemBuilder, R.layout.list_comments_mini_item, parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
|
||||
if (!(infoItem instanceof CommentsInfoItem)) return;
|
||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.getAuthorThumbnail(),
|
||||
itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
|
||||
itemThumbnailView.setOnClickListener(view -> {
|
||||
if(StringUtil.isBlank(item.getAuthorEndpoint())) return;
|
||||
try {
|
||||
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
|
||||
NavigationHelper.openChannelFragment(
|
||||
activity.getSupportFragmentManager(),
|
||||
item.getServiceId(),
|
||||
item.getAuthorEndpoint(),
|
||||
item.getAuthorName());
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) itemBuilder.getContext(), e);
|
||||
}
|
||||
});
|
||||
|
||||
streamUrl = item.getUrl();
|
||||
|
||||
itemContentView.setLines(commentDefaultLines);
|
||||
commentText = item.getCommentText();
|
||||
itemContentView.setText(commentText);
|
||||
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
|
||||
|
||||
if (itemContentView.getLineCount() == 0) {
|
||||
itemContentView.post(this::ellipsize);
|
||||
} else {
|
||||
ellipsize();
|
||||
}
|
||||
|
||||
if (null != item.getLikeCount()) {
|
||||
itemLikesCountView.setText(String.valueOf(item.getLikeCount()));
|
||||
}
|
||||
itemPublishedTime.setText(item.getPublishedTime());
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
toggleEllipsize();
|
||||
if (itemBuilder.getOnCommentsSelectedListener() != null) {
|
||||
itemBuilder.getOnCommentsSelectedListener().selected(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void ellipsize() {
|
||||
if (itemContentView.getLineCount() > commentDefaultLines){
|
||||
int endOfLastLine = itemContentView.getLayout().getLineEnd(commentDefaultLines - 1);
|
||||
int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine -2);
|
||||
if(end == -1) end = Math.max(endOfLastLine -2, 0);
|
||||
String newVal = itemContentView.getText().subSequence(0, end) + " …";
|
||||
itemContentView.setText(newVal);
|
||||
}
|
||||
linkify();
|
||||
}
|
||||
|
||||
private void toggleEllipsize() {
|
||||
if (itemContentView.getText().toString().equals(commentText)) {
|
||||
if (itemContentView.getLineCount() > commentDefaultLines) ellipsize();
|
||||
} else {
|
||||
expand();
|
||||
}
|
||||
}
|
||||
|
||||
private void expand() {
|
||||
itemContentView.setMaxLines(commentExpandedLines);
|
||||
itemContentView.setText(commentText);
|
||||
linkify();
|
||||
}
|
||||
|
||||
private void linkify(){
|
||||
Linkify.addLinks(itemContentView, Linkify.WEB_URLS);
|
||||
Linkify.addLinks(itemContentView, pattern, null, null, timestampLink);
|
||||
itemContentView.setMovementMethod(null);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 12.02.17.
|
||||
@@ -35,5 +36,8 @@ public abstract class InfoItemHolder extends RecyclerView.ViewHolder {
|
||||
this.itemBuilder = infoItemBuilder;
|
||||
}
|
||||
|
||||
public abstract void updateFromItem(final InfoItem infoItem);
|
||||
public abstract void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager);
|
||||
|
||||
public void updateState(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
|
||||
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||
@@ -30,7 +31,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem) {
|
||||
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
|
||||
if (!(infoItem instanceof PlaylistInfoItem)) return;
|
||||
final PlaylistInfoItem item = (PlaylistInfoItem) infoItem;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
/*
|
||||
@@ -40,8 +41,8 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem) {
|
||||
super.updateFromItem(infoItem);
|
||||
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
|
||||
super.updateFromItem(infoItem, historyRecordManager);
|
||||
|
||||
if (!(infoItem instanceof StreamInfoItem)) return;
|
||||
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
||||
|
||||
@@ -7,12 +7,18 @@ import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
|
||||
@@ -20,6 +26,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
public final TextView itemVideoTitleView;
|
||||
public final TextView itemUploaderView;
|
||||
public final TextView itemDurationView;
|
||||
public final AnimatedProgressBar itemProgressView;
|
||||
|
||||
StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
|
||||
super(infoItemBuilder, layoutId, parent);
|
||||
@@ -28,6 +35,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView);
|
||||
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
|
||||
itemDurationView = itemView.findViewById(R.id.itemDurationView);
|
||||
itemProgressView = itemView.findViewById(R.id.itemProgressView);
|
||||
}
|
||||
|
||||
public StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
@@ -35,7 +43,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem) {
|
||||
public void updateFromItem(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
|
||||
if (!(infoItem instanceof StreamInfoItem)) return;
|
||||
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
||||
|
||||
@@ -47,13 +55,24 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||
R.color.duration_background_color));
|
||||
itemDurationView.setVisibility(View.VISIBLE);
|
||||
|
||||
StreamStateEntity state2 = historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
|
||||
if (state2 != null) {
|
||||
itemProgressView.setVisibility(View.VISIBLE);
|
||||
itemProgressView.setMax((int) item.getDuration());
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state2.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
} else if (item.getStreamType() == StreamType.LIVE_STREAM) {
|
||||
itemDurationView.setText(R.string.duration_live);
|
||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||
R.color.live_duration_background_color));
|
||||
itemDurationView.setVisibility(View.VISIBLE);
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
} else {
|
||||
itemDurationView.setVisibility(View.GONE);
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||
@@ -83,6 +102,24 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateState(final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
|
||||
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
||||
|
||||
StreamStateEntity state = historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
|
||||
if (state != null && item.getDuration() > 0 && item.getStreamType() != StreamType.LIVE_STREAM) {
|
||||
itemProgressView.setMax((int) item.getDuration());
|
||||
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||
AnimationUtils.animateView(itemProgressView, true, 500);
|
||||
}
|
||||
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
AnimationUtils.animateView(itemProgressView, false, 500);
|
||||
}
|
||||
}
|
||||
|
||||
private void enableLongClick(final StreamInfoItem item) {
|
||||
itemView.setLongClickable(true);
|
||||
itemView.setOnLongClickListener(view -> {
|
||||
@@ -97,4 +134,4 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
itemView.setLongClickable(false);
|
||||
itemView.setOnLongClickListener(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.schabi.newpipe.local;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
@@ -8,6 +10,8 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.local.holder.LocalItemHolder;
|
||||
import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder;
|
||||
import org.schabi.newpipe.local.holder.LocalPlaylistItemHolder;
|
||||
@@ -64,6 +68,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
||||
|
||||
private final LocalItemBuilder localItemBuilder;
|
||||
private final ArrayList<LocalItem> localItems;
|
||||
private final HistoryRecordManager recordManager;
|
||||
private final DateFormat dateFormat;
|
||||
|
||||
private boolean showFooter = false;
|
||||
@@ -71,11 +76,12 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
||||
private View header = null;
|
||||
private View footer = null;
|
||||
|
||||
public LocalItemListAdapter(Activity activity) {
|
||||
localItemBuilder = new LocalItemBuilder(activity);
|
||||
public LocalItemListAdapter(Context context) {
|
||||
recordManager = new HistoryRecordManager(context);
|
||||
localItemBuilder = new LocalItemBuilder(context);
|
||||
localItems = new ArrayList<>();
|
||||
dateFormat = DateFormat.getDateInstance(DateFormat.SHORT,
|
||||
Localization.getPreferredLocale(activity));
|
||||
Localization.getPreferredLocale(context));
|
||||
}
|
||||
|
||||
public void setSelectedListener(OnClickGesture<LocalItem> listener) {
|
||||
@@ -86,38 +92,33 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
||||
localItemBuilder.setOnItemSelectedListener(null);
|
||||
}
|
||||
|
||||
public void addItems(List<? extends LocalItem> data) {
|
||||
if (data != null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addItems() before > localItems.size() = " +
|
||||
localItems.size() + ", data.size() = " + data.size());
|
||||
}
|
||||
public void addItems(@Nullable List<? extends LocalItem> data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
if (DEBUG) Log.d(TAG, "addItems() before > localItems.size() = " +
|
||||
localItems.size() + ", data.size() = " + data.size());
|
||||
|
||||
int offsetStart = sizeConsideringHeader();
|
||||
localItems.addAll(data);
|
||||
int offsetStart = sizeConsideringHeader();
|
||||
localItems.addAll(data);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addItems() after > offsetStart = " + offsetStart +
|
||||
", localItems.size() = " + localItems.size() +
|
||||
", header = " + header + ", footer = " + footer +
|
||||
", showFooter = " + showFooter);
|
||||
}
|
||||
if (DEBUG) Log.d(TAG, "addItems() after > offsetStart = " + offsetStart +
|
||||
", localItems.size() = " + localItems.size() +
|
||||
", header = " + header + ", footer = " + footer +
|
||||
", showFooter = " + showFooter);
|
||||
notifyItemRangeInserted(offsetStart, data.size());
|
||||
|
||||
notifyItemRangeInserted(offsetStart, data.size());
|
||||
if (footer != null && showFooter) {
|
||||
int footerNow = sizeConsideringHeader();
|
||||
notifyItemMoved(offsetStart, footerNow);
|
||||
|
||||
if (footer != null && showFooter) {
|
||||
int footerNow = sizeConsideringHeader();
|
||||
notifyItemMoved(offsetStart, footerNow);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart +
|
||||
" to " + footerNow);
|
||||
}
|
||||
if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart +
|
||||
" to " + footerNow);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeItem(final LocalItem data) {
|
||||
final int index = localItems.indexOf(data);
|
||||
|
||||
localItems.remove(index);
|
||||
notifyItemRemoved(index + (header != null ? 1 : 0));
|
||||
}
|
||||
@@ -219,8 +220,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int type) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateViewHolder() called with: parent = [" +
|
||||
parent + "], type = [" + type + "]");
|
||||
switch (type) {
|
||||
@@ -251,7 +253,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
if (DEBUG) Log.d(TAG, "onBindViewHolder() called with: holder = [" +
|
||||
holder.getClass().getSimpleName() + "], position = [" + position + "]");
|
||||
|
||||
@@ -259,7 +261,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
||||
// If header isn't null, offset the items by -1
|
||||
if (header != null) position--;
|
||||
|
||||
((LocalItemHolder) holder).updateFromItem(localItems.get(position), dateFormat);
|
||||
((LocalItemHolder) holder).updateFromItem(localItems.get(position), recordManager, dateFormat);
|
||||
} else if (holder instanceof HeaderFooterHolder && position == 0 && header != null) {
|
||||
((HeaderFooterHolder) holder).view = header;
|
||||
} else if (holder instanceof HeaderFooterHolder && position == sizeConsideringHeader()
|
||||
@@ -268,6 +270,21 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
if (!payloads.isEmpty() && holder instanceof LocalItemHolder) {
|
||||
for (Object payload : payloads) {
|
||||
if (payload instanceof StreamStateEntity) {
|
||||
((LocalItemHolder) holder).updateState(localItems.get(header == null ? position : position - 1), recordManager);
|
||||
} else if (payload instanceof Boolean) {
|
||||
((LocalItemHolder) holder).updateState(localItems.get(header == null ? position : position - 1), recordManager);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onBindViewHolder(holder, position);
|
||||
}
|
||||
}
|
||||
|
||||
public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) {
|
||||
return new GridLayoutManager.SpanSizeLookup() {
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.schabi.newpipe.local.dialog;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
@@ -28,7 +27,7 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
|
||||
public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
private static final String TAG = PlaylistAppendDialog.class.getCanonicalName();
|
||||
@@ -36,7 +35,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
private RecyclerView playlistRecyclerView;
|
||||
private LocalItemListAdapter playlistAdapter;
|
||||
|
||||
private Disposable playlistReactor;
|
||||
private CompositeDisposable playlistDisposables = new CompositeDisposable();
|
||||
|
||||
public static PlaylistAppendDialog fromStreamInfo(final StreamInfo info) {
|
||||
PlaylistAppendDialog dialog = new PlaylistAppendDialog();
|
||||
@@ -99,9 +98,9 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
final View newPlaylistButton = view.findViewById(R.id.newPlaylist);
|
||||
newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog());
|
||||
|
||||
playlistReactor = playlistManager.getPlaylists()
|
||||
playlistDisposables.add(playlistManager.getPlaylists()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::onPlaylistsReceived);
|
||||
.subscribe(this::onPlaylistsReceived));
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -111,10 +110,12 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
if (playlistReactor != null) playlistReactor.dispose();
|
||||
if (playlistAdapter != null) playlistAdapter.unsetSelectedListener();
|
||||
playlistDisposables.dispose();
|
||||
if (playlistAdapter != null) {
|
||||
playlistAdapter.unsetSelectedListener();
|
||||
}
|
||||
|
||||
playlistReactor = null;
|
||||
playlistDisposables.clear();
|
||||
playlistRecyclerView = null;
|
||||
playlistAdapter = null;
|
||||
}
|
||||
@@ -148,13 +149,12 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
@NonNull List<StreamEntity> streams) {
|
||||
if (getStreams() == null) return;
|
||||
|
||||
@SuppressLint("ShowToast")
|
||||
final Toast successToast = Toast.makeText(getContext(),
|
||||
R.string.playlist_add_stream_success, Toast.LENGTH_SHORT);
|
||||
|
||||
manager.appendToPlaylist(playlist.uid, streams)
|
||||
playlistDisposables.add(manager.appendToPlaylist(playlist.uid, streams)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> successToast.show());
|
||||
.subscribe(ignored -> successToast.show()));
|
||||
|
||||
getDialog().dismiss();
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionService;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
@@ -262,7 +262,7 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
|
||||
* If chosen feed already displayed, then we request another feed from another
|
||||
* subscription, until the subscription table runs out of new items.
|
||||
* <p>
|
||||
* This Observer is self-contained and will dispose itself when complete. However, this
|
||||
* This Observer is self-contained and will close itself when complete. However, this
|
||||
* does not obey the fragment lifecycle and may continue running in the background
|
||||
* until it is complete. This is done due to RxJava2 no longer propagate errors once
|
||||
* an observer is unsubscribed while the thread process is still running.
|
||||
|
||||
@@ -26,25 +26,32 @@ import android.support.annotation.NonNull;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.AppDatabase;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
||||
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
import org.schabi.newpipe.database.stream.dao.StreamDAO;
|
||||
import org.schabi.newpipe.database.stream.dao.StreamStateDAO;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Maybe;
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
@@ -80,9 +87,9 @@ public class HistoryRecordManager {
|
||||
final Date currentTime = new Date();
|
||||
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
||||
final long streamId = streamTable.upsert(new StreamEntity(info));
|
||||
StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry();
|
||||
StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
|
||||
|
||||
if (latestEntry != null && latestEntry.getStreamUid() == streamId) {
|
||||
if (latestEntry != null) {
|
||||
streamHistoryTable.delete(latestEntry);
|
||||
latestEntry.setAccessDate(currentTime);
|
||||
latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1);
|
||||
@@ -99,7 +106,12 @@ public class HistoryRecordManager {
|
||||
}
|
||||
|
||||
public Single<Integer> deleteWholeStreamHistory() {
|
||||
return Single.fromCallable(() -> streamHistoryTable.deleteAll())
|
||||
return Single.fromCallable(streamHistoryTable::deleteAll)
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<Integer> deleteCompelteStreamStateHistory() {
|
||||
return Single.fromCallable(streamStateTable::deleteAll)
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
@@ -159,8 +171,8 @@ public class HistoryRecordManager {
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<Integer> deleteWholeSearchHistory() {
|
||||
return Single.fromCallable(() -> searchHistoryTable.deleteAll())
|
||||
public Single<Integer> deleteCompleteSearchHistory() {
|
||||
return Single.fromCallable(searchHistoryTable::deleteAll)
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
@@ -180,21 +192,104 @@ public class HistoryRecordManager {
|
||||
// Stream State History
|
||||
///////////////////////////////////////////////////////
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Maybe<StreamStateEntity> loadStreamState(final StreamInfo info) {
|
||||
return Maybe.fromCallable(() -> streamTable.upsert(new StreamEntity(info)))
|
||||
.flatMap(streamId -> streamStateTable.getState(streamId).firstElement())
|
||||
.flatMap(states -> states.isEmpty() ? Maybe.empty() : Maybe.just(states.get(0)))
|
||||
public Maybe<StreamHistoryEntity> getStreamHistory(final StreamInfo info) {
|
||||
return Maybe.fromCallable(() -> {
|
||||
final long streamId = streamTable.upsert(new StreamEntity(info));
|
||||
return streamHistoryTable.getLatestEntry(streamId);
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Maybe<StreamStateEntity> loadStreamState(final PlayQueueItem queueItem) {
|
||||
return queueItem.getStream()
|
||||
.map((info) -> streamTable.upsert(new StreamEntity(info)))
|
||||
.flatMapPublisher(streamStateTable::getState)
|
||||
.firstElement()
|
||||
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
|
||||
.filter(state -> state.isValid((int) queueItem.getDuration()))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Maybe<Long> saveStreamState(@NonNull final StreamInfo info, final long progressTime) {
|
||||
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
||||
public Maybe<StreamStateEntity> loadStreamState(final StreamInfo info) {
|
||||
return Single.fromCallable(() -> streamTable.upsert(new StreamEntity(info)))
|
||||
.flatMapPublisher(streamStateTable::getState)
|
||||
.firstElement()
|
||||
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
|
||||
.filter(state -> state.isValid((int) info.getDuration()))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Completable saveStreamState(@NonNull final StreamInfo info, final long progressTime) {
|
||||
return Completable.fromAction(() -> database.runInTransaction(() -> {
|
||||
final long streamId = streamTable.upsert(new StreamEntity(info));
|
||||
return streamStateTable.upsert(new StreamStateEntity(streamId, progressTime));
|
||||
final StreamStateEntity state = new StreamStateEntity(streamId, progressTime);
|
||||
if (state.isValid((int) info.getDuration())) {
|
||||
streamStateTable.upsert(state);
|
||||
} else {
|
||||
streamStateTable.deleteState(streamId);
|
||||
}
|
||||
})).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<StreamStateEntity[]> loadStreamState(final InfoItem info) {
|
||||
return Single.fromCallable(() -> {
|
||||
final List<StreamEntity> entities = streamTable.getStream(info.getServiceId(), info.getUrl()).blockingFirst();
|
||||
if (entities.isEmpty()) {
|
||||
return new StreamStateEntity[]{null};
|
||||
}
|
||||
final List<StreamStateEntity> states = streamStateTable.getState(entities.get(0).getUid()).blockingFirst();
|
||||
if (states.isEmpty()) {
|
||||
return new StreamStateEntity[]{null};
|
||||
}
|
||||
return new StreamStateEntity[]{states.get(0)};
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<List<StreamStateEntity>> loadStreamStateBatch(final List<InfoItem> infos) {
|
||||
return Single.fromCallable(() -> {
|
||||
final List<StreamStateEntity> result = new ArrayList<>(infos.size());
|
||||
for (InfoItem info : infos) {
|
||||
final List<StreamEntity> entities = streamTable.getStream(info.getServiceId(), info.getUrl()).blockingFirst();
|
||||
if (entities.isEmpty()) {
|
||||
result.add(null);
|
||||
continue;
|
||||
}
|
||||
final List<StreamStateEntity> states = streamStateTable.getState(entities.get(0).getUid()).blockingFirst();
|
||||
if (states.isEmpty()) {
|
||||
result.add(null);
|
||||
continue;
|
||||
}
|
||||
result.add(states.get(0));
|
||||
}
|
||||
return result;
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<List<StreamStateEntity>> loadLocalStreamStateBatch(final List<? extends LocalItem> items) {
|
||||
return Single.fromCallable(() -> {
|
||||
final List<StreamStateEntity> result = new ArrayList<>(items.size());
|
||||
for (LocalItem item : items) {
|
||||
long streamId;
|
||||
if (item instanceof StreamStatisticsEntry) {
|
||||
streamId = ((StreamStatisticsEntry) item).streamId;
|
||||
} else if (item instanceof PlaylistStreamEntity) {
|
||||
streamId = ((PlaylistStreamEntity) item).getStreamUid();
|
||||
} else if (item instanceof PlaylistStreamEntry) {
|
||||
streamId = ((PlaylistStreamEntry) item).streamId;
|
||||
} else {
|
||||
result.add(null);
|
||||
continue;
|
||||
}
|
||||
final List<StreamStateEntity> states = streamStateTable.getState(streamId).blockingFirst();
|
||||
if (states.isEmpty()) {
|
||||
result.add(null);
|
||||
continue;
|
||||
}
|
||||
result.add(states.get(0));
|
||||
}
|
||||
return result;
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////
|
||||
// Utility
|
||||
///////////////////////////////////////////////////////
|
||||
@@ -202,4 +297,5 @@ public class HistoryRecordManager {
|
||||
public Single<Integer> removeOrphanedRecords() {
|
||||
return Single.fromCallable(streamTable::deleteOrphans).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@ package org.schabi.newpipe.local.history;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
@@ -19,15 +22,20 @@ import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -104,6 +112,12 @@ public class StatisticsPlaylistFragment
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
inflater.inflate(R.menu.menu_history, menu);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment LifeCycle - Views
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
@@ -155,6 +169,53 @@ public class StatisticsPlaylistFragment
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_history_clear:
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.delete_view_history_alert)
|
||||
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
|
||||
.setPositiveButton(R.string.delete, ((dialog, which) -> {
|
||||
final Disposable onDelete = recordManager.deleteWholeStreamHistory()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
howManyDeleted -> Toast.makeText(getContext(),
|
||||
R.string.watch_history_deleted,
|
||||
Toast.LENGTH_SHORT).show(),
|
||||
throwable -> ErrorActivity.reportError(getContext(),
|
||||
throwable,
|
||||
SettingsActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(
|
||||
UserAction.DELETE_FROM_HISTORY,
|
||||
"none",
|
||||
"Delete view history",
|
||||
R.string.general_error)));
|
||||
|
||||
final Disposable onClearOrphans = recordManager.removeOrphanedRecords()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
howManyDeleted -> {},
|
||||
throwable -> ErrorActivity.reportError(getContext(),
|
||||
throwable,
|
||||
SettingsActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(
|
||||
UserAction.DELETE_FROM_HISTORY,
|
||||
"none",
|
||||
"Delete search history",
|
||||
R.string.general_error)));
|
||||
disposables.add(onClearOrphans);
|
||||
disposables.add(onDelete);
|
||||
}))
|
||||
.create()
|
||||
.show();
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment LifeCycle - Loading
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
@@ -250,11 +311,11 @@ public class StatisticsPlaylistFragment
|
||||
}
|
||||
|
||||
headerPlayAllButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
|
||||
headerPopupButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||
headerBackgroundButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
sortButton.setOnClickListener(view -> toggleSortMode());
|
||||
|
||||
hideLoading();
|
||||
@@ -297,52 +358,44 @@ public class StatisticsPlaylistFragment
|
||||
startLoading(true);
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueueStartingAt(StreamStatisticsEntry infoItem) {
|
||||
return getPlayQueue(Math.max(itemListAdapter.getItemsList().indexOf(infoItem), 0));
|
||||
}
|
||||
|
||||
private void showStreamDialog(final StreamStatisticsEntry item) {
|
||||
final Context context = getContext();
|
||||
final Activity activity = getActivity();
|
||||
if (context == null || context.getResources() == null || getActivity() == null) return;
|
||||
if (context == null || context.getResources() == null || activity == null) return;
|
||||
final StreamInfoItem infoItem = item.toStreamInfoItem();
|
||||
|
||||
final String[] commands = new String[]{
|
||||
context.getResources().getString(R.string.enqueue_on_background),
|
||||
context.getResources().getString(R.string.enqueue_on_popup),
|
||||
context.getResources().getString(R.string.start_here_on_main),
|
||||
context.getResources().getString(R.string.start_here_on_background),
|
||||
context.getResources().getString(R.string.start_here_on_popup),
|
||||
context.getResources().getString(R.string.delete),
|
||||
context.getResources().getString(R.string.share)
|
||||
};
|
||||
if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) {
|
||||
StreamDialogEntry.setEnabledEntries(
|
||||
StreamDialogEntry.enqueue_on_background,
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
StreamDialogEntry.delete,
|
||||
StreamDialogEntry.append_playlist,
|
||||
StreamDialogEntry.share);
|
||||
} else {
|
||||
StreamDialogEntry.setEnabledEntries(
|
||||
StreamDialogEntry.enqueue_on_background,
|
||||
StreamDialogEntry.enqueue_on_popup,
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
StreamDialogEntry.start_here_on_popup,
|
||||
StreamDialogEntry.delete,
|
||||
StreamDialogEntry.append_playlist,
|
||||
StreamDialogEntry.share);
|
||||
|
||||
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||
final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0);
|
||||
switch (i) {
|
||||
case 0:
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(infoItem));
|
||||
break;
|
||||
case 1:
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(infoItem));
|
||||
break;
|
||||
case 2:
|
||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
|
||||
break;
|
||||
case 3:
|
||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
|
||||
break;
|
||||
case 4:
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||
break;
|
||||
case 5:
|
||||
deleteEntry(index);
|
||||
break;
|
||||
case 6:
|
||||
shareUrl(item.toStreamInfoItem().getName(), item.toStreamInfoItem().getUrl());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
StreamDialogEntry.start_here_on_popup.setCustomAction(
|
||||
(fragment, infoItemDuplicate) -> NavigationHelper.playOnPopupPlayer(context, getPlayQueueStartingAt(item), true));
|
||||
}
|
||||
|
||||
new InfoItemDialog(getActivity(), infoItem, commands, actions).show();
|
||||
StreamDialogEntry.start_here_on_background.setCustomAction(
|
||||
(fragment, infoItemDuplicate) -> NavigationHelper.playOnBackgroundPlayer(context, getPlayQueueStartingAt(item), true));
|
||||
StreamDialogEntry.delete.setCustomAction((fragment, infoItemDuplicate) ->
|
||||
deleteEntry(Math.max(itemListAdapter.getItemsList().indexOf(item), 0)));
|
||||
|
||||
new InfoItemDialog(activity, infoItem, StreamDialogEntry.getCommands(context), (dialog, which) ->
|
||||
StreamDialogEntry.clickOn(which, this, infoItem)).show();
|
||||
}
|
||||
|
||||
private void deleteEntry(final int index) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
|
||||
import java.text.DateFormat;
|
||||
|
||||
@@ -38,5 +39,8 @@ public abstract class LocalItemHolder extends RecyclerView.ViewHolder {
|
||||
this.itemBuilder = itemBuilder;
|
||||
}
|
||||
|
||||
public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat);
|
||||
public abstract void updateFromItem(final LocalItem item, HistoryRecordManager historyRecordManager, final DateFormat dateFormat);
|
||||
|
||||
public void updateState(final LocalItem localItem, HistoryRecordManager historyRecordManager) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.view.ViewGroup;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
|
||||
import java.text.DateFormat;
|
||||
@@ -21,7 +22,7 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
||||
public void updateFromItem(final LocalItem localItem, HistoryRecordManager historyRecordManager, final DateFormat dateFormat) {
|
||||
if (!(localItem instanceof PlaylistMetadataEntry)) return;
|
||||
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
|
||||
|
||||
@@ -32,6 +33,6 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
|
||||
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
||||
|
||||
super.updateFromItem(localItem, dateFormat);
|
||||
super.updateFromItem(localItem, historyRecordManager, dateFormat);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,18 @@ import android.widget.TextView;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
|
||||
@@ -24,6 +30,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
public final TextView itemAdditionalDetailsView;
|
||||
public final TextView itemDurationView;
|
||||
public final View itemHandleView;
|
||||
public final AnimatedProgressBar itemProgressView;
|
||||
|
||||
LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
|
||||
super(infoItemBuilder, layoutId, parent);
|
||||
@@ -33,6 +40,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
itemAdditionalDetailsView = itemView.findViewById(R.id.itemAdditionalDetails);
|
||||
itemDurationView = itemView.findViewById(R.id.itemDurationView);
|
||||
itemHandleView = itemView.findViewById(R.id.itemHandle);
|
||||
itemProgressView = itemView.findViewById(R.id.itemProgressView);
|
||||
}
|
||||
|
||||
public LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
@@ -40,7 +48,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
||||
public void updateFromItem(final LocalItem localItem, HistoryRecordManager historyRecordManager, final DateFormat dateFormat) {
|
||||
if (!(localItem instanceof PlaylistStreamEntry)) return;
|
||||
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
|
||||
|
||||
@@ -53,6 +61,15 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||
R.color.duration_background_color));
|
||||
itemDurationView.setVisibility(View.VISIBLE);
|
||||
|
||||
StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{ add(localItem); }}).blockingGet().get(0);
|
||||
if (state != null) {
|
||||
itemProgressView.setVisibility(View.VISIBLE);
|
||||
itemProgressView.setMax((int) item.duration);
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
itemDurationView.setVisibility(View.GONE);
|
||||
}
|
||||
@@ -79,6 +96,25 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
itemHandleView.setOnTouchListener(getOnTouchListener(item));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateState(LocalItem localItem, HistoryRecordManager historyRecordManager) {
|
||||
if (!(localItem instanceof PlaylistStreamEntry)) return;
|
||||
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
|
||||
|
||||
StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{ add(localItem); }}).blockingGet().get(0);
|
||||
if (state != null && item.duration > 0) {
|
||||
itemProgressView.setMax((int) item.duration);
|
||||
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||
AnimationUtils.animateView(itemProgressView, true, 500);
|
||||
}
|
||||
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
AnimationUtils.animateView(itemProgressView, false, 500);
|
||||
}
|
||||
}
|
||||
|
||||
private View.OnTouchListener getOnTouchListener(final PlaylistStreamEntry item) {
|
||||
return (view, motionEvent) -> {
|
||||
view.performClick();
|
||||
|
||||
@@ -10,12 +10,18 @@ import android.widget.TextView;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 01.08.16.
|
||||
@@ -45,6 +51,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
||||
public final TextView itemDurationView;
|
||||
@Nullable
|
||||
public final TextView itemAdditionalDetails;
|
||||
public final AnimatedProgressBar itemProgressView;
|
||||
|
||||
public LocalStatisticStreamItemHolder(LocalItemBuilder itemBuilder, ViewGroup parent) {
|
||||
this(itemBuilder, R.layout.list_stream_item, parent);
|
||||
@@ -58,6 +65,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
||||
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
|
||||
itemDurationView = itemView.findViewById(R.id.itemDurationView);
|
||||
itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails);
|
||||
itemProgressView = itemView.findViewById(R.id.itemProgressView);
|
||||
}
|
||||
|
||||
private String getStreamInfoDetailLine(final StreamStatisticsEntry entry,
|
||||
@@ -70,7 +78,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
||||
public void updateFromItem(final LocalItem localItem, HistoryRecordManager historyRecordManager, final DateFormat dateFormat) {
|
||||
if (!(localItem instanceof StreamStatisticsEntry)) return;
|
||||
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
|
||||
|
||||
@@ -82,8 +90,18 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||
R.color.duration_background_color));
|
||||
itemDurationView.setVisibility(View.VISIBLE);
|
||||
|
||||
StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{ add(localItem); }}).blockingGet().get(0);
|
||||
if (state != null) {
|
||||
itemProgressView.setVisibility(View.VISIBLE);
|
||||
itemProgressView.setMax((int) item.duration);
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
itemDurationView.setVisibility(View.GONE);
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (itemAdditionalDetails != null) {
|
||||
@@ -108,4 +126,23 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateState(LocalItem localItem, HistoryRecordManager historyRecordManager) {
|
||||
if (!(localItem instanceof StreamStatisticsEntry)) return;
|
||||
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
|
||||
|
||||
StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{ add(localItem); }}).blockingGet().get(0);
|
||||
if (state != null && item.duration > 0) {
|
||||
itemProgressView.setMax((int) item.duration);
|
||||
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||
AnimationUtils.animateView(itemProgressView, true, 500);
|
||||
}
|
||||
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
AnimationUtils.animateView(itemProgressView, false, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.widget.TextView;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
|
||||
import java.text.DateFormat;
|
||||
|
||||
@@ -31,7 +32,7 @@ public abstract class PlaylistItemHolder extends LocalItemHolder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
||||
public void updateFromItem(final LocalItem localItem, HistoryRecordManager historyRecordManager, final DateFormat dateFormat) {
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||
itemBuilder.getOnItemSelectedListener().selected(localItem);
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
@@ -21,7 +22,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
||||
public void updateFromItem(final LocalItem localItem, HistoryRecordManager historyRecordManager, final DateFormat dateFormat) {
|
||||
if (!(localItem instanceof PlaylistRemoteEntity)) return;
|
||||
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
|
||||
|
||||
@@ -33,6 +34,6 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
||||
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
||||
|
||||
super.updateFromItem(localItem, dateFormat);
|
||||
super.updateFromItem(localItem, historyRecordManager, dateFormat);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.schabi.newpipe.local.playlist;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
@@ -26,14 +25,16 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -318,11 +319,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
setVideoCount(itemListAdapter.getItemsList().size());
|
||||
|
||||
headerPlayAllButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
|
||||
headerPopupButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||
headerBackgroundButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
|
||||
hideLoading();
|
||||
}
|
||||
@@ -510,59 +511,48 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private PlayQueue getPlayQueueStartingAt(PlaylistStreamEntry infoItem) {
|
||||
return getPlayQueue(Math.max(itemListAdapter.getItemsList().indexOf(infoItem), 0));
|
||||
}
|
||||
|
||||
protected void showStreamItemDialog(final PlaylistStreamEntry item) {
|
||||
final Context context = getContext();
|
||||
final Activity activity = getActivity();
|
||||
if (context == null || context.getResources() == null || getActivity() == null) return;
|
||||
|
||||
if (context == null || context.getResources() == null || activity == null) return;
|
||||
final StreamInfoItem infoItem = item.toStreamInfoItem();
|
||||
|
||||
final String[] commands = new String[]{
|
||||
context.getResources().getString(R.string.enqueue_on_background),
|
||||
context.getResources().getString(R.string.enqueue_on_popup),
|
||||
context.getResources().getString(R.string.start_here_on_main),
|
||||
context.getResources().getString(R.string.start_here_on_background),
|
||||
context.getResources().getString(R.string.start_here_on_popup),
|
||||
context.getResources().getString(R.string.set_as_playlist_thumbnail),
|
||||
context.getResources().getString(R.string.delete),
|
||||
context.getResources().getString(R.string.share)
|
||||
};
|
||||
if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) {
|
||||
StreamDialogEntry.setEnabledEntries(
|
||||
StreamDialogEntry.enqueue_on_background,
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
StreamDialogEntry.set_as_playlist_thumbnail,
|
||||
StreamDialogEntry.delete,
|
||||
StreamDialogEntry.append_playlist,
|
||||
StreamDialogEntry.share);
|
||||
} else {
|
||||
StreamDialogEntry.setEnabledEntries(
|
||||
StreamDialogEntry.enqueue_on_background,
|
||||
StreamDialogEntry.enqueue_on_popup,
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
StreamDialogEntry.start_here_on_popup,
|
||||
StreamDialogEntry.set_as_playlist_thumbnail,
|
||||
StreamDialogEntry.delete,
|
||||
StreamDialogEntry.append_playlist,
|
||||
StreamDialogEntry.share);
|
||||
|
||||
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||
final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0);
|
||||
switch (i) {
|
||||
case 0:
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context,
|
||||
new SinglePlayQueue(infoItem));
|
||||
break;
|
||||
case 1:
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new
|
||||
SinglePlayQueue(infoItem));
|
||||
break;
|
||||
case 2:
|
||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
|
||||
break;
|
||||
case 3:
|
||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
|
||||
break;
|
||||
case 4:
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||
break;
|
||||
case 5:
|
||||
changeThumbnailUrl(item.thumbnailUrl);
|
||||
break;
|
||||
case 6:
|
||||
deleteItem(item);
|
||||
break;
|
||||
case 7:
|
||||
shareUrl(item.toStreamInfoItem().getName(), item.toStreamInfoItem().getUrl());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
StreamDialogEntry.start_here_on_popup.setCustomAction(
|
||||
(fragment, infoItemDuplicate) -> NavigationHelper.playOnPopupPlayer(context, getPlayQueueStartingAt(item), true));
|
||||
}
|
||||
|
||||
new InfoItemDialog(getActivity(), infoItem, commands, actions).show();
|
||||
StreamDialogEntry.start_here_on_background.setCustomAction(
|
||||
(fragment, infoItemDuplicate) -> NavigationHelper.playOnBackgroundPlayer(context, getPlayQueueStartingAt(item), true));
|
||||
StreamDialogEntry.set_as_playlist_thumbnail.setCustomAction(
|
||||
(fragment, infoItemDuplicate) -> changeThumbnailUrl(item.thumbnailUrl));
|
||||
StreamDialogEntry.delete.setCustomAction(
|
||||
(fragment, infoItemDuplicate) -> deleteItem(item));
|
||||
|
||||
new InfoItemDialog(activity, infoItem, StreamDialogEntry.getCommands(context), (dialog, which) ->
|
||||
StreamDialogEntry.clickOn(which, this, infoItem)).show();
|
||||
}
|
||||
|
||||
private void setInitialData(long playlistId, String name) {
|
||||
|
||||
@@ -23,7 +23,6 @@ import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
@@ -48,15 +47,14 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService;
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.CollapsibleView;
|
||||
|
||||
@@ -130,6 +128,11 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
||||
subscriptionService = SubscriptionService.getInstance(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||
@@ -376,7 +379,6 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
||||
|
||||
});
|
||||
|
||||
//noinspection ConstantConditions
|
||||
whatsNewItemListHeader.setOnClickListener(v -> {
|
||||
FragmentManager fragmentManager = getFM();
|
||||
NavigationHelper.openWhatsNewFragment(fragmentManager);
|
||||
@@ -390,17 +392,17 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
||||
if (context == null || context.getResources() == null || getActivity() == null) return;
|
||||
|
||||
final String[] commands = new String[]{
|
||||
context.getResources().getString(R.string.share),
|
||||
context.getResources().getString(R.string.unsubscribe)
|
||||
context.getResources().getString(R.string.unsubscribe),
|
||||
context.getResources().getString(R.string.share)
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||
switch (i) {
|
||||
case 0:
|
||||
shareChannel(selectedItem);
|
||||
deleteChannel(selectedItem);
|
||||
break;
|
||||
case 1:
|
||||
deleteChannel(selectedItem);
|
||||
shareChannel(selectedItem);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -424,12 +426,12 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
||||
|
||||
}
|
||||
|
||||
private void shareChannel (ChannelInfoItem selectedItem) {
|
||||
shareUrl(selectedItem.getName(), selectedItem.getUrl());
|
||||
private void shareChannel(ChannelInfoItem selectedItem) {
|
||||
ShareUtils.shareUrl(getContext(), selectedItem.getName(), selectedItem.getUrl());
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private void deleteChannel (ChannelInfoItem selectedItem) {
|
||||
private void deleteChannel(ChannelInfoItem selectedItem) {
|
||||
subscriptionService.subscriptionTable()
|
||||
.getSubscription(selectedItem.getServiceId(), selectedItem.getUrl())
|
||||
.toObservable()
|
||||
@@ -441,7 +443,7 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
||||
|
||||
|
||||
|
||||
private Observer<List<SubscriptionEntity>> getDeleteObserver(){
|
||||
private Observer<List<SubscriptionEntity>> getDeleteObserver() {
|
||||
return new Observer<List<SubscriptionEntity>>() {
|
||||
@Override
|
||||
public void onSubscribe(Disposable d) {
|
||||
|
||||
@@ -147,11 +147,16 @@ public class SubscriptionService {
|
||||
}
|
||||
|
||||
private boolean isSubscriptionUpToDate(final ChannelInfo info, final SubscriptionEntity entity) {
|
||||
return info.getUrl().equals(entity.getUrl()) &&
|
||||
return equalsAndNotNull(info.getUrl(), entity.getUrl()) &&
|
||||
info.getServiceId() == entity.getServiceId() &&
|
||||
info.getName().equals(entity.getName()) &&
|
||||
info.getAvatarUrl().equals(entity.getAvatarUrl()) &&
|
||||
info.getDescription().equals(entity.getDescription()) &&
|
||||
equalsAndNotNull(info.getAvatarUrl(), entity.getAvatarUrl()) &&
|
||||
equalsAndNotNull(info.getDescription(), entity.getDescription()) &&
|
||||
info.getSubscriberCount() == entity.getSubscriberCount();
|
||||
}
|
||||
|
||||
private boolean equalsAndNotNull(final Object o1, final Object o2) {
|
||||
return (o1 != null && o2 != null)
|
||||
&& o1.equals(o2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
|
||||
/**
|
||||
* Fixes a leak caused by AudioManager using an Activity context.
|
||||
* Tracked at https://android-review.googlesource.com/#/c/140481/1 and
|
||||
* https://github.com/square/leakcanary/issues/205
|
||||
* Source:
|
||||
* https://gist.github.com/jankovd/891d96f476f7a9ce24e2
|
||||
*/
|
||||
public class AudioServiceLeakFix extends ContextWrapper {
|
||||
|
||||
AudioServiceLeakFix(Context base) {
|
||||
super(base);
|
||||
}
|
||||
|
||||
public static ContextWrapper preventLeakOf(Context base) {
|
||||
return new AudioServiceLeakFix(base);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getSystemService(String name) {
|
||||
if (Context.AUDIO_SERVICE.equals(name)) {
|
||||
return getApplicationContext().getSystemService(name);
|
||||
}
|
||||
return super.getSystemService(name);
|
||||
}
|
||||
}
|
||||
@@ -130,6 +130,11 @@ public final class BackgroundPlayer extends Service {
|
||||
onClose();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return mBinder;
|
||||
@@ -145,6 +150,7 @@ public final class BackgroundPlayer extends Service {
|
||||
lockManager.releaseWifiAndCpu();
|
||||
}
|
||||
if (basePlayerImpl != null) {
|
||||
basePlayerImpl.savePlaybackState();
|
||||
basePlayerImpl.stopActivityBinding();
|
||||
basePlayerImpl.destroy();
|
||||
}
|
||||
@@ -270,6 +276,8 @@ public final class BackgroundPlayer extends Service {
|
||||
protected class BasePlayerImpl extends BasePlayer {
|
||||
|
||||
@NonNull final private AudioPlaybackResolver resolver;
|
||||
private int cachedDuration;
|
||||
private String cachedDurationString;
|
||||
|
||||
BasePlayerImpl(Context context) {
|
||||
super(context);
|
||||
@@ -344,10 +352,14 @@ public final class BackgroundPlayer extends Service {
|
||||
|
||||
if (!shouldUpdateOnProgress) return;
|
||||
resetNotification();
|
||||
if(Build.VERSION.SDK_INT >= 26 /*Oreo*/) updateNotificationThumbnail();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O /*Oreo*/) updateNotificationThumbnail();
|
||||
if (bigNotRemoteView != null) {
|
||||
if(cachedDuration != duration) {
|
||||
cachedDuration = duration;
|
||||
cachedDurationString = getTimeString(duration);
|
||||
}
|
||||
bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false);
|
||||
bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + getTimeString(duration));
|
||||
bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + cachedDurationString);
|
||||
}
|
||||
if (notRemoteView != null) {
|
||||
notRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false);
|
||||
|
||||
@@ -23,9 +23,11 @@ import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.media.AudioManager;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
@@ -145,6 +147,8 @@ public abstract class BasePlayer implements
|
||||
@NonNull
|
||||
public static final String APPEND_ONLY = "append_only";
|
||||
@NonNull
|
||||
public static final String RESUME_PLAYBACK = "resume_playback";
|
||||
@NonNull
|
||||
public static final String SELECT_ON_APPEND = "select_on_append";
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -183,6 +187,7 @@ public abstract class BasePlayer implements
|
||||
protected MediaSessionManager mediaSessionManager;
|
||||
|
||||
private boolean isPrepared = false;
|
||||
private Disposable stateLoader;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@@ -207,8 +212,7 @@ public abstract class BasePlayer implements
|
||||
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
this.dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter);
|
||||
|
||||
final TrackSelection.Factory trackSelectionFactory =
|
||||
PlayerHelper.getQualitySelector(context, bandwidthMeter);
|
||||
final TrackSelection.Factory trackSelectionFactory = PlayerHelper.getQualitySelector(context);
|
||||
this.trackSelector = new CustomTrackSelector(trackSelectionFactory);
|
||||
|
||||
this.loadControl = new LoadController(context);
|
||||
@@ -225,7 +229,7 @@ public abstract class BasePlayer implements
|
||||
public void initPlayer(final boolean playOnReady) {
|
||||
if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]");
|
||||
|
||||
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl);
|
||||
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderFactory, trackSelector, loadControl);
|
||||
simpleExoPlayer.addListener(this);
|
||||
simpleExoPlayer.setPlayWhenReady(playOnReady);
|
||||
simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context));
|
||||
@@ -270,6 +274,34 @@ public abstract class BasePlayer implements
|
||||
final boolean playbackSkipSilence = intent.getBooleanExtra(PLAYBACK_SKIP_SILENCE,
|
||||
getPlaybackSkipSilence());
|
||||
|
||||
// seek to timestamp if stream is already playing
|
||||
if (simpleExoPlayer != null
|
||||
&& queue.size() == 1
|
||||
&& playQueue != null
|
||||
&& playQueue.getItem() != null
|
||||
&& queue.getItem().getUrl().equals(playQueue.getItem().getUrl())
|
||||
&& queue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET
|
||||
) {
|
||||
simpleExoPlayer.seekTo(playQueue.getIndex(), queue.getItem().getRecoveryPosition());
|
||||
return;
|
||||
|
||||
} else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) && isPlaybackResumeEnabled()) {
|
||||
final PlayQueueItem item = queue.getItem();
|
||||
if (item != null && item.getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) {
|
||||
stateLoader = recordManager.loadStreamState(item)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doFinally(() -> initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence,
|
||||
/*playOnInit=*/true))
|
||||
.subscribe(
|
||||
state -> queue.setRecovery(queue.getIndex(), state.getProgressTime()),
|
||||
error -> {
|
||||
if (DEBUG) error.printStackTrace();
|
||||
}
|
||||
);
|
||||
databaseUpdateReactor.add(stateLoader);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Good to go...
|
||||
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence,
|
||||
/*playOnInit=*/true);
|
||||
@@ -307,6 +339,7 @@ public abstract class BasePlayer implements
|
||||
if (audioReactor != null) audioReactor.dispose();
|
||||
if (playbackManager != null) playbackManager.dispose();
|
||||
if (mediaSessionManager != null) mediaSessionManager.dispose();
|
||||
if (stateLoader != null) stateLoader.dispose();
|
||||
|
||||
if (playQueueAdapter != null) {
|
||||
playQueueAdapter.unsetSelectedListener();
|
||||
@@ -604,6 +637,9 @@ public abstract class BasePlayer implements
|
||||
break;
|
||||
case Player.STATE_ENDED: // 4
|
||||
changeState(STATE_COMPLETED);
|
||||
if (currentMetadata != null) {
|
||||
resetPlaybackState(currentMetadata.getMetadata());
|
||||
}
|
||||
isPrepared = false;
|
||||
break;
|
||||
}
|
||||
@@ -710,6 +746,7 @@ public abstract class BasePlayer implements
|
||||
case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
|
||||
case DISCONTINUITY_REASON_INTERNAL:
|
||||
if (playQueue.getIndex() != newWindowIndex) {
|
||||
resetPlaybackState(playQueue.getItem());
|
||||
playQueue.setIndex(newWindowIndex);
|
||||
}
|
||||
break;
|
||||
@@ -739,6 +776,9 @@ public abstract class BasePlayer implements
|
||||
@Override
|
||||
public void onSeekProcessed() {
|
||||
if (DEBUG) Log.d(TAG, "ExoPlayer - onSeekProcessed() called");
|
||||
if (isPrepared) {
|
||||
savePlaybackState();
|
||||
}
|
||||
}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback Listener
|
||||
@@ -1006,27 +1046,46 @@ public abstract class BasePlayer implements
|
||||
}
|
||||
}
|
||||
|
||||
protected void savePlaybackState(final StreamInfo info, final long progress) {
|
||||
private void savePlaybackState(final StreamInfo info, final long progress) {
|
||||
if (info == null) return;
|
||||
final Disposable stateSaver = recordManager.saveStreamState(info, progress)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.onErrorComplete()
|
||||
.subscribe(
|
||||
ignored -> {/* successful */},
|
||||
error -> Log.e(TAG, "savePlaybackState() failure: ", error)
|
||||
);
|
||||
databaseUpdateReactor.add(stateSaver);
|
||||
if (DEBUG) Log.d(TAG, "savePlaybackState() called");
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
|
||||
final Disposable stateSaver = recordManager.saveStreamState(info, progress)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError((e) -> {
|
||||
if (DEBUG) e.printStackTrace();
|
||||
})
|
||||
.onErrorComplete()
|
||||
.subscribe();
|
||||
databaseUpdateReactor.add(stateSaver);
|
||||
}
|
||||
}
|
||||
|
||||
private void savePlaybackState() {
|
||||
private void resetPlaybackState(final PlayQueueItem queueItem) {
|
||||
if (queueItem == null) return;
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
|
||||
final Disposable stateSaver = queueItem.getStream()
|
||||
.flatMapCompletable(info -> recordManager.saveStreamState(info, 0))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError((e) -> {
|
||||
if (DEBUG) e.printStackTrace();
|
||||
})
|
||||
.onErrorComplete()
|
||||
.subscribe();
|
||||
databaseUpdateReactor.add(stateSaver);
|
||||
}
|
||||
}
|
||||
|
||||
public void resetPlaybackState(final StreamInfo info) {
|
||||
savePlaybackState(info, 0);
|
||||
}
|
||||
|
||||
public void savePlaybackState() {
|
||||
if (simpleExoPlayer == null || currentMetadata == null) return;
|
||||
final StreamInfo currentInfo = currentMetadata.getMetadata();
|
||||
|
||||
if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD_MILLIS &&
|
||||
simpleExoPlayer.getCurrentPosition() <
|
||||
simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD_MILLIS) {
|
||||
savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition());
|
||||
}
|
||||
savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition());
|
||||
}
|
||||
|
||||
private void maybeUpdateCurrentMetadata() {
|
||||
@@ -1214,4 +1273,10 @@ public abstract class BasePlayer implements
|
||||
public boolean gotDestroyed() {
|
||||
return simpleExoPlayer == null;
|
||||
}
|
||||
|
||||
private boolean isPlaybackResumeEnabled() {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)
|
||||
&& prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,13 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.database.ContentObserver;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.provider.Settings;
|
||||
import android.support.annotation.ColorInt;
|
||||
@@ -46,6 +48,7 @@ import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
@@ -75,6 +78,7 @@ import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
@@ -111,6 +115,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
private boolean isInMultiWindow;
|
||||
private boolean isBackPressed;
|
||||
|
||||
private ContentObserver rotationObserver;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Activity LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -145,6 +151,23 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
rotationObserver = new ContentObserver(new Handler()) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
super.onChange(selfChange);
|
||||
if (globalScreenOrientationLocked()) {
|
||||
final boolean lastOrientationWasLandscape = defaultPreferences.getBoolean(
|
||||
getString(R.string.last_orientation_landscape_key), false);
|
||||
setLandscape(lastOrientationWasLandscape);
|
||||
} else {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
}
|
||||
}
|
||||
};
|
||||
getContentResolver().registerContentObserver(
|
||||
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
|
||||
false, rotationObserver);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -237,10 +260,24 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
playerState = createPlayerState();
|
||||
playerImpl.destroy();
|
||||
|
||||
if (rotationObserver != null)
|
||||
getContentResolver().unregisterContentObserver(rotationObserver);
|
||||
|
||||
isInMultiWindow = false;
|
||||
isBackPressed = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(newBase));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
playerImpl.savePlaybackState();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// State Saving
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -277,14 +314,9 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
if (DEBUG) Log.d(TAG, "showSystemUi() called");
|
||||
if (playerImpl != null && playerImpl.queueVisible) return;
|
||||
|
||||
final int visibility;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
|
||||
} else {
|
||||
visibility = View.STATUS_BAR_VISIBLE;
|
||||
}
|
||||
final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
@ColorInt final int systenUiColor =
|
||||
@@ -353,11 +385,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
|
||||
protected void setShuffleButton(final ImageButton shuffleButton, final boolean shuffled) {
|
||||
final int shuffleAlpha = shuffled ? 255 : 77;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
shuffleButton.setImageAlpha(shuffleAlpha);
|
||||
} else {
|
||||
shuffleButton.setAlpha(shuffleAlpha);
|
||||
}
|
||||
shuffleButton.setImageAlpha(shuffleAlpha);
|
||||
}
|
||||
|
||||
private boolean isInMultiWindow() {
|
||||
@@ -397,6 +425,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
private ImageButton playPauseButton;
|
||||
private ImageButton playPreviousButton;
|
||||
private ImageButton playNextButton;
|
||||
private Button closeButton;
|
||||
|
||||
private RelativeLayout queueLayout;
|
||||
private ImageButton itemsListCloseButton;
|
||||
@@ -406,6 +435,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
private boolean queueVisible;
|
||||
|
||||
private ImageButton moreOptionsButton;
|
||||
private ImageButton shareButton;
|
||||
private ImageButton toggleOrientationButton;
|
||||
private ImageButton switchPopupButton;
|
||||
private ImageButton switchBackgroundButton;
|
||||
@@ -437,9 +467,11 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
this.playPauseButton = rootView.findViewById(R.id.playPauseButton);
|
||||
this.playPreviousButton = rootView.findViewById(R.id.playPreviousButton);
|
||||
this.playNextButton = rootView.findViewById(R.id.playNextButton);
|
||||
this.closeButton = rootView.findViewById(R.id.closeButton);
|
||||
|
||||
this.moreOptionsButton = rootView.findViewById(R.id.moreOptionsButton);
|
||||
this.secondaryControls = rootView.findViewById(R.id.secondaryControls);
|
||||
this.shareButton = rootView.findViewById(R.id.share);
|
||||
this.toggleOrientationButton = rootView.findViewById(R.id.toggleOrientation);
|
||||
this.switchBackgroundButton = rootView.findViewById(R.id.switchBackground);
|
||||
this.switchPopupButton = rootView.findViewById(R.id.switchPopup);
|
||||
@@ -483,8 +515,10 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
playPauseButton.setOnClickListener(this);
|
||||
playPreviousButton.setOnClickListener(this);
|
||||
playNextButton.setOnClickListener(this);
|
||||
closeButton.setOnClickListener(this);
|
||||
|
||||
moreOptionsButton.setOnClickListener(this);
|
||||
shareButton.setOnClickListener(this);
|
||||
toggleOrientationButton.setOnClickListener(this);
|
||||
switchBackgroundButton.setOnClickListener(this);
|
||||
switchPopupButton.setOnClickListener(this);
|
||||
@@ -579,7 +613,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
this.getPlaybackSpeed(),
|
||||
this.getPlaybackPitch(),
|
||||
this.getPlaybackSkipSilence(),
|
||||
this.getPlaybackQuality()
|
||||
this.getPlaybackQuality(),
|
||||
false
|
||||
);
|
||||
context.startService(intent);
|
||||
|
||||
@@ -601,7 +636,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
this.getPlaybackSpeed(),
|
||||
this.getPlaybackPitch(),
|
||||
this.getPlaybackSkipSilence(),
|
||||
this.getPlaybackQuality()
|
||||
this.getPlaybackQuality(),
|
||||
false
|
||||
);
|
||||
context.startService(intent);
|
||||
|
||||
@@ -635,6 +671,9 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
} else if (v.getId() == moreOptionsButton.getId()) {
|
||||
onMoreOptionsClicked();
|
||||
|
||||
} else if (v.getId() == shareButton.getId()) {
|
||||
onShareClicked();
|
||||
|
||||
} else if (v.getId() == toggleOrientationButton.getId()) {
|
||||
onScreenRotationClicked();
|
||||
|
||||
@@ -644,6 +683,9 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
} else if (v.getId() == switchBackgroundButton.getId()) {
|
||||
onPlayBackgroundButtonClicked();
|
||||
|
||||
} else if (v.getId() == closeButton.getId()) {
|
||||
onPlaybackShutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
if (getCurrentState() != STATE_COMPLETED) {
|
||||
@@ -688,6 +730,13 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
showControls(DEFAULT_CONTROLS_DURATION);
|
||||
}
|
||||
|
||||
private void onShareClicked() {
|
||||
// share video at the current time (youtube.com/watch?v=ID&t=SECONDS)
|
||||
ShareUtils.shareUrl(MainVideoPlayer.this,
|
||||
playerImpl.getVideoTitle(),
|
||||
playerImpl.getVideoUrl() + "&t=" + String.valueOf(playerImpl.getPlaybackSeekBar().getProgress()/1000));
|
||||
}
|
||||
|
||||
private void onScreenRotationClicked() {
|
||||
if (DEBUG) Log.d(TAG, "onScreenRotationClicked() called");
|
||||
toggleOrientation();
|
||||
@@ -770,6 +819,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
super.onBlocked();
|
||||
playPauseButton.setImageResource(R.drawable.ic_pause_white);
|
||||
animatePlayButtons(false, 100);
|
||||
animateView(closeButton, false, DEFAULT_CONTROLS_DURATION);
|
||||
getRootView().setKeepScreenOn(true);
|
||||
}
|
||||
|
||||
@@ -785,6 +835,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> {
|
||||
playPauseButton.setImageResource(R.drawable.ic_pause_white);
|
||||
animatePlayButtons(true, 200);
|
||||
animateView(closeButton, false, DEFAULT_CONTROLS_DURATION);
|
||||
});
|
||||
|
||||
getRootView().setKeepScreenOn(true);
|
||||
@@ -796,6 +847,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> {
|
||||
playPauseButton.setImageResource(R.drawable.ic_play_arrow_white);
|
||||
animatePlayButtons(true, 200);
|
||||
animateView(closeButton, false, DEFAULT_CONTROLS_DURATION);
|
||||
});
|
||||
|
||||
showSystemUi();
|
||||
@@ -815,8 +867,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> {
|
||||
playPauseButton.setImageResource(R.drawable.ic_replay_white);
|
||||
animatePlayButtons(true, DEFAULT_CONTROLS_DURATION);
|
||||
animateView(closeButton, true, DEFAULT_CONTROLS_DURATION);
|
||||
});
|
||||
|
||||
getRootView().setKeepScreenOn(false);
|
||||
super.onCompleted();
|
||||
}
|
||||
@@ -851,8 +903,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]");
|
||||
getControlsVisibilityHandler().removeCallbacksAndMessages(null);
|
||||
getControlsVisibilityHandler().postDelayed(() ->
|
||||
animateView(getControlsRoot(), false, duration, 0,
|
||||
MainVideoPlayer.this::hideSystemUi),
|
||||
animateView(getControlsRoot(), false, duration, 0,
|
||||
MainVideoPlayer.this::hideSystemUi),
|
||||
/*delayMillis=*/delay
|
||||
);
|
||||
}
|
||||
@@ -1056,9 +1108,9 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
|
||||
final int resId =
|
||||
currentProgressPercent <= 0 ? R.drawable.ic_volume_off_white_72dp
|
||||
: currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_72dp
|
||||
: currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_72dp
|
||||
: R.drawable.ic_volume_up_white_72dp;
|
||||
: currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_72dp
|
||||
: currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_72dp
|
||||
: R.drawable.ic_volume_up_white_72dp;
|
||||
|
||||
playerImpl.getVolumeImageView().setImageDrawable(
|
||||
AppCompatResources.getDrawable(getApplicationContext(), resId)
|
||||
@@ -1082,8 +1134,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
|
||||
final int resId =
|
||||
currentProgressPercent < 0.25 ? R.drawable.ic_brightness_low_white_72dp
|
||||
: currentProgressPercent < 0.75 ? R.drawable.ic_brightness_medium_white_72dp
|
||||
: R.drawable.ic_brightness_high_white_72dp;
|
||||
: currentProgressPercent < 0.75 ? R.drawable.ic_brightness_medium_white_72dp
|
||||
: R.drawable.ic_brightness_high_white_72dp;
|
||||
|
||||
playerImpl.getBrightnessImageView().setImageDrawable(
|
||||
AppCompatResources.getDrawable(getApplicationContext(), resId)
|
||||
|
||||
@@ -181,6 +181,11 @@ public final class PopupVideoPlayer extends Service {
|
||||
closePopup();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return mBinder;
|
||||
@@ -320,6 +325,7 @@ public final class PopupVideoPlayer extends Service {
|
||||
isPopupClosing = true;
|
||||
|
||||
if (playerImpl != null) {
|
||||
playerImpl.savePlaybackState();
|
||||
if (playerImpl.getRootView() != null) {
|
||||
windowManager.removeView(playerImpl.getRootView());
|
||||
}
|
||||
@@ -560,7 +566,8 @@ public final class PopupVideoPlayer extends Service {
|
||||
this.getPlaybackSpeed(),
|
||||
this.getPlaybackPitch(),
|
||||
this.getPlaybackSkipSilence(),
|
||||
this.getPlaybackQuality()
|
||||
this.getPlaybackQuality(),
|
||||
false
|
||||
);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
@@ -626,6 +633,7 @@ public final class PopupVideoPlayer extends Service {
|
||||
@Override
|
||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||
super.onLoadingComplete(imageUri, view, loadedImage);
|
||||
if (playerImpl == null) return;
|
||||
// rebuild notification here since remote view does not release bitmaps,
|
||||
// causing memory leaks
|
||||
resetNotification();
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.schabi.newpipe.player;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.provider.Settings;
|
||||
@@ -189,7 +188,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
this.player.getPlaybackSpeed(),
|
||||
this.player.getPlaybackPitch(),
|
||||
this.player.getPlaybackSkipSilence(),
|
||||
null
|
||||
null,
|
||||
false
|
||||
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
}
|
||||
|
||||
@@ -653,11 +653,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
final int shuffleAlpha = shuffled ? 255 : 77;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
shuffleButton.setImageAlpha(shuffleAlpha);
|
||||
} else {
|
||||
shuffleButton.setAlpha(shuffleAlpha);
|
||||
}
|
||||
shuffleButton.setImageAlpha(shuffleAlpha);
|
||||
}
|
||||
|
||||
private void onPlaybackParameterChanged(final PlaybackParameters parameters) {
|
||||
|
||||
@@ -212,7 +212,6 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
|
||||
@Override
|
||||
public void initListeners() {
|
||||
super.initListeners();
|
||||
playbackSeekBar.setOnSeekBarChangeListener(this);
|
||||
playbackSpeedTextView.setOnClickListener(this);
|
||||
qualityTextView.setOnClickListener(this);
|
||||
@@ -316,7 +315,6 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
captionPopupMenu.setOnDismissListener(this);
|
||||
}
|
||||
|
||||
|
||||
private void updateStreamRelatedViews() {
|
||||
if (getCurrentMetadata() == null) return;
|
||||
|
||||
@@ -510,11 +508,11 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
|
||||
// Normalize mismatching language strings
|
||||
final String preferredLanguage = trackSelector.getPreferredTextLanguage();
|
||||
|
||||
// Build UI
|
||||
buildCaptionMenu(availableLanguages);
|
||||
if (trackSelector.getParameters().getRendererDisabled(textRenderer) ||
|
||||
preferredLanguage == null || !availableLanguages.contains(preferredLanguage)) {
|
||||
preferredLanguage == null || (!availableLanguages.contains(preferredLanguage)
|
||||
&& !containsCaseInsensitive(availableLanguages, preferredLanguage))) {
|
||||
captionTextView.setText(R.string.caption_none);
|
||||
} else {
|
||||
captionTextView.setText(preferredLanguage);
|
||||
@@ -522,6 +520,15 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
captionTextView.setVisibility(availableLanguages.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
// workaround to match normalized captions like english to English or deutsch to Deutsch
|
||||
private static boolean containsCaseInsensitive(List<String> list, String toFind) {
|
||||
for(String i : list){
|
||||
if(i.equalsIgnoreCase(toFind))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// General Player
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -535,6 +542,11 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed()));
|
||||
|
||||
super.onPrepared(playWhenReady);
|
||||
|
||||
if (simpleExoPlayer.getCurrentPosition() != 0 && !isControlsVisible()) {
|
||||
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
||||
controlsVisibilityHandler.postDelayed(this::showControlsThenHide, DEFAULT_CONTROLS_DURATION);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -12,13 +12,11 @@ import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
import com.google.android.exoplayer2.analytics.AnalyticsListener;
|
||||
|
||||
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
|
||||
AudioRendererEventListener {
|
||||
AnalyticsListener {
|
||||
|
||||
private static final String TAG = "AudioFocusReactor";
|
||||
|
||||
@@ -42,7 +40,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
|
||||
this.player = player;
|
||||
this.context = context;
|
||||
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
player.addAudioDebugListener(this);
|
||||
player.addAnalyticsListener(this);
|
||||
|
||||
if (SHOULD_BUILD_FOCUS_REQUEST) {
|
||||
request = new AudioFocusRequest.Builder(FOCUS_GAIN_TYPE)
|
||||
@@ -57,7 +55,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
|
||||
|
||||
public void dispose() {
|
||||
abandonAudioFocus();
|
||||
player.removeAudioDebugListener(this);
|
||||
player.removeAnalyticsListener(this);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -131,7 +129,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
|
||||
private void onAudioFocusLossCanDuck() {
|
||||
Log.d(TAG, "onAudioFocusLossCanDuck() called");
|
||||
// Set the volume to 1/10 on ducking
|
||||
animateAudio(player.getVolume(), DUCK_AUDIO_TO);
|
||||
player.setVolume(DUCK_AUDIO_TO);
|
||||
}
|
||||
|
||||
private void animateAudio(final float from, final float to) {
|
||||
@@ -164,29 +162,12 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAudioSessionId(int i) {
|
||||
public void onAudioSessionId(EventTime eventTime, int audioSessionId) {
|
||||
if (!PlayerHelper.isUsingDSP(context)) return;
|
||||
|
||||
final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
|
||||
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, i);
|
||||
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId);
|
||||
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName());
|
||||
context.sendBroadcast(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioEnabled(DecoderCounters decoderCounters) {}
|
||||
|
||||
@Override
|
||||
public void onAudioDecoderInitialized(String s, long l, long l1) {}
|
||||
|
||||
@Override
|
||||
public void onAudioInputFormatChanged(Format format) {}
|
||||
|
||||
@Override
|
||||
public void onAudioSinkUnderrun(int bufferSize,
|
||||
long bufferSizeMs,
|
||||
long elapsedSinceLastFeedMs) {}
|
||||
|
||||
@Override
|
||||
public void onAudioDisabled(DecoderCounters decoderCounters) {}
|
||||
}
|
||||
|
||||
@@ -33,14 +33,14 @@ import java.io.File;
|
||||
|
||||
public CacheFactory(@NonNull final Context context,
|
||||
@NonNull final String userAgent,
|
||||
@NonNull final TransferListener<? super DataSource> transferListener) {
|
||||
@NonNull final TransferListener transferListener) {
|
||||
this(context, userAgent, transferListener, PlayerHelper.getPreferredCacheSize(context),
|
||||
PlayerHelper.getPreferredFileSize(context));
|
||||
}
|
||||
|
||||
private CacheFactory(@NonNull final Context context,
|
||||
@NonNull final String userAgent,
|
||||
@NonNull final TransferListener<? super DataSource> transferListener,
|
||||
@NonNull final TransferListener transferListener,
|
||||
final long maxCacheSize,
|
||||
final long maxFileSize) {
|
||||
this.maxFileSize = maxFileSize;
|
||||
|
||||
@@ -2,17 +2,12 @@ package org.schabi.newpipe.player.helper;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultLoadControl;
|
||||
import com.google.android.exoplayer2.LoadControl;
|
||||
import com.google.android.exoplayer2.Renderer;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DefaultAllocator;
|
||||
|
||||
import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS;
|
||||
import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_TARGET_BUFFER_BYTES;
|
||||
|
||||
public class LoadController implements LoadControl {
|
||||
|
||||
@@ -36,15 +31,10 @@ public class LoadController implements LoadControl {
|
||||
final int optimalPlaybackBufferMs) {
|
||||
this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000;
|
||||
|
||||
final DefaultAllocator allocator = new DefaultAllocator(true,
|
||||
C.DEFAULT_BUFFER_SEGMENT_SIZE);
|
||||
|
||||
internalLoadControl = new DefaultLoadControl(allocator,
|
||||
/*minBufferMs=*/minimumPlaybackbufferMs,
|
||||
/*maxBufferMs=*/optimalPlaybackBufferMs,
|
||||
/*bufferForPlaybackMs=*/initialPlaybackBufferMs,
|
||||
/*bufferForPlaybackAfterRebufferMs=*/initialPlaybackBufferMs,
|
||||
DEFAULT_TARGET_BUFFER_BYTES, DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS);
|
||||
DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder();
|
||||
builder.setBufferDurationsMs(minimumPlaybackbufferMs, optimalPlaybackBufferMs,
|
||||
initialPlaybackBufferMs, initialPlaybackBufferMs);
|
||||
internalLoadControl = builder.createDefaultLoadControl();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
|
||||
public class PlayerDataSource {
|
||||
@@ -24,7 +25,7 @@ public class PlayerDataSource {
|
||||
|
||||
public PlayerDataSource(@NonNull final Context context,
|
||||
@NonNull final String userAgent,
|
||||
@NonNull final TransferListener<? super DataSource> transferListener) {
|
||||
@NonNull final TransferListener transferListener) {
|
||||
cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener);
|
||||
cachelessDataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener);
|
||||
}
|
||||
@@ -32,21 +33,21 @@ public class PlayerDataSource {
|
||||
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
|
||||
return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory(
|
||||
cachelessDataSourceFactory), cachelessDataSourceFactory)
|
||||
.setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY)
|
||||
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
|
||||
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
|
||||
}
|
||||
|
||||
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
|
||||
return new HlsMediaSource.Factory(cachelessDataSourceFactory)
|
||||
.setAllowChunklessPreparation(true)
|
||||
.setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY);
|
||||
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
|
||||
}
|
||||
|
||||
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
|
||||
return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(
|
||||
cachelessDataSourceFactory), cachelessDataSourceFactory)
|
||||
.setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY)
|
||||
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
|
||||
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
|
||||
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS, true);
|
||||
}
|
||||
|
||||
public SsMediaSource.Factory getSsMediaSourceFactory() {
|
||||
@@ -65,7 +66,7 @@ public class PlayerDataSource {
|
||||
|
||||
public ExtractorMediaSource.Factory getExtractorMediaSourceFactory() {
|
||||
return new ExtractorMediaSource.Factory(cacheDataSourceFactory)
|
||||
.setMinLoadableRetryCount(EXTRACTOR_MINIMUM_RETRY);
|
||||
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
|
||||
}
|
||||
|
||||
public ExtractorMediaSource.Factory getExtractorMediaSourceFactory(@NonNull final String key) {
|
||||
|
||||
@@ -14,7 +14,6 @@ import com.google.android.exoplayer2.text.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
@@ -45,7 +44,9 @@ import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MOD
|
||||
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
||||
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.*;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
|
||||
|
||||
public class PlayerHelper {
|
||||
private PlayerHelper() {}
|
||||
@@ -68,10 +69,10 @@ public class PlayerHelper {
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static String getTimeString(int milliSeconds) {
|
||||
long seconds = (milliSeconds % 60000L) / 1000L;
|
||||
long minutes = (milliSeconds % 3600000L) / 60000L;
|
||||
long hours = (milliSeconds % 86400000L) / 3600000L;
|
||||
long days = (milliSeconds % (86400000L * 7L)) / 86400000L;
|
||||
int seconds = (milliSeconds % 60000) / 1000;
|
||||
int minutes = (milliSeconds % 3600000) / 60000;
|
||||
int hours = (milliSeconds % 86400000) / 3600000;
|
||||
int days = (milliSeconds % (86400000 * 7)) / 86400000;
|
||||
|
||||
stringBuilder.setLength(0);
|
||||
return days > 0 ? stringFormatter.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds).toString()
|
||||
@@ -238,9 +239,8 @@ public class PlayerHelper {
|
||||
return 60000;
|
||||
}
|
||||
|
||||
public static TrackSelection.Factory getQualitySelector(@NonNull final Context context,
|
||||
@NonNull final BandwidthMeter meter) {
|
||||
return new AdaptiveTrackSelection.Factory(meter,
|
||||
public static TrackSelection.Factory getQualitySelector(@NonNull final Context context) {
|
||||
return new AdaptiveTrackSelection.Factory(
|
||||
/*bufferDurationRequiredForQualityIncrease=*/1000,
|
||||
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
|
||||
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package org.schabi.newpipe.player.mediasource;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.source.BaseMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
|
||||
@@ -79,7 +80,7 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -88,7 +89,7 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo
|
||||
|
||||
|
||||
@Override
|
||||
protected void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
|
||||
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
||||
Log.e(TAG, "Loading failed source: ", error);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,13 @@ package org.schabi.newpipe.player.mediasource;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
|
||||
@@ -36,9 +37,8 @@ public class LoadedMediaSource implements ManagedMediaSource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareSource(ExoPlayer player, boolean isTopLevelSource,
|
||||
SourceInfoRefreshListener listener) {
|
||||
source.prepareSource(player, isTopLevelSource, listener);
|
||||
public void prepareSource(SourceInfoRefreshListener listener, @Nullable TransferListener mediaTransferListener) {
|
||||
source.prepareSource(listener, mediaTransferListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -47,8 +47,8 @@ public class LoadedMediaSource implements ManagedMediaSource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
return source.createPeriod(id, allocator);
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
return source.createPeriod(id, allocator, startPositionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package org.schabi.newpipe.player.mediasource;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
@@ -86,21 +86,22 @@ public class ManagedMediaSourcePlaylist {
|
||||
/**
|
||||
* Invalidates the {@link ManagedMediaSource} at the given index by replacing it
|
||||
* with a {@link PlaceholderMediaSource}.
|
||||
* @see #update(int, ManagedMediaSource, Runnable)
|
||||
* @see #update(int, ManagedMediaSource, Handler, Runnable)
|
||||
* */
|
||||
public synchronized void invalidate(final int index,
|
||||
@Nullable final Handler handler,
|
||||
@Nullable final Runnable finalizingAction) {
|
||||
if (get(index) instanceof PlaceholderMediaSource) return;
|
||||
update(index, new PlaceholderMediaSource(), finalizingAction);
|
||||
update(index, new PlaceholderMediaSource(), handler, finalizingAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the {@link ManagedMediaSource} in {@link ConcatenatingMediaSource}
|
||||
* at the given index with a given {@link ManagedMediaSource}.
|
||||
* @see #update(int, ManagedMediaSource, Runnable)
|
||||
* @see #update(int, ManagedMediaSource, Handler, Runnable)
|
||||
* */
|
||||
public synchronized void update(final int index, @NonNull final ManagedMediaSource source) {
|
||||
update(index, source, /*doNothing=*/null);
|
||||
update(index, source, null, /*doNothing=*/null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,9 +109,10 @@ public class ManagedMediaSourcePlaylist {
|
||||
* at the given index with a given {@link ManagedMediaSource}. If the index is out of bound,
|
||||
* then the replacement is ignored.
|
||||
* @see ConcatenatingMediaSource#addMediaSource
|
||||
* @see ConcatenatingMediaSource#removeMediaSource(int, Runnable)
|
||||
* @see ConcatenatingMediaSource#removeMediaSource(int, Handler, Runnable)
|
||||
* */
|
||||
public synchronized void update(final int index, @NonNull final ManagedMediaSource source,
|
||||
@Nullable final Handler handler,
|
||||
@Nullable final Runnable finalizingAction) {
|
||||
if (index < 0 || index >= internalSource.getSize()) return;
|
||||
|
||||
@@ -126,6 +128,6 @@ public class ManagedMediaSourcePlaylist {
|
||||
|
||||
// Because of the above race condition, it is thus only safe to synchronize the player
|
||||
// in the finalizing action AFTER the removal is complete and the timeline has changed.
|
||||
internalSource.removeMediaSource(index, finalizingAction);
|
||||
internalSource.removeMediaSource(index, handler, finalizingAction);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
package org.schabi.newpipe.player.mediasource;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.source.BaseMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
|
||||
public class PlaceholderMediaSource extends BaseMediaSource implements ManagedMediaSource {
|
||||
// Do nothing, so this will stall the playback
|
||||
@Override public void maybeThrowSourceInfoRefreshError() {}
|
||||
@Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { return null; }
|
||||
@Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { return null; }
|
||||
@Override public void releasePeriod(MediaPeriod mediaPeriod) {}
|
||||
@Override protected void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {}
|
||||
@Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {}
|
||||
@Override protected void releaseSourceInternal() {}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.schabi.newpipe.player.playback;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
import android.support.v4.media.MediaMetadataCompat;
|
||||
|
||||
import org.schabi.newpipe.player.BasePlayer;
|
||||
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
|
||||
@@ -54,6 +56,15 @@ public class BasePlayerMediaSession implements MediaSessionCallback {
|
||||
.setTitle(item.getTitle())
|
||||
.setSubtitle(item.getUploader());
|
||||
|
||||
// set additional metadata for A2DP/AVRCP
|
||||
Bundle additionalMetadata = new Bundle();
|
||||
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, item.getTitle());
|
||||
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, item.getUploader());
|
||||
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration() * 1000);
|
||||
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, index + 1);
|
||||
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
|
||||
descriptionBuilder.setExtras(additionalMetadata);
|
||||
|
||||
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
|
||||
if (thumbnailUri != null) descriptionBuilder.setIconUri(thumbnailUri);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.schabi.newpipe.player.playback;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
@@ -17,7 +18,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
* is mostly a copy-paste from {@link DefaultTrackSelector}.
|
||||
*
|
||||
* This is a hack and should be removed once ExoPlayer fixes language normalization to accept
|
||||
* a broader set of languages.
|
||||
* a broader set of languages.
|
||||
* */
|
||||
public class CustomTrackSelector extends DefaultTrackSelector {
|
||||
private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000;
|
||||
@@ -52,8 +53,8 @@ public class CustomTrackSelector extends DefaultTrackSelector {
|
||||
|
||||
/** @see DefaultTrackSelector#selectTextTrack(TrackGroupArray, int[][], Parameters) */
|
||||
@Override
|
||||
protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatSupport,
|
||||
Parameters params) {
|
||||
protected Pair<TrackSelection, Integer> selectTextTrack(TrackGroupArray groups, int[][] formatSupport,
|
||||
Parameters params) {
|
||||
TrackGroup selectedGroup = null;
|
||||
int selectedTrackIndex = 0;
|
||||
int selectedTrackScore = 0;
|
||||
@@ -106,7 +107,9 @@ public class CustomTrackSelector extends DefaultTrackSelector {
|
||||
}
|
||||
}
|
||||
}
|
||||
return selectedGroup == null ? null
|
||||
: new FixedTrackSelection(selectedGroup, selectedTrackIndex);
|
||||
return selectedGroup == null
|
||||
? null
|
||||
: Pair.create(
|
||||
new FixedTrackSelection(selectedGroup, selectedTrackIndex), selectedTrackScore);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
package org.schabi.newpipe.player.playback;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.util.ArraySet;
|
||||
@@ -103,6 +103,8 @@ public class MediaSourceManager {
|
||||
|
||||
@NonNull private ManagedMediaSourcePlaylist playlist;
|
||||
|
||||
private Handler removeMediaSourceHandler = new Handler();
|
||||
|
||||
public MediaSourceManager(@NonNull final PlaybackListener listener,
|
||||
@NonNull final PlayQueue playQueue) {
|
||||
this(listener, playQueue, /*loadDebounceMillis=*/400L,
|
||||
@@ -156,7 +158,7 @@ public class MediaSourceManager {
|
||||
* Dispose the manager and releases all message buses and loaders.
|
||||
* */
|
||||
public void dispose() {
|
||||
if (DEBUG) Log.d(TAG, "dispose() called.");
|
||||
if (DEBUG) Log.d(TAG, "close() called.");
|
||||
|
||||
debouncedSignal.onComplete();
|
||||
debouncedLoader.dispose();
|
||||
@@ -395,7 +397,7 @@ public class MediaSourceManager {
|
||||
if (isCorrectionNeeded(item)) {
|
||||
if (DEBUG) Log.d(TAG, "MediaSource - Updating index=[" + itemIndex + "] with " +
|
||||
"title=[" + item.getTitle() + "] at url=[" + item.getUrl() + "]");
|
||||
playlist.update(itemIndex, mediaSource, this::maybeSynchronizePlayer);
|
||||
playlist.update(itemIndex, mediaSource, removeMediaSourceHandler, this::maybeSynchronizePlayer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,7 +443,7 @@ public class MediaSourceManager {
|
||||
|
||||
if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " +
|
||||
"index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]");
|
||||
playlist.invalidate(currentIndex, this::loadImmediate);
|
||||
playlist.invalidate(currentIndex, removeMediaSourceHandler, this::loadImmediate);
|
||||
}
|
||||
|
||||
private void maybeClearLoaders() {
|
||||
|
||||
@@ -16,6 +16,11 @@ public final class SinglePlayQueue extends PlayQueue {
|
||||
super(0, Collections.singletonList(new PlayQueueItem(info)));
|
||||
}
|
||||
|
||||
public SinglePlayQueue(final StreamInfo info, final long startPosition) {
|
||||
super(0, Collections.singletonList(new PlayQueueItem(info)));
|
||||
getItem().setRecoveryPosition(startPosition);
|
||||
}
|
||||
|
||||
public SinglePlayQueue(final List<StreamInfoItem> items, final int index) {
|
||||
super(index, playQueueItemsOf(items));
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
@@ -93,15 +94,17 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||
// Below are auxiliary media sources
|
||||
|
||||
// Create subtitle sources
|
||||
for (final SubtitlesStream subtitle : info.getSubtitles()) {
|
||||
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
|
||||
if (mimeType == null) continue;
|
||||
if(info.getSubtitles() != null) {
|
||||
for (final SubtitlesStream subtitle : info.getSubtitles()) {
|
||||
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
|
||||
if (mimeType == null) continue;
|
||||
|
||||
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
|
||||
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
|
||||
final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
|
||||
.createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
|
||||
mediaSources.add(textSource);
|
||||
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
|
||||
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
|
||||
final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
|
||||
.createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
|
||||
mediaSources.add(textSource);
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaSources.size() == 1) {
|
||||
|
||||
@@ -15,8 +15,11 @@ public enum UserAction {
|
||||
REQUESTED_CHANNEL("requested channel"),
|
||||
REQUESTED_PLAYLIST("requested playlist"),
|
||||
REQUESTED_KIOSK("requested kiosk"),
|
||||
REQUESTED_COMMENTS("requested comments"),
|
||||
DELETE_FROM_HISTORY("delete from history"),
|
||||
PLAY_STREAM("Play stream");
|
||||
PLAY_STREAM("Play stream"),
|
||||
DOWNLOAD_POSTPROCESSING("download post-processing"),
|
||||
DOWNLOAD_FAILED("download failed");
|
||||
|
||||
|
||||
private final String message;
|
||||
|
||||
@@ -274,7 +274,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
else if (v instanceof String)
|
||||
prefEdit.putString(key, ((String) v));
|
||||
}
|
||||
prefEdit.apply();
|
||||
prefEdit.commit();
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.v7.preference.Preference;
|
||||
import android.util.Log;
|
||||
|
||||
@@ -12,19 +18,56 @@ import com.nononsenseapps.filepicker.Utils;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
|
||||
public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||
private static final int REQUEST_DOWNLOAD_PATH = 0x1235;
|
||||
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
private String DOWNLOAD_PATH_PREFERENCE;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
|
||||
public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||
private static final int REQUEST_DOWNLOAD_VIDEO_PATH = 0x1235;
|
||||
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
|
||||
public static final boolean IGNORE_RELEASE_ON_OLD_PATH = true;
|
||||
|
||||
private String DOWNLOAD_PATH_VIDEO_PREFERENCE;
|
||||
private String DOWNLOAD_PATH_AUDIO_PREFERENCE;
|
||||
|
||||
private Preference prefPathVideo;
|
||||
private Preference prefPathAudio;
|
||||
private Preference prefStorageAsk;
|
||||
|
||||
private Context ctx;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
initKeys();
|
||||
DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key);
|
||||
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
|
||||
final String downloadStorageAsk = getString(R.string.downloads_storage_ask);
|
||||
|
||||
prefPathVideo = findPreference(DOWNLOAD_PATH_VIDEO_PREFERENCE);
|
||||
prefPathAudio = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE);
|
||||
prefStorageAsk = findPreference(downloadStorageAsk);
|
||||
|
||||
updatePreferencesSummary();
|
||||
updatePathPickers(!defaultPreferences.getBoolean(downloadStorageAsk, false));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
prefStorageAsk.setSummary(R.string.downloads_storage_ask_summary);
|
||||
}
|
||||
|
||||
if (hasInvalidPath(DOWNLOAD_PATH_VIDEO_PREFERENCE) || hasInvalidPath(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||
updatePreferencesSummary();
|
||||
}
|
||||
|
||||
prefStorageAsk.setOnPreferenceChangeListener((preference, value) -> {
|
||||
updatePathPickers(!(boolean) value);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -32,52 +75,184 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||
addPreferencesFromResource(R.xml.download_settings);
|
||||
}
|
||||
|
||||
private void initKeys() {
|
||||
DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key);
|
||||
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
ctx = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
ctx = null;
|
||||
prefStorageAsk.setOnPreferenceChangeListener(null);
|
||||
}
|
||||
|
||||
private void updatePreferencesSummary() {
|
||||
findPreference(DOWNLOAD_PATH_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_PREFERENCE, getString(R.string.download_path_summary)));
|
||||
findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary)));
|
||||
showPathInSummary(DOWNLOAD_PATH_VIDEO_PREFERENCE, R.string.download_path_summary, prefPathVideo);
|
||||
showPathInSummary(DOWNLOAD_PATH_AUDIO_PREFERENCE, R.string.download_path_audio_summary, prefPathAudio);
|
||||
}
|
||||
|
||||
private void showPathInSummary(String prefKey, @StringRes int defaultString, Preference target) {
|
||||
String rawUri = defaultPreferences.getString(prefKey, null);
|
||||
if (rawUri == null || rawUri.isEmpty()) {
|
||||
target.setSummary(getString(defaultString));
|
||||
return;
|
||||
}
|
||||
|
||||
if (rawUri.charAt(0) == File.separatorChar) {
|
||||
target.setSummary(rawUri);
|
||||
return;
|
||||
}
|
||||
if (rawUri.startsWith(ContentResolver.SCHEME_FILE)) {
|
||||
target.setSummary(new File(URI.create(rawUri)).getPath());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
rawUri = URLDecoder.decode(rawUri, StandardCharsets.UTF_8.name());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
target.setSummary(rawUri);
|
||||
}
|
||||
|
||||
private boolean isFileUri(String path) {
|
||||
return path.charAt(0) == File.separatorChar || path.startsWith(ContentResolver.SCHEME_FILE);
|
||||
}
|
||||
|
||||
private boolean hasInvalidPath(String prefKey) {
|
||||
String value = defaultPreferences.getString(prefKey, null);
|
||||
return value == null || value.isEmpty();
|
||||
}
|
||||
|
||||
private void updatePathPickers(boolean enabled) {
|
||||
prefPathVideo.setEnabled(enabled);
|
||||
prefPathAudio.setEnabled(enabled);
|
||||
}
|
||||
|
||||
// FIXME: after releasing the old path, all downloads created on the folder becomes inaccessible
|
||||
private void forgetSAFTree(Context ctx, String oldPath) {
|
||||
if (IGNORE_RELEASE_ON_OLD_PATH) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldPath == null || oldPath.isEmpty() || isFileUri(oldPath)) return;
|
||||
|
||||
try {
|
||||
Uri uri = Uri.parse(oldPath);
|
||||
|
||||
ctx.getContentResolver().releasePersistableUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||
ctx.revokeUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||
|
||||
Log.i(TAG, "Revoke old path permissions success on " + oldPath);
|
||||
} catch (Exception err) {
|
||||
Log.e(TAG, "Error revoking old path permissions on " + oldPath, err);
|
||||
}
|
||||
}
|
||||
|
||||
private void showMessageDialog(@StringRes int title, @StringRes int message) {
|
||||
AlertDialog.Builder msg = new AlertDialog.Builder(ctx);
|
||||
msg.setTitle(title);
|
||||
msg.setMessage(message);
|
||||
msg.setPositiveButton(android.R.string.ok, null);
|
||||
msg.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceTreeClick(Preference preference) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]");
|
||||
Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]");
|
||||
}
|
||||
|
||||
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)
|
||||
|| preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||
Intent i = new Intent(getActivity(), FilePickerActivityHelper.class)
|
||||
String key = preference.getKey();
|
||||
int request;
|
||||
|
||||
if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE)) {
|
||||
request = REQUEST_DOWNLOAD_VIDEO_PATH;
|
||||
} else if (key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||
request = REQUEST_DOWNLOAD_AUDIO_PATH;
|
||||
} else {
|
||||
return super.onPreferenceTreeClick(preference);
|
||||
}
|
||||
|
||||
Intent i;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && NewPipeSettings.useStorageAccessFramework(ctx)) {
|
||||
i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
.putExtra("android.content.extra.SHOW_ADVANCED", true)
|
||||
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||
} else {
|
||||
i = new Intent(getActivity(), FilePickerActivityHelper.class)
|
||||
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false)
|
||||
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true)
|
||||
.putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR);
|
||||
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)) {
|
||||
startActivityForResult(i, REQUEST_DOWNLOAD_PATH);
|
||||
} else if (preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||
startActivityForResult(i, REQUEST_DOWNLOAD_AUDIO_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
return super.onPreferenceTreeClick(preference);
|
||||
startActivityForResult(i, request);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]");
|
||||
Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], " +
|
||||
"resultCode = [" + resultCode + "], data = [" + data + "]"
|
||||
);
|
||||
}
|
||||
|
||||
if ((requestCode == REQUEST_DOWNLOAD_PATH || requestCode == REQUEST_DOWNLOAD_AUDIO_PATH)
|
||||
&& resultCode == Activity.RESULT_OK && data.getData() != null) {
|
||||
String key = getString(requestCode == REQUEST_DOWNLOAD_PATH ? R.string.download_path_key : R.string.download_path_audio_key);
|
||||
String path = Utils.getFileForUri(data.getData()).getAbsolutePath();
|
||||
if (resultCode != Activity.RESULT_OK) return;
|
||||
|
||||
defaultPreferences.edit().putString(key, path).apply();
|
||||
updatePreferencesSummary();
|
||||
String key;
|
||||
if (requestCode == REQUEST_DOWNLOAD_VIDEO_PATH)
|
||||
key = DOWNLOAD_PATH_VIDEO_PREFERENCE;
|
||||
else if (requestCode == REQUEST_DOWNLOAD_AUDIO_PATH)
|
||||
key = DOWNLOAD_PATH_AUDIO_PREFERENCE;
|
||||
else
|
||||
return;
|
||||
|
||||
Uri uri = data.getData();
|
||||
if (uri == null) {
|
||||
showMessageDialog(R.string.general_error, R.string.invalid_directory);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// revoke permissions on the old save path (required for SAF only)
|
||||
final Context ctx = getContext();
|
||||
if (ctx == null) throw new NullPointerException("getContext()");
|
||||
|
||||
forgetSAFTree(ctx, defaultPreferences.getString(key, ""));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !FilePickerActivityHelper.isOwnFileUri(ctx, uri)) {
|
||||
// steps to acquire the selected path:
|
||||
// 1. acquire permissions on the new save path
|
||||
// 2. save the new path, if step(2) was successful
|
||||
try {
|
||||
ctx.grantUriPermission(ctx.getPackageName(), uri, StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||
|
||||
StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, uri, null);
|
||||
Log.i(TAG, "Acquiring tree success from " + uri.toString());
|
||||
|
||||
if (!mainStorage.canWrite())
|
||||
throw new IOException("No write permissions on " + uri.toString());
|
||||
} catch (IOException err) {
|
||||
Log.e(TAG, "Error acquiring tree from " + uri.toString(), err);
|
||||
showMessageDialog(R.string.general_error, R.string.no_available_dir);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
File target = Utils.getFileForUri(uri);
|
||||
if (!target.canWrite()) {
|
||||
showMessageDialog(R.string.download_to_sdcard_error_title, R.string.download_to_sdcard_error_message);
|
||||
return;
|
||||
}
|
||||
uri = Uri.fromFile(target);
|
||||
}
|
||||
|
||||
defaultPreferences.edit().putString(key, uri.toString()).apply();
|
||||
updatePreferencesSummary();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import io.reactivex.disposables.Disposable;
|
||||
|
||||
public class HistorySettingsFragment extends BasePreferenceFragment {
|
||||
private String cacheWipeKey;
|
||||
private String viewsHistroyClearKey;
|
||||
private String viewsHistoryClearKey;
|
||||
private String searchHistoryClearKey;
|
||||
private HistoryRecordManager recordManager;
|
||||
private CompositeDisposable disposables;
|
||||
@@ -27,7 +27,7 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
cacheWipeKey = getString(R.string.metadata_cache_wipe_key);
|
||||
viewsHistroyClearKey = getString(R.string.clear_views_history_key);
|
||||
viewsHistoryClearKey = getString(R.string.clear_views_history_key);
|
||||
searchHistoryClearKey = getString(R.string.clear_search_history_key);
|
||||
recordManager = new HistoryRecordManager(getActivity());
|
||||
disposables = new CompositeDisposable();
|
||||
@@ -46,16 +46,31 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
if (preference.getKey().equals(viewsHistroyClearKey)) {
|
||||
if (preference.getKey().equals(viewsHistoryClearKey)) {
|
||||
new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.delete_view_history_alert)
|
||||
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
|
||||
.setPositiveButton(R.string.delete, ((dialog, which) -> {
|
||||
final Disposable onDeletePlaybackStates = recordManager.deleteCompelteStreamStateHistory()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
howManyDeleted -> Toast.makeText(getActivity(),
|
||||
R.string.watch_history_states_deleted,
|
||||
Toast.LENGTH_SHORT).show(),
|
||||
throwable -> ErrorActivity.reportError(getContext(),
|
||||
throwable,
|
||||
SettingsActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(
|
||||
UserAction.DELETE_FROM_HISTORY,
|
||||
"none",
|
||||
"Delete view history",
|
||||
R.string.general_error)));
|
||||
|
||||
final Disposable onDelete = recordManager.deleteWholeStreamHistory()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
howManyDeleted -> Toast.makeText(getActivity(),
|
||||
R.string.view_history_deleted,
|
||||
R.string.watch_history_deleted,
|
||||
Toast.LENGTH_SHORT).show(),
|
||||
throwable -> ErrorActivity.reportError(getContext(),
|
||||
throwable,
|
||||
@@ -78,6 +93,7 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
|
||||
"none",
|
||||
"Delete search history",
|
||||
R.string.general_error)));
|
||||
disposables.add(onDeletePlaybackStates);
|
||||
disposables.add(onClearOrphans);
|
||||
disposables.add(onDelete);
|
||||
}))
|
||||
@@ -90,7 +106,7 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
|
||||
.setTitle(R.string.delete_search_history_alert)
|
||||
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
|
||||
.setPositiveButton(R.string.delete, ((dialog, which) -> {
|
||||
final Disposable onDelete = recordManager.deleteWholeSearchHistory()
|
||||
final Disposable onDelete = recordManager.deleteCompleteSearchHistory()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
howManyDeleted -> Toast.makeText(getActivity(),
|
||||
|
||||
@@ -70,57 +70,39 @@ public class NewPipeSettings {
|
||||
getAudioDownloadFolder(context);
|
||||
}
|
||||
|
||||
public static File getVideoDownloadFolder(Context context) {
|
||||
return getDir(context, R.string.download_path_key, Environment.DIRECTORY_MOVIES);
|
||||
private static void getVideoDownloadFolder(Context context) {
|
||||
getDir(context, R.string.download_path_video_key, Environment.DIRECTORY_MOVIES);
|
||||
}
|
||||
|
||||
public static String getVideoDownloadPath(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String key = context.getString(R.string.download_path_key);
|
||||
return prefs.getString(key, Environment.DIRECTORY_MOVIES);
|
||||
private static void getAudioDownloadFolder(Context context) {
|
||||
getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC);
|
||||
}
|
||||
|
||||
public static File getAudioDownloadFolder(Context context) {
|
||||
return getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC);
|
||||
}
|
||||
|
||||
public static String getAudioDownloadPath(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String key = context.getString(R.string.download_path_audio_key);
|
||||
return prefs.getString(key, Environment.DIRECTORY_MUSIC);
|
||||
}
|
||||
|
||||
private static File getDir(Context context, int keyID, String defaultDirectoryName) {
|
||||
private static void getDir(Context context, int keyID, String defaultDirectoryName) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String key = context.getString(keyID);
|
||||
String downloadPath = prefs.getString(key, null);
|
||||
if ((downloadPath != null) && (!downloadPath.isEmpty())) return new File(downloadPath.trim());
|
||||
if ((downloadPath != null) && (!downloadPath.isEmpty())) return;
|
||||
|
||||
final File dir = getDir(defaultDirectoryName);
|
||||
SharedPreferences.Editor spEditor = prefs.edit();
|
||||
spEditor.putString(key, getNewPipeChildFolderPathForDir(dir));
|
||||
spEditor.apply();
|
||||
return dir;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static File getDir(String defaultDirectoryName) {
|
||||
return new File(Environment.getExternalStorageDirectory(), defaultDirectoryName);
|
||||
}
|
||||
|
||||
public static void resetDownloadFolders(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
resetDownloadFolder(prefs, context.getString(R.string.download_path_audio_key), Environment.DIRECTORY_MUSIC);
|
||||
resetDownloadFolder(prefs, context.getString(R.string.download_path_key), Environment.DIRECTORY_MOVIES);
|
||||
}
|
||||
|
||||
private static void resetDownloadFolder(SharedPreferences prefs, String key, String defaultDirectoryName) {
|
||||
SharedPreferences.Editor spEditor = prefs.edit();
|
||||
spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName)));
|
||||
spEditor.apply();
|
||||
}
|
||||
|
||||
private static String getNewPipeChildFolderPathForDir(File dir) {
|
||||
return new File(dir, "NewPipe").getAbsolutePath();
|
||||
@NonNull
|
||||
public static File getDir(String defaultDirectoryName) {
|
||||
return new File(Environment.getExternalStorageDirectory(), defaultDirectoryName);
|
||||
}
|
||||
|
||||
private static String getNewPipeChildFolderPathForDir(File dir) {
|
||||
return new File(dir, "NewPipe").toURI().toString();
|
||||
}
|
||||
|
||||
public static boolean useStorageAccessFramework(Context context) {
|
||||
final String key = context.getString(R.string.storage_use_saf);
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
return prefs.getBoolean(key, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import android.widget.TextView;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
@@ -124,9 +123,6 @@ public class SelectKioskFragment extends DialogFragment {
|
||||
throws Exception {
|
||||
|
||||
for(StreamingService service : NewPipe.getServices()) {
|
||||
//TODO: Multi-service support
|
||||
if (service.getServiceId() != ServiceList.YouTube.getServiceId() && !DEBUG) continue;
|
||||
|
||||
for(String kioskId : service.getKioskList().getAvailableKiosks()) {
|
||||
String name = String.format(getString(R.string.service_kiosk_string),
|
||||
service.getServiceInfo().getName(),
|
||||
|
||||
@@ -229,6 +229,12 @@ public class ChooseTabsFragment extends Fragment {
|
||||
returnList.add(new ChooseTabListItem(tab.getTabId(), getString(R.string.channel_page_summary),
|
||||
tab.getTabIconRes(context)));
|
||||
break;
|
||||
case DEFAULT_KIOSK:
|
||||
if (!tabList.contains(tab)) {
|
||||
returnList.add(new ChooseTabListItem(tab.getTabId(), "Default Kiosk",
|
||||
ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_hot)));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!tabList.contains(tab)) {
|
||||
returnList.add(new ChooseTabListItem(context, tab));
|
||||
@@ -310,6 +316,9 @@ public class ChooseTabsFragment extends Fragment {
|
||||
case CHANNEL:
|
||||
tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab).getChannelServiceId()) + "/" + tabName;
|
||||
break;
|
||||
case DEFAULT_KIOSK:
|
||||
tabName = "Default Kiosk";
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,10 @@ import android.support.v4.app.Fragment;
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonSink;
|
||||
|
||||
import org.jsoup.helper.StringUtil;
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.fragments.BlankFragment;
|
||||
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
|
||||
@@ -19,6 +22,7 @@ import org.schabi.newpipe.local.feed.FeedFragment;
|
||||
import org.schabi.newpipe.local.history.StatisticsPlaylistFragment;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionFragment;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
public abstract class Tab {
|
||||
@@ -111,6 +115,12 @@ public abstract class Tab {
|
||||
return new KioskTab(jsonObject);
|
||||
case CHANNEL:
|
||||
return new ChannelTab(jsonObject);
|
||||
case DEFAULT_KIOSK:
|
||||
DefaultKioskTab tab = new DefaultKioskTab();
|
||||
if(!StringUtil.isBlank(tab.getKioskId())){
|
||||
return tab;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +138,8 @@ public abstract class Tab {
|
||||
BOOKMARKS(new BookmarksTab()),
|
||||
HISTORY(new HistoryTab()),
|
||||
KIOSK(new KioskTab()),
|
||||
CHANNEL(new ChannelTab());
|
||||
CHANNEL(new ChannelTab()),
|
||||
DEFAULT_KIOSK(new DefaultKioskTab());
|
||||
|
||||
private Tab tab;
|
||||
|
||||
@@ -413,4 +424,55 @@ public abstract class Tab {
|
||||
return channelName;
|
||||
}
|
||||
}
|
||||
|
||||
public static class DefaultKioskTab extends Tab {
|
||||
public static final int ID = 7;
|
||||
|
||||
private int kioskServiceId;
|
||||
private String kioskId;
|
||||
|
||||
protected DefaultKioskTab() {
|
||||
initKiosk();
|
||||
}
|
||||
|
||||
public void initKiosk() {
|
||||
this.kioskServiceId = ServiceHelper.getSelectedServiceId(App.getApp());
|
||||
try {
|
||||
this.kioskId = NewPipe.getService(this.kioskServiceId).getKioskList().getDefaultKioskId();
|
||||
} catch (ExtractionException e) {
|
||||
this.kioskId = "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTabId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTabName(Context context) {
|
||||
return KioskTranslator.getTranslatedKioskName(kioskId, context);
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
@Override
|
||||
public int getTabIconRes(Context context) {
|
||||
final int kioskIcon = KioskTranslator.getKioskIcons(kioskId, context);
|
||||
|
||||
if (kioskIcon <= 0) {
|
||||
throw new IllegalStateException("Kiosk ID is not valid: \"" + kioskId + "\"");
|
||||
}
|
||||
|
||||
return kioskIcon;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KioskFragment getFragment() throws ExtractionException {
|
||||
return KioskFragment.getInstance(kioskServiceId, kioskId);
|
||||
}
|
||||
|
||||
public String getKioskId() {
|
||||
return kioskId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,27 +9,18 @@ import com.grack.nanojson.JsonParserException;
|
||||
import com.grack.nanojson.JsonStringWriter;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
|
||||
import org.schabi.newpipe.settings.tabs.Tab.Type;
|
||||
import org.jsoup.helper.StringUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
|
||||
|
||||
/**
|
||||
* Class to get a JSON representation of a list of tabs, and the other way around.
|
||||
*/
|
||||
public class TabsJsonHelper {
|
||||
private static final String JSON_TABS_ARRAY_KEY = "tabs";
|
||||
|
||||
protected static final List<Tab> FALLBACK_INITIAL_TABS_LIST = Collections.unmodifiableList(Arrays.asList(
|
||||
new Tab.KioskTab(YouTube.getServiceId(), "Trending"),
|
||||
Type.SUBSCRIPTIONS.getTab(),
|
||||
Type.BOOKMARKS.getTab()
|
||||
));
|
||||
|
||||
public static class InvalidJsonException extends Exception {
|
||||
private InvalidJsonException() {
|
||||
super();
|
||||
@@ -48,7 +39,7 @@ public class TabsJsonHelper {
|
||||
* Try to reads the passed JSON and returns the list of tabs if no error were encountered.
|
||||
* <p>
|
||||
* If the JSON is null or empty, or the list of tabs that it represents is empty, the
|
||||
* {@link #FALLBACK_INITIAL_TABS_LIST fallback list} will be returned.
|
||||
* {@link #getDefaultTabs fallback list} will be returned.
|
||||
* <p>
|
||||
* Tabs with invalid ids (i.e. not in the {@link Tab.Type} enum) will be ignored.
|
||||
*
|
||||
@@ -58,7 +49,7 @@ public class TabsJsonHelper {
|
||||
*/
|
||||
public static List<Tab> getTabsFromJson(@Nullable String tabsJson) throws InvalidJsonException {
|
||||
if (tabsJson == null || tabsJson.isEmpty()) {
|
||||
return FALLBACK_INITIAL_TABS_LIST;
|
||||
return getDefaultTabs();
|
||||
}
|
||||
|
||||
final List<Tab> returnTabs = new ArrayList<>();
|
||||
@@ -86,12 +77,22 @@ public class TabsJsonHelper {
|
||||
}
|
||||
|
||||
if (returnTabs.isEmpty()) {
|
||||
return FALLBACK_INITIAL_TABS_LIST;
|
||||
return getDefaultTabs();
|
||||
}
|
||||
|
||||
return returnTabs;
|
||||
}
|
||||
|
||||
public static List<Tab> getDefaultTabs(){
|
||||
List<Tab> tabs = new ArrayList<>();
|
||||
Tab.DefaultKioskTab tab = new Tab.DefaultKioskTab();
|
||||
if(!StringUtil.isBlank(tab.getKioskId())){
|
||||
tabs.add(tab);
|
||||
}
|
||||
tabs.add(Tab.Type.SUBSCRIPTIONS.getTab());
|
||||
tabs.add(Tab.Type.BOOKMARKS.getTab());
|
||||
return Collections.unmodifiableList(tabs);
|
||||
}
|
||||
/**
|
||||
* Get a JSON representation from a list of tabs.
|
||||
*
|
||||
|
||||
@@ -44,7 +44,7 @@ public class TabsManager {
|
||||
}
|
||||
|
||||
public List<Tab> getDefaultTabs() {
|
||||
return TabsJsonHelper.FALLBACK_INITIAL_TABS_LIST;
|
||||
return TabsJsonHelper.getDefaultTabs();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package org.schabi.newpipe.streams;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
@@ -15,89 +16,239 @@ public class DataReader {
|
||||
public final static int INTEGER_SIZE = 4;
|
||||
public final static int FLOAT_SIZE = 4;
|
||||
|
||||
private long pos;
|
||||
public final SharpStream stream;
|
||||
private final boolean rewind;
|
||||
private final static int BUFFER_SIZE = 128 * 1024;// 128 KiB
|
||||
|
||||
private long position = 0;
|
||||
private final SharpStream stream;
|
||||
|
||||
private InputStream view;
|
||||
private int viewSize;
|
||||
|
||||
public DataReader(SharpStream stream) {
|
||||
this.rewind = stream.canRewind();
|
||||
this.stream = stream;
|
||||
this.pos = 0L;
|
||||
this.readOffset = this.readBuffer.length;
|
||||
}
|
||||
|
||||
public long position() {
|
||||
return pos;
|
||||
return position;
|
||||
}
|
||||
|
||||
public final int readInt() throws IOException {
|
||||
public int read() throws IOException {
|
||||
if (fillBuffer()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
position++;
|
||||
readCount--;
|
||||
|
||||
return readBuffer[readOffset++] & 0xFF;
|
||||
}
|
||||
|
||||
public long skipBytes(long amount) throws IOException {
|
||||
if (readCount < 0) {
|
||||
return 0;
|
||||
} else if (readCount == 0) {
|
||||
amount = stream.skip(amount);
|
||||
} else {
|
||||
if (readCount > amount) {
|
||||
readCount -= (int) amount;
|
||||
readOffset += (int) amount;
|
||||
} else {
|
||||
amount = readCount + stream.skip(amount - readCount);
|
||||
readCount = 0;
|
||||
readOffset = readBuffer.length;
|
||||
}
|
||||
}
|
||||
|
||||
position += amount;
|
||||
return amount;
|
||||
}
|
||||
|
||||
public int readInt() throws IOException {
|
||||
primitiveRead(INTEGER_SIZE);
|
||||
return primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
|
||||
}
|
||||
|
||||
public final int read() throws IOException {
|
||||
int value = stream.read();
|
||||
if (value == -1) {
|
||||
throw new EOFException();
|
||||
}
|
||||
|
||||
pos++;
|
||||
return value;
|
||||
public short readShort() throws IOException {
|
||||
primitiveRead(SHORT_SIZE);
|
||||
return (short) (primitive[0] << 8 | primitive[1]);
|
||||
}
|
||||
|
||||
public final long skipBytes(long amount) throws IOException {
|
||||
amount = stream.skip(amount);
|
||||
pos += amount;
|
||||
return amount;
|
||||
}
|
||||
|
||||
public final long readLong() throws IOException {
|
||||
public long readLong() throws IOException {
|
||||
primitiveRead(LONG_SIZE);
|
||||
long high = primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
|
||||
long low = primitive[4] << 24 | primitive[5] << 16 | primitive[6] << 8 | primitive[7];
|
||||
return high << 32 | low;
|
||||
}
|
||||
|
||||
public final short readShort() throws IOException {
|
||||
primitiveRead(SHORT_SIZE);
|
||||
return (short) (primitive[0] << 8 | primitive[1]);
|
||||
}
|
||||
|
||||
public final int read(byte[] buffer) throws IOException {
|
||||
public int read(byte[] buffer) throws IOException {
|
||||
return read(buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
public final int read(byte[] buffer, int offset, int count) throws IOException {
|
||||
int res = stream.read(buffer, offset, count);
|
||||
pos += res;
|
||||
public int read(byte[] buffer, int offset, int count) throws IOException {
|
||||
if (readCount < 0) {
|
||||
return -1;
|
||||
}
|
||||
int total = 0;
|
||||
|
||||
return res;
|
||||
if (count >= readBuffer.length) {
|
||||
if (readCount > 0) {
|
||||
System.arraycopy(readBuffer, readOffset, buffer, offset, readCount);
|
||||
readOffset += readCount;
|
||||
|
||||
offset += readCount;
|
||||
count -= readCount;
|
||||
|
||||
total = readCount;
|
||||
readCount = 0;
|
||||
}
|
||||
total += Math.max(stream.read(buffer, offset, count), 0);
|
||||
} else {
|
||||
while (count > 0 && !fillBuffer()) {
|
||||
int read = Math.min(readCount, count);
|
||||
System.arraycopy(readBuffer, readOffset, buffer, offset, read);
|
||||
|
||||
readOffset += read;
|
||||
readCount -= read;
|
||||
|
||||
offset += read;
|
||||
count -= read;
|
||||
|
||||
total += read;
|
||||
}
|
||||
}
|
||||
|
||||
position += total;
|
||||
return total;
|
||||
}
|
||||
|
||||
public final boolean available() {
|
||||
return stream.available() > 0;
|
||||
public boolean available() {
|
||||
return readCount > 0 || stream.available() > 0;
|
||||
}
|
||||
|
||||
public void rewind() throws IOException {
|
||||
stream.rewind();
|
||||
pos = 0;
|
||||
|
||||
if ((position - viewSize) > 0) {
|
||||
viewSize = 0;// drop view
|
||||
} else {
|
||||
viewSize += position;
|
||||
}
|
||||
|
||||
position = 0;
|
||||
readOffset = readBuffer.length;
|
||||
}
|
||||
|
||||
public boolean canRewind() {
|
||||
return rewind;
|
||||
return stream.canRewind();
|
||||
}
|
||||
|
||||
private short[] primitive = new short[LONG_SIZE];
|
||||
/**
|
||||
* Wraps this instance of {@code DataReader} into {@code InputStream}
|
||||
* object. Note: Any read in the {@code DataReader} will not modify
|
||||
* (decrease) the view size
|
||||
*
|
||||
* @param size the size of the view
|
||||
* @return the view
|
||||
*/
|
||||
public InputStream getView(int size) {
|
||||
if (view == null) {
|
||||
view = new InputStream() {
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (viewSize < 1) {
|
||||
return -1;
|
||||
}
|
||||
int res = DataReader.this.read();
|
||||
if (res > 0) {
|
||||
viewSize--;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer) throws IOException {
|
||||
return read(buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int offset, int count) throws IOException {
|
||||
if (viewSize < 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int res = DataReader.this.read(buffer, offset, Math.min(viewSize, count));
|
||||
viewSize -= res;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long amount) throws IOException {
|
||||
if (viewSize < 1) {
|
||||
return 0;
|
||||
}
|
||||
int res = (int) DataReader.this.skipBytes(Math.min(amount, viewSize));
|
||||
viewSize -= res;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() {
|
||||
return viewSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
viewSize = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
viewSize = size;
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private final short[] primitive = new short[LONG_SIZE];
|
||||
|
||||
private void primitiveRead(int amount) throws IOException {
|
||||
byte[] buffer = new byte[amount];
|
||||
int read = stream.read(buffer, 0, amount);
|
||||
pos += read;
|
||||
int read = read(buffer, 0, amount);
|
||||
|
||||
if (read != amount) {
|
||||
throw new EOFException("Truncated data, missing " + String.valueOf(amount - read) + " bytes");
|
||||
throw new EOFException("Truncated stream, missing " + String.valueOf(amount - read) + " bytes");
|
||||
}
|
||||
|
||||
for (int i = 0; i < buffer.length; i++) {
|
||||
primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" datatype is signed and is very annoying
|
||||
for (int i = 0; i < amount; i++) {
|
||||
primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" data type in java is signed and is very annoying
|
||||
}
|
||||
}
|
||||
|
||||
private final byte[] readBuffer = new byte[BUFFER_SIZE];
|
||||
private int readOffset;
|
||||
private int readCount;
|
||||
|
||||
private boolean fillBuffer() throws IOException {
|
||||
if (readCount < 0) {
|
||||
return true;
|
||||
}
|
||||
if (readOffset >= readBuffer.length) {
|
||||
readCount = stream.read(readBuffer);
|
||||
if (readCount < 1) {
|
||||
readCount = -1;
|
||||
return true;
|
||||
}
|
||||
readOffset = 0;
|
||||
}
|
||||
|
||||
return readCount < 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
package org.schabi.newpipe.streams;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
@@ -35,14 +33,29 @@ public class Mp4DashReader {
|
||||
private static final int ATOM_TREX = 0x74726578;
|
||||
private static final int ATOM_TKHD = 0x746B6864;
|
||||
private static final int ATOM_MFRA = 0x6D667261;
|
||||
private static final int ATOM_TFRA = 0x74667261;
|
||||
private static final int ATOM_MDHD = 0x6D646864;
|
||||
private static final int ATOM_EDTS = 0x65647473;
|
||||
private static final int ATOM_ELST = 0x656C7374;
|
||||
private static final int ATOM_HDLR = 0x68646C72;
|
||||
private static final int ATOM_MINF = 0x6D696E66;
|
||||
private static final int ATOM_DINF = 0x64696E66;
|
||||
private static final int ATOM_STBL = 0x7374626C;
|
||||
private static final int ATOM_STSD = 0x73747364;
|
||||
private static final int ATOM_VMHD = 0x766D6864;
|
||||
private static final int ATOM_SMHD = 0x736D6864;
|
||||
|
||||
private static final int BRAND_DASH = 0x64617368;
|
||||
private static final int BRAND_ISO5 = 0x69736F35;
|
||||
|
||||
private static final int HANDLER_VIDE = 0x76696465;
|
||||
private static final int HANDLER_SOUN = 0x736F756E;
|
||||
private static final int HANDLER_SUBT = 0x73756274;
|
||||
// </editor-fold>
|
||||
|
||||
private final DataReader stream;
|
||||
|
||||
private Mp4Track[] tracks = null;
|
||||
private int[] brands = null;
|
||||
|
||||
private Box box;
|
||||
private Moof moof;
|
||||
@@ -50,9 +63,10 @@ public class Mp4DashReader {
|
||||
private boolean chunkZero = false;
|
||||
|
||||
private int selectedTrack = -1;
|
||||
private Box backupBox = null;
|
||||
|
||||
public enum TrackKind {
|
||||
Audio, Video, Other
|
||||
Audio, Video, Subtitles, Other
|
||||
}
|
||||
|
||||
public Mp4DashReader(SharpStream source) {
|
||||
@@ -65,8 +79,15 @@ public class Mp4DashReader {
|
||||
}
|
||||
|
||||
box = readBox(ATOM_FTYP);
|
||||
if (parse_ftyp() != BRAND_DASH) {
|
||||
throw new NoSuchElementException("Main Brand is not dash");
|
||||
brands = parse_ftyp(box);
|
||||
switch (brands[0]) {
|
||||
case BRAND_DASH:
|
||||
case BRAND_ISO5:// ¿why not?
|
||||
break;
|
||||
default:
|
||||
throw new NoSuchElementException(
|
||||
"Not a MPEG-4 DASH container, major brand is not 'dash' or 'iso5' is " + boxName(brands[0])
|
||||
);
|
||||
}
|
||||
|
||||
Moov moov = null;
|
||||
@@ -84,8 +105,6 @@ public class Mp4DashReader {
|
||||
break;
|
||||
case ATOM_MFRA:
|
||||
break;
|
||||
case ATOM_MDAT:
|
||||
throw new IOException("Expected moof, found mdat");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,15 +126,26 @@ public class Mp4DashReader {
|
||||
}
|
||||
}
|
||||
|
||||
if (moov.trak[i].tkhd.bHeight == 0 && moov.trak[i].tkhd.bWidth == 0) {
|
||||
tracks[i].kind = moov.trak[i].tkhd.bVolume == 0 ? TrackKind.Other : TrackKind.Audio;
|
||||
} else {
|
||||
tracks[i].kind = TrackKind.Video;
|
||||
switch (moov.trak[i].mdia.hdlr.subType) {
|
||||
case HANDLER_VIDE:
|
||||
tracks[i].kind = TrackKind.Video;
|
||||
break;
|
||||
case HANDLER_SOUN:
|
||||
tracks[i].kind = TrackKind.Audio;
|
||||
break;
|
||||
case HANDLER_SUBT:
|
||||
tracks[i].kind = TrackKind.Subtitles;
|
||||
break;
|
||||
default:
|
||||
tracks[i].kind = TrackKind.Other;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
backupBox = box;
|
||||
}
|
||||
|
||||
public Mp4Track selectTrack(int index) {
|
||||
Mp4Track selectTrack(int index) {
|
||||
selectedTrack = index;
|
||||
return tracks[index];
|
||||
}
|
||||
@@ -126,7 +156,7 @@ public class Mp4DashReader {
|
||||
* @return list with a basic info
|
||||
* @throws IOException if the source stream is not seekeable
|
||||
*/
|
||||
public int getFragmentsCount() throws IOException {
|
||||
int getFragmentsCount() throws IOException {
|
||||
if (selectedTrack < 0) {
|
||||
throw new IllegalStateException("track no selected");
|
||||
}
|
||||
@@ -136,7 +166,6 @@ public class Mp4DashReader {
|
||||
|
||||
Box tmp;
|
||||
int count = 0;
|
||||
long orig_offset = stream.position();
|
||||
|
||||
if (box.type == ATOM_MOOF) {
|
||||
tmp = box;
|
||||
@@ -162,17 +191,36 @@ public class Mp4DashReader {
|
||||
ensure(tmp);
|
||||
} while (stream.available() && (tmp = readBox()) != null);
|
||||
|
||||
stream.rewind();
|
||||
stream.skipBytes((int) orig_offset);
|
||||
rewind();
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public int[] getBrands() {
|
||||
if (brands == null) throw new IllegalStateException("Not parsed");
|
||||
return brands;
|
||||
}
|
||||
|
||||
public void rewind() throws IOException {
|
||||
if (!stream.canRewind()) {
|
||||
throw new IOException("The provided stream doesn't allow seek");
|
||||
}
|
||||
if (box == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
box = backupBox;
|
||||
chunkZero = false;
|
||||
|
||||
stream.rewind();
|
||||
stream.skipBytes(backupBox.offset + (DataReader.INTEGER_SIZE * 2));
|
||||
}
|
||||
|
||||
public Mp4Track[] getAvailableTracks() {
|
||||
return tracks;
|
||||
}
|
||||
|
||||
public Mp4TrackChunk getNextChunk() throws IOException {
|
||||
public Mp4DashChunk getNextChunk(boolean infoOnly) throws IOException {
|
||||
Mp4Track track = tracks[selectedTrack];
|
||||
|
||||
while (stream.available()) {
|
||||
@@ -208,7 +256,7 @@ public class Mp4DashReader {
|
||||
if (hasFlag(moof.traf.tfhd.bFlags, 0x10)) {
|
||||
moof.traf.trun.chunkSize = moof.traf.tfhd.defaultSampleSize * moof.traf.trun.entryCount;
|
||||
} else {
|
||||
moof.traf.trun.chunkSize = box.size - 8;
|
||||
moof.traf.trun.chunkSize = (int) (box.size - 8);
|
||||
}
|
||||
}
|
||||
if (!hasFlag(moof.traf.trun.bFlags, 0x900) && moof.traf.trun.chunkDuration == 0) {
|
||||
@@ -228,9 +276,12 @@ public class Mp4DashReader {
|
||||
continue;// find another chunk
|
||||
}
|
||||
|
||||
Mp4TrackChunk chunk = new Mp4TrackChunk();
|
||||
Mp4DashChunk chunk = new Mp4DashChunk();
|
||||
chunk.moof = moof;
|
||||
chunk.data = new TrackDataChunk(stream, moof.traf.trun.chunkSize);
|
||||
if (!infoOnly) {
|
||||
chunk.data = stream.getView(moof.traf.trun.chunkSize);
|
||||
}
|
||||
|
||||
moof = null;
|
||||
|
||||
stream.skipBytes(chunk.moof.traf.trun.dataOffset);
|
||||
@@ -269,6 +320,10 @@ public class Mp4DashReader {
|
||||
b.size = stream.readInt();
|
||||
b.type = stream.readInt();
|
||||
|
||||
if (b.size == 1) {
|
||||
b.size = stream.readLong();
|
||||
}
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
@@ -280,6 +335,25 @@ public class Mp4DashReader {
|
||||
return b;
|
||||
}
|
||||
|
||||
private byte[] readFullBox(Box ref) throws IOException {
|
||||
// full box reading is limited to 2 GiB, and should be enough
|
||||
int size = (int) ref.size;
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(size);
|
||||
buffer.putInt(size);
|
||||
buffer.putInt(ref.type);
|
||||
|
||||
int read = size - 8;
|
||||
|
||||
if (stream.read(buffer.array(), 8, read) != read) {
|
||||
throw new EOFException(
|
||||
String.format("EOF reached in box: type=%s offset=%s size=%s", boxName(ref.type), ref.offset, ref.size)
|
||||
);
|
||||
}
|
||||
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
private void ensure(Box ref) throws IOException {
|
||||
long skip = ref.offset + ref.size - stream.position();
|
||||
|
||||
@@ -310,6 +384,14 @@ public class Mp4DashReader {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Box untilAnyBox(Box ref) throws IOException {
|
||||
if (stream.position() >= (ref.offset + ref.size)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return readBox();
|
||||
}
|
||||
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultState="collapsed" desc="Box readers">
|
||||
@@ -329,7 +411,7 @@ public class Mp4DashReader {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -397,14 +479,14 @@ public class Mp4DashReader {
|
||||
|
||||
private long parse_tfdt() throws IOException {
|
||||
int version = stream.read();
|
||||
stream.skipBytes(3);// flags
|
||||
stream.skipBytes(3);// flags
|
||||
return version == 0 ? readUint() : stream.readLong();
|
||||
}
|
||||
|
||||
private Trun parse_trun() throws IOException {
|
||||
Trun obj = new Trun();
|
||||
obj.bFlags = stream.readInt();
|
||||
obj.entryCount = stream.readInt();// unsigned int
|
||||
obj.entryCount = stream.readInt();// unsigned int
|
||||
|
||||
obj.entries_rowSize = 0;
|
||||
if (hasFlag(obj.bFlags, 0x0100)) {
|
||||
@@ -448,11 +530,18 @@ public class Mp4DashReader {
|
||||
return obj;
|
||||
}
|
||||
|
||||
private int parse_ftyp() throws IOException {
|
||||
int brand = stream.readInt();
|
||||
private int[] parse_ftyp(Box ref) throws IOException {
|
||||
int i = 0;
|
||||
int[] list = new int[(int) ((ref.offset + ref.size - stream.position() - 4) / 4)];
|
||||
|
||||
list[i++] = stream.readInt();// major brand
|
||||
|
||||
stream.skipBytes(4);// minor version
|
||||
|
||||
return brand;
|
||||
for (; i < list.length; i++)
|
||||
list[i] = stream.readInt();// compatible brands
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private Mvhd parse_mvhd() throws IOException {
|
||||
@@ -521,32 +610,66 @@ public class Mp4DashReader {
|
||||
trak.tkhd = parse_tkhd();
|
||||
ensure(b);
|
||||
|
||||
b = untilBox(ref, ATOM_MDIA);
|
||||
trak.mdia = new byte[b.size];
|
||||
while ((b = untilBox(ref, ATOM_MDIA, ATOM_EDTS)) != null) {
|
||||
switch (b.type) {
|
||||
case ATOM_MDIA:
|
||||
trak.mdia = parse_mdia(b);
|
||||
break;
|
||||
case ATOM_EDTS:
|
||||
trak.edst_elst = parse_edts(b);
|
||||
break;
|
||||
}
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(trak.mdia);
|
||||
buffer.putInt(b.size);
|
||||
buffer.putInt(ATOM_MDIA);
|
||||
stream.read(trak.mdia, 8, b.size - 8);
|
||||
|
||||
trak.mdia_mdhd_timeScale = parse_mdia(buffer);
|
||||
ensure(b);
|
||||
}
|
||||
|
||||
return trak;
|
||||
}
|
||||
|
||||
private int parse_mdia(ByteBuffer data) {
|
||||
while (data.hasRemaining()) {
|
||||
int end = data.position() + data.getInt();
|
||||
if (data.getInt() == ATOM_MDHD) {
|
||||
byte version = data.get();
|
||||
data.position(data.position() + 3 + ((version == 0 ? 4 : 8) * 2));
|
||||
return data.getInt();
|
||||
}
|
||||
private Mdia parse_mdia(Box ref) throws IOException {
|
||||
Mdia obj = new Mdia();
|
||||
|
||||
data.position(end);
|
||||
Box b;
|
||||
while ((b = untilBox(ref, ATOM_MDHD, ATOM_HDLR, ATOM_MINF)) != null) {
|
||||
switch (b.type) {
|
||||
case ATOM_MDHD:
|
||||
obj.mdhd = readFullBox(b);
|
||||
|
||||
// read time scale
|
||||
ByteBuffer buffer = ByteBuffer.wrap(obj.mdhd);
|
||||
byte version = buffer.get(8);
|
||||
buffer.position(12 + ((version == 0 ? 4 : 8) * 2));
|
||||
obj.mdhd_timeScale = buffer.getInt();
|
||||
break;
|
||||
case ATOM_HDLR:
|
||||
obj.hdlr = parse_hdlr(b);
|
||||
break;
|
||||
case ATOM_MINF:
|
||||
obj.minf = parse_minf(b);
|
||||
break;
|
||||
}
|
||||
ensure(b);
|
||||
}
|
||||
|
||||
return 0;// this NEVER should happen
|
||||
return obj;
|
||||
}
|
||||
|
||||
private Hdlr parse_hdlr(Box ref) throws IOException {
|
||||
// version
|
||||
// flags
|
||||
stream.skipBytes(4);
|
||||
|
||||
Hdlr obj = new Hdlr();
|
||||
obj.bReserved = new byte[12];
|
||||
|
||||
obj.type = stream.readInt();
|
||||
obj.subType = stream.readInt();
|
||||
stream.read(obj.bReserved);
|
||||
|
||||
// component name (is a ansi/ascii string)
|
||||
stream.skipBytes((ref.offset + ref.size) - stream.position());
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private Moov parse_moov(Box ref) throws IOException {
|
||||
@@ -570,7 +693,7 @@ public class Mp4DashReader {
|
||||
ensure(b);
|
||||
}
|
||||
|
||||
moov.trak = tmp.toArray(new Trak[tmp.size()]);
|
||||
moov.trak = tmp.toArray(new Trak[0]);
|
||||
|
||||
return moov;
|
||||
}
|
||||
@@ -584,7 +707,7 @@ public class Mp4DashReader {
|
||||
ensure(b);
|
||||
}
|
||||
|
||||
return tmp.toArray(new Trex[tmp.size()]);
|
||||
return tmp.toArray(new Trex[0]);
|
||||
}
|
||||
|
||||
private Trex parse_trex() throws IOException {
|
||||
@@ -602,74 +725,74 @@ public class Mp4DashReader {
|
||||
return obj;
|
||||
}
|
||||
|
||||
private Tfra parse_tfra() throws IOException {
|
||||
int version = stream.read();
|
||||
|
||||
stream.skipBytes(3);// flags
|
||||
|
||||
Tfra tfra = new Tfra();
|
||||
tfra.trackId = stream.readInt();
|
||||
|
||||
stream.skipBytes(3);// reserved
|
||||
int bFlags = stream.read();
|
||||
int size_tts = ((bFlags >> 4) & 3) + ((bFlags >> 2) & 3) + (bFlags & 3);
|
||||
|
||||
tfra.entries_time = new int[stream.readInt()];
|
||||
|
||||
for (int i = 0; i < tfra.entries_time.length; i++) {
|
||||
tfra.entries_time[i] = version == 0 ? stream.readInt() : (int) stream.readLong();
|
||||
stream.skipBytes(size_tts + (version == 0 ? 4 : 8));
|
||||
private Elst parse_edts(Box ref) throws IOException {
|
||||
Box b = untilBox(ref, ATOM_ELST);
|
||||
if (b == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return tfra;
|
||||
}
|
||||
|
||||
private Sidx parse_sidx() throws IOException {
|
||||
int version = stream.read();
|
||||
Elst obj = new Elst();
|
||||
|
||||
boolean v1 = stream.read() == 1;
|
||||
stream.skipBytes(3);// flags
|
||||
|
||||
Sidx obj = new Sidx();
|
||||
obj.referenceId = stream.readInt();
|
||||
obj.timescale = stream.readInt();
|
||||
int entryCount = stream.readInt();
|
||||
if (entryCount < 1) {
|
||||
obj.bMediaRate = 0x00010000;// default media rate (1.0)
|
||||
return obj;
|
||||
}
|
||||
|
||||
// earliest presentation entries_time
|
||||
// first offset
|
||||
// reserved
|
||||
stream.skipBytes((2 * (version == 0 ? 4 : 8)) + 2);
|
||||
if (v1) {
|
||||
stream.skipBytes(DataReader.LONG_SIZE);// segment duration
|
||||
obj.MediaTime = stream.readLong();
|
||||
// ignore all remain entries
|
||||
stream.skipBytes((entryCount - 1) * (DataReader.LONG_SIZE * 2));
|
||||
} else {
|
||||
stream.skipBytes(DataReader.INTEGER_SIZE);// segment duration
|
||||
obj.MediaTime = stream.readInt();
|
||||
}
|
||||
|
||||
obj.entries_subsegmentDuration = new int[stream.readShort()];
|
||||
obj.bMediaRate = stream.readInt();
|
||||
|
||||
for (int i = 0; i < obj.entries_subsegmentDuration.length; i++) {
|
||||
// reference type
|
||||
// referenced size
|
||||
stream.skipBytes(4);
|
||||
obj.entries_subsegmentDuration[i] = stream.readInt();// unsigned int
|
||||
return obj;
|
||||
}
|
||||
|
||||
// starts with SAP
|
||||
// SAP type
|
||||
// SAP delta entries_time
|
||||
stream.skipBytes(4);
|
||||
private Minf parse_minf(Box ref) throws IOException {
|
||||
Minf obj = new Minf();
|
||||
|
||||
Box b;
|
||||
while ((b = untilAnyBox(ref)) != null) {
|
||||
|
||||
switch (b.type) {
|
||||
case ATOM_DINF:
|
||||
obj.dinf = readFullBox(b);
|
||||
break;
|
||||
case ATOM_STBL:
|
||||
obj.stbl_stsd = parse_stbl(b);
|
||||
break;
|
||||
case ATOM_VMHD:
|
||||
case ATOM_SMHD:
|
||||
obj.$mhd = readFullBox(b);
|
||||
break;
|
||||
|
||||
}
|
||||
ensure(b);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private Tfra[] parse_mfra(Box ref, int trackCount) throws IOException {
|
||||
ArrayList<Tfra> tmp = new ArrayList<>(trackCount);
|
||||
long limit = ref.offset + ref.size;
|
||||
/**
|
||||
* this only read the "stsd" box inside
|
||||
*/
|
||||
private byte[] parse_stbl(Box ref) throws IOException {
|
||||
Box b = untilBox(ref, ATOM_STSD);
|
||||
|
||||
while (stream.position() < limit) {
|
||||
box = readBox();
|
||||
|
||||
if (box.type == ATOM_TFRA) {
|
||||
tmp.add(parse_tfra());
|
||||
}
|
||||
|
||||
ensure(box);
|
||||
if (b == null) {
|
||||
return new byte[0];// this never should happens (missing codec startup data)
|
||||
}
|
||||
|
||||
return tmp.toArray(new Tfra[tmp.size()]);
|
||||
return readFullBox(b);
|
||||
}
|
||||
|
||||
// </editor-fold>
|
||||
@@ -679,14 +802,7 @@ public class Mp4DashReader {
|
||||
|
||||
int type;
|
||||
long offset;
|
||||
int size;
|
||||
}
|
||||
|
||||
class Sidx {
|
||||
|
||||
int timescale;
|
||||
int referenceId;
|
||||
int[] entries_subsegmentDuration;
|
||||
long size;
|
||||
}
|
||||
|
||||
public class Moof {
|
||||
@@ -711,12 +827,16 @@ public class Mp4DashReader {
|
||||
int defaultSampleFlags;
|
||||
}
|
||||
|
||||
public class TrunEntry {
|
||||
class TrunEntry {
|
||||
|
||||
int sampleDuration;
|
||||
int sampleSize;
|
||||
int sampleFlags;
|
||||
int sampleCompositionTimeOffset;
|
||||
|
||||
boolean hasCompositionTimeOffset;
|
||||
boolean isKeyframe;
|
||||
|
||||
public int sampleDuration;
|
||||
public int sampleSize;
|
||||
public int sampleFlags;
|
||||
public int sampleCompositionTimeOffset;
|
||||
}
|
||||
|
||||
public class Trun {
|
||||
@@ -749,6 +869,31 @@ public class Mp4DashReader {
|
||||
entry.sampleCompositionTimeOffset = buffer.getInt();
|
||||
}
|
||||
|
||||
entry.hasCompositionTimeOffset = hasFlag(bFlags, 0x0800);
|
||||
entry.isKeyframe = !hasFlag(entry.sampleFlags, 0x10000);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
public TrunEntry getAbsoluteEntry(int i, Tfhd header) {
|
||||
TrunEntry entry = getEntry(i);
|
||||
|
||||
if (!hasFlag(bFlags, 0x0100) && hasFlag(header.bFlags, 0x20)) {
|
||||
entry.sampleFlags = header.defaultSampleFlags;
|
||||
}
|
||||
|
||||
if (!hasFlag(bFlags, 0x0200) && hasFlag(header.bFlags, 0x10)) {
|
||||
entry.sampleSize = header.defaultSampleSize;
|
||||
}
|
||||
|
||||
if (!hasFlag(bFlags, 0x0100) && hasFlag(header.bFlags, 0x08)) {
|
||||
entry.sampleDuration = header.defaultSampleDuration;
|
||||
}
|
||||
|
||||
if (i == 0 && hasFlag(bFlags, 0x0004)) {
|
||||
entry.sampleFlags = bFirstSampleFlags;
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
@@ -768,9 +913,9 @@ public class Mp4DashReader {
|
||||
public class Trak {
|
||||
|
||||
public Tkhd tkhd;
|
||||
public int mdia_mdhd_timeScale;
|
||||
public Elst edst_elst;
|
||||
public Mdia mdia;
|
||||
|
||||
byte[] mdia;
|
||||
}
|
||||
|
||||
class Mvhd {
|
||||
@@ -786,12 +931,6 @@ public class Mp4DashReader {
|
||||
Trex[] mvex_trex;
|
||||
}
|
||||
|
||||
class Tfra {
|
||||
|
||||
int trackId;
|
||||
int[] entries_time;
|
||||
}
|
||||
|
||||
public class Trex {
|
||||
|
||||
private int trackId;
|
||||
@@ -801,6 +940,34 @@ public class Mp4DashReader {
|
||||
int defaultSampleFlags;
|
||||
}
|
||||
|
||||
public class Elst {
|
||||
|
||||
public long MediaTime;
|
||||
public int bMediaRate;
|
||||
}
|
||||
|
||||
public class Mdia {
|
||||
|
||||
public int mdhd_timeScale;
|
||||
public byte[] mdhd;
|
||||
public Hdlr hdlr;
|
||||
public Minf minf;
|
||||
}
|
||||
|
||||
public class Hdlr {
|
||||
|
||||
public int type;
|
||||
public int subType;
|
||||
public byte[] bReserved;
|
||||
}
|
||||
|
||||
public class Minf {
|
||||
|
||||
public byte[] dinf;
|
||||
public byte[] stbl_stsd;
|
||||
public byte[] $mhd;
|
||||
}
|
||||
|
||||
public class Mp4Track {
|
||||
|
||||
public TrackKind kind;
|
||||
@@ -808,10 +975,43 @@ public class Mp4DashReader {
|
||||
public Trex trex;
|
||||
}
|
||||
|
||||
public class Mp4TrackChunk {
|
||||
public class Mp4DashChunk {
|
||||
|
||||
public InputStream data;
|
||||
public Moof moof;
|
||||
private int i = 0;
|
||||
|
||||
public TrunEntry getNextSampleInfo() {
|
||||
if (i >= moof.traf.trun.entryCount) {
|
||||
return null;
|
||||
}
|
||||
return moof.traf.trun.getAbsoluteEntry(i++, moof.traf.tfhd);
|
||||
}
|
||||
|
||||
public Mp4DashSample getNextSample() throws IOException {
|
||||
if (data == null) {
|
||||
throw new IllegalStateException("This chunk has info only");
|
||||
}
|
||||
if (i >= moof.traf.trun.entryCount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Mp4DashSample sample = new Mp4DashSample();
|
||||
sample.info = moof.traf.trun.getAbsoluteEntry(i++, moof.traf.tfhd);
|
||||
sample.data = new byte[sample.info.sampleSize];
|
||||
|
||||
if (data.read(sample.data) != sample.info.sampleSize) {
|
||||
throw new EOFException("EOF reached while reading a sample");
|
||||
}
|
||||
|
||||
return sample;
|
||||
}
|
||||
}
|
||||
|
||||
public class Mp4DashSample {
|
||||
|
||||
public TrunEntry info;
|
||||
public byte[] data;
|
||||
}
|
||||
//</editor-fold>
|
||||
}
|
||||
|
||||
@@ -1,623 +0,0 @@
|
||||
package org.schabi.newpipe.streams;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Mp4TrackChunk;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Trak;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Trex;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.schabi.newpipe.streams.Mp4DashReader.hasFlag;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class Mp4DashWriter {
|
||||
|
||||
private final static byte DIMENSIONAL_FIVE = 5;
|
||||
private final static byte DIMENSIONAL_TWO = 2;
|
||||
private final static short DEFAULT_TIMESCALE = 1000;
|
||||
private final static int BUFFER_SIZE = 8 * 1024;
|
||||
private final static byte DEFAULT_TREX_SIZE = 32;
|
||||
private final static byte[] TFRA_TTS_DEFAULT = new byte[]{0x01, 0x01, 0x01};
|
||||
private final static int EPOCH_OFFSET = 2082844800;
|
||||
|
||||
private Mp4Track[] infoTracks;
|
||||
private SharpStream[] sourceTracks;
|
||||
|
||||
private Mp4DashReader[] readers;
|
||||
private final long time;
|
||||
|
||||
private boolean done = false;
|
||||
private boolean parsed = false;
|
||||
|
||||
private long written = 0;
|
||||
private ArrayList<ArrayList<Integer>> chunkTimes;
|
||||
private ArrayList<Long> moofOffsets;
|
||||
private ArrayList<Integer> fragSizes;
|
||||
|
||||
public Mp4DashWriter(SharpStream... source) {
|
||||
sourceTracks = source;
|
||||
readers = new Mp4DashReader[sourceTracks.length];
|
||||
infoTracks = new Mp4Track[sourceTracks.length];
|
||||
time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET;
|
||||
}
|
||||
|
||||
public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException {
|
||||
if (!parsed) {
|
||||
throw new IllegalStateException("All sources must be parsed first");
|
||||
}
|
||||
|
||||
return readers[sourceIndex].getAvailableTracks();
|
||||
}
|
||||
|
||||
public void parseSources() throws IOException, IllegalStateException {
|
||||
if (done) {
|
||||
throw new IllegalStateException("already done");
|
||||
}
|
||||
if (parsed) {
|
||||
throw new IllegalStateException("already parsed");
|
||||
}
|
||||
|
||||
try {
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
readers[i] = new Mp4DashReader(sourceTracks[i]);
|
||||
readers[i].parse();
|
||||
}
|
||||
|
||||
} finally {
|
||||
parsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void selectTracks(int... trackIndex) throws IOException {
|
||||
if (done) {
|
||||
throw new IOException("already done");
|
||||
}
|
||||
if (chunkTimes != null) {
|
||||
throw new IOException("tracks already selected");
|
||||
}
|
||||
|
||||
try {
|
||||
chunkTimes = new ArrayList<>(readers.length);
|
||||
moofOffsets = new ArrayList<>(32);
|
||||
fragSizes = new ArrayList<>(32);
|
||||
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
infoTracks[i] = readers[i].selectTrack(trackIndex[i]);
|
||||
|
||||
chunkTimes.add(new ArrayList<Integer>(32));
|
||||
}
|
||||
|
||||
} finally {
|
||||
parsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public long getBytesWritten() {
|
||||
return written;
|
||||
}
|
||||
|
||||
public void build(SharpStream out) throws IOException, RuntimeException {
|
||||
if (done) {
|
||||
throw new RuntimeException("already done");
|
||||
}
|
||||
if (!out.canWrite()) {
|
||||
throw new IOException("the provided output is not writable");
|
||||
}
|
||||
|
||||
long sidxOffsets = -1;
|
||||
int maxFrags = 0;
|
||||
|
||||
for (SharpStream stream : sourceTracks) {
|
||||
if (!stream.canRewind()) {
|
||||
sidxOffsets = -2;// sidx not available
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
dump(make_ftyp(), out);
|
||||
dump(make_moov(), out);
|
||||
|
||||
if (sidxOffsets == -1 && out.canRewind()) {
|
||||
//<editor-fold defaultstate="collapsed" desc="calculate sidx">
|
||||
int reserved = 0;
|
||||
for (Mp4DashReader reader : readers) {
|
||||
int count = reader.getFragmentsCount();
|
||||
if (count > maxFrags) {
|
||||
maxFrags = count;
|
||||
}
|
||||
reserved += 12 + calcSidxBodySize(count);
|
||||
}
|
||||
if (maxFrags > 0xFFFF) {
|
||||
sidxOffsets = -3;// TODO: to many fragments, needs a multi-sidx implementation
|
||||
} else {
|
||||
sidxOffsets = written;
|
||||
dump(make_free(reserved), out);
|
||||
}
|
||||
//</editor-fold>
|
||||
}
|
||||
ArrayList<Mp4TrackChunk> chunks = new ArrayList<>(readers.length);
|
||||
chunks.add(null);
|
||||
|
||||
int read;
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int sequenceNumber = 1;
|
||||
|
||||
while (true) {
|
||||
chunks.clear();
|
||||
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
Mp4TrackChunk chunk = readers[i].getNextChunk();
|
||||
if (chunk == null || chunk.moof.traf.trun.chunkSize < 1) {
|
||||
continue;
|
||||
}
|
||||
chunk.moof.traf.tfhd.trackId = i + 1;
|
||||
chunks.add(chunk);
|
||||
|
||||
if (sequenceNumber == 1) {
|
||||
if (chunk.moof.traf.trun.entryCount > 0 && hasFlag(chunk.moof.traf.trun.bFlags, 0x0800)) {
|
||||
chunkTimes.get(i).add(chunk.moof.traf.trun.getEntry(0).sampleCompositionTimeOffset);
|
||||
} else {
|
||||
chunkTimes.get(i).add(0);
|
||||
}
|
||||
}
|
||||
|
||||
chunkTimes.get(i).add(chunk.moof.traf.trun.chunkDuration);
|
||||
}
|
||||
|
||||
if (chunks.size() < 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
long offset = written;
|
||||
moofOffsets.add(offset);
|
||||
|
||||
dump(make_moof(sequenceNumber++, chunks, offset), out);
|
||||
dump(make_mdat(chunks), out);
|
||||
|
||||
for (Mp4TrackChunk chunk : chunks) {
|
||||
while ((read = chunk.data.read(buffer)) > 0) {
|
||||
out.write(buffer, 0, read);
|
||||
written += read;
|
||||
}
|
||||
}
|
||||
|
||||
fragSizes.add((int) (written - offset));
|
||||
}
|
||||
|
||||
dump(make_mfra(), out);
|
||||
|
||||
if (sidxOffsets > 0 && moofOffsets.size() == maxFrags) {
|
||||
long len = written;
|
||||
|
||||
out.rewind();
|
||||
out.skip(sidxOffsets);
|
||||
|
||||
written = sidxOffsets;
|
||||
sidxOffsets = moofOffsets.get(0);
|
||||
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
dump(make_sidx(i, sidxOffsets - written), out);
|
||||
}
|
||||
|
||||
written = len;
|
||||
}
|
||||
} finally {
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return done;
|
||||
}
|
||||
|
||||
public boolean isParsed() {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
done = true;
|
||||
parsed = true;
|
||||
|
||||
for (SharpStream src : sourceTracks) {
|
||||
src.dispose();
|
||||
}
|
||||
|
||||
sourceTracks = null;
|
||||
readers = null;
|
||||
infoTracks = null;
|
||||
moofOffsets = null;
|
||||
chunkTimes = null;
|
||||
}
|
||||
|
||||
// <editor-fold defaultstate="collapsed" desc="Utils">
|
||||
private void dump(byte[][] buffer, SharpStream stream) throws IOException {
|
||||
for (byte[] buff : buffer) {
|
||||
stream.write(buff);
|
||||
written += buff.length;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[][] lengthFor(byte[][] buffer) {
|
||||
int length = 0;
|
||||
for (byte[] buff : buffer) {
|
||||
length += buff.length;
|
||||
}
|
||||
|
||||
ByteBuffer.wrap(buffer[0]).putInt(length);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private int calcSidxBodySize(int entryCount) {
|
||||
return 4 + 4 + 8 + 8 + 4 + (entryCount * 12);
|
||||
}
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultstate="collapsed" desc="Box makers">
|
||||
private byte[][] make_moof(int sequence, ArrayList<Mp4TrackChunk> chunks, long referenceOffset) {
|
||||
int pos = 2;
|
||||
TrunExtra[] extra = new TrunExtra[chunks.size()];
|
||||
|
||||
byte[][] buffer = new byte[pos + (extra.length * DIMENSIONAL_FIVE)][];
|
||||
buffer[0] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x66,// info header
|
||||
0x00, 0x00, 0x00, 0x10, 0x6D, 0x66, 0x68, 0x64, 0x00, 0x00, 0x00, 0x00//mfhd
|
||||
};
|
||||
buffer[1] = new byte[4];
|
||||
ByteBuffer.wrap(buffer[1]).putInt(sequence);
|
||||
|
||||
for (int i = 0; i < extra.length; i++) {
|
||||
extra[i] = new TrunExtra();
|
||||
for (byte[] buff : make_traf(chunks.get(i), extra[i], referenceOffset)) {
|
||||
buffer[pos++] = buff;
|
||||
}
|
||||
}
|
||||
|
||||
lengthFor(buffer);
|
||||
|
||||
int offset = 8 + ByteBuffer.wrap(buffer[0]).getInt();
|
||||
|
||||
for (int i = 0; i < extra.length; i++) {
|
||||
extra[i].byteBuffer.putInt(offset);
|
||||
offset += chunks.get(i).moof.traf.trun.chunkSize;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private byte[][] make_traf(Mp4TrackChunk chunk, TrunExtra extra, long moofOffset) {
|
||||
byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
|
||||
buffer[0] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x66,
|
||||
0x00, 0x00, 0x00, 0x00, 0x74, 0x66, 0x68, 0x64
|
||||
};
|
||||
|
||||
int flags = (chunk.moof.traf.tfhd.bFlags & 0x38) | 0x01;
|
||||
byte tfhdBodySize = 8 + 8;
|
||||
if (hasFlag(flags, 0x08)) {
|
||||
tfhdBodySize += 4;
|
||||
}
|
||||
if (hasFlag(flags, 0x10)) {
|
||||
tfhdBodySize += 4;
|
||||
}
|
||||
if (hasFlag(flags, 0x20)) {
|
||||
tfhdBodySize += 4;
|
||||
}
|
||||
buffer[1] = new byte[tfhdBodySize];
|
||||
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
|
||||
set.position(4);
|
||||
set.putInt(chunk.moof.traf.tfhd.trackId);
|
||||
set.putLong(moofOffset);
|
||||
if (hasFlag(flags, 0x08)) {
|
||||
set.putInt(chunk.moof.traf.tfhd.defaultSampleDuration);
|
||||
}
|
||||
if (hasFlag(flags, 0x10)) {
|
||||
set.putInt(chunk.moof.traf.tfhd.defaultSampleSize);
|
||||
}
|
||||
if (hasFlag(flags, 0x20)) {
|
||||
set.putInt(chunk.moof.traf.tfhd.defaultSampleFlags);
|
||||
}
|
||||
set.putInt(0, flags);
|
||||
ByteBuffer.wrap(buffer[0]).putInt(8, 8 + tfhdBodySize);
|
||||
|
||||
buffer[2] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x14,
|
||||
0x74, 0x66, 0x64, 0x74,
|
||||
0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
ByteBuffer.wrap(buffer[2]).putLong(12, chunk.moof.traf.tfdt);
|
||||
|
||||
buffer[3] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x75, 0x6E,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
buffer[4] = chunk.moof.traf.trun.bEntries;
|
||||
|
||||
lengthFor(buffer);
|
||||
|
||||
set = ByteBuffer.wrap(buffer[3]);
|
||||
set.putInt(buffer[3].length + buffer[4].length);
|
||||
set.position(8);
|
||||
set.putInt((chunk.moof.traf.trun.bFlags | 0x01) & 0x0F01);
|
||||
set.putInt(chunk.moof.traf.trun.entryCount);
|
||||
extra.byteBuffer = set;
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private byte[][] make_mdat(ArrayList<Mp4TrackChunk> chunks) {
|
||||
byte[][] buffer = new byte[][]{
|
||||
{
|
||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x61, 0x74
|
||||
}
|
||||
};
|
||||
|
||||
int length = 0;
|
||||
|
||||
for (Mp4TrackChunk chunk : chunks) {
|
||||
length += chunk.moof.traf.trun.chunkSize;
|
||||
}
|
||||
|
||||
ByteBuffer.wrap(buffer[0]).putInt(length + 8);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private byte[][] make_ftyp() {
|
||||
return new byte[][]{
|
||||
{
|
||||
0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x64, 0x61, 0x73, 0x68, 0x00, 0x00, 0x00, 0x00,
|
||||
0x6D, 0x70, 0x34, 0x31, 0x69, 0x73, 0x6F, 0x6D, 0x69, 0x73, 0x6F, 0x36, 0x69, 0x73, 0x6F, 0x32
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private byte[][] make_mvhd() {
|
||||
byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
|
||||
|
||||
buffer[0] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 0x00
|
||||
};
|
||||
buffer[1] = new byte[28];
|
||||
buffer[2] = new byte[]{
|
||||
0x00, 0x01, 0x00, 0x00, 0x01, 0x00,// default volume and rate
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,// reserved values
|
||||
// default matrix
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x40, 0x00, 0x00, 0x00
|
||||
};
|
||||
buffer[3] = new byte[24];// predefined
|
||||
buffer[4] = ByteBuffer.allocate(4).putInt(infoTracks.length + 1).array();
|
||||
|
||||
long longestTrack = 0;
|
||||
|
||||
for (Mp4Track track : infoTracks) {
|
||||
long tmp = (long) ((track.trak.tkhd.duration / (double) track.trak.mdia_mdhd_timeScale) * DEFAULT_TIMESCALE);
|
||||
if (tmp > longestTrack) {
|
||||
longestTrack = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuffer.wrap(buffer[1])
|
||||
.putLong(time)
|
||||
.putLong(time)
|
||||
.putInt(DEFAULT_TIMESCALE)
|
||||
.putLong(longestTrack);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private byte[][] make_trak(int trackId, Trak trak) throws RuntimeException {
|
||||
if (trak.tkhd.matrix.length != 36) {
|
||||
throw new RuntimeException("bad track matrix length (expected 36)");
|
||||
}
|
||||
|
||||
byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
|
||||
|
||||
buffer[0] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B,// trak header
|
||||
0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 // tkhd header
|
||||
};
|
||||
buffer[1] = new byte[48];
|
||||
buffer[2] = trak.tkhd.matrix;
|
||||
buffer[3] = new byte[8];
|
||||
buffer[4] = trak.mdia;
|
||||
|
||||
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
|
||||
set.putLong(time);
|
||||
set.putLong(time);
|
||||
set.putInt(trackId);
|
||||
set.position(24);
|
||||
set.putLong(trak.tkhd.duration);
|
||||
set.position(40);
|
||||
set.putShort(trak.tkhd.bLayer);
|
||||
set.putShort(trak.tkhd.bAlternateGroup);
|
||||
set.putShort(trak.tkhd.bVolume);
|
||||
|
||||
ByteBuffer.wrap(buffer[3])
|
||||
.putInt(trak.tkhd.bWidth)
|
||||
.putInt(trak.tkhd.bHeight);
|
||||
|
||||
return lengthFor(buffer);
|
||||
}
|
||||
|
||||
private byte[][] make_moov() throws RuntimeException {
|
||||
int pos = 1;
|
||||
byte[][] buffer = new byte[2 + (DIMENSIONAL_TWO * infoTracks.length) + (DIMENSIONAL_FIVE * infoTracks.length) + DIMENSIONAL_FIVE + 1][];
|
||||
|
||||
buffer[0] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x76
|
||||
};
|
||||
|
||||
for (byte[] buff : make_mvhd()) {
|
||||
buffer[pos++] = buff;
|
||||
}
|
||||
|
||||
for (int i = 0; i < infoTracks.length; i++) {
|
||||
for (byte[] buff : make_trak(i + 1, infoTracks[i].trak)) {
|
||||
buffer[pos++] = buff;
|
||||
}
|
||||
}
|
||||
|
||||
buffer[pos] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x76, 0x65, 0x78
|
||||
};
|
||||
|
||||
ByteBuffer.wrap(buffer[pos++]).putInt((infoTracks.length * DEFAULT_TREX_SIZE) + 8);
|
||||
|
||||
for (int i = 0; i < infoTracks.length; i++) {
|
||||
for (byte[] buff : make_trex(i + 1, infoTracks[i].trex)) {
|
||||
buffer[pos++] = buff;
|
||||
}
|
||||
}
|
||||
|
||||
// default udta
|
||||
buffer[pos] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x5C, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x54, 0x6D, 0x65, 0x74, 0x61,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00,
|
||||
0x1F, (byte) 0xA9, 0x63, 0x6D, 0x74, 0x00, 0x00, 0x00, 0x17, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00,
|
||||
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
|
||||
};
|
||||
|
||||
return lengthFor(buffer);
|
||||
}
|
||||
|
||||
private byte[][] make_trex(int trackId, Trex trex) {
|
||||
byte[][] buffer = new byte[][]{
|
||||
{
|
||||
0x00, 0x00, 0x00, 0x20, 0x74, 0x72, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00
|
||||
},
|
||||
new byte[20]
|
||||
};
|
||||
|
||||
ByteBuffer.wrap(buffer[1])
|
||||
.putInt(trackId)
|
||||
.putInt(trex.defaultSampleDescriptionIndex)
|
||||
.putInt(trex.defaultSampleDuration)
|
||||
.putInt(trex.defaultSampleSize)
|
||||
.putInt(trex.defaultSampleFlags);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private byte[][] make_tfra(int trackId, List<Integer> times, List<Long> moofOffsets) {
|
||||
int entryCount = times.size() - 1;
|
||||
byte[][] buffer = new byte[DIMENSIONAL_TWO][];
|
||||
buffer[0] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x74, 0x66, 0x72, 0x61, 0x01, 0x00, 0x00, 0x00
|
||||
};
|
||||
buffer[1] = new byte[12 + ((16 + TFRA_TTS_DEFAULT.length) * entryCount)];
|
||||
|
||||
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
|
||||
set.putInt(trackId);
|
||||
set.position(8);
|
||||
set.putInt(entryCount);
|
||||
|
||||
long decodeTime = 0;
|
||||
|
||||
for (int i = 0; i < entryCount; i++) {
|
||||
decodeTime += times.get(i);
|
||||
set.putLong(decodeTime);
|
||||
set.putLong(moofOffsets.get(i));
|
||||
set.put(TFRA_TTS_DEFAULT);// default values: traf number/trun number/sample number
|
||||
}
|
||||
|
||||
return lengthFor(buffer);
|
||||
}
|
||||
|
||||
private byte[][] make_mfra() {
|
||||
byte[][] buffer = new byte[2 + (DIMENSIONAL_TWO * infoTracks.length)][];
|
||||
buffer[0] = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x66, 0x72, 0x61
|
||||
};
|
||||
int pos = 1;
|
||||
|
||||
for (int i = 0; i < infoTracks.length; i++) {
|
||||
for (byte[] buff : make_tfra(i + 1, chunkTimes.get(i), moofOffsets)) {
|
||||
buffer[pos++] = buff;
|
||||
}
|
||||
}
|
||||
|
||||
buffer[pos] = new byte[]{// mfro
|
||||
0x00, 0x00, 0x00, 0x10, 0x6D, 0x66, 0x72, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
lengthFor(buffer);
|
||||
|
||||
ByteBuffer set = ByteBuffer.wrap(buffer[pos]);
|
||||
set.position(12);
|
||||
set.put(buffer[0], 0, 4);
|
||||
|
||||
return buffer;
|
||||
|
||||
}
|
||||
|
||||
private byte[][] make_sidx(int internalTrackId, long firstOffset) {
|
||||
List<Integer> times = chunkTimes.get(internalTrackId);
|
||||
int count = times.size() - 1;// the first item is ignored (composition time)
|
||||
|
||||
if (count > 65535) {
|
||||
throw new OutOfMemoryError("to many fragments. sidx limit is 65535, found " + String.valueOf(count));
|
||||
}
|
||||
|
||||
byte[][] buffer = new byte[][]{
|
||||
new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x73, 0x69, 0x64, 0x78, 0x01, 0x00, 0x00, 0x00
|
||||
},
|
||||
new byte[calcSidxBodySize(count)]
|
||||
};
|
||||
|
||||
lengthFor(buffer);
|
||||
|
||||
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
|
||||
set.putInt(internalTrackId + 1);
|
||||
set.putInt(infoTracks[internalTrackId].trak.mdia_mdhd_timeScale);
|
||||
set.putLong(0);
|
||||
set.putLong(firstOffset - ByteBuffer.wrap(buffer[0]).getInt());
|
||||
set.putInt(0xFFFF & count);// unsigned
|
||||
|
||||
int i = 0;
|
||||
while (i < count) {
|
||||
set.putInt(fragSizes.get(i) & 0x7fffffff);// default reference type is 0
|
||||
set.putInt(times.get(i + 1));
|
||||
set.putInt(0x90000000);// default SAP settings
|
||||
i++;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private byte[][] make_free(int totalSize) {
|
||||
return lengthFor(new byte[][]{
|
||||
new byte[]{0x00, 0x00, 0x00, 0x00, 0x66, 0x72, 0x65, 0x65},
|
||||
new byte[totalSize - 8]// this is waste of RAM
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
//</editor-fold>
|
||||
|
||||
class TrunExtra {
|
||||
|
||||
ByteBuffer byteBuffer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,810 @@
|
||||
package org.schabi.newpipe.streams;
|
||||
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Hdlr;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Mdia;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashChunk;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashSample;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track;
|
||||
import org.schabi.newpipe.streams.Mp4DashReader.TrunEntry;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class Mp4FromDashWriter {
|
||||
|
||||
private final static int EPOCH_OFFSET = 2082844800;
|
||||
private final static short DEFAULT_TIMESCALE = 1000;
|
||||
private final static byte SAMPLES_PER_CHUNK_INIT = 2;
|
||||
private final static byte SAMPLES_PER_CHUNK = 6;// ffmpeg uses 2, basic uses 1 (with 60fps uses 21 or 22). NewPipe will use 6
|
||||
private final static long THRESHOLD_FOR_CO64 = 0xFFFEFFFFL;// near 3.999 GiB
|
||||
private final static int THRESHOLD_MOOV_LENGTH = (256 * 1024) + (2048 * 1024); // 2.2 MiB enough for: 1080p 60fps 00h35m00s
|
||||
|
||||
private final long time;
|
||||
|
||||
private ByteBuffer auxBuffer;
|
||||
private SharpStream outStream;
|
||||
|
||||
private long lastWriteOffset = -1;
|
||||
private long writeOffset;
|
||||
|
||||
private boolean moovSimulation = true;
|
||||
|
||||
private boolean done = false;
|
||||
private boolean parsed = false;
|
||||
|
||||
private Mp4Track[] tracks;
|
||||
private SharpStream[] sourceTracks;
|
||||
|
||||
private Mp4DashReader[] readers;
|
||||
private Mp4DashChunk[] readersChunks;
|
||||
|
||||
private int overrideMainBrand = 0x00;
|
||||
|
||||
public Mp4FromDashWriter(SharpStream... sources) throws IOException {
|
||||
for (SharpStream src : sources) {
|
||||
if (!src.canRewind() && !src.canRead()) {
|
||||
throw new IOException("All sources must be readable and allow rewind");
|
||||
}
|
||||
}
|
||||
|
||||
sourceTracks = sources;
|
||||
readers = new Mp4DashReader[sourceTracks.length];
|
||||
readersChunks = new Mp4DashChunk[readers.length];
|
||||
time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET;
|
||||
}
|
||||
|
||||
public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException {
|
||||
if (!parsed) {
|
||||
throw new IllegalStateException("All sources must be parsed first");
|
||||
}
|
||||
|
||||
return readers[sourceIndex].getAvailableTracks();
|
||||
}
|
||||
|
||||
public void parseSources() throws IOException, IllegalStateException {
|
||||
if (done) {
|
||||
throw new IllegalStateException("already done");
|
||||
}
|
||||
if (parsed) {
|
||||
throw new IllegalStateException("already parsed");
|
||||
}
|
||||
|
||||
try {
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
readers[i] = new Mp4DashReader(sourceTracks[i]);
|
||||
readers[i].parse();
|
||||
}
|
||||
|
||||
} finally {
|
||||
parsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void selectTracks(int... trackIndex) throws IOException {
|
||||
if (done) {
|
||||
throw new IOException("already done");
|
||||
}
|
||||
if (tracks != null) {
|
||||
throw new IOException("tracks already selected");
|
||||
}
|
||||
|
||||
try {
|
||||
tracks = new Mp4Track[readers.length];
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
tracks[i] = readers[i].selectTrack(trackIndex[i]);
|
||||
}
|
||||
} finally {
|
||||
parsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void setMainBrand(int brandId) {
|
||||
overrideMainBrand = brandId;
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return done;
|
||||
}
|
||||
|
||||
public boolean isParsed() {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
done = true;
|
||||
parsed = true;
|
||||
|
||||
for (SharpStream src : sourceTracks) {
|
||||
src.close();
|
||||
}
|
||||
|
||||
tracks = null;
|
||||
sourceTracks = null;
|
||||
|
||||
readers = null;
|
||||
readersChunks = null;
|
||||
|
||||
auxBuffer = null;
|
||||
outStream = null;
|
||||
}
|
||||
|
||||
public void build(SharpStream output) throws IOException {
|
||||
if (done) {
|
||||
throw new RuntimeException("already done");
|
||||
}
|
||||
if (!output.canWrite()) {
|
||||
throw new IOException("the provided output is not writable");
|
||||
}
|
||||
|
||||
//
|
||||
// WARNING: the muxer requires at least 8 samples of every track
|
||||
// not allowed for very short tracks (less than 0.5 seconds)
|
||||
//
|
||||
outStream = output;
|
||||
int read = 8;// mdat box header size
|
||||
long totalSampleSize = 0;
|
||||
int[] sampleExtra = new int[readers.length];
|
||||
int[] defaultMediaTime = new int[readers.length];
|
||||
int[] defaultSampleDuration = new int[readers.length];
|
||||
int[] sampleCount = new int[readers.length];
|
||||
|
||||
TablesInfo[] tablesInfo = new TablesInfo[tracks.length];
|
||||
for (int i = 0; i < tablesInfo.length; i++) {
|
||||
tablesInfo[i] = new TablesInfo();
|
||||
}
|
||||
|
||||
//<editor-fold defaultstate="expanded" desc="calculate stbl sample tables size and required moov values">
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
int samplesSize = 0;
|
||||
int sampleSizeChanges = 0;
|
||||
int compositionOffsetLast = -1;
|
||||
|
||||
Mp4DashChunk chunk;
|
||||
while ((chunk = readers[i].getNextChunk(true)) != null) {
|
||||
|
||||
if (defaultMediaTime[i] < 1 && chunk.moof.traf.tfhd.defaultSampleDuration > 0) {
|
||||
defaultMediaTime[i] = chunk.moof.traf.tfhd.defaultSampleDuration;
|
||||
}
|
||||
|
||||
read += chunk.moof.traf.trun.chunkSize;
|
||||
sampleExtra[i] += chunk.moof.traf.trun.chunkDuration;// calculate track duration
|
||||
|
||||
TrunEntry info;
|
||||
while ((info = chunk.getNextSampleInfo()) != null) {
|
||||
if (info.isKeyframe) {
|
||||
tablesInfo[i].stss++;
|
||||
}
|
||||
|
||||
if (info.sampleDuration > defaultSampleDuration[i]) {
|
||||
defaultSampleDuration[i] = info.sampleDuration;
|
||||
}
|
||||
|
||||
tablesInfo[i].stsz++;
|
||||
if (samplesSize != info.sampleSize) {
|
||||
samplesSize = info.sampleSize;
|
||||
sampleSizeChanges++;
|
||||
}
|
||||
|
||||
if (info.hasCompositionTimeOffset) {
|
||||
if (info.sampleCompositionTimeOffset != compositionOffsetLast) {
|
||||
tablesInfo[i].ctts++;
|
||||
compositionOffsetLast = info.sampleCompositionTimeOffset;
|
||||
}
|
||||
}
|
||||
|
||||
totalSampleSize += info.sampleSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultMediaTime[i] < 1) {
|
||||
defaultMediaTime[i] = defaultSampleDuration[i];
|
||||
}
|
||||
|
||||
readers[i].rewind();
|
||||
|
||||
int tmp = tablesInfo[i].stsz - SAMPLES_PER_CHUNK_INIT;
|
||||
tablesInfo[i].stco = (tmp / SAMPLES_PER_CHUNK) + 1;// +1 for samples in first chunk
|
||||
|
||||
tmp = tmp % SAMPLES_PER_CHUNK;
|
||||
if (tmp == 0) {
|
||||
tablesInfo[i].stsc = 2;// first chunk (init) and succesive chunks
|
||||
tablesInfo[i].stsc_bEntries = new int[]{
|
||||
1, SAMPLES_PER_CHUNK_INIT, 1,
|
||||
2, SAMPLES_PER_CHUNK, 1
|
||||
};
|
||||
} else {
|
||||
tablesInfo[i].stsc = 3;// first chunk (init) and succesive chunks and remain chunk
|
||||
tablesInfo[i].stsc_bEntries = new int[]{
|
||||
1, SAMPLES_PER_CHUNK_INIT, 1,
|
||||
2, SAMPLES_PER_CHUNK, 1,
|
||||
tablesInfo[i].stco + 1, tmp, 1
|
||||
};
|
||||
tablesInfo[i].stco++;
|
||||
}
|
||||
|
||||
sampleCount[i] = tablesInfo[i].stsz;
|
||||
|
||||
if (sampleSizeChanges == 1) {
|
||||
tablesInfo[i].stsz = 0;
|
||||
tablesInfo[i].stsz_default = samplesSize;
|
||||
} else {
|
||||
tablesInfo[i].stsz_default = 0;
|
||||
}
|
||||
|
||||
if (tablesInfo[i].stss == tablesInfo[i].stsz) {
|
||||
tablesInfo[i].stss = -1;// for audio tracks (all samples are keyframes)
|
||||
}
|
||||
|
||||
// ensure track duration
|
||||
if (tracks[i].trak.tkhd.duration < 1) {
|
||||
tracks[i].trak.tkhd.duration = sampleExtra[i];// this never should happen
|
||||
}
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
boolean is64 = read > THRESHOLD_FOR_CO64;
|
||||
|
||||
// calculate the moov size;
|
||||
int auxSize = make_moov(defaultMediaTime, tablesInfo, is64);
|
||||
|
||||
if (auxSize < THRESHOLD_MOOV_LENGTH) {
|
||||
auxBuffer = ByteBuffer.allocate(auxSize);// cache moov in the memory
|
||||
}
|
||||
|
||||
moovSimulation = false;
|
||||
writeOffset = 0;
|
||||
|
||||
final int ftyp_size = make_ftyp();
|
||||
|
||||
// reserve moov space in the output stream
|
||||
/*if (outStream.canSetLength()) {
|
||||
long length = writeOffset + auxSize;
|
||||
outStream.setLength(length);
|
||||
outSeek(length);
|
||||
} else {*/
|
||||
if (auxSize > 0) {
|
||||
int length = auxSize;
|
||||
byte[] buffer = new byte[8 * 1024];// 8 KiB
|
||||
while (length > 0) {
|
||||
int count = Math.min(length, buffer.length);
|
||||
outWrite(buffer, 0, count);
|
||||
length -= count;
|
||||
}
|
||||
}
|
||||
|
||||
if (auxBuffer == null) {
|
||||
outSeek(ftyp_size);
|
||||
}
|
||||
|
||||
// tablesInfo contais row counts
|
||||
// and after returning from make_moov() will contain table offsets
|
||||
make_moov(defaultMediaTime, tablesInfo, is64);
|
||||
|
||||
// write tables: stts stsc
|
||||
// reset for ctts table: sampleCount sampleExtra
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
writeEntryArray(tablesInfo[i].stts, 2, sampleCount[i], defaultSampleDuration[i]);
|
||||
writeEntryArray(tablesInfo[i].stsc, tablesInfo[i].stsc_bEntries.length, tablesInfo[i].stsc_bEntries);
|
||||
tablesInfo[i].stsc_bEntries = null;
|
||||
if (tablesInfo[i].ctts > 0) {
|
||||
sampleCount[i] = 1;// index is not base zero
|
||||
sampleExtra[i] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (auxBuffer == null) {
|
||||
outRestore();
|
||||
}
|
||||
|
||||
outWrite(make_mdat(totalSampleSize, is64));
|
||||
|
||||
int[] sampleIndex = new int[readers.length];
|
||||
int[] sizes = new int[SAMPLES_PER_CHUNK];
|
||||
int[] sync = new int[SAMPLES_PER_CHUNK];
|
||||
|
||||
int written = readers.length;
|
||||
while (written > 0) {
|
||||
written = 0;
|
||||
|
||||
for (int i = 0; i < readers.length; i++) {
|
||||
if (sampleIndex[i] < 0) {
|
||||
continue;// track is done
|
||||
}
|
||||
|
||||
long chunkOffset = writeOffset;
|
||||
int syncCount = 0;
|
||||
int limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK;
|
||||
|
||||
int j = 0;
|
||||
for (; j < limit; j++) {
|
||||
Mp4DashSample sample = getNextSample(i);
|
||||
|
||||
if (sample == null) {
|
||||
if (tablesInfo[i].ctts > 0 && sampleExtra[i] >= 0) {
|
||||
writeEntryArray(tablesInfo[i].ctts, 1, sampleCount[i], sampleExtra[i]);// flush last entries
|
||||
}
|
||||
sampleIndex[i] = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
sampleIndex[i]++;
|
||||
|
||||
if (tablesInfo[i].ctts > 0) {
|
||||
if (sample.info.sampleCompositionTimeOffset == sampleExtra[i]) {
|
||||
sampleCount[i]++;
|
||||
} else {
|
||||
if (sampleExtra[i] >= 0) {
|
||||
tablesInfo[i].ctts = writeEntryArray(tablesInfo[i].ctts, 2, sampleCount[i], sampleExtra[i]);
|
||||
outRestore();
|
||||
}
|
||||
sampleCount[i] = 1;
|
||||
sampleExtra[i] = sample.info.sampleCompositionTimeOffset;
|
||||
}
|
||||
}
|
||||
|
||||
if (tablesInfo[i].stss > 0 && sample.info.isKeyframe) {
|
||||
sync[syncCount++] = sampleIndex[i];
|
||||
}
|
||||
|
||||
if (tablesInfo[i].stsz > 0) {
|
||||
sizes[j] = sample.data.length;
|
||||
}
|
||||
|
||||
outWrite(sample.data, 0, sample.data.length);
|
||||
}
|
||||
|
||||
if (j > 0) {
|
||||
written++;
|
||||
|
||||
if (tablesInfo[i].stsz > 0) {
|
||||
tablesInfo[i].stsz = writeEntryArray(tablesInfo[i].stsz, j, sizes);
|
||||
}
|
||||
|
||||
if (syncCount > 0) {
|
||||
tablesInfo[i].stss = writeEntryArray(tablesInfo[i].stss, syncCount, sync);
|
||||
}
|
||||
|
||||
if (is64) {
|
||||
tablesInfo[i].stco = writeEntry64(tablesInfo[i].stco, chunkOffset);
|
||||
} else {
|
||||
tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, (int) chunkOffset);
|
||||
}
|
||||
|
||||
outRestore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (auxBuffer != null) {
|
||||
// dump moov
|
||||
outSeek(ftyp_size);
|
||||
outStream.write(auxBuffer.array(), 0, auxBuffer.capacity());
|
||||
auxBuffer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private Mp4DashSample getNextSample(int track) throws IOException {
|
||||
if (readersChunks[track] == null) {
|
||||
readersChunks[track] = readers[track].getNextChunk(false);
|
||||
if (readersChunks[track] == null) {
|
||||
return null;// EOF reached
|
||||
}
|
||||
}
|
||||
|
||||
Mp4DashSample sample = readersChunks[track].getNextSample();
|
||||
if (sample == null) {
|
||||
readersChunks[track] = null;
|
||||
return getNextSample(track);
|
||||
} else {
|
||||
return sample;
|
||||
}
|
||||
}
|
||||
|
||||
// <editor-fold defaultstate="expanded" desc="Stbl handling">
|
||||
private int writeEntry64(int offset, long value) throws IOException {
|
||||
outBackup();
|
||||
|
||||
auxSeek(offset);
|
||||
auxWrite(ByteBuffer.allocate(8).putLong(value).array());
|
||||
|
||||
return offset + 8;
|
||||
}
|
||||
|
||||
private int writeEntryArray(int offset, int count, int... values) throws IOException {
|
||||
outBackup();
|
||||
|
||||
auxSeek(offset);
|
||||
|
||||
int size = count * 4;
|
||||
ByteBuffer buffer = ByteBuffer.allocate(size);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
buffer.putInt(values[i]);
|
||||
}
|
||||
|
||||
auxWrite(buffer.array());
|
||||
|
||||
return offset + size;
|
||||
}
|
||||
|
||||
private void outBackup() {
|
||||
if (auxBuffer == null && lastWriteOffset < 0) {
|
||||
lastWriteOffset = writeOffset;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore to the previous position before the first call to writeEntry64()
|
||||
* or writeEntryArray() methods.
|
||||
*/
|
||||
private void outRestore() throws IOException {
|
||||
if (lastWriteOffset > 0) {
|
||||
outSeek(lastWriteOffset);
|
||||
lastWriteOffset = -1;
|
||||
}
|
||||
}
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultstate="expanded" desc="Utils">
|
||||
private void outWrite(byte[] buffer) throws IOException {
|
||||
outWrite(buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
private void outWrite(byte[] buffer, int offset, int count) throws IOException {
|
||||
writeOffset += count;
|
||||
outStream.write(buffer, offset, count);
|
||||
}
|
||||
|
||||
private void outSeek(long offset) throws IOException {
|
||||
if (outStream.canSeek()) {
|
||||
outStream.seek(offset);
|
||||
writeOffset = offset;
|
||||
} else if (outStream.canRewind()) {
|
||||
outStream.rewind();
|
||||
writeOffset = 0;
|
||||
outSkip(offset);
|
||||
} else {
|
||||
throw new IOException("cannot seek or rewind the output stream");
|
||||
}
|
||||
}
|
||||
|
||||
private void outSkip(long amount) throws IOException {
|
||||
outStream.skip(amount);
|
||||
writeOffset += amount;
|
||||
}
|
||||
|
||||
private int lengthFor(int offset) throws IOException {
|
||||
int size = auxOffset() - offset;
|
||||
|
||||
if (moovSimulation) {
|
||||
return size;
|
||||
}
|
||||
|
||||
auxSeek(offset);
|
||||
auxWrite(size);
|
||||
auxSkip(size - 4);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
private int make(int type, int extra, int columns, int rows) throws IOException {
|
||||
final byte base = 16;
|
||||
int size = columns * rows * 4;
|
||||
int total = size + base;
|
||||
int offset = auxOffset();
|
||||
|
||||
if (extra >= 0) {
|
||||
total += 4;
|
||||
}
|
||||
|
||||
auxWrite(ByteBuffer.allocate(12)
|
||||
.putInt(total)
|
||||
.putInt(type)
|
||||
.putInt(0x00)// default version & flags
|
||||
.array()
|
||||
);
|
||||
|
||||
if (extra >= 0) {
|
||||
//size += 4;// commented for auxiliar buffer !!!
|
||||
offset += 4;
|
||||
auxWrite(extra);
|
||||
}
|
||||
|
||||
auxWrite(rows);
|
||||
auxSkip(size);
|
||||
|
||||
return offset + base;
|
||||
}
|
||||
|
||||
private void auxWrite(int value) throws IOException {
|
||||
auxWrite(ByteBuffer.allocate(4)
|
||||
.putInt(value)
|
||||
.array()
|
||||
);
|
||||
}
|
||||
|
||||
private void auxWrite(byte[] buffer) throws IOException {
|
||||
if (moovSimulation) {
|
||||
writeOffset += buffer.length;
|
||||
} else if (auxBuffer == null) {
|
||||
outWrite(buffer, 0, buffer.length);
|
||||
} else {
|
||||
auxBuffer.put(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private void auxSeek(int offset) throws IOException {
|
||||
if (moovSimulation) {
|
||||
writeOffset = offset;
|
||||
} else if (auxBuffer == null) {
|
||||
outSeek(offset);
|
||||
} else {
|
||||
auxBuffer.position(offset);
|
||||
}
|
||||
}
|
||||
|
||||
private void auxSkip(int amount) throws IOException {
|
||||
if (moovSimulation) {
|
||||
writeOffset += amount;
|
||||
} else if (auxBuffer == null) {
|
||||
outSkip(amount);
|
||||
} else {
|
||||
auxBuffer.position(auxBuffer.position() + amount);
|
||||
}
|
||||
}
|
||||
|
||||
private int auxOffset() {
|
||||
return auxBuffer == null ? (int) writeOffset : auxBuffer.position();
|
||||
}
|
||||
// </editor-fold>
|
||||
|
||||
// <editor-fold defaultstate="expanded" desc="Box makers">
|
||||
private int make_ftyp() throws IOException {
|
||||
byte[] buffer = new byte[]{
|
||||
0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70,// ftyp
|
||||
0x6D, 0x70, 0x34, 0x32,// mayor brand (mp42)
|
||||
0x00, 0x00, 0x02, 0x00,// default minor version (512)
|
||||
0x6D, 0x70, 0x34, 0x31, 0x69, 0x73, 0x6F, 0x6D, 0x69, 0x73, 0x6F, 0x32// compatible brands: mp41 isom iso2
|
||||
};
|
||||
|
||||
if (overrideMainBrand != 0)
|
||||
ByteBuffer.wrap(buffer).putInt(8, overrideMainBrand);
|
||||
|
||||
outWrite(buffer);
|
||||
|
||||
return buffer.length;
|
||||
}
|
||||
|
||||
private byte[] make_mdat(long refSize, boolean is64) {
|
||||
if (is64) {
|
||||
refSize += 16;
|
||||
} else {
|
||||
refSize += 8;
|
||||
}
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(is64 ? 16 : 8)
|
||||
.putInt(is64 ? 0x01 : (int) refSize)
|
||||
.putInt(0x6D646174);// mdat
|
||||
|
||||
if (is64) {
|
||||
buffer.putLong(refSize);
|
||||
}
|
||||
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
private void make_mvhd(long longestTrack) throws IOException {
|
||||
auxWrite(new byte[]{
|
||||
0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 0x00
|
||||
});
|
||||
auxWrite(ByteBuffer.allocate(28)
|
||||
.putLong(time)
|
||||
.putLong(time)
|
||||
.putInt(DEFAULT_TIMESCALE)
|
||||
.putLong(longestTrack)
|
||||
.array()
|
||||
);
|
||||
|
||||
auxWrite(new byte[]{
|
||||
0x00, 0x01, 0x00, 0x00, 0x01, 0x00,// default volume and rate
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,// reserved values
|
||||
// default matrix
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x40, 0x00, 0x00, 0x00
|
||||
});
|
||||
auxWrite(new byte[24]);// predefined
|
||||
auxWrite(ByteBuffer.allocate(4)
|
||||
.putInt(tracks.length + 1)
|
||||
.array()
|
||||
);
|
||||
}
|
||||
|
||||
private int make_moov(int[] defaultMediaTime, TablesInfo[] tablesInfo, boolean is64) throws RuntimeException, IOException {
|
||||
int start = auxOffset();
|
||||
|
||||
auxWrite(new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x76
|
||||
});
|
||||
|
||||
long longestTrack = 0;
|
||||
long[] durations = new long[tracks.length];
|
||||
|
||||
for (int i = 0; i < durations.length; i++) {
|
||||
durations[i] = (long) Math.ceil(
|
||||
((double) tracks[i].trak.tkhd.duration / tracks[i].trak.mdia.mdhd_timeScale) * DEFAULT_TIMESCALE
|
||||
);
|
||||
|
||||
if (durations[i] > longestTrack) {
|
||||
longestTrack = durations[i];
|
||||
}
|
||||
}
|
||||
|
||||
make_mvhd(longestTrack);
|
||||
|
||||
for (int i = 0; i < tracks.length; i++) {
|
||||
if (tracks[i].trak.tkhd.matrix.length != 36) {
|
||||
throw new RuntimeException("bad track matrix length (expected 36) in track n°" + i);
|
||||
}
|
||||
make_trak(i, durations[i], defaultMediaTime[i], tablesInfo[i], is64);
|
||||
}
|
||||
|
||||
// udta/meta/ilst/©too
|
||||
auxWrite(new byte[]{
|
||||
0x00, 0x00, 0x00, 0x5C, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x54, 0x6D, 0x65, 0x74, 0x61,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00,
|
||||
0x1F, (byte) 0xA9, 0x74, 0x6F, 0x6F, 0x00, 0x00, 0x00, 0x17, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00,
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
|
||||
});
|
||||
|
||||
return lengthFor(start);
|
||||
}
|
||||
|
||||
private void make_trak(int index, long duration, int defaultMediaTime, TablesInfo tables, boolean is64) throws IOException {
|
||||
int start = auxOffset();
|
||||
|
||||
auxWrite(new byte[]{
|
||||
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B,// trak header
|
||||
0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 // tkhd header
|
||||
});
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(48);
|
||||
buffer.putLong(time);
|
||||
buffer.putLong(time);
|
||||
buffer.putInt(index + 1);
|
||||
buffer.position(24);
|
||||
buffer.putLong(duration);
|
||||
buffer.position(40);
|
||||
buffer.putShort(tracks[index].trak.tkhd.bLayer);
|
||||
buffer.putShort(tracks[index].trak.tkhd.bAlternateGroup);
|
||||
buffer.putShort(tracks[index].trak.tkhd.bVolume);
|
||||
auxWrite(buffer.array());
|
||||
|
||||
auxWrite(tracks[index].trak.tkhd.matrix);
|
||||
auxWrite(ByteBuffer.allocate(8)
|
||||
.putInt(tracks[index].trak.tkhd.bWidth)
|
||||
.putInt(tracks[index].trak.tkhd.bHeight)
|
||||
.array()
|
||||
);
|
||||
|
||||
auxWrite(new byte[]{
|
||||
0x00, 0x00, 0x00, 0x24, 0x65, 0x64, 0x74, 0x73,// edts header
|
||||
0x00, 0x00, 0x00, 0x1C, 0x65, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01// elst header
|
||||
});
|
||||
|
||||
int bMediaRate;
|
||||
int mediaTime;
|
||||
|
||||
if (tracks[index].trak.edst_elst == null) {
|
||||
// is a audio track ¿is edst/elst opcional for audio tracks?
|
||||
mediaTime = 0x00;// ffmpeg set this value as zero, instead of defaultMediaTime
|
||||
bMediaRate = 0x00010000;
|
||||
} else {
|
||||
mediaTime = (int) tracks[index].trak.edst_elst.MediaTime;
|
||||
bMediaRate = tracks[index].trak.edst_elst.bMediaRate;
|
||||
}
|
||||
|
||||
auxWrite(ByteBuffer
|
||||
.allocate(12)
|
||||
.putInt((int) duration)
|
||||
.putInt(mediaTime)
|
||||
.putInt(bMediaRate)
|
||||
.array()
|
||||
);
|
||||
|
||||
make_mdia(tracks[index].trak.mdia, tables, is64);
|
||||
|
||||
lengthFor(start);
|
||||
}
|
||||
|
||||
private void make_mdia(Mdia mdia, TablesInfo tablesInfo, boolean is64) throws IOException {
|
||||
|
||||
int start_mdia = auxOffset();
|
||||
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x61});// mdia
|
||||
auxWrite(mdia.mdhd);
|
||||
auxWrite(make_hdlr(mdia.hdlr));
|
||||
|
||||
int start_minf = auxOffset();
|
||||
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x69, 0x6E, 0x66});// minf
|
||||
auxWrite(mdia.minf.$mhd);
|
||||
auxWrite(mdia.minf.dinf);
|
||||
|
||||
int start_stbl = auxOffset();
|
||||
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x62, 0x6C});// stbl
|
||||
auxWrite(mdia.minf.stbl_stsd);
|
||||
|
||||
//
|
||||
// In audio tracks the following tables is not required: ssts ctts
|
||||
// And stsz can be empty if has a default sample size
|
||||
//
|
||||
if (moovSimulation) {
|
||||
make(0x73747473, -1, 2, 1);
|
||||
if (tablesInfo.stss > 0) {
|
||||
make(0x73747373, -1, 1, tablesInfo.stss);
|
||||
}
|
||||
if (tablesInfo.ctts > 0) {
|
||||
make(0x63747473, -1, 2, tablesInfo.ctts);
|
||||
}
|
||||
make(0x73747363, -1, 3, tablesInfo.stsc);
|
||||
make(0x7374737A, tablesInfo.stsz_default, 1, tablesInfo.stsz);
|
||||
make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco);
|
||||
} else {
|
||||
tablesInfo.stts = make(0x73747473, -1, 2, 1);
|
||||
if (tablesInfo.stss > 0) {
|
||||
tablesInfo.stss = make(0x73747373, -1, 1, tablesInfo.stss);
|
||||
}
|
||||
if (tablesInfo.ctts > 0) {
|
||||
tablesInfo.ctts = make(0x63747473, -1, 2, tablesInfo.ctts);
|
||||
}
|
||||
tablesInfo.stsc = make(0x73747363, -1, 3, tablesInfo.stsc);
|
||||
tablesInfo.stsz = make(0x7374737A, tablesInfo.stsz_default, 1, tablesInfo.stsz);
|
||||
tablesInfo.stco = make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco);
|
||||
}
|
||||
|
||||
lengthFor(start_stbl);
|
||||
lengthFor(start_minf);
|
||||
lengthFor(start_mdia);
|
||||
}
|
||||
|
||||
private byte[] make_hdlr(Hdlr hdlr) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(new byte[]{
|
||||
0x00, 0x00, 0x00, 0x77, 0x68, 0x64, 0x6C, 0x72,// hdlr
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// binary string "ISO Media file created in NewPipe (A libre lightweight streaming frontend for Android)."
|
||||
0x49, 0x53, 0x4F, 0x20, 0x4D, 0x65, 0x64, 0x69, 0x61, 0x20, 0x66, 0x69, 0x6C, 0x65, 0x20, 0x63,
|
||||
0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x20, 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70,
|
||||
0x65, 0x20, 0x28, 0x41, 0x20, 0x6C, 0x69, 0x62, 0x72, 0x65, 0x20, 0x6C, 0x69, 0x67, 0x68, 0x74,
|
||||
0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x20, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, 0x69, 0x6E, 0x67,
|
||||
0x20, 0x66, 0x72, 0x6F, 0x6E, 0x74, 0x65, 0x6E, 0x64, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x41, 0x6E,
|
||||
0x64, 0x72, 0x6F, 0x69, 0x64, 0x29, 0x2E
|
||||
});
|
||||
|
||||
buffer.position(12);
|
||||
buffer.putInt(hdlr.type);
|
||||
buffer.putInt(hdlr.subType);
|
||||
buffer.put(hdlr.bReserved);// always is a zero array
|
||||
|
||||
return buffer.array();
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
class TablesInfo {
|
||||
|
||||
public int stts;
|
||||
public int stsc;
|
||||
public int[] stsc_bEntries;
|
||||
public int ctts;
|
||||
public int stsz;
|
||||
public int stsz_default;
|
||||
public int stss;
|
||||
public int stco;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.schabi.newpipe.streams;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
@@ -12,8 +13,6 @@ import java.nio.charset.Charset;
|
||||
import java.text.ParseException;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
@@ -27,11 +26,11 @@ public class SubtitleConverter {
|
||||
|
||||
public void dumpTTML(SharpStream in, final SharpStream out, final boolean ignoreEmptyFrames, final boolean detectYoutubeDuplicateLines
|
||||
) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException {
|
||||
|
||||
|
||||
final FrameWriter callback = new FrameWriter() {
|
||||
int frameIndex = 0;
|
||||
final Charset charset = Charset.forName("utf-8");
|
||||
|
||||
|
||||
@Override
|
||||
public void yield(SubtitleFrame frame) throws IOException {
|
||||
if (ignoreEmptyFrames && frame.isEmptyText()) {
|
||||
@@ -48,13 +47,13 @@ public class SubtitleConverter {
|
||||
out.write(NEW_LINE.getBytes(charset));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
read_xml_based(in, callback, detectYoutubeDuplicateLines,
|
||||
"tt", "xmlns", "http://www.w3.org/ns/ttml",
|
||||
new String[]{"timedtext", "head", "wp"},
|
||||
new String[]{"body", "div", "p"},
|
||||
"begin", "end", true
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
private void read_xml_based(SharpStream source, FrameWriter callback, boolean detectYoutubeDuplicateLines,
|
||||
@@ -70,7 +69,7 @@ public class SubtitleConverter {
|
||||
* Language parsing is not supported
|
||||
*/
|
||||
|
||||
byte[] buffer = new byte[source.available()];
|
||||
byte[] buffer = new byte[(int) source.available()];
|
||||
source.read(buffer);
|
||||
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
@@ -206,7 +205,7 @@ public class SubtitleConverter {
|
||||
}
|
||||
}
|
||||
|
||||
private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) throws XPathExpressionException {
|
||||
private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) {
|
||||
Element ref = xml.getDocumentElement();
|
||||
|
||||
for (int i = 0; i < path.length - 1; i++) {
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
package org.schabi.newpipe.streams;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class TrackDataChunk extends InputStream {
|
||||
|
||||
private final DataReader base;
|
||||
private int size;
|
||||
|
||||
public TrackDataChunk(DataReader base, int size) {
|
||||
this.base = base;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (size < 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int res = base.read();
|
||||
|
||||
if (res >= 0) {
|
||||
size--;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer) throws IOException {
|
||||
return read(buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int offset, int count) throws IOException {
|
||||
count = Math.min(size, count);
|
||||
int read = base.read(buffer, offset, count);
|
||||
size -= count;
|
||||
return read;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long amount) throws IOException {
|
||||
long res = base.skipBytes(Math.min(amount, size));
|
||||
size -= res;
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
size = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
package org.schabi.newpipe.streams;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -121,7 +122,7 @@ public class WebMReader {
|
||||
}
|
||||
|
||||
private String readString(Element parent) throws IOException {
|
||||
return new String(readBlob(parent), "utf-8");
|
||||
return new String(readBlob(parent), StandardCharsets.UTF_8);// or use "utf-8"
|
||||
}
|
||||
|
||||
private byte[] readBlob(Element parent) throws IOException {
|
||||
@@ -193,6 +194,7 @@ public class WebMReader {
|
||||
return elem;
|
||||
}
|
||||
}
|
||||
|
||||
ensure(elem);
|
||||
}
|
||||
|
||||
@@ -306,7 +308,7 @@ public class WebMReader {
|
||||
entry.trackNumber = readNumber(elem);
|
||||
break;
|
||||
case ID_TrackType:
|
||||
entry.trackType = (int)readNumber(elem);
|
||||
entry.trackType = (int) readNumber(elem);
|
||||
break;
|
||||
case ID_CodecID:
|
||||
entry.codecId = readString(elem);
|
||||
@@ -445,7 +447,7 @@ public class WebMReader {
|
||||
|
||||
public class SimpleBlock {
|
||||
|
||||
public TrackDataChunk data;
|
||||
public InputStream data;
|
||||
|
||||
SimpleBlock(Element ref) {
|
||||
this.ref = ref;
|
||||
@@ -492,7 +494,7 @@ public class WebMReader {
|
||||
|
||||
currentSimpleBlock = readSimpleBlock(elem);
|
||||
if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) {
|
||||
currentSimpleBlock.data = new TrackDataChunk(stream, (int) currentSimpleBlock.dataSize);
|
||||
currentSimpleBlock.data = stream.getView((int) currentSimpleBlock.dataSize);
|
||||
return currentSimpleBlock;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
package org.schabi.newpipe.streams;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.streams.WebMReader.Cluster;
|
||||
import org.schabi.newpipe.streams.WebMReader.Segment;
|
||||
import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
|
||||
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author kapodamy
|
||||
*/
|
||||
public class WebMWriter {
|
||||
@@ -94,10 +94,6 @@ public class WebMWriter {
|
||||
}
|
||||
}
|
||||
|
||||
public long getBytesWritten() {
|
||||
return written;
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return done;
|
||||
}
|
||||
@@ -111,7 +107,7 @@ public class WebMWriter {
|
||||
parsed = true;
|
||||
|
||||
for (SharpStream src : sourceTracks) {
|
||||
src.dispose();
|
||||
src.close();
|
||||
}
|
||||
|
||||
sourceTracks = null;
|
||||
@@ -138,42 +134,42 @@ public class WebMWriter {
|
||||
|
||||
/* segment */
|
||||
listBuffer.add(new byte[]{
|
||||
0x18, 0x53, (byte) 0x80, 0x67, 0x01,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// segment content size
|
||||
0x18, 0x53, (byte) 0x80, 0x67, 0x01,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// segment content size
|
||||
});
|
||||
|
||||
long baseSegmentOffset = written + listBuffer.get(0).length;
|
||||
|
||||
/* seek head */
|
||||
listBuffer.add(new byte[]{
|
||||
0x11, 0x4d, (byte) 0x9b, 0x74, (byte) 0xbe,
|
||||
0x4d, (byte) 0xbb, (byte) 0x8b,
|
||||
0x53, (byte) 0xab, (byte) 0x84, 0x15, 0x49, (byte) 0xa9, 0x66, 0x53,
|
||||
(byte) 0xac, (byte) 0x81, /*info offset*/ 0x43,
|
||||
0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab,
|
||||
(byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81,
|
||||
/*tracks offset*/ 0x6a,
|
||||
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1f,
|
||||
0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84, /*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00,
|
||||
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1c, 0x53,
|
||||
(byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84, /*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00
|
||||
0x11, 0x4d, (byte) 0x9b, 0x74, (byte) 0xbe,
|
||||
0x4d, (byte) 0xbb, (byte) 0x8b,
|
||||
0x53, (byte) 0xab, (byte) 0x84, 0x15, 0x49, (byte) 0xa9, 0x66, 0x53,
|
||||
(byte) 0xac, (byte) 0x81, /*info offset*/ 0x43,
|
||||
0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab,
|
||||
(byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81,
|
||||
/*tracks offset*/ 0x6a,
|
||||
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1f,
|
||||
0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84, /*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00,
|
||||
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1c, 0x53,
|
||||
(byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84, /*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00
|
||||
});
|
||||
|
||||
/* info */
|
||||
listBuffer.add(new byte[]{
|
||||
0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0xa2, 0x2a, (byte) 0xd7, (byte) 0xb1
|
||||
0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0xa2, 0x2a, (byte) 0xd7, (byte) 0xb1
|
||||
});
|
||||
listBuffer.add(encode(DEFAULT_TIMECODE_SCALE, true));// this value MUST NOT exceed 4 bytes
|
||||
listBuffer.add(new byte[]{0x44, (byte) 0x89, (byte) 0x84,
|
||||
0x00, 0x00, 0x00, 0x00,// info.duration
|
||||
|
||||
/* MuxingApp */
|
||||
0x4d, (byte) 0x80, (byte) 0x87, 0x4E,
|
||||
0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string
|
||||
|
||||
/* WritingApp */
|
||||
0x57, 0x41, (byte) 0x87, 0x4E,
|
||||
0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
|
||||
0x00, 0x00, 0x00, 0x00,// info.duration
|
||||
|
||||
/* MuxingApp */
|
||||
0x4d, (byte) 0x80, (byte) 0x87, 0x4E,
|
||||
0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string
|
||||
|
||||
/* WritingApp */
|
||||
0x57, 0x41, (byte) 0x87, 0x4E,
|
||||
0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
|
||||
});
|
||||
|
||||
/* tracks */
|
||||
@@ -200,7 +196,6 @@ public class WebMWriter {
|
||||
long nextCueTime = infoTracks[cuesForTrackId].trackType == 1 ? -1 : 0;
|
||||
ArrayList<KeyFrame> keyFrames = new ArrayList<>(32);
|
||||
|
||||
//ArrayList<Block> chunks = new ArrayList<>(readers.length);
|
||||
ArrayList<Long> clusterOffsets = new ArrayList<>(32);
|
||||
ArrayList<Integer> clusterSizes = new ArrayList<>(32);
|
||||
|
||||
@@ -283,24 +278,21 @@ public class WebMWriter {
|
||||
|
||||
long segmentSize = written - offsetSegmentSizeSet - 7;
|
||||
|
||||
// final step write offsets and sizes
|
||||
out.rewind();
|
||||
written = 0;
|
||||
|
||||
skipTo(out, offsetSegmentSizeSet);
|
||||
/* ---- final step write offsets and sizes ---- */
|
||||
seekTo(out, offsetSegmentSizeSet);
|
||||
writeLong(out, segmentSize);
|
||||
|
||||
if (predefinedDurations[durationFromTrackId] > -1) {
|
||||
duration += predefinedDurations[durationFromTrackId];// this value is full-filled in makeTrackEntry() method
|
||||
}
|
||||
skipTo(out, offsetInfoDurationSet);
|
||||
seekTo(out, offsetInfoDurationSet);
|
||||
writeFloat(out, duration);
|
||||
|
||||
firstClusterOffset -= baseSegmentOffset;
|
||||
skipTo(out, offsetClusterSet);
|
||||
seekTo(out, offsetClusterSet);
|
||||
writeInt(out, firstClusterOffset);
|
||||
|
||||
skipTo(out, cueReservedOffset);
|
||||
seekTo(out, cueReservedOffset);
|
||||
|
||||
/* Cue */
|
||||
dump(new byte[]{0x1c, 0x53, (byte) 0xbb, 0x6b, 0x20, 0x00, 0x00}, out);
|
||||
@@ -321,20 +313,16 @@ public class WebMWriter {
|
||||
voidBuffer.putShort((short) (firstClusterOffset - written - 4));
|
||||
dump(voidBuffer.array(), out);
|
||||
|
||||
out.rewind();
|
||||
written = 0;
|
||||
|
||||
skipTo(out, offsetCuesSet);
|
||||
seekTo(out, offsetCuesSet);
|
||||
writeInt(out, (int) (cueReservedOffset - baseSegmentOffset));
|
||||
|
||||
skipTo(out, cueReservedOffset + 5);
|
||||
seekTo(out, cueReservedOffset + 5);
|
||||
writeShort(out, cueSize);
|
||||
|
||||
for (int i = 0; i < clusterSizes.size(); i++) {
|
||||
skipTo(out, clusterOffsets.get(i));
|
||||
byte[] size = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x200000).array();
|
||||
out.write(size, 1, 3);
|
||||
written += 3;
|
||||
seekTo(out, clusterOffsets.get(i));
|
||||
byte[] buffer = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x10000000).array();
|
||||
dump(buffer, out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,20 +353,29 @@ public class WebMWriter {
|
||||
bloq.dataSize = (int) res.dataSize;
|
||||
bloq.trackNumber = internalTrackId;
|
||||
bloq.flags = res.flags;
|
||||
bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale, DEFAULT_TIMECODE_SCALE);
|
||||
bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale);
|
||||
bloq.absoluteTimecode += readersCluter[internalTrackId].timecode;
|
||||
|
||||
return bloq;
|
||||
}
|
||||
|
||||
private short convertTimecode(int time, long oldTimeScale, int newTimeScale) {
|
||||
return (short) (time * (newTimeScale / oldTimeScale));
|
||||
private short convertTimecode(int time, long oldTimeScale) {
|
||||
return (short) (time * (DEFAULT_TIMECODE_SCALE / oldTimeScale));
|
||||
}
|
||||
|
||||
private void skipTo(SharpStream stream, long absoluteOffset) throws IOException {
|
||||
absoluteOffset -= written;
|
||||
written += absoluteOffset;
|
||||
stream.skip(absoluteOffset);
|
||||
private void seekTo(SharpStream stream, long offset) throws IOException {
|
||||
if (stream.canSeek()) {
|
||||
stream.seek(offset);
|
||||
} else {
|
||||
if (offset > written) {
|
||||
stream.skip(offset - written);
|
||||
} else {
|
||||
stream.rewind();
|
||||
stream.skip(offset);
|
||||
}
|
||||
}
|
||||
|
||||
written = offset;
|
||||
}
|
||||
|
||||
private void writeLong(SharpStream stream, long number) throws IOException {
|
||||
@@ -453,7 +450,7 @@ public class WebMWriter {
|
||||
/* cluster */
|
||||
dump(new byte[]{0x1f, 0x43, (byte) 0xb6, 0x75}, stream);
|
||||
clusterOffsets.add(written);// warning: max cluster size is 256 MiB
|
||||
dump(new byte[]{0x20, 0x00, 0x00}, stream);
|
||||
dump(new byte[]{0x10, 0x00, 0x00, 0x00}, stream);
|
||||
|
||||
startOffset = written;// size for the this cluster
|
||||
|
||||
@@ -468,12 +465,12 @@ public class WebMWriter {
|
||||
private void makeEBML(SharpStream stream) throws IOException {
|
||||
// deafult values
|
||||
dump(new byte[]{
|
||||
0x1A, 0x45, (byte) 0xDF, (byte) 0xA3, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x1F, 0x42, (byte) 0x86, (byte) 0x81, 0x01,
|
||||
0x42, (byte) 0xF7, (byte) 0x81, 0x01, 0x42, (byte) 0xF2, (byte) 0x81, 0x04,
|
||||
0x42, (byte) 0xF3, (byte) 0x81, 0x08, 0x42, (byte) 0x82, (byte) 0x84, 0x77,
|
||||
0x65, 0x62, 0x6D, 0x42, (byte) 0x87, (byte) 0x81, 0x02,
|
||||
0x42, (byte) 0x85, (byte) 0x81, 0x02
|
||||
0x1A, 0x45, (byte) 0xDF, (byte) 0xA3, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x1F, 0x42, (byte) 0x86, (byte) 0x81, 0x01,
|
||||
0x42, (byte) 0xF7, (byte) 0x81, 0x01, 0x42, (byte) 0xF2, (byte) 0x81, 0x04,
|
||||
0x42, (byte) 0xF3, (byte) 0x81, 0x08, 0x42, (byte) 0x82, (byte) 0x84, 0x77,
|
||||
0x65, 0x62, 0x6D, 0x42, (byte) 0x87, (byte) 0x81, 0x02,
|
||||
0x42, (byte) 0x85, (byte) 0x81, 0x02
|
||||
}, stream);
|
||||
}
|
||||
|
||||
@@ -618,9 +615,10 @@ public class WebMWriter {
|
||||
|
||||
int offset = withLength ? 1 : 0;
|
||||
byte[] buffer = new byte[offset + length];
|
||||
long marker = (long) Math.floor((length - 1) / 8);
|
||||
long marker = (long) Math.floor((length - 1f) / 8f);
|
||||
|
||||
for (int i = length - 1, mul = 1; i >= 0; i--, mul *= 0x100) {
|
||||
float mul = 1;
|
||||
for (int i = length - 1; i >= 0; i--, mul *= 0x100) {
|
||||
long b = (long) Math.floor(number / mul);
|
||||
if (!withLength && i == marker) {
|
||||
b = b | (0x80 >> (length - 1));
|
||||
@@ -637,11 +635,7 @@ public class WebMWriter {
|
||||
|
||||
private ArrayList<byte[]> encode(String value) {
|
||||
byte[] str;
|
||||
try {
|
||||
str = value.getBytes("utf-8");
|
||||
} catch (UnsupportedEncodingException err) {
|
||||
str = value.getBytes();
|
||||
}
|
||||
str = value.getBytes(StandardCharsets.UTF_8);// or use "utf-8"
|
||||
|
||||
ArrayList<byte[]> buffer = new ArrayList<>(2);
|
||||
buffer.add(encode(str.length, false));
|
||||
@@ -720,9 +714,10 @@ public class WebMWriter {
|
||||
return (flags & 0x80) == 0x80;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, (flags & 0x80) == 0x80, absoluteTimecode);
|
||||
return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, isKeyframe(), absoluteTimecode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package org.schabi.newpipe.streams.io;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* based c#
|
||||
* based on c#
|
||||
*/
|
||||
public abstract class SharpStream {
|
||||
public abstract class SharpStream implements Closeable {
|
||||
|
||||
public abstract int read() throws IOException;
|
||||
|
||||
@@ -15,16 +16,14 @@ public abstract class SharpStream {
|
||||
|
||||
public abstract long skip(long amount) throws IOException;
|
||||
|
||||
|
||||
public abstract int available();
|
||||
public abstract long available();
|
||||
|
||||
public abstract void rewind() throws IOException;
|
||||
|
||||
public abstract boolean isClosed();
|
||||
|
||||
public abstract void dispose();
|
||||
|
||||
public abstract boolean isDisposed();
|
||||
|
||||
@Override
|
||||
public abstract void close();
|
||||
|
||||
public abstract boolean canRewind();
|
||||
|
||||
@@ -32,6 +31,13 @@ public abstract class SharpStream {
|
||||
|
||||
public abstract boolean canWrite();
|
||||
|
||||
public boolean canSetLength() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean canSeek() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public abstract void write(byte value) throws IOException;
|
||||
|
||||
@@ -39,9 +45,19 @@ public abstract class SharpStream {
|
||||
|
||||
public abstract void write(byte[] buffer, int offset, int count) throws IOException;
|
||||
|
||||
public abstract void flush() throws IOException;
|
||||
public void flush() throws IOException {
|
||||
// STUB
|
||||
}
|
||||
|
||||
public void setLength(long length) throws IOException {
|
||||
throw new IOException("Not implemented");
|
||||
}
|
||||
|
||||
public void seek(long offset) throws IOException {
|
||||
throw new IOException("Not implemented");
|
||||
}
|
||||
|
||||
public long length() throws IOException {
|
||||
throw new UnsupportedOperationException("Unsupported operation");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.animation.ArgbEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.FloatRange;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.animation.FastOutSlowInInterpolator;
|
||||
import android.util.Log;
|
||||
@@ -363,4 +364,24 @@ public class AnimationUtils {
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
public static void slideUp(final View view,
|
||||
long duration,
|
||||
long delay,
|
||||
@FloatRange(from = 0.0f, to = 1.0f) float translationPercent) {
|
||||
int translationY = (int) (view.getResources().getDisplayMetrics().heightPixels *
|
||||
(translationPercent));
|
||||
|
||||
view.animate().setListener(null).cancel();
|
||||
view.setAlpha(0f);
|
||||
view.setTranslationY(translationY);
|
||||
view.setVisibility(View.VISIBLE);
|
||||
view.animate()
|
||||
.alpha(1f)
|
||||
.translationY(0)
|
||||
.setStartDelay(delay)
|
||||
.setDuration(duration)
|
||||
.setInterpolator(new FastOutSlowInInterpolator())
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Layout;
|
||||
import android.text.Selection;
|
||||
import android.text.Spannable;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
public class CommentTextOnTouchListener implements View.OnTouchListener {
|
||||
|
||||
public static final CommentTextOnTouchListener INSTANCE = new CommentTextOnTouchListener();
|
||||
|
||||
private static final Pattern timestampPattern = Pattern.compile("(.*)#timestamp=(\\d+)");
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
if(!(v instanceof TextView)){
|
||||
return false;
|
||||
}
|
||||
TextView widget = (TextView) v;
|
||||
Object text = widget.getText();
|
||||
if (text instanceof Spanned) {
|
||||
Spannable buffer = (Spannable) text;
|
||||
|
||||
int action = event.getAction();
|
||||
|
||||
if (action == MotionEvent.ACTION_UP
|
||||
|| action == MotionEvent.ACTION_DOWN) {
|
||||
int x = (int) event.getX();
|
||||
int y = (int) event.getY();
|
||||
|
||||
x -= widget.getTotalPaddingLeft();
|
||||
y -= widget.getTotalPaddingTop();
|
||||
|
||||
x += widget.getScrollX();
|
||||
y += widget.getScrollY();
|
||||
|
||||
Layout layout = widget.getLayout();
|
||||
int line = layout.getLineForVertical(y);
|
||||
int off = layout.getOffsetForHorizontal(line, x);
|
||||
|
||||
ClickableSpan[] link = buffer.getSpans(off, off,
|
||||
ClickableSpan.class);
|
||||
|
||||
if (link.length != 0) {
|
||||
if (action == MotionEvent.ACTION_UP) {
|
||||
boolean handled = false;
|
||||
if(link[0] instanceof URLSpan){
|
||||
handled = handleUrl(v.getContext(), (URLSpan) link[0]);
|
||||
}
|
||||
if(!handled) link[0].onClick(widget);
|
||||
} else if (action == MotionEvent.ACTION_DOWN) {
|
||||
Selection.setSelection(buffer,
|
||||
buffer.getSpanStart(link[0]),
|
||||
buffer.getSpanEnd(link[0]));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean handleUrl(Context context, URLSpan urlSpan) {
|
||||
String url = urlSpan.getURL();
|
||||
int seconds = -1;
|
||||
Matcher matcher = timestampPattern.matcher(url);
|
||||
if(matcher.matches()){
|
||||
url = matcher.group(1);
|
||||
seconds = Integer.parseInt(matcher.group(2));
|
||||
}
|
||||
StreamingService service;
|
||||
StreamingService.LinkType linkType;
|
||||
try {
|
||||
service = NewPipe.getServiceByUrl(url);
|
||||
linkType = service.getLinkTypeByUrl(url);
|
||||
} catch (ExtractionException e) {
|
||||
return false;
|
||||
}
|
||||
if(linkType == StreamingService.LinkType.NONE){
|
||||
return false;
|
||||
}
|
||||
if(linkType == StreamingService.LinkType.STREAM && seconds != -1){
|
||||
return playOnPopup(context, url, service, seconds);
|
||||
}else{
|
||||
NavigationHelper.openRouterActivity(context, url);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean playOnPopup(Context context, String url, StreamingService service, int seconds) {
|
||||
LinkHandlerFactory factory = service.getStreamLHFactory();
|
||||
String cleanUrl = null;
|
||||
try {
|
||||
cleanUrl = factory.getUrl(factory.getId(url));
|
||||
} catch (ParsingException e) {
|
||||
return false;
|
||||
}
|
||||
Single single = ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false);
|
||||
single.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(info -> {
|
||||
PlayQueue playQueue = new SinglePlayQueue((StreamInfo) info, seconds*1000);
|
||||
NavigationHelper.playOnPopupPlayer(context, playQueue, false);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -29,10 +29,12 @@ import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.extractor.Info;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.SuggestionExtractor;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
@@ -46,6 +48,7 @@ import org.schabi.newpipe.report.UserAction;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Maybe;
|
||||
@@ -60,7 +63,7 @@ public final class ExtractorHelper {
|
||||
}
|
||||
|
||||
private static void checkServiceId(int serviceId) {
|
||||
if(serviceId == Constants.NO_SERVICE_ID) {
|
||||
if (serviceId == Constants.NO_SERVICE_ID) {
|
||||
throw new IllegalArgumentException("serviceId is NO_SERVICE_ID");
|
||||
}
|
||||
}
|
||||
@@ -95,17 +98,20 @@ public final class ExtractorHelper {
|
||||
public static Single<List<String>> suggestionsFor(final int serviceId,
|
||||
final String query) {
|
||||
checkServiceId(serviceId);
|
||||
return Single.fromCallable(() ->
|
||||
NewPipe.getService(serviceId)
|
||||
.getSuggestionExtractor()
|
||||
.suggestionList(query));
|
||||
return Single.fromCallable(() -> {
|
||||
SuggestionExtractor extractor = NewPipe.getService(serviceId)
|
||||
.getSuggestionExtractor();
|
||||
return extractor != null
|
||||
? extractor.suggestionList(query)
|
||||
: Collections.emptyList();
|
||||
});
|
||||
}
|
||||
|
||||
public static Single<StreamInfo> getStreamInfo(final int serviceId,
|
||||
final String url,
|
||||
boolean forceLoad) {
|
||||
checkServiceId(serviceId);
|
||||
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() ->
|
||||
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.STREAM, Single.fromCallable(() ->
|
||||
StreamInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||
}
|
||||
|
||||
@@ -113,29 +119,45 @@ public final class ExtractorHelper {
|
||||
final String url,
|
||||
boolean forceLoad) {
|
||||
checkServiceId(serviceId);
|
||||
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() ->
|
||||
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.CHANNEL, Single.fromCallable(() ->
|
||||
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||
}
|
||||
|
||||
public static Single<InfoItemsPage> getMoreChannelItems(final int serviceId,
|
||||
final String url,
|
||||
final String nextStreamsUrl) {
|
||||
final String url,
|
||||
final String nextStreamsUrl) {
|
||||
checkServiceId(serviceId);
|
||||
return Single.fromCallable(() ->
|
||||
ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl));
|
||||
}
|
||||
|
||||
public static Single<CommentsInfo> getCommentsInfo(final int serviceId,
|
||||
final String url,
|
||||
boolean forceLoad) {
|
||||
checkServiceId(serviceId);
|
||||
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.COMMENT, Single.fromCallable(() ->
|
||||
CommentsInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||
}
|
||||
|
||||
public static Single<InfoItemsPage> getMoreCommentItems(final int serviceId,
|
||||
final CommentsInfo info,
|
||||
final String nextPageUrl) {
|
||||
checkServiceId(serviceId);
|
||||
return Single.fromCallable(() ->
|
||||
CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPageUrl));
|
||||
}
|
||||
|
||||
public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId,
|
||||
final String url,
|
||||
boolean forceLoad) {
|
||||
checkServiceId(serviceId);
|
||||
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() ->
|
||||
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST, Single.fromCallable(() ->
|
||||
PlaylistInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||
}
|
||||
|
||||
public static Single<InfoItemsPage> getMorePlaylistItems(final int serviceId,
|
||||
final String url,
|
||||
final String nextStreamsUrl) {
|
||||
final String url,
|
||||
final String nextStreamsUrl) {
|
||||
checkServiceId(serviceId);
|
||||
return Single.fromCallable(() ->
|
||||
PlaylistInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl));
|
||||
@@ -144,7 +166,7 @@ public final class ExtractorHelper {
|
||||
public static Single<KioskInfo> getKioskInfo(final int serviceId,
|
||||
final String url,
|
||||
boolean forceLoad) {
|
||||
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() ->
|
||||
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST, Single.fromCallable(() ->
|
||||
KioskInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||
}
|
||||
|
||||
@@ -168,16 +190,17 @@ public final class ExtractorHelper {
|
||||
private static <I extends Info> Single<I> checkCache(boolean forceLoad,
|
||||
int serviceId,
|
||||
String url,
|
||||
InfoItem.InfoType infoType,
|
||||
Single<I> loadFromNetwork) {
|
||||
checkServiceId(serviceId);
|
||||
loadFromNetwork = loadFromNetwork.doOnSuccess(info -> cache.putInfo(serviceId, url, info));
|
||||
loadFromNetwork = loadFromNetwork.doOnSuccess(info -> cache.putInfo(serviceId, url, info, infoType));
|
||||
|
||||
Single<I> load;
|
||||
if (forceLoad) {
|
||||
cache.removeInfo(serviceId, url);
|
||||
cache.removeInfo(serviceId, url, infoType);
|
||||
load = loadFromNetwork;
|
||||
} else {
|
||||
load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url),
|
||||
load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url, infoType),
|
||||
loadFromNetwork.toMaybe())
|
||||
.firstElement() //Take the first valid
|
||||
.toSingle();
|
||||
@@ -189,20 +212,24 @@ public final class ExtractorHelper {
|
||||
/**
|
||||
* Default implementation uses the {@link InfoCache} to get cached results
|
||||
*/
|
||||
public static <I extends Info> Maybe<I> loadFromCache(final int serviceId, final String url) {
|
||||
public static <I extends Info> Maybe<I> loadFromCache(final int serviceId, final String url, InfoItem.InfoType infoType) {
|
||||
checkServiceId(serviceId);
|
||||
return Maybe.defer(() -> {
|
||||
//noinspection unchecked
|
||||
I info = (I) cache.getFromKey(serviceId, url);
|
||||
if (MainActivity.DEBUG) Log.d(TAG, "loadFromCache() called, info > " + info);
|
||||
//noinspection unchecked
|
||||
I info = (I) cache.getFromKey(serviceId, url, infoType);
|
||||
if (MainActivity.DEBUG) Log.d(TAG, "loadFromCache() called, info > " + info);
|
||||
|
||||
// Only return info if it's not null (it is cached)
|
||||
if (info != null) {
|
||||
return Maybe.just(info);
|
||||
}
|
||||
// Only return info if it's not null (it is cached)
|
||||
if (info != null) {
|
||||
return Maybe.just(info);
|
||||
}
|
||||
|
||||
return Maybe.empty();
|
||||
});
|
||||
return Maybe.empty();
|
||||
});
|
||||
}
|
||||
|
||||
public static boolean isCached(final int serviceId, final String url, InfoItem.InfoType infoType) {
|
||||
return null != loadFromCache(serviceId, url, infoType).blockingGet();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,8 +247,6 @@ public final class ExtractorHelper {
|
||||
context.startActivity(intent);
|
||||
} else if (exception instanceof IOException) {
|
||||
Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show();
|
||||
} else if (exception instanceof YoutubeStreamExtractor.GemaException) {
|
||||
Toast.makeText(context, R.string.blocked_by_gema, Toast.LENGTH_LONG).show();
|
||||
} else if (exception instanceof ContentNotAvailableException) {
|
||||
Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.schabi.newpipe.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.support.annotation.NonNull;
|
||||
@@ -29,7 +30,7 @@ public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.File
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
if(ThemeHelper.isLightThemeSelected(this)) {
|
||||
if (ThemeHelper.isLightThemeSelected(this)) {
|
||||
this.setTheme(R.style.FilePickerThemeLight);
|
||||
} else {
|
||||
this.setTheme(R.style.FilePickerThemeDark);
|
||||
@@ -73,6 +74,11 @@ public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.File
|
||||
.putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_NEW_FILE);
|
||||
}
|
||||
|
||||
public static boolean isOwnFileUri(@NonNull Context context, @NonNull Uri uri) {
|
||||
if (uri.getAuthority() == null) return false;
|
||||
return uri.getAuthority().startsWith(context.getPackageName());
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Internal
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@@ -10,6 +10,9 @@ import java.util.regex.Pattern;
|
||||
|
||||
public class FilenameUtils {
|
||||
|
||||
private static final String CHARSET_MOST_SPECIAL = "[\\n\\r|?*<\":\\\\>/']+";
|
||||
private static final String CHARSET_ONLY_LETTERS_AND_DIGITS = "[^\\w\\d]+";
|
||||
|
||||
/**
|
||||
* #143 #44 #42 #22: make sure that the filename does not contain illegal chars.
|
||||
* @param context the context to retrieve strings and preferences from
|
||||
@@ -18,11 +21,28 @@ public class FilenameUtils {
|
||||
*/
|
||||
public static String createFilename(Context context, String title) {
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String key = context.getString(R.string.settings_file_charset_key);
|
||||
final String value = sharedPreferences.getString(key, context.getString(R.string.default_file_charset_value));
|
||||
Pattern pattern = Pattern.compile(value);
|
||||
|
||||
final String charset_ld = context.getString(R.string.charset_letters_and_digits_value);
|
||||
final String charset_ms = context.getString(R.string.charset_most_special_value);
|
||||
final String defaultCharset = context.getString(R.string.default_file_charset_value);
|
||||
|
||||
final String replacementChar = sharedPreferences.getString(context.getString(R.string.settings_file_replacement_character_key), "_");
|
||||
String selectedCharset = sharedPreferences.getString(context.getString(R.string.settings_file_charset_key), null);
|
||||
|
||||
final String charset;
|
||||
|
||||
if (selectedCharset == null || selectedCharset.isEmpty()) selectedCharset = defaultCharset;
|
||||
|
||||
if (selectedCharset.equals(charset_ld)) {
|
||||
charset = CHARSET_ONLY_LETTERS_AND_DIGITS;
|
||||
} else if (selectedCharset.equals(charset_ms)) {
|
||||
charset = CHARSET_MOST_SPECIAL;
|
||||
} else {
|
||||
charset = selectedCharset;// ¿is the user using a custom charset?
|
||||
}
|
||||
|
||||
Pattern pattern = Pattern.compile(charset);
|
||||
|
||||
return createFilename(title, pattern, replacementChar);
|
||||
}
|
||||
|
||||
|
||||
10
app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java
Normal file
10
app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
|
||||
public class FireTvUtils {
|
||||
public static boolean isFireTv(){
|
||||
final String AMAZON_FEATURE_FIRE_TV = "amazon.hardware.fire_tv";
|
||||
return App.getApp().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.extractor.Info;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -52,27 +53,27 @@ public final class InfoCache {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Info getFromKey(int serviceId, @NonNull String url) {
|
||||
public Info getFromKey(int serviceId, @NonNull String url, @NonNull InfoItem.InfoType infoType) {
|
||||
if (DEBUG) Log.d(TAG, "getFromKey() called with: serviceId = [" + serviceId + "], url = [" + url + "]");
|
||||
synchronized (lruCache) {
|
||||
return getInfo(keyOf(serviceId, url));
|
||||
return getInfo(keyOf(serviceId, url, infoType));
|
||||
}
|
||||
}
|
||||
|
||||
public void putInfo(int serviceId, @NonNull String url, @NonNull Info info) {
|
||||
public void putInfo(int serviceId, @NonNull String url, @NonNull Info info, @NonNull InfoItem.InfoType infoType) {
|
||||
if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]");
|
||||
|
||||
final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId());
|
||||
synchronized (lruCache) {
|
||||
final CacheData data = new CacheData(info, expirationMillis);
|
||||
lruCache.put(keyOf(serviceId, url), data);
|
||||
lruCache.put(keyOf(serviceId, url, infoType), data);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeInfo(int serviceId, @NonNull String url) {
|
||||
public void removeInfo(int serviceId, @NonNull String url, @NonNull InfoItem.InfoType infoType) {
|
||||
if (DEBUG) Log.d(TAG, "removeInfo() called with: serviceId = [" + serviceId + "], url = [" + url + "]");
|
||||
synchronized (lruCache) {
|
||||
lruCache.remove(keyOf(serviceId, url));
|
||||
lruCache.remove(keyOf(serviceId, url, infoType));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,8 +99,8 @@ public final class InfoCache {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String keyOf(final int serviceId, @NonNull final String url) {
|
||||
return serviceId + url;
|
||||
private static String keyOf(final int serviceId, @NonNull final String url, @NonNull InfoItem.InfoType infoType) {
|
||||
return serviceId + url + infoType.toString();
|
||||
}
|
||||
|
||||
private static void removeStaleCache() {
|
||||
|
||||
@@ -31,6 +31,8 @@ public class KioskTranslator {
|
||||
return c.getString(R.string.top_50);
|
||||
case "New & hot":
|
||||
return c.getString(R.string.new_and_hot);
|
||||
case "conferences":
|
||||
return c.getString(R.string.conferences);
|
||||
default:
|
||||
return kioskId;
|
||||
}
|
||||
@@ -44,6 +46,8 @@ public class KioskTranslator {
|
||||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
|
||||
case "New & hot":
|
||||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
|
||||
case "conferences":
|
||||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -430,24 +430,26 @@ public final class ListHelper {
|
||||
*/
|
||||
private static String getResolutionLimit(Context context) {
|
||||
String resolutionLimit = null;
|
||||
if (!isWifiActive(context)) {
|
||||
if (isMeteredNetwork(context)) {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String defValue = context.getString(R.string.limit_data_usage_none_key);
|
||||
String value = preferences.getString(
|
||||
context.getString(R.string.limit_mobile_data_usage_key), defValue);
|
||||
resolutionLimit = value.equals(defValue) ? null : value;
|
||||
resolutionLimit = defValue.equals(value) ? null : value;
|
||||
}
|
||||
return resolutionLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Are we connected to wifi?
|
||||
* The current network is metered (like mobile data)?
|
||||
* @param context App context
|
||||
* @return {@code true} if connected to wifi
|
||||
* @return {@code true} if connected to a metered network
|
||||
*/
|
||||
private static boolean isWifiActive(Context context)
|
||||
private static boolean isMeteredNetwork(Context context)
|
||||
{
|
||||
ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
return manager != null && manager.getActiveNetworkInfo() != null && manager.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_WIFI;
|
||||
if (manager == null || manager.getActiveNetworkInfo() == null) return false;
|
||||
|
||||
return manager.isActiveNetworkMetered();
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user