mirror of
https://github.com/androidx/media.git
synced 2025-04-29 22:36:54 +08:00
Compare commits
811 Commits
76088cd6af
...
51efcad672
Author | SHA1 | Date | |
---|---|---|---|
![]() |
51efcad672 | ||
![]() |
fdbf2a48f7 | ||
![]() |
df3b138391 | ||
![]() |
7c0fb628ac | ||
![]() |
05827ea5ed | ||
![]() |
c558ae43c5 | ||
![]() |
8212b964d1 | ||
![]() |
bd104b1cc4 | ||
![]() |
efb109dd88 | ||
![]() |
dadd970f6e | ||
![]() |
25dc53551d | ||
![]() |
53a3144f96 | ||
![]() |
89cfee6e31 | ||
![]() |
8fa785fc27 | ||
![]() |
5c1e0d964a | ||
![]() |
fa71bf229e | ||
![]() |
e2017e35db | ||
![]() |
0a284f4927 | ||
![]() |
8e2ed3bf4d | ||
![]() |
d8914c31e2 | ||
![]() |
1512b5b622 | ||
![]() |
f357f0a966 | ||
![]() |
5957daadee | ||
![]() |
9a91b2774e | ||
![]() |
a3f4b3dd1f | ||
![]() |
f2739302af | ||
![]() |
24b3bf21b8 | ||
![]() |
23468ed55c | ||
![]() |
97606094ca | ||
![]() |
b215670445 | ||
![]() |
67ec5b76ad | ||
![]() |
6b003993a5 | ||
![]() |
d2703a12cc | ||
![]() |
fb57e45a58 | ||
![]() |
d9deda7b6e | ||
![]() |
5d1e8d1279 | ||
![]() |
3b55f18013 | ||
![]() |
e1d328db11 | ||
![]() |
766dbd497e | ||
![]() |
595fd48cd7 | ||
![]() |
768f25e163 | ||
![]() |
2cb6d4312b | ||
![]() |
c7fa2c3de6 | ||
![]() |
c66c221b28 | ||
![]() |
fd2e874b5d | ||
![]() |
3083a48ecb | ||
![]() |
f7479347c0 | ||
![]() |
4d3f71a0c6 | ||
![]() |
9ba0a1b470 | ||
![]() |
f739e2d68c | ||
![]() |
fc4112beee | ||
![]() |
9052313245 | ||
![]() |
c4a77475c4 | ||
![]() |
954a1d44e4 | ||
![]() |
8b9e9203a1 | ||
![]() |
317bf3c6c0 | ||
![]() |
521c385d4b | ||
![]() |
a578d43324 | ||
![]() |
28b70f7e85 | ||
![]() |
d755a0477d | ||
![]() |
1f691106f1 | ||
![]() |
77c8ddd884 | ||
![]() |
50f6bdd1fd | ||
![]() |
ff65d7b05b | ||
![]() |
6c143230d7 | ||
![]() |
62ef83b68e | ||
![]() |
8ee2532db3 | ||
![]() |
4d50e8a815 | ||
![]() |
2c123dedbb | ||
![]() |
7ed1ebea3a | ||
![]() |
b8468bbee3 | ||
![]() |
a5d7be959b | ||
![]() |
073e50a893 | ||
![]() |
8e5368a807 | ||
![]() |
816a119617 | ||
![]() |
13e14b8e31 | ||
![]() |
41062d3910 | ||
![]() |
e393587787 | ||
![]() |
d880ab5d4f | ||
![]() |
75da514b7f | ||
![]() |
cb2df35a5b | ||
![]() |
ed05bc67c3 | ||
![]() |
4a8d28cef8 | ||
![]() |
16add9922d | ||
![]() |
e58d9120bc | ||
![]() |
bb358241b9 | ||
![]() |
bc872503b9 | ||
![]() |
b49374c8fe | ||
![]() |
3f10fa92bb | ||
![]() |
625ff234e4 | ||
![]() |
88e7636ae0 | ||
![]() |
57bbb6c5c6 | ||
![]() |
9ad06570b5 | ||
![]() |
7db4802f02 | ||
![]() |
1ceafc7a61 | ||
![]() |
3b897241d2 | ||
![]() |
2a3e733161 | ||
![]() |
22e5f25648 | ||
![]() |
71a2b7d72d | ||
![]() |
89b5420bc8 | ||
![]() |
d14807c836 | ||
![]() |
be76766f95 | ||
![]() |
8bd1db5f2c | ||
![]() |
41af00f100 | ||
![]() |
841e27ae5c | ||
![]() |
cd6e61d856 | ||
![]() |
12afdfbaea | ||
![]() |
dfd2b75720 | ||
![]() |
385498c24e | ||
![]() |
22853a5c4c | ||
![]() |
12072f7248 | ||
![]() |
9e22f03718 | ||
![]() |
04d9a751c6 | ||
![]() |
2a91d47ea9 | ||
![]() |
f5e583332b | ||
![]() |
cea67e8826 | ||
![]() |
56bd32da96 | ||
![]() |
38cfd7dc36 | ||
![]() |
b9ef0353cf | ||
![]() |
ee4f9d9140 | ||
![]() |
2155c37b08 | ||
![]() |
6e332e9b91 | ||
![]() |
813973bb58 | ||
![]() |
be51913b81 | ||
![]() |
5de3ee86e3 | ||
![]() |
1310496809 | ||
![]() |
75607ac1eb | ||
![]() |
a5e5374ba0 | ||
![]() |
a5689735a1 | ||
![]() |
447d784636 | ||
![]() |
1b882fec0c | ||
![]() |
fafd12bcfe | ||
![]() |
9f60eb3825 | ||
![]() |
5510635620 | ||
![]() |
50d4e66308 | ||
![]() |
d022b570f2 | ||
![]() |
1a996d87ca | ||
![]() |
faf555e12b | ||
![]() |
cadecf0219 | ||
![]() |
aa6183e883 | ||
![]() |
0ba3bf66c6 | ||
![]() |
1190980616 | ||
![]() |
65e7b599d8 | ||
![]() |
babc2dd416 | ||
![]() |
5e6fb88372 | ||
![]() |
d23d4fc314 | ||
![]() |
d641f6a04c | ||
![]() |
097771306d | ||
![]() |
e229f957e6 | ||
![]() |
a56a0bd928 | ||
![]() |
baf46d36d9 | ||
![]() |
6a4aa4515e | ||
![]() |
edc44eefd2 | ||
![]() |
85158ec841 | ||
![]() |
b90610b95a | ||
![]() |
a80e7be029 | ||
![]() |
4f3ed2490a | ||
![]() |
2fe92bfca5 | ||
![]() |
bcce7b5949 | ||
![]() |
449b81d510 | ||
![]() |
92a06606b4 | ||
![]() |
4c163553e7 | ||
![]() |
97a1d31b5d | ||
![]() |
f8f66bdfaa | ||
![]() |
05e66d9cf6 | ||
![]() |
a19f68c87e | ||
![]() |
ae3f962769 | ||
![]() |
a4cc0f2384 | ||
![]() |
2aab921aa2 | ||
![]() |
d0a3d31e56 | ||
![]() |
3e56d2a6fb | ||
![]() |
decfb9b0a9 | ||
![]() |
1431497e7f | ||
![]() |
6e9a2cc0cd | ||
![]() |
aaa7e9e3cb | ||
![]() |
93c129449a | ||
![]() |
35d5bd9675 | ||
![]() |
4ed9abd05b | ||
![]() |
82cb1d8ac7 | ||
![]() |
5f4c30c431 | ||
![]() |
9c0a9c19b7 | ||
![]() |
99f2a9f152 | ||
![]() |
79b61d05a6 | ||
![]() |
d6844699c5 | ||
![]() |
0fb4e3ba11 | ||
![]() |
2b07ece0e5 | ||
![]() |
39d0881083 | ||
![]() |
df575a8d19 | ||
![]() |
344214d711 | ||
![]() |
fc1d133454 | ||
![]() |
9f96fe81f3 | ||
![]() |
ad18ae9c42 | ||
![]() |
8466a957c3 | ||
![]() |
045b8e6a52 | ||
![]() |
3f4e0bdb04 | ||
![]() |
17100259cd | ||
![]() |
6b31b4620c | ||
![]() |
9af43c7381 | ||
![]() |
3c0e2ee198 | ||
![]() |
c9a936e153 | ||
![]() |
fc6df77831 | ||
![]() |
98f52a1dbb | ||
![]() |
6e53d9b3bd | ||
![]() |
dfef16d5c6 | ||
![]() |
e15438322d | ||
![]() |
bb37aad170 | ||
![]() |
bb9b3bd660 | ||
![]() |
8d22482f79 | ||
![]() |
ddcf455d03 | ||
![]() |
c058d97a97 | ||
![]() |
706c363104 | ||
![]() |
80e6fa2aa7 | ||
![]() |
0b9ca1e70b | ||
![]() |
80a734f4f1 | ||
![]() |
c3962d2fe6 | ||
![]() |
227a4d76b1 | ||
![]() |
1772050ece | ||
![]() |
382f30616a | ||
![]() |
dddcdf1613 | ||
![]() |
a7a5d6e92b | ||
![]() |
6b54372df8 | ||
![]() |
c1242ffef1 | ||
![]() |
61b7dfd7ba | ||
![]() |
916018d9fe | ||
![]() |
b6d4886887 | ||
![]() |
9bb254f697 | ||
![]() |
8b0cfda178 | ||
![]() |
15bdbf735d | ||
![]() |
431efc9c50 | ||
![]() |
635e699965 | ||
![]() |
5421a74d06 | ||
![]() |
7c10ef03e4 | ||
![]() |
6fe011beb4 | ||
![]() |
a31c7ad9a8 | ||
![]() |
fb58bdfd52 | ||
![]() |
c22798f99c | ||
![]() |
8abd36fb27 | ||
![]() |
190563b8eb | ||
![]() |
c797249998 | ||
![]() |
a0618eb0ec | ||
![]() |
4ac4f7e2e0 | ||
![]() |
a4d9a3e096 | ||
![]() |
297b2b9956 | ||
![]() |
fda8b8a35d | ||
![]() |
4328d29f34 | ||
![]() |
1732892927 | ||
![]() |
b2c31b0743 | ||
![]() |
0d8e42238e | ||
![]() |
f5f5b63bf7 | ||
![]() |
22142e0db3 | ||
![]() |
492574bded | ||
![]() |
e9b82ee951 | ||
![]() |
04c2d22178 | ||
![]() |
b49eaf9e87 | ||
![]() |
0936b549ae | ||
![]() |
c5feb28838 | ||
![]() |
fa4cc7c65c | ||
![]() |
62341f31f9 | ||
![]() |
a2016f03c6 | ||
![]() |
ee4a0ea3df | ||
![]() |
736c7528cb | ||
![]() |
2b9c7f8392 | ||
![]() |
465399b57e | ||
![]() |
1b2e391971 | ||
![]() |
fbf9be2f00 | ||
![]() |
2361624222 | ||
![]() |
e9e0569425 | ||
![]() |
8f17ab84f8 | ||
![]() |
2eb8e53f8c | ||
![]() |
f05e6a7d6e | ||
![]() |
adb9306e2d | ||
![]() |
eefad8cbfa | ||
![]() |
6300a55eb2 | ||
![]() |
1892435fb3 | ||
![]() |
b25d6ef249 | ||
![]() |
a4d28b4f10 | ||
![]() |
52387bb975 | ||
![]() |
d18ad57e30 | ||
![]() |
8b33a0a50f | ||
![]() |
0a27e7946f | ||
![]() |
b183bfb0cb | ||
![]() |
36be62ee8c | ||
![]() |
28027c64fd | ||
![]() |
33c3d5140e | ||
![]() |
93f9e6574c | ||
![]() |
e61b521b4a | ||
![]() |
6291cfda86 | ||
![]() |
77790df5a2 | ||
![]() |
30c5c66db4 | ||
![]() |
d9b61b060f | ||
![]() |
4744f082af | ||
![]() |
b2f9c1e252 | ||
![]() |
ce0512845f | ||
![]() |
6658dc097b | ||
![]() |
90c91fa5d7 | ||
![]() |
47a5166a1d | ||
![]() |
671810235f | ||
![]() |
8520c66fd8 | ||
![]() |
391b72e257 | ||
![]() |
f52d98eafd | ||
![]() |
3cd0e1ad3d | ||
![]() |
0254b2d16e | ||
![]() |
250bc86669 | ||
![]() |
27a6883768 | ||
![]() |
7fbf511d30 | ||
![]() |
721776c30e | ||
![]() |
c26a633d83 | ||
![]() |
71cb246913 | ||
![]() |
c3b13a60a3 | ||
![]() |
72a71e8b9c | ||
![]() |
f6eb2e6dd5 | ||
![]() |
d0b757886e | ||
![]() |
64462b99fc | ||
![]() |
b225383958 | ||
![]() |
2df50f209e | ||
![]() |
cd511ea60b | ||
![]() |
b9d12837b4 | ||
![]() |
9608ae4e3d | ||
![]() |
8a709a7d76 | ||
![]() |
e1b57c130d | ||
![]() |
0e6d39de29 | ||
![]() |
04ff17d939 | ||
![]() |
709d3fd35e | ||
![]() |
314413365b | ||
![]() |
7d4ddfbb91 | ||
![]() |
d9776e74c8 | ||
![]() |
b321c8d3bd | ||
![]() |
bb3b85a359 | ||
![]() |
30038079c4 | ||
![]() |
79d41aac7e | ||
![]() |
48e3eaa623 | ||
![]() |
3a00960831 | ||
![]() |
0cf52ed45d | ||
![]() |
b54d8737cf | ||
![]() |
281a0e7ac8 | ||
![]() |
b3e18729d3 | ||
![]() |
1bea11637b | ||
![]() |
d16d80ca49 | ||
![]() |
39c35b779b | ||
![]() |
d894217a4e | ||
![]() |
bcae7abde9 | ||
![]() |
8dca430e42 | ||
![]() |
6f187a3859 | ||
![]() |
229aadc91b | ||
![]() |
ec50358784 | ||
![]() |
e11aabb794 | ||
![]() |
871381288c | ||
![]() |
2dc6af1fae | ||
![]() |
31e5142b72 | ||
![]() |
9ac91aaadd | ||
![]() |
38363acc8d | ||
![]() |
a9f9928eb3 | ||
![]() |
5a87aab2f7 | ||
![]() |
0074a97333 | ||
![]() |
0044d0cebc | ||
![]() |
b2aa8d6d21 | ||
![]() |
6c2d25184c | ||
![]() |
575a2ebbd8 | ||
![]() |
8fe0b7ac69 | ||
![]() |
ff80ab220f | ||
![]() |
8d9cb7e5d0 | ||
![]() |
618c2cf952 | ||
![]() |
82bed39140 | ||
![]() |
026700a9d9 | ||
![]() |
fbd6dfa439 | ||
![]() |
d8333b37cf | ||
![]() |
d01d10ce0e | ||
![]() |
871c15946b | ||
![]() |
115f09dd6d | ||
![]() |
750baf2c05 | ||
![]() |
7ecaebe3d6 | ||
![]() |
cd5d5bde27 | ||
![]() |
9b71f2a3ba | ||
![]() |
682889f91d | ||
![]() |
01c51d8475 | ||
![]() |
9cc4532f84 | ||
![]() |
969c50d60f | ||
![]() |
2caa0d39eb | ||
![]() |
48e3c6fd75 | ||
![]() |
d6e4642bcf | ||
![]() |
afd601f670 | ||
![]() |
6da58758a8 | ||
![]() |
3b9737f88a | ||
![]() |
16617f5a8b | ||
![]() |
515de89973 | ||
![]() |
f22d91d9e5 | ||
![]() |
8188f7a865 | ||
![]() |
5c40498054 | ||
![]() |
1b0e2fb75e | ||
![]() |
684c394019 | ||
![]() |
f06bfc2e15 | ||
![]() |
c12b1768a6 | ||
![]() |
5c3c3b91f3 | ||
![]() |
915130eb00 | ||
![]() |
cb578c0f37 | ||
![]() |
5da67a0033 | ||
![]() |
59789b7209 | ||
![]() |
506bc91dd8 | ||
![]() |
9d610894cf | ||
![]() |
1ac847ad60 | ||
![]() |
ca58867816 | ||
![]() |
44aa87c832 | ||
![]() |
c3b58f2434 | ||
![]() |
896bd0d330 | ||
![]() |
2f507650cc | ||
![]() |
2d37823695 | ||
![]() |
1c044c98ad | ||
![]() |
16dcf91c1c | ||
![]() |
6d2331fd13 | ||
![]() |
9b628d4542 | ||
![]() |
d4f4a2c1d4 | ||
![]() |
aa2ee8f702 | ||
![]() |
32ab258c43 | ||
![]() |
3dede2415d | ||
![]() |
acc41cb5f7 | ||
![]() |
6287a22285 | ||
![]() |
2d11a339de | ||
![]() |
4b26eb2800 | ||
![]() |
601b025b3c | ||
![]() |
71f82df57f | ||
![]() |
12566a50d5 | ||
![]() |
3fe1f2a734 | ||
![]() |
d5d85558c1 | ||
![]() |
319ac2e5af | ||
![]() |
1326a92350 | ||
![]() |
36466cf883 | ||
![]() |
72672b7f87 | ||
![]() |
07b658a6da | ||
![]() |
522883bf16 | ||
![]() |
3bce3af1a3 | ||
![]() |
83b5eb0040 | ||
![]() |
e0496ff88d | ||
![]() |
beda44520a | ||
![]() |
684273e4e1 | ||
![]() |
c636d9181b | ||
![]() |
77a215f39c | ||
![]() |
4997ea82fa | ||
![]() |
0b0c198f59 | ||
![]() |
f587ac2a67 | ||
![]() |
3936c27b6d | ||
![]() |
de31a3745c | ||
![]() |
893b3775d4 | ||
![]() |
c222bb8e03 | ||
![]() |
da05a1a66b | ||
![]() |
2f880bf051 | ||
![]() |
55d9626b5a | ||
![]() |
f149cb28a3 | ||
![]() |
b2e8489119 | ||
![]() |
424b43d2eb | ||
![]() |
f55e0f3a3f | ||
![]() |
af32e859ed | ||
![]() |
8d2f531470 | ||
![]() |
c377a34a5a | ||
![]() |
3e3cd8e6ac | ||
![]() |
b34a5b4e5d | ||
![]() |
dc21f3add2 | ||
![]() |
6689fee2b2 | ||
![]() |
be63e156bb | ||
![]() |
bb62278627 | ||
![]() |
ef19740c92 | ||
![]() |
e017213ee8 | ||
![]() |
459162c692 | ||
![]() |
987869e456 | ||
![]() |
fc3f096888 | ||
![]() |
1f6c795407 | ||
![]() |
12546070ee | ||
![]() |
aa0e7298ca | ||
![]() |
38d12f25e1 | ||
![]() |
1a7da45dd9 | ||
![]() |
0a75447785 | ||
![]() |
1d2ffcb165 | ||
![]() |
b816e2f284 | ||
![]() |
038d7c8cb9 | ||
![]() |
862791f837 | ||
![]() |
b6724e2115 | ||
![]() |
f1a0e4b0b7 | ||
![]() |
b5a1efdbce | ||
![]() |
92e0b5978f | ||
![]() |
5ab9a7856f | ||
![]() |
f60d2b4146 | ||
![]() |
6193f7c38f | ||
![]() |
3e94bd6125 | ||
![]() |
fb2d5f905d | ||
![]() |
e927d7b986 | ||
![]() |
06b94f5448 | ||
![]() |
c770a6ab6f | ||
![]() |
46ab6cf030 | ||
![]() |
bffa253e3a | ||
![]() |
9828d104b5 | ||
![]() |
593899c2b2 | ||
![]() |
c870fb7594 | ||
![]() |
876abe315b | ||
![]() |
148641f049 | ||
![]() |
d72453a6cd | ||
![]() |
9a3dc3f03a | ||
![]() |
10ca39a779 | ||
![]() |
e8f60d4fec | ||
![]() |
7821e7702b | ||
![]() |
e4993779db | ||
![]() |
18d156fe3c | ||
![]() |
0037388660 | ||
![]() |
1df0ee2052 | ||
![]() |
e503535ec2 | ||
![]() |
547a164f42 | ||
![]() |
675c1d898a | ||
![]() |
ecdf7c5df7 | ||
![]() |
8908d82cac | ||
![]() |
25c927e9f3 | ||
![]() |
b2ba5da09b | ||
![]() |
19b276d6a7 | ||
![]() |
d214e90ce4 | ||
![]() |
da4376d48d | ||
![]() |
77d33645cc | ||
![]() |
f113a78e0d | ||
![]() |
e27238666a | ||
![]() |
7f62f74737 | ||
![]() |
21e627da1c | ||
![]() |
55c81f6ed6 | ||
![]() |
037a6f3219 | ||
![]() |
38145b6e89 | ||
![]() |
55a1290bfd | ||
![]() |
24ea6c4c14 | ||
![]() |
c5af8ba360 | ||
![]() |
2683331299 | ||
![]() |
01422d8322 | ||
![]() |
6d265476e4 | ||
![]() |
4d925bde81 | ||
![]() |
852091f2d9 | ||
![]() |
f3f4646296 | ||
![]() |
dfe3c90f9a | ||
![]() |
08619bf6fa | ||
![]() |
6e29b96337 | ||
![]() |
270543555d | ||
![]() |
c861947bee | ||
![]() |
52e45e0864 | ||
![]() |
bff5523bb6 | ||
![]() |
60133b0c7e | ||
![]() |
a371824dbc | ||
![]() |
6cf3004d62 | ||
![]() |
d5d5771cd2 | ||
![]() |
96c35966d8 | ||
![]() |
de76d7932f | ||
![]() |
465873ac28 | ||
![]() |
b19b6ccc60 | ||
![]() |
46578ee0a6 | ||
![]() |
2a4bae01ef | ||
![]() |
e357629400 | ||
![]() |
f257e5511f | ||
![]() |
6a3def3ccb | ||
![]() |
2fc379a61b | ||
![]() |
3eb36d67bd | ||
![]() |
bb20eb4975 | ||
![]() |
0d8f1d5ab9 | ||
![]() |
7ef1a7ab8b | ||
![]() |
25a782e10a | ||
![]() |
c0fb4b7aff | ||
![]() |
827966b7a4 | ||
![]() |
73c4bb6e1f | ||
![]() |
52dfe25542 | ||
![]() |
79effe7b60 | ||
![]() |
5282fe3125 | ||
![]() |
754dfd76c8 | ||
![]() |
e5110e6442 | ||
![]() |
407bc4fec9 | ||
![]() |
089eaa1d5d | ||
![]() |
cf4488aa1f | ||
![]() |
d92c9aa8a1 | ||
![]() |
66e8b53b43 | ||
![]() |
3c01500a4e | ||
![]() |
dc7a0ca22f | ||
![]() |
646a6352a2 | ||
![]() |
d702e1d496 | ||
![]() |
b9c9d95b90 | ||
![]() |
98723dc0a8 | ||
![]() |
ccc7b22ff4 | ||
![]() |
26f10effc2 | ||
![]() |
c7ca9fbe7d | ||
![]() |
b782cdff52 | ||
![]() |
9c6d9e9e47 | ||
![]() |
9ae136becb | ||
![]() |
fff6e2e169 | ||
![]() |
66ec022bf0 | ||
![]() |
8be2556efd | ||
![]() |
2568ff73cb | ||
![]() |
7b3effb871 | ||
![]() |
cbb8e2f1e6 | ||
![]() |
0143884cd7 | ||
![]() |
ecc5cd889f | ||
![]() |
1af0b5b432 | ||
![]() |
c3d4a3d683 | ||
![]() |
c50867c81d | ||
![]() |
68c0b8115f | ||
![]() |
11fc0871ac | ||
![]() |
c503a63878 | ||
![]() |
3b7d19e94e | ||
![]() |
e6448f3498 | ||
![]() |
2379d0f18c | ||
![]() |
93b4c6ef47 | ||
![]() |
6453054878 | ||
![]() |
16a15b94ca | ||
![]() |
301ef207f2 | ||
![]() |
74611bbdc0 | ||
![]() |
0f96ad247d | ||
![]() |
c3d4722197 | ||
![]() |
9db66d6c6b | ||
![]() |
8393c2e445 | ||
![]() |
175dca41df | ||
![]() |
64e92cb8e1 | ||
![]() |
838c621d00 | ||
![]() |
59106bba1c | ||
![]() |
19b38c83b6 | ||
![]() |
40f187e4b4 | ||
![]() |
de0e08397e | ||
![]() |
ed288fca46 | ||
![]() |
4acd1b970c | ||
![]() |
01c784775e | ||
![]() |
fa790bd73c | ||
![]() |
0c982a7994 | ||
![]() |
f84f44bf6b | ||
![]() |
53953dd377 | ||
![]() |
b329806859 | ||
![]() |
2b54b1ebbe | ||
![]() |
91dfaab93f | ||
![]() |
19d71266ec | ||
![]() |
b956861442 | ||
![]() |
f6116a121a | ||
![]() |
7b9cfd1964 | ||
![]() |
d164ce221a | ||
![]() |
3d51b36e99 | ||
![]() |
31ae260db4 | ||
![]() |
f9fd8badec | ||
![]() |
ab723fc8ff | ||
![]() |
5336d71c22 | ||
![]() |
08470140ac | ||
![]() |
d38aba92fe | ||
![]() |
28b75f7d29 | ||
![]() |
286273c10e | ||
![]() |
12cb803486 | ||
![]() |
9fb4ed91b6 | ||
![]() |
76e4abe428 | ||
![]() |
6f81b5792b | ||
![]() |
212bd943f5 | ||
![]() |
278eaf47ad | ||
![]() |
0270267e08 | ||
![]() |
261ca326c5 | ||
![]() |
aebf822c3c | ||
![]() |
c3e72a87e5 | ||
![]() |
af1c13524c | ||
![]() |
a8ed6494c3 | ||
![]() |
bd90ef38b0 | ||
![]() |
aebda58314 | ||
![]() |
d8ad18383d | ||
![]() |
7edbaa3f2c | ||
![]() |
0b1695124b | ||
![]() |
06718c5df3 | ||
![]() |
b0c6106882 | ||
![]() |
38e1efafc2 | ||
![]() |
4910b2cdc0 | ||
![]() |
544d7aa2dc | ||
![]() |
1b302e879a | ||
![]() |
676a3872a5 | ||
![]() |
f991e1f023 | ||
![]() |
0e020f778c | ||
![]() |
71bc177e39 | ||
![]() |
47f4dabb98 | ||
![]() |
84f8beb884 | ||
![]() |
5a8a250a1a | ||
![]() |
1ecb7c3fbf | ||
![]() |
7dd428534e | ||
![]() |
b5db8a6cbe | ||
![]() |
6cbf77b3f0 | ||
![]() |
2b27e33784 | ||
![]() |
2a9963424b | ||
![]() |
15583f7c64 | ||
![]() |
0462349902 | ||
![]() |
df07fa35d8 | ||
![]() |
27de9f02e0 | ||
![]() |
27371db225 | ||
![]() |
08a141328d | ||
![]() |
129cf8ea72 | ||
![]() |
96b923b610 | ||
![]() |
14094b5094 | ||
![]() |
7f94aaf49f | ||
![]() |
7c0cffdca8 | ||
![]() |
7a8f05f736 | ||
![]() |
b1f2efd218 | ||
![]() |
51055d7e79 | ||
![]() |
b0df3b2da3 | ||
![]() |
dd6e88889a | ||
![]() |
7e7764de5e | ||
![]() |
772bd20f7d | ||
![]() |
4601a42aee | ||
![]() |
e67660f326 | ||
![]() |
fe14525a97 | ||
![]() |
9e088ac2b8 | ||
![]() |
5bb9e1a932 | ||
![]() |
9653fa64fe | ||
![]() |
a913a16db4 | ||
![]() |
84ab67cca3 | ||
![]() |
39c734963f | ||
![]() |
a15571c8ee | ||
![]() |
8811b454bb | ||
![]() |
0fdc930444 | ||
![]() |
aeb9d12a16 | ||
![]() |
d051b4b993 | ||
![]() |
e25bc2c1a5 | ||
![]() |
683a5b8403 | ||
![]() |
7d3e6b0f27 | ||
![]() |
37f795ea82 | ||
![]() |
7cf500bc2d | ||
![]() |
473a4a7680 | ||
![]() |
d3298391b2 | ||
![]() |
b565f47d18 | ||
![]() |
e7f5d4d441 | ||
![]() |
5581529bfb | ||
![]() |
16f65cee58 | ||
![]() |
5dbe1efa9e | ||
![]() |
8adcd35c75 | ||
![]() |
2d05744333 | ||
![]() |
45317394da | ||
![]() |
d2fb779929 | ||
![]() |
af1b5b5102 | ||
![]() |
757f223d8a | ||
![]() |
94e37ca7d2 | ||
![]() |
2f198c4c06 | ||
![]() |
f181855c5e | ||
![]() |
b36de302f7 | ||
![]() |
5f99955f31 | ||
![]() |
4a406be1bf | ||
![]() |
d25a423888 | ||
![]() |
b5615d5e91 | ||
![]() |
7da71f792b | ||
![]() |
21526588be | ||
![]() |
f11130d59e | ||
![]() |
dfb7636138 | ||
![]() |
7b66209bca | ||
![]() |
75f29b6997 | ||
![]() |
e5133e78f5 | ||
![]() |
e677c8dccd | ||
![]() |
0038dda3c3 | ||
![]() |
6c932722e4 | ||
![]() |
e43720b83a | ||
![]() |
a645f704b8 | ||
![]() |
36a5a83b22 | ||
![]() |
cabc541a6f | ||
![]() |
7545a8929b | ||
![]() |
ee4f0c40bc | ||
![]() |
219565c15e | ||
![]() |
8260bb3d2e | ||
![]() |
31ece8cbd2 | ||
![]() |
d9ca3c734a | ||
![]() |
be8c58d51e | ||
![]() |
7335754b23 | ||
![]() |
70f2d516a0 | ||
![]() |
1780986270 | ||
![]() |
320cbc09f4 | ||
![]() |
b04b37074b | ||
![]() |
e926b0df1e | ||
![]() |
73790cf2a4 | ||
![]() |
0ecd35e24c | ||
![]() |
847c1252e2 | ||
![]() |
457bc55a4d | ||
![]() |
40cd64ab19 | ||
![]() |
5088e87195 | ||
![]() |
74bbd7727d | ||
![]() |
f2ecca3b6a | ||
![]() |
49337d9667 | ||
![]() |
513ebf67b7 | ||
![]() |
e4b32e4e31 | ||
![]() |
943e165f1f | ||
![]() |
ceac959c29 | ||
![]() |
0108fb938e | ||
![]() |
b64bf88272 | ||
![]() |
2f01900e83 | ||
![]() |
f52c7a1d5c | ||
![]() |
2a49ffcb23 | ||
![]() |
709246ac6a | ||
![]() |
5fffe03312 | ||
![]() |
4d711050bb | ||
![]() |
3e556a4b53 | ||
![]() |
13d6f37a84 | ||
![]() |
3ba2fa6c07 | ||
![]() |
ba3d2b3fef | ||
![]() |
08e6f30b68 | ||
![]() |
621a9aedba | ||
![]() |
545d37c104 | ||
![]() |
e8d29dc3d6 | ||
![]() |
c18c6b31ef | ||
![]() |
79078c390d | ||
![]() |
1381c92d76 | ||
![]() |
e7254b809a | ||
![]() |
b650119d4e | ||
![]() |
62e1383a5b | ||
![]() |
0f43554db8 | ||
![]() |
ffecefbec2 | ||
![]() |
b8d02d9a43 | ||
![]() |
fd133459f8 | ||
![]() |
d8321cd0fd | ||
![]() |
2822919df6 | ||
![]() |
6b6d66c7ce | ||
![]() |
5eb064d2d1 | ||
![]() |
7b09e6c1f2 | ||
![]() |
6acfa0cc68 | ||
![]() |
78964d0a90 | ||
![]() |
64a006b665 | ||
![]() |
e41181d4c3 | ||
![]() |
ac94afa4b1 | ||
![]() |
29bda3bb5c | ||
![]() |
b8c559ed32 | ||
![]() |
a5524c09c5 | ||
![]() |
563f4d845a |
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
@ -19,6 +19,7 @@ body:
|
||||
options:
|
||||
- Media3 main branch
|
||||
- Media3 pre-release (alpha, beta or RC not in this list)
|
||||
- Media3 1.6.0
|
||||
- Media3 1.5.1
|
||||
- Media3 1.5.0
|
||||
- Media3 1.4.1
|
||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -4,6 +4,7 @@ gen
|
||||
libs
|
||||
obj
|
||||
lint.xml
|
||||
.kotlin
|
||||
|
||||
# IntelliJ IDEA
|
||||
.idea
|
||||
@ -68,6 +69,7 @@ libraries/decoder_opus/src/main/jni/libopus
|
||||
|
||||
# FLAC decoder extension
|
||||
libraries/decoder_flac/src/main/jni/flac
|
||||
libraries/decoder_flac/src/main/jni/libflac
|
||||
|
||||
# FFmpeg decoder extension
|
||||
libraries/decoder_ffmpeg/src/main/jni/ffmpeg
|
||||
@ -80,3 +82,9 @@ libraries/datasource_cronet/libs/*
|
||||
|
||||
# MIDI decoder extension
|
||||
libraries/decoder_midi/lib
|
||||
|
||||
# IAMF decoder extension
|
||||
libraries/decoder_iamf/src/main/jni/libiamf
|
||||
|
||||
# MPEG-H decoder extension
|
||||
libraries/decoder_mpegh/src/main/jni/libmpegh
|
||||
|
420
RELEASENOTES.md
420
RELEASENOTES.md
@ -1,5 +1,375 @@
|
||||
# Release notes
|
||||
|
||||
## 1.6
|
||||
|
||||
### 1.6.0 (2025-03-26)
|
||||
|
||||
This release includes the following changes since the
|
||||
[1.5.1 release](#151-2024-12-19):
|
||||
|
||||
* Common Library:
|
||||
* Add `AudioManagerCompat` and `AudioFocusRequestCompat` to replace the
|
||||
equivalent classes in `androidx.media`.
|
||||
* Upgrade Kotlin from 1.9.20 to 2.0.20 and use Compose Compiler Gradle
|
||||
plugin. Upgrade KotlinX Coroutines library from 1.8.1 to 1.9.0.
|
||||
* Remove `Format.toBundle(boolean excludeMetadata)` method, use
|
||||
`Format.toBundle()` instead.
|
||||
* Fix bug in `SimpleBasePlayer` where setting a new
|
||||
`currentMediaItemIndex` in `State` after `setPlaylist` with `null`
|
||||
`MediaMetadata` does not reevaluate the metadata
|
||||
([#1940](https://github.com/androidx/media/issues/1940)).
|
||||
* Change `SimpleBasePlayer.State` access from protected to public to make
|
||||
it easier to handle updates in other classes
|
||||
([#2128](https://github.com/androidx/media/issues/2128)).
|
||||
* ExoPlayer:
|
||||
* Add `MediaExtractorCompat`, a new class that provides equivalent
|
||||
features to platform `MediaExtractor`.
|
||||
* Add experimental 'ExoPlayer' pre-warming support for playback using
|
||||
`MediaCodecVideoRenderer`. You can configure `DefaultRenderersFactory`
|
||||
through `experimentalSetEnableMediaCodecVideoRendererPrewarming` to
|
||||
provide a secondary `MediaCodecVideoRenderer` to `ExoPlayer`. If
|
||||
enabled, `ExoPlayer` pre-processes the video of consecutive media items
|
||||
during playback to reduce media item transition latency.
|
||||
* Reduce default values for `bufferForPlaybackMs` and
|
||||
`bufferForPlaybackAfterRebufferMs` in `DefaultLoadControl` to 1000 and
|
||||
2000 ms respectively.
|
||||
* Initialize `DeviceInfo` and device volume asynchronously (if enabled
|
||||
using `setDeviceVolumeControlEnabled`). These values aren't available
|
||||
instantly after `ExoPlayer.Builder.build()`, and `Player.Listener`
|
||||
notifies changes through `onDeviceInfoChanged` and
|
||||
`onDeviceVolumeChanged`.
|
||||
* Initial audio session id is no longer immediately available after
|
||||
creating the player. You can use
|
||||
`AnalyticsListener.onAudioSessionIdChanged` to listen to the initial
|
||||
update if required.
|
||||
* Consider language when selecting a video track. By default, select a
|
||||
'main' video track that matches the language of the selected audio
|
||||
track, if available. Explicit video language preferences can be
|
||||
expressed with
|
||||
`TrackSelectionParameters.Builder.setPreferredVideoLanguage(s)`.
|
||||
* Add `selectedAudioLanguage` parameter to
|
||||
`DefaultTrackSelector.selectVideoTrack()` method.
|
||||
* Add `retryCount` parameter to `MediaSourceEventListener.onLoadStarted`
|
||||
and corresponding `MediaSourceEventListener.EventDispatcher` methods.
|
||||
* Fix bug where playlist items or periods in multi-period DASH streams
|
||||
with durations that don't match the actual content could cause frame
|
||||
freezes at the end of the item
|
||||
([#1698](https://github.com/androidx/media/issues/1698)).
|
||||
* Move `BasePreloadManager.Listener` to a top-level
|
||||
`PreloadManagerListener`.
|
||||
* `RenderersFactory.createSecondaryRenderer` can be implemented to provide
|
||||
secondary renderers for pre-warming. Pre-warming enables quicker media
|
||||
item transitions during playback.
|
||||
* Enable sending `CmcdData` for manifest requests in adaptive streaming
|
||||
formats DASH, HLS, and SmoothStreaming
|
||||
([#1951](https://github.com/androidx/media/issues/1951)).
|
||||
* Provide `MediaCodecInfo` of the codec that will be initialized in
|
||||
`MediaCodecRenderer.onReadyToInitializeCodec`
|
||||
([#1963](https://github.com/androidx/media/pull/1963)).
|
||||
* Change `AdsMediaSource` to allow the `AdPlaybackStates` to grow by
|
||||
appending ad groups. Invalid modifications are detected and throw an
|
||||
exception.
|
||||
* Fix issue where additional decode-only frames may be displayed in quick
|
||||
succession when transitioning to content media after a mid-roll ad.
|
||||
* Make `DefaultRenderersFactory` add two `MetadataRenderer` instances to
|
||||
enable apps to receive two different schemes of metadata by default.
|
||||
* Reevaluate whether the ongoing load of a chunk should be cancelled when
|
||||
playback is paused
|
||||
([#1785](https://github.com/androidx/media/pull/1785)).
|
||||
* Add option to `ClippingMediaSource` to allow clipping in unseekable
|
||||
media.
|
||||
* Fix bug where seeking with pre-warming could block following media item
|
||||
transition.
|
||||
* Fix a bug where `ExoPlayer.isLoading()` remains `true` while it has
|
||||
transitioned to `STATE_IDLE` or `STATE_ENDED`
|
||||
([#2133](https://github.com/androidx/media/issues/2133)).
|
||||
* Add `lastRebufferRealtimeMs` to `LoadControl.Parameter`
|
||||
([#2113](https://github.com/androidx/media/pull/2113)).
|
||||
* Transformer:
|
||||
* Add support for transmuxing into alternative backward compatible
|
||||
formats.
|
||||
* Add support for transcoding and transmuxing Dolby Vision (profile 8)
|
||||
format.
|
||||
* Update parameters of `VideoFrameProcessor.registerInputStream` and
|
||||
`VideoFrameProcessor.Listener.onInputStreamRegistered` to use `Format`.
|
||||
* Generate HDR static metadata when using `DefaultEncoderFactory`.
|
||||
* Enable support for Android platform diagnostics using
|
||||
`MediaMetricsManager`. Transformer forwards editing events and
|
||||
performance data to the platform, which helps to provide system
|
||||
performance and debugging information on the device. This data may also
|
||||
be collected by Google
|
||||
[if sharing usage and diagnostics data is enabled](https://support.google.com/accounts/answer/6078260)
|
||||
by the user of the device. Apps can opt-out of contributing to platform
|
||||
diagnostics for Transformer with
|
||||
`Transformer.Builder.setUsePlatformDiagnostics(false)`.
|
||||
* Split `InAppMuxer` into `InAppMp4Muxer` and `InAppFragmentedMp4Muxer`.
|
||||
You use `InAppMp4Muxer` to produce a non-fragmented MP4 file, while
|
||||
`InAppFragmentedMp4Muxer` is for producing a fragmented MP4 file.
|
||||
* Move `Muxer` interface from `media3-muxer` to `media3-transformer`.
|
||||
* Add `MediaProjectionAssetLoader`, which provides media from a
|
||||
`MediaProjection` for screen recording, and add support for screen
|
||||
recording to the Transformer demo app.
|
||||
* Add `#getInputFormat()` to `Codec` interface.
|
||||
* Shift the responsibility to release the `GlObjectsProvider` onto the
|
||||
caller in `DefaultVideoFrameProcessor` and `DefaultVideoCompositor` when
|
||||
possible.
|
||||
* Extractors:
|
||||
* AVI: Fix handling of files with constant bitrate compressed audio where
|
||||
the stream header stores the number of bytes instead of the number of
|
||||
chunks.
|
||||
* Fix handling of NAL units with lengths expressed in 1 or 2 bytes (rather
|
||||
than 4).
|
||||
* Fix `ArrayIndexOutOfBoundsException` in MP4 edit lists when the edit
|
||||
list starts at a non-sync frame with no preceding sync frame
|
||||
([#2062](https://github.com/androidx/media/issues/2062)).
|
||||
* Fix issue where TS streams can get stuck on some devices
|
||||
([#2069](https://github.com/androidx/media/issues/2069)).
|
||||
* FLAC: Add support for 32-bit FLAC files. Previously these would fail to
|
||||
play with `IllegalStateException: Playback stuck buffering and not
|
||||
loading` ([#2197](https://github.com/androidx/media/issues/2197)).
|
||||
* Audio:
|
||||
* Fix `onAudioPositionAdvancing` to be called when playback resumes
|
||||
(previously it was called when playback was paused).
|
||||
* Don't bypass `SonicAudioProcessor` when `SpeedChangingAudioProcessor` is
|
||||
configured with default parameters.
|
||||
* Fix underflow in `Sonic#getOutputSize()` that could cause
|
||||
`DefaultAudioSink` to stall.
|
||||
* Fix `MediaCodecAudioRenderer.getDurationToProgressUs()` and
|
||||
`DecoderAudioRenderer.getDurationToProgressUs()` so that seeks correctly
|
||||
reset the provided durations.
|
||||
* Make `androidx.media3.common.audio.SonicAudioProcessor` final.
|
||||
* Add support for float PCM to `ChannelMappingAudioProcessor` and
|
||||
`TrimmingAudioProcessor`.
|
||||
* Video:
|
||||
* Change `MediaCodecVideoRenderer.shouldUsePlaceholderSurface` to
|
||||
protected so that applications can override to block usage of
|
||||
placeholder surfaces
|
||||
([#1905](https://github.com/androidx/media/pull/1905)).
|
||||
* Add experimental `ExoPlayer` AV1 sample dependency parsing to speed up
|
||||
seeking. Enable it with the new
|
||||
`DefaultRenderersFactory.experimentalSetParseAv1SampleDependencies` API.
|
||||
* Add experimental `ExoPlayer` API to drop late `MediaCodecVideoRenderer`
|
||||
decoder input buffers that are not depended on. Enable it with
|
||||
`DefaultRenderersFactory.experimentalSetLateThresholdToDropDecoderInputUs`.
|
||||
* Fix issue where a player without a surface was ready immediately and
|
||||
very slow decoding any pending frames
|
||||
([#1973](https://github.com/androidx/media/issues/1973)).
|
||||
* Exclude Xiaomi and OPPO devices from detached surface mode to avoid
|
||||
screen flickering
|
||||
([#2059](https://github.com/androidx/media/issues/2059)).
|
||||
* Text:
|
||||
* Add support for VobSub subtitles
|
||||
([#8260](https://github.com/google/ExoPlayer/issues/8260)).
|
||||
* Stop eagerly loading all subtitle files configured with
|
||||
`MediaItem.Builder.setSubtitleConfigurations`, and instead only load one
|
||||
if it is selected by track selection
|
||||
([#1721](https://github.com/androidx/media/issues/1721)).
|
||||
* TTML: Add support for referencing `tts:origin` and `tts:extent` using
|
||||
`style` ([#2953](https://github.com/google/ExoPlayer/issues/2953)).
|
||||
* Restrict WebVTT and SubRip timestamps to exactly 3 decimal places.
|
||||
Previously we incorrectly parsed any number of decimal places but always
|
||||
assumed the value was in milliseconds, leading to incorrect timestamps
|
||||
([#1997](https://github.com/androidx/media/issues/1997)).
|
||||
* Fix playback hanging when a playlist contains clipped items with CEA-608
|
||||
or CEA-708 captions.
|
||||
* Fix `IllegalStateException` when an SSA file contains a cue with zero
|
||||
duration (start and end time equal)
|
||||
([#2052](https://github.com/androidx/media/issues/2052)).
|
||||
* Suppress (and log) subtitle parsing errors when subtitles are muxed into
|
||||
the same container as audio and video
|
||||
([#2052](https://github.com/androidx/media/issues/2052)).
|
||||
* Fix handling of multi-byte UTF-8 characters in WebVTT files using CR
|
||||
line endings ([#2167](https://github.com/androidx/media/issues/2167)).
|
||||
* DRM:
|
||||
* Fix `MediaCodec$CryptoException: Operation not supported in this
|
||||
configuration` error when playing ClearKey content on API < 27 devices
|
||||
([#1732](https://github.com/androidx/media/issues/1732)).
|
||||
* Effect:
|
||||
* Moved the functionality of `OverlaySettings` into
|
||||
`StaticOverlaySettings`. `OverlaySettings` can be subclassed to allow
|
||||
dynamic overlay settings.
|
||||
* Muxers:
|
||||
* Moved `MuxerException` out of `Muxer` interface to avoid a very long
|
||||
fully qualified name.
|
||||
* Renamed `setSampleCopyEnabled()` method to `setSampleCopyingEnabled()`
|
||||
in both `Mp4Muxer.Builder` and `FragmentedMp4Muxer.Builder`.
|
||||
* `Mp4Muxer.addTrack()` and `FragmentedMp4Muxer.addTrack()` now return an
|
||||
`int` track ID instead of a `TrackToken`.
|
||||
* `Mp4Muxer` and `FragmentedMp4Muxer` no longer implement `Muxer`
|
||||
interface.
|
||||
* Disable `Mp4Muxer` sample batching and copying by default.
|
||||
* Fix a bug in `FragmentedMp4Muxer` that creates a lot of fragments when
|
||||
only audio track is written.
|
||||
* Session:
|
||||
* Keep foreground service state for an additional 10 minutes when playback
|
||||
pauses, stops or fails. This allows users to resume playback within this
|
||||
timeout without risking foreground service restrictions on various
|
||||
devices. Note that simply calling `player.pause()` can no longer be used
|
||||
to stop the foreground service before `stopSelf()` when overriding
|
||||
`onTaskRemoved`, use `MediaSessionService.pauseAllPlayersAndStopSelf()`
|
||||
instead.
|
||||
* Keep notification visible when playback enters an error or stopped
|
||||
state. The notification is only removed if the playlist is cleared or
|
||||
the player is released.
|
||||
* Improve handling of Android platform MediaSession actions ACTION_PLAY
|
||||
and ACTION_PAUSE to only set one of them according to the available
|
||||
commands and also accept if only one of them is set.
|
||||
* Add `Context` as a parameter to
|
||||
`MediaButtonReceiver.shouldStartForegroundService`
|
||||
([#1887](https://github.com/androidx/media/issues/1887)).
|
||||
* Fix bug where calling a `Player` method on a `MediaController` connected
|
||||
to a legacy session dropped changes from a pending update.
|
||||
* Make `MediaSession.setSessionActivity(PendingIntent)` accept null
|
||||
([#2109](https://github.com/androidx/media/issues/2109)).
|
||||
* Fix bug where a stale notification stays visible when the playlist is
|
||||
cleared ([#2211](https://github.com/androidx/media/issues/2211)).
|
||||
* UI:
|
||||
* Add state holders and composables to the `media3-ui-compose` module for
|
||||
`PlayerSurface`, `PresentationState`, `PlayPauseButtonState`,
|
||||
`NextButtonState`, `PreviousButtonState`, `RepeatButtonState`,
|
||||
`ShuffleButtonState` and `PlaybackSpeedState`.
|
||||
* Downloads:
|
||||
* Fix bug in `CacheWriter` that leaves data sources open and cache areas
|
||||
locked in case the data source throws an `Exception` other than
|
||||
`IOException`
|
||||
([#9760](https://github.com/google/ExoPlayer/issues/9760)).
|
||||
* HLS extension:
|
||||
* Add a first version of `HlsInterstitialsAdsLoader`. The ads loader reads
|
||||
the HLS interstitials of an HLS media playlist and maps them to the
|
||||
`AdPlaybackState` that is passed to the `AdsMediaSource`. This initial
|
||||
version only supports HLS VOD streams with `X-ASSET-URI` attributes.
|
||||
* Add `HlsInterstitialsAdsLoader.AdsMediaSourceFactory`. Apps can use it
|
||||
to create `AdsMediaSource` instances that use an
|
||||
`HlsInterstitialsAdsLoader` in a convenient and safe way.
|
||||
* Parse `SUPPLEMENTAL-CODECS` tag from HLS playlist to detect Dolby Vision
|
||||
formats ([#1785](https://github.com/androidx/media/pull/1785)).
|
||||
* Loosen the condition for seeking to sync positions in an HLS stream
|
||||
([#2209](https://github.com/androidx/media/issues/2209)).
|
||||
* DASH extension:
|
||||
* Add AC-4 Level-4 format support for DASH
|
||||
([#1898](https://github.com/androidx/media/pull/1898)).
|
||||
* Fix issue when calculating the update interval for ad insertion in
|
||||
multi-period live streams
|
||||
([#1698](https://github.com/androidx/media/issues/1698)).
|
||||
* Parse `scte214:supplementalCodecs` attribute from DASH manifest to
|
||||
detect Dolby Vision formats
|
||||
([#1785](https://github.com/androidx/media/pull/1785)).
|
||||
* Improve handling of period transitions in live streams where the period
|
||||
contains media samples beyond the declared period duration
|
||||
([#1698](https://github.com/androidx/media/issues/1698)).
|
||||
* Fix issue where adaptation sets marked with `adaptation-set-switching`
|
||||
but different languages or role flags are merged together
|
||||
([#2222](https://github.com/androidx/media/issues/2222)).
|
||||
* Decoder extensions (FFmpeg, VP9, AV1, etc.):
|
||||
* Add the MPEG-H decoder module which uses the native MPEG-H decoder
|
||||
module to decode MPEG-H audio
|
||||
([#1826](https://github.com/androidx/media/pull/1826)).
|
||||
* MIDI extension:
|
||||
* Plumb custom `AudioSink` and `AudioRendererEventListener` instances into
|
||||
`MidiRenderer`.
|
||||
* Cast extension:
|
||||
* Bump the `play-services-cast-framework` dependency to 21.5.0 to fix a
|
||||
`FLAG_MUTABLE` crash in apps targeting API 34+ on devices with Google
|
||||
Play services installed but disabled
|
||||
([#2178](https://github.com/androidx/media/issues/2178)).
|
||||
* Demo app:
|
||||
* Extend `demo-compose` with additional buttons and enhance
|
||||
`PlayerSurface` integration with scaling and shutter support.
|
||||
* Remove deprecated symbols:
|
||||
* Remove deprecated `AudioMixer.create()` method. Use
|
||||
`DefaultAudioMixer.Factory().create()` instead.
|
||||
* Remove the following deprecated `Transformer.Builder` methods:
|
||||
* `setTransformationRequest()`, use `setAudioMimeType()`,
|
||||
`setVideoMimeType()`, and `setHdrMode()` instead.
|
||||
* `setAudioProcessors()`, set the audio processor in an
|
||||
`EditedMediaItem.Builder.setEffects()`, and pass it to
|
||||
`Transformer.start()` instead.
|
||||
* `setVideoEffects()`, set video effect in an
|
||||
`EditedMediaItem.Builder.setEffects()`, and pass it to
|
||||
`Transformer.start()` instead.
|
||||
* `setRemoveAudio()`, use `EditedMediaItem.Builder.setRemoveAudio()`
|
||||
to remove the audio from the `EditedMediaItem` passed to
|
||||
`Transformer.start()` instead.
|
||||
* `setRemoveVideo()`, use `EditedMediaItem.Builder.setRemoveVideo()`
|
||||
to remove the video from the `EditedMediaItem` passed to
|
||||
`Transformer.start()` instead.
|
||||
* `setFlattenForSlowMotion()`, use
|
||||
`EditedMediaItem.Builder.setFlattenForSlowMotion()` to flatten the
|
||||
`EditedMediaItem` passed to `Transformer.start()` instead.
|
||||
* `setListener()`, use `addListener()`, `removeListener()` or
|
||||
`removeAllListeners()` instead.
|
||||
* Remove the following deprecated `Transformer.Listener` methods:
|
||||
* `onTransformationCompleted(MediaItem)`, use
|
||||
`onCompleted(Composition, ExportResult)` instead.
|
||||
* `onTransformationCompleted(MediaItem, TransformationResult)`, use
|
||||
`onCompleted(Composition, ExportResult)` instead.
|
||||
* `onTransformationError(MediaItem, Exception)`, use
|
||||
`onError(Composition, ExportResult, ExportException)` instead.
|
||||
* `onTransformationError(MediaItem, TransformationException)`, use
|
||||
`onError(Composition, ExportResult, ExportException)` instead.
|
||||
* `onTransformationError(MediaItem, TransformationResult,
|
||||
TransformationException)`, use `onError(Composition, ExportResult,
|
||||
ExportException)` instead.
|
||||
* `onFallbackApplied(MediaItem, TransformationRequest,
|
||||
TransformationRequest)`, use `onFallbackApplied(Composition,
|
||||
TransformationRequest, TransformationRequest)` instead.
|
||||
* Remove deprecated `TransformationResult` class. Use `ExportResult`
|
||||
instead.
|
||||
* Remove deprecated `TransformationException` class. Use `ExportException`
|
||||
instead.
|
||||
* Remove deprecated `Transformer.PROGRESS_STATE_NO_TRANSFORMATION`. Use
|
||||
`Transformer.PROGRESS_STATE_NOT_STARTED` instead.
|
||||
* Remove deprecated `Transformer.setListener()`. Use
|
||||
`Transformer.addListener()`, `Transformer.removeListener()` or
|
||||
`Transformer.removeAllListeners()` instead.
|
||||
* Remove deprecated `Transformer.startTransformation()`. Use
|
||||
`Transformer.start(MediaItem, String)` instead.
|
||||
* Remove deprecated `SingleFrameGlShaderProgram`. Use
|
||||
`BaseGlShaderProgram` instead.
|
||||
* Remove `Transformer.flattenForSlowMotion`. Use
|
||||
`EditedMediaItem.flattenForSlowMotion` instead.
|
||||
* Removed `ExoPlayer.VideoComponent`, `ExoPlayer.AudioComponent`,
|
||||
`ExoPlayer.TextComponent` and `ExoPlayer.DeviceComponent`.
|
||||
* Removed `androidx.media3.exoplayer.audio.SonicAudioProcessor`.
|
||||
* Removed the following deprecated `DownloadHelper` methods:
|
||||
* Constructor `DownloadHelper(MediaItem, @Nullable MediaSource,
|
||||
TrackSelectionParameters, RendererCapabilities[])`, use
|
||||
`DownloadHelper(MediaItem, @Nullable MediaSource,
|
||||
TrackSelectionParameters, RendererCapabilitiesList)` instead.
|
||||
* `getRendererCapabilities(RenderersFactory)`, equivalent
|
||||
functionality can be achieved by creating a
|
||||
`DefaultRendererCapabilitiesList` with a `RenderersFactory`, and
|
||||
calling `DefaultRendererCapabilitiesList.getRendererCapabilities()`.
|
||||
* Removed
|
||||
`PlayerNotificationManager.setMediaSessionToken(MediaSessionCompat)`
|
||||
method. Use
|
||||
`PlayerNotificationManager.setMediaSessionToken(MediaSession.Token)` and
|
||||
pass in `(MediaSession.Token) compatToken.getToken()` instead.
|
||||
|
||||
### 1.6.0-rc02 (2025-03-18)
|
||||
|
||||
Use the 1.6.0 [stable version](#160-2025-03-26).
|
||||
|
||||
### 1.6.0-rc01 (2025-03-12)
|
||||
|
||||
Use the 1.6.0 [stable version](#160-2025-03-26).
|
||||
|
||||
### 1.6.0-beta01 (2025-02-26)
|
||||
|
||||
Use the 1.6.0 [stable version](#160-2025-03-26).
|
||||
|
||||
### 1.6.0-alpha03 (2025-02-06)
|
||||
|
||||
Use the 1.6.0 [stable version](#160-2025-03-26).
|
||||
|
||||
### 1.6.0-alpha02 (2025-01-30)
|
||||
|
||||
Use the 1.6.0 [stable version](#160-2025-03-26).
|
||||
|
||||
### 1.6.0-alpha01 (2024-12-20)
|
||||
|
||||
Use the 1.6.0 [stable version](#160-2025-03-26).
|
||||
|
||||
## 1.5
|
||||
|
||||
### 1.5.1 (2024-12-19)
|
||||
@ -312,19 +682,19 @@ This release includes the following changes since the
|
||||
[#184](https://github.com/androidx/media/issues/184)).
|
||||
* Fix bug where the "None" choice in the text selection is not working if
|
||||
there are app-defined text track selection preferences.
|
||||
* DASH Extension:
|
||||
* DASH extension:
|
||||
* Add support for periods starting in the middle of a segment
|
||||
([#1440](https://github.com/androidx/media/issues/1440)).
|
||||
* Smooth Streaming Extension:
|
||||
* Smooth Streaming extension:
|
||||
* Fix a `Bad magic number for Bundle` error when playing SmoothStreaming
|
||||
streams with text tracks
|
||||
([#1779](https://github.com/androidx/media/issues/1779)).
|
||||
* RTSP Extension:
|
||||
* RTSP extension:
|
||||
* Fix user info removal for URLs that contain encoded @ characters
|
||||
([#1138](https://github.com/androidx/media/pull/1138)).
|
||||
* Fix crashing when parsing of RTP packets with header extensions
|
||||
([#1225](https://github.com/androidx/media/pull/1225)).
|
||||
* Decoder Extensions (FFmpeg, VP9, AV1, etc.):
|
||||
* Decoder extensions (FFmpeg, VP9, AV1, etc.):
|
||||
* Add the IAMF decoder module, which provides support for playback of MP4
|
||||
files containing IAMF tracks using the libiamf native library to
|
||||
synthesize audio.
|
||||
@ -333,7 +703,7 @@ This release includes the following changes since the
|
||||
binaural playback support is currently not available.
|
||||
* Add 16 KB page support for decoder extensions on Android 15
|
||||
([#1685](https://github.com/androidx/media/issues/1685)).
|
||||
* Cast Extension:
|
||||
* Cast extension:
|
||||
* Stop cleaning the timeline after the CastSession disconnects, which
|
||||
enables the sender app to resume playback locally after a disconnection.
|
||||
* Populate CastPlayer's `DeviceInfo` when a `Context` is provided. This
|
||||
@ -414,7 +784,7 @@ This release includes the following changes since the
|
||||
`MediaButtonReceiver` when deciding whether to ignore it to avoid a
|
||||
`ForegroundServiceDidNotStartInTimeException`
|
||||
([#1581](https://github.com/androidx/media/issues/1581)).
|
||||
* RTSP Extension:
|
||||
* RTSP extension:
|
||||
* Skip invalid Media Descriptions in SDP parsing
|
||||
([#1087](https://github.com/androidx/media/issues/1472)).
|
||||
|
||||
@ -759,12 +1129,12 @@ This release includes the following changes since the
|
||||
instances, which can eventually result in an app crashing with
|
||||
`IllegalStateException: Too many receivers, total of 1000, registered
|
||||
for pid` ([#1224](https://github.com/androidx/media/issues/1224)).
|
||||
* Cronet Extension:
|
||||
* Cronet extension:
|
||||
* Fix `SocketTimeoutException` in `CronetDataSource`. In some versions of
|
||||
Cronet, the request provided by the callback is not always the same.
|
||||
This leads to callback not completing and request timing out
|
||||
(https://issuetracker.google.com/328442628).
|
||||
* HLS Extension:
|
||||
* HLS extension:
|
||||
* Fix bug where pending EMSG samples waiting for a discontinuity were
|
||||
delegated in `HlsSampleStreamWrapper` with an incorrect offset causing
|
||||
an `IndexOutOfBoundsException` or an `IllegalArgumentException`
|
||||
@ -778,13 +1148,13 @@ This release includes the following changes since the
|
||||
* Fix bug where enabling CMCD for HLS live streams causes
|
||||
`ArrayIndexOutOfBoundsException`
|
||||
([#1395](https://github.com/androidx/media/issues/1395)).
|
||||
* DASH Extension:
|
||||
* DASH extension:
|
||||
* Fix bug where re-preparing a multi-period live stream can throw an
|
||||
`IndexOutOfBoundsException`
|
||||
([#1329](https://github.com/androidx/media/issues/1329)).
|
||||
* Add support for `dashif:Laurl` license urls
|
||||
([#1345](https://github.com/androidx/media/issues/1345)).
|
||||
* Cast Extension:
|
||||
* Cast extension:
|
||||
* Fix bug that converted the album title of the `MediaQueueItem` to the
|
||||
artist in the Media3 media item
|
||||
([#1255](https://github.com/androidx/media/pull/1255)).
|
||||
@ -932,13 +1302,13 @@ This release includes the following changes since the
|
||||
* Fallback to include audio track language name if `Locale` cannot
|
||||
identify a display name
|
||||
([#988](https://github.com/androidx/media/issues/988)).
|
||||
* DASH Extension:
|
||||
* DASH extension:
|
||||
* Populate all `Label` elements from the manifest into `Format.labels`
|
||||
([#1054](https://github.com/androidx/media/pull/1054)).
|
||||
* RTSP Extension:
|
||||
* RTSP extension:
|
||||
* Skip empty session information values (i-tags) in SDP parsing
|
||||
([#1087](https://github.com/androidx/media/issues/1087)).
|
||||
* Decoder Extensions (FFmpeg, VP9, AV1, MIDI, etc.):
|
||||
* Decoder extensions (FFmpeg, VP9, AV1, MIDI, etc.):
|
||||
* Disable the MIDI extension as a local dependency by default because it
|
||||
requires an additional Maven repository to be configured. Users who need
|
||||
this module from a local dependency
|
||||
@ -1091,12 +1461,12 @@ This release includes the following changes since the
|
||||
not transmitted between media controllers and sessions.
|
||||
* Add constructor to `MediaLibrarySession.Builder` that only takes a
|
||||
`Context` instead of a `MediaLibraryService`.
|
||||
* HLS Extension:
|
||||
* HLS extension:
|
||||
* Reduce `HlsMediaPeriod` to package-private visibility. This type
|
||||
shouldn't be directly depended on from outside the HLS package.
|
||||
* Resolve seeks to beginning of a segment more efficiently
|
||||
([#1031](https://github.com/androidx/media/pull/1031)).
|
||||
* Decoder Extensions (FFmpeg, VP9, AV1, MIDI, etc.):
|
||||
* Decoder extensions (FFmpeg, VP9, AV1, MIDI, etc.):
|
||||
* MIDI decoder: Ignore SysEx event messages
|
||||
([#710](https://github.com/androidx/media/pull/710)).
|
||||
* Test Utilities:
|
||||
@ -1194,16 +1564,16 @@ This release includes the following changes since the
|
||||
* Fix issue where the numbers in the fast forward button of the
|
||||
`PlayerControlView` were misaligned
|
||||
([#547](https://github.com/androidx/media/issues/547)).
|
||||
* DASH Extension:
|
||||
* DASH extension:
|
||||
* Parse "f800" as channel count of 5 for Dolby in DASH manifest
|
||||
([#688](https://github.com/androidx/media/issues/688)).
|
||||
* Decoder Extensions (FFmpeg, VP9, AV1, MIDI, etc.):
|
||||
* Decoder extensions (FFmpeg, VP9, AV1, MIDI, etc.):
|
||||
* MIDI: Fix issue where seeking forward skips the Program Change events
|
||||
([#704](https://github.com/androidx/media/issues/704)).
|
||||
* Migrate to FFmpeg 6.0 and update supported NDK to `r26b`
|
||||
([#707](https://github.com/androidx/media/pull/707),
|
||||
[#867](https://github.com/androidx/media/pull/867)).
|
||||
* Cast Extension:
|
||||
* Cast extension:
|
||||
* Sanitize creation of a `Timeline` to not crash the app when loading
|
||||
media fails on the cast device
|
||||
([#708](https://github.com/androidx/media/issues/708)).
|
||||
@ -1441,11 +1811,11 @@ This release includes the following changes since the
|
||||
add `dataSync` as `foregroundServiceType` in the manifest and add the
|
||||
`FOREGROUND_SERVICE_DATA_SYNC` permission
|
||||
([#11239](https://github.com/google/ExoPlayer/issues/11239)).
|
||||
* HLS Extension:
|
||||
* HLS extension:
|
||||
* Refresh the HLS live playlist with an interval calculated from the last
|
||||
load start time rather than the last load completed time
|
||||
([#663](https://github.com/androidx/media/issues/663)).
|
||||
* DASH Extension:
|
||||
* DASH extension:
|
||||
* Allow multiple of the same DASH identifier in segment template url.
|
||||
* Add experimental support for parsing subtitles during extraction. This
|
||||
has better support for merging overlapping subtitles, including
|
||||
@ -1453,7 +1823,7 @@ This release includes the following changes since the
|
||||
can enable this using
|
||||
`DashMediaSource.Factory.experimentalParseSubtitlesDuringExtraction()`
|
||||
([#288](https://github.com/androidx/media/issues/288)).
|
||||
* RTSP Extension:
|
||||
* RTSP extension:
|
||||
* Fix a race condition that could lead to `IndexOutOfBoundsException` when
|
||||
falling back to TCP, or playback hanging in some situations.
|
||||
* Check state in RTSP setup when returning loading state of
|
||||
@ -1464,7 +1834,7 @@ This release includes the following changes since the
|
||||
* Use RTSP Setup Response timeout value in time interval of sending
|
||||
keep-alive RTSP Options requests
|
||||
([#662](https://github.com/androidx/media/issues/662)).
|
||||
* Decoder Extensions (FFmpeg, VP9, AV1, MIDI, etc.):
|
||||
* Decoder extensions (FFmpeg, VP9, AV1, MIDI, etc.):
|
||||
* Release the MIDI decoder module, which provides support for playback of
|
||||
standard MIDI files using the Jsyn library to synthesize audio.
|
||||
* Add `DecoderOutputBuffer.shouldBeSkipped` to directly mark output
|
||||
@ -1741,20 +2111,20 @@ This release contains the following changes since the
|
||||
* Add Util methods `shouldShowPlayButton` and
|
||||
`handlePlayPauseButtonAction` to write custom UI elements with a
|
||||
play/pause button.
|
||||
* RTSP Extension:
|
||||
* RTSP extension:
|
||||
* For MPEG4-LATM, use default profile-level-id value if absent in Describe
|
||||
Response SDP message
|
||||
([#302](https://github.com/androidx/media/issues/302)).
|
||||
* Use base Uri for relative path resolution from the RTSP session if
|
||||
present in DESCRIBE response header
|
||||
([#11160](https://github.com/google/ExoPlayer/issues/11160)).
|
||||
* DASH Extension:
|
||||
* DASH extension:
|
||||
* Remove the media time offset from `MediaLoadData.startTimeMs` and
|
||||
`MediaLoadData.endTimeMs` for multi period DASH streams.
|
||||
* Fix a bug where re-preparing a multi-period live Dash media source
|
||||
produced a `IndexOutOfBoundsException`
|
||||
([#10838](https://github.com/google/ExoPlayer/issues/10838)).
|
||||
* HLS Extension:
|
||||
* HLS extension:
|
||||
* Add
|
||||
`HlsMediaSource.Factory.setTimestampAdjusterInitializationTimeoutMs(long)`
|
||||
to set a timeout for the loading thread to wait for the
|
||||
|
108
api.txt
108
api.txt
@ -464,6 +464,7 @@ package androidx.media3.common {
|
||||
field @Nullable public final CharSequence description;
|
||||
field @Nullable public final Integer discNumber;
|
||||
field @Nullable public final CharSequence displayTitle;
|
||||
field @Nullable public final Long durationMs;
|
||||
field @Nullable public final android.os.Bundle extras;
|
||||
field @Deprecated @Nullable @androidx.media3.common.MediaMetadata.FolderType public final Integer folderType;
|
||||
field @Nullable public final CharSequence genre;
|
||||
@ -502,6 +503,7 @@ package androidx.media3.common {
|
||||
method public androidx.media3.common.MediaMetadata.Builder setDescription(@Nullable CharSequence);
|
||||
method public androidx.media3.common.MediaMetadata.Builder setDiscNumber(@Nullable Integer);
|
||||
method public androidx.media3.common.MediaMetadata.Builder setDisplayTitle(@Nullable CharSequence);
|
||||
method public androidx.media3.common.MediaMetadata.Builder setDurationMs(@Nullable Long);
|
||||
method public androidx.media3.common.MediaMetadata.Builder setExtras(@Nullable android.os.Bundle);
|
||||
method @Deprecated public androidx.media3.common.MediaMetadata.Builder setFolderType(@Nullable @androidx.media3.common.MediaMetadata.FolderType Integer);
|
||||
method public androidx.media3.common.MediaMetadata.Builder setGenre(@Nullable CharSequence);
|
||||
@ -636,6 +638,7 @@ package androidx.media3.common {
|
||||
field public static final int ERROR_CODE_DECODING_FAILED = 4003; // 0xfa3
|
||||
field public static final int ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES = 4004; // 0xfa4
|
||||
field public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 4005; // 0xfa5
|
||||
field public static final int ERROR_CODE_DECODING_RESOURCES_RECLAIMED = 4006; // 0xfa6
|
||||
field public static final int ERROR_CODE_DISCONNECTED = -100; // 0xffffff9c
|
||||
field public static final int ERROR_CODE_DRM_CONTENT_ERROR = 6003; // 0x1773
|
||||
field public static final int ERROR_CODE_DRM_DEVICE_REVOKED = 6007; // 0x1777
|
||||
@ -676,7 +679,7 @@ package androidx.media3.common {
|
||||
field public final long timestampMs;
|
||||
}
|
||||
|
||||
@IntDef(open=true, value={androidx.media3.common.PlaybackException.ERROR_CODE_INVALID_STATE, androidx.media3.common.PlaybackException.ERROR_CODE_BAD_VALUE, androidx.media3.common.PlaybackException.ERROR_CODE_PERMISSION_DENIED, androidx.media3.common.PlaybackException.ERROR_CODE_NOT_SUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DISCONNECTED, androidx.media3.common.PlaybackException.ERROR_CODE_AUTHENTICATION_EXPIRED, androidx.media3.common.PlaybackException.ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.common.PlaybackException.ERROR_CODE_CONCURRENT_STREAM_LIMIT, androidx.media3.common.PlaybackException.ERROR_CODE_PARENTAL_CONTROL_RESTRICTED, androidx.media3.common.PlaybackException.ERROR_CODE_NOT_AVAILABLE_IN_REGION, androidx.media3.common.PlaybackException.ERROR_CODE_SKIP_LIMIT_REACHED, androidx.media3.common.PlaybackException.ERROR_CODE_SETUP_REQUIRED, androidx.media3.common.PlaybackException.ERROR_CODE_END_OF_PLAYLIST, androidx.media3.common.PlaybackException.ERROR_CODE_CONTENT_ALREADY_PLAYING, androidx.media3.common.PlaybackException.ERROR_CODE_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_REMOTE_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW, androidx.media3.common.PlaybackException.ERROR_CODE_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK, androidx.media3.common.PlaybackException.ERROR_CODE_IO_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, androidx.media3.common.PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, androidx.media3.common.PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NO_PERMISSION, androidx.media3.common.PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SCHEME_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_CONTENT_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DISALLOWED_OPERATION, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DEVICE_REVOKED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_EXPIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface PlaybackException.ErrorCode {
|
||||
@IntDef(open=true, value={androidx.media3.common.PlaybackException.ERROR_CODE_INVALID_STATE, androidx.media3.common.PlaybackException.ERROR_CODE_BAD_VALUE, androidx.media3.common.PlaybackException.ERROR_CODE_PERMISSION_DENIED, androidx.media3.common.PlaybackException.ERROR_CODE_NOT_SUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DISCONNECTED, androidx.media3.common.PlaybackException.ERROR_CODE_AUTHENTICATION_EXPIRED, androidx.media3.common.PlaybackException.ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.common.PlaybackException.ERROR_CODE_CONCURRENT_STREAM_LIMIT, androidx.media3.common.PlaybackException.ERROR_CODE_PARENTAL_CONTROL_RESTRICTED, androidx.media3.common.PlaybackException.ERROR_CODE_NOT_AVAILABLE_IN_REGION, androidx.media3.common.PlaybackException.ERROR_CODE_SKIP_LIMIT_REACHED, androidx.media3.common.PlaybackException.ERROR_CODE_SETUP_REQUIRED, androidx.media3.common.PlaybackException.ERROR_CODE_END_OF_PLAYLIST, androidx.media3.common.PlaybackException.ERROR_CODE_CONTENT_ALREADY_PLAYING, androidx.media3.common.PlaybackException.ERROR_CODE_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_REMOTE_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW, androidx.media3.common.PlaybackException.ERROR_CODE_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK, androidx.media3.common.PlaybackException.ERROR_CODE_IO_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, androidx.media3.common.PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, androidx.media3.common.PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NO_PERMISSION, androidx.media3.common.PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_RESOURCES_RECLAIMED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SCHEME_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_CONTENT_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DISALLOWED_OPERATION, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DEVICE_REVOKED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_EXPIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface PlaybackException.ErrorCode {
|
||||
}
|
||||
|
||||
public final class PlaybackParameters {
|
||||
@ -1093,12 +1096,13 @@ package androidx.media3.common {
|
||||
public class TrackSelectionParameters {
|
||||
method public androidx.media3.common.TrackSelectionParameters.Builder buildUpon();
|
||||
method public static androidx.media3.common.TrackSelectionParameters fromBundle(android.os.Bundle);
|
||||
method public static androidx.media3.common.TrackSelectionParameters getDefaults(android.content.Context);
|
||||
method @Deprecated public static androidx.media3.common.TrackSelectionParameters getDefaults(android.content.Context);
|
||||
method @CallSuper public android.os.Bundle toBundle();
|
||||
field public final com.google.common.collect.ImmutableSet<java.lang.Integer> disabledTrackTypes;
|
||||
field public final boolean forceHighestSupportedBitrate;
|
||||
field public final boolean forceLowestBitrate;
|
||||
field @androidx.media3.common.C.SelectionFlags public final int ignoredTextSelectionFlags;
|
||||
field public final boolean isViewportSizeLimitedByPhysicalDisplaySize;
|
||||
field public final int maxAudioBitrate;
|
||||
field public final int maxAudioChannelCount;
|
||||
field public final int maxVideoBitrate;
|
||||
@ -1118,13 +1122,15 @@ package androidx.media3.common {
|
||||
field public final com.google.common.collect.ImmutableList<java.lang.String> preferredVideoMimeTypes;
|
||||
field @androidx.media3.common.C.RoleFlags public final int preferredVideoRoleFlags;
|
||||
field public final boolean selectUndeterminedTextLanguage;
|
||||
field public final boolean usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager;
|
||||
field public final int viewportHeight;
|
||||
field public final boolean viewportOrientationMayChange;
|
||||
field public final int viewportWidth;
|
||||
}
|
||||
|
||||
public static class TrackSelectionParameters.Builder {
|
||||
ctor public TrackSelectionParameters.Builder(android.content.Context);
|
||||
ctor public TrackSelectionParameters.Builder();
|
||||
ctor @Deprecated @com.google.errorprone.annotations.InlineMe(replacement="this()") public TrackSelectionParameters.Builder(android.content.Context);
|
||||
method public androidx.media3.common.TrackSelectionParameters.Builder addOverride(androidx.media3.common.TrackSelectionOverride);
|
||||
method public androidx.media3.common.TrackSelectionParameters build();
|
||||
method public androidx.media3.common.TrackSelectionParameters.Builder clearOverride(androidx.media3.common.TrackGroup);
|
||||
@ -1151,7 +1157,8 @@ package androidx.media3.common {
|
||||
method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredAudioMimeTypes(java.lang.String...);
|
||||
method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredAudioRoleFlags(@androidx.media3.common.C.RoleFlags int);
|
||||
method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredTextLanguage(@Nullable String);
|
||||
method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(android.content.Context);
|
||||
method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings();
|
||||
method @Deprecated public androidx.media3.common.TrackSelectionParameters.Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(android.content.Context);
|
||||
method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredTextLanguages(java.lang.String...);
|
||||
method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredTextRoleFlags(@androidx.media3.common.C.RoleFlags int);
|
||||
method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredVideoMimeType(@Nullable String);
|
||||
@ -1160,7 +1167,8 @@ package androidx.media3.common {
|
||||
method public androidx.media3.common.TrackSelectionParameters.Builder setSelectUndeterminedTextLanguage(boolean);
|
||||
method public androidx.media3.common.TrackSelectionParameters.Builder setTrackTypeDisabled(@androidx.media3.common.C.TrackType int, boolean);
|
||||
method public androidx.media3.common.TrackSelectionParameters.Builder setViewportSize(int, int, boolean);
|
||||
method public androidx.media3.common.TrackSelectionParameters.Builder setViewportSizeToPhysicalDisplaySize(android.content.Context, boolean);
|
||||
method @Deprecated public androidx.media3.common.TrackSelectionParameters.Builder setViewportSizeToPhysicalDisplaySize(android.content.Context, boolean);
|
||||
method public androidx.media3.common.TrackSelectionParameters.Builder setViewportSizeToPhysicalDisplaySize(boolean);
|
||||
}
|
||||
|
||||
public final class Tracks {
|
||||
@ -1480,7 +1488,82 @@ package androidx.media3.exoplayer.util {
|
||||
package androidx.media3.session {
|
||||
|
||||
public final class CommandButton {
|
||||
field public static final int ICON_ALBUM = 57369; // 0xe019
|
||||
field public static final int ICON_ARTIST = 57370; // 0xe01a
|
||||
field public static final int ICON_BLOCK = 57675; // 0xe14b
|
||||
field public static final int ICON_BOOKMARK_FILLED = 1042534; // 0xfe866
|
||||
field public static final int ICON_BOOKMARK_UNFILLED = 59494; // 0xe866
|
||||
field public static final int ICON_CHECK_CIRCLE_FILLED = 1042540; // 0xfe86c
|
||||
field public static final int ICON_CHECK_CIRCLE_UNFILLED = 59500; // 0xe86c
|
||||
field public static final int ICON_CLOSED_CAPTIONS = 57372; // 0xe01c
|
||||
field public static final int ICON_CLOSED_CAPTIONS_OFF = 61916; // 0xf1dc
|
||||
field public static final int ICON_FAST_FORWARD = 57375; // 0xe01f
|
||||
field public static final int ICON_FEED = 57573; // 0xe0e5
|
||||
field public static final int ICON_FLAG_FILLED = 1040723; // 0xfe153
|
||||
field public static final int ICON_FLAG_UNFILLED = 57683; // 0xe153
|
||||
field public static final int ICON_HEART_FILLED = 1042557; // 0xfe87d
|
||||
field public static final int ICON_HEART_UNFILLED = 59517; // 0xe87d
|
||||
field public static final int ICON_MINUS = 57691; // 0xe15b
|
||||
field public static final int ICON_MINUS_CIRCLE_FILLED = 1040712; // 0xfe148
|
||||
field public static final int ICON_MINUS_CIRCLE_UNFILLED = 1040713; // 0xfe149
|
||||
field public static final int ICON_NEXT = 57412; // 0xe044
|
||||
field public static final int ICON_PAUSE = 57396; // 0xe034
|
||||
field public static final int ICON_PLAY = 57399; // 0xe037
|
||||
field public static final int ICON_PLAYBACK_SPEED = 57448; // 0xe068
|
||||
field public static final int ICON_PLAYBACK_SPEED_0_5 = 62690; // 0xf4e2
|
||||
field public static final int ICON_PLAYBACK_SPEED_0_8 = 1045730; // 0xff4e2
|
||||
field public static final int ICON_PLAYBACK_SPEED_1_0 = 61389; // 0xefcd
|
||||
field public static final int ICON_PLAYBACK_SPEED_1_2 = 62689; // 0xf4e1
|
||||
field public static final int ICON_PLAYBACK_SPEED_1_5 = 62688; // 0xf4e0
|
||||
field public static final int ICON_PLAYBACK_SPEED_1_8 = 1045728; // 0xff4e0
|
||||
field public static final int ICON_PLAYBACK_SPEED_2_0 = 62699; // 0xf4eb
|
||||
field public static final int ICON_PLAYLIST_ADD = 57403; // 0xe03b
|
||||
field public static final int ICON_PLAYLIST_REMOVE = 60288; // 0xeb80
|
||||
field public static final int ICON_PLUS = 57669; // 0xe145
|
||||
field public static final int ICON_PLUS_CIRCLE_FILLED = 1040711; // 0xfe147
|
||||
field public static final int ICON_PLUS_CIRCLE_UNFILLED = 57671; // 0xe147
|
||||
field public static final int ICON_PREVIOUS = 57413; // 0xe045
|
||||
field public static final int ICON_QUALITY = 58409; // 0xe429
|
||||
field public static final int ICON_QUEUE_ADD = 57436; // 0xe05c
|
||||
field public static final int ICON_QUEUE_NEXT = 57446; // 0xe066
|
||||
field public static final int ICON_QUEUE_REMOVE = 57447; // 0xe067
|
||||
field public static final int ICON_RADIO = 58654; // 0xe51e
|
||||
field public static final int ICON_REPEAT_ALL = 57408; // 0xe040
|
||||
field public static final int ICON_REPEAT_OFF = 1040448; // 0xfe040
|
||||
field public static final int ICON_REPEAT_ONE = 57409; // 0xe041
|
||||
field public static final int ICON_REWIND = 57376; // 0xe020
|
||||
field public static final int ICON_SETTINGS = 59576; // 0xe8b8
|
||||
field public static final int ICON_SHARE = 59405; // 0xe80d
|
||||
field public static final int ICON_SHUFFLE_OFF = 1040452; // 0xfe044
|
||||
field public static final int ICON_SHUFFLE_ON = 57411; // 0xe043
|
||||
field public static final int ICON_SHUFFLE_STAR = 1040451; // 0xfe043
|
||||
field public static final int ICON_SIGNAL = 61512; // 0xf048
|
||||
field public static final int ICON_SKIP_BACK = 57410; // 0xe042
|
||||
field public static final int ICON_SKIP_BACK_10 = 57433; // 0xe059
|
||||
field public static final int ICON_SKIP_BACK_15 = 1040473; // 0xfe059
|
||||
field public static final int ICON_SKIP_BACK_30 = 57434; // 0xe05a
|
||||
field public static final int ICON_SKIP_BACK_5 = 57435; // 0xe05b
|
||||
field public static final int ICON_SKIP_FORWARD = 63220; // 0xf6f4
|
||||
field public static final int ICON_SKIP_FORWARD_10 = 57430; // 0xe056
|
||||
field public static final int ICON_SKIP_FORWARD_15 = 1040470; // 0xfe056
|
||||
field public static final int ICON_SKIP_FORWARD_30 = 57431; // 0xe057
|
||||
field public static final int ICON_SKIP_FORWARD_5 = 57432; // 0xe058
|
||||
field public static final int ICON_STAR_FILLED = 1042488; // 0xfe838
|
||||
field public static final int ICON_STAR_UNFILLED = 59448; // 0xe838
|
||||
field public static final int ICON_STOP = 57415; // 0xe047
|
||||
field public static final int ICON_SUBTITLES = 57416; // 0xe048
|
||||
field public static final int ICON_SUBTITLES_OFF = 61298; // 0xef72
|
||||
field public static final int ICON_SYNC = 58919; // 0xe627
|
||||
field public static final int ICON_THUMB_DOWN_FILLED = 1042651; // 0xfe8db
|
||||
field public static final int ICON_THUMB_DOWN_UNFILLED = 59611; // 0xe8db
|
||||
field public static final int ICON_THUMB_UP_FILLED = 1042652; // 0xfe8dc
|
||||
field public static final int ICON_THUMB_UP_UNFILLED = 59612; // 0xe8dc
|
||||
field public static final int ICON_UNDEFINED = 0; // 0x0
|
||||
field public static final int ICON_VOLUME_DOWN = 57421; // 0xe04d
|
||||
field public static final int ICON_VOLUME_OFF = 57423; // 0xe04f
|
||||
field public static final int ICON_VOLUME_UP = 57424; // 0xe050
|
||||
field public final CharSequence displayName;
|
||||
field @androidx.media3.session.CommandButton.Icon public final int icon;
|
||||
field @DrawableRes public final int iconResId;
|
||||
field public final boolean isEnabled;
|
||||
field @androidx.media3.common.Player.Command public final int playerCommand;
|
||||
@ -1488,16 +1571,21 @@ package androidx.media3.session {
|
||||
}
|
||||
|
||||
public static final class CommandButton.Builder {
|
||||
ctor public CommandButton.Builder();
|
||||
ctor @Deprecated public CommandButton.Builder();
|
||||
ctor public CommandButton.Builder(@androidx.media3.session.CommandButton.Icon int);
|
||||
method public androidx.media3.session.CommandButton build();
|
||||
method public androidx.media3.session.CommandButton.Builder setCustomIconResId(@DrawableRes int);
|
||||
method public androidx.media3.session.CommandButton.Builder setDisplayName(CharSequence);
|
||||
method public androidx.media3.session.CommandButton.Builder setEnabled(boolean);
|
||||
method public androidx.media3.session.CommandButton.Builder setExtras(android.os.Bundle);
|
||||
method public androidx.media3.session.CommandButton.Builder setIconResId(@DrawableRes int);
|
||||
method @Deprecated public androidx.media3.session.CommandButton.Builder setIconResId(@DrawableRes int);
|
||||
method public androidx.media3.session.CommandButton.Builder setPlayerCommand(@androidx.media3.common.Player.Command int);
|
||||
method public androidx.media3.session.CommandButton.Builder setSessionCommand(androidx.media3.session.SessionCommand);
|
||||
}
|
||||
|
||||
@IntDef({androidx.media3.session.CommandButton.ICON_UNDEFINED, androidx.media3.session.CommandButton.ICON_PLAY, androidx.media3.session.CommandButton.ICON_PAUSE, androidx.media3.session.CommandButton.ICON_STOP, androidx.media3.session.CommandButton.ICON_NEXT, androidx.media3.session.CommandButton.ICON_PREVIOUS, androidx.media3.session.CommandButton.ICON_SKIP_FORWARD, androidx.media3.session.CommandButton.ICON_SKIP_FORWARD_5, androidx.media3.session.CommandButton.ICON_SKIP_FORWARD_10, androidx.media3.session.CommandButton.ICON_SKIP_FORWARD_15, androidx.media3.session.CommandButton.ICON_SKIP_FORWARD_30, androidx.media3.session.CommandButton.ICON_SKIP_BACK, androidx.media3.session.CommandButton.ICON_SKIP_BACK_5, androidx.media3.session.CommandButton.ICON_SKIP_BACK_10, androidx.media3.session.CommandButton.ICON_SKIP_BACK_15, androidx.media3.session.CommandButton.ICON_SKIP_BACK_30, androidx.media3.session.CommandButton.ICON_FAST_FORWARD, androidx.media3.session.CommandButton.ICON_REWIND, androidx.media3.session.CommandButton.ICON_REPEAT_ALL, androidx.media3.session.CommandButton.ICON_REPEAT_ONE, androidx.media3.session.CommandButton.ICON_REPEAT_OFF, androidx.media3.session.CommandButton.ICON_SHUFFLE_ON, androidx.media3.session.CommandButton.ICON_SHUFFLE_OFF, androidx.media3.session.CommandButton.ICON_SHUFFLE_STAR, androidx.media3.session.CommandButton.ICON_HEART_FILLED, androidx.media3.session.CommandButton.ICON_HEART_UNFILLED, androidx.media3.session.CommandButton.ICON_STAR_FILLED, androidx.media3.session.CommandButton.ICON_STAR_UNFILLED, androidx.media3.session.CommandButton.ICON_BOOKMARK_FILLED, androidx.media3.session.CommandButton.ICON_BOOKMARK_UNFILLED, androidx.media3.session.CommandButton.ICON_THUMB_UP_FILLED, androidx.media3.session.CommandButton.ICON_THUMB_UP_UNFILLED, androidx.media3.session.CommandButton.ICON_THUMB_DOWN_FILLED, androidx.media3.session.CommandButton.ICON_THUMB_DOWN_UNFILLED, androidx.media3.session.CommandButton.ICON_FLAG_FILLED, androidx.media3.session.CommandButton.ICON_FLAG_UNFILLED, androidx.media3.session.CommandButton.ICON_PLUS, androidx.media3.session.CommandButton.ICON_MINUS, androidx.media3.session.CommandButton.ICON_PLAYLIST_ADD, androidx.media3.session.CommandButton.ICON_PLAYLIST_REMOVE, androidx.media3.session.CommandButton.ICON_QUEUE_ADD, androidx.media3.session.CommandButton.ICON_QUEUE_NEXT, androidx.media3.session.CommandButton.ICON_QUEUE_REMOVE, androidx.media3.session.CommandButton.ICON_BLOCK, androidx.media3.session.CommandButton.ICON_PLUS_CIRCLE_FILLED, androidx.media3.session.CommandButton.ICON_PLUS_CIRCLE_UNFILLED, androidx.media3.session.CommandButton.ICON_MINUS_CIRCLE_FILLED, androidx.media3.session.CommandButton.ICON_MINUS_CIRCLE_UNFILLED, androidx.media3.session.CommandButton.ICON_CHECK_CIRCLE_FILLED, androidx.media3.session.CommandButton.ICON_CHECK_CIRCLE_UNFILLED, androidx.media3.session.CommandButton.ICON_PLAYBACK_SPEED, androidx.media3.session.CommandButton.ICON_PLAYBACK_SPEED_0_5, androidx.media3.session.CommandButton.ICON_PLAYBACK_SPEED_0_8, androidx.media3.session.CommandButton.ICON_PLAYBACK_SPEED_1_0, androidx.media3.session.CommandButton.ICON_PLAYBACK_SPEED_1_2, androidx.media3.session.CommandButton.ICON_PLAYBACK_SPEED_1_5, androidx.media3.session.CommandButton.ICON_PLAYBACK_SPEED_1_8, androidx.media3.session.CommandButton.ICON_PLAYBACK_SPEED_2_0, androidx.media3.session.CommandButton.ICON_SETTINGS, androidx.media3.session.CommandButton.ICON_QUALITY, androidx.media3.session.CommandButton.ICON_SUBTITLES, androidx.media3.session.CommandButton.ICON_SUBTITLES_OFF, androidx.media3.session.CommandButton.ICON_CLOSED_CAPTIONS, androidx.media3.session.CommandButton.ICON_CLOSED_CAPTIONS_OFF, androidx.media3.session.CommandButton.ICON_SYNC, androidx.media3.session.CommandButton.ICON_SHARE, androidx.media3.session.CommandButton.ICON_VOLUME_UP, androidx.media3.session.CommandButton.ICON_VOLUME_DOWN, androidx.media3.session.CommandButton.ICON_VOLUME_OFF, androidx.media3.session.CommandButton.ICON_ARTIST, androidx.media3.session.CommandButton.ICON_ALBUM, androidx.media3.session.CommandButton.ICON_RADIO, androidx.media3.session.CommandButton.ICON_SIGNAL, androidx.media3.session.CommandButton.ICON_FEED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface CommandButton.Icon {
|
||||
}
|
||||
|
||||
public final class LibraryResult<V> {
|
||||
method public static <V> androidx.media3.session.LibraryResult<V> ofError(@androidx.media3.session.LibraryResult.Code int);
|
||||
method public static <V> androidx.media3.session.LibraryResult<V> ofError(@androidx.media3.session.LibraryResult.Code int, @Nullable androidx.media3.session.MediaLibraryService.LibraryParams);
|
||||
@ -1596,6 +1684,7 @@ package androidx.media3.session {
|
||||
method public final long getCurrentPosition();
|
||||
method public final androidx.media3.common.Timeline getCurrentTimeline();
|
||||
method public final androidx.media3.common.Tracks getCurrentTracks();
|
||||
method public final com.google.common.collect.ImmutableList<androidx.media3.session.CommandButton> getCustomLayout();
|
||||
method public final androidx.media3.common.DeviceInfo getDeviceInfo();
|
||||
method @IntRange(from=0) public final int getDeviceVolume();
|
||||
method public final long getDuration();
|
||||
@ -1615,6 +1704,7 @@ package androidx.media3.session {
|
||||
method public final long getSeekBackIncrement();
|
||||
method public final long getSeekForwardIncrement();
|
||||
method @Nullable public final android.app.PendingIntent getSessionActivity();
|
||||
method public final android.os.Bundle getSessionExtras();
|
||||
method public final boolean getShuffleModeEnabled();
|
||||
method public final long getTotalBufferedDuration();
|
||||
method public final androidx.media3.common.TrackSelectionParameters getTrackSelectionParameters();
|
||||
@ -1697,6 +1787,7 @@ package androidx.media3.session {
|
||||
public static interface MediaController.Listener {
|
||||
method public default void onAvailableSessionCommandsChanged(androidx.media3.session.MediaController, androidx.media3.session.SessionCommands);
|
||||
method public default com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> onCustomCommand(androidx.media3.session.MediaController, androidx.media3.session.SessionCommand, android.os.Bundle);
|
||||
method public default void onCustomLayoutChanged(androidx.media3.session.MediaController, java.util.List<androidx.media3.session.CommandButton>);
|
||||
method public default void onDisconnected(androidx.media3.session.MediaController);
|
||||
method public default void onExtrasChanged(androidx.media3.session.MediaController, android.os.Bundle);
|
||||
method public default com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> onSetCustomLayout(androidx.media3.session.MediaController, java.util.List<androidx.media3.session.CommandButton>);
|
||||
@ -1759,6 +1850,7 @@ package androidx.media3.session {
|
||||
method public final String getId();
|
||||
method public final androidx.media3.common.Player getPlayer();
|
||||
method @Nullable public final android.app.PendingIntent getSessionActivity();
|
||||
method public android.os.Bundle getSessionExtras();
|
||||
method public final androidx.media3.session.SessionToken getToken();
|
||||
method public final void release();
|
||||
method public final com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> sendCustomCommand(androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.session.SessionCommand, android.os.Bundle);
|
||||
@ -1777,6 +1869,7 @@ package androidx.media3.session {
|
||||
method public androidx.media3.session.MediaSession.Builder setExtras(android.os.Bundle);
|
||||
method public androidx.media3.session.MediaSession.Builder setId(String);
|
||||
method public androidx.media3.session.MediaSession.Builder setSessionActivity(android.app.PendingIntent);
|
||||
method public androidx.media3.session.MediaSession.Builder setSessionExtras(android.os.Bundle);
|
||||
}
|
||||
|
||||
public static interface MediaSession.Callback {
|
||||
@ -1887,6 +1980,7 @@ package androidx.media3.session {
|
||||
|
||||
public final class SessionToken {
|
||||
ctor public SessionToken(android.content.Context, android.content.ComponentName);
|
||||
method public static com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionToken> createSessionToken(android.content.Context, android.media.session.MediaSession.Token);
|
||||
method public static com.google.common.collect.ImmutableSet<androidx.media3.session.SessionToken> getAllServiceTokens(android.content.Context);
|
||||
method public android.os.Bundle getExtras();
|
||||
method public String getPackageName();
|
||||
|
@ -19,7 +19,8 @@ buildscript {
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.3.2'
|
||||
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.4'
|
||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10'
|
||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.20'
|
||||
classpath 'org.jetbrains.kotlin:compose-compiler-gradle-plugin:2.0.20'
|
||||
}
|
||||
}
|
||||
allprojects {
|
||||
|
@ -29,6 +29,10 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
checkTestSources true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
|
@ -12,15 +12,15 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
project.ext {
|
||||
releaseVersion = '1.5.1'
|
||||
releaseVersionCode = 1_005_001_3_00
|
||||
releaseVersion = '1.6.0'
|
||||
releaseVersionCode = 1_006_000_3_00
|
||||
minSdkVersion = 21
|
||||
// See https://developer.android.com/training/cars/media/automotive-os#automotive-module
|
||||
automotiveMinSdkVersion = 28
|
||||
appTargetSdkVersion = 34
|
||||
// Upgrading this requires [Internal ref: b/193254928] to be fixed, or some
|
||||
// additional robolectric config.
|
||||
targetSdkVersion = 30
|
||||
targetSdkVersion = 31
|
||||
compileSdkVersion = 35
|
||||
dexmakerVersion = '2.28.3'
|
||||
// Use the same JUnit version as the Android repo:
|
||||
@ -30,10 +30,11 @@ project.ext {
|
||||
// https://cs.android.com/android/platform/superproject/main/+/main:external/guava/METADATA
|
||||
guavaVersion = '33.3.1-android'
|
||||
glideVersion = '4.14.2'
|
||||
kotlinxCoroutinesVersion = '1.8.1'
|
||||
// Not the same as kotlin version, https://github.com/Kotlin/kotlinx.coroutines/releases
|
||||
kotlinxCoroutinesVersion = '1.9.0'
|
||||
leakCanaryVersion = '2.10'
|
||||
mockitoVersion = '3.12.4'
|
||||
robolectricVersion = '4.11'
|
||||
robolectricVersion = '4.14.1'
|
||||
// Keep this in sync with Google's internal Checker Framework version.
|
||||
checkerframeworkVersion = '3.13.0'
|
||||
errorProneVersion = '2.18.0'
|
||||
@ -46,11 +47,13 @@ project.ext {
|
||||
androidxConstraintLayoutVersion = '2.1.4'
|
||||
androidxCoreVersion = '1.8.0'
|
||||
androidxExifInterfaceVersion = '1.3.6'
|
||||
androidxLifecycleVersion = '2.6.0'
|
||||
androidxLifecycleVersion = '2.8.7'
|
||||
androidxMediaVersion = '1.7.0'
|
||||
androidxRecyclerViewVersion = '1.3.0'
|
||||
androidxMaterialVersion = '1.8.0'
|
||||
androidxTestCoreVersion = '1.5.0'
|
||||
androidxTestUiAutomatorVersion = '2.3.0'
|
||||
androidxWindowVersion = '1.3.0'
|
||||
androidxTestEspressoVersion = '3.5.1'
|
||||
androidxTestJUnitVersion = '1.1.5'
|
||||
androidxTestRunnerVersion = '1.5.2'
|
||||
|
@ -52,6 +52,8 @@ include modulePrefix + 'lib-ui'
|
||||
project(modulePrefix + 'lib-ui').projectDir = new File(rootDir, 'libraries/ui')
|
||||
include modulePrefix + 'lib-ui-leanback'
|
||||
project(modulePrefix + 'lib-ui-leanback').projectDir = new File(rootDir, 'libraries/ui_leanback')
|
||||
include modulePrefix + 'lib-ui-compose'
|
||||
project(modulePrefix + 'lib-ui-compose').projectDir = new File(rootDir, 'libraries/ui_compose')
|
||||
|
||||
include modulePrefix + 'lib-database'
|
||||
project(modulePrefix + 'lib-database').projectDir = new File(rootDir, 'libraries/database')
|
||||
@ -81,6 +83,8 @@ if (gradle.ext.has('androidxMediaEnableMidiModule') && gradle.ext.androidxMediaE
|
||||
include modulePrefix + 'lib-decoder-midi'
|
||||
project(modulePrefix + 'lib-decoder-midi').projectDir = new File(rootDir, 'libraries/decoder_midi')
|
||||
}
|
||||
include modulePrefix + 'lib-decoder-mpegh'
|
||||
project(modulePrefix + 'lib-decoder-mpegh').projectDir = new File(rootDir, 'libraries/decoder_mpegh')
|
||||
include modulePrefix + 'lib-decoder-opus'
|
||||
project(modulePrefix + 'lib-decoder-opus').projectDir = new File(rootDir, 'libraries/decoder_opus')
|
||||
include modulePrefix + 'lib-decoder-vp9'
|
||||
|
@ -14,6 +14,7 @@
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
|
||||
|
||||
android {
|
||||
namespace 'androidx.media3.demo.compose'
|
||||
@ -52,12 +53,8 @@ android {
|
||||
disable 'GoogleAppIndexingWarning','MissingTranslation'
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
compose true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.3"
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
@ -67,21 +64,21 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def composeBom = platform('androidx.compose:compose-bom:2024.05.00')
|
||||
def composeBom = platform('androidx.compose:compose-bom:2024.12.01')
|
||||
implementation composeBom
|
||||
|
||||
implementation 'androidx.activity:activity-compose:1.9.0'
|
||||
implementation 'androidx.compose.foundation:foundation-android:1.6.7'
|
||||
implementation 'androidx.compose.material3:material3-android:1.2.1'
|
||||
implementation 'androidx.compose.foundation:foundation'
|
||||
implementation 'androidx.compose.material3:material3'
|
||||
implementation 'androidx.compose.material:material-icons-extended'
|
||||
implementation 'androidx.compose.ui:ui-tooling-preview'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-compose:' + androidxLifecycleVersion
|
||||
implementation 'com.google.android.material:material:' + androidxMaterialVersion
|
||||
|
||||
implementation project(modulePrefix + 'lib-exoplayer')
|
||||
implementation project(modulePrefix + 'lib-ui-compose')
|
||||
debugImplementation 'androidx.compose.ui:ui-tooling'
|
||||
|
||||
// For detecting and debugging leaks only. LeakCanary is not needed for demo app to work.
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:' + leakCanaryVersion
|
||||
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:' + kotlinxCoroutinesVersion
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
testImplementation project(modulePrefix + 'test-utils')
|
||||
|
||||
}
|
||||
|
20
demos/compose/lint.xml
Normal file
20
demos/compose/lint.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2024 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<lint>
|
||||
<issue id="UnsafeOptInUsageError">
|
||||
<option name="opt-in" value="androidx.media3.common.util.UnstableApi" />
|
||||
</issue>
|
||||
</lint>
|
@ -15,49 +15,139 @@
|
||||
*/
|
||||
package androidx.media3.demo.compose
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.demo.compose.buttons.ExtraControls
|
||||
import androidx.media3.demo.compose.buttons.MinimalControls
|
||||
import androidx.media3.demo.compose.data.videos
|
||||
import androidx.media3.demo.compose.layout.CONTENT_SCALES
|
||||
import androidx.media3.demo.compose.layout.noRippleClickable
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.ui.compose.PlayerSurface
|
||||
import androidx.media3.ui.compose.SURFACE_TYPE_SURFACE_VIEW
|
||||
import androidx.media3.ui.compose.modifiers.resizeWithContentScale
|
||||
import androidx.media3.ui.compose.state.rememberPresentationState
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
Surface {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val exoPlayer = remember {
|
||||
ExoPlayer.Builder(context).build().apply {
|
||||
setMediaItem(MediaItem.fromUri(videos[0]))
|
||||
prepare()
|
||||
playWhenReady = true
|
||||
repeatMode = Player.REPEAT_MODE_ONE
|
||||
}
|
||||
}
|
||||
PlayerSurface(
|
||||
player = exoPlayer,
|
||||
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
}
|
||||
setContent { ComposeDemoApp() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ComposeDemoApp(modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
var player by remember { mutableStateOf<Player?>(null) }
|
||||
|
||||
// See the following resources
|
||||
// https://developer.android.com/topic/libraries/architecture/lifecycle#onStop-and-savedState
|
||||
// https://developer.android.com/develop/ui/views/layout/support-multi-window-mode#multi-window_mode_configuration
|
||||
// https://developer.android.com/develop/ui/compose/layouts/adaptive/support-multi-window-mode#android_9
|
||||
|
||||
if (Build.VERSION.SDK_INT > 23) {
|
||||
// Initialize/release in onStart()/onStop() only because in a multi-window environment multiple
|
||||
// apps can be visible at the same time. The apps that are out-of-focus are paused, but video
|
||||
// playback should continue.
|
||||
LifecycleStartEffect(Unit) {
|
||||
player = initializePlayer(context)
|
||||
onStopOrDispose {
|
||||
player?.apply { release() }
|
||||
player = null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Call to onStop() is not guaranteed, hence we release the Player in onPause() instead
|
||||
LifecycleResumeEffect(Unit) {
|
||||
player = initializePlayer(context)
|
||||
onPauseOrDispose {
|
||||
player?.apply { release() }
|
||||
player = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
player?.let { MediaPlayerScreen(player = it, modifier = modifier.fillMaxSize()) }
|
||||
}
|
||||
|
||||
private fun initializePlayer(context: Context): Player =
|
||||
ExoPlayer.Builder(context).build().apply {
|
||||
setMediaItems(videos.map(MediaItem::fromUri))
|
||||
prepare()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MediaPlayerScreen(player: Player, modifier: Modifier = Modifier) {
|
||||
var showControls by remember { mutableStateOf(true) }
|
||||
var currentContentScaleIndex by remember { mutableIntStateOf(0) }
|
||||
val contentScale = CONTENT_SCALES[currentContentScaleIndex].second
|
||||
|
||||
val presentationState = rememberPresentationState(player)
|
||||
val scaledModifier = Modifier.resizeWithContentScale(contentScale, presentationState.videoSizeDp)
|
||||
|
||||
// Only use MediaPlayerScreen's modifier once for the top level Composable
|
||||
Box(modifier) {
|
||||
// Always leave PlayerSurface to be part of the Compose tree because it will be initialised in
|
||||
// the process. If this composable is guarded by some condition, it might never become visible
|
||||
// because the Player will not emit the relevant event, e.g. the first frame being ready.
|
||||
PlayerSurface(
|
||||
player = player,
|
||||
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
|
||||
modifier = scaledModifier.noRippleClickable { showControls = !showControls },
|
||||
)
|
||||
|
||||
if (presentationState.coverSurface) {
|
||||
// Cover the surface that is being prepared with a shutter
|
||||
// Do not use scaledModifier here, makes the Box be measured at 0x0
|
||||
Box(Modifier.matchParentSize().background(Color.Black))
|
||||
}
|
||||
|
||||
if (showControls) {
|
||||
// drawn on top of a potential shutter
|
||||
MinimalControls(player, Modifier.align(Alignment.Center))
|
||||
ExtraControls(
|
||||
player,
|
||||
Modifier.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter)
|
||||
.background(Color.Gray.copy(alpha = 0.4f))
|
||||
.navigationBarsPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { currentContentScaleIndex = currentContentScaleIndex.inc() % CONTENT_SCALES.size },
|
||||
modifier = Modifier.align(Alignment.TopCenter).padding(top = 48.dp),
|
||||
) {
|
||||
Text("ContentScale is ${CONTENT_SCALES[currentContentScaleIndex].first}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.media3.demo.compose.buttons
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.media3.common.Player
|
||||
|
||||
@Composable
|
||||
internal fun ExtraControls(player: Player, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
PlaybackSpeedPopUpButton(player)
|
||||
ShuffleButton(player)
|
||||
RepeatButton(player)
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.media3.demo.compose.buttons
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.media3.common.Player
|
||||
|
||||
/**
|
||||
* Minimal playback controls for a [Player].
|
||||
*
|
||||
* Includes buttons for seeking to a previous/next items or playing/pausing the playback.
|
||||
*/
|
||||
@Composable
|
||||
internal fun MinimalControls(player: Player, modifier: Modifier = Modifier) {
|
||||
val graySemiTransparentBackground = Color.Gray.copy(alpha = 0.1f)
|
||||
val modifierForIconButton =
|
||||
modifier.size(80.dp).background(graySemiTransparentBackground, CircleShape)
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
PreviousButton(player, modifierForIconButton)
|
||||
PlayPauseButton(player, modifierForIconButton)
|
||||
NextButton(player, modifierForIconButton)
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.media3.demo.compose.buttons
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.SkipNext
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.demo.compose.R
|
||||
import androidx.media3.ui.compose.state.rememberNextButtonState
|
||||
|
||||
@Composable
|
||||
internal fun NextButton(player: Player, modifier: Modifier = Modifier) {
|
||||
val state = rememberNextButtonState(player)
|
||||
IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
|
||||
Icon(
|
||||
Icons.Default.SkipNext,
|
||||
contentDescription = stringResource(R.string.next_button),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.media3.demo.compose.buttons
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.demo.compose.R
|
||||
import androidx.media3.ui.compose.state.rememberPlayPauseButtonState
|
||||
|
||||
@Composable
|
||||
internal fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
|
||||
val state = rememberPlayPauseButtonState(player)
|
||||
val icon = if (state.showPlay) Icons.Default.PlayArrow else Icons.Default.Pause
|
||||
val contentDescription =
|
||||
if (state.showPlay) stringResource(R.string.playpause_button_play)
|
||||
else stringResource(R.string.playpause_button_pause)
|
||||
IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
|
||||
Icon(icon, contentDescription = contentDescription, modifier = modifier)
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2025 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.media3.demo.compose.buttons
|
||||
|
||||
import android.view.Gravity
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.compose.ui.window.DialogWindowProvider
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.ui.compose.state.rememberPlaybackSpeedState
|
||||
|
||||
@Composable
|
||||
internal fun PlaybackSpeedPopUpButton(
|
||||
player: Player,
|
||||
modifier: Modifier = Modifier,
|
||||
speedSelection: List<Float> = listOf(0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f),
|
||||
) {
|
||||
val state = rememberPlaybackSpeedState(player)
|
||||
var openDialog by remember { mutableStateOf(false) }
|
||||
TextButton(onClick = { openDialog = true }, modifier = modifier, enabled = state.isEnabled) {
|
||||
// TODO: look into TextMeasurer to ensure 1.1 and 2.2 occupy the same space
|
||||
BasicText("%.1fx".format(state.playbackSpeed))
|
||||
}
|
||||
if (openDialog) {
|
||||
BottomDialogOfChoices(
|
||||
currentSpeed = state.playbackSpeed,
|
||||
choices = speedSelection,
|
||||
onDismissRequest = { openDialog = false },
|
||||
onSelectChoice = state::updatePlaybackSpeed,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomDialogOfChoices(
|
||||
currentSpeed: Float,
|
||||
choices: List<Float>,
|
||||
onDismissRequest: () -> Unit,
|
||||
onSelectChoice: (Float) -> Unit,
|
||||
) {
|
||||
Dialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
) {
|
||||
val dialogWindowProvider = LocalView.current.parent as? DialogWindowProvider
|
||||
dialogWindowProvider?.window?.let { window ->
|
||||
window.setGravity(Gravity.BOTTOM) // Move down, by default dialogs are in the centre
|
||||
window.setDimAmount(0f) // Remove dimmed background of ongoing playback
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.wrapContentSize().background(Color.LightGray)) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().wrapContentWidth(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
choices.forEach { speed ->
|
||||
TextButton(
|
||||
onClick = {
|
||||
onSelectChoice(speed)
|
||||
onDismissRequest()
|
||||
}
|
||||
) {
|
||||
var fontWeight = FontWeight(400)
|
||||
if (speed == currentSpeed) {
|
||||
fontWeight = FontWeight(1000)
|
||||
}
|
||||
Text("%.1fx".format(speed), fontWeight = fontWeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.media3.demo.compose.buttons
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.SkipPrevious
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.demo.compose.R
|
||||
import androidx.media3.ui.compose.state.rememberPreviousButtonState
|
||||
|
||||
@Composable
|
||||
internal fun PreviousButton(player: Player, modifier: Modifier = Modifier) {
|
||||
val state = rememberPreviousButtonState(player)
|
||||
IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
|
||||
Icon(
|
||||
Icons.Default.SkipPrevious,
|
||||
contentDescription = stringResource(R.string.previous_button),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.media3.demo.compose.buttons
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Repeat
|
||||
import androidx.compose.material.icons.filled.RepeatOn
|
||||
import androidx.compose.material.icons.filled.RepeatOneOn
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.demo.compose.R
|
||||
import androidx.media3.ui.compose.state.rememberRepeatButtonState
|
||||
|
||||
@Composable
|
||||
internal fun RepeatButton(player: Player, modifier: Modifier = Modifier) {
|
||||
val state = rememberRepeatButtonState(player)
|
||||
val icon = repeatModeIcon(state.repeatModeState)
|
||||
val contentDescription = repeatModeContentDescription(state.repeatModeState)
|
||||
IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
|
||||
Icon(icon, contentDescription = contentDescription, modifier = modifier)
|
||||
}
|
||||
}
|
||||
|
||||
private fun repeatModeIcon(repeatMode: @Player.RepeatMode Int): ImageVector {
|
||||
return when (repeatMode) {
|
||||
Player.REPEAT_MODE_OFF -> Icons.Default.Repeat
|
||||
Player.REPEAT_MODE_ONE -> Icons.Default.RepeatOneOn
|
||||
else -> Icons.Default.RepeatOn
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun repeatModeContentDescription(repeatMode: @Player.RepeatMode Int): String {
|
||||
return when (repeatMode) {
|
||||
Player.REPEAT_MODE_OFF -> stringResource(R.string.repeat_button_repeat_off_description)
|
||||
Player.REPEAT_MODE_ONE -> stringResource(R.string.repeat_button_repeat_one_description)
|
||||
else -> stringResource(R.string.repeat_button_repeat_all_description)
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.media3.demo.compose.buttons
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Shuffle
|
||||
import androidx.compose.material.icons.filled.ShuffleOn
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.demo.compose.R
|
||||
import androidx.media3.ui.compose.state.rememberShuffleButtonState
|
||||
|
||||
@Composable
|
||||
internal fun ShuffleButton(player: Player, modifier: Modifier = Modifier) {
|
||||
val state = rememberShuffleButtonState(player)
|
||||
val icon = if (state.shuffleOn) Icons.Default.ShuffleOn else Icons.Default.Shuffle
|
||||
val contentDescription =
|
||||
if (state.shuffleOn) {
|
||||
stringResource(R.string.shuffle_button_shuffle_on_description)
|
||||
} else {
|
||||
stringResource(R.string.shuffle_button_shuffle_off_description)
|
||||
}
|
||||
IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
|
||||
Icon(icon, contentDescription = contentDescription, modifier = modifier)
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ package androidx.media3.demo.compose.data
|
||||
val videos =
|
||||
listOf(
|
||||
"https://html5demos.com/assets/dizzy.mp4",
|
||||
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-0/shortform_2.mp4",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-0/shortform_3.mp4",
|
||||
)
|
||||
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.media3.demo.compose.layout
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
internal fun Modifier.noRippleClickable(onClick: () -> Unit): Modifier =
|
||||
clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null, // to prevent the ripple from the tap
|
||||
) {
|
||||
onClick()
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.media3.demo.compose.layout
|
||||
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
|
||||
val CONTENT_SCALES =
|
||||
listOf(
|
||||
"Fit" to ContentScale.Fit,
|
||||
"Crop" to ContentScale.Crop,
|
||||
"None" to ContentScale.None,
|
||||
"Inside" to ContentScale.Inside,
|
||||
"FillBounds" to ContentScale.FillBounds,
|
||||
"FillHeight" to ContentScale.FillHeight,
|
||||
"FillWidth" to ContentScale.FillWidth,
|
||||
)
|
@ -15,12 +15,13 @@
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">Media3 Compose Demo</string>
|
||||
<string name="current_playlist_name">Current playlist</string>
|
||||
<string name="open_player_content_description">Click to view your play list</string>
|
||||
<string name="added_media_item_format">Added %1$s to playlist</string>
|
||||
<string name="shuffle">Shuffle</string>
|
||||
<string name="play_button">Play</string>
|
||||
<string name="waiting_for_metadata">Waiting for playlist to load…</string>
|
||||
<string name="notification_permission_denied">
|
||||
"Without notification access the app can't warn about failed background operations"</string>
|
||||
<string name="playpause_button_play">Play</string>
|
||||
<string name="playpause_button_pause">Pause</string>
|
||||
<string name="next_button">Next</string>
|
||||
<string name="previous_button">Previous</string>
|
||||
<string name="repeat_button_repeat_off_description">Current mode: Repeat none. Toggle repeat mode.</string>
|
||||
<string name="repeat_button_repeat_one_description">Current mode: Repeat one. Toggle repeat mode.</string>
|
||||
<string name="repeat_button_repeat_all_description">Current mode: Repeat all. Toggle repeat mode.</string>
|
||||
<string name="shuffle_button_shuffle_on_description">Disable shuffle mode.</string>
|
||||
<string name="shuffle_button_shuffle_off_description">Enable shuffle mode</string>
|
||||
</resources>
|
||||
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package androidx.media3.demo.composition;
|
||||
|
||||
import static android.content.pm.ActivityInfo.COLOR_MODE_HDR;
|
||||
import static androidx.media3.common.util.Util.SDK_INT;
|
||||
import static androidx.media3.transformer.Composition.HDR_MODE_EXPERIMENTAL_FORCE_INTERPRET_HDR_AS_SDR;
|
||||
import static androidx.media3.transformer.Composition.HDR_MODE_KEEP_HDR;
|
||||
import static androidx.media3.transformer.Composition.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_MEDIACODEC;
|
||||
@ -55,7 +57,8 @@ import androidx.media3.transformer.EditedMediaItemSequence;
|
||||
import androidx.media3.transformer.Effects;
|
||||
import androidx.media3.transformer.ExportException;
|
||||
import androidx.media3.transformer.ExportResult;
|
||||
import androidx.media3.transformer.InAppMuxer;
|
||||
import androidx.media3.transformer.InAppFragmentedMp4Muxer;
|
||||
import androidx.media3.transformer.InAppMp4Muxer;
|
||||
import androidx.media3.transformer.JsonUtil;
|
||||
import androidx.media3.transformer.Transformer;
|
||||
import androidx.media3.ui.PlayerView;
|
||||
@ -111,6 +114,9 @@ public final class CompositionPreviewActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (SDK_INT >= 26) {
|
||||
getWindow().setColorMode(COLOR_MODE_HDR);
|
||||
}
|
||||
setContentView(R.layout.composition_preview_activity);
|
||||
playerView = findViewById(R.id.composition_player_view);
|
||||
|
||||
@ -186,6 +192,18 @@ public final class CompositionPreviewActivity extends AppCompatActivity {
|
||||
exportStopwatch.reset();
|
||||
}
|
||||
|
||||
@SuppressWarnings("MissingSuperCall")
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (compositionPlayer != null) {
|
||||
compositionPlayer.pause();
|
||||
}
|
||||
if (exportStopwatch.isRunning()) {
|
||||
cancelExport();
|
||||
exportStopwatch.reset();
|
||||
}
|
||||
}
|
||||
|
||||
private Composition prepareComposition() {
|
||||
String[] presetUris = getResources().getStringArray(/* id= */ R.array.preset_uris);
|
||||
int[] presetDurationsUs = getResources().getIntArray(/* id= */ R.array.preset_durations);
|
||||
@ -279,7 +297,6 @@ public final class CompositionPreviewActivity extends AppCompatActivity {
|
||||
Log.e(TAG, "Preview error", error);
|
||||
}
|
||||
});
|
||||
player.setRepeatMode(Player.REPEAT_MODE_ALL);
|
||||
player.setComposition(composition);
|
||||
player.prepare();
|
||||
player.play();
|
||||
@ -346,21 +363,20 @@ public final class CompositionPreviewActivity extends AppCompatActivity {
|
||||
enableDebugTracingCheckBox.setOnCheckedChangeListener(
|
||||
(buttonView, isChecked) -> DebugTraceUtil.enableTracing = isChecked);
|
||||
|
||||
// Connect producing fragmented MP4 to using Media3 Muxer
|
||||
CheckBox useMedia3MuxerCheckBox =
|
||||
exportSettingsDialogView.findViewById(R.id.use_media3_muxer_checkbox);
|
||||
CheckBox produceFragmentedMp4CheckBox =
|
||||
exportSettingsDialogView.findViewById(R.id.produce_fragmented_mp4_checkbox);
|
||||
useMedia3MuxerCheckBox.setOnCheckedChangeListener(
|
||||
(buttonView, isChecked) -> {
|
||||
if (!isChecked) {
|
||||
produceFragmentedMp4CheckBox.setChecked(false);
|
||||
}
|
||||
});
|
||||
produceFragmentedMp4CheckBox.setOnCheckedChangeListener(
|
||||
CheckBox useMedia3Mp4MuxerCheckBox =
|
||||
exportSettingsDialogView.findViewById(R.id.use_media3_mp4_muxer_checkbox);
|
||||
CheckBox useMedia3FragmentedMp4MuxerCheckBox =
|
||||
exportSettingsDialogView.findViewById(R.id.use_media3_fragmented_mp4_muxer_checkbox);
|
||||
useMedia3Mp4MuxerCheckBox.setOnCheckedChangeListener(
|
||||
(buttonView, isChecked) -> {
|
||||
if (isChecked) {
|
||||
useMedia3MuxerCheckBox.setChecked(true);
|
||||
useMedia3FragmentedMp4MuxerCheckBox.setChecked(false);
|
||||
}
|
||||
});
|
||||
useMedia3FragmentedMp4MuxerCheckBox.setOnCheckedChangeListener(
|
||||
(buttonView, isChecked) -> {
|
||||
if (isChecked) {
|
||||
useMedia3Mp4MuxerCheckBox.setChecked(false);
|
||||
}
|
||||
});
|
||||
|
||||
@ -403,15 +419,15 @@ public final class CompositionPreviewActivity extends AppCompatActivity {
|
||||
transformerBuilder.setVideoMimeType(selectedVideoMimeType);
|
||||
}
|
||||
|
||||
CheckBox useMedia3MuxerCheckBox =
|
||||
exportSettingsDialogView.findViewById(R.id.use_media3_muxer_checkbox);
|
||||
CheckBox produceFragmentedMp4CheckBox =
|
||||
exportSettingsDialogView.findViewById(R.id.produce_fragmented_mp4_checkbox);
|
||||
if (useMedia3MuxerCheckBox.isChecked()) {
|
||||
transformerBuilder.setMuxerFactory(
|
||||
new InAppMuxer.Factory.Builder()
|
||||
.setOutputFragmentedMp4(produceFragmentedMp4CheckBox.isChecked())
|
||||
.build());
|
||||
CheckBox useMedia3Mp4MuxerCheckBox =
|
||||
exportSettingsDialogView.findViewById(R.id.use_media3_mp4_muxer_checkbox);
|
||||
CheckBox useMedia3FragmentedMp4MuxerCheckBox =
|
||||
exportSettingsDialogView.findViewById(R.id.use_media3_fragmented_mp4_muxer_checkbox);
|
||||
if (useMedia3Mp4MuxerCheckBox.isChecked()) {
|
||||
transformerBuilder.setMuxerFactory(new InAppMp4Muxer.Factory());
|
||||
}
|
||||
if (useMedia3FragmentedMp4MuxerCheckBox.isChecked()) {
|
||||
transformerBuilder.setMuxerFactory(new InAppFragmentedMp4Muxer.Factory());
|
||||
}
|
||||
|
||||
transformer =
|
||||
|
@ -18,6 +18,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
|
@ -79,12 +79,12 @@
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
<TextView
|
||||
android:text="@string/use_media3_muxer"
|
||||
android:text="@string/use_media3_mp4_muxer"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1" />
|
||||
<CheckBox
|
||||
android:id="@+id/use_media3_muxer_checkbox"
|
||||
android:id="@+id/use_media3_mp4_muxer_checkbox"
|
||||
android:layout_gravity="end"
|
||||
android:checked="false"
|
||||
android:layout_height="wrap_content"
|
||||
@ -96,12 +96,12 @@
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
<TextView
|
||||
android:text="@string/produce_fragmented_mp4"
|
||||
android:text="@string/use_media3_fragmented_mp4_muxer"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1" />
|
||||
<CheckBox
|
||||
android:id="@+id/produce_fragmented_mp4_checkbox"
|
||||
android:id="@+id/use_media3_fragmented_mp4_muxer_checkbox"
|
||||
android:layout_gravity="end"
|
||||
android:checked="false"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -34,6 +34,6 @@
|
||||
<string name="output_audio_mime_type" translatable="false">Output audio MIME type</string>
|
||||
<string name="output_video_mime_type" translatable="false">Output video MIME type</string>
|
||||
<string name="enable_debug_tracing" translatable="false">Enable debug tracing</string>
|
||||
<string name="use_media3_muxer" translatable="false">Use Media3 muxer</string>
|
||||
<string name="produce_fragmented_mp4" translatable="false">Produce fragmented MP4</string>
|
||||
<string name="use_media3_mp4_muxer" translatable="false">Use Media3 Mp4Muxer</string>
|
||||
<string name="use_media3_fragmented_mp4_muxer" translatable="false">Use Media3 FragmentedMp4Muxer</string>
|
||||
</resources>
|
||||
|
10
demos/effect/README.md
Normal file
10
demos/effect/README.md
Normal file
@ -0,0 +1,10 @@
|
||||
# Effect demo
|
||||
|
||||
This app demonstrates how to use the [Effect][] API to modify videos. It uses
|
||||
`setVideoEffects` method to add different effects to [ExoPlayer][].
|
||||
|
||||
See the [demos README](../README.md) for instructions on how to build and run
|
||||
this demo.
|
||||
|
||||
[Effect]: https://github.com/androidx/media/tree/release/libraries/effect
|
||||
[ExoPlayer]: https://github.com/androidx/media/tree/release/libraries/exoplayer
|
80
demos/effect/build.gradle
Normal file
80
demos/effect/build.gradle
Normal file
@ -0,0 +1,80 @@
|
||||
// Copyright 2024 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
|
||||
|
||||
android {
|
||||
namespace 'androidx.media3.demo.effect'
|
||||
|
||||
compileSdk project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.appTargetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
shrinkResources true
|
||||
minifyEnabled true
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
debug {
|
||||
jniDebuggable = true
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
// The demo app isn't indexed, and doesn't have translations.
|
||||
disable 'GoogleAppIndexingWarning','MissingTranslation'
|
||||
}
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def composeBom = platform('androidx.compose:compose-bom:2024.10.00')
|
||||
implementation composeBom
|
||||
|
||||
implementation 'androidx.activity:activity-compose:1.9.0'
|
||||
implementation 'androidx.compose.foundation:foundation'
|
||||
implementation 'androidx.compose.material3:material3'
|
||||
implementation 'com.google.android.material:material:' + androidxMaterialVersion
|
||||
|
||||
implementation project(modulePrefix + 'lib-exoplayer')
|
||||
implementation project(modulePrefix + 'lib-ui')
|
||||
implementation project(modulePrefix + 'lib-effect')
|
||||
|
||||
// For detecting and debugging leaks only. LeakCanary is not needed for demo app to work.
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:' + leakCanaryVersion
|
||||
}
|
20
demos/effect/lint.xml
Normal file
20
demos/effect/lint.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2024 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<lint>
|
||||
<issue id="UnsafeOptInUsageError">
|
||||
<option name="opt-in" value="androidx.media3.common.util.UnstableApi" />
|
||||
</issue>
|
||||
</lint>
|
40
demos/effect/src/main/AndroidManifest.xml
Normal file
40
demos/effect/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2024 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="androidx.media3.demo.effect">
|
||||
|
||||
<uses-sdk/>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.Media3EffectDemo">
|
||||
|
||||
<activity
|
||||
android:name="androidx.media3.demo.effect.EffectActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
27
demos/effect/src/main/assets/media.playlist.json
Normal file
27
demos/effect/src/main/assets/media.playlist.json
Normal file
@ -0,0 +1,27 @@
|
||||
[
|
||||
{
|
||||
"name": "Cats -> Dogs",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Android Block -> Dogs -> BigBuckBunny",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.media3.demo.effect
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
import android.os.Handler
|
||||
import androidx.media3.common.VideoFrameProcessingException
|
||||
import androidx.media3.common.util.Size
|
||||
import androidx.media3.common.util.Util
|
||||
import androidx.media3.effect.CanvasOverlay
|
||||
import kotlin.math.abs
|
||||
import kotlin.random.Random
|
||||
|
||||
/** Mimics an emitter of confetti, dropping from the center of the frame. */
|
||||
internal class ConfettiOverlay : CanvasOverlay(/* useInputFrameSize= */ true) {
|
||||
|
||||
private val confettiList = mutableListOf<Confetti>()
|
||||
private val paint = Paint()
|
||||
private val handler = Handler(Util.getCurrentOrMainLooper())
|
||||
|
||||
private var addConfettiTask: (() -> Unit)? = null
|
||||
private var width = 0f
|
||||
private var height = 0f
|
||||
private var started = false
|
||||
|
||||
override fun configure(videoSize: Size) {
|
||||
super.configure(videoSize)
|
||||
this.width = videoSize.width.toFloat()
|
||||
this.height = videoSize.height.toFloat()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onDraw(canvas: Canvas, presentationTimeUs: Long) {
|
||||
if (!started) {
|
||||
start()
|
||||
}
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
||||
|
||||
confettiList.removeAll { confetti ->
|
||||
confetti.y > height / 2 || confetti.x <= 0 || confetti.x > width
|
||||
}
|
||||
for (confetti in confettiList) {
|
||||
confetti.draw(canvas, paint)
|
||||
confetti.update()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(VideoFrameProcessingException::class)
|
||||
override fun release() {
|
||||
super.release()
|
||||
handler.post(this::stop)
|
||||
}
|
||||
|
||||
/** Starts the confetti. */
|
||||
fun start() {
|
||||
addConfettiTask = this::addConfetti
|
||||
handler.post(checkNotNull(addConfettiTask))
|
||||
started = true
|
||||
}
|
||||
|
||||
/** Stops the confetti. */
|
||||
fun stop() {
|
||||
handler.removeCallbacks(checkNotNull(addConfettiTask))
|
||||
confettiList.clear()
|
||||
started = false
|
||||
addConfettiTask = null
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addConfetti() {
|
||||
repeat(5) {
|
||||
confettiList.add(
|
||||
Confetti(
|
||||
text = CONFETTI_TEXTS[abs(Random.nextInt()) % CONFETTI_TEXTS.size],
|
||||
x = width / 2f,
|
||||
y = EMITTER_POSITION_Y.toFloat(),
|
||||
size = CONFETTI_BASE_SIZE + Random.nextInt(CONFETTI_SIZE_VARIATION),
|
||||
color = Color.HSVToColor(floatArrayOf(Random.nextInt(360).toFloat(), 0.6f, 0.8f)),
|
||||
)
|
||||
)
|
||||
}
|
||||
handler.postDelayed(this::addConfetti, /* delayMillis= */ 100)
|
||||
}
|
||||
|
||||
private class Confetti(
|
||||
private val text: String,
|
||||
private val size: Int,
|
||||
private val color: Int,
|
||||
var x: Float,
|
||||
var y: Float,
|
||||
) {
|
||||
private val speedX = 4 * (Random.nextFloat() * 2 - 1) // Random speed in x direction
|
||||
private val speedY = 4 * Random.nextFloat() // Random speed in y direction
|
||||
private val rotationSpeed = (Random.nextFloat() - 0.5f) * 4f // Random rotation speed
|
||||
|
||||
private var rotation = Random.nextFloat() * 360f
|
||||
|
||||
/** Draws the [Confetti] on the [Canvas]. */
|
||||
fun draw(canvas: Canvas, paint: Paint) {
|
||||
canvas.save()
|
||||
paint.color = color
|
||||
canvas.translate(x, y)
|
||||
canvas.rotate(rotation)
|
||||
paint.textSize = size.toFloat()
|
||||
canvas.drawText(text, /* x= */ 0f, /* y= */ 0f, paint) // Only draw text
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
/** Updates the [Confetti]. */
|
||||
fun update() {
|
||||
x += speedX
|
||||
y += speedY
|
||||
rotation += rotationSpeed
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val CONFETTI_TEXTS = listOf("❊", "✿", "❊", "✦︎", "♥︎", "☕︎")
|
||||
const val EMITTER_POSITION_Y = -50
|
||||
const val CONFETTI_BASE_SIZE = 30
|
||||
const val CONFETTI_SIZE_VARIATION = 10
|
||||
}
|
||||
}
|
@ -0,0 +1,588 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.demo.effect
|
||||
|
||||
import android.Manifest
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.Effect
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.common.util.Util.SDK_INT
|
||||
import androidx.media3.effect.Contrast
|
||||
import androidx.media3.effect.OverlayEffect
|
||||
import androidx.media3.effect.StaticOverlaySettings
|
||||
import androidx.media3.effect.TextOverlay
|
||||
import androidx.media3.effect.TextureOverlay
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.ui.PlayerView
|
||||
import com.google.common.collect.ImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class EffectActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val playlistHolderList = mutableStateOf<List<PlaylistHolder>>(emptyList())
|
||||
lifecycleScope.launch {
|
||||
playlistHolderList.value =
|
||||
loadPlaylistsFromJson(JSON_FILENAME, this@EffectActivity, "EffectActivity")
|
||||
}
|
||||
setContent { EffectDemo(playlistHolderList.value) }
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@Composable
|
||||
private fun EffectDemo(playlistHolderList: List<PlaylistHolder>) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val exoPlayer by remember {
|
||||
mutableStateOf(ExoPlayer.Builder(context).build().apply { playWhenReady = true })
|
||||
}
|
||||
var effectsEnabled by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(paddingValues),
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
InputChooser(
|
||||
playlistHolderList,
|
||||
onException = { message ->
|
||||
coroutineScope.launch { snackbarHostState.showSnackbar(message) }
|
||||
},
|
||||
) { mediaItems ->
|
||||
effectsEnabled = true
|
||||
exoPlayer.apply {
|
||||
setMediaItems(mediaItems)
|
||||
setVideoEffects(emptyList())
|
||||
prepare()
|
||||
}
|
||||
}
|
||||
PlayerScreen(exoPlayer)
|
||||
EffectControls(
|
||||
effectsEnabled,
|
||||
onApplyEffectsClicked = { videoEffects ->
|
||||
exoPlayer.apply {
|
||||
setVideoEffects(videoEffects)
|
||||
prepare()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InputChooser(
|
||||
playlistHolderList: List<PlaylistHolder>,
|
||||
onException: (String) -> Unit,
|
||||
onNewMediaItems: (List<MediaItem>) -> Unit,
|
||||
) {
|
||||
var showPresetInputChooser by remember { mutableStateOf(false) }
|
||||
var showLocalFileChooser by remember { mutableStateOf(false) }
|
||||
Row(
|
||||
Modifier.padding(vertical = dimensionResource(id = R.dimen.regular_padding)),
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.regular_padding)),
|
||||
) {
|
||||
Button(onClick = { showPresetInputChooser = true }) {
|
||||
Text(text = stringResource(id = R.string.choose_preset_input))
|
||||
}
|
||||
Button(onClick = { showLocalFileChooser = true }) {
|
||||
Text(text = stringResource(id = R.string.choose_local_file))
|
||||
}
|
||||
}
|
||||
if (showPresetInputChooser) {
|
||||
if (playlistHolderList.isNotEmpty()) {
|
||||
PresetInputChooser(
|
||||
playlistHolderList,
|
||||
onDismissRequest = { showPresetInputChooser = false },
|
||||
) { mediaItems ->
|
||||
onNewMediaItems(mediaItems)
|
||||
showPresetInputChooser = false
|
||||
}
|
||||
} else {
|
||||
onException(stringResource(id = R.string.no_loaded_playlists_error))
|
||||
showPresetInputChooser = false
|
||||
}
|
||||
}
|
||||
if (showLocalFileChooser) {
|
||||
LocalFileChooser(
|
||||
onException = { message ->
|
||||
onException(message)
|
||||
showLocalFileChooser = false
|
||||
}
|
||||
) { mediaItems ->
|
||||
onNewMediaItems(mediaItems)
|
||||
showLocalFileChooser = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PresetInputChooser(
|
||||
playlistHolderList: List<PlaylistHolder>,
|
||||
onDismissRequest: () -> Unit,
|
||||
onInputSelected: (List<MediaItem>) -> Unit,
|
||||
) {
|
||||
var selectedOption by remember { mutableStateOf(playlistHolderList.first()) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(stringResource(id = R.string.choose_preset_input)) },
|
||||
confirmButton = {
|
||||
Button(onClick = { onInputSelected(selectedOption.mediaItems) }) {
|
||||
Text(text = stringResource(id = R.string.ok))
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
playlistHolderList.forEach { playlistHolder ->
|
||||
Row(
|
||||
Modifier.fillMaxWidth()
|
||||
.selectable(
|
||||
(playlistHolder == selectedOption),
|
||||
onClick = { selectedOption = playlistHolder },
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RadioButton(
|
||||
selected = (playlistHolder == selectedOption),
|
||||
onClick = { selectedOption = playlistHolder },
|
||||
)
|
||||
Text(playlistHolder.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@Composable
|
||||
private fun LocalFileChooser(
|
||||
onException: (String) -> Unit,
|
||||
onFileSelected: (List<MediaItem>) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val localFileChooserLauncher =
|
||||
rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument(),
|
||||
onResult = { uri: Uri? ->
|
||||
if (uri != null) {
|
||||
onFileSelected(listOf(MediaItem.fromUri(uri)))
|
||||
} else {
|
||||
onException(getString(R.string.can_not_open_file_error))
|
||||
}
|
||||
},
|
||||
)
|
||||
val permissionLauncher =
|
||||
rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestPermission(),
|
||||
onResult = { isGranted: Boolean ->
|
||||
if (isGranted) {
|
||||
localFileChooserLauncher.launch(arrayOf("video/*"))
|
||||
} else {
|
||||
onException(getString(R.string.permission_not_granted_error))
|
||||
}
|
||||
},
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
val permission =
|
||||
if (SDK_INT >= 33) Manifest.permission.READ_MEDIA_VIDEO
|
||||
else Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
val permissionCheck = ContextCompat.checkSelfPermission(context, permission)
|
||||
if (permissionCheck == android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||
localFileChooserLauncher.launch(arrayOf("video/*"))
|
||||
} else {
|
||||
permissionLauncher.launch(permission)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PlayerScreen(exoPlayer: ExoPlayer) {
|
||||
val context = LocalContext.current
|
||||
AndroidView(
|
||||
factory = { PlayerView(context).apply { player = exoPlayer } },
|
||||
modifier =
|
||||
Modifier.height(dimensionResource(id = R.dimen.android_view_height))
|
||||
.padding(all = dimensionResource(id = R.dimen.regular_padding)),
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@Composable
|
||||
private fun EffectControls(enabled: Boolean, onApplyEffectsClicked: (List<Effect>) -> Unit) {
|
||||
var effectControlsState by remember { mutableStateOf(EffectControlsState()) }
|
||||
|
||||
Button(
|
||||
enabled = enabled && effectControlsState.effectsChanged,
|
||||
onClick = {
|
||||
val effectsList = mutableListOf<Effect>()
|
||||
|
||||
if (effectControlsState.contrastValue != 0f) {
|
||||
effectsList += Contrast(effectControlsState.contrastValue)
|
||||
}
|
||||
|
||||
val overlaysBuilder = ImmutableList.builder<TextureOverlay>()
|
||||
if (effectControlsState.confettiOverlayChecked) {
|
||||
overlaysBuilder.add(ConfettiOverlay())
|
||||
}
|
||||
val textOverlayText = effectControlsState.textOverlayText
|
||||
if (effectControlsState.textOverlayChecked && textOverlayText != null) {
|
||||
val spannableOverlayText = SpannableString(textOverlayText)
|
||||
spannableOverlayText.setSpan(
|
||||
ForegroundColorSpan(effectControlsState.textOverlayColor.toArgb()),
|
||||
/* start= */ 0,
|
||||
textOverlayText.length,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
|
||||
)
|
||||
val staticOverlaySettings =
|
||||
StaticOverlaySettings.Builder()
|
||||
.setAlphaScale(effectControlsState.textOverlayAlpha)
|
||||
.build()
|
||||
overlaysBuilder.add(
|
||||
TextOverlay.createStaticTextOverlay(spannableOverlayText, staticOverlaySettings)
|
||||
)
|
||||
}
|
||||
effectsList += OverlayEffect(overlaysBuilder.build())
|
||||
|
||||
onApplyEffectsClicked(effectsList)
|
||||
effectControlsState = effectControlsState.copy(effectsChanged = false)
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.apply_effects))
|
||||
}
|
||||
|
||||
EffectControlsList(enabled, effectControlsState) { newEffectControlsState ->
|
||||
effectControlsState = newEffectControlsState
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EffectControlsList(
|
||||
enabled: Boolean,
|
||||
effectControlsState: EffectControlsState,
|
||||
onEffectControlsStateChange: (EffectControlsState) -> Unit,
|
||||
) {
|
||||
LazyColumn(Modifier.padding(vertical = dimensionResource(id = R.dimen.small_padding))) {
|
||||
item {
|
||||
EffectItem(
|
||||
name = stringResource(id = R.string.contrast),
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
onEffectControlsStateChange(
|
||||
effectControlsState.copy(effectsChanged = true, contrastValue = 0f)
|
||||
)
|
||||
},
|
||||
) {
|
||||
Row {
|
||||
Text(
|
||||
text = "%.2f".format(effectControlsState.contrastValue),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(dimensionResource(id = R.dimen.large_padding)).weight(1f),
|
||||
)
|
||||
Slider(
|
||||
value = effectControlsState.contrastValue,
|
||||
onValueChange = { newContrastValue ->
|
||||
val newRoundedContrastValue = "%.2f".format(newContrastValue).toFloat()
|
||||
onEffectControlsStateChange(
|
||||
effectControlsState.copy(
|
||||
effectsChanged = true,
|
||||
contrastValue = newRoundedContrastValue,
|
||||
)
|
||||
)
|
||||
},
|
||||
valueRange = -1f..1f,
|
||||
modifier = Modifier.weight(4f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
EffectItem(
|
||||
name = stringResource(R.string.confetti_overlay),
|
||||
enabled = enabled,
|
||||
onCheckedChange = { checked ->
|
||||
onEffectControlsStateChange(
|
||||
effectControlsState.copy(effectsChanged = true, confettiOverlayChecked = checked)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
item {
|
||||
EffectItem(
|
||||
name = stringResource(R.string.custom_text_overlay),
|
||||
enabled = enabled,
|
||||
onCheckedChange = { checked ->
|
||||
onEffectControlsStateChange(
|
||||
effectControlsState.copy(effectsChanged = !checked, textOverlayChecked = checked)
|
||||
)
|
||||
},
|
||||
) {
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = effectControlsState.textOverlayText ?: "",
|
||||
onValueChange = { newTextOverlayText ->
|
||||
onEffectControlsStateChange(
|
||||
effectControlsState.copy(
|
||||
effectsChanged = true,
|
||||
textOverlayText = newTextOverlayText.ifEmpty { null },
|
||||
)
|
||||
)
|
||||
},
|
||||
label = { Text(stringResource(R.string.text)) },
|
||||
singleLine = true,
|
||||
modifier =
|
||||
Modifier.fillMaxWidth().padding(bottom = dimensionResource(R.dimen.large_padding)),
|
||||
)
|
||||
Row {
|
||||
ColorsDropDownMenu(effectControlsState.textOverlayColor) { color ->
|
||||
onEffectControlsStateChange(
|
||||
effectControlsState.copy(
|
||||
effectsChanged = effectControlsState.textOverlayText != null,
|
||||
textOverlayColor = color,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Row {
|
||||
Text(
|
||||
text =
|
||||
stringResource(R.string.alpha) +
|
||||
" = %.2f".format(effectControlsState.textOverlayAlpha),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier =
|
||||
Modifier.padding(dimensionResource(id = R.dimen.large_padding)).weight(1f),
|
||||
)
|
||||
Slider(
|
||||
value = effectControlsState.textOverlayAlpha,
|
||||
onValueChange = { newAlphaValue ->
|
||||
val newRoundedAlphaValue = "%.2f".format(newAlphaValue).toFloat()
|
||||
onEffectControlsStateChange(
|
||||
effectControlsState.copy(
|
||||
effectsChanged = effectControlsState.textOverlayText != null,
|
||||
textOverlayAlpha = newRoundedAlphaValue,
|
||||
)
|
||||
)
|
||||
},
|
||||
valueRange = 0f..1f,
|
||||
modifier = Modifier.weight(2f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@kotlin.OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ColorsDropDownMenu(color: Color, onItemSelected: (Color) -> Unit) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it },
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = dimensionResource(R.dimen.large_padding)),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable),
|
||||
value = COLOR_NAMES[color] ?: stringResource(R.string.unknown_color),
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = true,
|
||||
label = { Text(stringResource(R.string.text_color)) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
colors = ExposedDropdownMenuDefaults.textFieldColors(),
|
||||
)
|
||||
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
for (color in COLORS) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
COLOR_NAMES[color] ?: stringResource(R.string.unknown_color),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onItemSelected(color)
|
||||
expanded = false
|
||||
},
|
||||
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
|
||||
leadingIcon = {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.size(dimensionResource(R.dimen.color_circle_size))
|
||||
.background(color, CircleShape)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EffectItem(
|
||||
name: String,
|
||||
enabled: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit = {},
|
||||
content: @Composable () -> Unit = {},
|
||||
) {
|
||||
var checked by rememberSaveable { mutableStateOf(false) }
|
||||
Card(
|
||||
modifier =
|
||||
Modifier.padding(
|
||||
vertical = dimensionResource(id = R.dimen.small_padding),
|
||||
horizontal = dimensionResource(id = R.dimen.regular_padding),
|
||||
)
|
||||
.clickable(enabled = enabled && !checked) {
|
||||
checked = !checked
|
||||
onCheckedChange(checked)
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
Modifier.padding(dimensionResource(id = R.dimen.large_padding))
|
||||
.animateContentSize(animationSpec = tween(durationMillis = 200, easing = LinearEasing))
|
||||
) {
|
||||
Row {
|
||||
Column(Modifier.weight(1f).padding(dimensionResource(id = R.dimen.large_padding))) {
|
||||
Text(text = name, style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
Checkbox(
|
||||
enabled = enabled,
|
||||
checked = checked,
|
||||
onCheckedChange = {
|
||||
checked = !checked
|
||||
onCheckedChange(checked)
|
||||
},
|
||||
)
|
||||
}
|
||||
if (checked) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class EffectControlsState(
|
||||
val effectsChanged: Boolean = false,
|
||||
val contrastValue: Float = 0f,
|
||||
val confettiOverlayChecked: Boolean = false,
|
||||
val textOverlayChecked: Boolean = false,
|
||||
val textOverlayText: String? = null,
|
||||
val textOverlayColor: Color = COLORS[0],
|
||||
val textOverlayAlpha: Float = 1f,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val JSON_FILENAME = "media.playlist.json"
|
||||
val COLORS =
|
||||
listOf(
|
||||
Color.Black,
|
||||
Color.DarkGray,
|
||||
Color.Gray,
|
||||
Color.LightGray,
|
||||
Color.White,
|
||||
Color.Red,
|
||||
Color.Green,
|
||||
Color.Blue,
|
||||
Color.Yellow,
|
||||
Color.Cyan,
|
||||
Color.Magenta,
|
||||
)
|
||||
val COLOR_NAMES =
|
||||
mapOf(
|
||||
Color.Black to "Black",
|
||||
Color.DarkGray to "Dark Gray",
|
||||
Color.Gray to "Gray",
|
||||
Color.LightGray to "Light Gray",
|
||||
Color.White to "White",
|
||||
Color.Red to "Red",
|
||||
Color.Green to "Green",
|
||||
Color.Blue to "Blue",
|
||||
Color.Yellow to "Yellow",
|
||||
Color.Cyan to "Cyan",
|
||||
Color.Magenta to "Magenta",
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.demo.effect
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.JsonReader
|
||||
import android.util.Log
|
||||
import androidx.media3.common.MediaItem
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal suspend fun loadPlaylistsFromJson(
|
||||
jsonFilename: String,
|
||||
context: Context,
|
||||
tag: String,
|
||||
): List<PlaylistHolder> =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
context.assets.open(jsonFilename).use { inputStream ->
|
||||
val reader = JsonReader(InputStreamReader(inputStream, StandardCharsets.UTF_8))
|
||||
val playlistHolders = buildList {
|
||||
reader.beginArray()
|
||||
while (reader.hasNext()) {
|
||||
readPlaylist(reader)?.let { add(it) }
|
||||
}
|
||||
reader.endArray()
|
||||
}
|
||||
playlistHolders
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(tag, context.getString(R.string.playlist_loading_error, jsonFilename, e))
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun readPlaylist(reader: JsonReader): PlaylistHolder? {
|
||||
val playlistHolder = PlaylistHolder("", emptyList())
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
val name = reader.nextName()
|
||||
if (name.equals("name")) {
|
||||
playlistHolder.title = reader.nextString()
|
||||
} else if (name.equals("playlist")) {
|
||||
playlistHolder.mediaItems = buildList {
|
||||
reader.beginArray()
|
||||
while (reader.hasNext()) {
|
||||
reader.beginObject()
|
||||
reader.nextName()
|
||||
add(MediaItem.fromUri(Uri.parse(reader.nextString())))
|
||||
reader.endObject()
|
||||
}
|
||||
reader.endArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
// Only return the playlistHolder object if it has media items
|
||||
return if (playlistHolder.mediaItems.isNotEmpty()) playlistHolder else null
|
||||
}
|
||||
|
||||
internal data class PlaylistHolder(var title: String, var mediaItems: List<MediaItem>)
|
BIN
demos/effect/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
demos/effect/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
BIN
demos/effect/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
demos/effect/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
BIN
demos/effect/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
demos/effect/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
BIN
demos/effect/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
demos/effect/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.3 KiB |
BIN
demos/effect/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
demos/effect/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
31
demos/effect/src/main/res/values-night/themes.xml
Normal file
31
demos/effect/src/main/res/values-night/themes.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2024 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Media3EffectDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
24
demos/effect/src/main/res/values/colors.xml
Normal file
24
demos/effect/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2024 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
22
demos/effect/src/main/res/values/dimens.xml
Normal file
22
demos/effect/src/main/res/values/dimens.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2024 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<dimen name="small_padding">4dp</dimen>
|
||||
<dimen name="regular_padding">8dp</dimen>
|
||||
<dimen name="large_padding">12dp</dimen>
|
||||
<dimen name="android_view_height">256dp</dimen>
|
||||
<dimen name="color_circle_size">40dp</dimen>
|
||||
</resources>
|
33
demos/effect/src/main/res/values/strings.xml
Normal file
33
demos/effect/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2024 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">Effect Demo</string>
|
||||
<string name="choose_preset_input">Choose preset input</string>
|
||||
<string name="choose_local_file">Choose local file</string>
|
||||
<string name="apply_effects">Apply effects</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="playlist_loading_error">Error loading playlist from %1$s: %2$s</string>
|
||||
<string name="no_loaded_playlists_error">There are no loaded preset inputs.</string>
|
||||
<string name="can_not_open_file_error">"File couldn't be opened. Please try again."</string>
|
||||
<string name="permission_not_granted_error">"Permission was not granted."</string>
|
||||
<string name="contrast">Contrast</string>
|
||||
<string name="confetti_overlay">Confetti Overlay</string>
|
||||
<string name="custom_text_overlay">Custom Text Overlay</string>
|
||||
<string name="text">Text</string>
|
||||
<string name="text_color">Text color</string>
|
||||
<string name="unknown_color">Unknown color</string>
|
||||
<string name="alpha">Alpha</string>
|
||||
</resources>
|
31
demos/effect/src/main/res/values/themes.xml
Normal file
31
demos/effect/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2024 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Media3EffectDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
@ -14,7 +14,6 @@
|
||||
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
namespace 'androidx.media3.demo.main'
|
||||
@ -90,6 +89,7 @@ dependencies {
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-iamf')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-vp9')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-midi')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-mpegh')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'lib-datasource-rtmp')
|
||||
}
|
||||
|
||||
|
@ -257,6 +257,10 @@
|
||||
{
|
||||
"name": "Apple media playlist (AAC)",
|
||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8"
|
||||
},
|
||||
{
|
||||
"name": "Bitmovin (FMP4)",
|
||||
"uri": "https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s-fmp4/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -280,15 +284,25 @@
|
||||
"name": "IMA sample ad tags",
|
||||
"samples": [
|
||||
{
|
||||
"name": "Single inline linear",
|
||||
"name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator="
|
||||
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator="
|
||||
},
|
||||
{
|
||||
"name": "VMAP empty midroll",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||
"ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll"
|
||||
},
|
||||
{
|
||||
"name": "Single skippable inline",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dskippablelinear&correlator="
|
||||
},
|
||||
{
|
||||
"name": "Single inline linear",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator="
|
||||
},
|
||||
{
|
||||
"name": "Single redirect linear",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||
@ -349,16 +363,6 @@
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpodbumper&cmsid=496&vid=short_onecue&correlator="
|
||||
},
|
||||
{
|
||||
"name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator="
|
||||
},
|
||||
{
|
||||
"name": "VMAP empty midroll",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||
"ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll"
|
||||
},
|
||||
{
|
||||
"name": "VMAP full, empty, full midrolls",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||
@ -396,6 +400,10 @@
|
||||
{
|
||||
"name": "IMA DAI streams",
|
||||
"samples": [
|
||||
{
|
||||
"name": "DASH VOD: Tears of Steel (11 periods, pre/mid/post), 2/5/2 ads [5/10s]",
|
||||
"uri": "ssai://dai.google.com/?contentSourceId=2559737&videoId=tos-dash&format=0&adsId=1"
|
||||
},
|
||||
{
|
||||
"name": "HLS VOD: Demo (skippable pre/post), single ads [30 s]",
|
||||
"uri": "ssai://dai.google.com/?contentSourceId=2483977&videoId=ima-vod-skippable-test&format=2&adsId=1"
|
||||
@ -408,10 +416,6 @@
|
||||
"name": "HLS Live: Big Buck Bunny (mid), 3 ads [10/10/10s]",
|
||||
"uri": "ssai://dai.google.com/?assetKey=sN_IYUG8STe1ZzhIIE_ksA&format=2&adsId=3"
|
||||
},
|
||||
{
|
||||
"name": "DASH VOD: Tears of Steel (11 periods, pre/mid/post), 2/5/2 ads [5/10s]",
|
||||
"uri": "ssai://dai.google.com/?contentSourceId=2559737&videoId=tos-dash&format=0&adsId=1"
|
||||
},
|
||||
{
|
||||
"name": "DASH live: Tears of Steel (mid), 3 ads each [10 s]",
|
||||
"uri": "ssai://dai.google.com/?assetKey=jNVjPZwzSkyeGiaNQTPqiQ&format=0&adsId=1"
|
||||
@ -420,6 +424,34 @@
|
||||
"name": "DASH live: New Tears of Steel (mid), 3 ads each [10 s]",
|
||||
"uri": "ssai://dai.google.com/?assetKey=PSzZMzAkSXCmlJOWDmRj8Q&format=0&adsId=12"
|
||||
},
|
||||
{
|
||||
"name": "Playlist: No ads - HLS live: Big Buck Bunny - No ads",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "ssai://dai.google.com/?assetKey=sN_IYUG8STe1ZzhIIE_ksA&format=2&adsId=3"
|
||||
},
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Playlist: No ads - DASH live: Tears of Steel (mid) - No ads",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "ssai://dai.google.com/?assetKey=PSzZMzAkSXCmlJOWDmRj8Q&format=0&adsId=1"
|
||||
},
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "DASH live: Unencrypted stream with 30s ad breaks every minute",
|
||||
"uri": "ssai://dai.google.com/?assetKey=0ndl1dJcRmKDUPxTRjvdog&format=0&adsId=21"
|
||||
@ -480,34 +512,6 @@
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Playlist: No ads - DASH live: Tears of Steel (mid) - No ads",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "ssai://dai.google.com/?assetKey=PSzZMzAkSXCmlJOWDmRj8Q&format=0&adsId=1"
|
||||
},
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Playlist: No ads - HLS live: Big Buck Bunny - No ads",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
},
|
||||
{
|
||||
"uri": "ssai://dai.google.com/?assetKey=sN_IYUG8STe1ZzhIIE_ksA&format=2&adsId=3"
|
||||
},
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -762,6 +766,10 @@
|
||||
{
|
||||
"name": "Immersive Audio Format Sample (MP4, IAMF)",
|
||||
"uri": "https://github.com/AOMediaCodec/libiamf/raw/main/tests/test_000036_s.mp4"
|
||||
},
|
||||
{
|
||||
"name": "MPEG-H HD (MP4, H265)",
|
||||
"uri": "https://media.githubusercontent.com/media/Fraunhofer-IIS/mpegh-test-content/main/TRI_Fileset_17_514H_D1_D2_D3_O1_24bit1080p50.mp4"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -315,7 +315,7 @@ public class DownloadTracker {
|
||||
TrackSelectionDialog.createForTracksAndParameters(
|
||||
/* titleId= */ R.string.exo_download_description,
|
||||
tracks,
|
||||
DownloadHelper.getDefaultTrackSelectorParameters(context),
|
||||
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
|
||||
/* allowAdaptiveSelections= */ false,
|
||||
/* allowMultipleOverrides= */ true,
|
||||
/* onTracksSelectedListener= */ this,
|
||||
|
@ -61,7 +61,6 @@ import androidx.media3.datasource.DataSourceUtil;
|
||||
import androidx.media3.datasource.DataSpec;
|
||||
import androidx.media3.exoplayer.RenderersFactory;
|
||||
import androidx.media3.exoplayer.offline.DownloadService;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.io.IOException;
|
||||
@ -74,6 +73,7 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
@ -524,7 +524,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
|
||||
private PlaylistGroup getGroup(String groupName, List<PlaylistGroup> groups) {
|
||||
for (int i = 0; i < groups.size(); i++) {
|
||||
if (Objects.equal(groupName, groups.get(i).title)) {
|
||||
if (Objects.equals(groupName, groups.get(i).title)) {
|
||||
return groups.get(i);
|
||||
}
|
||||
}
|
||||
|
@ -41,8 +41,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
|
||||
MediaItemTree.initialize(context.assets)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) // TODO: b/328238954 - Remove once new CommandButton icons are stable.
|
||||
private val customLayoutCommandButtons: List<CommandButton> =
|
||||
private val commandButtons: List<CommandButton> =
|
||||
listOf(
|
||||
CommandButton.Builder(CommandButton.ICON_SHUFFLE_OFF)
|
||||
.setDisplayName(context.getString(R.string.exo_controls_shuffle_on_description))
|
||||
@ -59,7 +58,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
|
||||
MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
|
||||
.also { builder ->
|
||||
// Put all custom session commands in the list that may be used by the notification.
|
||||
customLayoutCommandButtons.forEach { commandButton ->
|
||||
commandButtons.forEach { commandButton ->
|
||||
commandButton.sessionCommand?.let { builder.add(it) }
|
||||
}
|
||||
}
|
||||
@ -78,13 +77,13 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
|
||||
session.isAutoCompanionController(controller)
|
||||
) {
|
||||
// Select the button to display.
|
||||
val customLayout = customLayoutCommandButtons[if (session.player.shuffleModeEnabled) 1 else 0]
|
||||
val customButton = commandButtons[if (session.player.shuffleModeEnabled) 1 else 0]
|
||||
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||
.setAvailableSessionCommands(mediaNotificationSessionCommands)
|
||||
.setCustomLayout(ImmutableList.of(customLayout))
|
||||
.setMediaButtonPreferences(ImmutableList.of(customButton))
|
||||
.build()
|
||||
}
|
||||
// Default commands without custom layout for common controllers.
|
||||
// Default commands without media button preferences for common controllers.
|
||||
return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build()
|
||||
}
|
||||
|
||||
@ -98,19 +97,19 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
|
||||
if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) {
|
||||
// Enable shuffling.
|
||||
session.player.shuffleModeEnabled = true
|
||||
// Change the custom layout to contain the `Disable shuffling` command.
|
||||
session.setCustomLayout(
|
||||
// Change the media button preferences to contain the `Disable shuffling` button.
|
||||
session.setMediaButtonPreferences(
|
||||
session.mediaNotificationControllerInfo!!,
|
||||
ImmutableList.of(customLayoutCommandButtons[1]),
|
||||
ImmutableList.of(commandButtons[1]),
|
||||
)
|
||||
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||
} else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) {
|
||||
// Disable shuffling.
|
||||
session.player.shuffleModeEnabled = false
|
||||
// Change the custom layout to contain the `Enable shuffling` command.
|
||||
session.setCustomLayout(
|
||||
// Change the media button preferences to contain the `Enable shuffling` button.
|
||||
session.setMediaButtonPreferences(
|
||||
session.mediaNotificationControllerInfo!!,
|
||||
ImmutableList.of(customLayoutCommandButtons[0]),
|
||||
ImmutableList.of(commandButtons[0]),
|
||||
)
|
||||
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||
}
|
||||
|
@ -76,6 +76,7 @@ dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
|
||||
implementation 'androidx.constraintlayout:constraintlayout:' + androidxConstraintLayoutVersion
|
||||
implementation 'androidx.recyclerview:recyclerview:' + androidxRecyclerViewVersion
|
||||
implementation 'androidx.window:window:' + androidxWindowVersion
|
||||
implementation 'com.google.android.material:material:' + androidxMaterialVersion
|
||||
implementation project(modulePrefix + 'lib-effect')
|
||||
implementation project(modulePrefix + 'lib-exoplayer')
|
||||
|
@ -24,6 +24,10 @@
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||
|
||||
<!-- For media projection. -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@ -64,5 +68,9 @@
|
||||
android:label="@string/app_name"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar"/>
|
||||
<service
|
||||
android:name=".TransformerActivity$DemoMediaProjectionService"
|
||||
android:foregroundServiceType="mediaProjection"
|
||||
android:exported="false"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.media3.demo.transformer;
|
||||
|
||||
import android.animation.FloatEvaluator;
|
||||
import android.animation.Keyframe;
|
||||
import android.animation.PropertyValuesHolder;
|
||||
import android.animation.TypeEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.util.Pair;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
import androidx.media3.common.OverlaySettings;
|
||||
import androidx.media3.common.VideoFrameProcessingException;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.effect.DrawableOverlay;
|
||||
import androidx.media3.effect.TextureOverlay;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
/**
|
||||
* An animated {@link TextureOverlay} using {@link android.animation}.
|
||||
*
|
||||
* <p>The rotation is controlled by a simple {@link ValueAnimator}, while the position is controlled
|
||||
* by key frames.
|
||||
*/
|
||||
public class AnimatedLogoOverlay extends DrawableOverlay {
|
||||
|
||||
private static final long ROTATION_PERIOD_MS = 2_000;
|
||||
private static final long POSITION_PERIOD_MS = 5_000;
|
||||
private static final float POSITION_X_BOUND = 0.8f;
|
||||
private static final float POSITION_Y_BOUND = 0.7f;
|
||||
|
||||
private final Drawable logo;
|
||||
private final AnimatedOverlaySettings overlaySettings;
|
||||
|
||||
public AnimatedLogoOverlay(Context context) {
|
||||
try {
|
||||
logo = context.getPackageManager().getApplicationIcon(context.getPackageName());
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
logo.setBounds(
|
||||
/* left= */ 0, /* top= */ 0, logo.getIntrinsicWidth(), logo.getIntrinsicHeight());
|
||||
|
||||
ValueAnimator rotationAnimator = ValueAnimator.ofFloat(0, 360);
|
||||
rotationAnimator.setRepeatMode(ValueAnimator.RESTART);
|
||||
rotationAnimator.setRepeatCount(ValueAnimator.INFINITE);
|
||||
rotationAnimator.setDuration(ROTATION_PERIOD_MS);
|
||||
// Rotate the logo with a constant angular velocity.
|
||||
rotationAnimator.setInterpolator(new LinearInterpolator());
|
||||
|
||||
Keyframe[] keyFrames = new Keyframe[5];
|
||||
keyFrames[0] =
|
||||
Keyframe.ofObject(/* fraction= */ 0f, Pair.create(-POSITION_X_BOUND, -POSITION_Y_BOUND));
|
||||
keyFrames[2] =
|
||||
Keyframe.ofObject(/* fraction= */ 0.5f, Pair.create(POSITION_X_BOUND, POSITION_Y_BOUND));
|
||||
keyFrames[1] =
|
||||
Keyframe.ofObject(/* fraction= */ 0.25f, Pair.create(-POSITION_X_BOUND, POSITION_Y_BOUND));
|
||||
keyFrames[3] =
|
||||
Keyframe.ofObject(/* fraction= */ 0.75f, Pair.create(POSITION_X_BOUND, -POSITION_Y_BOUND));
|
||||
keyFrames[4] =
|
||||
Keyframe.ofObject(/* fraction= */ 1f, Pair.create(-POSITION_X_BOUND, -POSITION_Y_BOUND));
|
||||
PropertyValuesHolder positionValuesHolder =
|
||||
PropertyValuesHolder.ofKeyframe("position", keyFrames);
|
||||
|
||||
ValueAnimator positionAnimator = ValueAnimator.ofPropertyValuesHolder(positionValuesHolder);
|
||||
// The position can also be animated using separate animators for x and y, the purpose of
|
||||
// PairEvaluator is to use one animator for both x and y.
|
||||
positionAnimator.setEvaluator(new AnimatedOverlaySettings.PairEvaluator());
|
||||
positionAnimator.setRepeatMode(ValueAnimator.RESTART);
|
||||
positionAnimator.setRepeatCount(ValueAnimator.INFINITE);
|
||||
positionAnimator.setDuration(POSITION_PERIOD_MS);
|
||||
|
||||
overlaySettings = new AnimatedOverlaySettings(rotationAnimator, positionAnimator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable getDrawable(long presentationTimeUs) {
|
||||
return logo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OverlaySettings getOverlaySettings(long presentationTimeUs) {
|
||||
overlaySettings.setCurrentPresentationTimeUs(presentationTimeUs);
|
||||
return overlaySettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() throws VideoFrameProcessingException {
|
||||
super.release();
|
||||
overlaySettings.stopAnimation();
|
||||
}
|
||||
|
||||
private static class AnimatedOverlaySettings implements OverlaySettings {
|
||||
private final ValueAnimator rotationAnimator;
|
||||
private final ValueAnimator positionAnimator;
|
||||
private final Handler mainThreadHandler;
|
||||
|
||||
private boolean started;
|
||||
|
||||
public AnimatedOverlaySettings(ValueAnimator rotationAnimator, ValueAnimator positionAnimator) {
|
||||
this.rotationAnimator = rotationAnimator;
|
||||
this.positionAnimator = positionAnimator;
|
||||
mainThreadHandler = new Handler(Util.getCurrentOrMainLooper());
|
||||
}
|
||||
|
||||
public void setCurrentPresentationTimeUs(long presentationTimeUs) {
|
||||
// Sets the animation time to the video presentation time, so the animation is presentation
|
||||
// time based.
|
||||
rotationAnimator.setCurrentPlayTime(presentationTimeUs / 1000);
|
||||
positionAnimator.setCurrentPlayTime(presentationTimeUs / 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getRotationDegrees() {
|
||||
maybeStartAnimator();
|
||||
return (float) rotationAnimator.getAnimatedValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<Float, Float> getBackgroundFrameAnchor() {
|
||||
maybeStartAnimator();
|
||||
return (Pair<Float, Float>) positionAnimator.getAnimatedValue();
|
||||
}
|
||||
|
||||
public void stopAnimation() {
|
||||
mainThreadHandler.post(
|
||||
() -> {
|
||||
rotationAnimator.cancel();
|
||||
positionAnimator.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
private void maybeStartAnimator() {
|
||||
if (!started) {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
mainThreadHandler.post(
|
||||
() -> {
|
||||
rotationAnimator.start();
|
||||
positionAnimator.start();
|
||||
latch.countDown();
|
||||
});
|
||||
try {
|
||||
// Block until the animators are actually started, or they'll return null values.
|
||||
latch.await();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
started = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** An {@link TypeEvaluator} to animate position in the form of {@link Pair} of floats. */
|
||||
private static class PairEvaluator implements TypeEvaluator<Pair<Float, Float>> {
|
||||
private final FloatEvaluator floatEvaluator;
|
||||
|
||||
private PairEvaluator() {
|
||||
floatEvaluator = new FloatEvaluator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<Float, Float> evaluate(
|
||||
float fraction, Pair<Float, Float> startValue, Pair<Float, Float> endValue) {
|
||||
return Pair.create(
|
||||
floatEvaluator.evaluate(fraction, startValue.first, endValue.first),
|
||||
floatEvaluator.evaluate(fraction, startValue.second, endValue.second));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.media3.demo.transformer;
|
||||
|
||||
import static java.lang.Math.toRadians;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.RectF;
|
||||
import androidx.media3.common.OverlaySettings;
|
||||
import androidx.media3.effect.CanvasOverlay;
|
||||
import androidx.media3.effect.StaticOverlaySettings;
|
||||
|
||||
/* package */ final class ClockOverlay extends CanvasOverlay {
|
||||
private static final int CLOCK_COLOR = Color.WHITE;
|
||||
|
||||
private static final int DIAL_SIZE = 200;
|
||||
private static final float DIAL_WIDTH = 3.f;
|
||||
private static final float NEEDLE_WIDTH = 3.f;
|
||||
private static final int NEEDLE_LENGTH = DIAL_SIZE / 2 - 20;
|
||||
private static final int CENTRE_X = DIAL_SIZE / 2;
|
||||
private static final int CENTRE_Y = DIAL_SIZE / 2;
|
||||
private static final int DIAL_INSET = 5;
|
||||
private static final RectF DIAL_BOUND =
|
||||
new RectF(
|
||||
/* left= */ DIAL_INSET,
|
||||
/* top= */ DIAL_INSET,
|
||||
/* right= */ DIAL_SIZE - DIAL_INSET,
|
||||
/* bottom= */ DIAL_SIZE - DIAL_INSET);
|
||||
private static final int HUB_SIZE = 5;
|
||||
|
||||
private static final float BOTTOM_RIGHT_ANCHOR_X = 1.f;
|
||||
private static final float BOTTOM_RIGHT_ANCHOR_Y = -1.f;
|
||||
private static final float ANCHOR_INSET_X = 0.1f;
|
||||
private static final float ANCHOR_INSET_Y = -0.1f;
|
||||
|
||||
private final Paint dialPaint;
|
||||
private final Paint needlePaint;
|
||||
private final Paint hubPaint;
|
||||
|
||||
public ClockOverlay() {
|
||||
super(/* useInputFrameSize= */ false);
|
||||
setCanvasSize(/* width= */ DIAL_SIZE, /* height= */ DIAL_SIZE);
|
||||
|
||||
dialPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
dialPaint.setStyle(Paint.Style.STROKE);
|
||||
dialPaint.setStrokeWidth(DIAL_WIDTH);
|
||||
dialPaint.setColor(CLOCK_COLOR);
|
||||
|
||||
needlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
needlePaint.setStrokeWidth(NEEDLE_WIDTH);
|
||||
needlePaint.setColor(CLOCK_COLOR);
|
||||
|
||||
hubPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
hubPaint.setColor(CLOCK_COLOR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas, long presentationTimeUs) {
|
||||
// Clears the canvas
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||
|
||||
// Draw the dial
|
||||
canvas.drawArc(
|
||||
DIAL_BOUND, /* startAngle= */ 0, /* sweepAngle= */ 360, /* useCenter= */ false, dialPaint);
|
||||
|
||||
// Draw the needle
|
||||
float angle = 6 * presentationTimeUs / 1_000_000.f - 90;
|
||||
double radians = toRadians(angle);
|
||||
|
||||
float startX = CENTRE_X - (float) (10 * Math.cos(radians));
|
||||
float startY = CENTRE_Y - (float) (10 * Math.sin(radians));
|
||||
float endX = CENTRE_X + (float) (NEEDLE_LENGTH * Math.cos(radians));
|
||||
float endY = CENTRE_Y + (float) (NEEDLE_LENGTH * Math.sin(radians));
|
||||
|
||||
canvas.drawLine(startX, startY, endX, endY, needlePaint);
|
||||
|
||||
// Draw a small hub at the center
|
||||
canvas.drawCircle(CENTRE_X, CENTRE_Y, HUB_SIZE, hubPaint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OverlaySettings getOverlaySettings(long presentationTimeUs) {
|
||||
return new StaticOverlaySettings.Builder()
|
||||
.setBackgroundFrameAnchor(
|
||||
BOTTOM_RIGHT_ANCHOR_X - ANCHOR_INSET_X, BOTTOM_RIGHT_ANCHOR_Y - ANCHOR_INSET_Y)
|
||||
.setOverlayFrameAnchor(BOTTOM_RIGHT_ANCHOR_X, BOTTOM_RIGHT_ANCHOR_Y)
|
||||
.build();
|
||||
}
|
||||
}
|
@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.media3.demo.transformer;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Handler;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.VideoFrameProcessingException;
|
||||
import androidx.media3.common.util.Size;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.effect.CanvasOverlay;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/** Mimics an emitter of confetti, dropping from the center of the frame. */
|
||||
/* package */ final class ConfettiOverlay extends CanvasOverlay {
|
||||
|
||||
private static final ImmutableList<String> CONFETTI_TEXTS =
|
||||
ImmutableList.of("❊", "✿", "❊", "✦︎", "♥︎", "☕︎");
|
||||
private static final int EMITTER_POSITION_Y = -50;
|
||||
private static final int CONFETTI_BASE_SIZE = 30;
|
||||
private static final int CONFETTI_SIZE_VARIATION = 10;
|
||||
|
||||
private final List<Confetti> confettiList;
|
||||
private final Random random;
|
||||
private final Paint paint;
|
||||
private final Handler handler;
|
||||
@Nullable private Runnable runnable;
|
||||
private int width;
|
||||
private int height;
|
||||
private boolean started;
|
||||
|
||||
public ConfettiOverlay() {
|
||||
super(/* useInputFrameSize= */ true);
|
||||
confettiList = new ArrayList<>();
|
||||
random = new Random();
|
||||
paint = new Paint();
|
||||
paint.setAntiAlias(true);
|
||||
handler = new Handler(Util.getCurrentOrMainLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(Size videoSize) {
|
||||
super.configure(videoSize);
|
||||
this.width = videoSize.getWidth();
|
||||
this.height = videoSize.getHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onDraw(Canvas canvas, long presentationTimeUs) {
|
||||
if (!started) {
|
||||
start();
|
||||
}
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||
|
||||
for (int i = 0; i < confettiList.size(); i++) {
|
||||
Confetti confetti = confettiList.get(i);
|
||||
if (confetti.y > (float) height / 2 || confetti.x <= 0 || confetti.x > width) {
|
||||
confettiList.remove(confetti);
|
||||
continue;
|
||||
}
|
||||
confetti.draw(canvas, paint);
|
||||
confetti.update();
|
||||
}
|
||||
}
|
||||
|
||||
/** Starts the confetti. */
|
||||
public void start() {
|
||||
runnable = this::addConfetti;
|
||||
handler.post(runnable);
|
||||
started = true;
|
||||
}
|
||||
|
||||
/** Stops the confetti. */
|
||||
public void stop() {
|
||||
checkStateNotNull(runnable);
|
||||
handler.removeCallbacks(runnable);
|
||||
confettiList.clear();
|
||||
started = false;
|
||||
runnable = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() throws VideoFrameProcessingException {
|
||||
super.release();
|
||||
handler.post(this::stop);
|
||||
}
|
||||
|
||||
private synchronized void addConfetti() {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
confettiList.add(
|
||||
new Confetti(
|
||||
CONFETTI_TEXTS.get(Math.abs(random.nextInt()) % CONFETTI_TEXTS.size()),
|
||||
random,
|
||||
/* x= */ (float) width / 2,
|
||||
/* y= */ EMITTER_POSITION_Y,
|
||||
/* size= */ CONFETTI_BASE_SIZE + random.nextInt(CONFETTI_SIZE_VARIATION),
|
||||
/* color= */ Color.HSVToColor(
|
||||
new float[] {
|
||||
/* hue= */ random.nextInt(360), /* saturation= */ 0.6f, /* value= */ 0.8f
|
||||
})));
|
||||
}
|
||||
handler.postDelayed(this::addConfetti, /* delayMillis= */ 100);
|
||||
}
|
||||
|
||||
private static final class Confetti {
|
||||
private final String text;
|
||||
private final float speedX;
|
||||
private final float speedY;
|
||||
private final int size;
|
||||
private final int color;
|
||||
|
||||
private float x;
|
||||
private float y;
|
||||
|
||||
public Confetti(String text, Random random, float x, float y, int size, int color) {
|
||||
this.text = text;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.size = size;
|
||||
this.color = color;
|
||||
speedX = 4 * (random.nextFloat() * 2 - 1); // Random speed in x direction
|
||||
speedY = 4 * random.nextFloat(); // Random downward speed
|
||||
}
|
||||
|
||||
/** Draws the {@code Confetti} on the {@link Canvas}. */
|
||||
public void draw(Canvas canvas, Paint paint) {
|
||||
canvas.save();
|
||||
paint.setColor(color);
|
||||
paint.setTextSize(size);
|
||||
canvas.drawText(text, x, y, paint);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
/** Updates the {@code Confetti}. */
|
||||
public void update() {
|
||||
x += speedX;
|
||||
y += speedY;
|
||||
}
|
||||
}
|
||||
}
|
@ -77,8 +77,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
public static final String ENABLE_ANALYZER_MODE = "enable_analyzer_mode";
|
||||
public static final String ENABLE_DEBUG_PREVIEW = "enable_debug_preview";
|
||||
public static final String ABORT_SLOW_EXPORT = "abort_slow_export";
|
||||
public static final String USE_MEDIA3_MUXER = "use_media3_muxer";
|
||||
public static final String PRODUCE_FRAGMENTED_MP4 = "produce_fragmented_mp4";
|
||||
public static final String USE_MEDIA3_MP4_MUXER = "use_media3_mp4_muxer";
|
||||
public static final String USE_MEDIA3_FRAGMENTED_MP4_MUXER = "use_media3_fragmented_mp4_muxer";
|
||||
public static final String HDR_MODE = "hdr_mode";
|
||||
public static final String AUDIO_EFFECTS_SELECTIONS = "audio_effects_selections";
|
||||
public static final String VIDEO_EFFECTS_SELECTIONS = "video_effects_selections";
|
||||
@ -114,13 +114,17 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
public static final int OVERLAY_LOGO_AND_TIMER_INDEX = 10;
|
||||
public static final int BITMAP_OVERLAY_INDEX = 11;
|
||||
public static final int TEXT_OVERLAY_INDEX = 12;
|
||||
public static final int CLOCK_OVERLAY_INDEX = 13;
|
||||
public static final int CONFETTI_OVERLAY_INDEX = 14;
|
||||
public static final int ANIMATING_LOGO_OVERLAY = 15;
|
||||
|
||||
// Audio effect selections.
|
||||
public static final int HIGH_PITCHED_INDEX = 0;
|
||||
public static final int SAMPLE_RATE_INDEX = 1;
|
||||
public static final int SKIP_SILENCE_INDEX = 2;
|
||||
public static final int CHANNEL_MIXING_INDEX = 3;
|
||||
public static final int VOLUME_SCALING_INDEX = 4;
|
||||
public static final int SAMPLE_RATE_48K_INDEX = 1;
|
||||
public static final int SAMPLE_RATE_96K_INDEX = 2;
|
||||
public static final int SKIP_SILENCE_INDEX = 3;
|
||||
public static final int CHANNEL_MIXING_INDEX = 4;
|
||||
public static final int VOLUME_SCALING_INDEX = 5;
|
||||
|
||||
// Color filter options.
|
||||
public static final int COLOR_FILTER_GRAYSCALE = 0;
|
||||
@ -173,8 +177,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
private CheckBox enableDebugPreviewCheckBox;
|
||||
private CheckBox enableDebugTracingCheckBox;
|
||||
private CheckBox abortSlowExportCheckBox;
|
||||
private CheckBox useMedia3Muxer;
|
||||
private CheckBox produceFragmentedMp4CheckBox;
|
||||
private CheckBox useMedia3Mp4Muxer;
|
||||
private CheckBox useMedia3FragmentedMp4Muxer;
|
||||
private Spinner hdrModeSpinner;
|
||||
private Button selectAudioEffectsButton;
|
||||
private Button selectVideoEffectsButton;
|
||||
@ -262,7 +266,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
MimeTypes.VIDEO_H264,
|
||||
MimeTypes.VIDEO_H265,
|
||||
MimeTypes.VIDEO_MP4V,
|
||||
MimeTypes.VIDEO_AV1);
|
||||
MimeTypes.VIDEO_AV1,
|
||||
MimeTypes.VIDEO_DOLBY_VISION);
|
||||
|
||||
ArrayAdapter<String> resolutionHeightAdapter =
|
||||
new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
|
||||
@ -299,18 +304,18 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
(buttonView, isChecked) -> DebugTraceUtil.enableTracing = isChecked);
|
||||
|
||||
abortSlowExportCheckBox = findViewById(R.id.abort_slow_export_checkbox);
|
||||
useMedia3Muxer = findViewById(R.id.use_media3_muxer_checkbox);
|
||||
produceFragmentedMp4CheckBox = findViewById(R.id.produce_fragmented_mp4_checkbox);
|
||||
useMedia3Muxer.setOnCheckedChangeListener(
|
||||
(buttonView, isChecked) -> {
|
||||
if (!isChecked) {
|
||||
produceFragmentedMp4CheckBox.setChecked(false);
|
||||
}
|
||||
});
|
||||
produceFragmentedMp4CheckBox.setOnCheckedChangeListener(
|
||||
useMedia3Mp4Muxer = findViewById(R.id.use_media3_mp4_muxer_checkbox);
|
||||
useMedia3FragmentedMp4Muxer = findViewById(R.id.use_media3_fragmented_mp4_muxer_checkbox);
|
||||
useMedia3Mp4Muxer.setOnCheckedChangeListener(
|
||||
(buttonView, isChecked) -> {
|
||||
if (isChecked) {
|
||||
useMedia3Muxer.setChecked(true);
|
||||
useMedia3FragmentedMp4Muxer.setChecked(false);
|
||||
}
|
||||
});
|
||||
useMedia3FragmentedMp4Muxer.setOnCheckedChangeListener(
|
||||
(buttonView, isChecked) -> {
|
||||
if (isChecked) {
|
||||
useMedia3Mp4Muxer.setChecked(false);
|
||||
}
|
||||
});
|
||||
|
||||
@ -403,8 +408,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
bundle.putBoolean(ENABLE_ANALYZER_MODE, enableAnalyzerModeCheckBox.isChecked());
|
||||
bundle.putBoolean(ENABLE_DEBUG_PREVIEW, enableDebugPreviewCheckBox.isChecked());
|
||||
bundle.putBoolean(ABORT_SLOW_EXPORT, abortSlowExportCheckBox.isChecked());
|
||||
bundle.putBoolean(USE_MEDIA3_MUXER, useMedia3Muxer.isChecked());
|
||||
bundle.putBoolean(PRODUCE_FRAGMENTED_MP4, produceFragmentedMp4CheckBox.isChecked());
|
||||
bundle.putBoolean(USE_MEDIA3_MP4_MUXER, useMedia3Mp4Muxer.isChecked());
|
||||
bundle.putBoolean(USE_MEDIA3_FRAGMENTED_MP4_MUXER, useMedia3FragmentedMp4Muxer.isChecked());
|
||||
String selectedHdrMode = String.valueOf(hdrModeSpinner.getSelectedItem());
|
||||
bundle.putInt(HDR_MODE, HDR_MODE_DESCRIPTIONS.get(selectedHdrMode));
|
||||
bundle.putBooleanArray(AUDIO_EFFECTS_SELECTIONS, audioEffectsSelections);
|
||||
|
@ -20,7 +20,8 @@ import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.effect.OverlaySettings;
|
||||
import androidx.media3.common.OverlaySettings;
|
||||
import androidx.media3.effect.StaticOverlaySettings;
|
||||
import androidx.media3.effect.TextOverlay;
|
||||
import androidx.media3.effect.TextureOverlay;
|
||||
import java.util.Locale;
|
||||
@ -31,11 +32,11 @@ import java.util.Locale;
|
||||
*/
|
||||
/* package */ final class TimerOverlay extends TextOverlay {
|
||||
|
||||
private final OverlaySettings overlaySettings;
|
||||
private final StaticOverlaySettings overlaySettings;
|
||||
|
||||
public TimerOverlay() {
|
||||
overlaySettings =
|
||||
new OverlaySettings.Builder()
|
||||
new StaticOverlaySettings.Builder()
|
||||
// Place the timer in the bottom left corner of the screen with some padding from the
|
||||
// edges.
|
||||
.setOverlayFrameAnchor(/* x= */ -1f, /* y= */ -1f)
|
||||
|
@ -15,25 +15,33 @@
|
||||
*/
|
||||
package androidx.media3.demo.transformer;
|
||||
|
||||
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
||||
import static android.Manifest.permission.READ_MEDIA_VIDEO;
|
||||
import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.Assertions.checkState;
|
||||
import static androidx.media3.common.util.Util.SDK_INT;
|
||||
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
|
||||
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS;
|
||||
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_NOT_STARTED;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.projection.MediaProjection;
|
||||
import android.media.projection.MediaProjectionManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
@ -45,10 +53,13 @@ import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.DebugViewProvider;
|
||||
import androidx.media3.common.Effect;
|
||||
@ -71,13 +82,13 @@ import androidx.media3.effect.GlShaderProgram;
|
||||
import androidx.media3.effect.HslAdjustment;
|
||||
import androidx.media3.effect.LanczosResample;
|
||||
import androidx.media3.effect.OverlayEffect;
|
||||
import androidx.media3.effect.OverlaySettings;
|
||||
import androidx.media3.effect.Presentation;
|
||||
import androidx.media3.effect.RgbAdjustment;
|
||||
import androidx.media3.effect.RgbFilter;
|
||||
import androidx.media3.effect.RgbMatrix;
|
||||
import androidx.media3.effect.ScaleAndRotateTransformation;
|
||||
import androidx.media3.effect.SingleColorLut;
|
||||
import androidx.media3.effect.StaticOverlaySettings;
|
||||
import androidx.media3.effect.TextOverlay;
|
||||
import androidx.media3.effect.TextureOverlay;
|
||||
import androidx.media3.exoplayer.DefaultLoadControl;
|
||||
@ -92,12 +103,16 @@ import androidx.media3.transformer.Effects;
|
||||
import androidx.media3.transformer.ExperimentalAnalyzerModeFactory;
|
||||
import androidx.media3.transformer.ExportException;
|
||||
import androidx.media3.transformer.ExportResult;
|
||||
import androidx.media3.transformer.InAppMuxer;
|
||||
import androidx.media3.transformer.InAppFragmentedMp4Muxer;
|
||||
import androidx.media3.transformer.InAppMp4Muxer;
|
||||
import androidx.media3.transformer.JsonUtil;
|
||||
import androidx.media3.transformer.MediaProjectionAssetLoader;
|
||||
import androidx.media3.transformer.ProgressHolder;
|
||||
import androidx.media3.transformer.Transformer;
|
||||
import androidx.media3.transformer.VideoEncoderSettings;
|
||||
import androidx.media3.ui.AspectRatioFrameLayout;
|
||||
import androidx.media3.ui.PlayerView;
|
||||
import androidx.window.layout.WindowMetricsCalculator;
|
||||
import com.google.android.material.card.MaterialCardView;
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator;
|
||||
import com.google.common.base.Stopwatch;
|
||||
@ -109,7 +124,6 @@ import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -120,12 +134,13 @@ import org.json.JSONObject;
|
||||
public final class TransformerActivity extends AppCompatActivity {
|
||||
private static final String TAG = "TransformerActivity";
|
||||
private static final int IMAGE_DURATION_MS = 5_000;
|
||||
private static final int IMAGE_FRAME_RATE_FPS = 30;
|
||||
private static final int DEFAULT_FRAME_RATE_FPS = 30;
|
||||
private static int LOAD_CONTROL_MIN_BUFFER_MS = 5_000;
|
||||
private static int LOAD_CONTROL_MAX_BUFFER_MS = 5_000;
|
||||
|
||||
private Button displayInputButton;
|
||||
private MaterialCardView inputCardView;
|
||||
private MaterialCardView outputCardView;
|
||||
private TextView inputTextView;
|
||||
private ImageView inputImageView;
|
||||
private PlayerView inputPlayerView;
|
||||
@ -137,10 +152,13 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
private LinearProgressIndicator progressIndicator;
|
||||
private Button pauseButton;
|
||||
private Button resumeButton;
|
||||
private Button stopCaptureButton;
|
||||
private Stopwatch exportStopwatch;
|
||||
private AspectRatioFrameLayout debugFrame;
|
||||
|
||||
@Nullable private DebugTextViewHelper debugTextViewHelper;
|
||||
@Nullable private Intent screenCaptureToken;
|
||||
@Nullable private MediaProjection mediaProjection;
|
||||
@Nullable private ExoPlayer inputPlayer;
|
||||
@Nullable private ExoPlayer outputPlayer;
|
||||
@Nullable private Transformer transformer;
|
||||
@ -153,6 +171,7 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
setContentView(R.layout.transformer_activity);
|
||||
|
||||
inputCardView = findViewById(R.id.input_card_view);
|
||||
outputCardView = findViewById(R.id.output_card_view);
|
||||
inputTextView = findViewById(R.id.input_text_view);
|
||||
inputImageView = findViewById(R.id.input_image_view);
|
||||
inputPlayerView = findViewById(R.id.input_player_view);
|
||||
@ -166,6 +185,8 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
pauseButton.setOnClickListener(view -> pauseExport());
|
||||
resumeButton = findViewById(R.id.resume_button);
|
||||
resumeButton.setOnClickListener(view -> startExport());
|
||||
stopCaptureButton = findViewById(R.id.stop_capture_button);
|
||||
stopCaptureButton.setOnClickListener(view -> mediaProjection.stop());
|
||||
debugFrame = findViewById(R.id.debug_aspect_ratio_frame_layout);
|
||||
displayInputButton = findViewById(R.id.display_input_button);
|
||||
displayInputButton.setOnClickListener(view -> toggleInputVideoDisplay());
|
||||
@ -184,7 +205,10 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
startExport();
|
||||
// Restart exporting, unless this is a capture session which can run in the background.
|
||||
if (!isUsingMediaProjection()) {
|
||||
startExport();
|
||||
}
|
||||
|
||||
inputPlayerView.onResume();
|
||||
outputPlayerView.onResume();
|
||||
@ -194,32 +218,72 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
|
||||
if (transformer != null) {
|
||||
transformer.cancel();
|
||||
transformer = null;
|
||||
}
|
||||
|
||||
// The stop watch is reset after cancelling the export, in case cancelling causes the stop watch
|
||||
// to be stopped in a transformer callback.
|
||||
exportStopwatch.reset();
|
||||
|
||||
inputPlayerView.onPause();
|
||||
outputPlayerView.onPause();
|
||||
releasePlayer();
|
||||
|
||||
outputFile.delete();
|
||||
outputFile = null;
|
||||
if (oldOutputFile != null) {
|
||||
oldOutputFile.delete();
|
||||
oldOutputFile = null;
|
||||
// Keep the capture session going to allow capturing other apps while backgrounded.
|
||||
if (!isUsingMediaProjection()) {
|
||||
releasePlayers();
|
||||
cleanUpExport();
|
||||
}
|
||||
}
|
||||
|
||||
private void startExport() {
|
||||
requestReadVideoPermission(/* activity= */ this);
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
if (isUsingMediaProjection()) {
|
||||
releasePlayers();
|
||||
mediaProjection.stop();
|
||||
mediaProjection = null;
|
||||
screenCaptureToken = null;
|
||||
}
|
||||
cleanUpExport();
|
||||
}
|
||||
|
||||
private void startExport() {
|
||||
Intent intent = getIntent();
|
||||
Uri inputUri = checkNotNull(intent.getData());
|
||||
|
||||
if (inputUri.toString().equals("transformer_surface_asset:media_projection")
|
||||
&& screenCaptureToken == null) {
|
||||
// MediaProjection can only start once the foreground service is running.
|
||||
MediaProjectionManager mediaProjectionManager =
|
||||
(MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
|
||||
Context context = this;
|
||||
LocalBroadcastManager.getInstance(context)
|
||||
.registerReceiver(
|
||||
new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = checkNotNull(intent.getAction());
|
||||
if (action.equals(DemoMediaProjectionService.ACTION_EVENT_STARTED)) {
|
||||
LocalBroadcastManager.getInstance(context)
|
||||
.unregisterReceiver(/* receiver= */ this);
|
||||
// The service has started so media projection can start.
|
||||
startExport();
|
||||
}
|
||||
}
|
||||
},
|
||||
new IntentFilter(DemoMediaProjectionService.ACTION_EVENT_STARTED));
|
||||
registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
activityResult -> {
|
||||
int resultCode = activityResult.getResultCode();
|
||||
if (resultCode == RESULT_OK) {
|
||||
screenCaptureToken = activityResult.getData();
|
||||
Intent startServiceIntent = new Intent(context, DemoMediaProjectionService.class);
|
||||
ContextCompat.startForegroundService(context, startServiceIntent);
|
||||
} else if (resultCode == RESULT_CANCELED) {
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.launch(mediaProjectionManager.createScreenCaptureIntent());
|
||||
inputCardView.setVisibility(View.GONE);
|
||||
outputCardView.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
outputFile =
|
||||
createExternalCacheFile("transformer-output-" + Clock.DEFAULT.elapsedRealtime() + ".mp4");
|
||||
@ -229,6 +293,7 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
String outputFilePath = outputFile.getAbsolutePath();
|
||||
@Nullable Bundle bundle = intent.getExtras();
|
||||
MediaItem mediaItem = createMediaItem(bundle, inputUri);
|
||||
Util.maybeRequestReadStoragePermission(/* activity= */ this, mediaItem);
|
||||
Transformer transformer = createTransformer(bundle, inputUri, outputFilePath);
|
||||
Composition composition = createComposition(mediaItem, bundle);
|
||||
exportStopwatch.reset();
|
||||
@ -245,10 +310,20 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
outputVideoTextView.setVisibility(View.GONE);
|
||||
debugTextView.setVisibility(View.GONE);
|
||||
informationTextView.setText(R.string.export_started);
|
||||
outputCardView.setVisibility(View.VISIBLE);
|
||||
progressViewGroup.setVisibility(View.VISIBLE);
|
||||
pauseButton.setVisibility(View.VISIBLE);
|
||||
resumeButton.setVisibility(View.GONE);
|
||||
progressIndicator.setProgress(0);
|
||||
if (isUsingMediaProjection()) {
|
||||
pauseButton.setVisibility(View.GONE);
|
||||
resumeButton.setVisibility(View.GONE);
|
||||
stopCaptureButton.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
pauseButton.setVisibility(View.VISIBLE);
|
||||
resumeButton.setVisibility(View.GONE);
|
||||
stopCaptureButton.setVisibility(View.GONE);
|
||||
}
|
||||
Handler mainHandler = new Handler(getMainLooper());
|
||||
ProgressHolder progressHolder = new ProgressHolder();
|
||||
mainHandler.post(
|
||||
@ -314,21 +389,16 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
transformerBuilder.setVideoMimeType(videoMimeType);
|
||||
}
|
||||
|
||||
transformerBuilder.setEncoderFactory(
|
||||
new DefaultEncoderFactory.Builder(this.getApplicationContext())
|
||||
.setEnableFallback(bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK))
|
||||
.build());
|
||||
|
||||
if (!bundle.getBoolean(ConfigurationActivity.ABORT_SLOW_EXPORT)) {
|
||||
transformerBuilder.setMaxDelayBetweenMuxerSamplesMs(C.TIME_UNSET);
|
||||
}
|
||||
|
||||
if (bundle.getBoolean(ConfigurationActivity.USE_MEDIA3_MUXER)) {
|
||||
transformerBuilder.setMuxerFactory(
|
||||
new InAppMuxer.Factory.Builder()
|
||||
.setOutputFragmentedMp4(
|
||||
bundle.getBoolean(ConfigurationActivity.PRODUCE_FRAGMENTED_MP4))
|
||||
.build());
|
||||
if (bundle.getBoolean(ConfigurationActivity.USE_MEDIA3_MP4_MUXER)) {
|
||||
transformerBuilder.setMuxerFactory(new InAppMp4Muxer.Factory());
|
||||
}
|
||||
|
||||
if (bundle.getBoolean(ConfigurationActivity.USE_MEDIA3_FRAGMENTED_MP4_MUXER)) {
|
||||
transformerBuilder.setMuxerFactory(new InAppFragmentedMp4Muxer.Factory());
|
||||
}
|
||||
|
||||
if (bundle.getBoolean(ConfigurationActivity.ENABLE_DEBUG_PREVIEW)) {
|
||||
@ -341,6 +411,32 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
VideoEncoderSettings videoEncoderSettings = VideoEncoderSettings.DEFAULT;
|
||||
if (screenCaptureToken != null) {
|
||||
MediaProjectionManager mediaProjectionManager =
|
||||
(MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
|
||||
MediaProjection mediaProjection =
|
||||
mediaProjectionManager.getMediaProjection(RESULT_OK, checkNotNull(screenCaptureToken));
|
||||
Rect bounds =
|
||||
WindowMetricsCalculator.getOrCreate()
|
||||
.computeCurrentWindowMetrics(/* activity= */ this)
|
||||
.getBounds();
|
||||
int densityDpi = getResources().getConfiguration().densityDpi;
|
||||
transformerBuilder.setAssetLoaderFactory(
|
||||
new MediaProjectionAssetLoader.Factory(mediaProjection, bounds, densityDpi));
|
||||
this.mediaProjection = mediaProjection;
|
||||
videoEncoderSettings =
|
||||
videoEncoderSettings
|
||||
.buildUpon()
|
||||
.setRepeatPreviousFrameIntervalUs(C.MICROS_PER_SECOND / DEFAULT_FRAME_RATE_FPS)
|
||||
.build();
|
||||
}
|
||||
transformerBuilder.setEncoderFactory(
|
||||
new DefaultEncoderFactory.Builder(this.getApplicationContext())
|
||||
.setEnableFallback(bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK))
|
||||
.setRequestedVideoEncoderSettings(videoEncoderSettings)
|
||||
.build());
|
||||
|
||||
return transformerBuilder.build();
|
||||
}
|
||||
|
||||
@ -359,7 +455,7 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
private Composition createComposition(MediaItem mediaItem, @Nullable Bundle bundle) {
|
||||
EditedMediaItem.Builder editedMediaItemBuilder = new EditedMediaItem.Builder(mediaItem);
|
||||
// For image inputs. Automatically ignored if input is audio/video.
|
||||
editedMediaItemBuilder.setFrameRate(IMAGE_FRAME_RATE_FPS);
|
||||
editedMediaItemBuilder.setFrameRate(DEFAULT_FRAME_RATE_FPS);
|
||||
if (bundle != null) {
|
||||
ImmutableList<AudioProcessor> audioProcessors = createAudioProcessorsFromBundle(bundle);
|
||||
ImmutableList<Effect> videoEffects = createVideoEffectsFromBundle(bundle);
|
||||
@ -394,14 +490,18 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
ImmutableList.Builder<AudioProcessor> processors = new ImmutableList.Builder<>();
|
||||
|
||||
if (selectedAudioEffects[ConfigurationActivity.HIGH_PITCHED_INDEX]
|
||||
|| selectedAudioEffects[ConfigurationActivity.SAMPLE_RATE_INDEX]) {
|
||||
|| selectedAudioEffects[ConfigurationActivity.SAMPLE_RATE_48K_INDEX]
|
||||
|| selectedAudioEffects[ConfigurationActivity.SAMPLE_RATE_96K_INDEX]) {
|
||||
SonicAudioProcessor sonicAudioProcessor = new SonicAudioProcessor();
|
||||
if (selectedAudioEffects[ConfigurationActivity.HIGH_PITCHED_INDEX]) {
|
||||
sonicAudioProcessor.setPitch(2f);
|
||||
}
|
||||
if (selectedAudioEffects[ConfigurationActivity.SAMPLE_RATE_INDEX]) {
|
||||
if (selectedAudioEffects[ConfigurationActivity.SAMPLE_RATE_48K_INDEX]) {
|
||||
sonicAudioProcessor.setOutputSampleRateHz(48_000);
|
||||
}
|
||||
if (selectedAudioEffects[ConfigurationActivity.SAMPLE_RATE_96K_INDEX]) {
|
||||
sonicAudioProcessor.setOutputSampleRateHz(96_000);
|
||||
}
|
||||
processors.add(sonicAudioProcessor);
|
||||
}
|
||||
|
||||
@ -417,20 +517,9 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
if (mixToMono || scaleVolumeToHalf) {
|
||||
ChannelMixingAudioProcessor mixingAudioProcessor = new ChannelMixingAudioProcessor();
|
||||
for (int inputChannelCount = 1; inputChannelCount <= 6; inputChannelCount++) {
|
||||
ChannelMixingMatrix matrix;
|
||||
if (mixToMono) {
|
||||
float[] mixingCoefficients = new float[inputChannelCount];
|
||||
// Each channel is equally weighted in the mix to mono.
|
||||
Arrays.fill(mixingCoefficients, 1f / inputChannelCount);
|
||||
matrix =
|
||||
new ChannelMixingMatrix(
|
||||
inputChannelCount, /* outputChannelCount= */ 1, mixingCoefficients);
|
||||
} else {
|
||||
// Identity matrix.
|
||||
matrix =
|
||||
ChannelMixingMatrix.create(
|
||||
inputChannelCount, /* outputChannelCount= */ inputChannelCount);
|
||||
}
|
||||
ChannelMixingMatrix matrix =
|
||||
ChannelMixingMatrix.createForConstantPower(
|
||||
inputChannelCount, /* outputChannelCount= */ mixToMono ? 1 : inputChannelCount);
|
||||
|
||||
// Apply the volume adjustment.
|
||||
mixingAudioProcessor.putChannelMixingMatrix(
|
||||
@ -599,8 +688,8 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
private OverlayEffect createOverlayEffectFromBundle(Bundle bundle, boolean[] selectedEffects) {
|
||||
ImmutableList.Builder<TextureOverlay> overlaysBuilder = new ImmutableList.Builder<>();
|
||||
if (selectedEffects[ConfigurationActivity.OVERLAY_LOGO_AND_TIMER_INDEX]) {
|
||||
OverlaySettings logoSettings =
|
||||
new OverlaySettings.Builder()
|
||||
StaticOverlaySettings logoSettings =
|
||||
new StaticOverlaySettings.Builder()
|
||||
// Place the logo in the bottom left corner of the screen with some padding from the
|
||||
// edges.
|
||||
.setOverlayFrameAnchor(/* x= */ -1f, /* y= */ -1f)
|
||||
@ -619,8 +708,8 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
overlaysBuilder.add(logoOverlay, timerOverlay);
|
||||
}
|
||||
if (selectedEffects[ConfigurationActivity.BITMAP_OVERLAY_INDEX]) {
|
||||
OverlaySettings overlaySettings =
|
||||
new OverlaySettings.Builder()
|
||||
StaticOverlaySettings overlaySettings =
|
||||
new StaticOverlaySettings.Builder()
|
||||
.setAlphaScale(
|
||||
bundle.getFloat(
|
||||
ConfigurationActivity.BITMAP_OVERLAY_ALPHA, /* defaultValue= */ 1))
|
||||
@ -633,8 +722,8 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
overlaysBuilder.add(bitmapOverlay);
|
||||
}
|
||||
if (selectedEffects[ConfigurationActivity.TEXT_OVERLAY_INDEX]) {
|
||||
OverlaySettings overlaySettings =
|
||||
new OverlaySettings.Builder()
|
||||
StaticOverlaySettings overlaySettings =
|
||||
new StaticOverlaySettings.Builder()
|
||||
.setAlphaScale(
|
||||
bundle.getFloat(ConfigurationActivity.TEXT_OVERLAY_ALPHA, /* defaultValue= */ 1))
|
||||
.build();
|
||||
@ -648,6 +737,15 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
TextOverlay textOverlay = TextOverlay.createStaticTextOverlay(overlayText, overlaySettings);
|
||||
overlaysBuilder.add(textOverlay);
|
||||
}
|
||||
if (selectedEffects[ConfigurationActivity.CLOCK_OVERLAY_INDEX]) {
|
||||
overlaysBuilder.add(new ClockOverlay());
|
||||
}
|
||||
if (selectedEffects[ConfigurationActivity.CONFETTI_OVERLAY_INDEX]) {
|
||||
overlaysBuilder.add(new ConfettiOverlay());
|
||||
}
|
||||
if (selectedEffects[ConfigurationActivity.ANIMATING_LOGO_OVERLAY]) {
|
||||
overlaysBuilder.add(new AnimatedLogoOverlay(this.getApplicationContext()));
|
||||
}
|
||||
|
||||
ImmutableList<TextureOverlay> overlays = overlaysBuilder.build();
|
||||
return overlays.isEmpty() ? null : new OverlayEffect(overlays);
|
||||
@ -658,6 +756,9 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
informationTextView.setText(R.string.export_error);
|
||||
progressViewGroup.setVisibility(View.GONE);
|
||||
debugFrame.removeAllViews();
|
||||
if (isUsingMediaProjection()) {
|
||||
mediaProjection.stop();
|
||||
}
|
||||
Toast.makeText(getApplicationContext(), "Export error: " + exportException, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
Log.e(TAG, "Export error", exportException);
|
||||
@ -701,9 +802,8 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
private void playMediaItems(MediaItem inputMediaItem, MediaItem outputMediaItem) {
|
||||
inputPlayerView.setPlayer(null);
|
||||
outputPlayerView.setPlayer(null);
|
||||
releasePlayer();
|
||||
releasePlayers();
|
||||
|
||||
Uri uri = checkNotNull(inputMediaItem.localConfiguration).uri;
|
||||
ExoPlayer outputPlayer =
|
||||
new ExoPlayer.Builder(/* context= */ this)
|
||||
.setLoadControl(
|
||||
@ -722,6 +822,7 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
this.outputPlayer = outputPlayer;
|
||||
|
||||
// Only support showing jpg images.
|
||||
Uri uri = checkNotNull(inputMediaItem.localConfiguration).uri;
|
||||
if (uri.toString().endsWith("jpg")) {
|
||||
inputPlayerView.setVisibility(View.GONE);
|
||||
inputImageView.setVisibility(View.VISIBLE);
|
||||
@ -735,6 +836,12 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
throw new IllegalArgumentException("Failed to load bitmap.", e);
|
||||
}
|
||||
} else if (isUsingMediaProjection()) {
|
||||
inputCardView.setVisibility(View.GONE);
|
||||
displayInputButton.setVisibility(View.GONE);
|
||||
Intent stopIntent = new Intent(/* context= */ this, DemoMediaProjectionService.class);
|
||||
stopIntent.setAction(DemoMediaProjectionService.ACTION_STOP);
|
||||
ContextCompat.startForegroundService(/* context= */ this, stopIntent);
|
||||
} else {
|
||||
inputPlayerView.setVisibility(View.VISIBLE);
|
||||
inputImageView.setVisibility(View.GONE);
|
||||
@ -785,7 +892,7 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void releasePlayer() {
|
||||
private void releasePlayers() {
|
||||
if (debugTextViewHelper != null) {
|
||||
debugTextViewHelper.stop();
|
||||
debugTextViewHelper = null;
|
||||
@ -800,12 +907,22 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private static void requestReadVideoPermission(AppCompatActivity activity) {
|
||||
String permission = SDK_INT >= 33 ? READ_MEDIA_VIDEO : READ_EXTERNAL_STORAGE;
|
||||
if (ActivityCompat.checkSelfPermission(activity, permission)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(activity, new String[] {permission}, /* requestCode= */ 0);
|
||||
private void cleanUpExport() {
|
||||
if (transformer != null) {
|
||||
transformer.cancel();
|
||||
transformer = null;
|
||||
}
|
||||
if (outputFile != null) {
|
||||
outputFile.delete();
|
||||
outputFile = null;
|
||||
}
|
||||
if (oldOutputFile != null) {
|
||||
oldOutputFile.delete();
|
||||
oldOutputFile = null;
|
||||
}
|
||||
// The stop watch is reset after cancelling the export, in case cancelling causes the stop watch
|
||||
// to be stopped in a transformer callback.
|
||||
exportStopwatch.reset();
|
||||
}
|
||||
|
||||
private void showToast(@StringRes int messageResource) {
|
||||
@ -837,6 +954,54 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
oldOutputFile = outputFile;
|
||||
}
|
||||
|
||||
private boolean isUsingMediaProjection() {
|
||||
return mediaProjection != null;
|
||||
}
|
||||
|
||||
/** Foreground service that's required by the media projection APIs. */
|
||||
public static final class DemoMediaProjectionService extends Service {
|
||||
private static final String CHANNEL_ID = "DemoMediaProjectionServiceChannel";
|
||||
private static final String CHANNEL_NAME = "Media projection";
|
||||
private static final int NOTIFICATION_ID = 1;
|
||||
private static final String ACTION_EVENT_STARTED = "started";
|
||||
private static final String ACTION_STOP = "stop";
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (ACTION_STOP.equals(intent.getAction())) {
|
||||
stopSelf();
|
||||
} else {
|
||||
Context context = this;
|
||||
Notification notification =
|
||||
new NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.exo_icon_play)
|
||||
.build();
|
||||
if (Util.SDK_INT >= 26) {
|
||||
NotificationChannel channel =
|
||||
new NotificationChannel(
|
||||
CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
||||
NotificationManager manager = getSystemService(NotificationManager.class);
|
||||
manager.createNotificationChannel(channel);
|
||||
}
|
||||
if (Util.SDK_INT >= 29) {
|
||||
startForeground(NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification);
|
||||
}
|
||||
// Notify that the service is started (and it's now safe to set up media projection).
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(new Intent(ACTION_EVENT_STARTED));
|
||||
}
|
||||
return START_STICKY;
|
||||
}
|
||||
}
|
||||
|
||||
private final class DemoDebugViewProvider implements DebugViewProvider {
|
||||
|
||||
@Nullable private SurfaceView surfaceView;
|
||||
|
@ -239,18 +239,18 @@
|
||||
android:layout_weight="1">
|
||||
<TextView
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/use_media3_muxer" />
|
||||
android:text="@string/use_media3_mp4_muxer" />
|
||||
<CheckBox
|
||||
android:id="@+id/use_media3_muxer_checkbox"
|
||||
android:id="@+id/use_media3_mp4_muxer_checkbox"
|
||||
android:layout_gravity="end"/>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
android:layout_weight="1">
|
||||
<TextView
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/produce_fragmented_mp4" />
|
||||
android:text="@string/use_media3_fragmented_mp4_muxer" />
|
||||
<CheckBox
|
||||
android:id="@+id/produce_fragmented_mp4_checkbox"
|
||||
android:id="@+id/use_media3_fragmented_mp4_muxer_checkbox"
|
||||
android:layout_gravity="end"/>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
|
@ -165,6 +165,12 @@
|
||||
android:layout_width="match_parent"
|
||||
android:text="@string/resume"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/stop_capture_button"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:text="@string/stop_capture"/>
|
||||
|
||||
<androidx.media3.ui.AspectRatioFrameLayout
|
||||
android:id="@+id/debug_aspect_ratio_frame_layout"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -28,10 +28,14 @@
|
||||
<item>Overlay logo and timer</item>
|
||||
<item>Custom Bitmap Overlay</item>
|
||||
<item>Custom Text Overlay</item>
|
||||
<item>Clock Overlay</item>
|
||||
<item>Confetti Overlay</item>
|
||||
<item>Animated logo</item>
|
||||
</string-array>
|
||||
<string-array name="audio_effects_names">
|
||||
<item>High pitched</item>
|
||||
<item>Sample rate of 48000Hz</item>
|
||||
<item>Sample rate of 96000Hz</item>
|
||||
<item>Skip silence</item>
|
||||
<item>Mix channels into mono</item>
|
||||
<item>Scale volume to 50%</item>
|
||||
@ -54,6 +58,7 @@
|
||||
<item>HDR (HDR10+) H265 limited range video (encoding may fail)</item>
|
||||
<item>HDR (HLG) H265 limited range video (encoding may fail)</item>
|
||||
<item>720p H264 video with no audio (B-frames)</item>
|
||||
<item>Record screen</item>
|
||||
</string-array>
|
||||
<string-array name="preset_uris">
|
||||
<item>https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4</item>
|
||||
@ -73,5 +78,6 @@
|
||||
<item>https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-s21-hdr-hdr10.mp4</item>
|
||||
<item>https://storage.googleapis.com/exoplayer-test-media-1/mp4/Pixel7Pro_HLG_1080P.mp4</item>
|
||||
<item>https://storage.googleapis.com/exoplayer-test-media-1/mp4/sample_video_track_only.mp4</item>
|
||||
<item>transformer_surface_asset:media_projection</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
@ -32,8 +32,8 @@
|
||||
<string name="enable_debug_preview" translatable="false">Enable debug preview</string>
|
||||
<string name="enable_debug_tracing" translatable="false">Enable debug tracing</string>
|
||||
<string name="abort_slow_export" translatable="false">Abort slow export</string>
|
||||
<string name="use_media3_muxer" translatable="false">Use Media3 muxer</string>
|
||||
<string name="produce_fragmented_mp4" translatable="false">Produce fragmented MP4</string>
|
||||
<string name="use_media3_mp4_muxer" translatable="false">Use Media3 Mp4Muxer</string>
|
||||
<string name="use_media3_fragmented_mp4_muxer" translatable="false">Use Media3 FragmentedMp4Muxer</string>
|
||||
<string name="trim" translatable="false">Trim</string>
|
||||
<string name="hdr_mode" translatable="false">HDR mode</string>
|
||||
<string name="select_audio_effects" translatable="false">Add audio effects</string>
|
||||
@ -44,6 +44,7 @@
|
||||
<string name="debug_preview" translatable="false">Debug preview:</string>
|
||||
<string name="pause" translatable="false">Pause</string>
|
||||
<string name="resume" translatable="false">Resume</string>
|
||||
<string name="stop_capture" translatable="false">Stop capture</string>
|
||||
<string name="debug_preview_not_available" translatable="false">No debug preview available.</string>
|
||||
<string name="export_started" translatable="false">Export started</string>
|
||||
<string name="export_timer" translatable="false">Export started %d seconds ago.</string>
|
||||
|
@ -24,9 +24,9 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'com.google.android.gms:play-services-cast-framework:21.3.0'
|
||||
api 'com.google.android.gms:play-services-cast-framework:21.5.0'
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
implementation project(modulePrefix + 'lib-common')
|
||||
api project(modulePrefix + 'lib-common')
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||
testImplementation project(modulePrefix + 'test-utils')
|
||||
|
@ -15,7 +15,6 @@
|
||||
*/
|
||||
package androidx.media3.cast;
|
||||
|
||||
import static androidx.annotation.VisibleForTesting.PROTECTED;
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
import static androidx.media3.common.util.Util.SDK_INT;
|
||||
import static androidx.media3.common.util.Util.castNonNull;
|
||||
@ -74,6 +73,7 @@ import com.google.android.gms.common.api.PendingResult;
|
||||
import com.google.android.gms.common.api.ResultCallback;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
@ -467,8 +467,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
// onPositionDiscontinuity(PositionInfo, PositionInfo, @DiscontinuityReason int).
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
@VisibleForTesting(otherwise = PROTECTED)
|
||||
public void seekTo(
|
||||
protected void seekTo(
|
||||
int mediaItemIndex,
|
||||
long positionMs,
|
||||
@Player.Command int seekCommand,
|
||||
@ -639,7 +638,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
|
||||
@Override
|
||||
public TrackSelectionParameters getTrackSelectionParameters() {
|
||||
return TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT;
|
||||
return TrackSelectionParameters.DEFAULT;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -911,7 +910,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
? currentTimeline.getPeriod(currentWindowIndex, period, /* setIds= */ true).uid
|
||||
: null;
|
||||
if (!playingPeriodChangedByTimelineChange
|
||||
&& !Util.areEqual(oldPeriodUid, currentPeriodUid)
|
||||
&& !Objects.equals(oldPeriodUid, currentPeriodUid)
|
||||
&& pendingSeekCount == 0) {
|
||||
// Report discontinuity and media item auto transition.
|
||||
currentTimeline.getPeriod(oldWindowIndex, period, /* setIds= */ true);
|
||||
|
@ -35,12 +35,14 @@ import androidx.annotation.VisibleForTesting;
|
||||
import androidx.media3.common.util.NullableType;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.errorprone.annotations.InlineMe;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents ad group times and information on the state and URIs of ads within each ad group.
|
||||
@ -70,7 +72,7 @@ public final class AdPlaybackState {
|
||||
|
||||
/**
|
||||
* The original number of ads in the ad group in case the ad group is only partially available,
|
||||
* or {@link C#LENGTH_UNSET} if unknown. An ad can be partially available when a server side
|
||||
* or {@link C#LENGTH_UNSET} if unknown. An ad can be partially available when a server-side
|
||||
* inserted ad live stream is joined while an ad is already playing and some ad information is
|
||||
* missing.
|
||||
*/
|
||||
@ -90,6 +92,9 @@ public final class AdPlaybackState {
|
||||
/** The durations of each ad in the ad group, in microseconds. */
|
||||
public final long[] durationsUs;
|
||||
|
||||
/** The optional IDs of the ads. */
|
||||
public final @NullableType String[] ids;
|
||||
|
||||
/**
|
||||
* The offset in microseconds which should be added to the content stream when resuming playback
|
||||
* after the ad group.
|
||||
@ -99,6 +104,9 @@ public final class AdPlaybackState {
|
||||
/** Whether this ad group is server-side inserted and part of the content stream. */
|
||||
public final boolean isServerSideInserted;
|
||||
|
||||
/** Whether this is an ignorable placeholder that must not be attempted to be played. */
|
||||
public final boolean isPlaceholder;
|
||||
|
||||
/**
|
||||
* Creates a new ad group with an unspecified number of ads.
|
||||
*
|
||||
@ -114,7 +122,9 @@ public final class AdPlaybackState {
|
||||
/* mediaItems= */ new MediaItem[0],
|
||||
/* durationsUs= */ new long[0],
|
||||
/* contentResumeOffsetUs= */ 0,
|
||||
/* isServerSideInserted= */ false);
|
||||
/* isServerSideInserted= */ false,
|
||||
/* ids= */ new String[0],
|
||||
/* isPlaceholder= */ false);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Intentionally assigning deprecated field
|
||||
@ -126,7 +136,9 @@ public final class AdPlaybackState {
|
||||
@NullableType MediaItem[] mediaItems,
|
||||
long[] durationsUs,
|
||||
long contentResumeOffsetUs,
|
||||
boolean isServerSideInserted) {
|
||||
boolean isServerSideInserted,
|
||||
@NullableType String[] ids,
|
||||
boolean isPlaceholder) {
|
||||
checkArgument(states.length == mediaItems.length);
|
||||
this.timeUs = timeUs;
|
||||
this.count = count;
|
||||
@ -140,6 +152,8 @@ public final class AdPlaybackState {
|
||||
for (int i = 0; i < uris.length; i++) {
|
||||
uris[i] = mediaItems[i] == null ? null : checkNotNull(mediaItems[i].localConfiguration).uri;
|
||||
}
|
||||
this.ids = ids;
|
||||
this.isPlaceholder = isPlaceholder;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -155,7 +169,7 @@ public final class AdPlaybackState {
|
||||
* lastPlayedAdIndex}, or {@link #count} if no later ads should be played. If no ads have been
|
||||
* played, pass -1 to get the index of the first ad to play.
|
||||
*
|
||||
* <p>Note: {@linkplain #isServerSideInserted Server side inserted ads} are always considered
|
||||
* <p>Note: {@linkplain #isServerSideInserted server-side inserted ads} are always considered
|
||||
* playable.
|
||||
*/
|
||||
public int getNextAdIndexToPlay(@IntRange(from = -1) int lastPlayedAdIndex) {
|
||||
@ -191,8 +205,24 @@ public final class AdPlaybackState {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isLivePostrollPlaceholder() {
|
||||
return isServerSideInserted && timeUs == C.TIME_END_OF_SOURCE && count == C.LENGTH_UNSET;
|
||||
/**
|
||||
* Returns whether this is a is a placeholder ad group.
|
||||
*
|
||||
* @param isServerSideInserted Whether the postroll placeholder must be server-side inserted.
|
||||
* @return true only if this ad group has a matching {@link #isServerSideInserted} flag.
|
||||
*/
|
||||
public boolean isLivePostrollPlaceholder(boolean isServerSideInserted) {
|
||||
return (this.isServerSideInserted == isServerSideInserted) && isLivePostrollPlaceholder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this is a placeholder ad group. It can be server-side inserted or not. Use
|
||||
* {@link #isLivePostrollPlaceholder(boolean)} if you want to differentiate.
|
||||
*
|
||||
* @return true only if this is a live postroll placeholder.
|
||||
*/
|
||||
public boolean isLivePostrollPlaceholder() {
|
||||
return isPlaceholder && timeUs == C.TIME_END_OF_SOURCE && count == C.LENGTH_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -211,7 +241,9 @@ public final class AdPlaybackState {
|
||||
&& Arrays.equals(states, adGroup.states)
|
||||
&& Arrays.equals(durationsUs, adGroup.durationsUs)
|
||||
&& contentResumeOffsetUs == adGroup.contentResumeOffsetUs
|
||||
&& isServerSideInserted == adGroup.isServerSideInserted;
|
||||
&& isServerSideInserted == adGroup.isServerSideInserted
|
||||
&& Arrays.equals(ids, adGroup.ids)
|
||||
&& isPlaceholder == adGroup.isPlaceholder;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -224,6 +256,8 @@ public final class AdPlaybackState {
|
||||
result = 31 * result + Arrays.hashCode(durationsUs);
|
||||
result = 31 * result + (int) (contentResumeOffsetUs ^ (contentResumeOffsetUs >>> 32));
|
||||
result = 31 * result + (isServerSideInserted ? 1 : 0);
|
||||
result = 31 * result + Arrays.hashCode(ids);
|
||||
result = 31 * result + (isPlaceholder ? 1 : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -238,7 +272,9 @@ public final class AdPlaybackState {
|
||||
mediaItems,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
isServerSideInserted,
|
||||
ids,
|
||||
isPlaceholder);
|
||||
}
|
||||
|
||||
/** Returns a new instance with the ad count set to {@code count}. */
|
||||
@ -247,6 +283,7 @@ public final class AdPlaybackState {
|
||||
@AdState int[] states = copyStatesWithSpaceForAdCount(this.states, count);
|
||||
long[] durationsUs = copyDurationsUsWithSpaceForAdCount(this.durationsUs, count);
|
||||
@NullableType MediaItem[] mediaItems = Arrays.copyOf(this.mediaItems, count);
|
||||
@NullableType String[] ids = Arrays.copyOf(this.ids, count);
|
||||
return new AdGroup(
|
||||
timeUs,
|
||||
count,
|
||||
@ -255,7 +292,9 @@ public final class AdPlaybackState {
|
||||
mediaItems,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
isServerSideInserted,
|
||||
ids,
|
||||
isPlaceholder);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -281,6 +320,9 @@ public final class AdPlaybackState {
|
||||
@NullableType MediaItem[] mediaItems = Arrays.copyOf(this.mediaItems, states.length);
|
||||
mediaItems[index] = mediaItem;
|
||||
states[index] = AD_STATE_AVAILABLE;
|
||||
@NullableType
|
||||
String[] ids =
|
||||
this.ids.length == states.length ? this.ids : Arrays.copyOf(this.ids, states.length);
|
||||
return new AdGroup(
|
||||
timeUs,
|
||||
count,
|
||||
@ -289,7 +331,9 @@ public final class AdPlaybackState {
|
||||
mediaItems,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
isServerSideInserted,
|
||||
ids,
|
||||
isPlaceholder);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -317,6 +361,9 @@ public final class AdPlaybackState {
|
||||
this.mediaItems.length == states.length
|
||||
? this.mediaItems
|
||||
: Arrays.copyOf(this.mediaItems, states.length);
|
||||
@NullableType
|
||||
String[] ids =
|
||||
this.ids.length == states.length ? this.ids : Arrays.copyOf(this.ids, states.length);
|
||||
states[index] = state;
|
||||
return new AdGroup(
|
||||
timeUs,
|
||||
@ -326,7 +373,9 @@ public final class AdPlaybackState {
|
||||
mediaItems,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
isServerSideInserted,
|
||||
ids,
|
||||
isPlaceholder);
|
||||
}
|
||||
|
||||
/** Returns a new instance with the specified ad durations, in microseconds. */
|
||||
@ -345,7 +394,39 @@ public final class AdPlaybackState {
|
||||
mediaItems,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
isServerSideInserted,
|
||||
ids,
|
||||
isPlaceholder);
|
||||
}
|
||||
|
||||
/** Returns a new instance with the specified ID for the given ad index. */
|
||||
@CheckResult
|
||||
public AdGroup withAdId(String adId, @IntRange(from = 0) int index) {
|
||||
@AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1);
|
||||
long[] durationsUs =
|
||||
this.durationsUs.length == states.length
|
||||
? this.durationsUs
|
||||
: copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length);
|
||||
@NullableType
|
||||
MediaItem[] mediaItems =
|
||||
this.mediaItems.length == states.length
|
||||
? this.mediaItems
|
||||
: Arrays.copyOf(this.mediaItems, states.length);
|
||||
@NullableType
|
||||
String[] ids =
|
||||
this.ids.length == states.length ? this.ids : Arrays.copyOf(this.ids, states.length);
|
||||
ids[index] = adId;
|
||||
return new AdGroup(
|
||||
timeUs,
|
||||
count,
|
||||
originalCount,
|
||||
states,
|
||||
mediaItems,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted,
|
||||
ids,
|
||||
isPlaceholder);
|
||||
}
|
||||
|
||||
/** Returns an instance with the specified {@link #contentResumeOffsetUs}. */
|
||||
@ -359,7 +440,9 @@ public final class AdPlaybackState {
|
||||
mediaItems,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
isServerSideInserted,
|
||||
ids,
|
||||
isPlaceholder);
|
||||
}
|
||||
|
||||
/** Returns an instance with the specified value for {@link #isServerSideInserted}. */
|
||||
@ -373,7 +456,9 @@ public final class AdPlaybackState {
|
||||
mediaItems,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
isServerSideInserted,
|
||||
ids,
|
||||
isPlaceholder);
|
||||
}
|
||||
|
||||
/** Returns an instance with the specified value for {@link #originalCount}. */
|
||||
@ -386,7 +471,9 @@ public final class AdPlaybackState {
|
||||
mediaItems,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
isServerSideInserted,
|
||||
ids,
|
||||
isPlaceholder);
|
||||
}
|
||||
|
||||
/** Removes the last ad from the ad group. */
|
||||
@ -398,6 +485,7 @@ public final class AdPlaybackState {
|
||||
if (durationsUs.length > newCount) {
|
||||
newDurationsUs = Arrays.copyOf(durationsUs, newCount);
|
||||
}
|
||||
@NullableType String[] newIds = Arrays.copyOf(ids, newCount);
|
||||
return new AdGroup(
|
||||
timeUs,
|
||||
newCount,
|
||||
@ -406,7 +494,9 @@ public final class AdPlaybackState {
|
||||
newMediaItems,
|
||||
newDurationsUs,
|
||||
/* contentResumeOffsetUs= */ Util.sum(newDurationsUs),
|
||||
isServerSideInserted);
|
||||
isServerSideInserted,
|
||||
newIds,
|
||||
isPlaceholder);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -424,7 +514,9 @@ public final class AdPlaybackState {
|
||||
/* mediaItems= */ new MediaItem[0],
|
||||
/* durationsUs= */ new long[0],
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
isServerSideInserted,
|
||||
ids,
|
||||
isPlaceholder);
|
||||
}
|
||||
int count = this.states.length;
|
||||
@AdState int[] states = Arrays.copyOf(this.states, count);
|
||||
@ -441,7 +533,9 @@ public final class AdPlaybackState {
|
||||
mediaItems,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
isServerSideInserted,
|
||||
ids,
|
||||
isPlaceholder);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -470,7 +564,36 @@ public final class AdPlaybackState {
|
||||
mediaItems,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
isServerSideInserted,
|
||||
ids,
|
||||
isPlaceholder);
|
||||
}
|
||||
|
||||
private AdGroup withIsPlaceholder(boolean isPlaceholder, boolean isServerSideInserted) {
|
||||
return new AdGroup(
|
||||
timeUs,
|
||||
count,
|
||||
originalCount,
|
||||
states,
|
||||
mediaItems,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted,
|
||||
ids,
|
||||
isPlaceholder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the ad with the given ad ID, or {@link C#INDEX_UNSET} if the ad ID can't
|
||||
* be found.
|
||||
*/
|
||||
public int getIndexOfAdId(String adId) {
|
||||
for (int i = 0; i < ids.length; i++) {
|
||||
if (Objects.equals(ids[i], adId)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
@ -500,6 +623,8 @@ public final class AdPlaybackState {
|
||||
private static final String FIELD_IS_SERVER_SIDE_INSERTED = Util.intToStringMaxRadix(6);
|
||||
private static final String FIELD_ORIGINAL_COUNT = Util.intToStringMaxRadix(7);
|
||||
@VisibleForTesting static final String FIELD_MEDIA_ITEMS = Util.intToStringMaxRadix(8);
|
||||
static final String FIELD_IDS = Util.intToStringMaxRadix(9);
|
||||
static final String FIELD_IS_PLACEHOLDER = Util.intToStringMaxRadix(10);
|
||||
|
||||
// Intentionally assigning deprecated field.
|
||||
// putParcelableArrayList actually supports null elements.
|
||||
@ -516,6 +641,8 @@ public final class AdPlaybackState {
|
||||
bundle.putLongArray(FIELD_DURATIONS_US, durationsUs);
|
||||
bundle.putLong(FIELD_CONTENT_RESUME_OFFSET_US, contentResumeOffsetUs);
|
||||
bundle.putBoolean(FIELD_IS_SERVER_SIDE_INSERTED, isServerSideInserted);
|
||||
bundle.putStringArrayList(FIELD_IDS, new ArrayList<>(Arrays.asList(ids)));
|
||||
bundle.putBoolean(FIELD_IS_PLACEHOLDER, isPlaceholder);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@ -536,6 +663,8 @@ public final class AdPlaybackState {
|
||||
@Nullable long[] durationsUs = bundle.getLongArray(FIELD_DURATIONS_US);
|
||||
long contentResumeOffsetUs = bundle.getLong(FIELD_CONTENT_RESUME_OFFSET_US);
|
||||
boolean isServerSideInserted = bundle.getBoolean(FIELD_IS_SERVER_SIDE_INSERTED);
|
||||
@Nullable ArrayList<String> ids = bundle.getStringArrayList(FIELD_IDS);
|
||||
boolean isPlaceholder = bundle.getBoolean(FIELD_IS_PLACEHOLDER);
|
||||
return new AdGroup(
|
||||
timeUs,
|
||||
count,
|
||||
@ -544,7 +673,9 @@ public final class AdPlaybackState {
|
||||
getMediaItemsFromBundleArrays(mediaItemBundleList, uriList),
|
||||
durationsUs == null ? new long[0] : durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
isServerSideInserted,
|
||||
ids == null ? new String[0] : ids.toArray(new String[0]),
|
||||
isPlaceholder);
|
||||
}
|
||||
|
||||
private ArrayList<@NullableType Bundle> getMediaItemsArrayBundles() {
|
||||
@ -844,14 +975,21 @@ public final class AdPlaybackState {
|
||||
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
|
||||
}
|
||||
|
||||
/** Returns an instance with the specified value for {@link #adsId}. */
|
||||
@CheckResult
|
||||
public AdPlaybackState withAdsId(Object adsId) {
|
||||
return new AdPlaybackState(
|
||||
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance with the specified ad marked as {@linkplain #AD_STATE_AVAILABLE available}.
|
||||
*
|
||||
* <p>Must not be called with client side inserted ad groups. Client side inserted ads should use
|
||||
* <p>Must not be called with client-side inserted ad groups. Client-side inserted ads should use
|
||||
* {@link #withAvailableAdMediaItem}.
|
||||
*
|
||||
* @throws IllegalStateException in case this methods is called on an ad group that {@linkplain
|
||||
* AdGroup#isServerSideInserted is not server side inserted}.
|
||||
* AdGroup#isServerSideInserted is not server-side inserted}.
|
||||
*/
|
||||
@CheckResult
|
||||
public AdPlaybackState withAvailableAd(
|
||||
@ -907,6 +1045,17 @@ public final class AdPlaybackState {
|
||||
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
|
||||
}
|
||||
|
||||
/** Returns an instance with the specified ad ID for the given ad. */
|
||||
@CheckResult
|
||||
public AdPlaybackState withAdId(
|
||||
@IntRange(from = 0) int adGroupIndex, @IntRange(from = 0) int adIndexInAdGroup, String adId) {
|
||||
int adjustedIndex = adGroupIndex - removedAdGroupCount;
|
||||
AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
|
||||
adGroups[adjustedIndex] = adGroups[adjustedIndex].withAdId(adId, adIndexInAdGroup);
|
||||
return new AdPlaybackState(
|
||||
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance with all ads in the specified ad group skipped (except for those already
|
||||
* marked as played or in the error state).
|
||||
@ -1066,25 +1215,56 @@ public final class AdPlaybackState {
|
||||
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #withLivePostrollPlaceholderAppended(boolean)} and pass {@code true}
|
||||
* instead.
|
||||
*/
|
||||
@InlineMe(replacement = "this.withLivePostrollPlaceholderAppended(true)")
|
||||
@Deprecated
|
||||
public AdPlaybackState withLivePostrollPlaceholderAppended() {
|
||||
return withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a live postroll placeholder ad group to the ad playback state.
|
||||
*
|
||||
* <p>Adding such a placeholder is only required for periods of server side ad insertion live
|
||||
* streams. A player is not expected to play this placeholder. It is only used to indicate that
|
||||
* another ad group with this ad group index will be inserted in the future.
|
||||
* <p>Adding such a placeholder is only required for periods of live streams. A player is not
|
||||
* expected to play this placeholder. It is only used to indicate that another ad group with this
|
||||
* ad group index will be inserted in the future.
|
||||
*
|
||||
* <p>See {@link #endsWithLivePostrollPlaceHolder()} also.
|
||||
* <p>See {@link #endsWithLivePostrollPlaceHolder()} and {@link
|
||||
* #endsWithLivePostrollPlaceHolder(boolean)} also.
|
||||
*
|
||||
* @param isServerSideInserted Whether this is a server-side inserted ad (single stream).
|
||||
* @return The new ad playback state instance ending with a live postroll placeholder.
|
||||
*/
|
||||
public AdPlaybackState withLivePostrollPlaceholderAppended() {
|
||||
public AdPlaybackState withLivePostrollPlaceholderAppended(boolean isServerSideInserted) {
|
||||
return withNewAdGroup(adGroupCount, /* adGroupTimeUs= */ C.TIME_END_OF_SOURCE)
|
||||
.withIsServerSideInserted(adGroupCount, true);
|
||||
.withIsPlaceholder(adGroupCount, /* isPlaceholder= */ true, isServerSideInserted);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
/* package */ AdPlaybackState withIsPlaceholder(
|
||||
int adGroupIndex, boolean isPlaceholder, boolean isServerSideInserted) {
|
||||
int adjustedIndex = adGroupIndex - removedAdGroupCount;
|
||||
if (adGroups[adjustedIndex].isPlaceholder == isPlaceholder
|
||||
&& adGroups[adjustedIndex].isServerSideInserted == isServerSideInserted) {
|
||||
return this;
|
||||
}
|
||||
AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
|
||||
adGroups[adjustedIndex] =
|
||||
adGroups[adjustedIndex].withIsPlaceholder(isPlaceholder, isServerSideInserted);
|
||||
return new AdPlaybackState(
|
||||
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the last ad group is a live postroll placeholder as inserted by {@link
|
||||
* #withLivePostrollPlaceholderAppended()}.
|
||||
* #withLivePostrollPlaceholderAppended(boolean)}.
|
||||
*
|
||||
* <p>Note: That either server-side or client-side inserted placeholders are considered. Use
|
||||
* {@link #endsWithLivePostrollPlaceHolder(boolean)} if you want to test for one or the other
|
||||
* only.
|
||||
*
|
||||
* @return Whether the ad playback state ends with a live postroll placeholder.
|
||||
*/
|
||||
@ -1094,7 +1274,22 @@ public final class AdPlaybackState {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the {@link AdGroup} at the given ad group index is a live postroll placeholder.
|
||||
* Returns whether the last ad group is a live postroll placeholder as inserted by {@link
|
||||
* #withLivePostrollPlaceholderAppended(boolean)} .
|
||||
*
|
||||
* @param isServerSideInserted Whether the trailing placeholder is server-side inserted.
|
||||
* @return Whether the ad playback state ends with a live postroll placeholder.
|
||||
*/
|
||||
public boolean endsWithLivePostrollPlaceHolder(boolean isServerSideInserted) {
|
||||
int adGroupIndex = adGroupCount - 1;
|
||||
return adGroupIndex >= 0 && isLivePostrollPlaceholder(adGroupIndex, isServerSideInserted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the {@link AdGroup} at the given ad group index is a live postroll placeholder.
|
||||
*
|
||||
* <p>Note: That either server-side or client-side inserted placeholders return true. Use {@link
|
||||
* #isLivePostrollPlaceholder(int, boolean)} if you want to test for one or the other only.
|
||||
*
|
||||
* @param adGroupIndex The ad group index.
|
||||
* @return True if the ad group at the given index is a live postroll placeholder, false if not.
|
||||
@ -1103,6 +1298,31 @@ public final class AdPlaybackState {
|
||||
return adGroupIndex == adGroupCount - 1 && getAdGroup(adGroupIndex).isLivePostrollPlaceholder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the {@link AdGroup} at the given ad group index is a live postroll placeholder
|
||||
* and either server or client-side inserted.
|
||||
*
|
||||
* @param adGroupIndex The ad group index.
|
||||
* @param isServerSideInserted Whether the placeholder is server-side inserted.
|
||||
* @return True if the ad group at the given index is a live postroll placeholder, false if not.
|
||||
*/
|
||||
public boolean isLivePostrollPlaceholder(int adGroupIndex, boolean isServerSideInserted) {
|
||||
return adGroupIndex == adGroupCount - 1
|
||||
&& getAdGroup(adGroupIndex).isLivePostrollPlaceholder(isServerSideInserted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the ad with the given ad ID in the given ad group, or {@link
|
||||
* C#INDEX_UNSET} if the ad ID can't be found.
|
||||
*
|
||||
* @param adGroupIndex The ad group index.
|
||||
* @param adId The ad ID.
|
||||
* @return The ad index in the ad group, or {@link C#INDEX_UNSET} if the ad ID is not found.
|
||||
*/
|
||||
public int getAdIndexOfAdId(int adGroupIndex, String adId) {
|
||||
return getAdGroup(adGroupIndex).getIndexOfAdId(adId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the ad playback state with the given ads ID.
|
||||
*
|
||||
@ -1124,7 +1344,9 @@ public final class AdPlaybackState {
|
||||
Arrays.copyOf(adGroup.mediaItems, adGroup.mediaItems.length),
|
||||
Arrays.copyOf(adGroup.durationsUs, adGroup.durationsUs.length),
|
||||
adGroup.contentResumeOffsetUs,
|
||||
adGroup.isServerSideInserted);
|
||||
adGroup.isServerSideInserted,
|
||||
adGroup.ids,
|
||||
adGroup.isPlaceholder);
|
||||
}
|
||||
return new AdPlaybackState(
|
||||
adsId,
|
||||
@ -1143,7 +1365,7 @@ public final class AdPlaybackState {
|
||||
return false;
|
||||
}
|
||||
AdPlaybackState that = (AdPlaybackState) o;
|
||||
return Util.areEqual(adsId, that.adsId)
|
||||
return Objects.equals(adsId, that.adsId)
|
||||
&& adGroupCount == that.adGroupCount
|
||||
&& adResumePositionUs == that.adResumePositionUs
|
||||
&& contentDurationUs == that.contentDurationUs
|
||||
@ -1226,7 +1448,7 @@ public final class AdPlaybackState {
|
||||
// placeholder in a period of a multi-period live window, or when c) the position actually is
|
||||
// before the given period duration.
|
||||
return periodDurationUs == C.TIME_UNSET
|
||||
|| (adGroup.isServerSideInserted && adGroup.count == C.LENGTH_UNSET)
|
||||
|| adGroup.isLivePostrollPlaceholder()
|
||||
|| positionUs < periodDurationUs;
|
||||
}
|
||||
return positionUs < adGroupPositionUs;
|
||||
|
@ -170,6 +170,43 @@ public final class AudioAttributes {
|
||||
return audioAttributesV21;
|
||||
}
|
||||
|
||||
/** Returns the {@link C.StreamType} corresponding to these audio attributes. */
|
||||
@UnstableApi
|
||||
public @C.StreamType int getStreamType() {
|
||||
// Flags to stream type mapping
|
||||
if ((flags & C.FLAG_AUDIBILITY_ENFORCED) == C.FLAG_AUDIBILITY_ENFORCED) {
|
||||
return C.STREAM_TYPE_SYSTEM;
|
||||
}
|
||||
// Usage to stream type mapping
|
||||
switch (usage) {
|
||||
case C.USAGE_ASSISTANCE_SONIFICATION:
|
||||
return C.STREAM_TYPE_SYSTEM;
|
||||
case C.USAGE_VOICE_COMMUNICATION:
|
||||
return C.STREAM_TYPE_VOICE_CALL;
|
||||
case C.USAGE_VOICE_COMMUNICATION_SIGNALLING:
|
||||
return C.STREAM_TYPE_DTMF;
|
||||
case C.USAGE_ALARM:
|
||||
return C.STREAM_TYPE_ALARM;
|
||||
case C.USAGE_NOTIFICATION_RINGTONE:
|
||||
return C.STREAM_TYPE_RING;
|
||||
case C.USAGE_NOTIFICATION:
|
||||
case C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST:
|
||||
case C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT:
|
||||
case C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED:
|
||||
case C.USAGE_NOTIFICATION_EVENT:
|
||||
return C.STREAM_TYPE_NOTIFICATION;
|
||||
case C.USAGE_ASSISTANCE_ACCESSIBILITY:
|
||||
return C.STREAM_TYPE_ACCESSIBILITY;
|
||||
case C.USAGE_MEDIA:
|
||||
case C.USAGE_GAME:
|
||||
case C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:
|
||||
case C.USAGE_ASSISTANT:
|
||||
case C.USAGE_UNKNOWN:
|
||||
default:
|
||||
return C.STREAM_TYPE_MUSIC;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
|
@ -15,15 +15,14 @@
|
||||
*/
|
||||
package androidx.media3.common;
|
||||
|
||||
import static androidx.annotation.VisibleForTesting.PROTECTED;
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.annotations.ForOverride;
|
||||
import java.util.List;
|
||||
|
||||
/** Abstract base {@link Player} which implements common implementation independent methods. */
|
||||
@ -276,8 +275,8 @@ public abstract class BasePlayer implements Player {
|
||||
* @param seekCommand The {@link Player.Command} used to trigger the seek.
|
||||
* @param isRepeatingCurrentItem Whether this seeks repeats the current item.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = PROTECTED)
|
||||
public abstract void seekTo(
|
||||
@ForOverride
|
||||
protected abstract void seekTo(
|
||||
int mediaItemIndex,
|
||||
long positionMs,
|
||||
@Player.Command int seekCommand,
|
||||
|
@ -29,7 +29,6 @@ import androidx.media3.common.util.Util;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.List;
|
||||
|
||||
// LINT.IfChange(javadoc)
|
||||
/**
|
||||
* A {@link Binder} to transfer a list of {@link Bundle Bundles} across processes by splitting the
|
||||
* list into multiple transactions.
|
||||
|
@ -30,6 +30,7 @@ import android.media.MediaCodec;
|
||||
import android.media.MediaCrypto;
|
||||
import android.media.MediaFormat;
|
||||
import android.net.Uri;
|
||||
import android.opengl.GLES20;
|
||||
import android.view.Surface;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
@ -327,8 +328,8 @@ public final class C {
|
||||
/**
|
||||
* Stream types for an {@link android.media.AudioTrack}. One of {@link #STREAM_TYPE_ALARM}, {@link
|
||||
* #STREAM_TYPE_DTMF}, {@link #STREAM_TYPE_MUSIC}, {@link #STREAM_TYPE_NOTIFICATION}, {@link
|
||||
* #STREAM_TYPE_RING}, {@link #STREAM_TYPE_SYSTEM}, {@link #STREAM_TYPE_VOICE_CALL} or {@link
|
||||
* #STREAM_TYPE_DEFAULT}.
|
||||
* #STREAM_TYPE_RING}, {@link #STREAM_TYPE_SYSTEM}, {@link #STREAM_TYPE_VOICE_CALL}, {@link
|
||||
* #STREAM_TYPE_ACCESSIBILITY} or {@link #STREAM_TYPE_DEFAULT}.
|
||||
*/
|
||||
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
|
||||
// with Kotlin usages from before TYPE_USE was added.
|
||||
@ -345,6 +346,7 @@ public final class C {
|
||||
STREAM_TYPE_RING,
|
||||
STREAM_TYPE_SYSTEM,
|
||||
STREAM_TYPE_VOICE_CALL,
|
||||
STREAM_TYPE_ACCESSIBILITY,
|
||||
STREAM_TYPE_DEFAULT
|
||||
})
|
||||
public @interface StreamType {}
|
||||
@ -370,6 +372,10 @@ public final class C {
|
||||
/** See {@link AudioManager#STREAM_VOICE_CALL}. */
|
||||
@UnstableApi public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL;
|
||||
|
||||
/** See {@link AudioManager#STREAM_ACCESSIBILITY}. */
|
||||
@UnstableApi
|
||||
public static final int STREAM_TYPE_ACCESSIBILITY = AudioManager.STREAM_ACCESSIBILITY;
|
||||
|
||||
/** The default stream type used by audio renderers. Equal to {@link #STREAM_TYPE_MUSIC}. */
|
||||
@UnstableApi public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC;
|
||||
|
||||
@ -611,6 +617,28 @@ public final class C {
|
||||
/** See {@link android.media.AudioAttributes#ALLOW_CAPTURE_BY_SYSTEM}. */
|
||||
public static final int ALLOW_CAPTURE_BY_SYSTEM = AudioAttributes.ALLOW_CAPTURE_BY_SYSTEM;
|
||||
|
||||
/**
|
||||
* Flags which represent a set of video codecs.
|
||||
*
|
||||
* <p>Possible flag values are:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link #VIDEO_CODEC_FLAG_H264}
|
||||
* <li>{@link #VIDEO_CODEC_FLAG_H265}
|
||||
* </ul>
|
||||
*/
|
||||
@UnstableApi
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(TYPE_USE)
|
||||
@IntDef(
|
||||
flag = true,
|
||||
value = {VIDEO_CODEC_FLAG_H264, VIDEO_CODEC_FLAG_H265})
|
||||
public @interface VideoCodecFlags {}
|
||||
|
||||
@UnstableApi public static final int VIDEO_CODEC_FLAG_H264 = 1;
|
||||
@UnstableApi public static final int VIDEO_CODEC_FLAG_H265 = 2;
|
||||
|
||||
/**
|
||||
* Flags which can apply to a buffer containing a media sample.
|
||||
*
|
||||
@ -670,6 +698,7 @@ public final class C {
|
||||
/** A non-realtime (as fast as possible) {@linkplain MediaFormat#KEY_PRIORITY codec priority}. */
|
||||
@UnstableApi public static final int MEDIA_CODEC_PRIORITY_NON_REALTIME = 1;
|
||||
|
||||
// LINT.IfChange
|
||||
/**
|
||||
* Video decoder output modes. Possible modes are {@link #VIDEO_OUTPUT_MODE_NONE}, {@link
|
||||
* #VIDEO_OUTPUT_MODE_YUV} and {@link #VIDEO_OUTPUT_MODE_SURFACE_YUV}.
|
||||
@ -690,6 +719,11 @@ public final class C {
|
||||
/** Video decoder output mode that renders 4:2:0 YUV planes directly to a surface. */
|
||||
@UnstableApi public static final int VIDEO_OUTPUT_MODE_SURFACE_YUV = 1;
|
||||
|
||||
// LINT.ThenChange(
|
||||
// ../../../../../../../decoder_av1/src/main/jni/gav1_jni.cc,
|
||||
// ../../../../../../../decoder_vp9/src/main/jni/vpx_jni.cc
|
||||
// )
|
||||
|
||||
/**
|
||||
* Video scaling modes for {@link MediaCodec}-based renderers. One of {@link
|
||||
* #VIDEO_SCALING_MODE_SCALE_TO_FIT}, {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING} or
|
||||
@ -781,6 +815,8 @@ public final class C {
|
||||
*/
|
||||
public static final int SELECTION_FLAG_AUTOSELECT = 1 << 2; // 4
|
||||
|
||||
// LINT.ThenChange("util/Util.java:selection_flags")
|
||||
|
||||
/** Represents an undetermined language as an ISO 639-2 language code. */
|
||||
public static final String LANGUAGE_UNDETERMINED = "und";
|
||||
|
||||
@ -1162,6 +1198,11 @@ public final class C {
|
||||
/** See {@link MediaFormat#COLOR_STANDARD_BT2020}. */
|
||||
@UnstableApi public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020;
|
||||
|
||||
// LINT.ThenChange(
|
||||
// util/MediaFormatUtil.java:color_space,
|
||||
// ColorInfo.java:color_space,
|
||||
// )
|
||||
|
||||
// LINT.IfChange(color_transfer)
|
||||
/**
|
||||
* Video/image color transfer characteristics. One of {@link Format#NO_VALUE}, {@link
|
||||
@ -1209,6 +1250,14 @@ public final class C {
|
||||
/** See {@link MediaFormat#COLOR_TRANSFER_HLG}. */
|
||||
@UnstableApi public static final int COLOR_TRANSFER_HLG = MediaFormat.COLOR_TRANSFER_HLG;
|
||||
|
||||
// LINT.ThenChange(
|
||||
// util/MediaFormatUtil.java:color_transfer,
|
||||
// ColorInfo.java:color_transfer,
|
||||
// ../../../../../../../effect/src/main/assets/shaders/fragment_shader_transformation_sdr_external_es2.glsl:color_transfer,
|
||||
// ../../../../../../../effect/src/main/assets/shaders/fragment_shader_transformation_external_yuv_es3.glsl:color_transfer,
|
||||
// ../../../../../../../effect/src/main/assets/shaders/fragment_shader_oetf_es3.glsl:color_transfer,
|
||||
// )
|
||||
|
||||
// LINT.IfChange(color_range)
|
||||
/**
|
||||
* Video color range. One of {@link Format#NO_VALUE}, {@link #COLOR_RANGE_LIMITED} or {@link
|
||||
@ -1227,6 +1276,11 @@ public final class C {
|
||||
/** See {@link MediaFormat#COLOR_RANGE_FULL}. */
|
||||
@UnstableApi public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL;
|
||||
|
||||
// LINT.ThenChange(
|
||||
// util/MediaFormatUtil.java:color_range,
|
||||
// ColorInfo.java:color_range,
|
||||
// )
|
||||
|
||||
/** Video projection types. */
|
||||
@UnstableApi
|
||||
@Documented
|
||||
@ -1520,6 +1574,8 @@ public final class C {
|
||||
*/
|
||||
public static final int ROLE_FLAG_AUXILIARY = 1 << 15;
|
||||
|
||||
// LINT.ThenChange("util/Util.java:role_flags")
|
||||
|
||||
/**
|
||||
* {@linkplain #ROLE_FLAG_AUXILIARY Auxiliary track types}. One of {@link
|
||||
* #AUXILIARY_TRACK_TYPE_UNDEFINED}, {@link #AUXILIARY_TRACK_TYPE_ORIGINAL}, {@link
|
||||
@ -1565,6 +1621,8 @@ public final class C {
|
||||
/** A timed metadata of depth video track. */
|
||||
@UnstableApi public static final int AUXILIARY_TRACK_TYPE_DEPTH_METADATA = 4;
|
||||
|
||||
// LINT.ThenChange("util/Util.java:auxiliary_track_type")
|
||||
|
||||
/**
|
||||
* Level of support for a format. One of {@link #FORMAT_HANDLED}, {@link
|
||||
* #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, {@link
|
||||
@ -1666,6 +1724,40 @@ public final class C {
|
||||
/** The first frame was rendered. */
|
||||
@UnstableApi public static final int FIRST_FRAME_RENDERED = 3;
|
||||
|
||||
/**
|
||||
* Texture filtering algorithm for minification.
|
||||
*
|
||||
* <p>Possible values are:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link #TEXTURE_MIN_FILTER_LINEAR}
|
||||
* <li>{@link #TEXTURE_MIN_FILTER_LINEAR_MIPMAP_LINEAR}
|
||||
* </ul>
|
||||
*
|
||||
* <p>The algorithms are ordered by increasing visual quality and computational cost.
|
||||
*/
|
||||
@UnstableApi
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(TYPE_USE)
|
||||
@IntDef({TEXTURE_MIN_FILTER_LINEAR, TEXTURE_MIN_FILTER_LINEAR_MIPMAP_LINEAR})
|
||||
public @interface TextureMinFilter {}
|
||||
|
||||
/**
|
||||
* Returns the weighted average of the four texture elements that are closest to the specified
|
||||
* texture coordinates.
|
||||
*/
|
||||
@UnstableApi public static final int TEXTURE_MIN_FILTER_LINEAR = GLES20.GL_LINEAR;
|
||||
|
||||
/**
|
||||
* Chooses the two mipmaps that most closely match the size of the pixel being textured and uses
|
||||
* the {@link C#TEXTURE_MIN_FILTER_LINEAR} criterion (a weighted average of the texture elements
|
||||
* that are closest to the specified texture coordinates) to produce a texture value from each
|
||||
* mipmap. The final texture value is a weighted average of those two values.
|
||||
*/
|
||||
@UnstableApi
|
||||
public static final int TEXTURE_MIN_FILTER_LINEAR_MIPMAP_LINEAR = GLES20.GL_LINEAR_MIPMAP_LINEAR;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link Util#usToMs(long)}.
|
||||
*/
|
||||
|
@ -487,6 +487,7 @@ public final class ColorInfo {
|
||||
default:
|
||||
return "Undefined color space " + colorSpace;
|
||||
}
|
||||
// LINT.ThenChange(C.java:color_space)
|
||||
}
|
||||
|
||||
private static String colorTransferToString(@C.ColorTransfer int colorTransfer) {
|
||||
@ -509,6 +510,7 @@ public final class ColorInfo {
|
||||
default:
|
||||
return "Undefined color transfer " + colorTransfer;
|
||||
}
|
||||
// LINT.ThenChange(C.java:color_transfer)
|
||||
}
|
||||
|
||||
private static String colorRangeToString(@C.ColorRange int colorRange) {
|
||||
@ -523,6 +525,7 @@ public final class ColorInfo {
|
||||
default:
|
||||
return "Undefined color range " + colorRange;
|
||||
}
|
||||
// LINT.ThenChange(C.java:color_range)
|
||||
}
|
||||
|
||||
private static final String FIELD_COLOR_SPACE = Util.intToStringMaxRadix(0);
|
||||
|
@ -30,6 +30,7 @@ import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.Objects;
|
||||
|
||||
/** Information about the playback device. */
|
||||
public final class DeviceInfo {
|
||||
@ -178,7 +179,7 @@ public final class DeviceInfo {
|
||||
return playbackType == other.playbackType
|
||||
&& minVolume == other.minVolume
|
||||
&& maxVolume == other.maxVolume
|
||||
&& Util.areEqual(routingControllerId, other.routingControllerId);
|
||||
&& Objects.equals(routingControllerId, other.routingControllerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -227,5 +228,4 @@ public final class DeviceInfo {
|
||||
.setRoutingControllerId(routingControllerId)
|
||||
.build();
|
||||
}
|
||||
;
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Initialization data for one or more DRM schemes. */
|
||||
@ -160,7 +161,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
|
||||
*/
|
||||
@CheckResult
|
||||
public DrmInitData copyWithSchemeType(@Nullable String schemeType) {
|
||||
if (Util.areEqual(this.schemeType, schemeType)) {
|
||||
if (Objects.equals(this.schemeType, schemeType)) {
|
||||
return this;
|
||||
}
|
||||
return new DrmInitData(schemeType, false, schemeDatas);
|
||||
@ -204,7 +205,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
|
||||
return false;
|
||||
}
|
||||
DrmInitData other = (DrmInitData) obj;
|
||||
return Util.areEqual(schemeType, other.schemeType)
|
||||
return Objects.equals(schemeType, other.schemeType)
|
||||
&& Arrays.equals(schemeDatas, other.schemeDatas);
|
||||
}
|
||||
|
||||
@ -352,9 +353,9 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
|
||||
return true;
|
||||
}
|
||||
SchemeData other = (SchemeData) obj;
|
||||
return Util.areEqual(licenseServerUrl, other.licenseServerUrl)
|
||||
&& Util.areEqual(mimeType, other.mimeType)
|
||||
&& Util.areEqual(uuid, other.uuid)
|
||||
return Objects.equals(licenseServerUrl, other.licenseServerUrl)
|
||||
&& Objects.equals(mimeType, other.mimeType)
|
||||
&& Objects.equals(uuid, other.uuid)
|
||||
&& Arrays.equals(data, other.data);
|
||||
}
|
||||
|
||||
|
@ -103,6 +103,7 @@ import java.util.UUID;
|
||||
* <li>{@link #projectionData}
|
||||
* <li>{@link #stereoMode}
|
||||
* <li>{@link #colorInfo}
|
||||
* <li>{@link #maxSubLayers}
|
||||
* </ul>
|
||||
*
|
||||
* <h2 id="audio-formats">Fields relevant to audio formats</h2>
|
||||
@ -179,6 +180,7 @@ public final class Format {
|
||||
@Nullable private byte[] projectionData;
|
||||
private @C.StereoMode int stereoMode;
|
||||
@Nullable private ColorInfo colorInfo;
|
||||
private int maxSubLayers;
|
||||
|
||||
// Audio specific.
|
||||
|
||||
@ -217,6 +219,7 @@ public final class Format {
|
||||
frameRate = NO_VALUE;
|
||||
pixelWidthHeightRatio = 1.0f;
|
||||
stereoMode = NO_VALUE;
|
||||
maxSubLayers = NO_VALUE;
|
||||
// Audio specific.
|
||||
channelCount = NO_VALUE;
|
||||
sampleRate = NO_VALUE;
|
||||
@ -268,6 +271,7 @@ public final class Format {
|
||||
this.projectionData = format.projectionData;
|
||||
this.stereoMode = format.stereoMode;
|
||||
this.colorInfo = format.colorInfo;
|
||||
this.maxSubLayers = format.maxSubLayers;
|
||||
// Audio specific.
|
||||
this.channelCount = format.channelCount;
|
||||
this.sampleRate = format.sampleRate;
|
||||
@ -656,6 +660,18 @@ public final class Format {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets {@link Format#maxSubLayers}. The default value is {@link #NO_VALUE}.
|
||||
*
|
||||
* @param maxSubLayers The {@link Format#maxSubLayers}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMaxSubLayers(int maxSubLayers) {
|
||||
this.maxSubLayers = maxSubLayers;
|
||||
return this;
|
||||
}
|
||||
|
||||
// Audio specific.
|
||||
|
||||
/**
|
||||
@ -1009,6 +1025,12 @@ public final class Format {
|
||||
/** The color metadata associated with the video, or null if not applicable. */
|
||||
@UnstableApi @Nullable public final ColorInfo colorInfo;
|
||||
|
||||
/**
|
||||
* The maximum number of temporal scalable sub-layers in the video bitstream, or {@link #NO_VALUE}
|
||||
* if not applicable.
|
||||
*/
|
||||
@UnstableApi public final int maxSubLayers;
|
||||
|
||||
// Audio specific.
|
||||
|
||||
/** The number of audio channels, or {@link #NO_VALUE} if unknown or not applicable. */
|
||||
@ -1127,6 +1149,7 @@ public final class Format {
|
||||
projectionData = builder.projectionData;
|
||||
stereoMode = builder.stereoMode;
|
||||
colorInfo = builder.colorInfo;
|
||||
maxSubLayers = builder.maxSubLayers;
|
||||
// Audio specific.
|
||||
channelCount = builder.channelCount;
|
||||
sampleRate = builder.sampleRate;
|
||||
@ -1309,6 +1332,7 @@ public final class Format {
|
||||
// [Omitted] projectionData.
|
||||
result = 31 * result + stereoMode;
|
||||
// [Omitted] colorInfo.
|
||||
result = 31 * result + maxSubLayers;
|
||||
// Audio specific.
|
||||
result = 31 * result + channelCount;
|
||||
result = 31 * result + sampleRate;
|
||||
@ -1351,6 +1375,7 @@ public final class Format {
|
||||
&& height == other.height
|
||||
&& rotationDegrees == other.rotationDegrees
|
||||
&& stereoMode == other.stereoMode
|
||||
&& maxSubLayers == other.maxSubLayers
|
||||
&& channelCount == other.channelCount
|
||||
&& sampleRate == other.sampleRate
|
||||
&& pcmEncoding == other.pcmEncoding
|
||||
@ -1452,6 +1477,9 @@ public final class Format {
|
||||
if (format.frameRate != NO_VALUE) {
|
||||
builder.append(", fps=").append(format.frameRate);
|
||||
}
|
||||
if (format.maxSubLayers != NO_VALUE) {
|
||||
builder.append(", maxSubLayers=").append(format.maxSubLayers);
|
||||
}
|
||||
if (format.channelCount != NO_VALUE) {
|
||||
builder.append(", channels=").append(format.channelCount);
|
||||
}
|
||||
@ -1496,7 +1524,8 @@ public final class Format {
|
||||
private static final String FIELD_AVERAGE_BITRATE = Util.intToStringMaxRadix(5);
|
||||
private static final String FIELD_PEAK_BITRATE = Util.intToStringMaxRadix(6);
|
||||
private static final String FIELD_CODECS = Util.intToStringMaxRadix(7);
|
||||
private static final String FIELD_METADATA = Util.intToStringMaxRadix(8);
|
||||
// Do not reuse this key.
|
||||
private static final String UNUSED_FIELD_METADATA = Util.intToStringMaxRadix(8);
|
||||
private static final String FIELD_CONTAINER_MIME_TYPE = Util.intToStringMaxRadix(9);
|
||||
private static final String FIELD_SAMPLE_MIME_TYPE = Util.intToStringMaxRadix(10);
|
||||
private static final String FIELD_MAX_INPUT_SIZE = Util.intToStringMaxRadix(11);
|
||||
@ -1522,22 +1551,14 @@ public final class Format {
|
||||
private static final String FIELD_TILE_COUNT_VERTICAL = Util.intToStringMaxRadix(31);
|
||||
private static final String FIELD_LABELS = Util.intToStringMaxRadix(32);
|
||||
private static final String FIELD_AUXILIARY_TRACK_TYPE = Util.intToStringMaxRadix(33);
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #toBundle(boolean)} instead.
|
||||
*/
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Bundle toBundle() {
|
||||
return toBundle(/* excludeMetadata= */ false);
|
||||
}
|
||||
private static final String FIELD_MAX_SUB_LAYERS = Util.intToStringMaxRadix(34);
|
||||
|
||||
/**
|
||||
* Returns a {@link Bundle} representing the information stored in this object. If {@code
|
||||
* excludeMetadata} is true, {@linkplain Format#metadata metadata} is excluded.
|
||||
*/
|
||||
@UnstableApi
|
||||
public Bundle toBundle(boolean excludeMetadata) {
|
||||
public Bundle toBundle() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(FIELD_ID, id);
|
||||
bundle.putString(FIELD_LABEL, label);
|
||||
@ -1552,10 +1573,7 @@ public final class Format {
|
||||
bundle.putInt(FIELD_AVERAGE_BITRATE, averageBitrate);
|
||||
bundle.putInt(FIELD_PEAK_BITRATE, peakBitrate);
|
||||
bundle.putString(FIELD_CODECS, codecs);
|
||||
if (!excludeMetadata) {
|
||||
// TODO (internal ref: b/239701618)
|
||||
bundle.putParcelable(FIELD_METADATA, metadata);
|
||||
}
|
||||
// The metadata does not implement toBundle() method, hence can not be added.
|
||||
// Container specific.
|
||||
bundle.putString(FIELD_CONTAINER_MIME_TYPE, containerMimeType);
|
||||
// Sample specific.
|
||||
@ -1579,6 +1597,7 @@ public final class Format {
|
||||
if (colorInfo != null) {
|
||||
bundle.putBundle(FIELD_COLOR_INFO, colorInfo.toBundle());
|
||||
}
|
||||
bundle.putInt(FIELD_MAX_SUB_LAYERS, maxSubLayers);
|
||||
// Audio specific.
|
||||
bundle.putInt(FIELD_CHANNEL_COUNT, channelCount);
|
||||
bundle.putInt(FIELD_SAMPLE_RATE, sampleRate);
|
||||
@ -1618,7 +1637,6 @@ public final class Format {
|
||||
.setAverageBitrate(bundle.getInt(FIELD_AVERAGE_BITRATE, DEFAULT.averageBitrate))
|
||||
.setPeakBitrate(bundle.getInt(FIELD_PEAK_BITRATE, DEFAULT.peakBitrate))
|
||||
.setCodecs(defaultIfNull(bundle.getString(FIELD_CODECS), DEFAULT.codecs))
|
||||
.setMetadata(defaultIfNull(bundle.getParcelable(FIELD_METADATA), DEFAULT.metadata))
|
||||
// Container specific.
|
||||
.setContainerMimeType(
|
||||
defaultIfNull(bundle.getString(FIELD_CONTAINER_MIME_TYPE), DEFAULT.containerMimeType))
|
||||
@ -1647,7 +1665,8 @@ public final class Format {
|
||||
.setPixelWidthHeightRatio(
|
||||
bundle.getFloat(FIELD_PIXEL_WIDTH_HEIGHT_RATIO, DEFAULT.pixelWidthHeightRatio))
|
||||
.setProjectionData(bundle.getByteArray(FIELD_PROJECTION_DATA))
|
||||
.setStereoMode(bundle.getInt(FIELD_STEREO_MODE, DEFAULT.stereoMode));
|
||||
.setStereoMode(bundle.getInt(FIELD_STEREO_MODE, DEFAULT.stereoMode))
|
||||
.setMaxSubLayers(bundle.getInt(FIELD_MAX_SUB_LAYERS, DEFAULT.maxSubLayers));
|
||||
Bundle colorInfoBundle = bundle.getBundle(FIELD_COLOR_INFO);
|
||||
if (colorInfoBundle != null) {
|
||||
builder.setColorInfo(ColorInfo.fromBundle(colorInfoBundle));
|
||||
|
@ -25,7 +25,6 @@ import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import java.util.List;
|
||||
|
||||
// LINT.IfChange(javadoc)
|
||||
/**
|
||||
* A {@link SimpleBasePlayer} that forwards all calls to another {@link Player} instance.
|
||||
*
|
||||
@ -60,7 +59,7 @@ public class ForwardingSimpleBasePlayer extends SimpleBasePlayer {
|
||||
|
||||
private final Player player;
|
||||
|
||||
private ForwardingPositionSupplier currentPositionSupplier;
|
||||
private LivePositionSuppliers livePositionSuppliers;
|
||||
private Metadata lastTimedMetadata;
|
||||
private @Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason;
|
||||
private @Player.DiscontinuityReason int pendingDiscontinuityReason;
|
||||
@ -78,7 +77,7 @@ public class ForwardingSimpleBasePlayer extends SimpleBasePlayer {
|
||||
this.lastTimedMetadata = new Metadata(/* presentationTimeUs= */ C.TIME_UNSET);
|
||||
this.playWhenReadyChangeReason = Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST;
|
||||
this.pendingDiscontinuityReason = Player.DISCONTINUITY_REASON_INTERNAL;
|
||||
this.currentPositionSupplier = new ForwardingPositionSupplier(player);
|
||||
this.livePositionSuppliers = new LivePositionSuppliers(player);
|
||||
player.addListener(
|
||||
new Listener() {
|
||||
@Override
|
||||
@ -99,15 +98,8 @@ public class ForwardingSimpleBasePlayer extends SimpleBasePlayer {
|
||||
@Player.DiscontinuityReason int reason) {
|
||||
pendingDiscontinuityReason = reason;
|
||||
pendingPositionDiscontinuityNewPositionMs = newPosition.positionMs;
|
||||
// Any previously created State will directly call through to player.getCurrentPosition
|
||||
// via the existing position supplier. From this point onwards, this is wrong as the
|
||||
// player had a discontinuity and will now return a new position unrelated to the old
|
||||
// State. We can disconnect these old State objects from the underlying Player by fixing
|
||||
// the position to the one before the discontinuity and using a new (live) position
|
||||
// supplier for future State objects.
|
||||
currentPositionSupplier.setConstant(
|
||||
oldPosition.positionMs, oldPosition.contentPositionMs);
|
||||
currentPositionSupplier = new ForwardingPositionSupplier(player);
|
||||
livePositionSuppliers.disconnect(oldPosition.positionMs, oldPosition.contentPositionMs);
|
||||
livePositionSuppliers = new LivePositionSuppliers(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -132,18 +124,18 @@ public class ForwardingSimpleBasePlayer extends SimpleBasePlayer {
|
||||
protected State getState() {
|
||||
// Ordered alphabetically by State.Builder setters.
|
||||
State.Builder state = new State.Builder();
|
||||
ForwardingPositionSupplier positionSupplier = currentPositionSupplier;
|
||||
LivePositionSuppliers positionSuppliers = livePositionSuppliers;
|
||||
if (player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) {
|
||||
state.setAdBufferedPositionMs(positionSupplier::getBufferedPositionMs);
|
||||
state.setAdPositionMs(positionSupplier::getCurrentPositionMs);
|
||||
state.setAdBufferedPositionMs(positionSuppliers.bufferedPositionSupplier);
|
||||
state.setAdPositionMs(positionSuppliers.currentPositionSupplier);
|
||||
}
|
||||
if (player.isCommandAvailable(Player.COMMAND_GET_AUDIO_ATTRIBUTES)) {
|
||||
state.setAudioAttributes(player.getAudioAttributes());
|
||||
}
|
||||
state.setAvailableCommands(player.getAvailableCommands());
|
||||
if (player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) {
|
||||
state.setContentBufferedPositionMs(positionSupplier::getContentBufferedPositionMs);
|
||||
state.setContentPositionMs(positionSupplier::getContentPositionMs);
|
||||
state.setContentBufferedPositionMs(positionSuppliers.contentBufferedPositionSupplier);
|
||||
state.setContentPositionMs(positionSuppliers.contentPositionSupplier);
|
||||
if (player.isCommandAvailable(Player.COMMAND_GET_TIMELINE)) {
|
||||
state.setCurrentAd(player.getCurrentAdGroupIndex(), player.getCurrentAdIndexInAdGroup());
|
||||
}
|
||||
@ -194,7 +186,7 @@ public class ForwardingSimpleBasePlayer extends SimpleBasePlayer {
|
||||
state.setSurfaceSize(player.getSurfaceSize());
|
||||
state.setTimedMetadata(lastTimedMetadata);
|
||||
if (player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) {
|
||||
state.setTotalBufferedDurationMs(positionSupplier::getTotalBufferedDurationMs);
|
||||
state.setTotalBufferedDurationMs(positionSuppliers.totalBufferedPositionSupplier);
|
||||
}
|
||||
state.setTrackSelectionParameters(player.getTrackSelectionParameters());
|
||||
state.setVideoSize(player.getVideoSize());
|
||||
@ -456,44 +448,29 @@ public class ForwardingSimpleBasePlayer extends SimpleBasePlayer {
|
||||
* Forwards to the changing position values of the wrapped player until the forwarding is
|
||||
* deactivated with constant values.
|
||||
*/
|
||||
private static final class ForwardingPositionSupplier {
|
||||
private static final class LivePositionSuppliers {
|
||||
|
||||
private final Player player;
|
||||
public final LivePositionSupplier currentPositionSupplier;
|
||||
public final LivePositionSupplier bufferedPositionSupplier;
|
||||
public final LivePositionSupplier contentPositionSupplier;
|
||||
public final LivePositionSupplier contentBufferedPositionSupplier;
|
||||
public final LivePositionSupplier totalBufferedPositionSupplier;
|
||||
|
||||
private long positionsMs;
|
||||
private long contentPositionMs;
|
||||
|
||||
public ForwardingPositionSupplier(Player player) {
|
||||
this.player = player;
|
||||
this.positionsMs = C.TIME_UNSET;
|
||||
this.contentPositionMs = C.TIME_UNSET;
|
||||
public LivePositionSuppliers(Player player) {
|
||||
currentPositionSupplier = new LivePositionSupplier(player::getCurrentPosition);
|
||||
bufferedPositionSupplier = new LivePositionSupplier(player::getBufferedPosition);
|
||||
contentPositionSupplier = new LivePositionSupplier(player::getContentPosition);
|
||||
contentBufferedPositionSupplier =
|
||||
new LivePositionSupplier(player::getContentBufferedPosition);
|
||||
totalBufferedPositionSupplier = new LivePositionSupplier(player::getTotalBufferedDuration);
|
||||
}
|
||||
|
||||
public void setConstant(long positionMs, long contentPositionMs) {
|
||||
this.positionsMs = positionMs;
|
||||
this.contentPositionMs = contentPositionMs;
|
||||
}
|
||||
|
||||
public long getCurrentPositionMs() {
|
||||
return positionsMs == C.TIME_UNSET ? player.getCurrentPosition() : positionsMs;
|
||||
}
|
||||
|
||||
public long getBufferedPositionMs() {
|
||||
return positionsMs == C.TIME_UNSET ? player.getBufferedPosition() : positionsMs;
|
||||
}
|
||||
|
||||
public long getContentPositionMs() {
|
||||
return contentPositionMs == C.TIME_UNSET ? player.getContentPosition() : contentPositionMs;
|
||||
}
|
||||
|
||||
public long getContentBufferedPositionMs() {
|
||||
return contentPositionMs == C.TIME_UNSET
|
||||
? player.getContentBufferedPosition()
|
||||
: contentPositionMs;
|
||||
}
|
||||
|
||||
public long getTotalBufferedDurationMs() {
|
||||
return positionsMs == C.TIME_UNSET ? player.getTotalBufferedDuration() : 0;
|
||||
public void disconnect(long positionMs, long contentPositionMs) {
|
||||
currentPositionSupplier.disconnect(positionMs);
|
||||
bufferedPositionSupplier.disconnect(positionMs);
|
||||
contentPositionSupplier.disconnect(contentPositionMs);
|
||||
contentBufferedPositionSupplier.disconnect(contentPositionMs);
|
||||
totalBufferedPositionSupplier.disconnect(/* finalValue= */ 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,123 +18,34 @@ package androidx.media3.common;
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
|
||||
/** Value class specifying information about a decoded video frame. */
|
||||
@UnstableApi
|
||||
public class FrameInfo {
|
||||
|
||||
/** A builder for {@link FrameInfo} instances. */
|
||||
public static final class Builder {
|
||||
|
||||
private ColorInfo colorInfo;
|
||||
private int width;
|
||||
private int height;
|
||||
private float pixelWidthHeightRatio;
|
||||
private long offsetToAddUs;
|
||||
|
||||
/**
|
||||
* Creates an instance with default values.
|
||||
*
|
||||
* @param colorInfo The {@link ColorInfo}.
|
||||
* @param width The frame width, in pixels.
|
||||
* @param height The frame height, in pixels.
|
||||
*/
|
||||
public Builder(ColorInfo colorInfo, int width, int height) {
|
||||
this.colorInfo = colorInfo;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
pixelWidthHeightRatio = 1;
|
||||
}
|
||||
|
||||
/** Creates an instance with the values of the provided {@link FrameInfo}. */
|
||||
public Builder(FrameInfo frameInfo) {
|
||||
colorInfo = frameInfo.colorInfo;
|
||||
width = frameInfo.width;
|
||||
height = frameInfo.height;
|
||||
pixelWidthHeightRatio = frameInfo.pixelWidthHeightRatio;
|
||||
offsetToAddUs = frameInfo.offsetToAddUs;
|
||||
}
|
||||
|
||||
/** Sets the {@link ColorInfo}. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setColorInfo(ColorInfo colorInfo) {
|
||||
this.colorInfo = colorInfo;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the frame width, in pixels. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setWidth(int width) {
|
||||
this.width = width;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the frame height, in pixels. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setHeight(int height) {
|
||||
this.height = height;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ratio of width over height for each pixel.
|
||||
*
|
||||
* <p>The default value is {@code 1}.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPixelWidthHeightRatio(float pixelWidthHeightRatio) {
|
||||
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@linkplain FrameInfo#offsetToAddUs offset to add} to the frame presentation
|
||||
* timestamp, in microseconds.
|
||||
*
|
||||
* <p>The default value is {@code 0}.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setOffsetToAddUs(long offsetToAddUs) {
|
||||
this.offsetToAddUs = offsetToAddUs;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Builds a {@link FrameInfo} instance. */
|
||||
public FrameInfo build() {
|
||||
return new FrameInfo(colorInfo, width, height, pixelWidthHeightRatio, offsetToAddUs);
|
||||
}
|
||||
}
|
||||
|
||||
/** The {@link ColorInfo} of the frame. */
|
||||
public final ColorInfo colorInfo;
|
||||
|
||||
/** The width of the frame, in pixels. */
|
||||
public final int width;
|
||||
|
||||
/** The height of the frame, in pixels. */
|
||||
public final int height;
|
||||
|
||||
/** The ratio of width over height for each pixel. */
|
||||
public final float pixelWidthHeightRatio;
|
||||
|
||||
/**
|
||||
* The offset that must be added to the frame presentation timestamp, in microseconds.
|
||||
* The {@link Format} of the frame.
|
||||
*
|
||||
* <p>This offset is not part of the input timestamps. It is added to the frame timestamps before
|
||||
* processing, and is retained in the output timestamps.
|
||||
* <p>The {@link Format#colorInfo} must be set, and the {@link Format#width} and {@link
|
||||
* Format#height} must be greater than 0.
|
||||
*/
|
||||
public final Format format;
|
||||
|
||||
/** The offset that must be added to the frame presentation timestamp, in microseconds. */
|
||||
public final long offsetToAddUs;
|
||||
|
||||
private FrameInfo(
|
||||
ColorInfo colorInfo, int width, int height, float pixelWidthHeightRatio, long offsetToAddUs) {
|
||||
checkArgument(width > 0, "width must be positive, but is: " + width);
|
||||
checkArgument(height > 0, "height must be positive, but is: " + height);
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @param format See {@link #format}.
|
||||
* @param offsetToAddUs See {@link #offsetToAddUs}.
|
||||
*/
|
||||
public FrameInfo(Format format, long offsetToAddUs) {
|
||||
checkArgument(format.colorInfo != null, "format colorInfo must be set");
|
||||
checkArgument(format.width > 0, "format width must be positive, but is: " + format.width);
|
||||
checkArgument(format.height > 0, "format height must be positive, but is: " + format.height);
|
||||
|
||||
this.colorInfo = colorInfo;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
|
||||
this.format = format;
|
||||
this.offsetToAddUs = offsetToAddUs;
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.base.Objects;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A rating expressed as "heart" or "no heart". It can be used to indicate whether the content is a
|
||||
@ -60,7 +60,7 @@ public final class HeartRating extends Rating {
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(rated, isHeart);
|
||||
return Objects.hash(rated, isHeart);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -21,6 +21,7 @@ import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import java.util.Objects;
|
||||
|
||||
/** A label for a {@link Format}. */
|
||||
@UnstableApi
|
||||
@ -55,7 +56,7 @@ public class Label {
|
||||
return false;
|
||||
}
|
||||
Label label = (Label) o;
|
||||
return Util.areEqual(language, label.language) && Util.areEqual(value, label.value);
|
||||
return Objects.equals(language, label.language) && Objects.equals(value, label.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -39,6 +39,7 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Representation of a media item. */
|
||||
@ -915,8 +916,8 @@ public final class MediaItem {
|
||||
|
||||
DrmConfiguration other = (DrmConfiguration) obj;
|
||||
return scheme.equals(other.scheme)
|
||||
&& Util.areEqual(licenseUri, other.licenseUri)
|
||||
&& Util.areEqual(licenseRequestHeaders, other.licenseRequestHeaders)
|
||||
&& Objects.equals(licenseUri, other.licenseUri)
|
||||
&& Objects.equals(licenseRequestHeaders, other.licenseRequestHeaders)
|
||||
&& multiSession == other.multiSession
|
||||
&& forceDefaultLicenseUri == other.forceDefaultLicenseUri
|
||||
&& playClearContentWithoutKey == other.playClearContentWithoutKey
|
||||
@ -1090,7 +1091,7 @@ public final class MediaItem {
|
||||
}
|
||||
|
||||
AdsConfiguration other = (AdsConfiguration) obj;
|
||||
return adTagUri.equals(other.adTagUri) && Util.areEqual(adsId, other.adsId);
|
||||
return adTagUri.equals(other.adTagUri) && Objects.equals(adsId, other.adsId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1209,14 +1210,14 @@ public final class MediaItem {
|
||||
LocalConfiguration other = (LocalConfiguration) obj;
|
||||
|
||||
return uri.equals(other.uri)
|
||||
&& Util.areEqual(mimeType, other.mimeType)
|
||||
&& Util.areEqual(drmConfiguration, other.drmConfiguration)
|
||||
&& Util.areEqual(adsConfiguration, other.adsConfiguration)
|
||||
&& Objects.equals(mimeType, other.mimeType)
|
||||
&& Objects.equals(drmConfiguration, other.drmConfiguration)
|
||||
&& Objects.equals(adsConfiguration, other.adsConfiguration)
|
||||
&& streamKeys.equals(other.streamKeys)
|
||||
&& Util.areEqual(customCacheKey, other.customCacheKey)
|
||||
&& Objects.equals(customCacheKey, other.customCacheKey)
|
||||
&& subtitleConfigurations.equals(other.subtitleConfigurations)
|
||||
&& Util.areEqual(tag, other.tag)
|
||||
&& Util.areEqual(imageDurationMs, other.imageDurationMs);
|
||||
&& Objects.equals(tag, other.tag)
|
||||
&& imageDurationMs == other.imageDurationMs;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1714,12 +1715,12 @@ public final class MediaItem {
|
||||
SubtitleConfiguration other = (SubtitleConfiguration) obj;
|
||||
|
||||
return uri.equals(other.uri)
|
||||
&& Util.areEqual(mimeType, other.mimeType)
|
||||
&& Util.areEqual(language, other.language)
|
||||
&& Objects.equals(mimeType, other.mimeType)
|
||||
&& Objects.equals(language, other.language)
|
||||
&& selectionFlags == other.selectionFlags
|
||||
&& roleFlags == other.roleFlags
|
||||
&& Util.areEqual(label, other.label)
|
||||
&& Util.areEqual(id, other.id);
|
||||
&& Objects.equals(label, other.label)
|
||||
&& Objects.equals(id, other.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -2216,8 +2217,8 @@ public final class MediaItem {
|
||||
return false;
|
||||
}
|
||||
RequestMetadata that = (RequestMetadata) o;
|
||||
return Util.areEqual(mediaUri, that.mediaUri)
|
||||
&& Util.areEqual(searchQuery, that.searchQuery)
|
||||
return Objects.equals(mediaUri, that.mediaUri)
|
||||
&& Objects.equals(searchQuery, that.searchQuery)
|
||||
&& ((extras == null) == (that.extras == null));
|
||||
}
|
||||
|
||||
@ -2337,12 +2338,12 @@ public final class MediaItem {
|
||||
|
||||
MediaItem other = (MediaItem) obj;
|
||||
|
||||
return Util.areEqual(mediaId, other.mediaId)
|
||||
return Objects.equals(mediaId, other.mediaId)
|
||||
&& clippingConfiguration.equals(other.clippingConfiguration)
|
||||
&& Util.areEqual(localConfiguration, other.localConfiguration)
|
||||
&& Util.areEqual(liveConfiguration, other.liveConfiguration)
|
||||
&& Util.areEqual(mediaMetadata, other.mediaMetadata)
|
||||
&& Util.areEqual(requestMetadata, other.requestMetadata);
|
||||
&& Objects.equals(localConfiguration, other.localConfiguration)
|
||||
&& Objects.equals(liveConfiguration, other.liveConfiguration)
|
||||
&& Objects.equals(mediaMetadata, other.mediaMetadata)
|
||||
&& Objects.equals(requestMetadata, other.requestMetadata);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -29,11 +29,11 @@ public final class MediaLibraryInfo {
|
||||
|
||||
/** The version of the library expressed as a string, for example "1.2.3" or "1.2.0-beta01". */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
||||
public static final String VERSION = "1.5.1";
|
||||
public static final String VERSION = "1.6.0";
|
||||
|
||||
/** The version of the library expressed as {@code TAG + "/" + VERSION}. */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final String VERSION_SLASHY = "AndroidXMedia3/1.5.1";
|
||||
public static final String VERSION_SLASHY = "AndroidXMedia3/1.6.0";
|
||||
|
||||
/**
|
||||
* The version of the library expressed as an integer, for example 1002003300.
|
||||
@ -47,7 +47,7 @@ public final class MediaLibraryInfo {
|
||||
* (123-045-006-3-00).
|
||||
*/
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final int VERSION_INT = 1_005_001_3_00;
|
||||
public static final int VERSION_INT = 1_006_000_3_00;
|
||||
|
||||
/** Whether the library was compiled with {@link Assertions} checks enabled. */
|
||||
public static final boolean ASSERTIONS_ENABLED = true;
|
||||
|
@ -29,7 +29,6 @@ import androidx.annotation.IntRange;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.lang.annotation.Documented;
|
||||
@ -39,6 +38,7 @@ import java.lang.annotation.Target;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Metadata of a {@link MediaItem}, playlist, or a combination of multiple sources of {@link
|
||||
@ -194,7 +194,6 @@ public final class MediaMetadata {
|
||||
*
|
||||
* @throws IllegalArgumentException if the duration is negative.
|
||||
*/
|
||||
@UnstableApi
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setDurationMs(@Nullable Long durationMs) {
|
||||
checkArgument(durationMs == null || durationMs >= 0);
|
||||
@ -250,8 +249,8 @@ public final class MediaMetadata {
|
||||
@CanIgnoreReturnValue
|
||||
public Builder maybeSetArtworkData(byte[] artworkData, @PictureType int artworkDataType) {
|
||||
if (this.artworkData == null
|
||||
|| Util.areEqual(artworkDataType, PICTURE_TYPE_FRONT_COVER)
|
||||
|| !Util.areEqual(this.artworkDataType, PICTURE_TYPE_FRONT_COVER)) {
|
||||
|| artworkDataType == PICTURE_TYPE_FRONT_COVER
|
||||
|| !Objects.equals(this.artworkDataType, PICTURE_TYPE_FRONT_COVER)) {
|
||||
this.artworkData = artworkData.clone();
|
||||
this.artworkDataType = artworkDataType;
|
||||
}
|
||||
@ -1029,7 +1028,7 @@ public final class MediaMetadata {
|
||||
* informational purpose only. For retrieving the duration of the media item currently being
|
||||
* played, use {@link Player#getDuration()} instead.
|
||||
*/
|
||||
@UnstableApi @Nullable public final Long durationMs;
|
||||
@Nullable public final Long durationMs;
|
||||
|
||||
/** Optional user {@link Rating}. */
|
||||
@Nullable public final Rating userRating;
|
||||
@ -1222,47 +1221,47 @@ public final class MediaMetadata {
|
||||
return false;
|
||||
}
|
||||
MediaMetadata that = (MediaMetadata) obj;
|
||||
return Util.areEqual(title, that.title)
|
||||
&& Util.areEqual(artist, that.artist)
|
||||
&& Util.areEqual(albumTitle, that.albumTitle)
|
||||
&& Util.areEqual(albumArtist, that.albumArtist)
|
||||
&& Util.areEqual(displayTitle, that.displayTitle)
|
||||
&& Util.areEqual(subtitle, that.subtitle)
|
||||
&& Util.areEqual(description, that.description)
|
||||
&& Util.areEqual(durationMs, that.durationMs)
|
||||
&& Util.areEqual(userRating, that.userRating)
|
||||
&& Util.areEqual(overallRating, that.overallRating)
|
||||
return Objects.equals(title, that.title)
|
||||
&& Objects.equals(artist, that.artist)
|
||||
&& Objects.equals(albumTitle, that.albumTitle)
|
||||
&& Objects.equals(albumArtist, that.albumArtist)
|
||||
&& Objects.equals(displayTitle, that.displayTitle)
|
||||
&& Objects.equals(subtitle, that.subtitle)
|
||||
&& Objects.equals(description, that.description)
|
||||
&& Objects.equals(durationMs, that.durationMs)
|
||||
&& Objects.equals(userRating, that.userRating)
|
||||
&& Objects.equals(overallRating, that.overallRating)
|
||||
&& Arrays.equals(artworkData, that.artworkData)
|
||||
&& Util.areEqual(artworkDataType, that.artworkDataType)
|
||||
&& Util.areEqual(artworkUri, that.artworkUri)
|
||||
&& Util.areEqual(trackNumber, that.trackNumber)
|
||||
&& Util.areEqual(totalTrackCount, that.totalTrackCount)
|
||||
&& Util.areEqual(folderType, that.folderType)
|
||||
&& Util.areEqual(isBrowsable, that.isBrowsable)
|
||||
&& Util.areEqual(isPlayable, that.isPlayable)
|
||||
&& Util.areEqual(recordingYear, that.recordingYear)
|
||||
&& Util.areEqual(recordingMonth, that.recordingMonth)
|
||||
&& Util.areEqual(recordingDay, that.recordingDay)
|
||||
&& Util.areEqual(releaseYear, that.releaseYear)
|
||||
&& Util.areEqual(releaseMonth, that.releaseMonth)
|
||||
&& Util.areEqual(releaseDay, that.releaseDay)
|
||||
&& Util.areEqual(writer, that.writer)
|
||||
&& Util.areEqual(composer, that.composer)
|
||||
&& Util.areEqual(conductor, that.conductor)
|
||||
&& Util.areEqual(discNumber, that.discNumber)
|
||||
&& Util.areEqual(totalDiscCount, that.totalDiscCount)
|
||||
&& Util.areEqual(genre, that.genre)
|
||||
&& Util.areEqual(compilation, that.compilation)
|
||||
&& Util.areEqual(station, that.station)
|
||||
&& Util.areEqual(mediaType, that.mediaType)
|
||||
&& Util.areEqual(supportedCommands, that.supportedCommands)
|
||||
&& Objects.equals(artworkDataType, that.artworkDataType)
|
||||
&& Objects.equals(artworkUri, that.artworkUri)
|
||||
&& Objects.equals(trackNumber, that.trackNumber)
|
||||
&& Objects.equals(totalTrackCount, that.totalTrackCount)
|
||||
&& Objects.equals(folderType, that.folderType)
|
||||
&& Objects.equals(isBrowsable, that.isBrowsable)
|
||||
&& Objects.equals(isPlayable, that.isPlayable)
|
||||
&& Objects.equals(recordingYear, that.recordingYear)
|
||||
&& Objects.equals(recordingMonth, that.recordingMonth)
|
||||
&& Objects.equals(recordingDay, that.recordingDay)
|
||||
&& Objects.equals(releaseYear, that.releaseYear)
|
||||
&& Objects.equals(releaseMonth, that.releaseMonth)
|
||||
&& Objects.equals(releaseDay, that.releaseDay)
|
||||
&& Objects.equals(writer, that.writer)
|
||||
&& Objects.equals(composer, that.composer)
|
||||
&& Objects.equals(conductor, that.conductor)
|
||||
&& Objects.equals(discNumber, that.discNumber)
|
||||
&& Objects.equals(totalDiscCount, that.totalDiscCount)
|
||||
&& Objects.equals(genre, that.genre)
|
||||
&& Objects.equals(compilation, that.compilation)
|
||||
&& Objects.equals(station, that.station)
|
||||
&& Objects.equals(mediaType, that.mediaType)
|
||||
&& Objects.equals(supportedCommands, that.supportedCommands)
|
||||
&& ((extras == null) == (that.extras == null));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Hashing deprecated fields.
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(
|
||||
return Objects.hash(
|
||||
title,
|
||||
artist,
|
||||
albumTitle,
|
||||
|
@ -15,8 +15,6 @@
|
||||
*/
|
||||
package androidx.media3.common;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
@ -26,10 +24,10 @@ import java.util.List;
|
||||
|
||||
/** A collection of metadata entries. */
|
||||
@UnstableApi
|
||||
public final class Metadata implements Parcelable {
|
||||
public final class Metadata {
|
||||
|
||||
/** A metadata entry. */
|
||||
public interface Entry extends Parcelable {
|
||||
public interface Entry {
|
||||
|
||||
/**
|
||||
* Returns the {@link Format} that can be used to decode the wrapped metadata in {@link
|
||||
@ -100,14 +98,6 @@ public final class Metadata implements Parcelable {
|
||||
this(presentationTimeUs, entries.toArray(new Entry[0]));
|
||||
}
|
||||
|
||||
/* package */ Metadata(Parcel in) {
|
||||
entries = new Metadata.Entry[in.readInt()];
|
||||
for (int i = 0; i < entries.length; i++) {
|
||||
entries[i] = in.readParcelable(Entry.class.getClassLoader());
|
||||
}
|
||||
presentationTimeUs = in.readLong();
|
||||
}
|
||||
|
||||
/** Returns the number of metadata entries. */
|
||||
public int length() {
|
||||
return entries.length;
|
||||
@ -190,33 +180,4 @@ public final class Metadata implements Parcelable {
|
||||
+ Arrays.toString(entries)
|
||||
+ (presentationTimeUs == C.TIME_UNSET ? "" : ", presentationTimeUs=" + presentationTimeUs);
|
||||
}
|
||||
|
||||
// Parcelable implementation.
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(entries.length);
|
||||
for (Entry entry : entries) {
|
||||
dest.writeParcelable(entry, 0);
|
||||
}
|
||||
dest.writeLong(presentationTimeUs);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<Metadata> CREATOR =
|
||||
new Parcelable.Creator<Metadata>() {
|
||||
@Override
|
||||
public Metadata createFromParcel(Parcel in) {
|
||||
return new Metadata(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Metadata[] newArray(int size) {
|
||||
return new Metadata[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ public final class MimeTypes {
|
||||
public static final String VIDEO_WEBM = BASE_TYPE_VIDEO + "/webm";
|
||||
public static final String VIDEO_H263 = BASE_TYPE_VIDEO + "/3gpp";
|
||||
public static final String VIDEO_H264 = BASE_TYPE_VIDEO + "/avc";
|
||||
@UnstableApi public static final String VIDEO_APV = BASE_TYPE_VIDEO + "/apv";
|
||||
public static final String VIDEO_H265 = BASE_TYPE_VIDEO + "/hevc";
|
||||
@UnstableApi public static final String VIDEO_VP8 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp8";
|
||||
@UnstableApi public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp9";
|
||||
@ -579,6 +580,36 @@ public final class MimeTypes {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given {@code codecs} and {@code supplementalCodecs} correspond to a valid
|
||||
* Dolby Vision codec.
|
||||
*
|
||||
* @param codecs An RFC 6381 codecs string for the base codec. may be null.
|
||||
* @param supplementalCodecs An optional RFC 6381 codecs string for supplemental codecs.
|
||||
* @return Whether the given {@code codecs} and {@code supplementalCodecs} correspond to a valid
|
||||
* Dolby Vision codec.
|
||||
*/
|
||||
@UnstableApi
|
||||
public static boolean isDolbyVisionCodec(
|
||||
@Nullable String codecs, @Nullable String supplementalCodecs) {
|
||||
if (codecs == null) {
|
||||
return false;
|
||||
}
|
||||
if (codecs.startsWith("dvhe") || codecs.startsWith("dvh1")) {
|
||||
// profile 5
|
||||
return true;
|
||||
}
|
||||
if (supplementalCodecs == null) {
|
||||
return false;
|
||||
}
|
||||
// profiles 8, 9 and 10
|
||||
return (supplementalCodecs.startsWith("dvhe") && codecs.startsWith("hev1"))
|
||||
|| (supplementalCodecs.startsWith("dvh1") && codecs.startsWith("hvc1"))
|
||||
|| (supplementalCodecs.startsWith("dvav") && codecs.startsWith("avc3"))
|
||||
|| (supplementalCodecs.startsWith("dva1") && codecs.startsWith("avc1"))
|
||||
|| (supplementalCodecs.startsWith("dav1") && codecs.startsWith("av01"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link C.TrackType track type} constant corresponding to a specified MIME type,
|
||||
* which may be {@link C#TRACK_TYPE_UNKNOWN} if it could not be determined.
|
||||
|
@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.media3.common;
|
||||
|
||||
import android.util.Pair;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
/**
|
||||
* Provides information of how an input texture (for example, a {@code TextureOverlay} or in {@code
|
||||
* VideoCompositor}) is presented.
|
||||
*/
|
||||
@UnstableApi
|
||||
public interface OverlaySettings {
|
||||
|
||||
/** The default alpha scale value of the overlay. */
|
||||
float DEFAULT_ALPHA_SCALE = 1f;
|
||||
|
||||
/** The default coordinates for the anchor point of the overlay within the background frame. */
|
||||
Pair<Float, Float> DEFAULT_BACKGROUND_FRAME_ANCHOR = Pair.create(0f, 0f);
|
||||
|
||||
/** The default coordinates for the anchor point of the overlay frame. */
|
||||
Pair<Float, Float> DEFAULT_OVERLAY_FRAME_ANCHOR = Pair.create(0f, 0f);
|
||||
|
||||
/** The default scaling of the overlay. */
|
||||
Pair<Float, Float> DEFAULT_SCALE = Pair.create(1f, 1f);
|
||||
|
||||
/** The default rotation of the overlay, counter-clockwise. */
|
||||
float DEFAULT_ROTATION_DEGREES = 0f;
|
||||
|
||||
/** The default luminance multiplier of an SDR overlay when overlaid on a HDR frame. */
|
||||
float DEFAULT_HDR_LUMINANCE_MULTIPLIER = 1f;
|
||||
|
||||
/**
|
||||
* Returns the alpha scale value of the overlay, altering its translucency.
|
||||
*
|
||||
* <p>An {@code alphaScale} value of {@code 1} means no change is applied. A value below {@code 1}
|
||||
* increases translucency, and a value above {@code 1} reduces translucency.
|
||||
*
|
||||
* <p>The default value is {@link #DEFAULT_ALPHA_SCALE}.
|
||||
*/
|
||||
default float getAlphaScale() {
|
||||
return DEFAULT_ALPHA_SCALE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the coordinates for the anchor point of the overlay within the background frame.
|
||||
*
|
||||
* <p>The coordinates are specified in Normalised Device Coordinates (NDCs) relative to the
|
||||
* background frame. The ranges for x and y are from {@code -1} to {@code 1}. The default value is
|
||||
* {@code (0,0)}, the center of the background frame.
|
||||
*
|
||||
* <p>The overlay's {@linkplain #getOverlayFrameAnchor anchor point} will be positioned at the
|
||||
* anchor point returned from this method. For example, a value of {@code (1,1)} will move the
|
||||
* {@linkplain #getOverlayFrameAnchor overlay's anchor} to the top right corner. That is, if the
|
||||
* overlay's anchor is at {@code (1,1)} (the top right corner), the overlay's top right corner
|
||||
* will be aligned with that of the background frame; whereas if the overlay's anchor is at {@code
|
||||
* (0,0)} (the center), the overlay's center will be positioned at the top right corner of the
|
||||
* background frame.
|
||||
*
|
||||
* <p>The default value is {@link #DEFAULT_BACKGROUND_FRAME_ANCHOR}.
|
||||
*/
|
||||
default Pair<Float, Float> getBackgroundFrameAnchor() {
|
||||
return DEFAULT_BACKGROUND_FRAME_ANCHOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the coordinates for the anchor point within the overlay.
|
||||
*
|
||||
* <p>The anchor point is the point inside the overlay that is placed on the {@linkplain
|
||||
* #getBackgroundFrameAnchor background frame anchor}
|
||||
*
|
||||
* <p>The coordinates are specified in Normalised Device Coordinates (NDCs) relative to the
|
||||
* overlay. The ranges for x and y are from {@code -1} to {@code 1}. The default value is {@code
|
||||
* (0,0)}, the center of the overlay.
|
||||
*
|
||||
* <p>See {@link #getBackgroundFrameAnchor} for examples of how to position an overlay.
|
||||
*
|
||||
* <p>The default value is {@link #DEFAULT_OVERLAY_FRAME_ANCHOR}.
|
||||
*/
|
||||
default Pair<Float, Float> getOverlayFrameAnchor() {
|
||||
return DEFAULT_OVERLAY_FRAME_ANCHOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the scaling of the overlay.
|
||||
*
|
||||
* <p>The default value is {@link #DEFAULT_SCALE}.
|
||||
*/
|
||||
default Pair<Float, Float> getScale() {
|
||||
return DEFAULT_SCALE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the rotation of the overlay, counter-clockwise.
|
||||
*
|
||||
* <p>The overlay is rotated at the center of its frame.
|
||||
*
|
||||
* <p>The default value is {@link #DEFAULT_ROTATION_DEGREES}.
|
||||
*/
|
||||
default float getRotationDegrees() {
|
||||
return DEFAULT_ROTATION_DEGREES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the luminance multiplier of an SDR overlay when overlaid on a HDR frame.
|
||||
*
|
||||
* <p>Scales the luminance of the overlay to adjust the output brightness of the overlay on the
|
||||
* frame. The default value is 1, which scales the overlay colors into the standard HDR luminance
|
||||
* within the processing pipeline. Use 0.5 to scale the luminance of the overlay to SDR range, so
|
||||
* that no extra luminance is added.
|
||||
*
|
||||
* <p>Currently only supported on text overlays
|
||||
*
|
||||
* <p>The default value is {@link #DEFAULT_HDR_LUMINANCE_MULTIPLIER}.
|
||||
*/
|
||||
default float getHdrLuminanceMultiplier() {
|
||||
return DEFAULT_HDR_LUMINANCE_MULTIPLIER;
|
||||
}
|
||||
}
|
@ -109,11 +109,11 @@ public class ParserException extends IOException {
|
||||
this.dataType = dataType;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return super.getMessage()
|
||||
+ " {contentIsMalformed="
|
||||
String superMessage = super.getMessage();
|
||||
return (superMessage != null ? superMessage + " " : "")
|
||||
+ "{contentIsMalformed="
|
||||
+ contentIsMalformed
|
||||
+ ", dataType="
|
||||
+ dataType
|
||||
|
@ -22,7 +22,7 @@ import androidx.annotation.FloatRange;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.base.Objects;
|
||||
import java.util.Objects;
|
||||
|
||||
/** A rating expressed as a percentage. */
|
||||
public final class PercentageRating extends Rating {
|
||||
@ -59,7 +59,7 @@ public final class PercentageRating extends Rating {
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(percent);
|
||||
return Objects.hash(percent);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -36,6 +36,7 @@ import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.Objects;
|
||||
|
||||
/** Thrown when a non locally recoverable playback failure occurs. */
|
||||
public class PlaybackException extends Exception {
|
||||
@ -91,6 +92,7 @@ public class PlaybackException extends Exception {
|
||||
ERROR_CODE_DECODING_FAILED,
|
||||
ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES,
|
||||
ERROR_CODE_DECODING_FORMAT_UNSUPPORTED,
|
||||
ERROR_CODE_DECODING_RESOURCES_RECLAIMED,
|
||||
ERROR_CODE_AUDIO_TRACK_INIT_FAILED,
|
||||
ERROR_CODE_AUDIO_TRACK_WRITE_FAILED,
|
||||
ERROR_CODE_AUDIO_TRACK_OFFLOAD_INIT_FAILED,
|
||||
@ -272,9 +274,8 @@ public class PlaybackException extends Exception {
|
||||
/** Caused by trying to decode content whose format is not supported. */
|
||||
public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 4005;
|
||||
|
||||
// TODO: b/322943860 - Stabilize error code and add to IntDef
|
||||
/** Caused by higher priority task reclaiming resources needed for decoding. */
|
||||
@UnstableApi public static final int ERROR_CODE_DECODING_RESOURCES_RECLAIMED = 4006;
|
||||
public static final int ERROR_CODE_DECODING_RESOURCES_RECLAIMED = 4006;
|
||||
|
||||
// AudioTrack errors (5xxx).
|
||||
|
||||
@ -553,17 +554,17 @@ public class PlaybackException extends Exception {
|
||||
@Nullable Throwable thisCause = getCause();
|
||||
@Nullable Throwable thatCause = other.getCause();
|
||||
if (thisCause != null && thatCause != null) {
|
||||
if (!Util.areEqual(thisCause.getMessage(), thatCause.getMessage())) {
|
||||
if (!Objects.equals(thisCause.getMessage(), thatCause.getMessage())) {
|
||||
return false;
|
||||
}
|
||||
if (!Util.areEqual(thisCause.getClass(), thatCause.getClass())) {
|
||||
if (!Objects.equals(thisCause.getClass(), thatCause.getClass())) {
|
||||
return false;
|
||||
}
|
||||
} else if (thisCause != null || thatCause != null) {
|
||||
return false;
|
||||
}
|
||||
return errorCode == other.errorCode
|
||||
&& Util.areEqual(getMessage(), other.getMessage())
|
||||
&& Objects.equals(getMessage(), other.getMessage())
|
||||
&& timestampMs == other.timestampMs;
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,6 @@ import androidx.media3.common.text.CueGroup;
|
||||
import androidx.media3.common.util.Size;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
@ -45,6 +44,7 @@ import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A media player interface defining high-level functionality, such as the ability to play, pause,
|
||||
@ -352,13 +352,13 @@ public interface Player {
|
||||
}
|
||||
PositionInfo that = (PositionInfo) o;
|
||||
return equalsForBundling(that)
|
||||
&& Objects.equal(windowUid, that.windowUid)
|
||||
&& Objects.equal(periodUid, that.periodUid);
|
||||
&& Objects.equals(windowUid, that.windowUid)
|
||||
&& Objects.equals(periodUid, that.periodUid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(
|
||||
return Objects.hash(
|
||||
windowUid,
|
||||
mediaItemIndex,
|
||||
mediaItem,
|
||||
@ -382,7 +382,7 @@ public interface Player {
|
||||
&& contentPositionMs == other.contentPositionMs
|
||||
&& adGroupIndex == other.adGroupIndex
|
||||
&& adIndexInAdGroup == other.adIndexInAdGroup
|
||||
&& Objects.equal(mediaItem, other.mediaItem);
|
||||
&& Objects.equals(mediaItem, other.mediaItem);
|
||||
}
|
||||
|
||||
@VisibleForTesting static final String FIELD_MEDIA_ITEM_INDEX = Util.intToStringMaxRadix(0);
|
||||
@ -2855,7 +2855,6 @@ public interface Player {
|
||||
*/
|
||||
TrackSelectionParameters getTrackSelectionParameters();
|
||||
|
||||
// LINT.IfChange(set_track_selection_parameters)
|
||||
/**
|
||||
* Sets the parameters constraining the track selection.
|
||||
*
|
||||
@ -3384,8 +3383,7 @@ public interface Player {
|
||||
*
|
||||
* <p>For devices with {@link DeviceInfo#PLAYBACK_TYPE_LOCAL local playback}, the volume returned
|
||||
* by this method varies according to the current {@link C.StreamType stream type}. The stream
|
||||
* type is determined by {@link AudioAttributes#usage} which can be converted to stream type with
|
||||
* {@link Util#getStreamTypeForAudioUsage(int)}.
|
||||
* type is determined by {@link AudioAttributes#getStreamType()}.
|
||||
*
|
||||
* <p>For devices with {@link DeviceInfo#PLAYBACK_TYPE_REMOTE remote playback}, the volume of the
|
||||
* remote device is returned.
|
||||
@ -3508,10 +3506,6 @@ public interface Player {
|
||||
* <p>If tunneling is enabled by the track selector, the specified audio attributes will be
|
||||
* ignored, but they will take effect if audio is later played without tunneling.
|
||||
*
|
||||
* <p>If the device is running a build before platform API version 21, audio attributes cannot be
|
||||
* set directly on the underlying audio track. In this case, the usage will be mapped onto an
|
||||
* equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}.
|
||||
*
|
||||
* <p>If audio focus should be handled, the {@link AudioAttributes#usage} must be {@link
|
||||
* C#USAGE_MEDIA} or {@link C#USAGE_GAME}. Other usages will throw an {@link
|
||||
* IllegalArgumentException}.
|
||||
|
@ -35,6 +35,7 @@ public interface PreviewingVideoGraph extends VideoGraph {
|
||||
* @param debugViewProvider A {@link DebugViewProvider}.
|
||||
* @param listener A {@link Listener}.
|
||||
* @param listenerExecutor The {@link Executor} on which the {@code listener} is invoked.
|
||||
* @param videoCompositorSettings The {@link VideoCompositorSettings}.
|
||||
* @param compositionEffects A list of {@linkplain Effect effects} to apply to the composition.
|
||||
* @param initialTimestampOffsetUs The timestamp offset for the first frame, in microseconds.
|
||||
* @return A new instance.
|
||||
@ -47,9 +48,16 @@ public interface PreviewingVideoGraph extends VideoGraph {
|
||||
DebugViewProvider debugViewProvider,
|
||||
Listener listener,
|
||||
Executor listenerExecutor,
|
||||
VideoCompositorSettings videoCompositorSettings,
|
||||
List<Effect> compositionEffects,
|
||||
long initialTimestampOffsetUs)
|
||||
throws VideoFrameProcessingException;
|
||||
|
||||
/**
|
||||
* Returns whether the {@link VideoGraph} implementation supports {@linkplain #registerInput
|
||||
* registering} multiple inputs.
|
||||
*/
|
||||
boolean supportsMultipleInputs();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -15,7 +15,6 @@
|
||||
*/
|
||||
package androidx.media3.common;
|
||||
|
||||
import static androidx.annotation.VisibleForTesting.PROTECTED;
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.Util.castNonNull;
|
||||
@ -35,7 +34,6 @@ import android.view.TextureView;
|
||||
import androidx.annotation.FloatRange;
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.media3.common.text.CueGroup;
|
||||
import androidx.media3.common.util.Clock;
|
||||
import androidx.media3.common.util.HandlerWrapper;
|
||||
@ -98,7 +96,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
|
||||
/** An immutable state description of the player. */
|
||||
protected static final class State {
|
||||
public static final class State {
|
||||
|
||||
/** A builder for {@link State} objects. */
|
||||
public static final class Builder {
|
||||
@ -161,7 +159,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
seekForwardIncrementMs = C.DEFAULT_SEEK_FORWARD_INCREMENT_MS;
|
||||
maxSeekToPreviousPositionMs = C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS;
|
||||
playbackParameters = PlaybackParameters.DEFAULT;
|
||||
trackSelectionParameters = TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT;
|
||||
trackSelectionParameters = TrackSelectionParameters.DEFAULT;
|
||||
audioAttributes = AudioAttributes.DEFAULT;
|
||||
volume = 1f;
|
||||
videoSize = VideoSize.UNKNOWN;
|
||||
@ -222,7 +220,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
this.playlist = ((PlaylistTimeline) state.timeline).playlist;
|
||||
} else {
|
||||
this.currentTracks = state.currentTracks;
|
||||
this.currentMetadata = state.currentMetadata;
|
||||
this.currentMetadata = state.usesDerivedMediaMetadata ? null : state.currentMetadata;
|
||||
}
|
||||
this.playlistMetadata = state.playlistMetadata;
|
||||
this.currentMediaItemIndex = state.currentMediaItemIndex;
|
||||
@ -958,9 +956,12 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
*/
|
||||
public final long discontinuityPositionMs;
|
||||
|
||||
private final boolean usesDerivedMediaMetadata;
|
||||
|
||||
private State(Builder builder) {
|
||||
Tracks currentTracks = builder.currentTracks;
|
||||
MediaMetadata currentMetadata = builder.currentMetadata;
|
||||
boolean usesDerivedMediaMetadata = false;
|
||||
if (builder.timeline.isEmpty()) {
|
||||
checkArgument(
|
||||
builder.playbackState == Player.STATE_IDLE
|
||||
@ -1016,6 +1017,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
getCombinedMediaMetadata(
|
||||
builder.timeline.getWindow(mediaItemIndex, new Timeline.Window()).mediaItem,
|
||||
checkNotNull(currentTracks));
|
||||
usesDerivedMediaMetadata = true;
|
||||
}
|
||||
}
|
||||
if (builder.playerError != null) {
|
||||
@ -1092,6 +1094,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
this.hasPositionDiscontinuity = builder.hasPositionDiscontinuity;
|
||||
this.positionDiscontinuityReason = builder.positionDiscontinuityReason;
|
||||
this.discontinuityPositionMs = builder.discontinuityPositionMs;
|
||||
this.usesDerivedMediaMetadata = usesDerivedMediaMetadata;
|
||||
}
|
||||
|
||||
/** Returns a {@link Builder} pre-populated with the current state values. */
|
||||
@ -1790,9 +1793,9 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
return this.uid.equals(mediaItemData.uid)
|
||||
&& this.tracks.equals(mediaItemData.tracks)
|
||||
&& this.mediaItem.equals(mediaItemData.mediaItem)
|
||||
&& Util.areEqual(this.mediaMetadata, mediaItemData.mediaMetadata)
|
||||
&& Util.areEqual(this.manifest, mediaItemData.manifest)
|
||||
&& Util.areEqual(this.liveConfiguration, mediaItemData.liveConfiguration)
|
||||
&& Objects.equals(this.mediaMetadata, mediaItemData.mediaMetadata)
|
||||
&& Objects.equals(this.manifest, mediaItemData.manifest)
|
||||
&& Objects.equals(this.liveConfiguration, mediaItemData.liveConfiguration)
|
||||
&& this.presentationStartTimeMs == mediaItemData.presentationStartTimeMs
|
||||
&& this.windowStartTimeMs == mediaItemData.windowStartTimeMs
|
||||
&& this.elapsedRealtimeEpochOffsetMs == mediaItemData.elapsedRealtimeEpochOffsetMs
|
||||
@ -2069,7 +2072,21 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
}
|
||||
}
|
||||
|
||||
/** A supplier for a position. */
|
||||
/**
|
||||
* A supplier for a position.
|
||||
*
|
||||
* <p>Convenience methods and classes for creating position suppliers:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Use {@link #getConstant} for constant or non-moving positions.
|
||||
* <li>Use {@link #getExtrapolating} for positions advancing with the system clock from a
|
||||
* provided start time.
|
||||
* <li>Use {@link LivePositionSupplier} for positions that can be directly obtained from a live
|
||||
* system. Note that these suppliers should be {@linkplain LivePositionSupplier#disconnect
|
||||
* disconnected} from the live source as soon as the position is no longer valid, for
|
||||
* example after a position discontinuity.
|
||||
* </ul>
|
||||
*/
|
||||
protected interface PositionSupplier {
|
||||
|
||||
/** An instance returning a constant position of zero. */
|
||||
@ -2102,6 +2119,48 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
long get();
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link PositionSupplier} connected to a live provider that returns a new value on each
|
||||
* invocation until it is {@linkplain #disconnect disconnected} from the live source.
|
||||
*
|
||||
* <p>The recommended usage of this class is to create a new instance connected to the live source
|
||||
* and keep returning this instance as long as the position source is still valid. As soon as the
|
||||
* position source becomes invalid, for example when handling a position discontinuity, call
|
||||
* {@link #disconnect} with the final position that will be returned for all future invocations.
|
||||
*/
|
||||
protected static final class LivePositionSupplier implements PositionSupplier {
|
||||
|
||||
private final PositionSupplier livePosition;
|
||||
|
||||
private long finalValue;
|
||||
|
||||
/**
|
||||
* Creates the live position supplier.
|
||||
*
|
||||
* @param livePosition The function returning the live position.
|
||||
*/
|
||||
public LivePositionSupplier(PositionSupplier livePosition) {
|
||||
this.livePosition = livePosition;
|
||||
this.finalValue = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the position supplier from the live source.
|
||||
*
|
||||
* <p>All future invocations of {@link #get()} will return the provided final position.
|
||||
*
|
||||
* @param finalValue The final position value.
|
||||
*/
|
||||
public void disconnect(long finalValue) {
|
||||
this.finalValue = finalValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long get() {
|
||||
return finalValue != C.TIME_UNSET ? finalValue : livePosition.get();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Position difference threshold below which we do not automatically report a position
|
||||
* discontinuity, in milliseconds.
|
||||
@ -2450,8 +2509,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
}
|
||||
|
||||
@Override
|
||||
@VisibleForTesting(otherwise = PROTECTED)
|
||||
public final void seekTo(
|
||||
protected final void seekTo(
|
||||
int mediaItemIndex,
|
||||
long positionMs,
|
||||
@Player.Command int seekCommand,
|
||||
@ -3402,7 +3460,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
* index is in the range {@code fromIndex} < {@code toIndex} <= {@link
|
||||
* #getMediaItemCount()}.
|
||||
* @param newIndex The new index of the first moved item. The index is in the range {@code 0}
|
||||
* <= {@code newIndex} < {@link #getMediaItemCount() - (toIndex - fromIndex)}.
|
||||
* <= {@code newIndex} <= {@link #getMediaItemCount() - (toIndex - fromIndex)}.
|
||||
* @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
|
||||
* changes caused by this call.
|
||||
*/
|
||||
@ -3417,9 +3475,9 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
* <p>Will only be called if {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} is available.
|
||||
*
|
||||
* @param fromIndex The start index of the items to replace. The index is in the range 0 <=
|
||||
* {@code fromIndex} < {@link #getMediaItemCount()}.
|
||||
* {@code fromIndex} <= {@link #getMediaItemCount()}.
|
||||
* @param toIndex The index of the first item not to be replaced (exclusive). The index is in the
|
||||
* range {@code fromIndex} < {@code toIndex} <= {@link #getMediaItemCount()}.
|
||||
* range {@code fromIndex} <= {@code toIndex} <= {@link #getMediaItemCount()}.
|
||||
* @param mediaItems The media items to replace the specified range with.
|
||||
* @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
|
||||
* changes caused by this call.
|
||||
@ -3428,6 +3486,9 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
protected ListenableFuture<?> handleReplaceMediaItems(
|
||||
int fromIndex, int toIndex, List<MediaItem> mediaItems) {
|
||||
ListenableFuture<?> addFuture = handleAddMediaItems(toIndex, mediaItems);
|
||||
if (fromIndex == toIndex) {
|
||||
return addFuture;
|
||||
}
|
||||
ListenableFuture<?> removeFuture = handleRemoveMediaItems(fromIndex, toIndex);
|
||||
return Util.transformFutureAsync(addFuture, unused -> removeFuture);
|
||||
}
|
||||
@ -3561,7 +3622,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
Player.EVENT_MEDIA_ITEM_TRANSITION,
|
||||
listener -> listener.onMediaItemTransition(mediaItem, mediaItemTransitionReason));
|
||||
}
|
||||
if (!Util.areEqual(previousState.playerError, newState.playerError)) {
|
||||
if (!Objects.equals(previousState.playerError, newState.playerError)) {
|
||||
listeners.queueEvent(
|
||||
Player.EVENT_PLAYER_ERROR,
|
||||
listener -> listener.onPlayerErrorChanged(newState.playerError));
|
||||
@ -4008,14 +4069,10 @@ public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
}
|
||||
// Only mark changes within the current item as a transition if we are repeating automatically
|
||||
// or via a seek to next/previous.
|
||||
if (positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION) {
|
||||
if ((getContentPositionMsInternal(previousState, window)
|
||||
> getContentPositionMsInternal(newState, window))
|
||||
|| (newState.hasPositionDiscontinuity
|
||||
&& newState.discontinuityPositionMs == C.TIME_UNSET
|
||||
&& isRepeatingCurrentItem)) {
|
||||
return MEDIA_ITEM_TRANSITION_REASON_REPEAT;
|
||||
}
|
||||
if (positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION
|
||||
&& getContentPositionMsInternal(previousState, window)
|
||||
> getContentPositionMsInternal(newState, window)) {
|
||||
return MEDIA_ITEM_TRANSITION_REASON_REPEAT;
|
||||
}
|
||||
if (positionDiscontinuityReason == DISCONTINUITY_REASON_SEEK && isRepeatingCurrentItem) {
|
||||
return MEDIA_ITEM_TRANSITION_REASON_SEEK;
|
||||
|
@ -23,7 +23,7 @@ import androidx.annotation.IntRange;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.base.Objects;
|
||||
import java.util.Objects;
|
||||
|
||||
/** A rating expressed as a fractional number of stars. */
|
||||
public final class StarRating extends Rating {
|
||||
@ -84,7 +84,7 @@ public final class StarRating extends Rating {
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(maxStars, starRating);
|
||||
return Objects.hash(maxStars, starRating);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -21,7 +21,7 @@ import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.base.Objects;
|
||||
import java.util.Objects;
|
||||
|
||||
/** A rating expressed as "thumbs up" or "thumbs down". */
|
||||
public final class ThumbRating extends Rating {
|
||||
@ -57,7 +57,7 @@ public final class ThumbRating extends Rating {
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(rated, isThumbsUp);
|
||||
return Objects.hash(rated, isThumbsUp);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -36,6 +36,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import com.google.errorprone.annotations.InlineMe;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
/**
|
||||
@ -371,10 +372,10 @@ public abstract class Timeline {
|
||||
return false;
|
||||
}
|
||||
Window that = (Window) obj;
|
||||
return Util.areEqual(uid, that.uid)
|
||||
&& Util.areEqual(mediaItem, that.mediaItem)
|
||||
&& Util.areEqual(manifest, that.manifest)
|
||||
&& Util.areEqual(liveConfiguration, that.liveConfiguration)
|
||||
return Objects.equals(uid, that.uid)
|
||||
&& Objects.equals(mediaItem, that.mediaItem)
|
||||
&& Objects.equals(manifest, that.manifest)
|
||||
&& Objects.equals(liveConfiguration, that.liveConfiguration)
|
||||
&& presentationStartTimeMs == that.presentationStartTimeMs
|
||||
&& windowStartTimeMs == that.windowStartTimeMs
|
||||
&& elapsedRealtimeEpochOffsetMs == that.elapsedRealtimeEpochOffsetMs
|
||||
@ -871,13 +872,13 @@ public abstract class Timeline {
|
||||
return false;
|
||||
}
|
||||
Period that = (Period) obj;
|
||||
return Util.areEqual(id, that.id)
|
||||
&& Util.areEqual(uid, that.uid)
|
||||
return Objects.equals(id, that.id)
|
||||
&& Objects.equals(uid, that.uid)
|
||||
&& windowIndex == that.windowIndex
|
||||
&& durationUs == that.durationUs
|
||||
&& positionInWindowUs == that.positionInWindowUs
|
||||
&& isPlaceholder == that.isPlaceholder
|
||||
&& Util.areEqual(adPlaybackState, that.adPlaybackState);
|
||||
&& Objects.equals(adPlaybackState, that.adPlaybackState);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -161,6 +161,11 @@ public final class TrackGroup {
|
||||
return id.equals(other.id) && Arrays.equals(formats, other.formats);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return id + ": " + Arrays.toString(formats);
|
||||
}
|
||||
|
||||
private static final String FIELD_FORMATS = Util.intToStringMaxRadix(0);
|
||||
private static final String FIELD_ID = Util.intToStringMaxRadix(1);
|
||||
|
||||
@ -169,7 +174,7 @@ public final class TrackGroup {
|
||||
Bundle bundle = new Bundle();
|
||||
ArrayList<Bundle> arrayList = new ArrayList<>(formats.length);
|
||||
for (Format format : formats) {
|
||||
arrayList.add(format.toBundle(/* excludeMetadata= */ true));
|
||||
arrayList.add(format.toBundle());
|
||||
}
|
||||
bundle.putParcelableArrayList(FIELD_FORMATS, arrayList);
|
||||
bundle.putString(FIELD_ID, id);
|
||||
|
@ -22,9 +22,7 @@ import static java.lang.annotation.ElementType.TYPE_USE;
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Point;
|
||||
import android.os.Bundle;
|
||||
import android.os.Looper;
|
||||
import android.view.accessibility.CaptioningManager;
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.IntDef;
|
||||
@ -37,6 +35,7 @@ import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import com.google.errorprone.annotations.InlineMe;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
@ -44,12 +43,10 @@ import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import org.checkerframework.checker.initialization.qual.UnknownInitialization;
|
||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||
|
||||
// LINT.IfChange(javadoc)
|
||||
/**
|
||||
* Parameters for controlling track selection.
|
||||
*
|
||||
@ -88,8 +85,10 @@ public class TrackSelectionParameters {
|
||||
private int minVideoBitrate;
|
||||
private int viewportWidth;
|
||||
private int viewportHeight;
|
||||
private boolean isViewportSizeLimitedByPhysicalDisplaySize;
|
||||
private boolean viewportOrientationMayChange;
|
||||
private ImmutableList<String> preferredVideoMimeTypes;
|
||||
private ImmutableList<String> preferredVideoLanguages;
|
||||
private @C.RoleFlags int preferredVideoRoleFlags;
|
||||
// Audio
|
||||
private ImmutableList<String> preferredAudioLanguages;
|
||||
@ -101,6 +100,7 @@ public class TrackSelectionParameters {
|
||||
// Text
|
||||
private ImmutableList<String> preferredTextLanguages;
|
||||
private @C.RoleFlags int preferredTextRoleFlags;
|
||||
private boolean usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager;
|
||||
private @C.SelectionFlags int ignoredTextSelectionFlags;
|
||||
private boolean selectUndeterminedTextLanguage;
|
||||
// Image
|
||||
@ -111,12 +111,7 @@ public class TrackSelectionParameters {
|
||||
private HashMap<TrackGroup, TrackSelectionOverride> overrides;
|
||||
private HashSet<@C.TrackType Integer> disabledTrackTypes;
|
||||
|
||||
/**
|
||||
* @deprecated {@link Context} constraints will not be set using this constructor. Use {@link
|
||||
* #Builder(Context)} instead.
|
||||
*/
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
/** Creates a builder with default initial values. */
|
||||
public Builder() {
|
||||
// Video
|
||||
maxVideoWidth = Integer.MAX_VALUE;
|
||||
@ -125,8 +120,10 @@ public class TrackSelectionParameters {
|
||||
maxVideoBitrate = Integer.MAX_VALUE;
|
||||
viewportWidth = Integer.MAX_VALUE;
|
||||
viewportHeight = Integer.MAX_VALUE;
|
||||
isViewportSizeLimitedByPhysicalDisplaySize = true;
|
||||
viewportOrientationMayChange = true;
|
||||
preferredVideoMimeTypes = ImmutableList.of();
|
||||
preferredVideoLanguages = ImmutableList.of();
|
||||
preferredVideoRoleFlags = 0;
|
||||
// Audio
|
||||
preferredAudioLanguages = ImmutableList.of();
|
||||
@ -138,6 +135,7 @@ public class TrackSelectionParameters {
|
||||
// Text
|
||||
preferredTextLanguages = ImmutableList.of();
|
||||
preferredTextRoleFlags = 0;
|
||||
usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager = true;
|
||||
ignoredTextSelectionFlags = 0;
|
||||
selectUndeterminedTextLanguage = false;
|
||||
// Image
|
||||
@ -150,15 +148,12 @@ public class TrackSelectionParameters {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder with default initial values.
|
||||
*
|
||||
* @param context Any context.
|
||||
* @deprecated Use {@link #Builder()} instead.
|
||||
*/
|
||||
@SuppressWarnings({"deprecation", "method.invocation"}) // Methods invoked are setter only.
|
||||
@Deprecated
|
||||
@InlineMe(replacement = "this()")
|
||||
public Builder(Context context) {
|
||||
this();
|
||||
setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(context);
|
||||
setViewportSizeToPhysicalDisplaySize(context, /* viewportOrientationMayChange= */ true);
|
||||
}
|
||||
|
||||
/** Creates a builder with the initial values specified in {@code initialValues}. */
|
||||
@ -171,44 +166,42 @@ public class TrackSelectionParameters {
|
||||
@UnstableApi
|
||||
protected Builder(Bundle bundle) {
|
||||
// Video
|
||||
maxVideoWidth = bundle.getInt(FIELD_MAX_VIDEO_WIDTH, DEFAULT_WITHOUT_CONTEXT.maxVideoWidth);
|
||||
maxVideoHeight =
|
||||
bundle.getInt(FIELD_MAX_VIDEO_HEIGHT, DEFAULT_WITHOUT_CONTEXT.maxVideoHeight);
|
||||
maxVideoFrameRate =
|
||||
bundle.getInt(FIELD_MAX_VIDEO_FRAMERATE, DEFAULT_WITHOUT_CONTEXT.maxVideoFrameRate);
|
||||
maxVideoBitrate =
|
||||
bundle.getInt(FIELD_MAX_VIDEO_BITRATE, DEFAULT_WITHOUT_CONTEXT.maxVideoBitrate);
|
||||
minVideoWidth = bundle.getInt(FIELD_MIN_VIDEO_WIDTH, DEFAULT_WITHOUT_CONTEXT.minVideoWidth);
|
||||
minVideoHeight =
|
||||
bundle.getInt(FIELD_MIN_VIDEO_HEIGHT, DEFAULT_WITHOUT_CONTEXT.minVideoHeight);
|
||||
minVideoFrameRate =
|
||||
bundle.getInt(FIELD_MIN_VIDEO_FRAMERATE, DEFAULT_WITHOUT_CONTEXT.minVideoFrameRate);
|
||||
minVideoBitrate =
|
||||
bundle.getInt(FIELD_MIN_VIDEO_BITRATE, DEFAULT_WITHOUT_CONTEXT.minVideoBitrate);
|
||||
viewportWidth = bundle.getInt(FIELD_VIEWPORT_WIDTH, DEFAULT_WITHOUT_CONTEXT.viewportWidth);
|
||||
viewportHeight = bundle.getInt(FIELD_VIEWPORT_HEIGHT, DEFAULT_WITHOUT_CONTEXT.viewportHeight);
|
||||
maxVideoWidth = bundle.getInt(FIELD_MAX_VIDEO_WIDTH, DEFAULT.maxVideoWidth);
|
||||
maxVideoHeight = bundle.getInt(FIELD_MAX_VIDEO_HEIGHT, DEFAULT.maxVideoHeight);
|
||||
maxVideoFrameRate = bundle.getInt(FIELD_MAX_VIDEO_FRAMERATE, DEFAULT.maxVideoFrameRate);
|
||||
maxVideoBitrate = bundle.getInt(FIELD_MAX_VIDEO_BITRATE, DEFAULT.maxVideoBitrate);
|
||||
minVideoWidth = bundle.getInt(FIELD_MIN_VIDEO_WIDTH, DEFAULT.minVideoWidth);
|
||||
minVideoHeight = bundle.getInt(FIELD_MIN_VIDEO_HEIGHT, DEFAULT.minVideoHeight);
|
||||
minVideoFrameRate = bundle.getInt(FIELD_MIN_VIDEO_FRAMERATE, DEFAULT.minVideoFrameRate);
|
||||
minVideoBitrate = bundle.getInt(FIELD_MIN_VIDEO_BITRATE, DEFAULT.minVideoBitrate);
|
||||
viewportWidth = bundle.getInt(FIELD_VIEWPORT_WIDTH, DEFAULT.viewportWidth);
|
||||
viewportHeight = bundle.getInt(FIELD_VIEWPORT_HEIGHT, DEFAULT.viewportHeight);
|
||||
isViewportSizeLimitedByPhysicalDisplaySize =
|
||||
viewportWidth == Integer.MAX_VALUE
|
||||
&& viewportHeight == Integer.MAX_VALUE
|
||||
&& bundle.getBoolean(
|
||||
FIELD_IS_VIEWPORT_SIZE_LIMITED_BY_PHYSICAL_DISPLAY_SIZE,
|
||||
DEFAULT.isViewportSizeLimitedByPhysicalDisplaySize);
|
||||
viewportOrientationMayChange =
|
||||
bundle.getBoolean(
|
||||
FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE,
|
||||
DEFAULT_WITHOUT_CONTEXT.viewportOrientationMayChange);
|
||||
FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE, DEFAULT.viewportOrientationMayChange);
|
||||
preferredVideoMimeTypes =
|
||||
ImmutableList.copyOf(
|
||||
firstNonNull(bundle.getStringArray(FIELD_PREFERRED_VIDEO_MIMETYPES), new String[0]));
|
||||
preferredVideoLanguages =
|
||||
ImmutableList.copyOf(
|
||||
firstNonNull(bundle.getStringArray(FIELD_PREFERRED_VIDEO_LANGUAGES), new String[0]));
|
||||
preferredVideoRoleFlags =
|
||||
bundle.getInt(
|
||||
FIELD_PREFERRED_VIDEO_ROLE_FLAGS, DEFAULT_WITHOUT_CONTEXT.preferredVideoRoleFlags);
|
||||
bundle.getInt(FIELD_PREFERRED_VIDEO_ROLE_FLAGS, DEFAULT.preferredVideoRoleFlags);
|
||||
// Audio
|
||||
String[] preferredAudioLanguages1 =
|
||||
firstNonNull(bundle.getStringArray(FIELD_PREFERRED_AUDIO_LANGUAGES), new String[0]);
|
||||
preferredAudioLanguages = normalizeLanguageCodes(preferredAudioLanguages1);
|
||||
preferredAudioRoleFlags =
|
||||
bundle.getInt(
|
||||
FIELD_PREFERRED_AUDIO_ROLE_FLAGS, DEFAULT_WITHOUT_CONTEXT.preferredAudioRoleFlags);
|
||||
bundle.getInt(FIELD_PREFERRED_AUDIO_ROLE_FLAGS, DEFAULT.preferredAudioRoleFlags);
|
||||
maxAudioChannelCount =
|
||||
bundle.getInt(
|
||||
FIELD_MAX_AUDIO_CHANNEL_COUNT, DEFAULT_WITHOUT_CONTEXT.maxAudioChannelCount);
|
||||
maxAudioBitrate =
|
||||
bundle.getInt(FIELD_MAX_AUDIO_BITRATE, DEFAULT_WITHOUT_CONTEXT.maxAudioBitrate);
|
||||
bundle.getInt(FIELD_MAX_AUDIO_CHANNEL_COUNT, DEFAULT.maxAudioChannelCount);
|
||||
maxAudioBitrate = bundle.getInt(FIELD_MAX_AUDIO_BITRATE, DEFAULT.maxAudioBitrate);
|
||||
preferredAudioMimeTypes =
|
||||
ImmutableList.copyOf(
|
||||
firstNonNull(bundle.getStringArray(FIELD_PREFERRED_AUDIO_MIME_TYPES), new String[0]));
|
||||
@ -218,29 +211,29 @@ public class TrackSelectionParameters {
|
||||
normalizeLanguageCodes(
|
||||
firstNonNull(bundle.getStringArray(FIELD_PREFERRED_TEXT_LANGUAGES), new String[0]));
|
||||
preferredTextRoleFlags =
|
||||
bundle.getInt(
|
||||
FIELD_PREFERRED_TEXT_ROLE_FLAGS, DEFAULT_WITHOUT_CONTEXT.preferredTextRoleFlags);
|
||||
bundle.getInt(FIELD_PREFERRED_TEXT_ROLE_FLAGS, DEFAULT.preferredTextRoleFlags);
|
||||
usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager =
|
||||
preferredTextLanguages.isEmpty()
|
||||
&& preferredTextRoleFlags == 0
|
||||
&& bundle.getBoolean(
|
||||
FIELD_USE_PREFERRED_TEXT_LANGUAGES_AND_ROLE_FLAGS_FROM_CAPTIONING_MANAGER,
|
||||
DEFAULT.usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager);
|
||||
ignoredTextSelectionFlags =
|
||||
bundle.getInt(
|
||||
FIELD_IGNORED_TEXT_SELECTION_FLAGS,
|
||||
DEFAULT_WITHOUT_CONTEXT.ignoredTextSelectionFlags);
|
||||
bundle.getInt(FIELD_IGNORED_TEXT_SELECTION_FLAGS, DEFAULT.ignoredTextSelectionFlags);
|
||||
selectUndeterminedTextLanguage =
|
||||
bundle.getBoolean(
|
||||
FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE,
|
||||
DEFAULT_WITHOUT_CONTEXT.selectUndeterminedTextLanguage);
|
||||
FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE, DEFAULT.selectUndeterminedTextLanguage);
|
||||
// Image
|
||||
isPrioritizeImageOverVideoEnabled =
|
||||
bundle.getBoolean(
|
||||
FIELD_IS_PREFER_IMAGE_OVER_VIDEO_ENABLED,
|
||||
DEFAULT_WITHOUT_CONTEXT.isPrioritizeImageOverVideoEnabled);
|
||||
FIELD_IS_PREFER_IMAGE_OVER_VIDEO_ENABLED, DEFAULT.isPrioritizeImageOverVideoEnabled);
|
||||
|
||||
// General
|
||||
forceLowestBitrate =
|
||||
bundle.getBoolean(FIELD_FORCE_LOWEST_BITRATE, DEFAULT_WITHOUT_CONTEXT.forceLowestBitrate);
|
||||
bundle.getBoolean(FIELD_FORCE_LOWEST_BITRATE, DEFAULT.forceLowestBitrate);
|
||||
forceHighestSupportedBitrate =
|
||||
bundle.getBoolean(
|
||||
FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE,
|
||||
DEFAULT_WITHOUT_CONTEXT.forceHighestSupportedBitrate);
|
||||
FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE, DEFAULT.forceHighestSupportedBitrate);
|
||||
@Nullable
|
||||
List<Bundle> overrideBundleList = bundle.getParcelableArrayList(FIELD_SELECTION_OVERRIDES);
|
||||
List<TrackSelectionOverride> overrideList =
|
||||
@ -284,6 +277,7 @@ public class TrackSelectionParameters {
|
||||
/** Overrides the value of the builder with the value of {@link TrackSelectionParameters}. */
|
||||
@EnsuresNonNull({
|
||||
"preferredVideoMimeTypes",
|
||||
"preferredVideoLanguages",
|
||||
"preferredAudioLanguages",
|
||||
"preferredAudioMimeTypes",
|
||||
"audioOffloadPreferences",
|
||||
@ -303,8 +297,11 @@ public class TrackSelectionParameters {
|
||||
minVideoBitrate = parameters.minVideoBitrate;
|
||||
viewportWidth = parameters.viewportWidth;
|
||||
viewportHeight = parameters.viewportHeight;
|
||||
isViewportSizeLimitedByPhysicalDisplaySize =
|
||||
parameters.isViewportSizeLimitedByPhysicalDisplaySize;
|
||||
viewportOrientationMayChange = parameters.viewportOrientationMayChange;
|
||||
preferredVideoMimeTypes = parameters.preferredVideoMimeTypes;
|
||||
preferredVideoLanguages = parameters.preferredVideoLanguages;
|
||||
preferredVideoRoleFlags = parameters.preferredVideoRoleFlags;
|
||||
// Audio
|
||||
preferredAudioLanguages = parameters.preferredAudioLanguages;
|
||||
@ -316,6 +313,8 @@ public class TrackSelectionParameters {
|
||||
// Text
|
||||
preferredTextLanguages = parameters.preferredTextLanguages;
|
||||
preferredTextRoleFlags = parameters.preferredTextRoleFlags;
|
||||
usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager =
|
||||
parameters.usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager;
|
||||
ignoredTextSelectionFlags = parameters.ignoredTextSelectionFlags;
|
||||
selectUndeterminedTextLanguage = parameters.selectUndeterminedTextLanguage;
|
||||
// Image
|
||||
@ -434,20 +433,31 @@ public class TrackSelectionParameters {
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to calling {@link #setViewportSize(int, int, boolean)} with the viewport size
|
||||
* obtained from {@link Util#getCurrentDisplayModeSize(Context)}.
|
||||
* Sets whether the viewport size should be assumed to the physical display size if no other
|
||||
* specific viewport size constraint is specified. Constrains video track selections for
|
||||
* adaptive content so that only tracks suitable for the viewport are selected.
|
||||
*
|
||||
* @param context Any context.
|
||||
* @param viewportOrientationMayChange Whether the viewport orientation may change during
|
||||
* playback.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setViewportSizeToPhysicalDisplaySize(boolean viewportOrientationMayChange) {
|
||||
this.isViewportSizeLimitedByPhysicalDisplaySize = true;
|
||||
this.viewportOrientationMayChange = viewportOrientationMayChange;
|
||||
this.viewportHeight = Integer.MAX_VALUE;
|
||||
this.viewportWidth = Integer.MAX_VALUE;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #setViewportSizeToPhysicalDisplaySize(boolean)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setViewportSizeToPhysicalDisplaySize(
|
||||
Context context, boolean viewportOrientationMayChange) {
|
||||
// Assume the viewport is fullscreen.
|
||||
Point viewportSize = Util.getCurrentDisplayModeSize(context);
|
||||
return setViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange);
|
||||
return setViewportSizeToPhysicalDisplaySize(viewportOrientationMayChange);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -477,6 +487,7 @@ public class TrackSelectionParameters {
|
||||
this.viewportWidth = viewportWidth;
|
||||
this.viewportHeight = viewportHeight;
|
||||
this.viewportOrientationMayChange = viewportOrientationMayChange;
|
||||
this.isViewportSizeLimitedByPhysicalDisplaySize = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -504,6 +515,36 @@ public class TrackSelectionParameters {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the preferred language for video tracks.
|
||||
*
|
||||
* @param preferredVideoLanguage Preferred video language as an IETF BCP 47 conformant tag, or
|
||||
* {@code null} to express no language preference for video track selection.
|
||||
* @return This builder.
|
||||
*/
|
||||
@UnstableApi
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPreferredVideoLanguage(@Nullable String preferredVideoLanguage) {
|
||||
return preferredVideoLanguage == null
|
||||
? setPreferredVideoLanguages()
|
||||
: setPreferredVideoLanguages(preferredVideoLanguage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the preferred languages for video tracks.
|
||||
*
|
||||
* @param preferredVideoLanguages Preferred video languages as IETF BCP 47 conformant tags in
|
||||
* order of preference, or an empty array to express no language preference for video track
|
||||
* selection.
|
||||
* @return This builder.
|
||||
*/
|
||||
@UnstableApi
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPreferredVideoLanguages(String... preferredVideoLanguages) {
|
||||
this.preferredVideoLanguages = normalizeLanguageCodes(preferredVideoLanguages);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the preferred {@link C.RoleFlags} for video tracks.
|
||||
*
|
||||
@ -620,33 +661,29 @@ public class TrackSelectionParameters {
|
||||
// Text
|
||||
|
||||
/**
|
||||
* Sets the preferred language and role flags for text tracks based on the accessibility
|
||||
* settings of {@link CaptioningManager}.
|
||||
* Sets whether the preferred languages and the preferred role flags for text tracks should be
|
||||
* set according the {@link CaptioningManager} preferences, if enabled in the system settings
|
||||
* and no other explicit language or role flag preferences are specified.
|
||||
*
|
||||
* <p>Does nothing when the {@link CaptioningManager} is disabled.
|
||||
*
|
||||
* @param context A {@link Context}.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings() {
|
||||
usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager = true;
|
||||
preferredTextLanguages = ImmutableList.of();
|
||||
preferredTextRoleFlags = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings()}
|
||||
* instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(
|
||||
Context context) {
|
||||
if (Util.SDK_INT < 23 && Looper.myLooper() == null) {
|
||||
// Android platform bug (pre-Marshmallow) that causes RuntimeExceptions when
|
||||
// CaptioningService is instantiated from a non-Looper thread. See [internal: b/143779904].
|
||||
return this;
|
||||
}
|
||||
CaptioningManager captioningManager =
|
||||
(CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
|
||||
if (captioningManager == null || !captioningManager.isEnabled()) {
|
||||
return this;
|
||||
}
|
||||
preferredTextRoleFlags = C.ROLE_FLAG_CAPTION | C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND;
|
||||
Locale preferredLocale = captioningManager.getLocale();
|
||||
if (preferredLocale != null) {
|
||||
preferredTextLanguages = ImmutableList.of(Util.getLocaleLanguageTag(preferredLocale));
|
||||
}
|
||||
return this;
|
||||
return setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -673,6 +710,7 @@ public class TrackSelectionParameters {
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPreferredTextLanguages(String... preferredTextLanguages) {
|
||||
this.preferredTextLanguages = normalizeLanguageCodes(preferredTextLanguages);
|
||||
this.usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -685,6 +723,7 @@ public class TrackSelectionParameters {
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPreferredTextRoleFlags(@C.RoleFlags int preferredTextRoleFlags) {
|
||||
this.preferredTextRoleFlags = preferredTextRoleFlags;
|
||||
this.usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -1043,35 +1082,21 @@ public class TrackSelectionParameters {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance with default values, except those obtained from the {@link Context}.
|
||||
*
|
||||
* <p>If possible, use {@link #getDefaults(Context)} instead.
|
||||
*
|
||||
* <p>This instance will not have the following settings:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link Builder#setViewportSizeToPhysicalDisplaySize(Context, boolean) Viewport
|
||||
* constraints} configured for the primary display.
|
||||
* <li>{@link Builder#setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(Context)
|
||||
* Preferred text language and role flags} configured to the accessibility settings of
|
||||
* {@link CaptioningManager}.
|
||||
* </ul>
|
||||
*/
|
||||
@UnstableApi
|
||||
@SuppressWarnings("deprecation")
|
||||
public static final TrackSelectionParameters DEFAULT_WITHOUT_CONTEXT = new Builder().build();
|
||||
/** An instance with default parameters. */
|
||||
@UnstableApi public static final TrackSelectionParameters DEFAULT = new Builder().build();
|
||||
|
||||
/**
|
||||
* @deprecated This instance is not configured using {@link Context} constraints. Use {@link
|
||||
* #getDefaults(Context)} instead.
|
||||
* @deprecated Use {@link #DEFAULT} instead.
|
||||
*/
|
||||
@UnstableApi @Deprecated
|
||||
public static final TrackSelectionParameters DEFAULT = DEFAULT_WITHOUT_CONTEXT;
|
||||
public static final TrackSelectionParameters DEFAULT_WITHOUT_CONTEXT = DEFAULT;
|
||||
|
||||
/** Returns an instance configured with default values. */
|
||||
/**
|
||||
* @deprecated Use {@link #DEFAULT} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public static TrackSelectionParameters getDefaults(Context context) {
|
||||
return new Builder(context).build();
|
||||
return DEFAULT;
|
||||
}
|
||||
|
||||
// Video
|
||||
@ -1135,6 +1160,13 @@ public class TrackSelectionParameters {
|
||||
*/
|
||||
public final int viewportHeight;
|
||||
|
||||
/**
|
||||
* Whether the viewport size should be assumed to the physical display size if no other specific
|
||||
* viewport size constraint is specified. Constrains video track selections for adaptive content
|
||||
* so that only tracks suitable for the viewport are selected. The default value is {@code true}.
|
||||
*/
|
||||
public final boolean isViewportSizeLimitedByPhysicalDisplaySize;
|
||||
|
||||
/**
|
||||
* Whether the viewport orientation may change during playback. Constrains video track selections
|
||||
* for adaptive content so that only tracks suitable for the viewport are selected. The default
|
||||
@ -1148,6 +1180,11 @@ public class TrackSelectionParameters {
|
||||
*/
|
||||
public final ImmutableList<String> preferredVideoMimeTypes;
|
||||
|
||||
/**
|
||||
* The preferred languages for video tracks as IETF BCP 47 conformant tags in order of preference.
|
||||
*/
|
||||
@UnstableApi public final ImmutableList<String> preferredVideoLanguages;
|
||||
|
||||
/**
|
||||
* The preferred {@link C.RoleFlags} for video tracks. {@code 0} selects the default track if
|
||||
* there is one, or the first track if there's no default. The default value is {@code 0}.
|
||||
@ -1196,19 +1233,24 @@ public class TrackSelectionParameters {
|
||||
/**
|
||||
* The preferred languages for text tracks as IETF BCP 47 conformant tags in order of preference.
|
||||
* An empty list selects the default track if there is one, or no track otherwise. The default
|
||||
* value is an empty list, or the language of the accessibility {@link CaptioningManager} if
|
||||
* enabled.
|
||||
* value is an empty list.
|
||||
*/
|
||||
public final ImmutableList<String> preferredTextLanguages;
|
||||
|
||||
/**
|
||||
* The preferred {@link C.RoleFlags} for text tracks. {@code 0} selects the default track if there
|
||||
* is one, or no track otherwise. The default value is {@code 0}, or {@link C#ROLE_FLAG_SUBTITLE}
|
||||
* | {@link C#ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND} if the accessibility {@link CaptioningManager}
|
||||
* is enabled.
|
||||
* is one, or no track otherwise. The default value is {@code 0}.
|
||||
*/
|
||||
public final @C.RoleFlags int preferredTextRoleFlags;
|
||||
|
||||
/**
|
||||
* Whether the preferred languages and the preferred role flags for text tracks should be set
|
||||
* according the {@link CaptioningManager} preferences, if enabled in the system settings and no
|
||||
* other explicit language or role flag preferences are specified. The default value is {@code
|
||||
* true}.
|
||||
*/
|
||||
public final boolean usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager;
|
||||
|
||||
/**
|
||||
* Bitmask of selection flags that are ignored for text track selections. See {@link
|
||||
* C.SelectionFlags}. The default value is {@code 0} (i.e., no flags are ignored).
|
||||
@ -1265,8 +1307,11 @@ public class TrackSelectionParameters {
|
||||
this.minVideoBitrate = builder.minVideoBitrate;
|
||||
this.viewportWidth = builder.viewportWidth;
|
||||
this.viewportHeight = builder.viewportHeight;
|
||||
this.isViewportSizeLimitedByPhysicalDisplaySize =
|
||||
builder.isViewportSizeLimitedByPhysicalDisplaySize;
|
||||
this.viewportOrientationMayChange = builder.viewportOrientationMayChange;
|
||||
this.preferredVideoMimeTypes = builder.preferredVideoMimeTypes;
|
||||
this.preferredVideoLanguages = builder.preferredVideoLanguages;
|
||||
this.preferredVideoRoleFlags = builder.preferredVideoRoleFlags;
|
||||
// Audio
|
||||
this.preferredAudioLanguages = builder.preferredAudioLanguages;
|
||||
@ -1278,6 +1323,8 @@ public class TrackSelectionParameters {
|
||||
// Text
|
||||
this.preferredTextLanguages = builder.preferredTextLanguages;
|
||||
this.preferredTextRoleFlags = builder.preferredTextRoleFlags;
|
||||
this.usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager =
|
||||
builder.usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager;
|
||||
this.ignoredTextSelectionFlags = builder.ignoredTextSelectionFlags;
|
||||
this.selectUndeterminedTextLanguage = builder.selectUndeterminedTextLanguage;
|
||||
// Image
|
||||
@ -1316,7 +1363,10 @@ public class TrackSelectionParameters {
|
||||
&& viewportOrientationMayChange == other.viewportOrientationMayChange
|
||||
&& viewportWidth == other.viewportWidth
|
||||
&& viewportHeight == other.viewportHeight
|
||||
&& isViewportSizeLimitedByPhysicalDisplaySize
|
||||
== other.isViewportSizeLimitedByPhysicalDisplaySize
|
||||
&& preferredVideoMimeTypes.equals(other.preferredVideoMimeTypes)
|
||||
&& preferredVideoLanguages.equals(other.preferredVideoLanguages)
|
||||
&& preferredVideoRoleFlags == other.preferredVideoRoleFlags
|
||||
// Audio
|
||||
&& preferredAudioLanguages.equals(other.preferredAudioLanguages)
|
||||
@ -1328,6 +1378,8 @@ public class TrackSelectionParameters {
|
||||
// Text
|
||||
&& preferredTextLanguages.equals(other.preferredTextLanguages)
|
||||
&& preferredTextRoleFlags == other.preferredTextRoleFlags
|
||||
&& usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager
|
||||
== other.usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager
|
||||
&& ignoredTextSelectionFlags == other.ignoredTextSelectionFlags
|
||||
&& selectUndeterminedTextLanguage == other.selectUndeterminedTextLanguage
|
||||
// Image
|
||||
@ -1354,7 +1406,9 @@ public class TrackSelectionParameters {
|
||||
result = 31 * result + (viewportOrientationMayChange ? 1 : 0);
|
||||
result = 31 * result + viewportWidth;
|
||||
result = 31 * result + viewportHeight;
|
||||
result = 31 * result + (isViewportSizeLimitedByPhysicalDisplaySize ? 1 : 0);
|
||||
result = 31 * result + preferredVideoMimeTypes.hashCode();
|
||||
result = 31 * result + preferredVideoLanguages.hashCode();
|
||||
result = 31 * result + preferredVideoRoleFlags;
|
||||
// Audio
|
||||
result = 31 * result + preferredAudioLanguages.hashCode();
|
||||
@ -1366,6 +1420,7 @@ public class TrackSelectionParameters {
|
||||
// Text
|
||||
result = 31 * result + preferredTextLanguages.hashCode();
|
||||
result = 31 * result + preferredTextRoleFlags;
|
||||
result = 31 * result + (usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager ? 1 : 0);
|
||||
result = 31 * result + ignoredTextSelectionFlags;
|
||||
result = 31 * result + (selectUndeterminedTextLanguage ? 1 : 0);
|
||||
// Image
|
||||
@ -1410,6 +1465,12 @@ public class TrackSelectionParameters {
|
||||
private static final String FIELD_AUDIO_OFFLOAD_PREFERENCES = Util.intToStringMaxRadix(30);
|
||||
private static final String FIELD_IS_PREFER_IMAGE_OVER_VIDEO_ENABLED =
|
||||
Util.intToStringMaxRadix(31);
|
||||
private static final String FIELD_PREFERRED_VIDEO_LANGUAGES = Util.intToStringMaxRadix(32);
|
||||
private static final String FIELD_IS_VIEWPORT_SIZE_LIMITED_BY_PHYSICAL_DISPLAY_SIZE =
|
||||
Util.intToStringMaxRadix(33);
|
||||
private static final String
|
||||
FIELD_USE_PREFERRED_TEXT_LANGUAGES_AND_ROLE_FLAGS_FROM_CAPTIONING_MANAGER =
|
||||
Util.intToStringMaxRadix(34);
|
||||
|
||||
/**
|
||||
* Defines a minimum field ID value for subclasses to use when implementing {@link #toBundle()}
|
||||
@ -1436,9 +1497,14 @@ public class TrackSelectionParameters {
|
||||
bundle.putInt(FIELD_MIN_VIDEO_BITRATE, minVideoBitrate);
|
||||
bundle.putInt(FIELD_VIEWPORT_WIDTH, viewportWidth);
|
||||
bundle.putInt(FIELD_VIEWPORT_HEIGHT, viewportHeight);
|
||||
bundle.putBoolean(
|
||||
FIELD_IS_VIEWPORT_SIZE_LIMITED_BY_PHYSICAL_DISPLAY_SIZE,
|
||||
isViewportSizeLimitedByPhysicalDisplaySize);
|
||||
bundle.putBoolean(FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE, viewportOrientationMayChange);
|
||||
bundle.putStringArray(
|
||||
FIELD_PREFERRED_VIDEO_MIMETYPES, preferredVideoMimeTypes.toArray(new String[0]));
|
||||
bundle.putStringArray(
|
||||
FIELD_PREFERRED_VIDEO_LANGUAGES, preferredVideoLanguages.toArray(new String[0]));
|
||||
bundle.putInt(FIELD_PREFERRED_VIDEO_ROLE_FLAGS, preferredVideoRoleFlags);
|
||||
// Audio
|
||||
bundle.putStringArray(
|
||||
@ -1452,6 +1518,9 @@ public class TrackSelectionParameters {
|
||||
bundle.putStringArray(
|
||||
FIELD_PREFERRED_TEXT_LANGUAGES, preferredTextLanguages.toArray(new String[0]));
|
||||
bundle.putInt(FIELD_PREFERRED_TEXT_ROLE_FLAGS, preferredTextRoleFlags);
|
||||
bundle.putBoolean(
|
||||
FIELD_USE_PREFERRED_TEXT_LANGUAGES_AND_ROLE_FLAGS_FROM_CAPTIONING_MANAGER,
|
||||
usePreferredTextLanguagesAndRoleFlagsFromCaptioningManager);
|
||||
bundle.putInt(FIELD_IGNORED_TEXT_SELECTION_FLAGS, ignoredTextSelectionFlags);
|
||||
bundle.putBoolean(FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE, selectUndeterminedTextLanguage);
|
||||
bundle.putInt(FIELD_AUDIO_OFFLOAD_MODE_PREFERENCE, audioOffloadPreferences.audioOffloadMode);
|
||||
|
@ -13,13 +13,13 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.effect;
|
||||
package androidx.media3.common;
|
||||
|
||||
import androidx.media3.common.util.Size;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import java.util.List;
|
||||
|
||||
/** Settings for the {@link VideoCompositor}. */
|
||||
/** Settings for the {@code VideoCompositor}. */
|
||||
@UnstableApi
|
||||
public interface VideoCompositorSettings {
|
||||
// TODO: b/262694346 - Consider adding more features, like selecting a:
|
||||
@ -45,7 +45,7 @@ public interface VideoCompositorSettings {
|
||||
*/
|
||||
@Override
|
||||
public OverlaySettings getOverlaySettings(int inputId, long presentationTimeUs) {
|
||||
return new OverlaySettings.Builder().build();
|
||||
return new OverlaySettings() {};
|
||||
}
|
||||
};
|
||||
|
@ -84,8 +84,8 @@ public interface VideoFrameProcessor {
|
||||
* Input frames come from the {@linkplain #getInputSurface input surface} and don't need to be
|
||||
* {@linkplain #registerInputFrame registered} (unlike with {@link #INPUT_TYPE_SURFACE}).
|
||||
*
|
||||
* <p>Every frame must use the {@linkplain #registerInputStream(int, List, FrameInfo) input
|
||||
* stream's registered} frame info. Also sets the surface's {@linkplain
|
||||
* <p>Every frame must use the {@linkplain #registerInputStream input stream's registered} frame
|
||||
* format. Also sets the surface's {@linkplain
|
||||
* android.graphics.SurfaceTexture#setDefaultBufferSize(int, int) default buffer size}.
|
||||
*/
|
||||
int INPUT_TYPE_SURFACE_AUTOMATIC_FRAME_REGISTRATION = 4;
|
||||
@ -131,8 +131,8 @@ public interface VideoFrameProcessor {
|
||||
interface Listener {
|
||||
|
||||
/**
|
||||
* Called when the {@link VideoFrameProcessor} finishes {@linkplain #registerInputStream(int,
|
||||
* List, FrameInfo) registering an input stream}.
|
||||
* Called when the {@link VideoFrameProcessor} finishes {@linkplain #registerInputStream
|
||||
* registering an input stream}.
|
||||
*
|
||||
* <p>The {@link VideoFrameProcessor} is now ready to accept new input {@linkplain
|
||||
* VideoFrameProcessor#registerInputFrame frames}, {@linkplain
|
||||
@ -140,11 +140,11 @@ public interface VideoFrameProcessor {
|
||||
* VideoFrameProcessor#queueInputTexture(int, long) textures}.
|
||||
*
|
||||
* @param inputType The {@link InputType} of the new input stream.
|
||||
* @param format The {@link Format} of the new input stream.
|
||||
* @param effects The list of {@link Effect effects} to apply to the new input stream.
|
||||
* @param frameInfo The {@link FrameInfo} of the new input stream.
|
||||
*/
|
||||
default void onInputStreamRegistered(
|
||||
@InputType int inputType, List<Effect> effects, FrameInfo frameInfo) {}
|
||||
@InputType int inputType, Format format, List<Effect> effects) {}
|
||||
|
||||
/**
|
||||
* Called when the output size changes.
|
||||
@ -157,6 +157,14 @@ public interface VideoFrameProcessor {
|
||||
*/
|
||||
default void onOutputSizeChanged(int width, int height) {}
|
||||
|
||||
/**
|
||||
* Called when the output frame rate changes.
|
||||
*
|
||||
* @param frameRate The output frame rate in frames per second, or {@link Format#NO_VALUE} if
|
||||
* unknown.
|
||||
*/
|
||||
default void onOutputFrameRateChanged(float frameRate) {}
|
||||
|
||||
/**
|
||||
* Called when an output frame with the given {@code presentationTimeUs} becomes available for
|
||||
* rendering.
|
||||
@ -196,8 +204,8 @@ public interface VideoFrameProcessor {
|
||||
/**
|
||||
* Provides an input {@link Bitmap} to the {@link VideoFrameProcessor}.
|
||||
*
|
||||
* <p>Can be called many times after {@link #registerInputStream(int, List, FrameInfo) registering
|
||||
* the input stream} to put multiple frames in the same input stream.
|
||||
* <p>Can be called many times after {@link #registerInputStream registering the input stream} to
|
||||
* put multiple frames in the same input stream.
|
||||
*
|
||||
* @param inputBitmap The {@link Bitmap} queued to the {@code VideoFrameProcessor}.
|
||||
* @param timestampIterator A {@link TimestampIterator} generating the exact timestamps that the
|
||||
@ -271,14 +279,20 @@ public interface VideoFrameProcessor {
|
||||
* #queueInputTexture queued}.
|
||||
*
|
||||
* <p>This method blocks the calling thread until the previous calls to this method finish, that
|
||||
* is when {@link Listener#onInputStreamRegistered(int, List, FrameInfo)} is called after the
|
||||
* is when {@link Listener#onInputStreamRegistered(int, Format, List)} is called after the
|
||||
* underlying processing pipeline has been adapted to the registered input stream.
|
||||
*
|
||||
* @param inputType The {@link InputType} of the new input stream.
|
||||
* @param format The {@link Format} of the new input stream. The {@link Format#colorInfo}, the
|
||||
* {@link Format#width}, the {@link Format#height} and the {@link
|
||||
* Format#pixelWidthHeightRatio} must be set.
|
||||
* @param effects The list of {@link Effect effects} to apply to the new input stream.
|
||||
* @param frameInfo The {@link FrameInfo} of the new input stream.
|
||||
* @param offsetToAddUs The offset that must be added to the frame presentation timestamps, in
|
||||
* microseconds. This offset is not part of the input timestamps. It is added to the frame
|
||||
* timestamps before processing, and is retained in the output timestamps.
|
||||
*/
|
||||
void registerInputStream(@InputType int inputType, List<Effect> effects, FrameInfo frameInfo);
|
||||
void registerInputStream(
|
||||
@InputType int inputType, Format format, List<Effect> effects, long offsetToAddUs);
|
||||
|
||||
/**
|
||||
* Informs the {@code VideoFrameProcessor} that a frame will be queued to its {@linkplain
|
||||
@ -287,11 +301,10 @@ public interface VideoFrameProcessor {
|
||||
* <p>Must be called before rendering a frame to the input surface. The caller must not render
|
||||
* frames to the {@linkplain #getInputSurface input surface} when {@code false} is returned.
|
||||
*
|
||||
* @return Whether the input frame was successfully registered. If {@link
|
||||
* #registerInputStream(int, List, FrameInfo)} is called, this method returns {@code false}
|
||||
* until {@link Listener#onInputStreamRegistered(int, List, FrameInfo)} is called. Otherwise,
|
||||
* a return value of {@code false} indicates the {@code VideoFrameProcessor} is not ready to
|
||||
* accept input.
|
||||
* @return Whether the input frame was successfully registered. If {@link #registerInputStream} is
|
||||
* called, this method returns {@code false} until {@link
|
||||
* Listener#onInputStreamRegistered(int, Format, List)} is called. Otherwise, a return value
|
||||
* of {@code false} indicates the {@code VideoFrameProcessor} is not ready to accept input.
|
||||
* @throws UnsupportedOperationException If the {@code VideoFrameProcessor} does not accept
|
||||
* {@linkplain #INPUT_TYPE_SURFACE surface input}.
|
||||
* @throws IllegalStateException If called after {@link #signalEndOfInput()} or before {@link
|
||||
|
@ -35,6 +35,14 @@ public interface VideoGraph {
|
||||
*/
|
||||
default void onOutputSizeChanged(int width, int height) {}
|
||||
|
||||
/**
|
||||
* Called when the output frame rate changes.
|
||||
*
|
||||
* @param frameRate The output frame rate in frames per second, or {@link Format#NO_VALUE} if
|
||||
* unknown.
|
||||
*/
|
||||
default void onOutputFrameRateChanged(float frameRate) {}
|
||||
|
||||
/**
|
||||
* Called when an output frame with the given {@code framePresentationTimeUs} becomes available
|
||||
* for rendering.
|
||||
|
@ -0,0 +1,344 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.common.audio;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
|
||||
import android.media.AudioFocusRequest;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.media3.common.AudioAttributes;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Compatibility version of an {@link AudioFocusRequest} with fallbacks for older Android versions.
|
||||
*/
|
||||
@UnstableApi
|
||||
public final class AudioFocusRequestCompat {
|
||||
|
||||
private final @AudioManagerCompat.AudioFocusGain int focusGain;
|
||||
private final AudioManager.OnAudioFocusChangeListener onAudioFocusChangeListener;
|
||||
private final Handler focusChangeHandler;
|
||||
private final AudioAttributes audioAttributes;
|
||||
private final boolean pauseOnDuck;
|
||||
|
||||
@Nullable private final Object frameworkAudioFocusRequest;
|
||||
|
||||
/* package */ AudioFocusRequestCompat(
|
||||
int focusGain,
|
||||
AudioManager.OnAudioFocusChangeListener onAudioFocusChangeListener,
|
||||
Handler focusChangeHandler,
|
||||
AudioAttributes audioFocusRequestCompat,
|
||||
boolean pauseOnDuck) {
|
||||
this.focusGain = focusGain;
|
||||
this.focusChangeHandler = focusChangeHandler;
|
||||
this.audioAttributes = audioFocusRequestCompat;
|
||||
this.pauseOnDuck = pauseOnDuck;
|
||||
|
||||
if (Util.SDK_INT < 26) {
|
||||
this.onAudioFocusChangeListener =
|
||||
new OnAudioFocusChangeListenerHandlerCompat(
|
||||
onAudioFocusChangeListener, focusChangeHandler);
|
||||
} else {
|
||||
this.onAudioFocusChangeListener = onAudioFocusChangeListener;
|
||||
}
|
||||
|
||||
if (Util.SDK_INT >= 26) {
|
||||
this.frameworkAudioFocusRequest =
|
||||
new AudioFocusRequest.Builder(focusGain)
|
||||
.setAudioAttributes(audioAttributes.getAudioAttributesV21().audioAttributes)
|
||||
.setWillPauseWhenDucked(pauseOnDuck)
|
||||
.setOnAudioFocusChangeListener(onAudioFocusChangeListener, focusChangeHandler)
|
||||
.build();
|
||||
} else {
|
||||
this.frameworkAudioFocusRequest = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of {@link AudioManagerCompat.AudioFocusGain} configured for this {@code
|
||||
* AudioFocusRequestCompat}.
|
||||
*/
|
||||
public @AudioManagerCompat.AudioFocusGain int getFocusGain() {
|
||||
return focusGain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link AudioAttributes} set for this {@code AudioFocusRequestCompat}, or the
|
||||
* default attributes if none were set.
|
||||
*/
|
||||
public AudioAttributes getAudioAttributes() {
|
||||
return audioAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the application that would use this {@code AudioFocusRequestCompat} would pause
|
||||
* when it is requested to duck. This value is only applicable on {@link
|
||||
* android.os.Build.VERSION_CODES#O} and later.
|
||||
*/
|
||||
public boolean willPauseWhenDucked() {
|
||||
return pauseOnDuck;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link AudioManager.OnAudioFocusChangeListener} set for this {@code
|
||||
* AudioFocusRequestCompat}.
|
||||
*
|
||||
* @return The {@link AudioManager.OnAudioFocusChangeListener} that was set.
|
||||
*/
|
||||
public AudioManager.OnAudioFocusChangeListener getOnAudioFocusChangeListener() {
|
||||
return onAudioFocusChangeListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Handler} to be used for the {@link AudioManager.OnAudioFocusChangeListener}.
|
||||
*/
|
||||
public Handler getFocusChangeHandler() {
|
||||
return focusChangeHandler;
|
||||
}
|
||||
|
||||
/** Returns new {@link Builder} with all values of this instance pre-populated. */
|
||||
public Builder buildUpon() {
|
||||
return new Builder(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof AudioFocusRequestCompat)) {
|
||||
return false;
|
||||
}
|
||||
AudioFocusRequestCompat that = (AudioFocusRequestCompat) o;
|
||||
return focusGain == that.focusGain
|
||||
&& pauseOnDuck == that.pauseOnDuck
|
||||
&& Objects.equals(onAudioFocusChangeListener, that.onAudioFocusChangeListener)
|
||||
&& Objects.equals(focusChangeHandler, that.focusChangeHandler)
|
||||
&& Objects.equals(audioAttributes, that.audioAttributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(
|
||||
focusGain, onAudioFocusChangeListener, focusChangeHandler, audioAttributes, pauseOnDuck);
|
||||
}
|
||||
|
||||
@RequiresApi(26)
|
||||
/* package */ AudioFocusRequest getAudioFocusRequest() {
|
||||
return (AudioFocusRequest) checkNotNull(frameworkAudioFocusRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class for {@link AudioFocusRequestCompat} objects.
|
||||
*
|
||||
* <p>The default values are:
|
||||
*
|
||||
* <ul>
|
||||
* <li>focus listener and handler: none
|
||||
* <li>audio attributes: {@link AudioAttributes#DEFAULT}
|
||||
* <li>pauses on duck: false
|
||||
* <li>supports delayed focus grant: false
|
||||
* </ul>
|
||||
*
|
||||
* <p>In contrast to a {@link AudioFocusRequest}, attempting to {@link #build()} an {@link
|
||||
* AudioFocusRequestCompat} without an {@link AudioManager.OnAudioFocusChangeListener} will throw
|
||||
* an {@link IllegalArgumentException}, because the listener is required for all API levels up to
|
||||
* API 26.
|
||||
*/
|
||||
public static final class Builder {
|
||||
private @AudioManagerCompat.AudioFocusGain int focusGain;
|
||||
@Nullable private AudioManager.OnAudioFocusChangeListener onAudioFocusChangeListener;
|
||||
@Nullable private Handler focusChangeHandler;
|
||||
private AudioAttributes audioAttributes;
|
||||
private boolean pauseOnDuck;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code Builder}, and specifies how audio focus will be requested.
|
||||
*
|
||||
* <p>By default there is no focus change listener, delayed focus is not supported, ducking is
|
||||
* suitable for the application, and the {@link AudioAttributes} are set to {@link
|
||||
* AudioAttributes#DEFAULT}.
|
||||
*
|
||||
* @param focusGain The type of {@link AudioManagerCompat.AudioFocusGain} that will be
|
||||
* requested.
|
||||
*/
|
||||
public Builder(@AudioManagerCompat.AudioFocusGain int focusGain) {
|
||||
this.audioAttributes = AudioAttributes.DEFAULT;
|
||||
this.focusGain = focusGain;
|
||||
}
|
||||
|
||||
private Builder(AudioFocusRequestCompat other) {
|
||||
focusGain = other.getFocusGain();
|
||||
onAudioFocusChangeListener = other.getOnAudioFocusChangeListener();
|
||||
focusChangeHandler = other.getFocusChangeHandler();
|
||||
audioAttributes = other.getAudioAttributes();
|
||||
pauseOnDuck = other.willPauseWhenDucked();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the type of {@link AudioManagerCompat.AudioFocusGain} that will be requested.
|
||||
*
|
||||
* @param focusGain The type of {@link AudioManagerCompat.AudioFocusGain} that will be
|
||||
* requested.
|
||||
* @return This {@code Builder} instance.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setFocusGain(@AudioManagerCompat.AudioFocusGain int focusGain) {
|
||||
checkArgument(isValidFocusGain(focusGain));
|
||||
this.focusGain = focusGain;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener called when audio focus changes after being requested with {@link
|
||||
* AudioManagerCompat#requestAudioFocus(AudioManager, AudioFocusRequestCompat)}, and until being
|
||||
* abandoned with {@link AudioManagerCompat#abandonAudioFocusRequest(AudioManager,
|
||||
* AudioFocusRequestCompat)}. Note that only focus changes (gains and losses) affecting the
|
||||
* focus owner are reported, not gains and losses of other focus requesters in the system. <br>
|
||||
* Notifications are delivered on the main thread.
|
||||
*
|
||||
* @param listener The {@link AudioManager.OnAudioFocusChangeListener} receiving the focus
|
||||
* change notifications.
|
||||
* @return This {@code Builder} instance.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setOnAudioFocusChangeListener(AudioManager.OnAudioFocusChangeListener listener) {
|
||||
return setOnAudioFocusChangeListener(listener, new Handler(Looper.getMainLooper()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener called when audio focus changes after being requested with {@link
|
||||
* AudioManagerCompat#requestAudioFocus(AudioManager, AudioFocusRequestCompat)}, and until being
|
||||
* abandoned with {@link AudioManagerCompat#abandonAudioFocusRequest(AudioManager,
|
||||
* AudioFocusRequestCompat)}. Note that only focus changes (gains and losses) affecting the
|
||||
* focus owner are reported, not gains and losses of other focus requesters in the system.
|
||||
*
|
||||
* @param listener The {@link AudioManager.OnAudioFocusChangeListener} receiving the focus
|
||||
* change notifications.
|
||||
* @param handler The {@link Handler} for the thread on which to execute the notifications.
|
||||
* @return This {@code Builder} instance.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setOnAudioFocusChangeListener(
|
||||
AudioManager.OnAudioFocusChangeListener listener, Handler handler) {
|
||||
checkNotNull(listener);
|
||||
checkNotNull(handler);
|
||||
onAudioFocusChangeListener = listener;
|
||||
focusChangeHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AudioAttributes} to be associated with the focus request, and which describe
|
||||
* the use case for which focus is requested. As the focus requests typically precede audio
|
||||
* playback, this information is used on certain platforms to declare the subsequent playback
|
||||
* use case. It is therefore good practice to use in this method the same {@code
|
||||
* AudioAttributes} as used for playback, see for example {@code
|
||||
* ExoPlayer.Builder.setAudioAttributes()}.
|
||||
*
|
||||
* @param attributes The {@link AudioAttributes} for the focus request.
|
||||
* @return This {@code Builder} instance.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setAudioAttributes(AudioAttributes attributes) {
|
||||
checkNotNull(attributes);
|
||||
audioAttributes = attributes;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares the intended behavior of the application with regards to audio ducking. See more
|
||||
* details in the {@link AudioFocusRequest} class documentation. Setting {@code pauseOnDuck} to
|
||||
* true will only have an effect on {@link android.os.Build.VERSION_CODES#O} and later.
|
||||
*
|
||||
* @param pauseOnDuck Use {@code true} if the application intends to pause audio playback when
|
||||
* losing focus with {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK}.
|
||||
* @return This {@code Builder} instance.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setWillPauseWhenDucked(boolean pauseOnDuck) {
|
||||
this.pauseOnDuck = pauseOnDuck;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@code AudioFocusRequestCompat} instance combining all the information gathered
|
||||
* by this builder's configuration methods.
|
||||
*
|
||||
* @return The {@code AudioFocusRequestCompat}.
|
||||
*/
|
||||
public AudioFocusRequestCompat build() {
|
||||
if (onAudioFocusChangeListener == null) {
|
||||
throw new IllegalStateException(
|
||||
"Can't build an AudioFocusRequestCompat instance without a listener");
|
||||
}
|
||||
return new AudioFocusRequestCompat(
|
||||
focusGain,
|
||||
onAudioFocusChangeListener,
|
||||
checkNotNull(focusChangeHandler),
|
||||
audioAttributes,
|
||||
pauseOnDuck);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a focus gain constant is a valid value for an audio focus request.
|
||||
*
|
||||
* @param focusGain value to check
|
||||
* @return true if focusGain is a valid value for an audio focus request.
|
||||
*/
|
||||
private static boolean isValidFocusGain(@AudioManagerCompat.AudioFocusGain int focusGain) {
|
||||
switch (focusGain) {
|
||||
case AudioManagerCompat.AUDIOFOCUS_GAIN:
|
||||
case AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT:
|
||||
case AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
|
||||
case AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to allow {@link AudioManager.OnAudioFocusChangeListener#onAudioFocusChange(int)} calls on
|
||||
* a specific thread prior to API 26.
|
||||
*/
|
||||
private static class OnAudioFocusChangeListenerHandlerCompat
|
||||
implements AudioManager.OnAudioFocusChangeListener {
|
||||
|
||||
private final Handler handler;
|
||||
private final AudioManager.OnAudioFocusChangeListener listener;
|
||||
|
||||
/* package */ OnAudioFocusChangeListenerHandlerCompat(
|
||||
AudioManager.OnAudioFocusChangeListener listener, Handler handler) {
|
||||
this.listener = listener;
|
||||
this.handler = Util.createHandler(handler.getLooper(), /* callback= */ null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioFocusChange(int focusChange) {
|
||||
Util.postOrRun(handler, () -> listener.onAudioFocusChange(focusChange));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,247 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.common.audio;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.BackgroundExecutor;
|
||||
import androidx.media3.common.util.ConditionVariable;
|
||||
import androidx.media3.common.util.Log;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/** Compatibility layer for {@link AudioManager} with fallbacks for older Android versions. */
|
||||
@UnstableApi
|
||||
public final class AudioManagerCompat {
|
||||
|
||||
private static final String TAG = "AudioManagerCompat";
|
||||
|
||||
/**
|
||||
* Audio focus gain types. One of {@link #AUDIOFOCUS_NONE}, {@link #AUDIOFOCUS_GAIN}, {@link
|
||||
* #AUDIOFOCUS_GAIN_TRANSIENT}, {@link #AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK} or {@link
|
||||
* #AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(TYPE_USE)
|
||||
@IntDef({
|
||||
AUDIOFOCUS_NONE,
|
||||
AUDIOFOCUS_GAIN,
|
||||
AUDIOFOCUS_GAIN_TRANSIENT,
|
||||
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,
|
||||
AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
|
||||
})
|
||||
public @interface AudioFocusGain {}
|
||||
|
||||
/** Used to indicate no audio focus has been gained or lost, or requested. */
|
||||
@SuppressWarnings("InlinedApi")
|
||||
public static final int AUDIOFOCUS_NONE = AudioManager.AUDIOFOCUS_NONE;
|
||||
|
||||
/** Used to indicate a gain of audio focus, or a request of audio focus, of unknown duration. */
|
||||
public static final int AUDIOFOCUS_GAIN = AudioManager.AUDIOFOCUS_GAIN;
|
||||
|
||||
/**
|
||||
* Used to indicate a temporary gain or request of audio focus, anticipated to last a short amount
|
||||
* of time. Examples of temporary changes are the playback of driving directions, or an event
|
||||
* notification.
|
||||
*/
|
||||
public static final int AUDIOFOCUS_GAIN_TRANSIENT = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT;
|
||||
|
||||
/**
|
||||
* Used to indicate a temporary request of audio focus, anticipated to last a short amount of
|
||||
* time, and where it is acceptable for other audio applications to keep playing after having
|
||||
* lowered their output level (also referred to as "ducking"). Examples of temporary changes are
|
||||
* the playback of driving directions where playback of music in the background is acceptable.
|
||||
*/
|
||||
public static final int AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK =
|
||||
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;
|
||||
|
||||
/**
|
||||
* Used to indicate a temporary request of audio focus, anticipated to last a short amount of
|
||||
* time, during which no other applications, or system components, should play anything. Examples
|
||||
* of exclusive and transient audio focus requests are voice memo recording and speech
|
||||
* recognition, during which the system shouldn't play any notifications, and media playback
|
||||
* should have paused.
|
||||
*/
|
||||
public static final int AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE =
|
||||
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE;
|
||||
|
||||
@SuppressWarnings("NonFinalStaticField") // Lazily initialized under class lock
|
||||
@Nullable
|
||||
private static AudioManager audioManager;
|
||||
|
||||
@SuppressWarnings("NonFinalStaticField") // Lazily initialized under class lock
|
||||
private static @MonotonicNonNull Context applicationContext;
|
||||
|
||||
/**
|
||||
* Returns the {@link AudioManager}.
|
||||
*
|
||||
* <p>This method avoids potential threading issues where AudioManager keeps access to the thread
|
||||
* it was created on until after this thread is stopped.
|
||||
*
|
||||
* <p>It is recommended to use this method from a background thread.
|
||||
*
|
||||
* @param context A {@link Context}.
|
||||
* @return The {@link AudioManager}.
|
||||
*/
|
||||
public static synchronized AudioManager getAudioManager(Context context) {
|
||||
Context applicationContext = context.getApplicationContext();
|
||||
if (AudioManagerCompat.applicationContext != applicationContext) {
|
||||
// Reset cached instance if the application context changed. This should only happen in tests.
|
||||
audioManager = null;
|
||||
}
|
||||
if (audioManager != null) {
|
||||
return audioManager;
|
||||
}
|
||||
@Nullable Looper myLooper = Looper.myLooper();
|
||||
if (myLooper == null || myLooper == Looper.getMainLooper()) {
|
||||
// The AudioManager will assume the main looper as default callback anyway, so create the
|
||||
// instance here without using BackgroundExecutor.
|
||||
audioManager = (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
return checkNotNull(audioManager);
|
||||
}
|
||||
// Create the audio manager on the BackgroundExecutor to avoid running the potentially blocking
|
||||
// command on the main thread but still use a thread that is guaranteed to exist for the
|
||||
// lifetime of the app.
|
||||
ConditionVariable audioManagerSetCondition = new ConditionVariable();
|
||||
BackgroundExecutor.get()
|
||||
.execute(
|
||||
() -> {
|
||||
audioManager =
|
||||
(AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
audioManagerSetCondition.open();
|
||||
});
|
||||
audioManagerSetCondition.blockUninterruptible();
|
||||
return checkNotNull(audioManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests audio focus. See the {@link AudioFocusRequestCompat} for information about the options
|
||||
* available to configure your request, and notification of focus gain and loss.
|
||||
*
|
||||
* @param audioManager The {@link AudioManager}.
|
||||
* @param focusRequest An {@link AudioFocusRequestCompat} instance used to configure how focus is
|
||||
* requested.
|
||||
* @return {@link AudioManager#AUDIOFOCUS_REQUEST_FAILED} or {@link
|
||||
* AudioManager#AUDIOFOCUS_REQUEST_GRANTED}.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public static int requestAudioFocus(
|
||||
AudioManager audioManager, AudioFocusRequestCompat focusRequest) {
|
||||
if (Util.SDK_INT >= 26) {
|
||||
return audioManager.requestAudioFocus(focusRequest.getAudioFocusRequest());
|
||||
} else {
|
||||
return audioManager.requestAudioFocus(
|
||||
focusRequest.getOnAudioFocusChangeListener(),
|
||||
focusRequest.getAudioAttributes().getStreamType(),
|
||||
focusRequest.getFocusGain());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abandon audio focus. Causes the previous focus owner, if any, to receive focus.
|
||||
*
|
||||
* @param audioManager The {@link AudioManager}.
|
||||
* @param focusRequest The {@link AudioFocusRequestCompat} that was used when requesting focus
|
||||
* with {@link #requestAudioFocus(AudioManager, AudioFocusRequestCompat)}.
|
||||
* @return {@link AudioManager#AUDIOFOCUS_REQUEST_FAILED} or {@link
|
||||
* AudioManager#AUDIOFOCUS_REQUEST_GRANTED}
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public static int abandonAudioFocusRequest(
|
||||
AudioManager audioManager, AudioFocusRequestCompat focusRequest) {
|
||||
if (Util.SDK_INT >= 26) {
|
||||
return audioManager.abandonAudioFocusRequest(focusRequest.getAudioFocusRequest());
|
||||
} else {
|
||||
return audioManager.abandonAudioFocus(focusRequest.getOnAudioFocusChangeListener());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum volume index for a particular stream.
|
||||
*
|
||||
* @param audioManager The {@link AudioManager}.
|
||||
* @param streamType The {@link C.StreamType} whose maximum volume index is returned.
|
||||
* @return The maximum valid volume index for the stream.
|
||||
*/
|
||||
@IntRange(from = 0)
|
||||
public static int getStreamMaxVolume(AudioManager audioManager, @C.StreamType int streamType) {
|
||||
return audioManager.getStreamMaxVolume(streamType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum volume index for a particular stream.
|
||||
*
|
||||
* @param audioManager The {@link AudioManager}.
|
||||
* @param streamType The {@link C.StreamType} whose minimum volume index is returned.
|
||||
* @return The minimum valid volume index for the stream.
|
||||
*/
|
||||
@IntRange(from = 0)
|
||||
public static int getStreamMinVolume(AudioManager audioManager, @C.StreamType int streamType) {
|
||||
return Util.SDK_INT >= 28 ? audioManager.getStreamMinVolume(streamType) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current volume for a particular stream.
|
||||
*
|
||||
* @param audioManager The {@link AudioManager}.
|
||||
* @param streamType The {@link C.StreamType} whose volume is returned.
|
||||
* @return The current volume of the stream.
|
||||
*/
|
||||
public static int getStreamVolume(AudioManager audioManager, @C.StreamType int streamType) {
|
||||
// AudioManager#getStreamVolume(int) throws an exception on some devices. See
|
||||
// https://github.com/google/ExoPlayer/issues/8191.
|
||||
try {
|
||||
return audioManager.getStreamVolume(streamType);
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(
|
||||
"AudioManagerCompat",
|
||||
"Could not retrieve stream volume for stream type " + streamType,
|
||||
e);
|
||||
return audioManager.getStreamMaxVolume(streamType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given stream is muted.
|
||||
*
|
||||
* @param audioManager The {@link AudioManager}.
|
||||
* @param streamType The {@link C.StreamType} to check.
|
||||
* @return Whether the stream is muted.
|
||||
*/
|
||||
public static boolean isStreamMute(AudioManager audioManager, @C.StreamType int streamType) {
|
||||
if (Util.SDK_INT >= 23) {
|
||||
return audioManager.isStreamMute(streamType);
|
||||
} else {
|
||||
return getStreamVolume(audioManager, streamType) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
private AudioManagerCompat() {}
|
||||
}
|
@ -20,9 +20,9 @@ import androidx.media3.common.C;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.base.Objects;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Interface for audio processors, which take audio data as input and transform it, potentially
|
||||
@ -107,7 +107,7 @@ public interface AudioProcessor {
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(sampleRate, channelCount, encoding);
|
||||
return Objects.hash(sampleRate, channelCount, encoding);
|
||||
}
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user